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.
- checksums.yaml +4 -4
- data/.editorconfig +13 -0
- data/.rubocop.yml +27 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +11 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +62 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +21 -0
- data/README.md +3 -1
- data/SECURITY.md +14 -0
- data/bench.rb +21 -0
- data/config/sus.rb +8 -0
- data/fixtures/components/say_hi.rb +15 -0
- data/fixtures/components.rb +7 -0
- data/fixtures/layout.rb +31 -0
- data/fixtures/page.rb +37 -0
- data/fixtures/view_helper.rb +22 -0
- data/gd/phlex/callable.rb +18 -0
- data/gd/phlex/sgml/attributes.rb +107 -0
- data/gd/support/helper.rb +23 -0
- data/lib/phlex/black_hole.rb +1 -1
- data/lib/phlex/context.rb +45 -8
- data/lib/phlex/csv.rb +133 -0
- data/lib/phlex/deferred_render.rb +1 -1
- data/lib/phlex/elements.rb +72 -19
- data/lib/phlex/helpers.rb +17 -5
- data/lib/phlex/html/standard_elements.rb +138 -96
- data/lib/phlex/html/void_elements.rb +17 -17
- data/lib/phlex/html.rb +16 -2
- data/lib/phlex/kit.rb +62 -0
- data/lib/phlex/sgml.rb +110 -62
- data/lib/phlex/svg/standard_elements.rb +64 -64
- data/lib/phlex/svg.rb +10 -0
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +34 -12
- data/phlex_logo.png +0 -0
- data/sig/phlex.rbs +4 -0
- metadata +29 -48
- data/lib/phlex/overrides/symbol/name.rb +0 -6
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
|
-
@
|
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 :
|
14
|
+
attr_accessor :buffer, :capturing, :user_context, :in_target_fragment
|
11
15
|
|
12
|
-
|
13
|
-
|
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
|
-
@
|
52
|
+
@buffer = new_buffer
|
18
53
|
@capturing = true
|
54
|
+
@fragments = nil
|
19
55
|
yield
|
20
56
|
ensure
|
21
|
-
@
|
57
|
+
@buffer = original_buffer
|
22
58
|
@capturing = original_capturing
|
59
|
+
@fragments = original_fragments
|
23
60
|
end
|
24
61
|
|
25
|
-
|
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
|
data/lib/phlex/elements.rb
CHANGED
@@ -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
|
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 ||=
|
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:
|
36
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
71
|
+
buffer << "</#{tag}>"
|
49
72
|
else # without content block
|
50
|
-
|
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
|
-
|
77
|
+
buffer << "<#{tag}>"
|
55
78
|
yield_content(&block)
|
56
|
-
|
79
|
+
buffer << "</#{tag}>"
|
57
80
|
else # without content block
|
58
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|