tina4ruby 3.13.137 → 3.13.139
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 +40 -0
- data/COMMERCIAL-LICENSE.md +8 -0
- data/LICENSE +373 -0
- data/LICENSE.txt +373 -21
- data/NOTICE +11 -0
- data/README.md +11 -7
- data/exe/tina4ruby +6 -0
- data/lib/tina4/ai.rb +6 -0
- data/lib/tina4/ai_client.rb +10 -3
- data/lib/tina4/api.rb +74 -8
- data/lib/tina4/auth.rb +108 -26
- data/lib/tina4/auto_crud.rb +83 -27
- data/lib/tina4/background.rb +7 -1
- data/lib/tina4/base64.rb +59 -0
- data/lib/tina4/cache.rb +6 -0
- data/lib/tina4/cache_backends/base_backend.rb +14 -0
- data/lib/tina4/cache_backends/database_backend.rb +6 -0
- data/lib/tina4/cache_backends/file_backend.rb +6 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +6 -0
- data/lib/tina4/cache_backends/memory_backend.rb +6 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +22 -1
- data/lib/tina4/cache_backends/redis_backend.rb +6 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +6 -0
- data/lib/tina4/cache_backends.rb +9 -1
- data/lib/tina4/cli.rb +27 -7
- data/lib/tina4/constants.rb +12 -8
- data/lib/tina4/container.rb +6 -0
- data/lib/tina4/context/chunker.rb +6 -0
- data/lib/tina4/context.rb +13 -5
- data/lib/tina4/cors.rb +66 -10
- data/lib/tina4/crud.rb +78 -9
- data/lib/tina4/database/sqlite3_adapter.rb +9 -1
- data/lib/tina4/database.rb +260 -44
- data/lib/tina4/database_adapter.rb +6 -0
- data/lib/tina4/database_result.rb +6 -0
- data/lib/tina4/database_url.rb +6 -0
- data/lib/tina4/debug.rb +6 -0
- data/lib/tina4/dev.rb +6 -0
- data/lib/tina4/dev_admin.rb +222 -89
- data/lib/tina4/dev_mailbox.rb +6 -1
- data/lib/tina4/dispatch_pipeline.rb +86 -1
- data/lib/tina4/docs.rb +6 -0
- data/lib/tina4/docstore.rb +23 -4
- data/lib/tina4/drivers/arango_graph_driver.rb +8 -1
- data/lib/tina4/drivers/bolt_graph_driver.rb +6 -0
- data/lib/tina4/drivers/firebird_driver.rb +95 -6
- data/lib/tina4/drivers/mongodb_driver.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +54 -39
- data/lib/tina4/drivers/mysql_driver.rb +6 -0
- data/lib/tina4/drivers/odbc_driver.rb +6 -0
- data/lib/tina4/drivers/postgres_driver.rb +35 -8
- data/lib/tina4/drivers/schema_split.rb +6 -0
- data/lib/tina4/drivers/sqlite_driver.rb +16 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +14 -6
- data/lib/tina4/error_overlay.rb +6 -0
- data/lib/tina4/events.rb +6 -0
- data/lib/tina4/feedback.rb +9 -2
- data/lib/tina4/field_types.rb +6 -0
- data/lib/tina4/form_parser.rb +241 -0
- data/lib/tina4/frond.rb +200 -58
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +6 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +6 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +6 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +6 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +6 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +6 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +6 -0
- data/lib/tina4/graph.rb +6 -0
- data/lib/tina4/graphql.rb +134 -35
- data/lib/tina4/health.rb +6 -0
- data/lib/tina4/html_element.rb +6 -0
- data/lib/tina4/http_server.rb +690 -0
- data/lib/tina4/imap_client.rb +352 -0
- data/lib/tina4/import_helper.rb +6 -0
- data/lib/tina4/job.rb +21 -2
- data/lib/tina4/localization.rb +6 -0
- data/lib/tina4/log.rb +6 -0
- data/lib/tina4/mail_socket.rb +124 -0
- data/lib/tina4/mcp.rb +18 -9
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +27 -10
- data/lib/tina4/migration.rb +6 -0
- data/lib/tina4/model_collection.rb +6 -0
- data/lib/tina4/mqtt.rb +9 -2
- data/lib/tina4/mqtt_message.rb +6 -0
- data/lib/tina4/orm.rb +72 -16
- data/lib/tina4/parse_json.rb +25 -0
- data/lib/tina4/plan.rb +9 -2
- data/lib/tina4/point.rb +6 -0
- data/lib/tina4/port_takeover.rb +6 -0
- data/lib/tina4/project_index.rb +6 -0
- data/lib/tina4/public/THIRD-PARTY-NOTICES.md +55 -0
- data/lib/tina4/public/bundled-component-provenance.json +48 -0
- data/lib/tina4/public/js/tina4js.min.js +13 -3
- data/lib/tina4/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- data/lib/tina4/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- data/lib/tina4/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- data/lib/tina4/public/licenses/tina4-css-MIT.txt +21 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-components.json +240 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- data/lib/tina4/public/licenses/tina4-js-MIT.txt +20 -0
- data/lib/tina4/push.rb +22 -4
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +18 -2
- data/lib/tina4/queue_backends/lite_backend.rb +18 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +18 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +18 -2
- data/lib/tina4/rack_app.rb +62 -50
- data/lib/tina4/rate_limiter.rb +6 -0
- data/lib/tina4/realtime/attachment.rb +6 -0
- data/lib/tina4/realtime/channel.rb +6 -0
- data/lib/tina4/realtime/channel_member.rb +6 -0
- data/lib/tina4/realtime/local_storage.rb +6 -0
- data/lib/tina4/realtime/message.rb +6 -0
- data/lib/tina4/realtime/s3_storage.rb +15 -1
- data/lib/tina4/realtime/storage.rb +6 -0
- data/lib/tina4/realtime/storage_backend.rb +6 -0
- data/lib/tina4/realtime/workspace.rb +6 -0
- data/lib/tina4/realtime.rb +10 -3
- data/lib/tina4/request.rb +90 -127
- data/lib/tina4/response.rb +71 -9
- data/lib/tina4/response_cache.rb +6 -0
- data/lib/tina4/router.rb +6 -0
- data/lib/tina4/secret_file.rb +34 -0
- data/lib/tina4/seeder.rb +9 -3
- data/lib/tina4/service.rb +8 -2
- data/lib/tina4/service_runner.rb +6 -0
- data/lib/tina4/session.rb +6 -0
- data/lib/tina4/session_handlers/database_handler.rb +42 -12
- data/lib/tina4/session_handlers/file_handler.rb +6 -0
- data/lib/tina4/session_handlers/memcached_handler.rb +6 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +108 -39
- data/lib/tina4/session_handlers/mongo_wire_client.rb +23 -9
- data/lib/tina4/session_handlers/redis_handler.rb +6 -0
- data/lib/tina4/session_handlers/resp_client.rb +6 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -0
- data/lib/tina4/shutdown.rb +17 -14
- data/lib/tina4/smtp_client.rb +222 -0
- data/lib/tina4/sql_translator.rb +93 -0
- data/lib/tina4/sqlite3_gem.rb +27 -0
- data/lib/tina4/sso.rb +52 -12
- data/lib/tina4/ssrf.rb +148 -0
- data/lib/tina4/swagger.rb +6 -0
- data/lib/tina4/template.rb +6 -0
- data/lib/tina4/test.rb +8 -2
- data/lib/tina4/test_client.rb +8 -1
- data/lib/tina4/testing.rb +8 -1
- data/lib/tina4/validator.rb +6 -0
- data/lib/tina4/version.rb +7 -1
- data/lib/tina4/webserver.rb +25 -143
- data/lib/tina4/websocket.rb +170 -20
- data/lib/tina4/websocket_backplane.rb +25 -2
- data/lib/tina4/wsdl.rb +54 -18
- data/lib/tina4/xml_parser.rb +268 -0
- data/lib/tina4.rb +98 -33
- data/lib/tina4ruby.rb +6 -0
- metadata +44 -140
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Ultipa graph adapter — the portable core built in GQL over tina4-ultipa.
|
|
4
10
|
#
|
data/lib/tina4/env.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
3
7
|
|
|
4
8
|
module Tina4
|
|
5
9
|
# Legacy env var names that v3.12 has retired. If any of these are set in
|
|
@@ -84,11 +88,15 @@ module Tina4
|
|
|
84
88
|
# signal for Auth.ensure_dev_secret to mint a per-machine random dev secret
|
|
85
89
|
# (saved to gitignored .env.local) in dev, or to emit the actionable
|
|
86
90
|
# "set TINA4_SECRET" warning in CI/prod. Parity with the Python master.
|
|
91
|
+
#
|
|
92
|
+
# TINA4_DEBUG is deliberately absent too: unset debug means OFF, and a boot
|
|
93
|
+
# side effect must never switch it on (ADR-0079 s3). `tina4ruby init`, an
|
|
94
|
+
# explicit developer action, writes TINA4_DEBUG=true into the project it
|
|
95
|
+
# scaffolds.
|
|
87
96
|
DEFAULT_ENV = {
|
|
88
97
|
"PROJECT_NAME" => "Tina4 Ruby Project",
|
|
89
98
|
"TINA4_SWAGGER_VERSION" => "1.0.0",
|
|
90
99
|
"TINA4_LOCALE" => "en",
|
|
91
|
-
"TINA4_DEBUG" => "true",
|
|
92
100
|
"TINA4_LOG_LEVEL" => "ALL"
|
|
93
101
|
}.freeze
|
|
94
102
|
|
|
@@ -260,11 +268,11 @@ module Tina4
|
|
|
260
268
|
File.join(root_dir, ".env")
|
|
261
269
|
end
|
|
262
270
|
|
|
271
|
+
# A missing .env gets the neutral defaults only. It no longer mints a
|
|
272
|
+
# TINA4_API_KEY: an MD5 of the clock is guessable, and that key opened
|
|
273
|
+
# the route auth gate and the dev-admin write gate (ADR-0079 s3).
|
|
263
274
|
def create_default_env(path)
|
|
264
|
-
|
|
265
|
-
content = DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n")
|
|
266
|
-
content += "\nTINA4_API_KEY=\"#{api_key}\"\n"
|
|
267
|
-
File.write(path, content)
|
|
275
|
+
File.write(path, DEFAULT_ENV.map { |k, v| "#{k}=\"#{v}\"" }.join("\n") + "\n")
|
|
268
276
|
end
|
|
269
277
|
|
|
270
278
|
# Parse a dotenv file into ENV.
|
data/lib/tina4/error_overlay.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4 Debug — Rich error overlay for development mode.
|
|
4
10
|
#
|
data/lib/tina4/events.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
# Tina4 Events — Simple observer pattern for decoupled communication.
|
|
4
10
|
#
|
data/lib/tina4/feedback.rb
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "net/http"
|
|
5
11
|
require "uri"
|
|
12
|
+
require_relative "parse_json"
|
|
6
13
|
|
|
7
14
|
module Tina4
|
|
8
15
|
# ── Customer feedback widget — server-side plumbing ─────────────────
|
|
@@ -170,7 +177,7 @@ module Tina4
|
|
|
170
177
|
req.body = JSON.generate(forward_body)
|
|
171
178
|
resp = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 60) { |h| h.request(req) }
|
|
172
179
|
parsed = begin
|
|
173
|
-
|
|
180
|
+
Tina4.parse_json(resp.body.to_s)
|
|
174
181
|
rescue JSON::ParserError
|
|
175
182
|
nil
|
|
176
183
|
end
|
|
@@ -293,7 +300,7 @@ module Tina4
|
|
|
293
300
|
input.rewind if input.respond_to?(:rewind)
|
|
294
301
|
raw = input.read
|
|
295
302
|
return nil if raw.nil? || raw.empty?
|
|
296
|
-
|
|
303
|
+
Tina4.parse_json(raw)
|
|
297
304
|
rescue JSON::ParserError
|
|
298
305
|
nil
|
|
299
306
|
end
|
data/lib/tina4/field_types.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module FieldTypes
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require "stringio"
|
|
10
|
+
|
|
11
|
+
module Tina4
|
|
12
|
+
# multipart/form-data parsing, owned by Tina4 (no Rack).
|
|
13
|
+
#
|
|
14
|
+
# Replaces Rack::Request#POST, which Tina4::Request used for multipart bodies
|
|
15
|
+
# until rack stopped being a dependency. Field names nest exactly the way Rack
|
|
16
|
+
# nested them - `user[name]` becomes a hash, `tags[]` an array, `a[][x]` an
|
|
17
|
+
# array of hashes, a repeated plain name keeps its last value - so a Ruby app
|
|
18
|
+
# that posts nested forms sees the same body it always did. spec/form_parser_spec.rb
|
|
19
|
+
# pins every shape against values captured from Rack 3.2.7.
|
|
20
|
+
#
|
|
21
|
+
# Files are parsed in the SAME pass and keyed by field name, each name mapping
|
|
22
|
+
# to a LIST of descriptors so a repeated file field keeps every file
|
|
23
|
+
# (fileupload_contract upload-repeated-field-list).
|
|
24
|
+
#
|
|
25
|
+
# The part delimiter is CRLF + "--" + boundary, as RFC 2046 defines it, so a
|
|
26
|
+
# file whose bytes happen to contain "--boundary" mid-line is not cut short.
|
|
27
|
+
module FormParser
|
|
28
|
+
# A name nested deeper than this is refused rather than recursed into.
|
|
29
|
+
# Rack's QueryParser param_depth_limit, kept for the same reason.
|
|
30
|
+
PARAM_DEPTH_LIMIT = 32
|
|
31
|
+
|
|
32
|
+
# A field name that is both a value and a container (`a=1` then `a[b]=2`)
|
|
33
|
+
# or nests past PARAM_DEPTH_LIMIT. Rack raised here too.
|
|
34
|
+
class ParameterError < StandardError; end
|
|
35
|
+
|
|
36
|
+
# Marks a hash the NESTING built, as opposed to a value that happens to be a
|
|
37
|
+
# hash. Only these may be descended into.
|
|
38
|
+
class NestedParams < Hash; end
|
|
39
|
+
private_constant :NestedParams
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# The boundary token from a multipart Content-Type, quotes removed; nil when
|
|
44
|
+
# there is none.
|
|
45
|
+
def boundary(content_type)
|
|
46
|
+
content_type.to_s.split(";").each do |part|
|
|
47
|
+
part = part.strip
|
|
48
|
+
next unless part.downcase.start_with?("boundary=")
|
|
49
|
+
|
|
50
|
+
value = part[9..].to_s.strip
|
|
51
|
+
value = value[1...-1] if value.length >= 2 && value.start_with?('"') && value.end_with?('"')
|
|
52
|
+
return value.empty? ? nil : value
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parse a multipart body.
|
|
58
|
+
#
|
|
59
|
+
# @return [Hash] { fields: Hash (nested, Rack-shaped), files: Hash{name => [descriptor]},
|
|
60
|
+
# error: ParameterError or nil }
|
|
61
|
+
# A descriptor is { filename:, type:, content:, size:, tempfile: } with the
|
|
62
|
+
# RAW client filename - Tina4::Request.save_upload sanitises it on write.
|
|
63
|
+
# A name conflict or over-deep nesting empties the fields and is returned
|
|
64
|
+
# as :error rather than raised, so one bad field name cannot lose the files.
|
|
65
|
+
def parse_multipart(body, content_type)
|
|
66
|
+
token = boundary(content_type)
|
|
67
|
+
fields = NestedParams.new
|
|
68
|
+
files = {}
|
|
69
|
+
return { fields: {}, files: files, error: nil } if token.nil? || body.nil? || body.empty?
|
|
70
|
+
|
|
71
|
+
pairs = []
|
|
72
|
+
each_part(body.b, token) do |headers, content|
|
|
73
|
+
name, filename = disposition_params(headers["content-disposition"].to_s)
|
|
74
|
+
next if name.nil? || name.empty?
|
|
75
|
+
|
|
76
|
+
if filename.nil?
|
|
77
|
+
pairs << [name.dup.force_encoding(Encoding::UTF_8), text_value(content, headers["content-type"])]
|
|
78
|
+
else
|
|
79
|
+
(files[name] ||= []) << {
|
|
80
|
+
filename: filename.dup.force_encoding(Encoding::UTF_8).scrub,
|
|
81
|
+
type: headers["content-type"] || "application/octet-stream",
|
|
82
|
+
content: content,
|
|
83
|
+
size: content.bytesize,
|
|
84
|
+
tempfile: StringIO.new(content)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
# A name conflict voids the FIELDS only (Rack raised for the whole form);
|
|
89
|
+
# the files were collected independently and survive it.
|
|
90
|
+
error = nil
|
|
91
|
+
begin
|
|
92
|
+
pairs.each { |name, value| normalize_params(fields, name, value, 0) }
|
|
93
|
+
rescue ParameterError => e
|
|
94
|
+
error = e
|
|
95
|
+
fields = NestedParams.new
|
|
96
|
+
end
|
|
97
|
+
{ fields: to_plain_hash(fields), files: files, error: error }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Yield [headers, content] for every part. headers keys are lowercased.
|
|
101
|
+
def each_part(body, token)
|
|
102
|
+
delimiter = "--#{token}".b
|
|
103
|
+
position = body.index(delimiter)
|
|
104
|
+
return if position.nil?
|
|
105
|
+
|
|
106
|
+
position += delimiter.bytesize
|
|
107
|
+
separator = "\r\n#{delimiter}".b
|
|
108
|
+
while position < body.bytesize
|
|
109
|
+
# "--" straight after a delimiter closes the body.
|
|
110
|
+
break if body.byteslice(position, 2) == "--"
|
|
111
|
+
|
|
112
|
+
position += 2 if body.byteslice(position, 2) == "\r\n"
|
|
113
|
+
header_end = body.index("\r\n\r\n", position)
|
|
114
|
+
break if header_end.nil?
|
|
115
|
+
|
|
116
|
+
next_delimiter = body.index(separator, header_end + 4)
|
|
117
|
+
break if next_delimiter.nil?
|
|
118
|
+
|
|
119
|
+
yield part_headers(body.byteslice(position, header_end - position)),
|
|
120
|
+
body.byteslice(header_end + 4, next_delimiter - header_end - 4)
|
|
121
|
+
position = next_delimiter + separator.bytesize
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def part_headers(block)
|
|
126
|
+
headers = {}
|
|
127
|
+
block.split("\r\n").each do |line|
|
|
128
|
+
name, value = line.split(":", 2)
|
|
129
|
+
next if value.nil?
|
|
130
|
+
|
|
131
|
+
headers[name.strip.downcase] = value.strip.force_encoding(Encoding::UTF_8).scrub
|
|
132
|
+
end
|
|
133
|
+
headers
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# name= and filename= from a Content-Disposition value, by PARAMETER (not by
|
|
137
|
+
# a substring match, which found "name=" inside "filename=").
|
|
138
|
+
def disposition_params(disposition)
|
|
139
|
+
params = {}
|
|
140
|
+
disposition.scan(/;\s*([A-Za-z0-9!#$%&'*+.^_`|~-]+)\s*=\s*("(?:[^"\\]|\\.)*"|[^;]*)/) do |key, raw|
|
|
141
|
+
value = raw.strip
|
|
142
|
+
value = value[1...-1].gsub(/\\(.)/, '\1') if value.start_with?('"') && value.end_with?('"') && value.length >= 2
|
|
143
|
+
params[key.downcase] ||= value
|
|
144
|
+
end
|
|
145
|
+
[params["name"], params["filename"]]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A text field is UTF-8 unless the part says `text/plain; charset=...`
|
|
149
|
+
# (Rack's rule). An unknown charset falls back to binary, never raises.
|
|
150
|
+
def text_value(content, part_type)
|
|
151
|
+
encoding = Encoding::UTF_8
|
|
152
|
+
if part_type
|
|
153
|
+
type, *parameters = part_type.split(";").map(&:strip)
|
|
154
|
+
if type.casecmp?("text/plain")
|
|
155
|
+
parameters.each do |parameter|
|
|
156
|
+
key, value = parameter.split("=", 2)
|
|
157
|
+
next unless key&.casecmp?("charset") && value
|
|
158
|
+
|
|
159
|
+
encoding = begin
|
|
160
|
+
Encoding.find(value.delete('"'))
|
|
161
|
+
rescue ArgumentError
|
|
162
|
+
Encoding::BINARY
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
content.dup.force_encoding(encoding)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Rack::QueryParser#normalize_params, ported. Returns params.
|
|
171
|
+
def normalize_params(params, name, value, depth)
|
|
172
|
+
raise ParameterError, "field name nests deeper than #{PARAM_DEPTH_LIMIT} levels" if depth >= PARAM_DEPTH_LIMIT
|
|
173
|
+
|
|
174
|
+
if depth.zero?
|
|
175
|
+
start = name.index("[", 1)
|
|
176
|
+
key = start ? name[0, start] : name
|
|
177
|
+
after = start ? name[start..] : ""
|
|
178
|
+
elsif name.start_with?("[]")
|
|
179
|
+
key = "[]"
|
|
180
|
+
after = name[2..]
|
|
181
|
+
elsif name.start_with?("[") && (close = name.index("]", 1))
|
|
182
|
+
key = name[1, close - 1]
|
|
183
|
+
after = name[(close + 1)..]
|
|
184
|
+
else
|
|
185
|
+
key = name
|
|
186
|
+
after = ""
|
|
187
|
+
end
|
|
188
|
+
return if key.empty?
|
|
189
|
+
|
|
190
|
+
if after.empty?
|
|
191
|
+
return [value] if key == "[]" && !depth.zero?
|
|
192
|
+
|
|
193
|
+
params[key] = value
|
|
194
|
+
elsif after == "["
|
|
195
|
+
params[name] = value
|
|
196
|
+
elsif after == "[]"
|
|
197
|
+
params[key] ||= []
|
|
198
|
+
raise ParameterError, "expected Array for `#{key}'" unless params[key].is_a?(Array)
|
|
199
|
+
|
|
200
|
+
params[key] << value
|
|
201
|
+
elsif after.start_with?("[]")
|
|
202
|
+
child_key = after[3, after.length - 4] if after[2] == "[" && after.end_with?("]")
|
|
203
|
+
child_key = after[2..] if child_key.nil? || child_key.empty? || child_key.include?("[") || child_key.include?("]")
|
|
204
|
+
params[key] ||= []
|
|
205
|
+
raise ParameterError, "expected Array for `#{key}'" unless params[key].is_a?(Array)
|
|
206
|
+
|
|
207
|
+
last = params[key].last
|
|
208
|
+
if last.is_a?(NestedParams) && !nested_key?(last, child_key)
|
|
209
|
+
normalize_params(last, child_key, value, depth + 1)
|
|
210
|
+
else
|
|
211
|
+
params[key] << normalize_params(NestedParams.new, child_key, value, depth + 1)
|
|
212
|
+
end
|
|
213
|
+
else
|
|
214
|
+
params[key] ||= NestedParams.new
|
|
215
|
+
raise ParameterError, "expected Hash for `#{key}'" unless params[key].is_a?(NestedParams)
|
|
216
|
+
|
|
217
|
+
params[key] = normalize_params(params[key], after, value, depth + 1)
|
|
218
|
+
end
|
|
219
|
+
params
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def nested_key?(hash, key)
|
|
223
|
+
return false if key.include?("[]")
|
|
224
|
+
|
|
225
|
+
key.split(/[\[\]]+/).reject(&:empty?).reduce(hash) do |level, part|
|
|
226
|
+
return false unless level.is_a?(NestedParams) && level.key?(part)
|
|
227
|
+
|
|
228
|
+
level[part]
|
|
229
|
+
end
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def to_plain_hash(value)
|
|
234
|
+
case value
|
|
235
|
+
when Hash then value.each_with_object({}) { |(key, child), plain| plain[key] = to_plain_hash(child) }
|
|
236
|
+
when Array then value.map { |child| to_plain_hash(child) }
|
|
237
|
+
else value
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|