padrino-helpers 0.16.2 → 0.17.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12402f16964cbfb4911bf7488933f4b378f949dea6e7f6e30013ea2e16f5ab03
4
- data.tar.gz: 19bad50f4edfeb5cf13d3c9052806ee450841134cd9335eddcdc70f77b88ecf3
3
+ metadata.gz: 51a50e7f371f332ea78163dcfb0bbe826f29e9a300731c71b7591dfef4515f16
4
+ data.tar.gz: 2bfcef2d07e371a896c4287e9870d939dd6a7a4d4c3f049ccfb29724e96cfbfe
5
5
  SHA512:
6
- metadata.gz: dec2dd1bf508adb4c26dfc9c95fcb898ea3567f0ed236187dd904af754dfa893111a1e6eaa4731212ea6b92c853c615d6aa7f4a303d0a0bb2db49d7496609539
7
- data.tar.gz: 4ef70dfe8ac251e5759685abcc9c648d3e24cc46378420a84ba58bc5150f8cd3398a038723d1c8cd5c8b9caf23a40a10be77af41a0b8fba3e920c327742079b8
6
+ metadata.gz: 65cdd233db5732c6561f6bd386efad8e3cd798b37e0497c87b79d69d8bf104bb54c0ec4294eddf456eb6fba8fc3dfef97604634ba652415a9d8677f9d3a0e9d9
7
+ data.tar.gz: d6cd233e716c7cbf85e2b76635ed7620c9f2d1a984af95d8b553b995b78c0716e10bc54e363f2feaa396b4abe17fd53f89d58385c2a73667bd199a373a61db94
@@ -49,7 +49,7 @@ module Padrino
49
49
  def precompiled_preamble(locals)
50
50
  original = super
51
51
  return original unless @is_padrino_app
52
- "__in_erb_template = true\n" << original
52
+ "__in_erb_template = true\n#{original}"
53
53
  end
54
54
  end
55
55
  end
@@ -22,7 +22,7 @@ module Padrino
22
22
 
23
23
  class ErubiTemplate < Tilt::ErubiTemplate
24
24
  def precompiled_preamble(*)
25
- "__in_erb_template = true\n" << super
25
+ "__in_erb_template = true\n#{super}"
26
26
  end
27
27
  end
28
28
  end
@@ -53,7 +53,7 @@ module Padrino
53
53
  def precompiled_preamble(locals)
54
54
  original = super
55
55
  return original unless @is_padrino_app
56
- "__in_erb_template = true\n" << original.rpartition("\n").first << "#{@outvar} = _buf = SafeBuffer.new\n"
56
+ "__in_erb_template = true\n#{original.rpartition("\n").first}#{@outvar} = _buf = SafeBuffer.new\n"
57
57
  end
58
58
  end
59
59
  end
@@ -20,7 +20,7 @@ module Padrino
20
20
  include SafeTemplate
21
21
 
22
22
  def precompiled_preamble(locals)
23
- "__in_hamlit_template = true\n" << super
23
+ "__in_hamlit_template = true\n#{super}"
24
24
  end
25
25
  end
26
26
  end
@@ -20,7 +20,7 @@ module Padrino
20
20
  include SafeTemplate
21
21
 
22
22
  def precompiled_preamble(locals)
23
- "__in_slim_template = true\n" << super
23
+ "__in_slim_template = true\n#{super}"
24
24
  end
25
25
  end
26
26
  end
@@ -111,7 +111,7 @@ module Padrino
111
111
  #
112
112
  def fetch_layout_path(given_layout, layouts_path = views)
113
113
  layout_name = (given_layout || @layout || :application).to_s
114
- cache_layout_path(layout_name) do
114
+ cache_layout_path([layouts_path.to_s, layout_name]) do
115
115
  if Pathname.new(layout_name).absolute? && Dir["#{layout_name}.*"].any? || Dir["#{layouts_path}/#{layout_name}.*"].any?
116
116
  layout_name
117
117
  else
@@ -132,12 +132,12 @@ module Padrino
132
132
  def cache_template_path(options)
133
133
  began_at = Time.now
134
134
  @_cached_templates ||= {}
135
- logging = defined?(settings) && settings.logging? && defined?(logger)
135
+ logging = defined?(settings) && settings.logging? && defined?(logger) && logger.respond_to?(:bench) && logger.debug?
136
136
  if !reload_templates? && (path = @_cached_templates[options])
137
- logger.debug :cached, began_at, path[0] if logging
137
+ logger.bench :cached, began_at, path[0] if logging
138
138
  else
139
- path = @_cached_templates[options] = yield(name)
140
- logger.debug :template, began_at, path[0] if path && logging
139
+ path = @_cached_templates[options] = yield
140
+ logger.bench :template, began_at, path[0] if path && logging
141
141
  end
142
142
  path
143
143
  end
@@ -293,11 +293,10 @@ module Padrino
293
293
  template_path = template_path.to_s
294
294
  controller_key = respond_to?(:request) && request.respond_to?(:controller) && request.controller
295
295
  rendering_options = [template_path, content_type || :html, locale]
296
+ options = DEFAULT_RENDERING_OPTIONS.merge(options)
297
+ view_path = options[:views] || settings.views || './views'
296
298
 
297
- settings.cache_template_path(["#{controller_key}/#{template_path}", rendering_options[1], rendering_options[2]]) do
298
- options = DEFAULT_RENDERING_OPTIONS.merge(options)
299
- view_path = options[:views] || settings.views || './views'
300
-
299
+ settings.cache_template_path([view_path.to_s, "#{controller_key}/#{template_path}", rendering_options[1], rendering_options[2]]) do
301
300
  template_candidates = glob_templates(view_path, template_path)
302
301
  selected_template = select_template(template_candidates, *rendering_options)
303
302
  selected_template ||= template_candidates.first unless options[:strict_format]
@@ -317,9 +316,13 @@ module Padrino
317
316
  def resolve_layout(layout, options = {})
318
317
  layouts_path = options[:layout_options] && options[:layout_options][:views] || options[:views] || settings.views || './views'
319
318
  template_path = settings.fetch_layout_path(layout, layouts_path)
319
+ if !layout && Pathname.new(template_path).absolute? && Dir["#{template_path}.*"].any?
320
+ layouts_path, _, template_path = template_path.rpartition('/')
321
+ options[:layout_options] = (options[:layout_options] || {}).merge(views: layouts_path)
322
+ end
320
323
  rendering_options = [template_path, content_type || :html, locale]
321
324
 
322
- settings.cache_template_path(rendering_options) do
325
+ settings.cache_template_path([layouts_path.to_s, *rendering_options]) do
323
326
  template_candidates = glob_templates(layouts_path, template_path)
324
327
  selected_template = select_template(template_candidates, *rendering_options)
325
328
 
@@ -1,3 +1,5 @@
1
+ require 'cgi/escape'
2
+
1
3
  module Padrino
2
4
  ##
3
5
  # Padrino::SafeBuffer is based on ActiveSupport::SafeBuffer
@@ -5,7 +5,7 @@ module Padrino
5
5
  #
6
6
  module AssetTagHelpers
7
7
  APPEND_ASSET_EXTENSIONS = %w[js css]
8
- ABSOLUTE_URL_PATTERN = %r{^(https?://)}
8
+ ABSOLUTE_URL_PATTERN = %r{\A(https?://)}
9
9
  ASSET_FOLDERS = {
10
10
  js: 'javascripts',
11
11
  css: 'stylesheets'
@@ -86,6 +86,7 @@ module Padrino
86
86
  name = block_given? ? '' : args.shift
87
87
  href = args.first
88
88
  options = { href: href ? escape_link(href) : '#' }.update(options)
89
+ options[:href] = escape_link(options[:href]) if options[:href]
89
90
  return name unless parse_conditions(href, options)
90
91
  block_given? ? content_tag(:a, options, &block) : content_tag(:a, name, options)
91
92
  end
@@ -315,8 +316,8 @@ module Padrino
315
316
  #
316
317
  def asset_path(kind, source = nil)
317
318
  kind, source = source, kind if source.nil?
318
- source = asset_normalize_extension(kind, escape_link(source.to_s))
319
- return source if source =~ ABSOLUTE_URL_PATTERN || source =~ %r{^/}
319
+ source = asset_normalize_extension(kind, source.to_s.gsub(' ', '%20'))
320
+ return source if source =~ ABSOLUTE_URL_PATTERN || source =~ %r{\A/}
320
321
  source = File.join(asset_folder_name(kind), source)
321
322
  timestamp = asset_timestamp(source)
322
323
  result_path = uri_root_path(source)
@@ -210,7 +210,7 @@ module Padrino
210
210
  # field_name(:number) => "user_telephone_attributes_number"
211
211
  # field_name(:street) => "user_addresses_attributes_0_street"
212
212
  def field_id(field = nil, value = nil)
213
- result = namespace && !is_nested ? "#{namespace}_" : ''
213
+ result = namespace && !is_nested ? "#{namespace}_" : +''
214
214
  result << field_id_fragment
215
215
  result << "_#{field}" if field
216
216
  result << "_#{value}" if value
@@ -94,6 +94,7 @@ module Padrino
94
94
  protect_from_csrf: is_protected_from_csrf?,
95
95
  'accept-charset' => 'UTF-8'
96
96
  }.update(options)
97
+ options[:action] = escape_link(options[:action]) if options[:action]
97
98
  options[:enctype] = 'multipart/form-data' if options.delete(:multipart)
98
99
 
99
100
  if (desired_method = options[:method]) =~ /get/i
@@ -847,7 +848,7 @@ module Padrino
847
848
  def adjust_color(color)
848
849
  return '#000000' unless color =~ COLOR_CODE_REGEXP
849
850
  return color if (color_size = color.size) == 7
850
- color.slice(1, color_size - 1).each_char.with_object('#') do |chr, obj|
851
+ color.slice(1, color_size - 1).each_char.with_object(+'#') do |chr, obj|
851
852
  obj << chr * 2
852
853
  end
853
854
  end
@@ -77,7 +77,7 @@ module Padrino
77
77
  def simple_format(text, options = {})
78
78
  t = options.delete(:tag) || :p
79
79
  start_tag = tag(t, options, true)
80
- text = escape_html(text.to_s.dup) unless text.html_safe?
80
+ text = text.html_safe? ? text.dup : escape_html(text.to_s.dup)
81
81
  text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
82
82
  text.gsub!(/\n\n+/, "</#{t}>\n\n#{start_tag}") # 2+ newline -> paragraph
83
83
  text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
@@ -103,7 +103,7 @@ module Padrino
103
103
  # pluralize(2, 'person') => '2 people'
104
104
  #
105
105
  def pluralize(count, singular, plural = nil)
106
- "#{count || 0} " + ([1, '1'].include?(count) ? singular : (plural || singular.pluralize))
106
+ "#{count || 0} " + ([1, '1'].include?(count) ? singular : (plural || Inflections.pluralize(singular)))
107
107
  end
108
108
 
109
109
  ##
@@ -235,17 +235,39 @@ module Padrino
235
235
  "<#{name}#{attributes}#{open ? '>' : ' />'}".html_safe
236
236
  end
237
237
 
238
+ SAFE_URI_SCHEMES = %w[http https mailto tel ftp].freeze
239
+
238
240
  ##
239
241
  # Returns an escaped document link.
242
+ # Rejects unsafe URI schemes (javascript:, data:, vbscript:, etc.)
243
+ # to prevent XSS via link injection.
240
244
  #
241
245
  # @example
242
246
  # escape_link('http://example.com/spaced link')
243
247
  # # => 'http://example.com/spaced%20link'
244
- # escape_link('already%20partially escaped')
245
- # # => 'already%20partially%20escaped'
248
+ # escape_link('javascript:alert(1)')
249
+ # # => '#'
246
250
  #
247
251
  def escape_link(link)
248
- link.gsub(' ', '%20')
252
+ return link.gsub(' ', '%20') unless unsafe_uri_scheme?(link)
253
+
254
+ '#'
255
+ end
256
+
257
+ ##
258
+ # Returns true if the URI uses an unsafe scheme.
259
+ #
260
+ def unsafe_uri_scheme?(uri)
261
+ return false unless uri
262
+
263
+ # Read the scheme the way browsers do: drop tabs and newlines, skip leading
264
+ # control characters and spaces, then take a run of scheme characters
265
+ # followed by a colon. Relative links like 'search?q=a:b' have no scheme.
266
+ # Everything is anchored at the start so long inputs are scanned once.
267
+ scheme = uri.to_s.delete("\t\n\r")[/\A[\x00-\x20]*([a-z][a-z0-9+.-]*):/i, 1]
268
+ return false unless scheme
269
+
270
+ !SAFE_URI_SCHEMES.include?(scheme.downcase)
249
271
  end
250
272
 
251
273
  private
@@ -256,7 +278,7 @@ module Padrino
256
278
  def tag_attributes(options)
257
279
  return '' unless options
258
280
 
259
- options.inject('') do |all, (key, value)|
281
+ options.inject(+'') do |all, (key, value)|
260
282
  next all unless value
261
283
 
262
284
  all << ' ' if all.empty?
@@ -283,7 +305,7 @@ module Padrino
283
305
  # Iterate through nested values.
284
306
  #
285
307
  def nested_values(attribute, hash)
286
- hash.inject('') do |all, (key, value)|
308
+ hash.inject(+'') do |all, (key, value)|
287
309
  attribute_with_name = "#{attribute}-#{key.to_s.tr('_', '-')}"
288
310
  all <<
289
311
  if value.is_a?(Hash)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  :test: bacon
3
3
  :mock: mocha
4
- :orm: datamapper
4
+ :orm: none
5
5
  :renderer: erb
6
6
  :script: jquery
@@ -76,6 +76,63 @@ describe 'AssetTagHelpers' do
76
76
  assert_html_has_tag(actual_link, 'a', href: '/register%20new%20user')
77
77
  end
78
78
 
79
+ it 'should reject javascript: URIs' do
80
+ actual_link = link_to('click', 'javascript:alert(1)')
81
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
82
+ end
83
+
84
+ it 'should reject javascript: URIs with mixed case and whitespace' do
85
+ actual_link = link_to('click', "JaVaScRiPt:\nalert(1)")
86
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
87
+ end
88
+
89
+ it 'should reject data: URIs' do
90
+ actual_link = link_to('click', 'data:text/html,<script>alert(1)</script>')
91
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
92
+ end
93
+
94
+ it 'should reject vbscript: URIs' do
95
+ actual_link = link_to('click', 'vbscript:MsgBox("xss")')
96
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
97
+ end
98
+
99
+ it 'should reject unsafe schemes passed via options hash' do
100
+ actual_link = link_to('click', '/safe', href: 'javascript:alert(1)')
101
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
102
+ end
103
+
104
+ it 'should allow http and https URIs' do
105
+ assert_match 'href="http://example.com"', link_to('click', 'http://example.com')
106
+ assert_match 'href="https://example.com"', link_to('click', 'https://example.com')
107
+ end
108
+
109
+ it 'should allow mailto and tel URIs' do
110
+ assert_match 'href="mailto:a@b.com"', link_to('email', 'mailto:a@b.com')
111
+ assert_match 'href="tel:+1234567890"', link_to('call', 'tel:+1234567890')
112
+ end
113
+
114
+ it 'should allow relative paths and anchors' do
115
+ assert_html_has_tag(link_to('click', '/path'), 'a', href: '/path')
116
+ assert_match 'href="#anchor"', link_to('click', '#anchor')
117
+ assert_match 'href="?query=1"', link_to('click', '?query=1')
118
+ end
119
+
120
+ it 'should allow relative paths with colons in the query or fragment' do
121
+ assert_match 'href="search?q=a:b"', link_to('click', 'search?q=a:b')
122
+ assert_match 'href="users/1?at=10:30"', link_to('click', 'users/1?at=10:30')
123
+ assert_match 'href="page#x:y"', link_to('click', 'page#x:y')
124
+ end
125
+
126
+ it 'should reject unsafe URIs preceded by control characters' do
127
+ actual_link = link_to('click', "\u0001 javascript:alert(1)")
128
+ assert_html_has_tag(actual_link, 'a', href: '#', content: 'click')
129
+ end
130
+
131
+ it 'should reject unsafe URIs with tabs or newlines inside the scheme' do
132
+ assert_html_has_tag(link_to('click', "java\tscript:alert(1)"), 'a', href: '#', content: 'click')
133
+ assert_html_has_tag(link_to('click', "java\nscript:alert(1)"), 'a', href: '#', content: 'click')
134
+ end
135
+
79
136
  it 'should not escape image_tag' do
80
137
  actual_link = link_to(image_tag('/my/fancy/image.png'), class: 'first', id: 'binky')
81
138
  assert_html_has_tag(actual_link, 'img', src: '/my/fancy/image.png')
@@ -410,5 +467,15 @@ describe 'AssetTagHelpers' do
410
467
  it 'should generate proper paths for public folder' do
411
468
  assert_match(%r{/files/file.ext\?\d+}, asset_path('files/file.ext'))
412
469
  end
470
+
471
+ it 'should keep colons in asset names' do
472
+ assert_match(%r{/images/a:b\.png\?\d+}, asset_path(:images, 'a:b.png'))
473
+ assert_match(%r{/images/photos/10:30\.png\?\d+}, asset_path(:images, 'photos/10:30.png'))
474
+ end
475
+
476
+ it 'should prefix sources whose later lines look absolute' do
477
+ assert_match(%r{\A/javascripts/data:}, asset_path(:js, "data:text/javascript,alert(1)//\n/x"))
478
+ assert_match(%r{\A/images/javascript:}, asset_path(:images, "javascript:alert(1)//\nhttp://x"))
479
+ end
413
480
  end
414
481
  end
@@ -19,6 +19,16 @@ describe 'FormHelpers' do
19
19
  assert_html_has_tag(actual_html, 'form input', type: 'hidden', name: '_method', count: 0)
20
20
  end
21
21
 
22
+ it 'should reject unsafe action schemes passed via options hash' do
23
+ actual_html = form_tag('/safe', action: 'javascript:alert(1)') { 'Demo' }
24
+ assert_html_has_tag(actual_html, :form, action: '#')
25
+ end
26
+
27
+ it 'should allow relative actions with colons in the query' do
28
+ actual_html = form_tag('search?q=a:b') { 'Demo' }
29
+ assert_html_has_tag(actual_html, :form, action: 'search?q=a:b')
30
+ end
31
+
22
32
  it 'should display correct text inputs within form_tag' do
23
33
  actual_html = form_tag('/register', "accept-charset": 'UTF-8', class: 'test') { text_field_tag(:username) }
24
34
  assert_html_has_tag(actual_html, 'form input', type: 'text', name: 'username')
@@ -39,6 +39,13 @@ describe 'FormatHelpers' do
39
39
  assert_equal "<p>Don't try to escape <b>me</b>!</p>", actual_text
40
40
  end
41
41
 
42
+ it 'should not modify already sanitized text it is given' do
43
+ text = "Keep me\nas I am".html_safe
44
+ simple_format(text)
45
+ assert_equal "Keep me\nas I am", text
46
+ assert_equal "<p>Keep me\n<br />as I am</p>", simple_format(text.freeze)
47
+ end
48
+
42
49
  describe 'wrapped in a custom tag' do
43
50
  it 'should format simple text into html format' do
44
51
  actual_text = simple_format("Here is some basic text...\n...with a line break.", tag: :div)
@@ -57,6 +64,28 @@ describe 'FormatHelpers' do
57
64
  end
58
65
  end
59
66
 
67
+ describe 'for #pluralize method' do
68
+ it 'should pluralize with the given count' do
69
+ assert_equal '1 person', pluralize(1, 'person')
70
+ assert_equal '2 people', pluralize(2, 'person')
71
+ assert_equal '2 folks', pluralize(2, 'person', 'folks')
72
+ end
73
+
74
+ it 'should pluralize without String#pluralize from ActiveSupport or padrino-gen' do
75
+ hidden = String.method_defined?(:pluralize)
76
+ if hidden
77
+ String.send(:alias_method, :__hidden_pluralize, :pluralize)
78
+ String.send(:remove_method, :pluralize)
79
+ end
80
+ assert_equal '2 items', pluralize(2, 'item')
81
+ ensure
82
+ if hidden
83
+ String.send(:alias_method, :pluralize, :__hidden_pluralize)
84
+ String.send(:remove_method, :__hidden_pluralize)
85
+ end
86
+ end
87
+ end
88
+
60
89
  describe 'for #word_wrap method' do
61
90
  it 'should return proper formatting for 8 max width' do
62
91
  actual_text = word_wrap('Once upon a time', line_width: 8)
@@ -218,6 +218,38 @@ describe 'Rendering' do
218
218
  get '/'
219
219
  assert_equal 'default layout at application', body
220
220
  end
221
+
222
+ it 'should use a class-level layout given as an absolute path' do
223
+ absolute_layout = File.expand_path('fixtures/layouts/layout', __dir__)
224
+ create_view :foo, 'erb file'
225
+ create_view :foo, 'js file', format: :js
226
+ mock_app do
227
+ layout absolute_layout
228
+ get('/layout_test', provides: %i[html js]) { render :foo }
229
+ end
230
+ get '/layout_test'
231
+ assert_equal 'erb file absolute layout', body
232
+ get '/layout_test.js'
233
+ assert_equal 'js file', body
234
+ end
235
+
236
+ it 'should use a class-level absolute layout with cached templates' do
237
+ absolute_layout = File.expand_path('fixtures/layouts/layout', __dir__)
238
+ create_layout :"layouts/layout", 'relative layout <%= yield %>'
239
+ create_view :foo, 'erb file'
240
+ mock_app do
241
+ disable :reload_templates
242
+ layout absolute_layout
243
+ get('/absolute') { render :foo }
244
+ get('/relative') { render :foo, layout: :layout }
245
+ end
246
+ 2.times do
247
+ get '/absolute'
248
+ assert_equal 'erb file absolute layout', body
249
+ get '/relative'
250
+ assert_equal 'relative layout erb file', body
251
+ end
252
+ end
221
253
  end
222
254
 
223
255
  it 'should solve layout in layouts paths' do
@@ -314,6 +346,24 @@ describe 'Rendering' do
314
346
  assert_equal 'bar', body
315
347
  end
316
348
 
349
+ it 'should render with logging and a logger without Padrino::Logger::Extensions' do
350
+ create_template :index, 'plain logger'
351
+ log = StringIO.new
352
+ save_logger = Padrino.logger
353
+ Padrino.logger = Logger.new(log)
354
+ mock_app do
355
+ enable :logging
356
+ get('/') { render(:index, layout: false) }
357
+ end
358
+ get '/'
359
+ assert_equal 200, status
360
+ assert_equal 'plain logger', body
361
+ assert_match(/TEMPLATE \(\d+\.\d+s\) .*index/, log.string)
362
+ ensure
363
+ Padrino.logger = save_logger
364
+ remove_views
365
+ end
366
+
317
367
  it 'should work properly with logging and layout' do
318
368
  create_layout :application, 'layout <%= yield %>'
319
369
  create_view :index, '<%= foo %>'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: padrino-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.2
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Padrino Team
@@ -19,14 +19,14 @@ dependencies:
19
19
  requirements:
20
20
  - - '='
21
21
  - !ruby/object:Gem::Version
22
- version: 0.16.2
22
+ version: 0.17.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - '='
28
28
  - !ruby/object:Gem::Version
29
- version: 0.16.2
29
+ version: 0.17.0
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: tilt
32
32
  requirement: !ruby/object:Gem::Requirement