phlex 1.9.3 → 1.10.0

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

Potentially problematic release.


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

data/lib/phlex/context.rb CHANGED
@@ -2,26 +2,63 @@
2
2
 
3
3
  # @api private
4
4
  class Phlex::Context
5
- def initialize
6
- @target = +""
5
+ def initialize(user_context = {})
6
+ @buffer = +""
7
7
  @capturing = false
8
+ @user_context = user_context
9
+ @fragments = nil
10
+ @in_target_fragment = false
11
+ @halt_signal = nil
8
12
  end
9
13
 
10
- attr_accessor :target, :capturing
14
+ attr_accessor :buffer, :capturing, :user_context, :in_target_fragment
11
15
 
12
- def capturing_into(new_target)
13
- original_target = @target
16
+ attr_reader :fragments
17
+
18
+ # Added for backwards compatibility with phlex-rails. We can remove this with 2.0
19
+ def target
20
+ @buffer
21
+ end
22
+
23
+ def target_fragments(fragments)
24
+ @fragments = fragments.to_h { |it| [it, true] }
25
+ end
26
+
27
+ def around_render
28
+ return yield if !@fragments || @halt_signal
29
+
30
+ catch do |signal|
31
+ @halt_signal = signal
32
+ yield
33
+ end
34
+ end
35
+
36
+ def begin_target(id)
37
+ @in_target_fragment = id
38
+ end
39
+
40
+ def end_target
41
+ @fragments.delete(@in_target_fragment)
42
+ @in_target_fragment = false
43
+ throw @halt_signal if @fragments.length == 0
44
+ end
45
+
46
+ def capturing_into(new_buffer)
47
+ original_buffer = @buffer
14
48
  original_capturing = @capturing
49
+ original_fragments = @fragments
15
50
 
16
51
  begin
17
- @target = new_target
52
+ @buffer = new_buffer
18
53
  @capturing = true
54
+ @fragments = nil
19
55
  yield
20
56
  ensure
21
- @target = original_target
57
+ @buffer = original_buffer
22
58
  @capturing = original_capturing
59
+ @fragments = original_fragments
23
60
  end
24
61
 
25
- new_target
62
+ new_buffer
26
63
  end
27
64
  end
data/lib/phlex/csv.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Phlex::CSV
4
+ include Phlex::Callable
5
+
6
+ FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"].to_h { |prefix| [prefix, true] }.freeze
7
+ SPACE_CHARACTERS = [" ", "\t", "\r"].to_h { |char| [char, true] }.freeze
8
+
9
+ def initialize(collection)
10
+ @collection = collection
11
+ @_headers = []
12
+ @_current_row = []
13
+ @_current_column_index = 0
14
+ @_view_context = nil
15
+ @_first = true
16
+ end
17
+
18
+ attr_reader :collection
19
+
20
+ def call(buffer = +"", view_context: nil)
21
+ unless escape_csv_injection? == true || escape_csv_injection? == false
22
+ raise <<~MESSAGE
23
+ You need to define escape_csv_injection? in #{self.class.name}, returning either `true` or `false`.
24
+
25
+ CSV injection is a security vulnerability where malicious spreadsheet formulae are used to execute code or exfiltrate data when a CSV is opened in a spreadsheet program such as Microsoft Excel or Google Sheets.
26
+
27
+ For more information, see https://owasp.org/www-community/attacks/CSV_Injection
28
+
29
+ If you're sure this CSV will never be opened in a spreadsheet program, you can disable CSV injection escapes:
30
+
31
+ def escape_csv_injection? = false
32
+
33
+ This is useful when using CSVs for byte-for-byte data exchange between secure systems.
34
+
35
+ Alternatively, you can enable CSV injection escapes at the cost of data integrity:
36
+
37
+ def escape_csv_injection? = true
38
+
39
+ Note: Enabling the CSV injection escapes will prefix any values that start with `=`, `+`, `-`, `@`, `\\t`, or `\\r` with a single quote `'` to prevent them from being interpreted as formulae by spreadsheet programs.
40
+
41
+ Unfortunately, there is no one-size-fits-all solution to CSV injection. You need to decide based on your specific use case.
42
+ MESSAGE
43
+ end
44
+
45
+ @_view_context = view_context
46
+
47
+ each_item do |record|
48
+ yielder(record) do |*args, **kwargs|
49
+ view_template(*args, **kwargs)
50
+
51
+ if @_first && render_headers?
52
+ buffer << @_headers.join(",") << "\n"
53
+ end
54
+
55
+ buffer << @_current_row.join(",") << "\n"
56
+ @_current_column_index = 0
57
+ @_current_row.clear
58
+ end
59
+
60
+ @_first = false
61
+ end
62
+
63
+ buffer
64
+ end
65
+
66
+ def filename
67
+ nil
68
+ end
69
+
70
+ def content_type
71
+ "text/csv"
72
+ end
73
+
74
+ private
75
+
76
+ def column(header = nil, value)
77
+ if @_first
78
+ @_headers << escape(header)
79
+ elsif header != @_headers[@_current_column_index]
80
+ raise "Inconsistent header."
81
+ end
82
+
83
+ @_current_row << escape(value)
84
+ @_current_column_index += 1
85
+ end
86
+
87
+ def each_item(&block)
88
+ collection.each(&block)
89
+ end
90
+
91
+ def yielder(record)
92
+ yield(record)
93
+ end
94
+
95
+ def template(...)
96
+ nil
97
+ end
98
+
99
+ # Override and set to `false` to disable rendering headers.
100
+ def render_headers?
101
+ true
102
+ end
103
+
104
+ # Override and set to `true` to strip leading and trailing whitespace from values.
105
+ def trim_whitespace?
106
+ false
107
+ end
108
+
109
+ # Override and set to `false` to disable CSV injection escapes or `true` to enable.
110
+ def escape_csv_injection?
111
+ nil
112
+ end
113
+
114
+ def helpers
115
+ @_view_context
116
+ end
117
+
118
+ def escape(value)
119
+ value = trim_whitespace? ? value.to_s.strip : value.to_s
120
+ first_char = value[0]
121
+ last_char = value[-1]
122
+
123
+ if escape_csv_injection? && FORMULA_PREFIXES[first_char]
124
+ # Prefix a single quote to prevent Excel, Google Docs, etc. from interpreting the value as a formula.
125
+ # See https://owasp.org/www-community/attacks/CSV_Injection
126
+ %("'#{value.gsub('"', '""')}")
127
+ elsif (!trim_whitespace? && (SPACE_CHARACTERS[first_char] || SPACE_CHARACTERS[last_char])) || value.include?('"') || value.include?(",") || value.include?("\n")
128
+ %("#{value.gsub('"', '""')}")
129
+ else
130
+ value
131
+ end
132
+ end
133
+ end
@@ -11,7 +11,7 @@
11
11
  # @tabs = []
12
12
  # end
13
13
  #
14
- # def template
14
+ # def view_template
15
15
  # @tabs.each { |t| a { t.name } }
16
16
  # @tabs.each { |t| article(&t.content) }
17
17
  # end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
- using Phlex::Overrides::Symbol::Name
5
- end
6
-
7
3
  # Extending this module provides the {register_element} macro for registering your own custom elements. It's already extended by {HTML} and {SVG}.
8
4
  # @example
9
5
  # module MyCustomElements
@@ -15,14 +11,14 @@ end
15
11
  # class MyComponent < Phlex::HTML
16
12
  # include MyCustomElements
17
13
  #
18
- # def template
14
+ # def view_template
19
15
  # trix_editor
20
16
  # end
21
17
  # end
22
18
  module Phlex::Elements
23
19
  # @api private
24
20
  def registered_elements
25
- @registered_elements ||= Concurrent::Map.new
21
+ @registered_elements ||= {}
26
22
  end
27
23
 
28
24
  # Register a custom element. This macro defines an element method for the current class and descendents only. There is no global element registry.
@@ -32,35 +28,64 @@ module Phlex::Elements
32
28
  # @note The methods defined by this macro depend on other methods from {SGML} so they should always be mixed into an {HTML} or {SVG} component.
33
29
  # @example Register the custom element `<trix-editor>`
34
30
  # register_element :trix_editor
35
- def register_element(method_name, tag: nil)
36
- tag ||= method_name.name.tr("_", "-")
31
+ def register_element(method_name, tag: method_name.name.tr("_", "-"), deprecated: false)
32
+ if deprecated
33
+ deprecation = <<~RUBY
34
+ Kernel.warn "#{deprecated}"
35
+ RUBY
36
+ else
37
+ deprecation = ""
38
+ end
37
39
 
38
40
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
39
41
  # frozen_string_literal: true
40
42
 
41
43
  def #{method_name}(**attributes, &block)
42
- target = @_context.target
44
+ #{deprecation}
45
+
46
+ context = @_context
47
+ buffer = context.buffer
48
+ fragment = context.fragments
49
+ target_found = false
50
+
51
+ if fragment
52
+ return if fragment.length == 0 # we found all our fragments already
53
+
54
+ id = attributes[:id]
55
+
56
+ if !context.in_target_fragment
57
+ if fragment[id]
58
+ context.begin_target(id)
59
+ target_found = true
60
+ else
61
+ yield(self) if block
62
+ return nil
63
+ end
64
+ end
65
+ end
43
66
 
44
67
  if attributes.length > 0 # with attributes
45
68
  if block # with content block
46
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
69
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
47
70
  yield_content(&block)
48
- target << "</#{tag}>"
71
+ buffer << "</#{tag}>"
49
72
  else # without content block
50
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << "></#{tag}>"
73
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << "></#{tag}>"
51
74
  end
52
75
  else # without attributes
53
76
  if block # with content block
54
- target << "<#{tag}>"
77
+ buffer << "<#{tag}>"
55
78
  yield_content(&block)
56
- target << "</#{tag}>"
79
+ buffer << "</#{tag}>"
57
80
  else # without content block
58
- target << "<#{tag}></#{tag}>"
81
+ buffer << "<#{tag}></#{tag}>"
59
82
  end
60
83
  end
61
84
 
62
85
  #{'flush' if tag == 'head'}
63
86
 
87
+ context.end_target if target_found
88
+
64
89
  nil
65
90
  end
66
91
 
@@ -73,19 +98,47 @@ module Phlex::Elements
73
98
  end
74
99
 
75
100
  # @api private
76
- def register_void_element(method_name, tag: method_name.name.tr("_", "-"))
101
+ def register_void_element(method_name, tag: method_name.name.tr("_", "-"), deprecated: false)
102
+ if deprecated
103
+ deprecation = <<~RUBY
104
+ Kernel.warn "#{deprecated}"
105
+ RUBY
106
+ else
107
+ deprecation = ""
108
+ end
109
+
77
110
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
78
111
  # frozen_string_literal: true
79
112
 
80
113
  def #{method_name}(**attributes)
81
- target = @_context.target
114
+ #{deprecation}
115
+ context = @_context
116
+ buffer = context.buffer
117
+ fragment = context.fragments
118
+
119
+ if fragment
120
+ return if fragment.length == 0 # we found all our fragments already
121
+
122
+ id = attributes[:id]
123
+
124
+ if !context.in_target_fragment
125
+ if fragment[id]
126
+ context.begin_target(id)
127
+ target_found = true
128
+ else
129
+ return nil
130
+ end
131
+ end
132
+ end
82
133
 
83
134
  if attributes.length > 0 # with attributes
84
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
135
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
85
136
  else # without attributes
86
- target << "<#{tag}>"
137
+ buffer << "<#{tag}>"
87
138
  end
88
139
 
140
+ context.end_target if target_found
141
+
89
142
  nil
90
143
  end
91
144
 
data/lib/phlex/helpers.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
- using Phlex::Overrides::Symbol::Name
5
- end
3
+ require "set"
6
4
 
7
5
  module Phlex::Helpers
8
6
  private
@@ -80,9 +78,23 @@ module Phlex::Helpers
80
78
  when Hash
81
79
  old.is_a?(Hash) ? mix(old, new) : new
82
80
  when Array
83
- old.is_a?(Array) ? (old + new) : new
81
+ case old
82
+ when Array then old + new
83
+ when Set then old.to_a + new
84
+ when Hash then new
85
+ else
86
+ [old] + new
87
+ end
88
+ when Set
89
+ case old
90
+ when Set then old + new
91
+ when Array then old + new.to_a
92
+ when Hash then new
93
+ else
94
+ new + [old]
95
+ end
84
96
  when String
85
- old.is_a?(String) ? "#{old} #{new}" : new
97
+ old.is_a?(String) ? "#{old} #{new}" : old + old.class[new]
86
98
  else
87
99
  new
88
100
  end