actionpack 2.3.8 → 2.3.9.pre

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

Potentially problematic release.


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

Files changed (35) hide show
  1. data/CHANGELOG +5 -0
  2. data/Rakefile +1 -1
  3. data/lib/action_controller/caching/fragments.rb +1 -1
  4. data/lib/action_controller/integration.rb +16 -5
  5. data/lib/action_controller/request.rb +2 -2
  6. data/lib/action_controller/rescue.rb +2 -2
  7. data/lib/action_controller/resources.rb +1 -1
  8. data/lib/action_controller/routing.rb +1 -1
  9. data/lib/action_controller/routing/route.rb +3 -3
  10. data/lib/action_controller/session/abstract_store.rb +152 -56
  11. data/lib/action_controller/session/cookie_store.rb +51 -16
  12. data/lib/action_controller/session/mem_cache_store.rb +9 -0
  13. data/lib/action_controller/test_process.rb +1 -1
  14. data/lib/action_controller/url_rewriter.rb +9 -1
  15. data/lib/action_pack/version.rb +1 -1
  16. data/lib/action_view/helpers/form_helper.rb +3 -3
  17. data/lib/action_view/helpers/form_options_helper.rb +1 -1
  18. data/lib/action_view/helpers/form_tag_helper.rb +1 -1
  19. data/lib/action_view/helpers/prototype_helper.rb +3 -3
  20. data/lib/action_view/helpers/text_helper.rb +27 -17
  21. data/lib/action_view/locale/en.yml +14 -14
  22. data/lib/action_view/template.rb +2 -2
  23. data/test/abstract_unit.rb +17 -0
  24. data/test/activerecord/active_record_store_test.rb +65 -18
  25. data/test/controller/integration_test.rb +26 -0
  26. data/test/controller/request/multipart_params_parsing_test.rb +15 -0
  27. data/test/controller/rescue_test.rb +5 -6
  28. data/test/controller/session/cookie_store_test.rb +80 -2
  29. data/test/controller/session/mem_cache_store_test.rb +65 -2
  30. data/test/controller/url_rewriter_test.rb +9 -3
  31. data/test/fixtures/session_autoload_test/session_autoload_test/foo.rb +10 -0
  32. data/test/template/form_helper_test.rb +17 -0
  33. data/test/template/form_options_helper_test.rb +4 -0
  34. data/test/template/text_helper_test.rb +47 -18
  35. metadata +14 -9
@@ -36,6 +36,8 @@ module ActionController
36
36
  #
37
37
  # Note that changing digest or secret invalidates all existing sessions!
38
38
  class CookieStore
39
+ include AbstractStore::SessionUtils
40
+
39
41
  # Cookies can typically store 4096 bytes.
40
42
  MAX = 4096
41
43
  SECRET_MIN_LENGTH = 30 # characters
@@ -93,20 +95,20 @@ module ActionController
93
95
  end
94
96
 
95
97
  def call(env)
96
- env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
97
- env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
98
-
98
+ prepare!(env)
99
+
99
100
  status, headers, body = @app.call(env)
100
101
 
101
102
  session_data = env[ENV_SESSION_KEY]
102
103
  options = env[ENV_SESSION_OPTIONS_KEY]
103
104
 
104
- if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
105
- session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
105
+ if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after]
106
+ session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
107
+
108
+ persistent_session_id!(session_data)
106
109
  session_data = marshal(session_data.to_hash)
107
110
 
108
111
  raise CookieOverflow if session_data.size > MAX
109
-
110
112
  cookie = Hash.new
111
113
  cookie[:value] = session_data
112
114
  unless options[:expire_after].nil?
@@ -114,17 +116,20 @@ module ActionController
114
116
  end
115
117
 
116
118
  cookie = build_cookie(@key, cookie.merge(options))
117
- unless headers[HTTP_SET_COOKIE].blank?
118
- headers[HTTP_SET_COOKIE] << "\n#{cookie}"
119
- else
120
- headers[HTTP_SET_COOKIE] = cookie
121
- end
119
+ headers[HTTP_SET_COOKIE] = [] if headers[HTTP_SET_COOKIE].blank?
120
+ headers[HTTP_SET_COOKIE] << cookie
122
121
  end
123
122
 
124
123
  [status, headers, body]
125
124
  end
126
125
 
127
126
  private
127
+
128
+ def prepare!(env)
129
+ env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
130
+ env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
131
+ end
132
+
128
133
  # Should be in Rack::Utils soon
129
134
  def build_cookie(key, value)
130
135
  case value
@@ -146,20 +151,46 @@ module ActionController
146
151
  end
147
152
 
148
153
  def load_session(env)
149
- request = Rack::Request.new(env)
150
- session_data = request.cookies[@key]
151
- data = unmarshal(session_data) || persistent_session_id!({})
154
+ data = unpacked_cookie_data(env)
155
+ data = persistent_session_id!(data)
152
156
  [data[:session_id], data]
153
157
  end
158
+
159
+ def extract_session_id(env)
160
+ if data = unpacked_cookie_data(env)
161
+ persistent_session_id!(data) unless data.empty?
162
+ data[:session_id]
163
+ else
164
+ nil
165
+ end
166
+ end
167
+
168
+ def current_session_id(env)
169
+ env[ENV_SESSION_OPTIONS_KEY][:id]
170
+ end
171
+
172
+ def exists?(env)
173
+ current_session_id(env).present?
174
+ end
175
+
176
+ def unpacked_cookie_data(env)
177
+ env["action_dispatch.request.unsigned_session_cookie"] ||= begin
178
+ stale_session_check! do
179
+ request = Rack::Request.new(env)
180
+ session_data = request.cookies[@key]
181
+ unmarshal(session_data) || {}
182
+ end
183
+ end
184
+ end
154
185
 
155
186
  # Marshal a session hash into safe cookie data. Include an integrity hash.
156
187
  def marshal(session)
157
- @verifier.generate(persistent_session_id!(session))
188
+ @verifier.generate(session)
158
189
  end
159
190
 
160
191
  # Unmarshal cookie data to a hash and verify its integrity.
161
192
  def unmarshal(cookie)
162
- persistent_session_id!(@verifier.verify(cookie)) if cookie
193
+ @verifier.verify(cookie) if cookie
163
194
  rescue ActiveSupport::MessageVerifier::InvalidSignature
164
195
  nil
165
196
  end
@@ -207,6 +238,10 @@ module ActionController
207
238
  ActiveSupport::SecureRandom.hex(16)
208
239
  end
209
240
 
241
+ def destroy(env)
242
+ # session data is stored on client; nothing to do here
243
+ end
244
+
210
245
  def persistent_session_id!(data)
211
246
  (data ||= {}).merge!(inject_persistent_session_id(data))
212
247
  end
@@ -43,6 +43,15 @@ begin
43
43
  rescue MemCache::MemCacheError, Errno::ECONNREFUSED
44
44
  return false
45
45
  end
46
+
47
+ def destroy(env)
48
+ if sid = current_session_id(env)
49
+ @pool.delete(sid)
50
+ end
51
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
52
+ false
53
+ end
54
+
46
55
  end
47
56
  end
48
57
  end
@@ -450,7 +450,7 @@ module ActionController #:nodoc:
450
450
  def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
451
451
  @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
452
452
  @request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
453
- returning __send__(request_method, action, parameters, session, flash) do
453
+ __send__(request_method, action, parameters, session, flash).tap do
454
454
  @request.env.delete 'HTTP_X_REQUESTED_WITH'
455
455
  @request.env.delete 'HTTP_ACCEPT'
456
456
  end
@@ -92,6 +92,14 @@ module ActionController
92
92
  # end
93
93
  # end
94
94
  module UrlWriter
95
+ RESERVED_PCHAR = ':@&=+$,;%'
96
+ SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
97
+ if RUBY_VERSION >= '1.9'
98
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
99
+ else
100
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
101
+ end
102
+
95
103
  def self.included(base) #:nodoc:
96
104
  ActionController::Routing::Routes.install_helpers(base)
97
105
  base.mattr_accessor :default_url_options
@@ -142,7 +150,7 @@ module ActionController
142
150
  end
143
151
  trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
144
152
  url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
145
- anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
153
+ anchor = "##{URI.escape(options.delete(:anchor).to_param.to_s, UNSAFE_PCHAR)}" if options[:anchor]
146
154
  generated = Routing::Routes.generate(options, {})
147
155
  url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
148
156
  url << anchor if anchor
@@ -2,7 +2,7 @@ module ActionPack #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 2
4
4
  MINOR = 3
5
- TINY = 8
5
+ TINY = 9
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -877,9 +877,9 @@ module ActionView
877
877
 
878
878
  def value_before_type_cast(object, method_name)
879
879
  unless object.nil?
880
- object.respond_to?(method_name + "_before_type_cast") ?
881
- object.send(method_name + "_before_type_cast") :
882
- object.send(method_name)
880
+ object.respond_to?(method_name) ?
881
+ object.send(method_name) :
882
+ object.send(method_name + "_before_type_cast")
883
883
  end
884
884
  end
885
885
 
@@ -481,7 +481,7 @@ module ActionView
481
481
  end
482
482
 
483
483
  zone_options += options_for_select(convert_zones[zones], selected)
484
- zone_options
484
+ zone_options.html_safe
485
485
  end
486
486
 
487
487
  private
@@ -440,7 +440,7 @@ module ActionView
440
440
 
441
441
  private
442
442
  def html_options_for_form(url_for_options, options, *parameters_for_url)
443
- returning options.stringify_keys do |html_options|
443
+ options.stringify_keys.tap do |html_options|
444
444
  html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
445
445
  html_options["action"] = url_for(url_for_options, *parameters_for_url)
446
446
  end
@@ -653,7 +653,7 @@ module ActionView
653
653
  # <script> tag.
654
654
  module GeneratorMethods
655
655
  def to_s #:nodoc:
656
- returning javascript = @lines * $/ do
656
+ (@lines * $/).tap do |javascript|
657
657
  if ActionView::Base.debug_rjs
658
658
  source = javascript.dup
659
659
  javascript.replace "try {\n#{source}\n} catch (e) "
@@ -981,8 +981,8 @@ module ActionView
981
981
  end
982
982
 
983
983
  def record(line)
984
- returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
985
- self << line
984
+ "#{line.to_s.chomp.gsub(/\;\z/, '')};".tap do |_line|
985
+ self << _line
986
986
  end
987
987
  end
988
988
 
@@ -532,9 +532,14 @@ module ActionView
532
532
  end
533
533
 
534
534
  AUTO_LINK_RE = %r{
535
- ( https?:// | www\. )
535
+ (?: ([\w+.:-]+:)// | www\. )
536
536
  [^\s<]+
537
- }x unless const_defined?(:AUTO_LINK_RE)
537
+ }x
538
+
539
+ # regexps for determining context, used high-volume
540
+ AUTO_LINK_CRE = [/<[^>]+$/, /^[^>]*>/, /<a\b.*?>/i, /<\/a>/i]
541
+
542
+ AUTO_EMAIL_RE = /[\w.!#\$%+-]+@[\w-]+(?:\.[\w-]+)+/
538
543
 
539
544
  BRACKETS = { ']' => '[', ')' => '(', '}' => '{' }
540
545
 
@@ -543,26 +548,26 @@ module ActionView
543
548
  def auto_link_urls(text, html_options = {})
544
549
  link_attributes = html_options.stringify_keys
545
550
  text.gsub(AUTO_LINK_RE) do
546
- href = $&
547
- punctuation = ''
548
- left, right = $`, $'
549
- # detect already linked URLs and URLs in the middle of a tag
550
- if left =~ /<[^>]+$/ && right =~ /^[^>]*>/
551
+ scheme, href = $1, $&
552
+ punctuation = []
553
+
554
+ if auto_linked?($`, $')
551
555
  # do not change string; URL is already linked
552
556
  href
553
557
  else
554
558
  # don't include trailing punctuation character as part of the URL
555
- if href.sub!(/[^\w\/-]$/, '') and punctuation = $& and opening = BRACKETS[punctuation]
556
- if href.scan(opening).size > href.scan(punctuation).size
557
- href << punctuation
558
- punctuation = ''
559
+ while href.sub!(/[^\w\/-]$/, '')
560
+ punctuation.push $&
561
+ if opening = BRACKETS[punctuation.last] and href.scan(opening).size > href.scan(punctuation.last).size
562
+ href << punctuation.pop
563
+ break
559
564
  end
560
565
  end
561
566
 
562
567
  link_text = block_given?? yield(href) : href
563
- href = 'http://' + href unless href =~ %r{^[a-z]+://}i
568
+ href = 'http://' + href unless scheme
564
569
 
565
- content_tag(:a, h(link_text), link_attributes.merge('href' => href)) + punctuation
570
+ content_tag(:a, h(link_text), link_attributes.merge('href' => href)) + punctuation.reverse.join('')
566
571
  end
567
572
  end
568
573
  end
@@ -570,11 +575,10 @@ module ActionView
570
575
  # Turns all email addresses into clickable links. If a block is given,
571
576
  # each email is yielded and the result is used as the link text.
572
577
  def auto_link_email_addresses(text, html_options = {})
573
- body = text.dup
574
- text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
575
- text = $1
578
+ text.gsub(AUTO_EMAIL_RE) do
579
+ text = $&
576
580
 
577
- if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/)
581
+ if auto_linked?($`, $')
578
582
  text
579
583
  else
580
584
  display_text = (block_given?) ? yield(text) : text
@@ -582,6 +586,12 @@ module ActionView
582
586
  end
583
587
  end
584
588
  end
589
+
590
+ # Detects already linked context or position in the middle of a tag
591
+ def auto_linked?(left, right)
592
+ (left =~ AUTO_LINK_CRE[0] and right =~ AUTO_LINK_CRE[1]) or
593
+ (left.rindex(AUTO_LINK_CRE[2]) and $' !~ AUTO_LINK_CRE[3])
594
+ end
585
595
  end
586
596
  end
587
597
  end
@@ -63,37 +63,37 @@
63
63
  half_a_minute: "half a minute"
64
64
  less_than_x_seconds:
65
65
  one: "less than 1 second"
66
- other: "less than {{count}} seconds"
66
+ other: "less than %{count} seconds"
67
67
  x_seconds:
68
68
  one: "1 second"
69
- other: "{{count}} seconds"
69
+ other: "%{count} seconds"
70
70
  less_than_x_minutes:
71
71
  one: "less than a minute"
72
- other: "less than {{count}} minutes"
72
+ other: "less than %{count} minutes"
73
73
  x_minutes:
74
74
  one: "1 minute"
75
- other: "{{count}} minutes"
75
+ other: "%{count} minutes"
76
76
  about_x_hours:
77
77
  one: "about 1 hour"
78
- other: "about {{count}} hours"
78
+ other: "about %{count} hours"
79
79
  x_days:
80
80
  one: "1 day"
81
- other: "{{count}} days"
81
+ other: "%{count} days"
82
82
  about_x_months:
83
83
  one: "about 1 month"
84
- other: "about {{count}} months"
84
+ other: "about %{count} months"
85
85
  x_months:
86
86
  one: "1 month"
87
- other: "{{count}} months"
87
+ other: "%{count} months"
88
88
  about_x_years:
89
89
  one: "about 1 year"
90
- other: "about {{count}} years"
90
+ other: "about %{count} years"
91
91
  over_x_years:
92
92
  one: "over 1 year"
93
- other: "over {{count}} years"
93
+ other: "over %{count} years"
94
94
  almost_x_years:
95
95
  one: "almost 1 year"
96
- other: "almost {{count}} years"
96
+ other: "almost %{count} years"
97
97
  prompts:
98
98
  year: "Year"
99
99
  month: "Month"
@@ -106,12 +106,12 @@
106
106
  errors:
107
107
  template:
108
108
  header:
109
- one: "1 error prohibited this {{model}} from being saved"
110
- other: "{{count}} errors prohibited this {{model}} from being saved"
109
+ one: "1 error prohibited this %{model} from being saved"
110
+ other: "%{count} errors prohibited this %{model} from being saved"
111
111
  # The variable :count is also available
112
112
  body: "There were problems with the following fields:"
113
113
 
114
114
  support:
115
115
  select:
116
116
  # default value for :prompt => true in FormOptionsHelper
117
- prompt: "Please select"
117
+ prompt: "Please select"
@@ -45,8 +45,8 @@ module ActionView #:nodoc:
45
45
  end
46
46
 
47
47
  def self.new_and_loaded(path)
48
- returning new(path) do |path|
49
- path.load!
48
+ new(path).tap do |_path|
49
+ _path.load!
50
50
  end
51
51
  end
52
52
 
@@ -58,4 +58,21 @@ class DummyMutex
58
58
  end
59
59
  end
60
60
 
61
+ class ActionController::IntegrationTest < ActiveSupport::TestCase
62
+ def with_autoload_path(path)
63
+ path = File.join(File.dirname(__FILE__), "fixtures", path)
64
+ if ActiveSupport::Dependencies.autoload_paths.include?(path)
65
+ yield
66
+ else
67
+ begin
68
+ ActiveSupport::Dependencies.autoload_paths << path
69
+ yield
70
+ ensure
71
+ ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
72
+ ActiveSupport::Dependencies.clear
73
+ end
74
+ end
75
+ end
76
+ end
77
+
61
78
  ActionController::Reloader.default_lock = DummyMutex.new
@@ -22,7 +22,6 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
22
22
  end
23
23
 
24
24
  def get_session_id
25
- session[:foo]
26
25
  render :text => "#{request.session_options[:id]}"
27
26
  end
28
27
 
@@ -45,23 +44,27 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
45
44
  ActiveRecord::SessionStore.session_class.drop_table!
46
45
  end
47
46
 
48
- def test_setting_and_getting_session_value
49
- with_test_route_set do
50
- get '/set_session_value'
51
- assert_response :success
52
- assert cookies['_session_id']
53
-
54
- get '/get_session_value'
55
- assert_response :success
56
- assert_equal 'foo: "bar"', response.body
57
-
58
- get '/set_session_value', :foo => "baz"
59
- assert_response :success
60
- assert cookies['_session_id']
61
-
62
- get '/get_session_value'
63
- assert_response :success
64
- assert_equal 'foo: "baz"', response.body
47
+ %w{ session sql_bypass }.each do |class_name|
48
+ define_method("test_setting_and_getting_session_value_with_#{class_name}_store") do
49
+ with_store class_name do
50
+ with_test_route_set do
51
+ get '/set_session_value'
52
+ assert_response :success
53
+ assert cookies['_session_id']
54
+
55
+ get '/get_session_value'
56
+ assert_response :success
57
+ assert_equal 'foo: "bar"', response.body
58
+
59
+ get '/set_session_value', :foo => "baz"
60
+ assert_response :success
61
+ assert cookies['_session_id']
62
+
63
+ get '/get_session_value'
64
+ assert_response :success
65
+ assert_equal 'foo: "baz"', response.body
66
+ end
67
+ end
65
68
  end
66
69
  end
67
70
 
@@ -107,6 +110,38 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
107
110
  end
108
111
  end
109
112
 
113
+ def test_getting_session_value
114
+ with_test_route_set do
115
+ get '/set_session_value'
116
+ assert_response :success
117
+ assert cookies['_session_id']
118
+
119
+ get '/get_session_value'
120
+ assert_response :success
121
+ assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
122
+ session_id = cookies["_session_id"]
123
+
124
+ get '/call_reset_session'
125
+ assert_response :success
126
+ assert_not_equal [], headers['Set-Cookie']
127
+
128
+ cookies["_session_id"] = session_id # replace our new session_id with our old, pre-reset session_id
129
+
130
+ get '/get_session_value'
131
+ assert_response :success
132
+ assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from the database"
133
+ end
134
+ end
135
+
136
+ def test_getting_from_nonexistent_session
137
+ with_test_route_set do
138
+ get '/get_session_value'
139
+ assert_response :success
140
+ assert_equal 'foo: nil', response.body
141
+ assert_nil cookies['_session_id'], "should only create session on write, not read"
142
+ end
143
+ end
144
+
110
145
  def test_prevents_session_fixation
111
146
  with_test_route_set do
112
147
  get '/set_session_value'
@@ -171,4 +206,16 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
171
206
  yield
172
207
  end
173
208
  end
209
+
210
+ def with_store(class_name)
211
+ begin
212
+ session_class = ActiveRecord::SessionStore.session_class
213
+ ActiveRecord::SessionStore.session_class = "ActiveRecord::SessionStore::#{class_name.camelize}".constantize
214
+ yield
215
+ rescue
216
+ ActiveRecord::SessionStore.session_class = session_class
217
+ raise
218
+ end
219
+ end
220
+
174
221
  end