tina4ruby 3.11.13 → 3.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/request.rb
CHANGED
|
@@ -1,255 +1,268 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "uri"
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
# A Hash subclass that supports indifferent access (both string and symbol keys).
|
|
7
|
-
# Used by Request#params so that params[:id] and params["id"] both work.
|
|
8
|
-
class IndifferentHash < Hash
|
|
9
|
-
def [](key)
|
|
10
|
-
super(convert_key(key))
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def []=(key, value)
|
|
14
|
-
super(convert_key(key), value)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def fetch(key, *args, &block)
|
|
18
|
-
super(convert_key(key), *args, &block)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def key?(key)
|
|
22
|
-
super(convert_key(key))
|
|
23
|
-
end
|
|
24
|
-
alias has_key? key?
|
|
25
|
-
alias include? key?
|
|
26
|
-
|
|
27
|
-
def delete(key, &block)
|
|
28
|
-
super(convert_key(key), &block)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def convert_key(key)
|
|
34
|
-
key.is_a?(Symbol) ? key.to_s : key
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
class Request
|
|
39
|
-
attr_reader :env, :method, :path, :query_string, :content_type,
|
|
40
|
-
:path_params, :ip
|
|
41
|
-
attr_accessor :user
|
|
42
|
-
|
|
43
|
-
# Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
|
|
44
|
-
TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
|
|
45
|
-
|
|
46
|
-
class PayloadTooLarge < StandardError; end
|
|
47
|
-
|
|
48
|
-
def initialize(env, path_params = {})
|
|
49
|
-
@env = env
|
|
50
|
-
@method = env["REQUEST_METHOD"]
|
|
51
|
-
@path = env["PATH_INFO"] || "/"
|
|
52
|
-
@query_string = env["QUERY_STRING"] || ""
|
|
53
|
-
@content_type = env["CONTENT_TYPE"] || ""
|
|
54
|
-
@path_params = path_params
|
|
55
|
-
|
|
56
|
-
# Check upload size limit
|
|
57
|
-
content_length = (env["CONTENT_LENGTH"] || 0).to_i
|
|
58
|
-
if content_length > TINA4_MAX_UPLOAD_SIZE
|
|
59
|
-
raise PayloadTooLarge,
|
|
60
|
-
"Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Client IP with X-Forwarded-For support
|
|
64
|
-
@ip = extract_client_ip
|
|
65
|
-
|
|
66
|
-
# Lazy-initialized fields (nil = not yet computed)
|
|
67
|
-
@headers = nil
|
|
68
|
-
@cookies = nil
|
|
69
|
-
@session = nil
|
|
70
|
-
@body_raw = nil
|
|
71
|
-
@params = nil
|
|
72
|
-
@files = nil
|
|
73
|
-
@json_body = nil
|
|
74
|
-
@query_hash = nil
|
|
75
|
-
@body_parsed = nil
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Full URL reconstruction
|
|
79
|
-
def url
|
|
80
|
-
scheme = env["rack.url_scheme"] || "http"
|
|
81
|
-
host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
|
|
82
|
-
port = env["SERVER_PORT"]
|
|
83
|
-
url_str = "#{scheme}://#{host}"
|
|
84
|
-
url_str += ":#{port}" if port && port != "80" && port != "443"
|
|
85
|
-
url_str += @path
|
|
86
|
-
url_str += "?#{@query_string}" unless @query_string.empty?
|
|
87
|
-
url_str
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Lazy accessors
|
|
91
|
-
def headers
|
|
92
|
-
@headers ||= extract_headers
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def cookies
|
|
96
|
-
@cookies ||= parse_cookies
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def session
|
|
100
|
-
@session ||= Tina4::Session.new(@env)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Raw body string
|
|
104
|
-
def body
|
|
105
|
-
@body_raw ||= read_body
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Parsed body (JSON or form data)
|
|
109
|
-
def body_parsed
|
|
110
|
-
@body_parsed ||= parse_body
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Parsed query string as hash
|
|
114
|
-
def query
|
|
115
|
-
@query_hash ||= parse_query_to_hash(@query_string)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def files
|
|
119
|
-
@files ||= extract_files
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Merged params: query + body + path_params (path_params highest priority)
|
|
123
|
-
# Supports both string and symbol key access (indifferent access).
|
|
124
|
-
def params
|
|
125
|
-
@params ||= build_params
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Look up a param by symbol or string key (indifferent access shortcut).
|
|
129
|
-
def param(key, default = nil)
|
|
130
|
-
params[key.to_s] || params[key.to_sym] || default
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def [](key)
|
|
134
|
-
params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def header(name)
|
|
138
|
-
headers[name.to_s.downcase.gsub("-", "_")]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def json_body
|
|
142
|
-
@json_body ||= begin
|
|
143
|
-
JSON.parse(body)
|
|
144
|
-
rescue JSON::ParserError, TypeError
|
|
145
|
-
{}
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def bearer_token
|
|
150
|
-
auth = header("authorization") || ""
|
|
151
|
-
auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
def extract_client_ip
|
|
157
|
-
# Check X-Forwarded-For first (proxy/load balancer)
|
|
158
|
-
forwarded = @env["HTTP_X_FORWARDED_FOR"]
|
|
159
|
-
if forwarded && !forwarded.empty?
|
|
160
|
-
# Take the first (original client) IP
|
|
161
|
-
forwarded.split(",").first.strip
|
|
162
|
-
else
|
|
163
|
-
@env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def extract_headers
|
|
168
|
-
h = {}
|
|
169
|
-
@env.each do |key, value|
|
|
170
|
-
if key.start_with?("HTTP_")
|
|
171
|
-
h[key[5..-1].downcase] = value
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
h
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def parse_cookies
|
|
178
|
-
cookie_str = @env["HTTP_COOKIE"]
|
|
179
|
-
return {} unless cookie_str && !cookie_str.empty?
|
|
180
|
-
|
|
181
|
-
result = {}
|
|
182
|
-
cookie_str.split(";").each do |pair|
|
|
183
|
-
key, value = pair.strip.split("=", 2)
|
|
184
|
-
result[key] = value if key
|
|
185
|
-
end
|
|
186
|
-
result
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def read_body
|
|
190
|
-
input = @env["rack.input"]
|
|
191
|
-
return "" unless input
|
|
192
|
-
input.rewind if input.respond_to?(:rewind)
|
|
193
|
-
data = input.read || ""
|
|
194
|
-
input.rewind if input.respond_to?(:rewind)
|
|
195
|
-
data
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def parse_body
|
|
199
|
-
if @content_type.include?("application/json")
|
|
200
|
-
json_body
|
|
201
|
-
elsif @content_type.include?("application/x-www-form-urlencoded")
|
|
202
|
-
parse_query_to_hash(body)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Tina4
|
|
6
|
+
# A Hash subclass that supports indifferent access (both string and symbol keys).
|
|
7
|
+
# Used by Request#params so that params[:id] and params["id"] both work.
|
|
8
|
+
class IndifferentHash < Hash
|
|
9
|
+
def [](key)
|
|
10
|
+
super(convert_key(key))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def []=(key, value)
|
|
14
|
+
super(convert_key(key), value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(key, *args, &block)
|
|
18
|
+
super(convert_key(key), *args, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def key?(key)
|
|
22
|
+
super(convert_key(key))
|
|
23
|
+
end
|
|
24
|
+
alias has_key? key?
|
|
25
|
+
alias include? key?
|
|
26
|
+
|
|
27
|
+
def delete(key, &block)
|
|
28
|
+
super(convert_key(key), &block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def convert_key(key)
|
|
34
|
+
key.is_a?(Symbol) ? key.to_s : key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Request
|
|
39
|
+
attr_reader :env, :method, :path, :query_string, :content_type,
|
|
40
|
+
:path_params, :ip
|
|
41
|
+
attr_accessor :user
|
|
42
|
+
|
|
43
|
+
# Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
|
|
44
|
+
TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
|
|
45
|
+
|
|
46
|
+
class PayloadTooLarge < StandardError; end
|
|
47
|
+
|
|
48
|
+
def initialize(env, path_params = {})
|
|
49
|
+
@env = env
|
|
50
|
+
@method = env["REQUEST_METHOD"]
|
|
51
|
+
@path = env["PATH_INFO"] || "/"
|
|
52
|
+
@query_string = env["QUERY_STRING"] || ""
|
|
53
|
+
@content_type = env["CONTENT_TYPE"] || ""
|
|
54
|
+
@path_params = path_params
|
|
55
|
+
|
|
56
|
+
# Check upload size limit
|
|
57
|
+
content_length = (env["CONTENT_LENGTH"] || 0).to_i
|
|
58
|
+
if content_length > TINA4_MAX_UPLOAD_SIZE
|
|
59
|
+
raise PayloadTooLarge,
|
|
60
|
+
"Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Client IP with X-Forwarded-For support
|
|
64
|
+
@ip = extract_client_ip
|
|
65
|
+
|
|
66
|
+
# Lazy-initialized fields (nil = not yet computed)
|
|
67
|
+
@headers = nil
|
|
68
|
+
@cookies = nil
|
|
69
|
+
@session = nil
|
|
70
|
+
@body_raw = nil
|
|
71
|
+
@params = nil
|
|
72
|
+
@files = nil
|
|
73
|
+
@json_body = nil
|
|
74
|
+
@query_hash = nil
|
|
75
|
+
@body_parsed = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Full URL reconstruction
|
|
79
|
+
def url
|
|
80
|
+
scheme = env["rack.url_scheme"] || "http"
|
|
81
|
+
host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
|
|
82
|
+
port = env["SERVER_PORT"]
|
|
83
|
+
url_str = "#{scheme}://#{host}"
|
|
84
|
+
url_str += ":#{port}" if port && port != "80" && port != "443"
|
|
85
|
+
url_str += @path
|
|
86
|
+
url_str += "?#{@query_string}" unless @query_string.empty?
|
|
87
|
+
url_str
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Lazy accessors
|
|
91
|
+
def headers
|
|
92
|
+
@headers ||= extract_headers
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cookies
|
|
96
|
+
@cookies ||= parse_cookies
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def session
|
|
100
|
+
@session ||= Tina4::Session.new(@env)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Raw body string
|
|
104
|
+
def body
|
|
105
|
+
@body_raw ||= read_body
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Parsed body (JSON or form data)
|
|
109
|
+
def body_parsed
|
|
110
|
+
@body_parsed ||= parse_body
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Parsed query string as hash
|
|
114
|
+
def query
|
|
115
|
+
@query_hash ||= parse_query_to_hash(@query_string)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def files
|
|
119
|
+
@files ||= extract_files
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Merged params: query + body + path_params (path_params highest priority)
|
|
123
|
+
# Supports both string and symbol key access (indifferent access).
|
|
124
|
+
def params
|
|
125
|
+
@params ||= build_params
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Look up a param by symbol or string key (indifferent access shortcut).
|
|
129
|
+
def param(key, default = nil)
|
|
130
|
+
params[key.to_s] || params[key.to_sym] || default
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def [](key)
|
|
134
|
+
params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def header(name)
|
|
138
|
+
headers[name.to_s.downcase.gsub("-", "_")]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def json_body
|
|
142
|
+
@json_body ||= begin
|
|
143
|
+
JSON.parse(body)
|
|
144
|
+
rescue JSON::ParserError, TypeError
|
|
145
|
+
{}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def bearer_token
|
|
150
|
+
auth = header("authorization") || ""
|
|
151
|
+
auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def extract_client_ip
|
|
157
|
+
# Check X-Forwarded-For first (proxy/load balancer)
|
|
158
|
+
forwarded = @env["HTTP_X_FORWARDED_FOR"]
|
|
159
|
+
if forwarded && !forwarded.empty?
|
|
160
|
+
# Take the first (original client) IP
|
|
161
|
+
forwarded.split(",").first.strip
|
|
162
|
+
else
|
|
163
|
+
@env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_headers
|
|
168
|
+
h = {}
|
|
169
|
+
@env.each do |key, value|
|
|
170
|
+
if key.start_with?("HTTP_")
|
|
171
|
+
h[key[5..-1].downcase] = value
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
h
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_cookies
|
|
178
|
+
cookie_str = @env["HTTP_COOKIE"]
|
|
179
|
+
return {} unless cookie_str && !cookie_str.empty?
|
|
180
|
+
|
|
181
|
+
result = {}
|
|
182
|
+
cookie_str.split(";").each do |pair|
|
|
183
|
+
key, value = pair.strip.split("=", 2)
|
|
184
|
+
result[key] = value if key
|
|
185
|
+
end
|
|
186
|
+
result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def read_body
|
|
190
|
+
input = @env["rack.input"]
|
|
191
|
+
return "" unless input
|
|
192
|
+
input.rewind if input.respond_to?(:rewind)
|
|
193
|
+
data = input.read || ""
|
|
194
|
+
input.rewind if input.respond_to?(:rewind)
|
|
195
|
+
data
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_body
|
|
199
|
+
if @content_type.include?("application/json")
|
|
200
|
+
json_body
|
|
201
|
+
elsif @content_type.include?("application/x-www-form-urlencoded")
|
|
202
|
+
parse_query_to_hash(body)
|
|
203
|
+
elsif @content_type.include?("multipart/form-data")
|
|
204
|
+
# Extract form fields from Rack's parsed multipart data.
|
|
205
|
+
# Files are handled separately by extract_files.
|
|
206
|
+
result = {}
|
|
207
|
+
form_hash = @env["rack.request.form_hash"] rescue nil
|
|
208
|
+
if form_hash
|
|
209
|
+
form_hash.each do |key, value|
|
|
210
|
+
# Skip file entries (handled by extract_files)
|
|
211
|
+
next if value.is_a?(Hash) && value[:tempfile]
|
|
212
|
+
result[key] = value
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
result
|
|
216
|
+
else
|
|
217
|
+
{}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_params
|
|
222
|
+
p = IndifferentHash.new
|
|
223
|
+
|
|
224
|
+
# Query string params
|
|
225
|
+
query.each { |k, v| p[k.to_s] = v }
|
|
226
|
+
|
|
227
|
+
# Body params
|
|
228
|
+
body_parsed.each { |k, v| p[k.to_s] = v }
|
|
229
|
+
|
|
230
|
+
# Path params (highest priority)
|
|
231
|
+
@path_params.each { |k, v| p[k.to_s] = v }
|
|
232
|
+
p
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def parse_query_to_hash(qs)
|
|
236
|
+
result = {}
|
|
237
|
+
return result if qs.nil? || qs.empty?
|
|
238
|
+
qs.split("&").each do |pair|
|
|
239
|
+
key, value = pair.split("=", 2)
|
|
240
|
+
result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
|
|
241
|
+
end
|
|
242
|
+
result
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def extract_files
|
|
246
|
+
result = {}
|
|
247
|
+
return result unless @content_type.include?("multipart/form-data")
|
|
248
|
+
begin
|
|
249
|
+
form_hash = @env["rack.request.form_hash"]
|
|
250
|
+
if form_hash
|
|
251
|
+
form_hash.each do |key, value|
|
|
252
|
+
if value.is_a?(Hash) && value[:tempfile]
|
|
253
|
+
result[key] = {
|
|
254
|
+
filename: value[:filename],
|
|
255
|
+
type: value[:type],
|
|
256
|
+
tempfile: value[:tempfile],
|
|
257
|
+
size: value[:tempfile].size
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
rescue StandardError
|
|
263
|
+
# Multipart parsing failed
|
|
264
|
+
end
|
|
265
|
+
result
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|