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
data/lib/tina4/frond.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 Frond Engine -- Lexer, parser, and runtime.
|
|
4
10
|
# Zero-dependency twig-like template engine.
|
|
@@ -8,13 +14,14 @@
|
|
|
8
14
|
|
|
9
15
|
require "json"
|
|
10
16
|
require "digest"
|
|
11
|
-
|
|
12
|
-
require "cgi"
|
|
17
|
+
require_relative "base64"
|
|
18
|
+
require "cgi/escape"
|
|
13
19
|
require "erb"
|
|
14
20
|
require "uri"
|
|
15
21
|
require "date"
|
|
16
22
|
require "time"
|
|
17
23
|
require "securerandom"
|
|
24
|
+
require_relative "parse_json"
|
|
18
25
|
|
|
19
26
|
module Tina4
|
|
20
27
|
# Marker class for strings that should not be auto-escaped in Frond.
|
|
@@ -484,6 +491,38 @@ module Tina4
|
|
|
484
491
|
# The escaping decision has to ask this rather than read the filter name out
|
|
485
492
|
# of the source: a denied `raw` that still marked its value safe made the
|
|
486
493
|
# allow-list entry governing XSS escaping inert.
|
|
494
|
+
# Words in an expression that are not variable references.
|
|
495
|
+
SANDBOX_EXPR_KEYWORDS = %w[true false none null nil and or not in is loop
|
|
496
|
+
if else empty starts ends with matches].freeze
|
|
497
|
+
SANDBOX_IDENT_RE = /(?<![.\w'"])([A-Za-z_][A-Za-z0-9_]*)/
|
|
498
|
+
SANDBOX_STRING_RE = /'[^']*'|"[^"]*"/
|
|
499
|
+
# Methods that must never be invoked through {{ obj.method }} — reflection,
|
|
500
|
+
# metaprogramming and process control (F6/ADR-0077).
|
|
501
|
+
UNSAFE_METHODS = %w[send __send__ public_send instance_eval instance_exec
|
|
502
|
+
eval class methods public_methods private_methods
|
|
503
|
+
instance_variables instance_variable_get
|
|
504
|
+
instance_variable_set define_singleton_method method
|
|
505
|
+
tap then object_id __id__ system exec spawn binding
|
|
506
|
+
singleton_class ancestors superclass constants
|
|
507
|
+
const_get remove_method].to_set.freeze
|
|
508
|
+
|
|
509
|
+
# Sandbox gate for a WHOLE expression (F6/ADR-0077): refuse a dunder walk
|
|
510
|
+
# or a root variable outside the allow-list, in output, conditions, set
|
|
511
|
+
# right-hand sides and for iterables alike.
|
|
512
|
+
def sandbox_expr_ok?(expr)
|
|
513
|
+
return true unless @sandbox
|
|
514
|
+
return false if expr.include?("__")
|
|
515
|
+
return true unless @allowed_vars
|
|
516
|
+
|
|
517
|
+
stripped = expr.gsub(SANDBOX_STRING_RE, "")
|
|
518
|
+
stripped.scan(SANDBOX_IDENT_RE).each do |(ident)|
|
|
519
|
+
next if SANDBOX_EXPR_KEYWORDS.include?(ident.downcase)
|
|
520
|
+
next if @filters.key?(ident)
|
|
521
|
+
return false unless @allowed_vars.include?(ident)
|
|
522
|
+
end
|
|
523
|
+
true
|
|
524
|
+
end
|
|
525
|
+
|
|
487
526
|
def filter_permitted?(name)
|
|
488
527
|
return true unless @sandbox && @allowed_filters
|
|
489
528
|
|
|
@@ -506,6 +545,83 @@ module Tina4
|
|
|
506
545
|
str.to_s.gsub(HTML_ESCAPE_RE, HTML_ESCAPE_MAP)
|
|
507
546
|
end
|
|
508
547
|
|
|
548
|
+
# -- Escape strategies (ADR-0077) ------------------------------------
|
|
549
|
+
# Shared by js_escape and e(strategy); byte-identical to the Python
|
|
550
|
+
# master. Twig-compatible.
|
|
551
|
+
JS_SAFE_RE = /[A-Za-z0-9,._]/
|
|
552
|
+
CSS_SAFE_RE = /[A-Za-z0-9]/
|
|
553
|
+
ATTR_SAFE_RE = /[A-Za-z0-9,.\-_]/
|
|
554
|
+
|
|
555
|
+
def self.js_escape_str(value)
|
|
556
|
+
out = +""
|
|
557
|
+
value.to_s.each_char do |ch|
|
|
558
|
+
if ch =~ JS_SAFE_RE
|
|
559
|
+
out << ch
|
|
560
|
+
next
|
|
561
|
+
end
|
|
562
|
+
code = ch.ord
|
|
563
|
+
if code < 0x80
|
|
564
|
+
out << format("\\x%02X", code)
|
|
565
|
+
elsif code <= 0xFFFF
|
|
566
|
+
out << format("\\u%04X", code)
|
|
567
|
+
else
|
|
568
|
+
code -= 0x10000
|
|
569
|
+
out << format("\\u%04X", 0xD800 + (code >> 10))
|
|
570
|
+
out << format("\\u%04X", 0xDC00 + (code & 0x3FF))
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
out
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def self.css_escape(value)
|
|
577
|
+
out = +""
|
|
578
|
+
value.to_s.each_char do |ch|
|
|
579
|
+
out << (ch =~ CSS_SAFE_RE ? ch : format("\\%06X ", ch.ord))
|
|
580
|
+
end
|
|
581
|
+
out
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def self.html_attr_escape(value)
|
|
585
|
+
out = +""
|
|
586
|
+
value.to_s.each_char do |ch|
|
|
587
|
+
out << (ch =~ ATTR_SAFE_RE ? ch : format("&#x%02X;", ch.ord))
|
|
588
|
+
end
|
|
589
|
+
out
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# RFC-3986 rawurlencode: unreserved set stays, everything else percent-encoded.
|
|
593
|
+
def self.url_escape(value)
|
|
594
|
+
value.to_s.b.gsub(/[^A-Za-z0-9\-_.~]/) { |c| format("%%%02X", c.ord) }
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Dispatch e(strategy)/escape(strategy). An unknown strategy raises, so a
|
|
598
|
+
# typo can never silently pass the value through unescaped.
|
|
599
|
+
def self.escape_strategy(value, strategy = "html")
|
|
600
|
+
s = strategy.to_s.strip.gsub(/\A['"]|['"]\z/, "")
|
|
601
|
+
case s
|
|
602
|
+
when "", "html"
|
|
603
|
+
Tina4::SafeString.new(escape_html(value.to_s))
|
|
604
|
+
when "js"
|
|
605
|
+
Tina4::SafeString.new(js_escape_str(value))
|
|
606
|
+
when "url"
|
|
607
|
+
Tina4::SafeString.new(url_escape(value))
|
|
608
|
+
when "css"
|
|
609
|
+
Tina4::SafeString.new(css_escape(value))
|
|
610
|
+
when "html_attr", "attr"
|
|
611
|
+
Tina4::SafeString.new(html_attr_escape(value))
|
|
612
|
+
else
|
|
613
|
+
raise ArgumentError, "Unknown escape strategy: #{strategy}"
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Confine a client-supplied media type to a strict type/subtype grammar
|
|
618
|
+
# (F3/ADR-0077); anything malformed falls back to a safe default.
|
|
619
|
+
MEDIA_TYPE_RE = %r{\A[A-Za-z0-9][A-Za-z0-9!\#$&^_.+-]*/[A-Za-z0-9][A-Za-z0-9!\#$&^_.+-]*\z}
|
|
620
|
+
def self.sanitise_media_type(type)
|
|
621
|
+
t = type.to_s.strip
|
|
622
|
+
t =~ MEDIA_TYPE_RE ? t : "application/octet-stream"
|
|
623
|
+
end
|
|
624
|
+
|
|
509
625
|
# Serializes a value to compact JSON text that is always valid JSON.
|
|
510
626
|
#
|
|
511
627
|
# Never raises and never returns an empty string: a non-finite float becomes
|
|
@@ -616,35 +732,54 @@ module Tina4
|
|
|
616
732
|
# Tokenizer
|
|
617
733
|
# -----------------------------------------------------------------------
|
|
618
734
|
|
|
619
|
-
#
|
|
620
|
-
|
|
621
|
-
|
|
735
|
+
# Walk literal delimiters instead of retrying a whole-template regex at
|
|
736
|
+
# every unmatched opener. This also bounds parsing on Ruby versions that
|
|
737
|
+
# do not memoize regexp backtracking.
|
|
622
738
|
def tokenize(source)
|
|
623
|
-
# 1. Extract raw blocks and replace with placeholders
|
|
624
739
|
raw_blocks = []
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
740
|
+
chunks = []
|
|
741
|
+
copied = 0
|
|
742
|
+
cursor = 0
|
|
743
|
+
raw_start = nil
|
|
744
|
+
while (start = source.index("{%", cursor))
|
|
745
|
+
cursor = start + 2
|
|
746
|
+
match = /\G\{%-?\s*(raw|endraw)\s*-?%\}/.match(source, start)
|
|
747
|
+
next unless match
|
|
748
|
+
finish = match.end(0) - 2
|
|
749
|
+
tag = match[1]
|
|
750
|
+
if tag == "raw" && raw_start.nil?
|
|
751
|
+
raw_start = [start, finish + 2]
|
|
752
|
+
elsif tag == "endraw" && raw_start
|
|
753
|
+
chunks << source[copied...raw_start[0]]
|
|
754
|
+
chunks << "\x00RAW_#{raw_blocks.length}\x00"
|
|
755
|
+
raw_blocks << source[raw_start[1]...start]
|
|
756
|
+
copied = finish + 2
|
|
757
|
+
raw_start = nil
|
|
758
|
+
end
|
|
629
759
|
end
|
|
760
|
+
chunks << source[copied..]
|
|
761
|
+
source = chunks.join
|
|
630
762
|
|
|
631
|
-
# 2. Normal tokenization
|
|
632
763
|
tokens = []
|
|
633
764
|
pos = 0
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
765
|
+
cursor = 0
|
|
766
|
+
missing_close = {}
|
|
767
|
+
closers = { "{%" => "%}", "{{" => "}}", "{#" => "#}" }
|
|
768
|
+
while (match = /\{%|\{\{|\{#/.match(source, cursor))
|
|
769
|
+
start = match.begin(0)
|
|
770
|
+
opener = match[0]
|
|
771
|
+
cursor = start + 2
|
|
772
|
+
next if missing_close[opener]
|
|
773
|
+
finish = source.index(closers.fetch(opener), cursor)
|
|
774
|
+
unless finish
|
|
775
|
+
missing_close[opener] = true
|
|
776
|
+
next
|
|
646
777
|
end
|
|
647
|
-
pos
|
|
778
|
+
tokens << [TEXT, source[pos...start]] if start > pos
|
|
779
|
+
raw = source[start...(finish + 2)]
|
|
780
|
+
type = { "{#" => COMMENT, "{{" => VAR, "{%" => BLOCK }.fetch(opener)
|
|
781
|
+
tokens << [type, raw]
|
|
782
|
+
pos = cursor = finish + 2
|
|
648
783
|
end
|
|
649
784
|
tokens << [TEXT, source[pos..]] if pos < source.length
|
|
650
785
|
|
|
@@ -1155,6 +1290,10 @@ module Tina4
|
|
|
1155
1290
|
end
|
|
1156
1291
|
|
|
1157
1292
|
def eval_var_raw(expr, context)
|
|
1293
|
+
# Sandbox: gate the whole condition/RHS (F6) so a comparison or iterable
|
|
1294
|
+
# cannot leak a blocked variable.
|
|
1295
|
+
return nil if @sandbox && !sandbox_expr_ok?(expr)
|
|
1296
|
+
|
|
1158
1297
|
var_name, filters = parse_filter_chain(expr)
|
|
1159
1298
|
value = eval_expr(var_name, context)
|
|
1160
1299
|
filters.each do |fname, args|
|
|
@@ -1232,16 +1371,8 @@ module Tina4
|
|
|
1232
1371
|
|
|
1233
1372
|
var_name, filters = parse_filter_chain(expr)
|
|
1234
1373
|
|
|
1235
|
-
# Sandbox:
|
|
1236
|
-
if @sandbox &&
|
|
1237
|
-
root_var = @dotted_split_cache[var_name]
|
|
1238
|
-
unless root_var
|
|
1239
|
-
root_var = var_name.split(".")[0].split("[")[0].strip
|
|
1240
|
-
cap_cache(@dotted_split_cache, MEMO_CACHE_MAX)
|
|
1241
|
-
@dotted_split_cache[var_name] = root_var
|
|
1242
|
-
end
|
|
1243
|
-
return "" if !root_var.empty? && !@allowed_vars.include?(root_var) && root_var != "loop"
|
|
1244
|
-
end
|
|
1374
|
+
# Sandbox: gate the whole expression (allow-list + dunder block, F6)
|
|
1375
|
+
return "" if @sandbox && !sandbox_expr_ok?(expr)
|
|
1245
1376
|
|
|
1246
1377
|
value = eval_expr(var_name, context)
|
|
1247
1378
|
|
|
@@ -1283,7 +1414,7 @@ module Tina4
|
|
|
1283
1414
|
when "title" then value.to_s.split.map(&:capitalize).join(" ")
|
|
1284
1415
|
when "string" then value.to_s
|
|
1285
1416
|
when "int" then value.to_i
|
|
1286
|
-
when "escape", "e" then
|
|
1417
|
+
when "escape", "e" then Frond.escape_strategy(value, "html")
|
|
1287
1418
|
else value
|
|
1288
1419
|
end
|
|
1289
1420
|
next
|
|
@@ -1296,9 +1427,17 @@ module Tina4
|
|
|
1296
1427
|
end
|
|
1297
1428
|
end
|
|
1298
1429
|
|
|
1299
|
-
# Auto-escape HTML unless marked safe or SafeString
|
|
1300
|
-
|
|
1301
|
-
|
|
1430
|
+
# Auto-escape HTML unless marked safe or SafeString. A plain string is
|
|
1431
|
+
# escaped directly; an Array/Hash/object is escaped on its rendered
|
|
1432
|
+
# string form (F4/ADR-0077) -- before this, only String values were
|
|
1433
|
+
# escaped, so {{ items }} holding markup emitted it raw. nil, booleans
|
|
1434
|
+
# and Numerics carry no HTML-special characters and are left untouched.
|
|
1435
|
+
if @auto_escape && !is_safe && !value.is_a?(SafeString)
|
|
1436
|
+
if value.is_a?(String)
|
|
1437
|
+
value = Frond.escape_html(value)
|
|
1438
|
+
elsif !value.nil? && value != true && value != false && !value.is_a?(Numeric)
|
|
1439
|
+
value = Tina4::SafeString.new(Frond.escape_html(value.to_s))
|
|
1440
|
+
end
|
|
1302
1441
|
end
|
|
1303
1442
|
|
|
1304
1443
|
value
|
|
@@ -2060,8 +2199,10 @@ module Tina4
|
|
|
2060
2199
|
end
|
|
2061
2200
|
idx = idx.to_i if idx.is_a?(Numeric)
|
|
2062
2201
|
value = idx.is_a?(Integer) ? value[idx] : nil
|
|
2063
|
-
elsif value.respond_to?(part.to_sym)
|
|
2064
|
-
|
|
2202
|
+
elsif !UNSAFE_METHODS.include?(part) && value.respond_to?(part.to_sym)
|
|
2203
|
+
# Only call a method the object publicly exposes, and never a
|
|
2204
|
+
# reflection/metaprogramming method (F6/ADR-0077).
|
|
2205
|
+
value = value.public_send(part.to_sym)
|
|
2065
2206
|
else
|
|
2066
2207
|
return nil
|
|
2067
2208
|
end
|
|
@@ -2235,7 +2376,11 @@ module Tina4
|
|
|
2235
2376
|
i += 1
|
|
2236
2377
|
end
|
|
2237
2378
|
|
|
2238
|
-
iterable =
|
|
2379
|
+
iterable = if @sandbox && !sandbox_expr_ok?(iterable_expr)
|
|
2380
|
+
nil
|
|
2381
|
+
else
|
|
2382
|
+
eval_expr(iterable_expr, context)
|
|
2383
|
+
end
|
|
2239
2384
|
|
|
2240
2385
|
if iterable.nil? || (iterable.respond_to?(:empty?) && iterable.empty?)
|
|
2241
2386
|
if else_tokens.any?
|
|
@@ -2852,25 +2997,25 @@ module Tina4
|
|
|
2852
2997
|
"striptags" => ->(v, *_a) { v.to_s.gsub(STRIPTAGS_RE, "") },
|
|
2853
2998
|
|
|
2854
2999
|
# -- Encoding --
|
|
2855
|
-
"escape" => ->(v, *
|
|
2856
|
-
"e" => ->(v, *
|
|
3000
|
+
"escape" => ->(v, *a) { Frond.escape_strategy(v, a[0] || "html") },
|
|
3001
|
+
"e" => ->(v, *a) { Frond.escape_strategy(v, a[0] || "html") },
|
|
2857
3002
|
"raw" => ->(v, *_a) { v },
|
|
2858
3003
|
"safe" => ->(v, *_a) { v },
|
|
2859
3004
|
# Frond.json_safe, never a bare JSON.generate: generate RAISES on an
|
|
2860
3005
|
# Infinity/NaN float, and the old `rescue v.to_s` answered that raise with
|
|
2861
3006
|
# Ruby inspect output that no JSON.parse will read. See Frond.json_safe.
|
|
2862
3007
|
"json_encode" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2863
|
-
"json_decode" => ->(v, *_a) { v.is_a?(String) ? (
|
|
2864
|
-
"base64_encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
2865
|
-
"base64encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
2866
|
-
"base64_decode" => ->(v, *_a) { Base64.decode64(v.to_s) },
|
|
2867
|
-
"base64decode" => ->(v, *_a) { Base64.decode64(v.to_s) },
|
|
3008
|
+
"json_decode" => ->(v, *_a) { v.is_a?(String) ? (Tina4.parse_json(v) rescue v) : v },
|
|
3009
|
+
"base64_encode" => ->(v, *_a) { Tina4::Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
3010
|
+
"base64encode" => ->(v, *_a) { Tina4::Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
3011
|
+
"base64_decode" => ->(v, *_a) { Tina4::Base64.decode64(v.to_s) },
|
|
3012
|
+
"base64decode" => ->(v, *_a) { Tina4::Base64.decode64(v.to_s) },
|
|
2868
3013
|
"data_uri" => ->(v, *_a) {
|
|
2869
3014
|
if v.is_a?(Hash)
|
|
2870
|
-
ct = v[:type] || v["type"] || "application/octet-stream"
|
|
3015
|
+
ct = Frond.sanitise_media_type(v[:type] || v["type"] || "application/octet-stream")
|
|
2871
3016
|
raw = v[:content] || v["content"] || ""
|
|
2872
3017
|
raw = raw.respond_to?(:read) ? raw.read : raw
|
|
2873
|
-
"data:#{ct};base64,#{Base64.strict_encode64(raw.to_s)}"
|
|
3018
|
+
Tina4::SafeString.new("data:#{ct};base64,#{Tina4::Base64.strict_encode64(raw.to_s)}")
|
|
2874
3019
|
else
|
|
2875
3020
|
v.to_s
|
|
2876
3021
|
end
|
|
@@ -2884,12 +3029,7 @@ module Tina4
|
|
|
2884
3029
|
# broke byte-parity for the one filter whose whole job is a wire format.
|
|
2885
3030
|
"to_json" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2886
3031
|
"tojson" => ->(v, *_a) { Frond.json_safe(v) },
|
|
2887
|
-
"js_escape" => ->(v, *_a) {
|
|
2888
|
-
Tina4::SafeString.new(
|
|
2889
|
-
v.to_s.gsub("\\", "\\\\").gsub("'", "\\'").gsub('"', '\\"')
|
|
2890
|
-
.gsub("\n", "\\n").gsub("\r", "\\r").gsub("\t", "\\t")
|
|
2891
|
-
)
|
|
2892
|
-
},
|
|
3032
|
+
"js_escape" => ->(v, *_a) { Tina4::SafeString.new(Frond.js_escape_str(v)) },
|
|
2893
3033
|
|
|
2894
3034
|
# -- Hashing --
|
|
2895
3035
|
"md5" => ->(v, *_a) { Digest::MD5.hexdigest(v.to_s) },
|
|
@@ -3118,9 +3258,11 @@ module Tina4
|
|
|
3118
3258
|
sid = form_token_session_id.to_s
|
|
3119
3259
|
payload["session_id"] = sid unless sid.empty?
|
|
3120
3260
|
|
|
3261
|
+
# get_token's expires_in is MINUTES, like every framework's. This used to
|
|
3262
|
+
# pass minutes * 60, so a form token lived 60 hours, not 60 minutes
|
|
3263
|
+
# (ADR-0079 s5).
|
|
3121
3264
|
ttl_minutes = (ENV["TINA4_TOKEN_LIMIT"] || "60").to_i
|
|
3122
|
-
expires_in
|
|
3123
|
-
Tina4::Auth.create_token(payload, expires_in: expires_in)
|
|
3265
|
+
Tina4::Auth.create_token(payload, expires_in: ttl_minutes)
|
|
3124
3266
|
end
|
|
3125
3267
|
|
|
3126
3268
|
def self.generate_form_token(descriptor = "")
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: Auth — JWT login with a visual demo page.
|
|
2
8
|
|
|
3
9
|
Tina4::Router.get("/gallery/auth") do |request, response|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: Database — raw SQL query demo.
|
|
2
8
|
|
|
3
9
|
Tina4::Router.get("/api/gallery/db/tables") do |request, response|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: Error Overlay — deliberately crash to demo the debug overlay.
|
|
2
8
|
#
|
|
3
9
|
# This route deliberately raises an error to showcase the error overlay.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: ORM — Product CRUD endpoints.
|
|
2
8
|
|
|
3
9
|
Tina4::Router.get("/api/gallery/products") do |request, response|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: Queue — interactive queue demo with visual web UI.
|
|
2
8
|
#
|
|
3
9
|
# Uses a SQLite database directly for the demo queue table,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: REST API — simple JSON endpoints.
|
|
2
8
|
|
|
3
9
|
Tina4::Router.get("/api/gallery/hello") do |request, response|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Copyright (c) 2026 Code Infinity
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
1
7
|
# Gallery: Templates — render an HTML page with dynamic data via template.
|
|
2
8
|
|
|
3
9
|
Tina4::Router.get "/gallery/page", template: "gallery_page.twig" do |request, response|
|
data/lib/tina4/graph.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
|
require "uri"
|
|
4
10
|
|