tina4ruby 3.0.0 → 3.9.2
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/README.md +120 -32
- data/lib/tina4/auth.rb +137 -27
- data/lib/tina4/auto_crud.rb +55 -3
- data/lib/tina4/cli.rb +228 -28
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database.rb +230 -26
- data/lib/tina4/database_result.rb +122 -8
- data/lib/tina4/dev_mailbox.rb +1 -1
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/frond.rb +314 -7
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +314 -16
- data/lib/tina4/localization.rb +1 -1
- data/lib/tina4/messenger.rb +111 -33
- data/lib/tina4/middleware.rb +349 -1
- data/lib/tina4/migration.rb +132 -11
- data/lib/tina4/orm.rb +149 -18
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
- data/lib/tina4/public/js/tina4js.min.js +47 -0
- data/lib/tina4/query_builder.rb +374 -0
- data/lib/tina4/queue.rb +219 -61
- data/lib/tina4/queue_backends/lite_backend.rb +42 -7
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
- data/lib/tina4/rack_app.rb +200 -11
- data/lib/tina4/request.rb +14 -1
- data/lib/tina4/response.rb +26 -0
- data/lib/tina4/response_cache.rb +446 -29
- data/lib/tina4/router.rb +127 -0
- data/lib/tina4/service_runner.rb +1 -1
- data/lib/tina4/session.rb +6 -1
- data/lib/tina4/session_handlers/database_handler.rb +66 -0
- data/lib/tina4/swagger.rb +1 -1
- data/lib/tina4/templates/errors/404.twig +2 -2
- data/lib/tina4/templates/errors/500.twig +1 -1
- data/lib/tina4/validator.rb +174 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +23 -4
- data/lib/tina4/websocket_backplane.rb +118 -0
- data/lib/tina4.rb +126 -5
- metadata +40 -3
|
@@ -5,12 +5,22 @@ module Tina4
|
|
|
5
5
|
class DatabaseResult
|
|
6
6
|
include Enumerable
|
|
7
7
|
|
|
8
|
-
attr_reader :records, :
|
|
8
|
+
attr_reader :records, :columns, :count, :limit, :offset, :sql,
|
|
9
|
+
:affected_rows, :last_id, :error
|
|
9
10
|
|
|
10
|
-
def initialize(records = [], sql: ""
|
|
11
|
+
def initialize(records = [], sql: "", columns: [], count: nil, limit: 10, offset: 0,
|
|
12
|
+
affected_rows: 0, last_id: nil, error: nil, db: nil)
|
|
11
13
|
@records = records || []
|
|
12
14
|
@sql = sql
|
|
13
|
-
@
|
|
15
|
+
@columns = columns.empty? && !@records.empty? ? @records.first.keys : columns
|
|
16
|
+
@count = count || @records.length
|
|
17
|
+
@limit = limit
|
|
18
|
+
@offset = offset
|
|
19
|
+
@affected_rows = affected_rows
|
|
20
|
+
@last_id = last_id
|
|
21
|
+
@error = error
|
|
22
|
+
@db = db
|
|
23
|
+
@column_info_cache = nil
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
def each(&block)
|
|
@@ -33,12 +43,26 @@ module Tina4
|
|
|
33
43
|
@records[index]
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
def length
|
|
47
|
+
@count
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def size
|
|
51
|
+
@count
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def success?
|
|
55
|
+
@error.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
36
58
|
def to_array
|
|
37
59
|
@records.map do |record|
|
|
38
60
|
record.is_a?(Hash) ? record : record.to_h
|
|
39
61
|
end
|
|
40
62
|
end
|
|
41
63
|
|
|
64
|
+
alias to_a to_array
|
|
65
|
+
|
|
42
66
|
def to_json(*_args)
|
|
43
67
|
JSON.generate(to_array)
|
|
44
68
|
end
|
|
@@ -54,11 +78,13 @@ module Tina4
|
|
|
54
78
|
lines.join("\n")
|
|
55
79
|
end
|
|
56
80
|
|
|
57
|
-
def to_paginate(page:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
def to_paginate(page: nil, per_page: nil)
|
|
82
|
+
per_page ||= @limit > 0 ? @limit : 10
|
|
83
|
+
page ||= @offset > 0 ? (@offset / per_page) + 1 : 1
|
|
84
|
+
total = @count
|
|
85
|
+
total_pages = [1, (total.to_f / per_page).ceil].max
|
|
86
|
+
slice_offset = (page - 1) * per_page
|
|
87
|
+
page_records = @records[slice_offset, per_page] || []
|
|
62
88
|
{
|
|
63
89
|
data: page_records,
|
|
64
90
|
page: page,
|
|
@@ -75,8 +101,96 @@ module Tina4
|
|
|
75
101
|
primary_key: primary_key, editable: editable)
|
|
76
102
|
end
|
|
77
103
|
|
|
104
|
+
# Return column metadata for the query's table.
|
|
105
|
+
#
|
|
106
|
+
# Lazy — only queries the database when explicitly called. Caches the
|
|
107
|
+
# result so subsequent calls return immediately without re-querying.
|
|
108
|
+
#
|
|
109
|
+
# Returns an array of hashes with keys:
|
|
110
|
+
# name, type, size, decimals, nullable, primary_key
|
|
111
|
+
def column_info
|
|
112
|
+
return @column_info_cache if @column_info_cache
|
|
113
|
+
|
|
114
|
+
table = extract_table_from_sql
|
|
115
|
+
|
|
116
|
+
if @db && table
|
|
117
|
+
begin
|
|
118
|
+
@column_info_cache = query_column_metadata(table)
|
|
119
|
+
return @column_info_cache
|
|
120
|
+
rescue StandardError
|
|
121
|
+
# Fall through to fallback
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@column_info_cache = fallback_column_info
|
|
126
|
+
@column_info_cache
|
|
127
|
+
end
|
|
128
|
+
|
|
78
129
|
private
|
|
79
130
|
|
|
131
|
+
def extract_table_from_sql
|
|
132
|
+
return nil if @sql.nil? || @sql.empty?
|
|
133
|
+
|
|
134
|
+
if (m = @sql.match(/\bFROM\s+["']?(\w+)["']?/i))
|
|
135
|
+
return m[1]
|
|
136
|
+
end
|
|
137
|
+
if (m = @sql.match(/\bINSERT\s+INTO\s+["']?(\w+)["']?/i))
|
|
138
|
+
return m[1]
|
|
139
|
+
end
|
|
140
|
+
if (m = @sql.match(/\bUPDATE\s+["']?(\w+)["']?/i))
|
|
141
|
+
return m[1]
|
|
142
|
+
end
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def query_column_metadata(table)
|
|
147
|
+
# Use the database's columns method which delegates to the driver
|
|
148
|
+
raw_cols = @db.columns(table)
|
|
149
|
+
normalize_columns(raw_cols)
|
|
150
|
+
rescue StandardError
|
|
151
|
+
fallback_column_info
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def normalize_columns(raw_cols)
|
|
155
|
+
raw_cols.map do |col|
|
|
156
|
+
col_type = (col[:type] || col["type"] || "UNKNOWN").to_s.upcase
|
|
157
|
+
size, decimals = parse_type_size(col_type)
|
|
158
|
+
{
|
|
159
|
+
name: (col[:name] || col["name"]).to_s,
|
|
160
|
+
type: col_type.sub(/\(.*\)/, ""),
|
|
161
|
+
size: size,
|
|
162
|
+
decimals: decimals,
|
|
163
|
+
nullable: col.key?(:nullable) ? col[:nullable] : (col.key?("nullable") ? col["nullable"] : true),
|
|
164
|
+
primary_key: col[:primary_key] || col["primary_key"] || col[:primary] || col["primary"] || false
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_type_size(type_str)
|
|
170
|
+
if (m = type_str.match(/\((\d+)(?:\s*,\s*(\d+))?\)/))
|
|
171
|
+
size = m[1].to_i
|
|
172
|
+
decimals = m[2] ? m[2].to_i : nil
|
|
173
|
+
[size, decimals]
|
|
174
|
+
else
|
|
175
|
+
[nil, nil]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def fallback_column_info
|
|
180
|
+
return [] if @records.empty?
|
|
181
|
+
keys = @records.first.is_a?(Hash) ? @records.first.keys : []
|
|
182
|
+
keys.map do |k|
|
|
183
|
+
{
|
|
184
|
+
name: k.to_s,
|
|
185
|
+
type: "UNKNOWN",
|
|
186
|
+
size: nil,
|
|
187
|
+
decimals: nil,
|
|
188
|
+
nullable: true,
|
|
189
|
+
primary_key: false
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
80
194
|
def escape_csv(value, separator)
|
|
81
195
|
str = value.to_s
|
|
82
196
|
if str.include?(separator) || str.include?('"') || str.include?("\n")
|
data/lib/tina4/dev_mailbox.rb
CHANGED
data/lib/tina4/env.rb
CHANGED
data/lib/tina4/frond.rb
CHANGED
|
@@ -56,21 +56,65 @@ module Tina4
|
|
|
56
56
|
# Fragment cache: key => [html, expires_at]
|
|
57
57
|
@fragment_cache = {}
|
|
58
58
|
|
|
59
|
+
# Token pre-compilation cache
|
|
60
|
+
@compiled = {} # {template_name => [tokens, mtime]}
|
|
61
|
+
@compiled_strings = {} # {md5_hash => tokens}
|
|
62
|
+
|
|
59
63
|
# Built-in global functions
|
|
60
64
|
register_builtin_globals
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
# Render a template file with data.
|
|
67
|
+
# Render a template file with data. Uses token caching for performance.
|
|
64
68
|
def render(template, data = {})
|
|
65
69
|
context = @globals.merge(stringify_keys(data))
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
|
|
71
|
+
path = File.join(@template_dir, template)
|
|
72
|
+
raise "Template not found: #{path}" unless File.exist?(path)
|
|
73
|
+
|
|
74
|
+
debug_mode = ENV.fetch("TINA4_DEBUG", "").downcase == "true"
|
|
75
|
+
cached = @compiled[template]
|
|
76
|
+
|
|
77
|
+
if cached
|
|
78
|
+
if debug_mode
|
|
79
|
+
# Dev mode: check if file changed
|
|
80
|
+
mtime = File.mtime(path)
|
|
81
|
+
if cached[1] == mtime
|
|
82
|
+
return execute_cached(cached[0], context)
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
# Production: skip mtime check, cache is permanent
|
|
86
|
+
return execute_cached(cached[0], context)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Cache miss — load, tokenize, cache
|
|
91
|
+
source = File.read(path, encoding: "utf-8")
|
|
92
|
+
mtime = File.mtime(path)
|
|
93
|
+
tokens = tokenize(source)
|
|
94
|
+
@compiled[template] = [tokens, mtime]
|
|
95
|
+
execute_with_tokens(source, tokens, context)
|
|
68
96
|
end
|
|
69
97
|
|
|
70
|
-
# Render a template string directly.
|
|
98
|
+
# Render a template string directly. Uses token caching for performance.
|
|
71
99
|
def render_string(source, data = {})
|
|
72
100
|
context = @globals.merge(stringify_keys(data))
|
|
73
|
-
|
|
101
|
+
|
|
102
|
+
key = Digest::MD5.hexdigest(source)
|
|
103
|
+
cached_tokens = @compiled_strings[key]
|
|
104
|
+
|
|
105
|
+
if cached_tokens
|
|
106
|
+
return execute_cached(cached_tokens, context)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
tokens = tokenize(source)
|
|
110
|
+
@compiled_strings[key] = tokens
|
|
111
|
+
execute_cached(tokens, context)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Clear all compiled template caches.
|
|
115
|
+
def clear_cache
|
|
116
|
+
@compiled.clear
|
|
117
|
+
@compiled_strings.clear
|
|
74
118
|
end
|
|
75
119
|
|
|
76
120
|
# Register a custom filter.
|
|
@@ -198,6 +242,35 @@ module Tina4
|
|
|
198
242
|
# Execution
|
|
199
243
|
# -----------------------------------------------------------------------
|
|
200
244
|
|
|
245
|
+
def execute_cached(tokens, context)
|
|
246
|
+
# Check if first non-text token is an extends block
|
|
247
|
+
tokens.each do |ttype, raw|
|
|
248
|
+
next if ttype == TEXT && raw.strip.empty?
|
|
249
|
+
if ttype == BLOCK
|
|
250
|
+
content, _, _ = strip_tag(raw)
|
|
251
|
+
if content.start_with?("extends ")
|
|
252
|
+
# Extends requires source-based execution for block extraction
|
|
253
|
+
source = tokens.map { |_, v| v }.join
|
|
254
|
+
return execute(source, context)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
break
|
|
258
|
+
end
|
|
259
|
+
render_tokens(tokens, context)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def execute_with_tokens(source, tokens, context)
|
|
263
|
+
# Handle extends first
|
|
264
|
+
if source =~ /\{%-?\s*extends\s+["'](.+?)["']\s*-?%\}/
|
|
265
|
+
parent_name = Regexp.last_match(1)
|
|
266
|
+
parent_source = load_template(parent_name)
|
|
267
|
+
child_blocks = extract_blocks(source)
|
|
268
|
+
return render_with_blocks(parent_source, context, child_blocks)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
render_tokens(tokens, context)
|
|
272
|
+
end
|
|
273
|
+
|
|
201
274
|
def execute(source, context)
|
|
202
275
|
# Handle extends first
|
|
203
276
|
if source =~ /\{%-?\s*extends\s+["'](.+?)["']\s*-?%\}/
|
|
@@ -290,6 +363,12 @@ module Tina4
|
|
|
290
363
|
when "cache"
|
|
291
364
|
result, i = handle_cache(tokens, i, context)
|
|
292
365
|
output << result
|
|
366
|
+
when "spaceless"
|
|
367
|
+
result, i = handle_spaceless(tokens, i, context)
|
|
368
|
+
output << result
|
|
369
|
+
when "autoescape"
|
|
370
|
+
result, i = handle_autoescape(tokens, i, context)
|
|
371
|
+
output << result
|
|
293
372
|
when "block", "endblock", "extends"
|
|
294
373
|
i += 1
|
|
295
374
|
else
|
|
@@ -312,6 +391,66 @@ module Tina4
|
|
|
312
391
|
# -----------------------------------------------------------------------
|
|
313
392
|
|
|
314
393
|
def eval_var(expr, context)
|
|
394
|
+
# Check for top-level ternary BEFORE splitting filters so that
|
|
395
|
+
# expressions like ``products|length != 1 ? "s" : ""`` work correctly.
|
|
396
|
+
ternary_pos = find_ternary(expr)
|
|
397
|
+
if ternary_pos != -1
|
|
398
|
+
cond_part = expr[0...ternary_pos].strip
|
|
399
|
+
rest = expr[(ternary_pos + 1)..]
|
|
400
|
+
colon_pos = find_colon(rest)
|
|
401
|
+
if colon_pos != -1
|
|
402
|
+
true_part = rest[0...colon_pos].strip
|
|
403
|
+
false_part = rest[(colon_pos + 1)..].strip
|
|
404
|
+
cond = eval_var_raw(cond_part, context)
|
|
405
|
+
return truthy?(cond) ? eval_var(true_part, context) : eval_var(false_part, context)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
eval_var_inner(expr, context)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def eval_var_raw(expr, context)
|
|
413
|
+
var_name, filters = parse_filter_chain(expr)
|
|
414
|
+
value = eval_expr(var_name, context)
|
|
415
|
+
filters.each do |fname, args|
|
|
416
|
+
next if fname == "raw" || fname == "safe"
|
|
417
|
+
fn = @filters[fname]
|
|
418
|
+
if fn
|
|
419
|
+
evaluated_args = args.map { |a| eval_filter_arg(a, context) }
|
|
420
|
+
value = fn.call(value, *evaluated_args)
|
|
421
|
+
else
|
|
422
|
+
# The filter name may include a trailing comparison operator,
|
|
423
|
+
# e.g. "length != 1". Extract the real filter name and the
|
|
424
|
+
# comparison suffix, apply the filter, then evaluate the comparison.
|
|
425
|
+
m = fname.match(/\A(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)\z/)
|
|
426
|
+
if m
|
|
427
|
+
real_filter = m[1]
|
|
428
|
+
op = m[2]
|
|
429
|
+
right_expr = m[3].strip
|
|
430
|
+
fn2 = @filters[real_filter]
|
|
431
|
+
if fn2
|
|
432
|
+
evaluated_args = args.map { |a| eval_filter_arg(a, context) }
|
|
433
|
+
value = fn2.call(value, *evaluated_args)
|
|
434
|
+
end
|
|
435
|
+
right = eval_expr(right_expr, context)
|
|
436
|
+
value = case op
|
|
437
|
+
when "!=" then value != right
|
|
438
|
+
when "==" then value == right
|
|
439
|
+
when ">=" then value >= right
|
|
440
|
+
when "<=" then value <= right
|
|
441
|
+
when ">" then value > right
|
|
442
|
+
when "<" then value < right
|
|
443
|
+
else false
|
|
444
|
+
end rescue false
|
|
445
|
+
else
|
|
446
|
+
value = eval_expr(fname, context)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
value
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def eval_var_inner(expr, context)
|
|
315
454
|
var_name, filters = parse_filter_chain(expr)
|
|
316
455
|
|
|
317
456
|
# Sandbox: check variable access
|
|
@@ -356,6 +495,68 @@ module Tina4
|
|
|
356
495
|
eval_expr(arg, context)
|
|
357
496
|
end
|
|
358
497
|
|
|
498
|
+
# Find the index of a top-level ``?`` that is part of a ternary operator.
|
|
499
|
+
# Respects quoted strings, parentheses, and skips ``??`` (null coalesce).
|
|
500
|
+
# Returns -1 if not found.
|
|
501
|
+
def find_ternary(expr)
|
|
502
|
+
depth = 0
|
|
503
|
+
in_quote = nil
|
|
504
|
+
i = 0
|
|
505
|
+
len = expr.length
|
|
506
|
+
while i < len
|
|
507
|
+
ch = expr[i]
|
|
508
|
+
if in_quote
|
|
509
|
+
in_quote = nil if ch == in_quote
|
|
510
|
+
i += 1
|
|
511
|
+
next
|
|
512
|
+
end
|
|
513
|
+
if ch == '"' || ch == "'"
|
|
514
|
+
in_quote = ch
|
|
515
|
+
i += 1
|
|
516
|
+
next
|
|
517
|
+
end
|
|
518
|
+
if ch == "("
|
|
519
|
+
depth += 1
|
|
520
|
+
elsif ch == ")"
|
|
521
|
+
depth -= 1
|
|
522
|
+
elsif ch == "?" && depth == 0
|
|
523
|
+
# Skip ``??`` (null coalesce)
|
|
524
|
+
if i + 1 < len && expr[i + 1] == "?"
|
|
525
|
+
i += 2
|
|
526
|
+
next
|
|
527
|
+
end
|
|
528
|
+
return i
|
|
529
|
+
end
|
|
530
|
+
i += 1
|
|
531
|
+
end
|
|
532
|
+
-1
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Find the index of the top-level ``:`` that separates the true/false
|
|
536
|
+
# branches of a ternary. Respects quotes and parentheses.
|
|
537
|
+
def find_colon(expr)
|
|
538
|
+
depth = 0
|
|
539
|
+
in_quote = nil
|
|
540
|
+
expr.each_char.with_index do |ch, i|
|
|
541
|
+
if in_quote
|
|
542
|
+
in_quote = nil if ch == in_quote
|
|
543
|
+
next
|
|
544
|
+
end
|
|
545
|
+
if ch == '"' || ch == "'"
|
|
546
|
+
in_quote = ch
|
|
547
|
+
next
|
|
548
|
+
end
|
|
549
|
+
if ch == "("
|
|
550
|
+
depth += 1
|
|
551
|
+
elsif ch == ")"
|
|
552
|
+
depth -= 1
|
|
553
|
+
elsif ch == ":" && depth == 0
|
|
554
|
+
return i
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
-1
|
|
558
|
+
end
|
|
559
|
+
|
|
359
560
|
# -----------------------------------------------------------------------
|
|
360
561
|
# Filter chain parser
|
|
361
562
|
# -----------------------------------------------------------------------
|
|
@@ -496,6 +697,13 @@ module Tina4
|
|
|
496
697
|
return truthy?(cond) ? eval_expr(ternary[2], context) : eval_expr(ternary[3], context)
|
|
497
698
|
end
|
|
498
699
|
|
|
700
|
+
# Jinja2-style inline if: value if condition else other_value
|
|
701
|
+
inline_if = expr.match(/\A(.+?)\s+if\s+(.+?)\s+else\s+(.+)\z/)
|
|
702
|
+
if inline_if
|
|
703
|
+
cond = eval_expr(inline_if[2], context)
|
|
704
|
+
return truthy?(cond) ? eval_expr(inline_if[1], context) : eval_expr(inline_if[3], context)
|
|
705
|
+
end
|
|
706
|
+
|
|
499
707
|
# Null coalescing: value ?? "default"
|
|
500
708
|
if expr.include?("??")
|
|
501
709
|
left, _, right = expr.partition("??")
|
|
@@ -1121,6 +1329,81 @@ module Tina4
|
|
|
1121
1329
|
[rendered, i]
|
|
1122
1330
|
end
|
|
1123
1331
|
|
|
1332
|
+
def handle_spaceless(tokens, start, context)
|
|
1333
|
+
body_tokens = []
|
|
1334
|
+
i = start + 1
|
|
1335
|
+
depth = 0
|
|
1336
|
+
while i < tokens.length
|
|
1337
|
+
if tokens[i][0] == BLOCK
|
|
1338
|
+
tc, _, _ = strip_tag(tokens[i][1])
|
|
1339
|
+
tag = tc.split[0] || ""
|
|
1340
|
+
if tag == "spaceless"
|
|
1341
|
+
depth += 1
|
|
1342
|
+
body_tokens << tokens[i]
|
|
1343
|
+
elsif tag == "endspaceless"
|
|
1344
|
+
if depth == 0
|
|
1345
|
+
i += 1
|
|
1346
|
+
break
|
|
1347
|
+
end
|
|
1348
|
+
depth -= 1
|
|
1349
|
+
body_tokens << tokens[i]
|
|
1350
|
+
else
|
|
1351
|
+
body_tokens << tokens[i]
|
|
1352
|
+
end
|
|
1353
|
+
else
|
|
1354
|
+
body_tokens << tokens[i]
|
|
1355
|
+
end
|
|
1356
|
+
i += 1
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
rendered = render_tokens(body_tokens.dup, context)
|
|
1360
|
+
rendered = rendered.gsub(/>\s+</, "><")
|
|
1361
|
+
[rendered, i]
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
def handle_autoescape(tokens, start, context)
|
|
1365
|
+
content, _, _ = strip_tag(tokens[start][1])
|
|
1366
|
+
mode_match = content.match(/\Aautoescape\s+(false|true)/)
|
|
1367
|
+
auto_escape_on = !(mode_match && mode_match[1] == "false")
|
|
1368
|
+
|
|
1369
|
+
body_tokens = []
|
|
1370
|
+
i = start + 1
|
|
1371
|
+
depth = 0
|
|
1372
|
+
while i < tokens.length
|
|
1373
|
+
if tokens[i][0] == BLOCK
|
|
1374
|
+
tc, _, _ = strip_tag(tokens[i][1])
|
|
1375
|
+
tag = tc.split[0] || ""
|
|
1376
|
+
if tag == "autoescape"
|
|
1377
|
+
depth += 1
|
|
1378
|
+
body_tokens << tokens[i]
|
|
1379
|
+
elsif tag == "endautoescape"
|
|
1380
|
+
if depth == 0
|
|
1381
|
+
i += 1
|
|
1382
|
+
break
|
|
1383
|
+
end
|
|
1384
|
+
depth -= 1
|
|
1385
|
+
body_tokens << tokens[i]
|
|
1386
|
+
else
|
|
1387
|
+
body_tokens << tokens[i]
|
|
1388
|
+
end
|
|
1389
|
+
else
|
|
1390
|
+
body_tokens << tokens[i]
|
|
1391
|
+
end
|
|
1392
|
+
i += 1
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
if !auto_escape_on
|
|
1396
|
+
old_auto_escape = @auto_escape
|
|
1397
|
+
@auto_escape = false
|
|
1398
|
+
rendered = render_tokens(body_tokens.dup, context)
|
|
1399
|
+
@auto_escape = old_auto_escape
|
|
1400
|
+
else
|
|
1401
|
+
rendered = render_tokens(body_tokens.dup, context)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
[rendered, i]
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1124
1407
|
# -----------------------------------------------------------------------
|
|
1125
1408
|
# Helpers
|
|
1126
1409
|
# -----------------------------------------------------------------------
|
|
@@ -1160,8 +1443,20 @@ module Tina4
|
|
|
1160
1443
|
"safe" => ->(v, *_a) { v },
|
|
1161
1444
|
"json_encode" => ->(v, *_a) { JSON.generate(v) rescue v.to_s },
|
|
1162
1445
|
"json_decode" => ->(v, *_a) { v.is_a?(String) ? (JSON.parse(v) rescue v) : v },
|
|
1163
|
-
"base64_encode" => ->(v, *_a) { Base64.strict_encode64(v.to_s) },
|
|
1446
|
+
"base64_encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
1447
|
+
"base64encode" => ->(v, *_a) { Base64.strict_encode64(v.is_a?(String) ? v : v.to_s) },
|
|
1164
1448
|
"base64_decode" => ->(v, *_a) { Base64.decode64(v.to_s) },
|
|
1449
|
+
"base64decode" => ->(v, *_a) { Base64.decode64(v.to_s) },
|
|
1450
|
+
"data_uri" => ->(v, *_a) {
|
|
1451
|
+
if v.is_a?(Hash)
|
|
1452
|
+
ct = v[:type] || v["type"] || "application/octet-stream"
|
|
1453
|
+
raw = v[:content] || v["content"] || ""
|
|
1454
|
+
raw = raw.respond_to?(:read) ? raw.read : raw
|
|
1455
|
+
"data:#{ct};base64,#{Base64.strict_encode64(raw.to_s)}"
|
|
1456
|
+
else
|
|
1457
|
+
v.to_s
|
|
1458
|
+
end
|
|
1459
|
+
},
|
|
1165
1460
|
"url_encode" => ->(v, *_a) { CGI.escape(v.to_s) },
|
|
1166
1461
|
|
|
1167
1462
|
# -- Hashing --
|
|
@@ -1312,6 +1607,14 @@ module Tina4
|
|
|
1312
1607
|
# - "checkout|order_123": payload is {"type" => "form", "context" => "checkout", "ref" => "order_123"}
|
|
1313
1608
|
#
|
|
1314
1609
|
# @return [String] <input type="hidden" name="formToken" value="TOKEN">
|
|
1610
|
+
# Session ID used by generate_form_token for CSRF session binding.
|
|
1611
|
+
# Set this before rendering templates to bind tokens to the current session.
|
|
1612
|
+
@form_token_session_id = ""
|
|
1613
|
+
|
|
1614
|
+
class << self
|
|
1615
|
+
attr_accessor :form_token_session_id
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1315
1618
|
def self.generate_form_token(descriptor = "")
|
|
1316
1619
|
require_relative "log"
|
|
1317
1620
|
require_relative "auth"
|
|
@@ -1327,7 +1630,11 @@ module Tina4
|
|
|
1327
1630
|
end
|
|
1328
1631
|
end
|
|
1329
1632
|
|
|
1330
|
-
|
|
1633
|
+
# Include session_id for CSRF session binding
|
|
1634
|
+
sid = form_token_session_id.to_s
|
|
1635
|
+
payload["session_id"] = sid unless sid.empty?
|
|
1636
|
+
|
|
1637
|
+
ttl_minutes = (ENV["TINA4_TOKEN_LIMIT"] || "60").to_i
|
|
1331
1638
|
expires_in = ttl_minutes * 60
|
|
1332
1639
|
token = Tina4::Auth.create_token(payload, expires_in: expires_in)
|
|
1333
1640
|
Tina4::SafeString.new(%(<input type="hidden" name="formToken" value="#{CGI.escapeHTML(token)}">))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "Queue", "description": "Background job producer and consumer", "try_url": "/
|
|
1
|
+
{"name": "Queue", "description": "Background job producer and consumer", "try_url": "/gallery/queue"}
|