actionview 7.0.1 → 7.1.1

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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +281 -202
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +693 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +33 -12
  8. data/lib/action_view/buffers.rb +106 -8
  9. data/lib/action_view/cache_expiry.rb +40 -43
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/deprecator.rb +7 -0
  12. data/lib/action_view/digestor.rb +1 -1
  13. data/lib/action_view/gem_version.rb +2 -2
  14. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  15. data/lib/action_view/helpers/asset_tag_helper.rb +133 -48
  16. data/lib/action_view/helpers/asset_url_helper.rb +13 -12
  17. data/lib/action_view/helpers/atom_feed_helper.rb +5 -5
  18. data/lib/action_view/helpers/cache_helper.rb +3 -9
  19. data/lib/action_view/helpers/capture_helper.rb +26 -12
  20. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  21. data/lib/action_view/helpers/controller_helper.rb +6 -0
  22. data/lib/action_view/helpers/csp_helper.rb +2 -2
  23. data/lib/action_view/helpers/csrf_helper.rb +3 -3
  24. data/lib/action_view/helpers/date_helper.rb +76 -64
  25. data/lib/action_view/helpers/debug_helper.rb +3 -3
  26. data/lib/action_view/helpers/form_helper.rb +62 -31
  27. data/lib/action_view/helpers/form_options_helper.rb +6 -3
  28. data/lib/action_view/helpers/form_tag_helper.rb +88 -44
  29. data/lib/action_view/helpers/javascript_helper.rb +1 -0
  30. data/lib/action_view/helpers/number_helper.rb +15 -13
  31. data/lib/action_view/helpers/output_safety_helper.rb +4 -4
  32. data/lib/action_view/helpers/rendering_helper.rb +5 -6
  33. data/lib/action_view/helpers/sanitize_helper.rb +34 -15
  34. data/lib/action_view/helpers/tag_helper.rb +27 -16
  35. data/lib/action_view/helpers/tags/base.rb +11 -52
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  37. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  38. data/lib/action_view/helpers/tags/collection_select.rb +3 -0
  39. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  40. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  41. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  42. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  43. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  44. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/select.rb +4 -1
  46. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  47. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  48. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  49. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  50. data/lib/action_view/helpers/tags/weekday_select.rb +3 -0
  51. data/lib/action_view/helpers/tags.rb +2 -0
  52. data/lib/action_view/helpers/text_helper.rb +33 -17
  53. data/lib/action_view/helpers/translation_helper.rb +6 -6
  54. data/lib/action_view/helpers/url_helper.rb +90 -65
  55. data/lib/action_view/helpers.rb +2 -0
  56. data/lib/action_view/layouts.rb +13 -8
  57. data/lib/action_view/log_subscriber.rb +49 -32
  58. data/lib/action_view/lookup_context.rb +29 -13
  59. data/lib/action_view/path_registry.rb +57 -0
  60. data/lib/action_view/path_set.rb +13 -14
  61. data/lib/action_view/railtie.rb +26 -3
  62. data/lib/action_view/record_identifier.rb +16 -9
  63. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  64. data/lib/action_view/renderer/collection_renderer.rb +9 -1
  65. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  66. data/lib/action_view/renderer/partial_renderer.rb +3 -2
  67. data/lib/action_view/renderer/renderer.rb +2 -0
  68. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -2
  69. data/lib/action_view/renderer/template_renderer.rb +3 -2
  70. data/lib/action_view/rendering.rb +24 -6
  71. data/lib/action_view/ripper_ast_parser.rb +6 -6
  72. data/lib/action_view/routing_url_for.rb +7 -4
  73. data/lib/action_view/template/error.rb +14 -1
  74. data/lib/action_view/template/handlers/builder.rb +4 -4
  75. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  76. data/lib/action_view/template/handlers/erb.rb +73 -1
  77. data/lib/action_view/template/handlers.rb +1 -1
  78. data/lib/action_view/template/html.rb +1 -1
  79. data/lib/action_view/template/raw_file.rb +1 -1
  80. data/lib/action_view/template/renderable.rb +1 -1
  81. data/lib/action_view/template/resolver.rb +15 -5
  82. data/lib/action_view/template/text.rb +1 -1
  83. data/lib/action_view/template/types.rb +25 -34
  84. data/lib/action_view/template.rb +227 -53
  85. data/lib/action_view/template_path.rb +2 -0
  86. data/lib/action_view/test_case.rb +174 -21
  87. data/lib/action_view/unbound_template.rb +15 -5
  88. data/lib/action_view/version.rb +1 -1
  89. data/lib/action_view/view_paths.rb +19 -28
  90. data/lib/action_view.rb +4 -1
  91. data/lib/assets/compiled/rails-ujs.js +36 -5
  92. metadata +27 -27
@@ -4,18 +4,20 @@ require "thread"
4
4
  require "delegate"
5
5
 
6
6
  module ActionView
7
- # = Action View Template
7
+ # = Action View \Template
8
8
  class Template
9
9
  extend ActiveSupport::Autoload
10
10
 
11
+ STRICT_LOCALS_REGEX = /\#\s+locals:\s+\((.*)\)/
12
+
11
13
  # === Encodings in ActionView::Template
12
14
  #
13
15
  # ActionView::Template is one of a few sources of potential
14
- # encoding issues in Rails. This is because the source for
16
+ # encoding issues in \Rails. This is because the source for
15
17
  # templates are usually read from disk, and Ruby (like most
16
18
  # encoding-aware programming languages) assumes that the
17
19
  # String retrieved through File IO is encoded in the
18
- # <tt>default_external</tt> encoding. In Rails, the default
20
+ # <tt>default_external</tt> encoding. In \Rails, the default
19
21
  # <tt>default_external</tt> encoding is UTF-8.
20
22
  #
21
23
  # As a result, if a user saves their template as ISO-8859-1
@@ -34,13 +36,13 @@ module ActionView
34
36
  # to the problem.
35
37
  # 2. The user can specify the encoding using Ruby-style
36
38
  # encoding comments in any template engine. If such
37
- # a comment is supplied, Rails will apply that encoding
39
+ # a comment is supplied, \Rails will apply that encoding
38
40
  # to the resulting compiled source returned by the
39
41
  # template handler.
40
42
  # 3. In all cases, we transcode the resulting String to
41
43
  # the UTF-8.
42
44
  #
43
- # This means that other parts of Rails can always assume
45
+ # This means that other parts of \Rails can always assume
44
46
  # that templates are encoded in UTF-8, even if the original
45
47
  # source of the template was not UTF-8.
46
48
  #
@@ -51,7 +53,7 @@ module ActionView
51
53
  # === Instructions for template handlers
52
54
  #
53
55
  # The easiest thing for you to do is to simply ignore
54
- # encodings. Rails will hand you the template source
56
+ # encodings. \Rails will hand you the template source
55
57
  # as the default_internal (generally UTF-8), raising
56
58
  # an exception for the user before sending the template
57
59
  # to you if it could not determine the original encoding.
@@ -68,7 +70,7 @@ module ActionView
68
70
  # you may indicate that you will handle encodings yourself
69
71
  # by implementing <tt>handles_encoding?</tt> on your handler.
70
72
  #
71
- # If you do, Rails will not try to encode the String
73
+ # If you do, \Rails will not try to encode the String
72
74
  # into the default_internal, passing you the unaltered
73
75
  # bytes tagged with the assumed encoding (from
74
76
  # default_external).
@@ -94,11 +96,58 @@ module ActionView
94
96
  #
95
97
  # Given this sub template rendering:
96
98
  #
97
- # <%= render "shared/header", { headline: "Welcome", person: person } %>
99
+ # <%= render "application/header", { headline: "Welcome", person: person } %>
98
100
  #
99
101
  # You can use +local_assigns+ in the sub templates to access the local variables:
100
102
  #
101
103
  # local_assigns[:headline] # => "Welcome"
104
+ #
105
+ # Each key in +local_assigns+ is available as a partial-local variable:
106
+ #
107
+ # local_assigns[:headline] # => "Welcome"
108
+ # headline # => "Welcome"
109
+ #
110
+ # Since +local_assigns+ is a +Hash+, it's compatible with Ruby 3.1's pattern
111
+ # matching assignment operator:
112
+ #
113
+ # local_assigns => { headline:, **options }
114
+ # headline # => "Welcome"
115
+ # options # => {}
116
+ #
117
+ # Pattern matching assignment also supports variable renaming:
118
+ #
119
+ # local_assigns => { headline: title }
120
+ # title # => "Welcome"
121
+ #
122
+ # If a template refers to a variable that isn't passed into the view as part
123
+ # of the <tt>locals: { ... }</tt> Hash, the template will raise an
124
+ # +ActionView::Template::Error+:
125
+ #
126
+ # <%# => raises ActionView::Template::Error %>
127
+ # <% alerts.each do |alert| %>
128
+ # <p><%= alert %></p>
129
+ # <% end %>
130
+ #
131
+ # Since +local_assigns+ returns a +Hash+ instance, you can conditionally
132
+ # read a variable, then fall back to a default value when
133
+ # the key isn't part of the <tt>locals: { ... }</tt> options:
134
+ #
135
+ # <% local_assigns.fetch(:alerts, []).each do |alert| %>
136
+ # <p><%= alert %></p>
137
+ # <% end %>
138
+ #
139
+ # Combining Ruby 3.1's pattern matching assignment with calls to
140
+ # +Hash#with_defaults+ enables compact partial-local variable
141
+ # assignments:
142
+ #
143
+ # <% local_assigns.with_defaults(alerts: []) => { headline:, alerts: } %>
144
+ #
145
+ # <h1><%= headline %></h1>
146
+ #
147
+ # <% alerts.each do |alert| %>
148
+ # <p><%= alert %></p>
149
+ # <% end %>
150
+ #
102
151
 
103
152
  eager_autoload do
104
153
  autoload :Error
@@ -107,6 +156,7 @@ module ActionView
107
156
  autoload :Handlers
108
157
  autoload :HTML
109
158
  autoload :Inline
159
+ autoload :Types
110
160
  autoload :Sources
111
161
  autoload :Text
112
162
  autoload :Types
@@ -117,11 +167,24 @@ module ActionView
117
167
  singleton_class.attr_accessor :frozen_string_literal
118
168
  @frozen_string_literal = false
119
169
 
170
+ class << self # :nodoc:
171
+ def mime_types_implementation=(implementation)
172
+ # This method isn't thread-safe, but it's not supposed
173
+ # to be called after initialization
174
+ if self::Types != implementation
175
+ remove_const(:Types)
176
+ const_set(:Types, implementation)
177
+ end
178
+ end
179
+ end
180
+
120
181
  attr_reader :identifier, :handler
121
- attr_reader :variable, :format, :variant, :locals, :virtual_path
182
+ attr_reader :variable, :format, :variant, :virtual_path
183
+
184
+ NONE = Object.new
122
185
 
123
186
  def initialize(source, identifier, handler, locals:, format: nil, variant: nil, virtual_path: nil)
124
- @source = source
187
+ @source = source.dup
125
188
  @identifier = identifier
126
189
  @handler = handler
127
190
  @compiled = false
@@ -137,6 +200,36 @@ module ActionView
137
200
  @format = format
138
201
  @variant = variant
139
202
  @compile_mutex = Mutex.new
203
+ @strict_locals = NONE
204
+ @type = nil
205
+ end
206
+
207
+ # The locals this template has been or will be compiled for, or nil if this
208
+ # is a strict locals template.
209
+ def locals
210
+ if strict_locals?
211
+ nil
212
+ else
213
+ @locals
214
+ end
215
+ end
216
+
217
+ def spot(location) # :nodoc:
218
+ ast = RubyVM::AbstractSyntaxTree.parse(compiled_source, keep_script_lines: true)
219
+ node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
220
+ node = find_node_by_id(ast, node_id)
221
+
222
+ ErrorHighlight.spot(node)
223
+ end
224
+
225
+ # Translate an error location returned by ErrorHighlight to the correct
226
+ # source location inside the template.
227
+ def translate_location(backtrace_location, spot)
228
+ if handler.respond_to?(:translate_location)
229
+ handler.translate_location(spot, backtrace_location, encode!) || spot
230
+ else
231
+ spot
232
+ end
140
233
  end
141
234
 
142
235
  # Returns whether the underlying handler supports streaming. If so,
@@ -151,10 +244,15 @@ module ActionView
151
244
  # This method is instrumented as "!render_template.action_view". Notice that
152
245
  # we use a bang in this instrumentation because you don't want to
153
246
  # consume this in production. This is only slow if it's being listened to.
154
- def render(view, locals, buffer = ActionView::OutputBuffer.new, add_to_stack: true, &block)
247
+ def render(view, locals, buffer = nil, add_to_stack: true, &block)
155
248
  instrument_render_template do
156
249
  compile!(view)
157
- view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, &block)
250
+ if buffer
251
+ view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, has_strict_locals: strict_locals?, &block)
252
+ nil
253
+ else
254
+ view._run(method_name, self, locals, OutputBuffer.new, add_to_stack: add_to_stack, has_strict_locals: strict_locals?, &block)&.to_s
255
+ end
158
256
  end
159
257
  rescue => e
160
258
  handle_render_error(view, e)
@@ -169,13 +267,16 @@ module ActionView
169
267
  end
170
268
 
171
269
  def inspect
172
- "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
270
+ "#<#{self.class.name} #{short_identifier} locals=#{locals.inspect}>"
173
271
  end
174
272
 
175
273
  def source
176
274
  @source.to_s
177
275
  end
178
276
 
277
+ LEADING_ENCODING_REGEXP = /\A#{ENCODING_FLAG}/
278
+ private_constant :LEADING_ENCODING_REGEXP
279
+
179
280
  # This method is responsible for properly setting the encoding of the
180
281
  # source. Until this point, we assume that the source is BINARY data.
181
282
  # If no additional information is supplied, we assume the encoding is
@@ -194,7 +295,7 @@ module ActionView
194
295
  # Look for # encoding: *. If we find one, we'll encode the
195
296
  # String in that encoding, otherwise, we'll use the
196
297
  # default external encoding.
197
- if source.sub!(/\A#{ENCODING_FLAG}/, "")
298
+ if source.sub!(LEADING_ENCODING_REGEXP, "")
198
299
  encoding = magic_encoding = $1
199
300
  else
200
301
  encoding = Encoding.default_external
@@ -222,6 +323,32 @@ module ActionView
222
323
  end
223
324
  end
224
325
 
326
+ # This method is responsible for marking a template as having strict locals
327
+ # which means the template can only accept the locals defined in a magic
328
+ # comment. For example, if your template acceps the locals +title+ and
329
+ # +comment_count+, add the following to your template file:
330
+ #
331
+ # <%# locals: (title: "Default title", comment_count: 0) %>
332
+ #
333
+ # Strict locals are useful for validating template arguments and for
334
+ # specifying defaults.
335
+ def strict_locals!
336
+ if @strict_locals == NONE
337
+ self.source.sub!(STRICT_LOCALS_REGEX, "")
338
+ @strict_locals = $1
339
+
340
+ return if @strict_locals.nil? # Magic comment not found
341
+
342
+ @strict_locals = "**nil" if @strict_locals.blank?
343
+ end
344
+
345
+ @strict_locals
346
+ end
347
+
348
+ # Returns whether a template is using strict locals.
349
+ def strict_locals?
350
+ strict_locals!
351
+ end
225
352
 
226
353
  # Exceptions are marshalled when using the parallel test runner with DRb, so we need
227
354
  # to ensure that references to the template object can be marshalled as well. This means forgoing
@@ -235,7 +362,26 @@ module ActionView
235
362
  @compile_mutex = Mutex.new
236
363
  end
237
364
 
365
+ def method_name # :nodoc:
366
+ @method_name ||= begin
367
+ m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
368
+ m.tr!("-", "_")
369
+ m
370
+ end
371
+ end
372
+
238
373
  private
374
+ def find_node_by_id(node, node_id)
375
+ return node if node.node_id == node_id
376
+
377
+ node.children.grep(node.class).each do |child|
378
+ found = find_node_by_id(child, node_id)
379
+ return found if found
380
+ end
381
+
382
+ false
383
+ end
384
+
239
385
  # Compile a template. This method ensures a template is compiled
240
386
  # just once and removes the source after it is compiled.
241
387
  def compile!(view)
@@ -260,27 +406,25 @@ module ActionView
260
406
  end
261
407
  end
262
408
 
263
- # Among other things, this method is responsible for properly setting
264
- # the encoding of the compiled template.
265
- #
266
- # If the template engine handles encodings, we send the encoded
267
- # String to the engine without further processing. This allows
268
- # the template engine to support additional mechanisms for
269
- # specifying the encoding. For instance, ERB supports <%# encoding: %>
270
- #
271
- # Otherwise, after we figure out the correct encoding, we then
272
- # encode the source into <tt>Encoding.default_internal</tt>.
273
- # In general, this means that templates will be UTF-8 inside of Rails,
274
- # regardless of the original source encoding.
275
- def compile(mod)
409
+ # This method compiles the source of the template. The compilation of templates
410
+ # involves setting strict_locals! if applicable, encoding the template, and setting
411
+ # frozen string literal.
412
+ def compiled_source
413
+ set_strict_locals = strict_locals!
276
414
  source = encode!
277
415
  code = @handler.call(self, source)
278
416
 
417
+ method_arguments =
418
+ if set_strict_locals
419
+ "output_buffer, #{set_strict_locals}"
420
+ else
421
+ "local_assigns, output_buffer"
422
+ end
423
+
279
424
  # Make sure that the resulting String to be eval'd is in the
280
425
  # encoding of the code
281
- original_source = source
282
426
  source = +<<-end_src
283
- def #{method_name}(local_assigns, output_buffer)
427
+ def #{method_name}(#{method_arguments})
284
428
  @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
285
429
  end
286
430
  end_src
@@ -299,17 +443,61 @@ module ActionView
299
443
  raise WrongEncodingError.new(source, Encoding.default_internal)
300
444
  end
301
445
 
446
+ if Template.frozen_string_literal
447
+ "# frozen_string_literal: true\n#{source}"
448
+ else
449
+ source
450
+ end
451
+ end
452
+
453
+ # Among other things, this method is responsible for properly setting
454
+ # the encoding of the compiled template.
455
+ #
456
+ # If the template engine handles encodings, we send the encoded
457
+ # String to the engine without further processing. This allows
458
+ # the template engine to support additional mechanisms for
459
+ # specifying the encoding. For instance, ERB supports <%# encoding: %>
460
+ #
461
+ # Otherwise, after we figure out the correct encoding, we then
462
+ # encode the source into <tt>Encoding.default_internal</tt>.
463
+ # In general, this means that templates will be UTF-8 inside of Rails,
464
+ # regardless of the original source encoding.
465
+ def compile(mod)
302
466
  begin
303
- if Template.frozen_string_literal
304
- mod.module_eval("# frozen_string_literal: true\n#{source}", identifier, -1)
305
- else
306
- mod.module_eval(source, identifier, 0)
307
- end
467
+ mod.module_eval(compiled_source, identifier, offset)
308
468
  rescue SyntaxError
309
469
  # Account for when code in the template is not syntactically valid; e.g. if we're using
310
470
  # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
311
471
  # the result into the template, but missing an end parenthesis.
312
- raise SyntaxErrorInTemplate.new(self, original_source)
472
+ raise SyntaxErrorInTemplate.new(self, encode!)
473
+ end
474
+
475
+ return unless strict_locals?
476
+
477
+ # Check compiled method parameters to ensure that only kwargs
478
+ # were provided as strict locals, preventing `locals: (foo, *foo)` etc
479
+ # and allowing `locals: (foo:)`.
480
+
481
+ non_kwarg_parameters =
482
+ (mod.instance_method(method_name).parameters - [[:req, :output_buffer]]).
483
+ select { |parameter| ![:keyreq, :key, :keyrest, :nokey].include?(parameter[0]) }
484
+
485
+ return unless non_kwarg_parameters.any?
486
+
487
+ mod.undef_method(method_name)
488
+
489
+ raise ArgumentError.new(
490
+ "#{non_kwarg_parameters.map { |_, name| "`#{name}`" }.to_sentence} set as non-keyword " \
491
+ "#{'argument'.pluralize(non_kwarg_parameters.length)} for #{short_identifier}. " \
492
+ "Locals can only be set as keyword arguments."
493
+ )
494
+ end
495
+
496
+ def offset
497
+ if Template.frozen_string_literal
498
+ -1
499
+ else
500
+ 0
313
501
  end
314
502
  end
315
503
 
@@ -323,32 +511,18 @@ module ActionView
323
511
  end
324
512
 
325
513
  def locals_code
514
+ return "" if strict_locals?
515
+
326
516
  # Only locals with valid variable names get set directly. Others will
327
517
  # still be available in local_assigns.
328
518
  locals = @locals - Module::RUBY_RESERVED_KEYWORDS
329
- deprecated_locals = locals.grep(/\A@+/)
330
- if deprecated_locals.any?
331
- ActiveSupport::Deprecation.warn(<<~MSG)
332
- Passing instance variables to `render` is deprecated.
333
- In Rails 7.1, #{deprecated_locals.to_sentence} will be ignored.
334
- MSG
335
- locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
336
- else
337
- locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
338
- end
519
+
520
+ locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
339
521
 
340
522
  # Assign for the same variable is to suppress unused variable warning
341
523
  locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
342
524
  end
343
525
 
344
- def method_name
345
- @method_name ||= begin
346
- m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
347
- m.tr!("-", "_")
348
- m
349
- end
350
- end
351
-
352
526
  def identifier_method_name
353
527
  short_identifier.tr("^a-z_", "_")
354
528
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
+ # = Action View \TemplatePath
5
+ #
4
6
  # Represents a template path within ActionView's lookup and rendering system,
5
7
  # like "users/show"
6
8
  #