view_component 3.0.0.rc2 → 3.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of view_component might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2171ee963d59844894858309db964f092fb423bc4828a3bccbc917608502278
4
- data.tar.gz: aaf7c734530bf46ea0fa5e7fdad4c6efaf7325eb45970fa2bea56d6c9f9a15c1
3
+ metadata.gz: f88375c08b20eb604992d315362fbdf6e0654fd87d7766d1ac404daecb1b57c9
4
+ data.tar.gz: 5f37ce63b609f5ff542a7e7c4629c952e3a02fc4e2ffc83df28aee86ff5d40cd
5
5
  SHA512:
6
- metadata.gz: 5f26a04b1af1b93be44db4436d57544c182c64a1d1036a2db62cecf0bc8b12da3f7ccf11e7cb8bf88c9c915495686ddcd774cca13a5adf7353dfc678d6c7ead1
7
- data.tar.gz: a982b51ed80dc7da05da996005b2e60ca7af576a91c5ef13cdd68698735635fb42834e7cc45d23e5cbf45326b4ab8d8b85a1e6ddb7b2967a6d1e45638c73d50a
6
+ metadata.gz: 425107ab124527b5199b4174437c17a0f2dae4447b77a9393f68dd8c7a88517e25a3100fa0ed4cf6190d7e3b38390c6fd5cf48718753a53169f72dfe50ae5d12
7
+ data.tar.gz: b1642a727d8a586d8dec7a2e7075b570f5ccb20cee0b3c41037ac0b119adc6261ca1d1fb1dea0558f4bb40a0a45ba73b111ce4816a85be6798e3fb2bfaf76daf
@@ -1,7 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
4
+ TEMP_DIR = FileUtils.mkdir_p("./tmp/view_components/").first
5
+
6
+ before_action :validate_test_env
7
+ before_action :validate_file_path
8
+
4
9
  def system_test_entrypoint
5
- render file: "./tmp/view_components/#{params.permit(:file)[:file]}"
10
+ render file: @path
11
+ end
12
+
13
+ private
14
+
15
+ def validate_test_env
16
+ raise "ViewComponentsSystemTestController must only be called in a test environment" unless Rails.env.test?
17
+ end
18
+
19
+ # Ensure that the file path is valid and doesn't target files outside
20
+ # the expected directory (e.g. via a path traversal or symlink attack)
21
+ def validate_file_path
22
+ base_path = ::File.realpath(TEMP_DIR)
23
+ @path = ::File.realpath(params.permit(:file)[:file], base_path)
24
+ unless @path.start_with?(base_path)
25
+ raise ArgumentError, "Invalid file path"
26
+ end
6
27
  end
7
28
  end
data/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,34 @@ nav_order: 5
10
10
 
11
11
  ## main
12
12
 
13
+ ## v3.0.0.rc3
14
+
15
+ Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
16
+
17
+ * Fix typos in generator docs.
18
+
19
+ *Sascha Karnatz*
20
+
21
+ * Add `TestHelpers#vc_test_controller`.
22
+
23
+ *Joel Hawksley*
24
+
25
+ * Document `config.view_component.capture_compatibility_patch_enabled` as option for the known incompatibilities with Rails form helpers.
26
+
27
+ *Tobias L. Maier*
28
+
29
+ * Add support for experimental inline templates.
30
+
31
+ *Blake Williams*
32
+
33
+ * Expose `translate` and `t` I18n methods on component classes.
34
+
35
+ *Elia Schito*
36
+
37
+ * Protect against Arbitrary File Read edge case in `ViewComponentsSystemTestController`.
38
+
39
+ *Nick Malcolm*
40
+
13
41
  ## v3.0.0.rc2
14
42
 
15
43
  Run into an issue with this release? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
@@ -481,6 +481,11 @@ module ViewComponent
481
481
  compiler.compiled?
482
482
  end
483
483
 
484
+ # @private
485
+ def ensure_compiled
486
+ compile unless compiled?
487
+ end
488
+
484
489
  # Compile templates to instance methods, assuming they haven't been compiled already.
485
490
  #
486
491
  # Do as much work as possible in this step, as doing so reduces the amount
@@ -51,24 +51,46 @@ module ViewComponent
51
51
  component_class.validate_collection_parameter!
52
52
  end
53
53
 
54
- templates.each do |template|
55
- # Remove existing compiled template methods,
56
- # as Ruby warns when redefining a method.
57
- method_name = call_method_name(template[:variant])
54
+ if has_inline_template?
55
+ template = component_class.inline_template
58
56
 
59
57
  redefinition_lock.synchronize do
60
- component_class.silence_redefinition_of_method(method_name)
58
+ component_class.silence_redefinition_of_method("call")
61
59
  # rubocop:disable Style/EvalWithLocation
62
- component_class.class_eval <<-RUBY, template[:path], 0
63
- def #{method_name}
64
- #{compiled_template(template[:path])}
60
+ component_class.class_eval <<-RUBY, template.path, template.lineno
61
+ def call
62
+ #{compiled_inline_template(template)}
65
63
  end
66
64
  RUBY
67
65
  # rubocop:enable Style/EvalWithLocation
66
+
67
+ component_class.silence_redefinition_of_method("render_template_for")
68
+ component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def render_template_for(variant = nil)
70
+ call
71
+ end
72
+ RUBY
73
+ end
74
+ else
75
+ templates.each do |template|
76
+ # Remove existing compiled template methods,
77
+ # as Ruby warns when redefining a method.
78
+ method_name = call_method_name(template[:variant])
79
+
80
+ redefinition_lock.synchronize do
81
+ component_class.silence_redefinition_of_method(method_name)
82
+ # rubocop:disable Style/EvalWithLocation
83
+ component_class.class_eval <<-RUBY, template[:path], 0
84
+ def #{method_name}
85
+ #{compiled_template(template[:path])}
86
+ end
87
+ RUBY
88
+ # rubocop:enable Style/EvalWithLocation
89
+ end
68
90
  end
69
- end
70
91
 
71
- define_render_template_for
92
+ define_render_template_for
93
+ end
72
94
 
73
95
  component_class.build_i18n_backend
74
96
 
@@ -103,12 +125,16 @@ module ViewComponent
103
125
  end
104
126
  end
105
127
 
128
+ def has_inline_template?
129
+ component_class.respond_to?(:inline_template) && component_class.inline_template.present?
130
+ end
131
+
106
132
  def template_errors
107
133
  @__vc_template_errors ||=
108
134
  begin
109
135
  errors = []
110
136
 
111
- if (templates + inline_calls).empty?
137
+ if (templates + inline_calls).empty? && !has_inline_template?
112
138
  errors << "Couldn't find a template file or inline render method for #{component_class}."
113
139
  end
114
140
 
@@ -216,9 +242,21 @@ module ViewComponent
216
242
  end
217
243
  end
218
244
 
245
+ def compiled_inline_template(template)
246
+ handler = ActionView::Template.handler_for_extension(template.language)
247
+ template.rstrip! if component_class.strip_trailing_whitespace?
248
+
249
+ compile_template(template.source, handler)
250
+ end
251
+
219
252
  def compiled_template(file_path)
220
253
  handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
221
254
  template = File.read(file_path)
255
+
256
+ compile_template(template, handler)
257
+ end
258
+
259
+ def compile_template(template, handler)
222
260
  template.rstrip! if component_class.strip_trailing_whitespace?
223
261
 
224
262
  if handler.method(:call).parameters.length > 1
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent # :nodoc:
4
+ module InlineTemplate
5
+ extend ActiveSupport::Concern
6
+ Template = Struct.new(:source, :language, :path, :lineno)
7
+
8
+ class_methods do
9
+ def method_missing(method, *args)
10
+ return super if !method.end_with?("_template")
11
+
12
+ if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
13
+ raise ViewComponent::ComponentError, "inline templates can only be defined once per-component"
14
+ end
15
+
16
+ if args.size != 1
17
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
18
+ end
19
+
20
+ ext = method.to_s.gsub("_template", "")
21
+ template = args.first
22
+
23
+ @__vc_inline_template_language = ext
24
+
25
+ caller = caller_locations(1..1)[0]
26
+ @__vc_inline_template = Template.new(
27
+ template,
28
+ ext,
29
+ caller.absolute_path || caller.path,
30
+ caller.lineno
31
+ )
32
+
33
+ @__vc_inline_template_defined = true
34
+ end
35
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
36
+
37
+ def respond_to_missing?(method, include_all = false)
38
+ method.end_with?("_template") || super
39
+ end
40
+
41
+ def inline_template
42
+ @__vc_inline_template
43
+ end
44
+
45
+ def inline_template_language
46
+ @__vc_inline_template_language if defined?(@__vc_inline_template_language)
47
+ end
48
+
49
+ def inherited(subclass)
50
+ super
51
+ subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -10,12 +10,12 @@ module ViewComponent
10
10
  # @param layout [String] The (optional) layout to use.
11
11
  # @return [Proc] A block that can be used to visit the path of the inline rendered component.
12
12
  def with_rendered_component_path(fragment, layout: false, &block)
13
- # Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
14
- FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
15
-
16
- file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
13
+ file = Tempfile.new(
14
+ ["rendered_#{fragment.class.name}", ".html"],
15
+ ViewComponentsSystemTestController::TEMP_DIR
16
+ )
17
17
  begin
18
- file.write(__vc_test_helpers_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
18
+ file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
19
19
  file.rewind
20
20
 
21
21
  block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
@@ -47,9 +47,9 @@ module ViewComponent
47
47
  @page = nil
48
48
  @rendered_content =
49
49
  if Rails.version.to_f >= 6.1
50
- __vc_test_helpers_controller.view_context.render(component, args, &block)
50
+ vc_test_controller.view_context.render(component, args, &block)
51
51
  else
52
- __vc_test_helpers_controller.view_context.render_component(component, &block)
52
+ vc_test_controller.view_context.render_component(component, &block)
53
53
  end
54
54
 
55
55
  Nokogiri::HTML.fragment(@rendered_content)
@@ -105,7 +105,7 @@ module ViewComponent
105
105
  # ```
106
106
  def render_in_view_context(*args, &block)
107
107
  @page = nil
108
- @rendered_content = __vc_test_helpers_controller.view_context.instance_exec(*args, &block)
108
+ @rendered_content = vc_test_controller.view_context.instance_exec(*args, &block)
109
109
  Nokogiri::HTML.fragment(@rendered_content)
110
110
  end
111
111
  ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
@@ -120,12 +120,12 @@ module ViewComponent
120
120
  #
121
121
  # @param variant [Symbol] The variant to be set for the provided block.
122
122
  def with_variant(variant)
123
- old_variants = __vc_test_helpers_controller.view_context.lookup_context.variants
123
+ old_variants = vc_test_controller.view_context.lookup_context.variants
124
124
 
125
- __vc_test_helpers_controller.view_context.lookup_context.variants = variant
125
+ vc_test_controller.view_context.lookup_context.variants = variant
126
126
  yield
127
127
  ensure
128
- __vc_test_helpers_controller.view_context.lookup_context.variants = old_variants
128
+ vc_test_controller.view_context.lookup_context.variants = old_variants
129
129
  end
130
130
 
131
131
  # Set the controller to be used while executing the given block,
@@ -139,12 +139,12 @@ module ViewComponent
139
139
  #
140
140
  # @param klass [ActionController::Base] The controller to be used.
141
141
  def with_controller_class(klass)
142
- old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
142
+ old_controller = defined?(@vc_test_controller) && @vc_test_controller
143
143
 
144
- @__vc_test_helpers_controller = __vc_test_helpers_build_controller(klass)
144
+ @vc_test_controller = __vc_test_helpers_build_controller(klass)
145
145
  yield
146
146
  ensure
147
- @__vc_test_helpers_controller = old_controller
147
+ @vc_test_controller = old_controller
148
148
  end
149
149
 
150
150
  # Set the URL of the current request (such as when using request-dependent path helpers):
@@ -161,7 +161,7 @@ module ViewComponent
161
161
  old_request_path_parameters = __vc_test_helpers_request.path_parameters
162
162
  old_request_query_parameters = __vc_test_helpers_request.query_parameters
163
163
  old_request_query_string = __vc_test_helpers_request.query_string
164
- old_controller = defined?(@__vc_test_helpers_controller) && @__vc_test_helpers_controller
164
+ old_controller = defined?(@vc_test_controller) && @vc_test_controller
165
165
 
166
166
  path, query = path.split("?", 2)
167
167
  __vc_test_helpers_request.path_info = path
@@ -174,16 +174,27 @@ module ViewComponent
174
174
  __vc_test_helpers_request.path_parameters = old_request_path_parameters
175
175
  __vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
176
176
  __vc_test_helpers_request.set_header(Rack::QUERY_STRING, old_request_query_string)
177
- @__vc_test_helpers_controller = old_controller
177
+ @vc_test_controller = old_controller
178
+ end
179
+
180
+ # Access the controller used by `render_inline`:
181
+ #
182
+ # ```ruby
183
+ # test "logged out user sees login link" do
184
+ # vc_test_controller.expects(:logged_in?).at_least_once.returns(false)
185
+ # render_inline(LoginComponent.new)
186
+ # assert_selector("[aria-label='You must be signed in']")
187
+ # end
188
+ # ```
189
+ #
190
+ # @return [ActionController::Base]
191
+ def vc_test_controller
192
+ @vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
178
193
  end
179
194
 
180
195
  # Note: We prefix private methods here to prevent collisions in consumer's tests.
181
196
  private
182
197
 
183
- def __vc_test_helpers_controller
184
- @__vc_test_helpers_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
185
- end
186
-
187
198
  def __vc_test_helpers_request
188
199
  @__vc_test_helpers_request ||=
189
200
  begin
@@ -21,7 +21,7 @@ module ViewComponent
21
21
  end
22
22
 
23
23
  def build_i18n_backend
24
- return if CompileCache.compiled? self
24
+ return if compiled?
25
25
 
26
26
  self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
27
27
  # Returning nil cleans up if translations file has been removed since the last compilation
@@ -32,6 +32,27 @@ module ViewComponent
32
32
  )
33
33
  end
34
34
  end
35
+
36
+ def i18n_key(key, scope = nil)
37
+ scope = scope.join(".") if scope.is_a? Array
38
+ key = key&.to_s unless key.is_a?(String)
39
+ key = "#{scope}.#{key}" if scope
40
+ key = "#{i18n_scope}#{key}" if key.start_with?(".")
41
+ key
42
+ end
43
+
44
+ def translate(key = nil, **options)
45
+ return key.map { |k| translate(k, **options) } if key.is_a?(Array)
46
+
47
+ ensure_compiled
48
+
49
+ locale = options.delete(:locale) || ::I18n.locale
50
+ key = i18n_key(key, options.delete(:scope))
51
+
52
+ i18n_backend.translate(locale, key, options)
53
+ end
54
+
55
+ alias_method :t, :translate
35
56
  end
36
57
 
37
58
  class I18nBackend < ::I18n::Backend::Simple
@@ -64,15 +85,10 @@ module ViewComponent
64
85
  return key.map { |k| translate(k, **options) } if key.is_a?(Array)
65
86
 
66
87
  locale = options.delete(:locale) || ::I18n.locale
67
- scope = options.delete(:scope)
68
- scope = scope.join(".") if scope.is_a? Array
69
- key = key&.to_s unless key.is_a?(String)
70
- key = "#{scope}.#{key}" if scope
71
- key = "#{i18n_scope}#{key}" if key.start_with?(".")
72
-
73
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
74
- html_escape_translation_options!(options)
75
- end
88
+ key = self.class.i18n_key(key, options.delete(:scope))
89
+ as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
90
+
91
+ html_escape_translation_options!(options) if as_html
76
92
 
77
93
  if key.start_with?(i18n_scope + ".")
78
94
  translated =
@@ -85,10 +101,7 @@ module ViewComponent
85
101
  return super(key, locale: locale, **options)
86
102
  end
87
103
 
88
- if HTML_SAFE_TRANSLATION_KEY.match?(key)
89
- translated = html_safe_translation(translated)
90
- end
91
-
104
+ translated = html_safe_translation(translated) if as_html
92
105
  translated
93
106
  else
94
107
  super(key, locale: locale, **options)
@@ -101,6 +114,8 @@ module ViewComponent
101
114
  self.class.i18n_scope
102
115
  end
103
116
 
117
+ private
118
+
104
119
  def html_safe_translation(translation)
105
120
  if translation.respond_to?(:map)
106
121
  translation.map { |element| html_safe_translation(element) }
@@ -112,18 +127,13 @@ module ViewComponent
112
127
  end
113
128
  end
114
129
 
115
- private
116
-
117
130
  def html_escape_translation_options!(options)
118
131
  options.each do |name, value|
119
- unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
120
- options[name] = ERB::Util.html_escape(value.to_s)
121
- end
122
- end
123
- end
132
+ next if ::I18n.reserved_keys_pattern.match?(name)
133
+ next if name == :count && value.is_a?(Numeric)
124
134
 
125
- def i18n_option?(name)
126
- (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
135
+ options[name] = ERB::Util.html_escape(value.to_s)
136
+ end
127
137
  end
128
138
  end
129
139
  end
@@ -5,7 +5,7 @@ module ViewComponent
5
5
  MAJOR = 3
6
6
  MINOR = 0
7
7
  PATCH = 0
8
- PRE = "rc2"
8
+ PRE = "rc3"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
@@ -13,6 +13,7 @@ module ViewComponent
13
13
  autoload :ComponentError
14
14
  autoload :Config
15
15
  autoload :Deprecation
16
+ autoload :InlineTemplate
16
17
  autoload :Instrumentation
17
18
  autoload :Preview
18
19
  autoload :PreviewTemplateError
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc2
4
+ version: 3.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ViewComponent Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-17 00:00:00.000000000 Z
11
+ date: 2023-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -363,6 +363,7 @@ files:
363
363
  - lib/view_component/docs_builder_component.html.erb
364
364
  - lib/view_component/docs_builder_component.rb
365
365
  - lib/view_component/engine.rb
366
+ - lib/view_component/inline_template.rb
366
367
  - lib/view_component/instrumentation.rb
367
368
  - lib/view_component/preview.rb
368
369
  - lib/view_component/preview_template_error.rb