diff options
Diffstat (limited to 'actionpack')
79 files changed, 1320 insertions, 576 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 3fc3e06160..913edbd8df 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,113 @@ ## Rails 4.0.0 (unreleased) ## +* Fix explicit names on multiple file fields. If a file field tag has + the multiple option, it is turned into an array field (appending `[]`), + but if an explicit name is passed to `file_field` the `[]` is not + appended. + Fixes #9830. + + *Ryan McGeary* + +* Add block support for the `mail_to` helper, similar to the `link_to` helper. + + *Sam Pohlenz* + +* Automatically configure cookie-based sessions to be encrypted if + `secret_key_base` is set, falling back to signed if only `secret_token` + is set. Automatically upgrade existing signed cookie-based sessions from + Rails 3.x to be encrypted if both `secret_key_base` and `secret_token` + are set, or signed with the new key generator if only `secret_token` is + set. This leaves only the `config.session_store :cookie_store` option and + removes the two new options introduced in 4.0.0.beta1: + `encrypted_cookie_store` and `upgrade_signature_to_encryption_cookie_store`. + + *Trevor Turk* + +* Ensure consistent fallback to the default layout lookup for layouts set + using symbols or procs that return `nil`. + + All of the following layouts will result in the default layout lookup: + + layout nil + + layout proc { nil } + + layout :returns_nil + def returns_nil + nil + end + + Previously symbols and procs which returned `nil` resulted in no layout which + differed from the `layout nil` behavior. To get the "no layout" behavior just + return `false` instead of `nil` for `layout`. + + *Chris Nicola* + +* Create `UpgradeLegacySignedCookieJar` to transparently upgrade existing signed + cookies generated by Rails 3.x to avoid invalidating them when upgrading to Rails 4.x. + + *Trevor Turk + Neeraj Singh* + +* Raise an `ArgumentError` when a clashing named route is defined. + + *Trevor Turk* + +* Allow default url options to accept host with protocol such as `http://` + + config.action_mailer.default_url_options = { host: "http://mydomain.com" } + + *Richard Schneeman* + +* Ensure that digest authentication responds with a 401 status when a basic + header is received. + + *Brad Dunbar* + +* Include I18n locale fallbacks in view lookup. + Fixes GH#3512. + + *Juan Barreneche* + +* Integration and functional tests allow headers and rack env + variables to be passed when performing requests. + Fixes #6513. + + Example: + + # integration test + get "/success", {}, "HTTP_REFERER" => "http://test.com/", + "Accepts" => "text/plain, text/html" + + # functional test + @request.headers["Accepts"] = "text/plain, text/html" + + *Yves Senn* + +* Http::Headers respects headers that are not prefixed with HTTP_ + + *Yves Senn* + +* Fix incorrectly appended square brackets to a multiple select box + if an explicit name has been given and it already ends with "[]" + + Before: + + select(:category, [], {}, multiple: true, name: "post[category][]") + # => <select name="post[category][][]" ...> + + After: + + select(:category, [], {}, multiple: true, name: "post[category][]") + # => <select name="post[category][]" ...> + + *Olek Janiszewski* + +* Fixed regression when using `assert_template` to verify files sent using + `render file: 'README.md'`. + Fixes #9464. + + *Justin Coyne* + * Fixed `ActionView::Helpers::CaptureHelper#content_for` regression when trying to use it in a boolean statement. Fixes #9360. @@ -39,6 +147,7 @@ *Thierry Zires* + ## Rails 4.0.0.beta1 (February 25, 2013) ## * Fix `respond_to` not using formats that have no block if all is present. *Michael Grosser* @@ -231,12 +340,12 @@ Client-IP and Remote-Addr headers, in that order. Document the rationale for that decision, and describe the options that can be passed to the RemoteIp middleware to change it. - Fix #7979 + Fixes #7979. *André Arko*, *Steve Klabnik*, *Alexey Gaziev* * Do not append second slash to `root_url` when using `trailing_slash: true` - Fix #8700 + Fixes #8700. Before: @@ -264,7 +373,7 @@ * Do not append `charset=` parameter when `head` is called with a `:content_type` option. - Fix #8661. + Fixes #8661. *Yves Senn* @@ -422,7 +531,7 @@ * Render every partial with a new `ActionView::PartialRenderer`. This resolves issues when rendering nested partials. - Fix #8197. + Fixes #8197. *Yves Senn* @@ -430,7 +539,7 @@ of mime types where template text is not html escaped by default. It prevents `Jack & Joe` from rendering as `Jack & Joe` for the whitelisted mime types. The default whitelist contains `text/plain`. - Fix #7976. + Fixes #7976. *Joost Baaij* @@ -446,7 +555,7 @@ check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1) # => <input name=\"post[foo][comment_ids][]\" type=\"hidden\" value=\"0\" /><input id=\"post_foo_comment_ids_1\" name=\"post[foo][comment_ids][]\" type=\"checkbox\" value=\"1\" /> - Fix #8108. + Fixes #8108. *Daniel Fox, Grant Hutchins & Trace Wax* @@ -469,7 +578,7 @@ *Josh Peek* * `assert_template` can be used to assert on the same template with different locals - Fix #3675. + Fixes #3675. *Yves Senn* @@ -480,7 +589,7 @@ * Accept `:remote` as symbolic option for `link_to` helper. *Riley Lynch* * Warn when the `:locals` option is passed to `assert_template` outside of a view test case - Fix #3415. + Fixes #3415. *Yves Senn* @@ -504,12 +613,12 @@ * Rename internal variables on `ActionController::TemplateAssertions` to prevent naming collisions. `@partials`, `@templates` and `@layouts` are now prefixed with an underscore. - Fix #7459. + Fixes #7459. *Yves Senn* * `resource` and `resources` don't modify the passed options hash. - Fix #7777. + Fixes #7777. *Yves Senn* @@ -573,7 +682,7 @@ *Guillermo Iguaran* * Log now displays the correct status code when an exception is raised. - Fix #7646. + Fixes #7646. *Yves Senn* diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 03eeb841ee..cc8351a489 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -26,5 +26,5 @@ Gem::Specification.new do |s| s.add_dependency 'erubis', '~> 2.7.0' s.add_development_dependency 'activemodel', version - s.add_development_dependency 'tzinfo', '~> 0.3.33' + s.add_development_dependency 'tzinfo', '~> 0.3.37' end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 56dc9ab7a1..af5de815bb 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -35,7 +35,7 @@ module AbstractController end def inherited(klass) # :nodoc: - # define the abstract ivar on subclasses so that we don't get + # Define the abstract ivar on subclasses so that we don't get # uninitialized ivar warnings unless klass.instance_variable_defined?(:@abstract) klass.instance_variable_set(:@abstract, false) diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 812a35735f..5ae8c6c3b0 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -29,7 +29,7 @@ module AbstractController # helper_method :current_user, :logged_in? # # def current_user - # @current_user ||= User.find_by_id(session[:user]) + # @current_user ||= User.find_by(id: session[:user]) # end # # def logged_in? @@ -59,7 +59,7 @@ module AbstractController # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Options - # * <tt>*args</tt> - Module, Symbol, String, :all + # * <tt>*args</tt> - Module, Symbol, String # * <tt>block</tt> - A block defining helper methods # # When the argument is a module it will be included directly in the template class. diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 91864f2a35..bac994496e 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -285,10 +285,9 @@ module AbstractController remove_possible_method(:_layout) prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] + default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super" name_clause = if name - <<-RUBY - lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first || super - RUBY + default_behavior else <<-RUBY super @@ -301,6 +300,7 @@ module AbstractController when Symbol <<-RUBY #{_layout}.tap do |layout| + return #{default_behavior} if layout.nil? unless layout.is_a?(String) || !layout raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \ "should have returned a String, false, or nil" @@ -308,8 +308,12 @@ module AbstractController end RUBY when Proc - define_method :_layout_from_proc, &_layout - _layout.arity == 0 ? "_layout_from_proc" : "_layout_from_proc(self)" + define_method :_layout_from_proc, &_layout + <<-RUBY + result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'}) + return #{default_behavior} if result.nil? + result + RUBY when false nil when true diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index f8e4cb4384..3f34add790 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -105,8 +105,8 @@ module AbstractController # # If a component extends the semantics of response_body # (as Action Controller extends it to be anything that - # responds to the method each), this method needs to - # overriden in order to still return a string. + # responds to the method each), this method needs to be + # overridden in order to still return a string. # :api: plugin def render_to_string(*args, &block) options = _normalize_render(*args, &block) diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index db48022b9f..02028d8e05 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -11,7 +11,7 @@ module AbstractController def translate(*args) key = args.first if key.is_a?(String) && (key[0] == '.') - key = "#{ controller_path.gsub('/', '.') }.#{ action_name }#{ key }" + key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }" args[0] = key end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 3d274e7dd7..7318c8b7ec 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -48,6 +48,11 @@ module ActionController info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") end + def unpermitted_parameters(event) + unpermitted_keys = event.payload[:keys] + debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") + end + %w(write_fragment read_fragment exist_fragment? expire_fragment expire_page write_page).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb index 2aa6b7adaf..af36ffa240 100644 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ b/actionpack/lib/action_controller/metal/hide_actions.rb @@ -27,7 +27,7 @@ module ActionController end def visible_action?(action_name) - action_methods.include?(action_name) + not hidden_actions.include?(action_name) end # Overrides AbstractController::Base#action_methods to remove any methods diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index e295002b16..158d552ec7 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -29,7 +29,7 @@ module ActionController # # protected # def set_account - # @account = Account.find_by_url_name(request.subdomains.first) + # @account = Account.find_by(url_name: request.subdomains.first) # end # # def authenticate @@ -299,6 +299,7 @@ module ActionController # allow a user to use new nonce without prompting user again for their # username and password. def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) + return false if value.nil? t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end @@ -344,7 +345,7 @@ module ActionController # # protected # def set_account - # @account = Account.find_by_url_name(request.subdomains.first) + # @account = Account.find_by(url_name: request.subdomains.first) # end # # def authenticate diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 32e5afa335..fb664a69dd 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -14,6 +14,7 @@ module ActionController # response.stream.write "hello world\n" # sleep 1 # } + # ensure # response.stream.close # end # end @@ -97,6 +98,10 @@ module ActionController def merge_default_headers(original, default) Header.new self, super end + + def handle_conditional_get! + super unless committed? + end end def process(name) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index e4dcd3213f..23d70c9ea2 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' +require 'stringio' module ActionController # Raised when a required parameter is missing. @@ -68,6 +69,8 @@ module ActionController # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> # in test and development environments, +false+ otherwise. # + # Examples: + # # params = ActionController::Parameters.new # params.permitted? # => false # @@ -339,7 +342,8 @@ module ActionController if unpermitted_keys.any? case self.class.action_on_unpermitted_parameters when :log - ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}" + name = "unpermitted_parameters.action_controller" + ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys) when :raise raise ActionController::UnpermittedParameters.new(unpermitted_keys) end @@ -417,7 +421,7 @@ module ActionController # Declaration { comment_ids: [] }. array_of_permitted_scalars_filter(params, key) else - # Declaration { user: :name } or { user: [:name, :age, { adress: ... }] }. + # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| if element.is_a?(Hash) element = self.class.new(element) unless element.respond_to?(:permit) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index bba1f1e201..b9a5e78fe9 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -16,6 +16,7 @@ module ActionController @_partials = Hash.new(0) @_templates = Hash.new(0) @_layouts = Hash.new(0) + @_files = Hash.new(0) ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload| path = payload[:layout] @@ -39,6 +40,16 @@ module ActionController @_templates[path] += 1 end + + ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload| + next if payload[:virtual_path] # files don't have virtual path + + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end + end end def teardown_subscriptions @@ -106,7 +117,7 @@ module ActionController end assert matches_template, msg when Hash - options.assert_valid_keys(:layout, :partial, :locals, :count) + options.assert_valid_keys(:layout, :partial, :locals, :count, :file) if options.key?(:layout) expected_layout = options[:layout] @@ -123,6 +134,10 @@ module ActionController end end + if options[:file] + assert_includes @_files.keys, options[:file] + end + if expected_partial = options[:partial] if expected_locals = options[:locals] if defined?(@_rendered_views) @@ -301,7 +316,7 @@ module ActionController # assert_response :found # # # Assert that the controller really put the book in the database. - # assert_not_nil Book.find_by_title("Love Hina") + # assert_not_nil Book.find_by(title: "Love Hina") # end # end # @@ -436,37 +451,55 @@ module ActionController end - # Executes a request simulating GET HTTP method and set/volley the response + # Simulate a GET request with the given parameters. + # + # - +action+: The controller action to call. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). + # - +session+: A Hash of parameters to store in the session. This may be +nil+. + # - +flash+: A Hash of parameters to store in the flash. This may be +nil+. + # + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +#post+, +#patch+, +#put+, +#delete+, and +#head+. + # Note that the request method is not verified. The different methods are + # available to make the tests more expressive. def get(action, *args) process(action, "GET", *args) end - # Executes a request simulating POST HTTP method and set/volley the response + # Simulate a POST request with the given parameters and set/volley the response. + # See +#get+ for more details. def post(action, *args) process(action, "POST", *args) end - # Executes a request simulating PATCH HTTP method and set/volley the response + # Simulate a PATCH request with the given parameters and set/volley the response. + # See +#get+ for more details. def patch(action, *args) process(action, "PATCH", *args) end - # Executes a request simulating PUT HTTP method and set/volley the response + # Simulate a PUT request with the given parameters and set/volley the response. + # See +#get+ for more details. def put(action, *args) process(action, "PUT", *args) end - # Executes a request simulating DELETE HTTP method and set/volley the response + # Simulate a DELETE request with the given parameters and set/volley the response. + # See +#get+ for more details. def delete(action, *args) process(action, "DELETE", *args) end - # Executes a request simulating HEAD HTTP method and set/volley the response + # Simulate a HEAD request with the given parameters and set/volley the response. + # See +#get+ for more details. def head(action, *args) process(action, "HEAD", *args) end - # Executes a request simulating OPTIONS HTTP method and set/volley the response + # Simulate a OPTIONS request with the given parameters and set/volley the response. + # See +#get+ for more details. def options(action, *args) process(action, "OPTIONS", *args) end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 618e2f3033..2c88bc3b1f 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -84,8 +84,6 @@ module ActionDispatch module Session autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' - autoload :EncryptedCookieStore, 'action_dispatch/middleware/session/cookie_store' - autoload :UpgradeSignatureToEncryptionCookieStore, 'action_dispatch/middleware/session/cookie_store' autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 0d6015d993..f9b278349e 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -92,7 +92,7 @@ module ActionDispatch LAST_MODIFIED = "Last-Modified".freeze ETAG = "ETag".freeze CACHE_CONTROL = "Cache-Control".freeze - SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate] + SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate] def cache_control_segments if cache_control = self[CACHE_CONTROL] @@ -108,7 +108,7 @@ module ActionDispatch cache_control_segments.each do |segment| directive, argument = segment.split('=', 2) - if SPESHUL_KEYS.include? directive + if SPECIAL_KEYS.include? directive key = directive.tr('-', '_') cache_control[key.to_sym] = argument || true else diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index dc04d4577b..2666cd4b0a 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,38 +1,62 @@ module ActionDispatch module Http class Headers + CGI_VARIABLES = %w( + CONTENT_TYPE CONTENT_LENGTH + HTTPS AUTH_TYPE GATEWAY_INTERFACE + PATH_INFO PATH_TRANSLATED QUERY_STRING + REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER + REQUEST_METHOD SCRIPT_NAME + SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE + ) + HTTP_HEADER = /\A[A-Za-z0-9-]+\z/ + include Enumerable + attr_reader :env def initialize(env = {}) - @headers = env + @env = env + end + + def [](key) + @env[env_name(key)] end - def [](header_name) - @headers[env_name(header_name)] + def []=(key, value) + @env[env_name(key)] = value end - def []=(k,v); @headers[k] = v; end - def key?(k); @headers.key? k; end + def key?(key); @env.key? key; end alias :include? :key? - def fetch(header_name, *args, &block) - @headers.fetch env_name(header_name), *args, &block + def fetch(key, *args, &block) + @env.fetch env_name(key), *args, &block end def each(&block) - @headers.each(&block) + @env.each(&block) end - private + def merge(headers_or_env) + headers = Http::Headers.new(env.dup) + headers.merge!(headers_or_env) + headers + end - # Converts a HTTP header name to an environment variable name if it is - # not contained within the headers hash. - def env_name(header_name) - @headers.include?(header_name) ? header_name : cgi_name(header_name) + def merge!(headers_or_env) + headers_or_env.each do |key, value| + self[env_name(key)] = value + end end - def cgi_name(k) - "HTTP_#{k.upcase.gsub(/-/, '_')}" + private + def env_name(key) + key = key.to_s + if key =~ HTTP_HEADER + key = key.upcase.tr('-', '_') + key = "HTTP_" + key unless CGI_VARIABLES.include?(key) + end + key end end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 446862aad0..246d9c121a 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -18,7 +18,7 @@ module ActionDispatch query_parameters.dup end params.merge!(path_parameters) - encode_params(params).with_indifferent_access + params.with_indifferent_access end end alias :params :parameters @@ -50,40 +50,33 @@ module ActionDispatch private + # Convert nested Hash to HashWithIndifferentAccess + # and UTF-8 encode both keys and values in nested Hash. + # # TODO: Validate that the characters are UTF-8. If they aren't, # you'll get a weird error down the road, but our form handling # should really prevent that from happening - def encode_params(params) + def normalize_encode_params(params) if params.is_a?(String) return params.force_encoding(Encoding::UTF_8).encode! elsif !params.is_a?(Hash) return params end + new_hash = {} params.each do |k, v| - case v - when Hash - encode_params(v) - when Array - v.map! {|el| encode_params(el) } - else - encode_params(v) - end - end - end - - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess - def normalize_parameters(value) - case value - when Hash - h = {} - value.each { |k, v| h[k] = normalize_parameters(v) } - h.with_indifferent_access - when Array - value.map { |e| normalize_parameters(e) } - else - value + new_key = k.is_a?(String) ? k.dup.force_encoding("UTF-8").encode! : k + new_hash[new_key] = + case v + when Hash + normalize_encode_params(v) + when Array + v.map! {|el| normalize_encode_params(el) } + else + normalize_encode_params(v) + end end + new_hash.with_indifferent_access end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7b04d6e851..ebd87c40b5 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -156,14 +156,29 @@ module ActionDispatch @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end + # Returns the +String+ full path including params of the last URL requested. + # + # # get "/articles" + # request.fullpath # => "/articles" + # + # # get "/articles?page=2" + # request.fullpath # => "/articles?page=2" def fullpath @fullpath ||= super end + # Returns the original request URL as a +String+. + # + # # get "/articles?page=2" + # request.original_url # => "http://www.example.com/articles?page=2" def original_url base_url + original_fullpath end + # The +String+ MIME type of the request. + # + # # get "/articles" + # request.media_type # => "application/x-www-form-urlencoded" def media_type content_mime_type.to_s end @@ -256,7 +271,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -264,7 +279,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {}) + @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {}) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 67cb7fbcb5..b57c84dec8 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -6,7 +6,7 @@ module ActionDispatch # of its interface is available directly for convenience. # # Uploaded files are temporary files whose lifespan is one request. When - # the object is finalized Ruby unlinks the file, so there is not need to + # the object is finalized Ruby unlinks the file, so there is no need to # clean them with a separate maintenance task. class UploadedFile # The basename of the file in the client. @@ -75,16 +75,16 @@ module ActionDispatch end module Upload # :nodoc: - # Convert nested Hash to ActiveSupport::HashWithIndifferentAccess and replace - # file upload hash with UploadedFile objects - def normalize_parameters(value) + # Replace file upload hash with UploadedFile objects + # when normalize and encode parameters. + def normalize_encode_params(value) if Hash === value && value.has_key?(:tempfile) UploadedFile.new(value) else super end end - private :normalize_parameters + private :normalize_encode_params end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 97ac462411..ab5399c8ea 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -59,8 +59,9 @@ module ActionDispatch result = "" unless options[:only_path] + protocol = extract_protocol(options) unless options[:protocol] == false - result << (options[:protocol] || "http") + result << protocol result << ":" unless result.match(%r{:|//}) end result << "//" unless result.match("//") @@ -83,6 +84,16 @@ module ActionDispatch end end + # Extracts protocol http:// or https:// from options[:host] + # needs to be called whether the :protocol is being used or not + def extract_protocol(options) + if options[:host] && match = options[:host].match(/(^.*:\/\/)(.*)/) + options[:protocol] ||= match[1] + options[:host] = match[2] + end + options[:protocol] || "http" + end + def host_or_subdomain_and_domain(options) return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 36a0db6e61..5b914f293d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' @@ -30,7 +31,7 @@ module ActionDispatch # # # Sets a signed cookie, which prevents users from tampering with its value. # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:key]</tt> + # # It can be read using the signed method <tt>cookies.signed[:name]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -52,13 +53,13 @@ module ActionDispatch # # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie: # - # cookies[:key] = { + # cookies[:name] = { # value: 'a yummy cookie', # expires: 1.year.from_now, # domain: 'domain.com' # } # - # cookies.delete(:key, domain: 'domain.com') + # cookies.delete(:name, domain: 'domain.com') # # The option symbols for setting cookies are: # @@ -69,7 +70,7 @@ module ActionDispatch # restrict to the domain level. If you use a schema like www.example.com # and want to share session with user.example.com set <tt>:domain</tt> # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with - # <tt>:all</tt> again when deleting keys. + # <tt>:all</tt> again when deleting cookies. # # domain: nil # Does not sets cookie domain. (default) # domain: :all # Allow the cookie for the top most level @@ -86,7 +87,8 @@ module ActionDispatch SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze - TOKEN_KEY = "action_dispatch.secret_token".freeze + SECRET_TOKEN = "action_dispatch.secret_token".freeze + SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -94,8 +96,99 @@ module ActionDispatch # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError + # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed + module ChainedCookieJars + # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: + # + # cookies.permanent[:prefers_open_id] = true + # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + # + # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. + # + # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: + # + # cookies.permanent.signed[:remember_me] = current_user.id + # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + def permanent + @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + end + + # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from + # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed + # cookie was tampered with by the user (or a 3rd party), nil will be returned. + # + # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # legacy cookies signed with the old key generator will be transparently upgraded. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # + # Example: + # + # cookies.signed[:discount] = 45 + # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ + # + # cookies.signed[:discount] # => 45 + def signed + @signed ||= + if @options[:upgrade_legacy_signed_cookies] + UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + else + SignedCookieJar.new(self, @key_generator, @options) + end + end + + # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. + # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. + # + # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # legacy cookies signed with the old key generator will be transparently upgraded. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # + # Example: + # + # cookies.encrypted[:discount] = 45 + # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # + # cookies.encrypted[:discount] # => 45 + def encrypted + @encrypted ||= + if @options[:upgrade_legacy_signed_cookies] + UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) + else + EncryptedCookieJar.new(self, @key_generator, @options) + end + end + + # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set. + # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. + def signed_or_encrypted + @signed_or_encrypted ||= + if @options[:secret_key_base].present? + encrypted + else + signed + end + end + end + + module VerifyAndUpgradeLegacySignedMessage + def initialize(*args) + super + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) + end + + def verify_and_upgrade_legacy_signed_message(name, signed_message) + @legacy_verifier.verify(signed_message).tap do |value| + self[name] = value + end + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + end + class CookieJar #:nodoc: - include Enumerable + include Enumerable, ChainedCookieJars # This regular expression is used to split the levels of a domain. # The top level domain can be any string without a period or @@ -115,7 +208,10 @@ module ActionDispatch { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - token_key: env[TOKEN_KEY] } + secret_token: env[SECRET_TOKEN], + secret_key_base: env[SECRET_KEY_BASE], + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + } end def self.build(request) @@ -184,7 +280,7 @@ module ActionDispatch # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! value = options[:value] @@ -195,10 +291,10 @@ module ActionDispatch handle_options(options) - if @cookies[key.to_s] != value or options[:expires] - @cookies[key.to_s] = value - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[name.to_s] != value or options[:expires] + @cookies[name.to_s] = value + @set_cookies[name.to_s] = options + @delete_cookies.delete(name.to_s) end value @@ -207,24 +303,24 @@ module ActionDispatch # Removes the cookie on the client machine by setting the value to an empty string # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. - def delete(key, options = {}) - return unless @cookies.has_key? key.to_s + def delete(name, options = {}) + return unless @cookies.has_key? name.to_s options.symbolize_keys! handle_options(options) - value = @cookies.delete(key.to_s) - @delete_cookies[key.to_s] = options + value = @cookies.delete(name.to_s) + @delete_cookies[name.to_s] = options value end # Whether the given cookie is to be deleted by this CookieJar. # Like <tt>[]=</tt>, you can pass in an options hash to test if a # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc. - def deleted?(key, options = {}) + def deleted?(name, options = {}) options.symbolize_keys! handle_options(options) - @delete_cookies[key.to_s] == options + @delete_cookies[name.to_s] == options end # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie @@ -232,59 +328,6 @@ module ActionDispatch @cookies.each_key{ |k| delete(k, options) } end - # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: - # - # cookies.permanent[:prefers_open_id] = true - # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - # - # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. - # - # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: - # - # cookies.permanent.signed[:remember_me] = current_user.id - # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from - # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will - # be raised. - # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. - # - # Example: - # - # cookies.signed[:discount] = 45 - # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ - # - # cookies.signed[:discount] # => 45 - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end - - # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this - def signed_using_old_secret #:nodoc: - @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:token_key]), @options) - end - - # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. - # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception - # will be raised. - # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. - # - # Example: - # - # cookies.encrypted[:discount] = 45 - # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ - # - # cookies.encrypted[:discount] # => 45 - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end - def write(headers) @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } @@ -299,24 +342,25 @@ module ActionDispatch self.always_write_cookie = false private - def write_cookie?(cookie) @secure || !cookie[:secure] || always_write_cookie end end class PermanentCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @key_generator = key_generator @options = options end - def [](key) + def [](name) @parent_jar[name.to_s] end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else @@ -324,28 +368,13 @@ module ActionDispatch end options[:expires] = 20.years.from_now - @parent_jar[key] = options - end - - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end - - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end - - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." + @parent_jar[name] = options end end class SignedCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options @@ -355,13 +384,11 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - @verifier.verify(signed_message) + verify(signed_message) end - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! options[:value] = @verifier.generate(options[:value]) @@ -370,32 +397,38 @@ module ActionDispatch end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE - @parent_jar[key] = options + @parent_jar[name] = options end - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end + private + def verify(signed_message) + @verifier.verify(signed_message) + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + end - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end + # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if + # config.secret_token and config.secret_key_base are both set. It reads + # legacy cookies signed with the old dummy key generator and re-saves + # them using the new key generator to provide a smooth upgrade path. + class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: + include VerifyAndUpgradeLegacySignedMessage - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." + def [](name) + if signed_message = @parent_jar[name] + verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + end end end class EncryptedCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) - if ActiveSupport::DummyKeyGenerator === key_generator - raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." + - "Set config.secret_key_base in config/initializers/secret_token.rb" + if ActiveSupport::LegacyKeyGenerator === key_generator + raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + "Read the upgrade documentation to learn more about this new config option." end @parent_jar = parent_jar @@ -405,16 +438,13 @@ module ActionDispatch @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) end - def [](key) - if encrypted_message = @parent_jar[key] - @encryptor.decrypt_and_verify(encrypted_message) + def [](name) + if encrypted_message = @parent_jar[name] + decrypt_and_verify(encrypted_message) end - rescue ActiveSupport::MessageVerifier::InvalidSignature, - ActiveSupport::MessageEncryptor::InvalidMessage - nil end - def []=(key, options) + def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else @@ -423,24 +453,28 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(options[:value]) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE - @parent_jar[key] = options + @parent_jar[name] = options end - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end + private + def decrypt_and_verify(encrypted_message) + @encryptor.decrypt_and_verify(encrypted_message) + rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage + nil + end + end - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end + # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore + # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # are both set. It reads legacy cookies signed with the old dummy key generator and + # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. + class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: + include VerifyAndUpgradeLegacySignedMessage - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." + def [](name) + if encrypted_or_signed_message = @parent_jar[name] + decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 93a2b52996..8879291dbd 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -101,7 +101,7 @@ module ActionDispatch (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4 - (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining + (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending )$) }x diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 1e6ed624b0..b9eb8036e9 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -4,36 +4,51 @@ require 'rack/session/cookie' module ActionDispatch module Session - # This cookie-based session store is the Rails default. Sessions typically - # contain at most a user_id and flash message; both fit within the 4K cookie - # size limit. Cookie-based sessions are dramatically faster than the - # alternatives. + # This cookie-based session store is the Rails default. It is + # dramatically faster than the alternatives. # - # If you have more than 4K of session data or don't want your data to be - # visible to the user, pick another session store. + # Sessions typically contain at most a user_id and flash message; both fit + # within the 4K cookie size limit. A CookieOverflow exception is raised if + # you attempt to store more than 4K of data. # - # CookieOverflow is raised if you attempt to store more than 4K of data. + # The cookie jar used for storage is automatically configured to be the + # best possible option given your application's configuration. # - # A message digest is included with the cookie to ensure data integrity: - # a user cannot alter his +user_id+ without knowing the secret key - # included in the hash. New apps are generated with a pregenerated secret - # in config/environment.rb. Set your own for old apps you're upgrading. + # If you only have secret_token set, your cookies will be signed, but + # not encrypted. This means a user cannot alter his +user_id+ without + # knowing your app's secret key, but can easily read his +user_id+. This + # was the default for Rails 3 apps. # - # Session options: + # If you have secret_key_base set, your cookies will be encrypted. This + # goes a step further than signed cookies in that encrypted cookies cannot + # be altered or read by users. This is the default starting in Rails 4. # - # * <tt>:secret</tt>: An application-wide key string. It's important that - # the secret is not vulnerable to a dictionary attack. Therefore, you - # should choose a secret consisting of random numbers and letters and - # more than 30 characters. + # If you have both secret_token and secret_key base set, your cookies will + # be encrypted, and signed cookies generated by Rails 3 will be + # transparently read and encrypted to provide a smooth upgrade path. # - # secret: '449fe2e7daee471bffae2fd8dc02313d' + # Configure your session store in config/initializers/session_store.rb: # - # * <tt>:digest</tt>: The message digest algorithm used to verify session - # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL, - # such as 'MD5', 'RIPEMD160', 'SHA256', etc. + # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' # - # To generate a secret key for an existing application, run - # "rake secret" and set the key in config/initializers/secret_token.rb. + # Configure your secret key in config/initializers/secret_token.rb: + # + # Myapp::Application.config.secret_key_base 'secret key' + # + # To generate a secret key for an existing application, run `rake secret`. + # + # If you are upgrading an existing Rails 3 app, you should leave your + # existing secret_token in place and simply add the new secret_key_base. + # Note that you should wait to set secret_key_base until you have 100% of + # your userbase on Rails 4 and are reasonably sure you will not need to + # rollback to Rails 3. This is because cookies signed based on the new + # secret_key_base in Rails 4 are not backwards compatible with Rails 3. + # You are free to leave your existing secret_token in place, not set the + # new secret_key_base, and ignore the deprecation warnings until you are + # reasonably sure that your upgrade is otherwise complete. Additionally, + # you should take care to make sure you are not relying on the ability to + # decode signed cookies generated by your app in external applications or + # Javascript before upgrading. # # Note that changing digest or secret invalidates all existing sessions! class CookieStore < Rack::Session::Abstract::ID @@ -100,42 +115,7 @@ module ActionDispatch def cookie_jar(env) request = ActionDispatch::Request.new(env) - request.cookie_jar.signed - end - end - - class EncryptedCookieStore < CookieStore - - private - - def cookie_jar(env) - request = ActionDispatch::Request.new(env) - request.cookie_jar.encrypted - end - end - - # This cookie store helps you upgrading apps that use +CookieStore+ to the new default +EncryptedCookieStore+ - # To use this CookieStore set - # - # Myapp::Application.config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session' - # - # in your config/initializers/session_store.rb - # - # You will also need to add - # - # Myapp::Application.config.secret_key_base = 'some secret' - # - # in your config/initializers/secret_token.rb, but do not remove +Myapp::Application.config.secret_token = 'some secret'+ - class UpgradeSignatureToEncryptionCookieStore < EncryptedCookieStore - private - - def get_cookie(env) - signed_using_old_secret_cookie_jar(env)[@key] || cookie_jar(env)[@key] - end - - def signed_using_old_secret_cookie_jar(env) - request = ActionDispatch::Request.new(env) - request.cookie_jar.signed_using_old_secret + request.cookie_jar.signed_or_encrypted end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb index 61690d3e50..cd3daff065 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb @@ -8,7 +8,7 @@ <h2>Failure reasons:</h2> <ol> <% @exception.failures.each do |route, reason| %> - <li><code><%= route.inspect.gsub('\\', '') %></code> failed because <%= reason.downcase %></li> + <li><code><%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %></li> <% end %> </ol> </p> diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index d55eb8109a..550c7d0e7b 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -69,6 +69,22 @@ module ActionDispatch # <tt>Routing::Mapper::Scoping#namespace</tt>, and # <tt>Routing::Mapper::Scoping#scope</tt>. # + # == Non-resourceful routes + # + # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper + # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>. + # + # get 'post/:id' => 'posts#show' + # post 'post/:id' => 'posts#create_comment' + # + # If your route needs to respond to more than one HTTP method (or all methods) then using the + # <tt>:via</tt> option on <tt>match</tt> is preferable. + # + # match 'post/:id' => 'posts#show', via: [:get, :post] + # + # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same + # URL will route to the <tt>show</tt> action. + # # == Named routes # # Routes can be named by passing an <tt>:as</tt> option, @@ -78,7 +94,7 @@ module ActionDispatch # Example: # # # In routes.rb - # match '/login' => 'accounts#login', as: 'login' + # get '/login' => 'accounts#login', as: 'login' # # # With render, redirect_to, tests, etc. # redirect_to login_url @@ -104,9 +120,9 @@ module ActionDispatch # # # In routes.rb # controller :blog do - # match 'blog/show' => :list - # match 'blog/delete' => :delete - # match 'blog/edit/:id' => :edit + # get 'blog/show' => :list + # get 'blog/delete' => :delete + # get 'blog/edit/:id' => :edit # end # # # provides named routes for show, delete, and edit @@ -116,7 +132,7 @@ module ActionDispatch # # Routes can generate pretty URLs. For example: # - # match '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: { + # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: { # year: /\d{4}/, # month: /\d{1,2}/, # day: /\d{1,2}/ @@ -131,7 +147,7 @@ module ActionDispatch # You can specify a regular expression to define a format for a parameter. # # controller 'geocode' do - # match 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode' => :show, constraints: { # postalcode: /\d{5}(-\d{4})?/ # } # @@ -139,13 +155,13 @@ module ActionDispatch # expression modifiers: # # controller 'geocode' do - # match 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode' => :show, constraints: { # postalcode: /hx\d\d\s\d[a-z]{2}/i # } # end # # controller 'geocode' do - # match 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode' => :show, constraints: { # postalcode: /# Postcode format # \d{5} #Prefix # (-\d{4})? #Suffix @@ -153,73 +169,21 @@ module ActionDispatch # } # end # - # Using the multiline match modifier will raise an +ArgumentError+. + # Using the multiline modifier will raise an +ArgumentError+. # Encoding regular expression modifiers are silently ignored. The # match will always use the default encoding or ASCII. # - # == Default route - # - # Consider the following route, which you will find commented out at the - # bottom of your generated <tt>config/routes.rb</tt>: - # - # match ':controller(/:action(/:id))(.:format)' - # - # This route states that it expects requests to consist of a - # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in - # turn is followed optionally by an <tt>:id</tt>, which in turn is followed - # optionally by a <tt>:format</tt>. - # - # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end - # up with: - # - # params = { controller: 'blog', - # action: 'edit', - # id: '22' - # } - # - # By not relying on default routes, you improve the security of your - # application since not all controller actions, which includes actions you - # might add at a later time, are exposed by default. - # - # == HTTP Methods - # - # Using the <tt>:via</tt> option when specifying a route allows you to - # restrict it to a specific HTTP method. Possible values are <tt>:post</tt>, - # <tt>:get</tt>, <tt>:patch</tt>, <tt>:put</tt>, <tt>:delete</tt> and - # <tt>:any</tt>. If your route needs to respond to more than one method you - # can use an array, e.g. <tt>[ :get, :post ]</tt>. The default value is - # <tt>:any</tt> which means that the route will respond to any of the HTTP - # methods. - # - # match 'post/:id' => 'posts#show', via: :get - # match 'post/:id' => 'posts#create_comment', via: :post - # - # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same - # URL will route to the <tt>show</tt> action. - # - # === HTTP helper methods - # - # An alternative method of specifying which HTTP method a route should respond to is to use the helper - # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>. - # - # get 'post/:id' => 'posts#show' - # post 'post/:id' => 'posts#create_comment' - # - # This syntax is less verbose and the intention is more apparent to someone else reading your code, - # however if your route needs to respond to more than one HTTP method (or all methods) then using the - # <tt>:via</tt> option on <tt>match</tt> is preferable. - # # == External redirects # # You can redirect any path to another path using the redirect helper in your router: # - # match "/stories" => redirect("/posts") + # get "/stories" => redirect("/posts") # # == Unicode character routes # # You can specify unicode character routes in your router: # - # match "こんにちは" => "welcome#index" + # get "こんにちは" => "welcome#index" # # == Routing to Rack Applications # @@ -227,7 +191,7 @@ module ActionDispatch # index action in the PostsController, you can specify any Rack application # as the endpoint for a matcher: # - # match "/application.js" => Sprockets + # get "/application.js" => Sprockets # # == Reloading routes # diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 4fbe156a72..45d2d3346e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -58,8 +58,8 @@ module ActionDispatch @set, @scope, @path, @options = set, scope, path, options @requirements, @conditions, @defaults = {}, {}, {} - normalize_path! normalize_options! + normalize_path! normalize_requirements! normalize_conditions! normalize_defaults! @@ -181,7 +181,8 @@ module ActionDispatch if !via_all && options[:via].blank? msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n\n" \ + "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ " Instead of: match \"controller#action\"\n" \ " Do: get \"controller#action\"" raise msg @@ -588,8 +589,7 @@ module ActionDispatch private def map_method(method, args, &block) options = args.extract_options! - options[:via] = method - options[:path] ||= args.first if args.first.is_a?(String) + options[:via] = method match(*args, options, &block) self end @@ -1369,18 +1369,23 @@ module ActionDispatch paths = [path] + rest end - path_without_format = path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, options) - options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - end - options[:anchor] = true unless options.key?(:anchor) if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end - paths.each { |_path| decomposed_match(_path, options.dup) } + paths.each do |_path| + route_options = options.dup + route_options[:path] ||= _path if _path.is_a?(String) + + path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') + if using_match_shorthand?(path_without_format, route_options) + route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + end + + decomposed_match(_path, route_options) + end self end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 619dd22ec1..07203428d4 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -403,11 +403,19 @@ module ActionDispatch def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) + if name && named_routes[name] + raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \ + "You may have defined two routes with the same name using the `:as` option, or " \ + "you may be overriding a route already defined by a resource with the same naming. " \ + "For the latter, you can restrict the routes created with `resources` as explained here: \n" \ + "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" + end + path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) - named_routes[name] = route if name && !named_routes[name] + named_routes[name] = route if name route end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ed4e88aab6..56c31255f3 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -17,7 +17,7 @@ module ActionDispatch # a Hash, or a String that is appropriately encoded # (<tt>application/x-www-form-urlencoded</tt> or # <tt>multipart/form-data</tt>). - # - +headers+: Additional headers to pass, as a Hash. The headers will be + # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be # merged into the Rack env hash. # # This method returns a Response object, which one can use to @@ -28,44 +28,44 @@ module ActionDispatch # # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with # +#post+, +#patch+, +#put+, +#delete+, and +#head+. - def get(path, parameters = nil, headers = nil) - process :get, path, parameters, headers + def get(path, parameters = nil, headers_or_env = nil) + process :get, path, parameters, headers_or_env end # Performs a POST request with the given parameters. See +#get+ for more # details. - def post(path, parameters = nil, headers = nil) - process :post, path, parameters, headers + def post(path, parameters = nil, headers_or_env = nil) + process :post, path, parameters, headers_or_env end # Performs a PATCH request with the given parameters. See +#get+ for more # details. - def patch(path, parameters = nil, headers = nil) - process :patch, path, parameters, headers + def patch(path, parameters = nil, headers_or_env = nil) + process :patch, path, parameters, headers_or_env end # Performs a PUT request with the given parameters. See +#get+ for more # details. - def put(path, parameters = nil, headers = nil) - process :put, path, parameters, headers + def put(path, parameters = nil, headers_or_env = nil) + process :put, path, parameters, headers_or_env end # Performs a DELETE request with the given parameters. See +#get+ for # more details. - def delete(path, parameters = nil, headers = nil) - process :delete, path, parameters, headers + def delete(path, parameters = nil, headers_or_env = nil) + process :delete, path, parameters, headers_or_env end # Performs a HEAD request with the given parameters. See +#get+ for more # details. - def head(path, parameters = nil, headers = nil) - process :head, path, parameters, headers + def head(path, parameters = nil, headers_or_env = nil) + process :head, path, parameters, headers_or_env end # Performs a OPTIONS request with the given parameters. See +#get+ for # more details. - def options(path, parameters = nil, headers = nil) - process :options, path, parameters, headers + def options(path, parameters = nil, headers_or_env = nil) + process :options, path, parameters, headers_or_env end # Performs an XMLHttpRequest request with the given parameters, mirroring @@ -74,11 +74,11 @@ module ActionDispatch # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart # string; the headers are a hash. - def xml_http_request(request_method, path, parameters = nil, headers = nil) - headers ||= {} - headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers) + def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil) + headers_or_env ||= {} + headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers_or_env) end alias xhr :xml_http_request @@ -95,40 +95,40 @@ module ActionDispatch # redirect. Note that the redirects are followed until the response is # not a redirect--this means you may run into an infinite loop if your # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers = nil) - process(http_method, path, parameters, headers) + def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil) + process(http_method, path, parameters, headers_or_env) follow_redirect! while redirect? status end # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:get, path, parameters, headers) + def get_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:get, path, parameters, headers_or_env) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:post, path, parameters, headers) + def post_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:post, path, parameters, headers_or_env) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def patch_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:patch, path, parameters, headers) + def patch_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:patch, path, parameters, headers_or_env) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:put, path, parameters, headers) + def put_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:put, path, parameters, headers_or_env) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:delete, path, parameters, headers) + def delete_via_redirect(path, parameters = nil, headers_or_env = nil) + request_via_redirect(:delete, path, parameters, headers_or_env) end end @@ -268,8 +268,7 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, rack_env = nil) - rack_env ||= {} + def process(method, path, parameters = nil, headers_or_env = nil) if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -300,10 +299,12 @@ module ActionDispatch "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => accept } + # this modifies the passed env directly + Http::Headers.new(env).merge!(headers_or_env || {}) session = Rack::Test::Session.new(_mock_session) - env.merge!(rack_env) + env.merge!(env) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 5c87a9cd7c..b5e47d78d1 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,10 +1,11 @@ module ActionPack - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta1" + # Returns the version of the currently loaded ActionPack as a Gem::Version + def self.version + Gem::Version.new "4.0.0.beta1" + end - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActionPack.version.segments + STRING = ActionPack.version.to_s end end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 1bad82159a..5afe435459 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -180,7 +180,7 @@ module ActionView # <title>My Website</title> # <%= yield :script %> # </head> - # <body class="<%= content_for?(:right_col) ? 'one-column' : 'two-column' %>"> + # <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>"> # <%= yield %> # <%= yield :right_col %> # </body> diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 3dae1fc87a..36cfb7fca7 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -433,7 +433,7 @@ module ActionView builder = instantiate_builder(object_name, object, options) output = capture(builder, &block) - html_options[:multipart] = builder.multipart? + html_options[:multipart] ||= builder.multipart? form_tag(options[:url] || {}, html_options) { output } end @@ -655,14 +655,6 @@ module ActionView # ... # <% end %> # - # When projects is already an association on Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects - # end - # # If you want to destroy any of the associated models through the # form, you have to enable it first using the <tt>:allow_destroy</tt> # option for +accepts_nested_attributes_for+: @@ -1425,14 +1417,6 @@ module ActionView # ... # <% end %> # - # When projects is already an association on Person you can use - # +accepts_nested_attributes_for+ to define the writer method for you: - # - # class Person < ActiveRecord::Base - # has_many :projects - # accepts_nested_attributes_for :projects - # end - # # If you want to destroy any of the associated models through the # form, you have to enable it first using the <tt>:allow_destroy</tt> # option for +accepts_nested_attributes_for+: diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 377819a80c..7e65ebb4e4 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -565,7 +565,7 @@ module ActionView if priority_zones if priority_zones.is_a?(Regexp) - priority_zones = zones.grep(priority_zones) + priority_zones = zones.select { |z| z =~ priority_zones } end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) @@ -752,7 +752,7 @@ module ActionView end def prompt_text(prompt) - prompt = prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', :default => 'Please select') + prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', :default => 'Please select') end end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 1adc8225f1..8abd5d6e9c 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -778,7 +778,7 @@ module ActionView # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) - name.to_s.gsub(']','').gsub(/[^-a-zA-Z0-9:.]/, "_") + name.to_s.delete(']').gsub(/[^-a-zA-Z0-9:.]/, "_") end end end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 878d3e0eda..edff98ddaa 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -81,7 +81,7 @@ module ActionView # # => <input class="ok" onclick="alert('Hello world!');" type="button" value="Greeting" /> # def button_to_function(name, function=nil, html_options={}) - message = "button_to_function is deprecated and will be removed from Rails 4.1. We recomend to use Unobtrusive JavaScript instead. " + + message = "button_to_function is deprecated and will be removed from Rails 4.1. We recommend using Unobtrusive JavaScript instead. " + "See http://guides.rubyonrails.org/working_with_javascript_in_rails.html#unobtrusive-javascript" ActiveSupport::Deprecation.warn message @@ -103,7 +103,7 @@ module ActionView # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a> # def link_to_function(name, function, html_options={}) - message = "link_to_function is deprecated and will be removed from Rails 4.1. We recomend to use Unobtrusive JavaScript instead. " + + message = "link_to_function is deprecated and will be removed from Rails 4.1. We recommend using Unobtrusive JavaScript instead. " + "See http://guides.rubyonrails.org/working_with_javascript_in_rails.html#unobtrusive-javascript" ActiveSupport::Deprecation.warn message diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb index 3d597079c4..10dec66b4f 100644 --- a/actionpack/lib/action_view/helpers/tags/base.rb +++ b/actionpack/lib/action_view/helpers/tags/base.rb @@ -73,27 +73,26 @@ module ActionView def add_default_name_and_id(options) if options.has_key?("index") - options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"]) } + options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) } options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } options.delete("index") elsif defined?(@auto_index) - options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index) } + options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) } options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else - options["name"] ||= options.fetch("name"){ tag_name } + options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) } options["id"] = options.fetch("id"){ tag_id } end - options["name"] += "[]" if options["multiple"] options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence end - def tag_name - "#{@object_name}[#{sanitized_method_name}]" + def tag_name(multiple = false) + "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" end - def tag_name_with_index(index) - "#{@object_name}[#{index}][#{sanitized_method_name}]" + def tag_name_with_index(index, multiple = false) + "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" end def tag_id diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 775d93ed39..22059a0170 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -425,8 +425,8 @@ module ActionView # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. # # ==== Obfuscation - # Prior to Rails 4.0, +mail_to+ provided options for encoding the address - # in order to hinder email harvesters. To take advantage of these options, + # Prior to Rails 4.0, +mail_to+ provided options for encoding the address + # in order to hinder email harvesters. To take advantage of these options, # install the +actionview-encoded_mail_to+ gem. # # ==== Examples @@ -439,18 +439,30 @@ module ActionView # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com", # subject: "This is an example email" # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a> - def mail_to(email_address, name = nil, html_options = {}) + # + # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: + # + # <%= mail_to "me@domain.com" do %> + # <strong>Email me:</strong> <span>me@domain.com</span> + # <% end %> + # # => <a href="mailto:me@domain.com"> + # <strong>Email me:</strong> <span>me@domain.com</span> + # </a> + def mail_to(email_address, name = nil, html_options = {}, &block) email_address = ERB::Util.html_escape(email_address) - html_options.stringify_keys! + html_options, name = name, nil if block_given? + html_options = (html_options || {}).stringify_keys extras = %w{ cc bcc body subject }.map { |item| option = html_options.delete(item) || next "#{item}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) - - content_tag "a", name || email_address.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe) + + html_options["href"] = "mailto:#{email_address}#{extras}".html_safe + + content_tag(:a, name || email_address.html_safe, html_options, &block) end # True if the current request URI was generated by the given +options+. diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 4e4816d983..d61cc0f304 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -43,7 +43,13 @@ module ActionView module Accessors #:nodoc: end - register_detail(:locale) { [I18n.locale, I18n.default_locale].uniq } + register_detail(:locale) do + locales = [I18n.locale] + locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks + locales << I18n.default_locale + locales.uniq! + locales + end register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } register_detail(:handlers){ Template::Handlers.extensions } diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index f73d14c79b..946db1df79 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -138,7 +138,7 @@ module ActionView # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. def render(view, locals, buffer=nil, &block) - ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do + ActiveSupport::Notifications.instrument("!render_template.action_view", virtual_path: @virtual_path, identifier: @identifier) do compile!(view) view.send(method_name, locals, buffer, &block) end @@ -324,7 +324,8 @@ module ActionView end def locals_code #:nodoc: - @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join + # Double assign to suppress the dreaded 'assigned but unused variable' warning + @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join end def method_name #:nodoc: diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 1a1083bf00..6b9b0a826b 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -109,7 +109,7 @@ module ActionView @cache.clear end - # Normalizes the arguments and passes it on to find_template. + # Normalizes the arguments and passes it on to find_templates. def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) @@ -255,7 +255,7 @@ module ActionView # # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") # - # This one allows you to keep files with different formats in seperated subdirectories, + # This one allows you to keep files with different formats in separate subdirectories, # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. # diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 463f192d0c..10b487f37a 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -219,6 +219,7 @@ module ActionView :@_routes, :@controller, :@_layouts, + :@_files, :@_rendered_views, :@method_name, :@output_buffer, diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb index 6b4ececda2..30b6b8b141 100644 --- a/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb @@ -77,7 +77,7 @@ module HTML # A regular expression of the valid characters used to separate protocols like # the ':' in 'http://foo.com' - self.protocol_separator = /:|(�*58)|(p)|(%|%)3A/ + self.protocol_separator = /:|(�*58)|(p)|(�*3a)|(%|%)3A/i # Specifies a Set of HTML attributes that can have URIs. self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) @@ -121,8 +121,8 @@ module HTML style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ') # gauntlet - if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ || - style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*$/ + if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ || + style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/ return '' end @@ -133,7 +133,7 @@ module HTML elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) unless val.split().any? do |keyword| !allowed_css_keywords.include?(keyword) && - keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/ + keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/ end clean << prop + ': ' + val + ';' end @@ -182,7 +182,7 @@ module HTML def contains_bad_protocols?(attr_name, value) uri_attributes.include?(attr_name) && - (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) + (value =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) end end end diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb index 60b6783b19..7f8609c408 100644 --- a/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb +++ b/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb @@ -537,7 +537,7 @@ module HTML # Get identifier, class, attribute name, pseudo or negation. while true # Element identifier. - next if statement.sub!(/^#(\?|[\w\-]+)/) do |match| + next if statement.sub!(/^#(\?|[\w\-]+)/) do id = $1 if id == "?" id = values.shift @@ -549,7 +549,7 @@ module HTML end # Class name. - next if statement.sub!(/^\.([\w\-]+)/) do |match| + next if statement.sub!(/^\.([\w\-]+)/) do class_name = $1 @source << ".#{class_name}" class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp) @@ -558,7 +558,7 @@ module HTML end # Attribute value. - next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match| + next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do name, equality, value = $1, $2, $3 if value == "?" value = values.shift @@ -575,7 +575,7 @@ module HTML end # Root element only. - next if statement.sub!(/^:root/) do |match| + next if statement.sub!(/^:root/) do pseudo << lambda do |element| element.parent.nil? || !element.parent.tag? end @@ -611,7 +611,7 @@ module HTML "" # Remove end # First/last child (of type). - next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match| + next if statement.sub!(/^:(first|last)-(child|of-type)/) do reverse = $1 == "last" of_type = $2 == "of-type" pseudo << nth_child(0, 1, of_type, reverse) @@ -619,7 +619,7 @@ module HTML "" # Remove end # Only child (of type). - next if statement.sub!(/^:only-(child|of-type)/) do |match| + next if statement.sub!(/^:only-(child|of-type)/) do of_type = $1 == "of-type" pseudo << only_child(of_type) @source << ":only-#{$1}" @@ -628,7 +628,7 @@ module HTML # Empty: no child elements or meaningful content (whitespaces # are ignored). - next if statement.sub!(/^:empty/) do |match| + next if statement.sub!(/^:empty/) do pseudo << lambda do |element| empty = true for child in element.children @@ -644,7 +644,7 @@ module HTML end # Content: match the text content of the element, stripping # leading and trailing spaces. - next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match| + next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do content = $1 if content == "?" content = values.shift diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index 1090af3060..8cba049485 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -259,7 +259,7 @@ module AbstractController end class TestCallbacksWithArgs < ActiveSupport::TestCase - test "callbacks still work when invoking process with multiple args" do + test "callbacks still work when invoking process with multiple arguments" do controller = CallbacksWithArgs.new controller.process(:index, " Howdy!") assert_equal "Hello world Howdy!", controller.response_body diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index c14d24905b..5709ad0378 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -42,7 +42,7 @@ module AbstractController end end - test "generated methods call custom with args received" do + test "generated methods call custom with arguments received" do collector = MyCollector.new collector.html collector.text(:foo) diff --git a/actionpack/test/abstract/layouts_test.rb b/actionpack/test/abstract/layouts_test.rb index 558a45b87f..92baad4523 100644 --- a/actionpack/test/abstract/layouts_test.rb +++ b/actionpack/test/abstract/layouts_test.rb @@ -72,13 +72,21 @@ module AbstractControllerTests end class WithProc < Base - layout proc { |c| "overwrite" } + layout proc { "overwrite" } def index render :template => ActionView::Template::Text.new("Hello proc!") end end + class WithProcReturningNil < Base + layout proc { nil } + + def index + render template: ActionView::Template::Text.new("Hello nil!") + end + end + class WithZeroArityProc < Base layout proc { "overwrite" } @@ -249,6 +257,12 @@ module AbstractControllerTests assert_equal "Overwrite Hello proc!", controller.response_body end + test "when layout is specified as a proc and the proc retuns nil, don't use a layout" do + controller = WithProcReturningNil.new + controller.process(:index) + assert_equal "Hello nil!", controller.response_body + end + test "when layout is specified as a proc without parameters it works just the same" do controller = WithZeroArityProc.new controller.process(:index) diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 7157bccfb3..8213997f4e 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -332,7 +332,7 @@ end module ActionDispatch module RoutingVerbs - def get(uri_or_host, path = nil, port = nil) + def get(uri_or_host, path = nil) host = uri_or_host.host unless path path ||= uri_or_host.path diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 5d727b3811..22a410db94 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -96,6 +96,14 @@ class ActionPackAssertionsController < ActionController::Base raise "post" if request.post? render :text => "request method: #{request.env['REQUEST_METHOD']}" end + + def render_file_absolute_path + render :file => File.expand_path('../../../README.rdoc', __FILE__) + end + + def render_file_relative_path + render :file => 'README.rdoc' + end end # Used to test that assert_response includes the exception message @@ -142,6 +150,16 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase assert_tag :content => "/action_pack_assertions/flash_me" end + def test_render_file_absolute_path + get :render_file_absolute_path + assert_match(/\A= Action Pack/, @response.body) + end + + def test_render_file_relative_path + get :render_file_relative_path + assert_match(/\A= Action Pack/, @response.body) + end + def test_get_request assert_raise(RuntimeError) { get :raise_exception_on_get } get :raise_exception_on_post @@ -441,6 +459,23 @@ class AssertTemplateTest < ActionController::TestCase assert_template :partial => '_partial' end + def test_file_with_absolute_path_success + get :render_file_absolute_path + assert_template :file => File.expand_path('../../../README.rdoc', __FILE__) + end + + def test_file_with_relative_path_success + get :render_file_relative_path + assert_template :file => 'README.rdoc' + end + + def test_with_file_failure + get :render_file_absolute_path + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template :file => 'test/hello_world' + end + end + def test_with_nil_passes_when_no_template_rendered get :nothing assert_template nil diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 9b42e7631f..d4f18d55a6 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -68,6 +68,12 @@ class RecordIdentifierWithoutDeprecationController < ActionController::Base include ActionView::RecordIdentifier end +class ActionMissingController < ActionController::Base + def action_missing(action) + render :text => "Response for #{action}" + end +end + class ControllerClassTests < ActiveSupport::TestCase def test_controller_path @@ -186,6 +192,12 @@ class PerformActionTest < ActionController::TestCase assert_raise(AbstractController::ActionNotFound) { get :hidden_action } assert_raise(AbstractController::ActionNotFound) { get :another_hidden_action } end + + def test_action_missing_should_work + use_controller ActionMissingController + get :arbitrary_action + assert_equal "Response for arbitrary_action", @response.body + end end class UrlOptionsTest < ActionController::TestCase diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 9d4356f546..3b874a739a 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -# FIXME remove DummyKeyGenerator and this require in 4.1 require 'active_support/key_generator' class FlashTest < ActionController::TestCase @@ -219,7 +218,7 @@ end class FlashIntegrationTest < ActionDispatch::IntegrationTest SessionKey = '_myapp_session' - Generator = ActiveSupport::DummyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33') + Generator = ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33') class TestController < ActionController::Base add_flash_types :bar diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 537de7a2dd..9f1c168209 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -# FIXME remove DummyKeyGenerator and this require in 4.1 require 'active_support/key_generator' class HttpDigestAuthenticationTest < ActionController::TestCase @@ -43,7 +42,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase setup do # Used as secret in generating nonce to prevent tampering of timestamp @secret = "4fb45da9e4ab4ddeb7580d6a35503d99" - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(@secret) + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret) end teardown do @@ -249,6 +248,14 @@ class HttpDigestAuthenticationTest < ActionController::TestCase assert_equal 'Definitely Maybe', @response.body end + test "when sent a basic auth header, returns Unauthorized" do + @request.env['HTTP_AUTHORIZATION'] = 'Basic Gwf2aXq8ZLF3Hxq=' + + get :display + + assert_response :unauthorized + end + private def encode_credentials(options) diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 72b882539c..c3bdf74d93 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -573,6 +573,21 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest def test_generate_url_without_controller assert_equal 'http://www.example.com/foo', url_for(:controller => "foo") end + + def test_pass_headers + get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" + + assert_equal "http://nohost.com", @request.env["HTTP_HOST"] + assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] + end + + def test_pass_env + get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + end + end class ApplicationIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index 71bcfd664e..34304cf640 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -94,6 +94,18 @@ class HasOwnLayoutController < LayoutTest layout 'item' end +class HasNilLayoutSymbol < LayoutTest + layout :nilz + + def nilz + nil + end +end + +class HasNilLayoutProc < LayoutTest + layout proc { nil } +end + class PrependsViewPathController < LayoutTest def hello prepend_view_path File.dirname(__FILE__) + '/../fixtures/layout_tests/alt/' @@ -142,6 +154,18 @@ class LayoutSetInResponseTest < ActionController::TestCase assert_template :layout => "layouts/item" end + def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default + @controller = HasNilLayoutSymbol.new + get :hello + assert_template layout: "layouts/layout_test" + end + + def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default + @controller = HasNilLayoutProc.new + get :hello + assert_template layout: "layouts/layout_test" + end + def test_layout_only_exception_when_included @controller = OnlyLayoutController.new get :hello diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 3b1a07d7af..5755444a65 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -48,6 +48,10 @@ module ActionController end response.stream.close end + + def with_stale + render :text => 'stale' if stale?(:etag => "123") + end end tests TestController @@ -117,5 +121,16 @@ module ActionController assert_equal 'zomg', response.body assert response.stream.closed?, 'stream should be closed' end + + def test_stale_without_etag + get :with_stale + assert_equal 200, @response.status.to_i + end + + def test_stale_with_etag + @request.if_none_match = Digest::MD5.hexdigest("123") + get :with_stale + assert_equal 304, @response.status.to_i + end end end diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index bac1d02977..6b02eedaed 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -25,4 +25,13 @@ class LocalizedTemplatesTest < ActionController::TestCase ensure I18n.locale = old_locale end + + def test_use_fallback_locales + I18n.locale = :"de-AT" + I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + I18n.fallbacks[:"de-AT"] = [:de] + + get :hello_world + assert_equal "Gutten Tag", @response.body + end end diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb index 2f1aa22208..9e5022c9f4 100644 --- a/actionpack/test/controller/new_base/render_partial_test.rb +++ b/actionpack/test/controller/new_base/render_partial_test.rb @@ -5,14 +5,14 @@ module RenderPartial class BasicController < ActionController::Base self.view_paths = [ActionView::FixtureResolver.new( - "render_partial/basic/_basic.html.erb" => "BasicPartial!", - "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>", - "render_partial/basic/with_json.html.erb" => "<%= render :partial => 'with_json', :formats => [:json] %>", - "render_partial/basic/_with_json.json.erb" => "<%= render :partial => 'final', :formats => [:json] %>", - "render_partial/basic/_final.json.erb" => "{ final: json }", - "render_partial/basic/overriden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overriden' %><%= @test_unchanged %>", - "render_partial/basic/_overriden.html.erb" => "ParentPartial!", - "render_partial/child/_overriden.html.erb" => "OverridenPartial!" + "render_partial/basic/_basic.html.erb" => "BasicPartial!", + "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>", + "render_partial/basic/with_json.html.erb" => "<%= render :partial => 'with_json', :formats => [:json] %>", + "render_partial/basic/_with_json.json.erb" => "<%= render :partial => 'final', :formats => [:json] %>", + "render_partial/basic/_final.json.erb" => "{ final: json }", + "render_partial/basic/overridden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overridden' %><%= @test_unchanged %>", + "render_partial/basic/_overridden.html.erb" => "ParentPartial!", + "render_partial/child/_overridden.html.erb" => "OverriddenPartial!" )] def html_with_json_inside_json @@ -24,7 +24,7 @@ module RenderPartial render :action => "basic" end - def overriden + def overridden @test_unchanged = 'hello' end end @@ -55,8 +55,8 @@ module RenderPartial end test "partial from child controller gets picked" do - get :overriden - assert_response("goodbyeOverridenPartial!goodbye") + get :overridden + assert_response("goodbyeOverriddenPartial!goodbye") end end diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb index cc7f12ac6d..5635e16234 100644 --- a/actionpack/test/controller/new_base/render_test.rb +++ b/actionpack/test/controller/new_base/render_test.rb @@ -7,10 +7,10 @@ module Render "render/blank_render/access_request.html.erb" => "The request: <%= request.method.to_s.upcase %>", "render/blank_render/access_action_name.html.erb" => "Action Name: <%= action_name %>", "render/blank_render/access_controller_name.html.erb" => "Controller Name: <%= controller_name %>", - "render/blank_render/overriden_with_own_view_paths_appended.html.erb" => "parent content", - "render/blank_render/overriden_with_own_view_paths_prepended.html.erb" => "parent content", - "render/blank_render/overriden.html.erb" => "parent content", - "render/child_render/overriden.html.erb" => "child content" + "render/blank_render/overridden_with_own_view_paths_appended.html.erb" => "parent content", + "render/blank_render/overridden_with_own_view_paths_prepended.html.erb" => "parent content", + "render/blank_render/overridden.html.erb" => "parent content", + "render/child_render/overridden.html.erb" => "child content" )] def index @@ -25,13 +25,13 @@ module Render render :action => "access_action_name" end - def overriden_with_own_view_paths_appended + def overridden_with_own_view_paths_appended end - def overriden_with_own_view_paths_prepended + def overridden_with_own_view_paths_prepended end - def overriden + def overridden end private @@ -49,8 +49,8 @@ module Render end class ChildRenderController < BlankRenderController - append_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_appended.html.erb" => "child content") - prepend_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_prepended.html.erb" => "child content") + append_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_appended.html.erb" => "child content") + prepend_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_prepended.html.erb" => "child content") end class RenderTest < Rack::TestCase @@ -114,17 +114,17 @@ module Render class TestViewInheritance < Rack::TestCase test "Template from child controller gets picked over parent one" do - get "/render/child_render/overriden" + get "/render/child_render/overridden" assert_body "child content" end test "Template from child controller with custom view_paths prepended gets picked over parent one" do - get "/render/child_render/overriden_with_own_view_paths_prepended" + get "/render/child_render/overridden_with_own_view_paths_prepended" assert_body "child content" end test "Template from child controller with custom view_paths appended gets picked over parent one" do - get "/render/child_render/overriden_with_own_view_paths_appended" + get "/render/child_render/overridden_with_own_view_paths_appended" assert_body "child content" end diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb index 43a8c05cda..c3c549fbfc 100644 --- a/actionpack/test/controller/output_escaping_test.rb +++ b/actionpack/test/controller/output_escaping_test.rb @@ -11,8 +11,6 @@ class OutputEscapingTest < ActiveSupport::TestCase end test "escapeHTML shouldn't touch explicitly safe strings" do - # TODO this seems easier to compose and reason about, but - # this should be verified assert_equal "<", ERB::Util.h("<".html_safe) end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 93e94f0f48..f735564305 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -715,17 +715,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def setup_request_method_routes_for(method) rs.draw do - match '/match' => 'books#get', :via => :get - match '/match' => 'books#post', :via => :post - match '/match' => 'books#put', :via => :put - match '/match' => 'books#patch', :via => :patch - match '/match' => 'books#delete', :via => :delete + match '/match' => "books##{method}", :via => method.to_sym end end %w(GET PATCH POST PUT DELETE).each do |request_method| define_method("test_request_method_recognized_with_#{request_method}") do - setup_request_method_routes_for(request_method) + setup_request_method_routes_for(request_method.downcase) params = rs.recognize_path("/match", :method => request_method) assert_equal request_method.downcase, params[:action] end @@ -908,12 +904,13 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal set.routes.first, set.named_routes[:hello] end - def test_earlier_named_routes_take_precedence - set.draw do - get '/hello/world' => 'a#b', :as => 'hello' - get '/hello' => 'a#b', :as => 'hello' + def test_duplicate_named_route_raises_rather_than_pick_precedence + assert_raise ArgumentError do + set.draw do + get '/hello/world' => 'a#b', :as => 'hello' + get '/hello' => 'a#b', :as => 'hello' + end end - assert_equal set.routes.first, set.named_routes[:hello] end def setup_named_route_test diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index 888791b874..69bf4b7720 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -47,7 +47,7 @@ module ShowExceptions end end - class ShowExceptionsOverridenController < ShowExceptionsController + class ShowExceptionsOverriddenController < ShowExceptionsController private def show_detailed_exceptions? @@ -55,15 +55,15 @@ module ShowExceptions end end - class ShowExceptionsOverridenTest < ActionDispatch::IntegrationTest + class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest test 'show error page' do - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) get '/', {'detailed' => '0'} assert_equal "500 error fixture\n", body end test 'show diagnostics message' do - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) get '/', {'detailed' => '1'} assert_match(/boom/, body) end @@ -71,7 +71,7 @@ module ShowExceptions class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest def test_render_json_exception - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) get "/", {}, 'HTTP_ACCEPT' => 'application/json' assert_response :internal_server_error assert_equal 'application/json', response.content_type.to_s @@ -79,7 +79,7 @@ module ShowExceptions end def test_render_xml_exception - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) get "/", {}, 'HTTP_ACCEPT' => 'application/xml' assert_response :internal_server_error assert_equal 'application/xml', response.content_type.to_s @@ -87,7 +87,7 @@ module ShowExceptions end def test_render_fallback_exception - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) get "/", {}, 'HTTP_ACCEPT' => 'text/csv' assert_response :internal_server_error assert_equal 'text/html', response.content_type.to_s @@ -96,7 +96,7 @@ module ShowExceptions class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest def test_render_failsafe_exception - @app = ShowExceptionsOverridenController.action(:boom) + @app = ShowExceptionsOverriddenController.action(:boom) @exceptions_app = @app.instance_variable_get(:@exceptions_app) @app.instance_variable_set(:@exceptions_app, nil) $stderr = StringIO.new diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index df31338f09..38b9794b4d 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -57,6 +57,10 @@ class TestCaseTest < ActionController::TestCase render :text => request.protocol end + def test_headers + render text: request.headers.env.to_json + end + def test_html_output render :text => <<HTML <html> @@ -626,6 +630,24 @@ XML assert_equal 2004, page[:year] end + test "set additional HTTP headers" do + @request.headers['Referer'] = "http://nohost.com/home" + @request.headers['Content-Type'] = "application/rss+xml" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"] + assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"] + end + + test "set additional env variables" do + @request.headers['HTTP_REFERER'] = "http://example.com/about" + @request.headers['CONTENT_TYPE'] = "application/json" + get :test_headers + parsed_env = JSON.parse(@response.body) + assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"] + assert_equal "application/json", parsed_env["CONTENT_TYPE"] + end + def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new assert_kind_of String, @request.path_parameters['id'] diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 5ada5a7603..91ac13e7c6 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -1,6 +1,14 @@ require 'abstract_unit' -# FIXME remove DummyKeyGenerator and this require in 4.1 + +begin + require 'openssl' + OpenSSL::PKCS5 +rescue LoadError, NameError + $stderr.puts "Skipping KeyGenerator test: broken OpenSSL install" +else + require 'active_support/key_generator' +require 'active_support/message_verifier' class CookiesTest < ActionController::TestCase class TestController < ActionController::Base @@ -67,11 +75,21 @@ class CookiesTest < ActionController::TestCase head :ok end + def get_signed_cookie + cookies.signed[:user_id] + head :ok + end + def set_encrypted_cookie cookies.encrypted[:foo] = 'bar' head :ok end + def get_encrypted_cookie + cookies.encrypted[:foo] + head :ok + end + def set_invalid_encrypted_cookie cookies[:invalid_cookie] = 'invalid--9170e00a57cfc27083363b5c75b835e477bd90cf' head :ok @@ -330,12 +348,17 @@ class CookiesTest < ActionController::TestCase assert response.headers["Set-Cookie"] =~ /user_name=david/ end - def test_permanent_cookie + def test_set_permanent_cookie get :set_permanent_cookie assert_match(/Jamie/, @response.headers["Set-Cookie"]) assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]) end + def test_read_permanent_cookie + get :set_permanent_cookie + assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name] + end + def test_signed_cookie get :set_signed_cookie assert_equal 45, @controller.send(:cookies).signed[:user_id] @@ -394,33 +417,148 @@ class CookiesTest < ActionController::TestCase def test_raises_argument_error_if_missing_secret assert_raise(ArgumentError, nil.inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(nil) + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil) get :set_signed_cookie } assert_raise(ArgumentError, ''.inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("") + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("") get :set_signed_cookie } end def test_raises_argument_error_if_secret_is_probably_insecure assert_raise(ArgumentError, "password".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("password") + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password") get :set_signed_cookie } assert_raise(ArgumentError, "secret".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("secret") + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret") get :set_signed_cookie } assert_raise(ArgumentError, "12345678901234567890123456789".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("12345678901234567890123456789") + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("12345678901234567890123456789") get :set_signed_cookie } end + def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = nil + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed + end + + def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set + @request.env["action_dispatch.secret_token"] = nil + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed + end + + def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed + end + + def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = nil + get :get_encrypted_cookie + assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted + end + + def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set + @request.env["action_dispatch.secret_token"] = nil + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :get_encrypted_cookie + assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted + end + + def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :get_encrypted_cookie + assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted + end + + def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set + @request.env["action_dispatch.secret_token"] = nil + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :get_encrypted_cookie + assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted + end + + def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :get_encrypted_cookie + assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted + end + + def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar') + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal 'bar', @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) + sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.headers["Cookie"] = "user_id=45" + get :get_signed_cookie + + assert_equal nil, @controller.send(:cookies).signed[:user_id] + assert_equal nil, @response.cookies["user_id"] + end + + def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.headers["Cookie"] = "foo=baz" + get :get_encrypted_cookie + + assert_equal nil, @controller.send(:cookies).encrypted[:foo] + assert_equal nil, @response.cookies["foo"] + end + def test_cookie_with_all_domain_option get :set_cookie_with_domain assert_response :success @@ -669,3 +807,5 @@ class CookiesTest < ActionController::TestCase end end end + +end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb index 42432510c3..9e37b96951 100644 --- a/actionpack/test/dispatch/header_test.rb +++ b/actionpack/test/dispatch/header_test.rb @@ -1,41 +1,137 @@ -require 'abstract_unit' +require "abstract_unit" class HeaderTest < ActiveSupport::TestCase - def setup + setup do @headers = ActionDispatch::Http::Headers.new( - "HTTP_CONTENT_TYPE" => "text/plain" + "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" ) end - def test_each + test "#new does not normalize the data" do + headers = ActionDispatch::Http::Headers.new( + "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com") + + assert_equal({"Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com"}, headers.env) + end + + test "#env returns the headers as env variables" do + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#each iterates through the env variables" do headers = [] @headers.each { |pair| headers << pair } - assert_equal [["HTTP_CONTENT_TYPE", "text/plain"]], headers + assert_equal [["CONTENT_TYPE", "text/plain"], + ["HTTP_REFERER", "/some/page"]], headers + end + + test "set new headers" do + @headers["Host"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "headers can contain numbers" do + @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" + + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"] + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"] + end + + test "set new env variables" do + @headers["HTTP_HOST"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] end - def test_setter - @headers['foo'] = "bar" - assert_equal "bar", @headers['foo'] + test "key?" do + assert @headers.key?("CONTENT_TYPE") + assert @headers.include?("CONTENT_TYPE") end - def test_key? - assert @headers.key?('HTTP_CONTENT_TYPE') - assert @headers.include?('HTTP_CONTENT_TYPE') + test "fetch with block" do + assert_equal "omg", @headers.fetch("notthere") { "omg" } end - def test_fetch_with_block - assert_equal 'omg', @headers.fetch('notthere') { 'omg' } + test "accessing http header" do + assert_equal "/some/page", @headers["Referer"] + assert_equal "/some/page", @headers["referer"] + assert_equal "/some/page", @headers["HTTP_REFERER"] end - test "content type" do + test "accessing special header" do assert_equal "text/plain", @headers["Content-Type"] assert_equal "text/plain", @headers["content-type"] assert_equal "text/plain", @headers["CONTENT_TYPE"] - assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"] end test "fetch" do assert_equal "text/plain", @headers.fetch("content-type", nil) - assert_equal "not found", @headers.fetch('not-found', 'not found') + assert_equal "not found", @headers.fetch("not-found", "not found") + end + + test "#merge! headers with mutation" do + @headers.merge!("Host" => "http://example.test", + "Content-Type" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.test", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "#merge! env with mutation" do + @headers.merge!("HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "merge without mutation" do + combined = @headers.merge("HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html") + assert_equal({"HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page"}, combined.env) + + assert_equal({"CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page"}, @headers.env) + end + + test "env variables with . are not modified" do + headers = ActionDispatch::Http::Headers.new + headers.merge! "rack.input" => "", + "rack.request.cookie_hash" => "", + "action_dispatch.logger" => "" + + assert_equal(["action_dispatch.logger", + "rack.input", + "rack.request.cookie_hash"], headers.env.keys.sort) + end + + test "symbols are treated as strings" do + headers = ActionDispatch::Http::Headers.new + headers.merge!(:SERVER_NAME => "example.com", + "HTTP_REFERER" => "/", + :Host => "test.com") + assert_equal "example.com", headers["SERVER_NAME"] + assert_equal "/", headers[:HTTP_REFERER] + assert_equal "test.com", headers["HTTP_HOST"] + end + + test "headers directly modifies the passed environment" do + env = {"HTTP_REFERER" => "/"} + headers = ActionDispatch::Http::Headers.new(env) + headers['Referer'] = "http://example.com/" + headers.merge! "CONTENT_TYPE" => "text/plain" + assert_equal({"HTTP_REFERER"=>"http://example.com/", + "CONTENT_TYPE"=>"text/plain"}, env) end end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index e2a9ba782d..6a2eb7da9f 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -75,7 +75,7 @@ class MimeTypeTest < ActiveSupport::TestCase assert_equal expect, Mime::Type.parse(accept) end - test "parse arbitarry media type parameters" do + test "parse arbitrary media type parameters" do accept = 'multipart/form-data; boundary="simple boundary"' expect = [Mime::MULTIPART_FORM] assert_equal expect, Mime::Type.parse(accept) diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 3b008fdff0..e5e28c28be 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -21,7 +21,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp, :at => "/sprockets" mount SprocketsApp => "/shorthand" - mount FakeEngine, :at => "/fakeengine" + mount FakeEngine, :at => "/fakeengine", :as => :fake mount FakeEngine, :at => "/getfake", :via => :get scope "/its_a" do diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 399f15199c..3c30a705e9 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -1,13 +1,15 @@ +# encoding: utf-8 require 'abstract_unit' class MultipartParamsParsingTest < ActionDispatch::IntegrationTest class TestController < ActionController::Base class << self - attr_accessor :last_request_parameters + attr_accessor :last_request_parameters, :last_parameters end def parse self.class.last_request_parameters = request.request_parameters + self.class.last_parameters = request.parameters head :ok end @@ -30,6 +32,23 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param')) end + test "parse single utf8 parameter" do + assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => 'Iñtërnâtiônàlizætiøn_value'}, + parse_multipart('single_utf8_param'), "request.request_parameters") + assert_equal( + 'Iñtërnâtiônàlizætiøn_value', + TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters") + end + + test "parse bracketed utf8 parameter" do + assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => { + 'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'} }, + parse_multipart('bracketed_utf8_param'), "request.request_parameters") + assert_equal( + {'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'}, + TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters") + end + test "parses text file" do params = parse_multipart('text_file') assert_equal %w(file foo), params.keys.sort diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index e9b59f55a7..9169658c22 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -164,7 +164,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest return end - object.each do |k,v| + object.each_value do |v| case v when Hash assert_utf8(v) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 2bf7056ff7..29703dd5b1 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1102,6 +1102,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'projects#index', @response.body end + def test_scope_with_format_option + draw do + get "direct/index", as: :no_format_direct, format: false + + scope format: false do + get "scoped/index", as: :no_format_scoped + end + end + + assert_equal "/direct/index", no_format_direct_path + assert_equal "/direct/index?format=html", no_format_direct_path(format: "html") + + assert_equal "/scoped/index", no_format_scoped_path + assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html") + + get '/scoped/index' + assert_equal "scoped#index", @response.body + + get '/scoped/index.html' + assert_equal "Not Found", @response.body + end + def test_index draw do get '/info' => 'projects#info', :as => 'info' @@ -1112,6 +1134,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'projects#info', @response.body end + def test_match_with_many_paths_containing_a_slash + draw do + get 'get/first', 'get/second', 'get/third', :to => 'get#show' + end + + get '/get/first' + assert_equal 'get#show', @response.body + + get '/get/second' + assert_equal 'get#show', @response.body + + get '/get/third' + assert_equal 'get#show', @response.body + end + def test_match_shorthand_with_no_scope draw do get 'account/overview' @@ -1134,6 +1171,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'account#shorthand', @response.body end + def test_match_shorthand_with_multiple_paths_inside_namespace + draw do + namespace :proposals do + put 'activate', 'inactivate' + end + end + + put '/proposals/activate' + assert_equal 'proposals#activate', @response.body + + put '/proposals/inactivate' + assert_equal 'proposals#inactivate', @response.body + end + def test_match_shorthand_inside_namespace_with_controller draw do namespace :api do @@ -2577,22 +2628,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') } end - def test_named_routes_collision_is_avoided_unless_explicitly_given_as - draw do - scope :as => "routes" do - get "/c/:id", :as => :collision, :to => "collision#show" - get "/collision", :to => "collision#show" - get "/no_collision", :to => "collision#show", :as => nil - - get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" - get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" - end - end - - assert_equal "/c/1", routes_collision_path(1) - assert_equal "/fc/1", routes_forced_collision_path(1) - end - def test_redirect_argument_error routes = Class.new { include ActionDispatch::Routing::Redirection }.new assert_raises(ArgumentError) { routes.redirect Object.new } @@ -2604,9 +2639,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "/c/:id", :as => :collision, :to => "collision#show" get "/collision", :to => "collision#show" get "/no_collision", :to => "collision#show", :as => nil - - get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" - get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" end end @@ -2657,6 +2689,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_duplicate_route_name_raises_error + assert_raise(ArgumentError) do + draw do + get '/collision', :to => 'collision#show', :as => 'collision' + get '/duplicate', :to => 'duplicate#show', :as => 'collision' + end + end + end + + def test_duplicate_route_name_via_resources_raises_error + assert_raise(ArgumentError) do + draw do + resources :collisions + get '/collision', :to => 'collision#show', :as => 'collision' + end + end + end + def test_nested_route_in_nested_resource draw do resources :posts, :only => [:index, :show] do diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index d8bf22dec8..e99ff46edf 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -1,12 +1,11 @@ require 'abstract_unit' require 'stringio' -# FIXME remove DummyKeyGenerator and this require in 4.1 require 'active_support/key_generator' class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = '_myapp_session' SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' - Generator = ActiveSupport::DummyKeyGenerator.new(SessionSecret) + Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret) Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1') SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16)) diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index e56e8ddc57..4123529092 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -48,6 +48,14 @@ module TestUrlGeneration https! assert_equal "http://www.example.com/foo", foo_url(:protocol => "http") end + + test "extracting protocol from host when protocol not present" do + assert_equal "httpz://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: nil) + end + + test "formatting host when protocol is present" do + assert_equal "http://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: "http://") + end end end diff --git a/actionpack/test/fixtures/multipart/bracketed_utf8_param b/actionpack/test/fixtures/multipart/bracketed_utf8_param new file mode 100644 index 0000000000..976ca44a45 --- /dev/null +++ b/actionpack/test/fixtures/multipart/bracketed_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/single_utf8_param b/actionpack/test/fixtures/multipart/single_utf8_param new file mode 100644 index 0000000000..b86f62d1e1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/single_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/public/images/rails.png b/actionpack/test/fixtures/public/images/rails.png Binary files differdeleted file mode 100644 index b8441f182e..0000000000 --- a/actionpack/test/fixtures/public/images/rails.png +++ /dev/null diff --git a/actionpack/test/fixtures/test/change_priorty.html.erb b/actionpack/test/fixtures/test/change_priority.html.erb index 5618977d05..5618977d05 100644 --- a/actionpack/test/fixtures/test/change_priorty.html.erb +++ b/actionpack/test/fixtures/test/change_priority.html.erb diff --git a/actionpack/test/template/debug_helper_test.rb b/actionpack/test/template/debug_helper_test.rb new file mode 100644 index 0000000000..42d06bd9ff --- /dev/null +++ b/actionpack/test/template/debug_helper_test.rb @@ -0,0 +1,8 @@ +require 'active_record_unit' + +class DebugHelperTest < ActionView::TestCase + def test_debug + company = Company.new(name: "firebase") + assert_match " name: firebase", debug(company) + end +end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 268bab6ad2..1ff320224d 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -361,6 +361,16 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, file_field("user", "avatar") end + def test_file_field_with_multiple_behavior + expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />' + assert_dom_equal expected, file_field("import", "file", :multiple => true) + end + + def test_file_field_with_multiple_behavior_and_explicit_name + expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />' + assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom") + end + def test_hidden_field assert_dom_equal( '<input id="post_title" name="post[title]" type="hidden" value="Hello World" />', @@ -2791,8 +2801,8 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_html_options_adds_options_to_form_tag - form_for(@post, html: { id: 'some_form', class: 'some_class' }) do |f| end - expected = whole_form("/posts/123", "some_form", "some_class", method: "patch") + form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data") assert_dom_equal expected, output_buffer end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 04cdd068c8..94ae8549f7 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -575,6 +575,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_multiple_and_with_explicit_name_ending_with_brackets + output_buffer = select(:post, :category, [], {include_hidden: false}, multiple: true, name: 'post[category][]') + assert_dom_equal( + "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>", + output_buffer + ) + end + def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true) assert_dom_equal( @@ -1087,12 +1095,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_priority_zones_as_regexp @firm = Firm.new("D") - priority_zones = /A|D/ @fake_timezones.each_with_index do |tz, i| - priority_zones.stubs(:===).with(tz).returns(i.zero? || i == 3) + tz.stubs(:=~).returns(i.zero? || i == 3) end - html = time_zone_select("firm", "time_zone", priority_zones) + html = time_zone_select("firm", "time_zone", /A|D/) assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"A\">A</option>\n" + "<option value=\"D\" selected=\"selected\">D</option>" + @@ -1104,11 +1111,32 @@ class FormOptionsHelperTest < ActionView::TestCase html end + def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones + @firm = Firm.new("D") + + priority_zones = /A|D/ + @fake_timezones.each do |tz| + priority_zones.stubs(:===).with(tz).raises(Exception) + end + + html = time_zone_select("firm", "time_zone", priority_zones) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html + end + def test_time_zone_select_with_default_time_zone_and_nil_value @firm = Firm.new() @firm.time_zone = nil - html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + + html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"A\">A</option>\n" + "<option value=\"B\" selected=\"selected\">B</option>\n" + "<option value=\"C\">C</option>\n" + @@ -1119,16 +1147,17 @@ class FormOptionsHelperTest < ActionView::TestCase end def test_time_zone_select_with_default_time_zone_and_value - @firm = Firm.new('D') - html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + - "</select>", - html + @firm = Firm.new('D') + + html = time_zone_select( "firm", "time_zone", nil, :default => 'B' ) + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + + "<option value=\"A\">A</option>\n" + + "<option value=\"B\">B</option>\n" + + "<option value=\"C\">C</option>\n" + + "<option value=\"D\" selected=\"selected\">D</option>\n" + + "<option value=\"E\">E</option>" + + "</select>", + html end def test_options_for_select_with_element_attributes diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb index d9b57776c9..b1c1b83807 100644 --- a/actionpack/test/template/html-scanner/sanitizer_test.rb +++ b/actionpack/test/template/html-scanner/sanitizer_test.rb @@ -200,6 +200,7 @@ class SanitizerTest < ActionController::TestCase %(<IMG SRC="jav
ascript:alert('XSS');">), %(<IMG SRC="jav
ascript:alert('XSS');">), %(<IMG SRC="  javascript:alert('XSS');">), + %(<IMG SRC="javascript:alert('XSS');">), %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i| define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do assert_sanitized img_hack, "<img>" @@ -279,6 +280,11 @@ class SanitizerTest < ActionController::TestCase assert_equal '', sanitize_css(raw) end + def test_should_sanitize_across_newlines + raw = %(\nwidth:\nexpression(alert('XSS'));\n) + assert_equal '', sanitize_css(raw) + end + def test_should_sanitize_img_vbscript assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />' end @@ -299,6 +305,15 @@ class SanitizerTest < ActionController::TestCase assert_sanitized "<span class=\"\\", "<span class=\"\\\">" end + def test_x03a + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">) + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" + assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">) + end + protected def assert_sanitized(input, expected = nil) @sanitizer ||= HTML::WhiteListSanitizer.new diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 8111e58527..2237d747be 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -61,7 +61,7 @@ module RenderTestCases def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names @view.lookup_context.formats = [:html] - assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priorty") + assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priority") end def test_render_template_with_a_missing_partial_of_another_format diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 5d87c96605..9b4c419807 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -51,7 +51,6 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal 'javascript:history.back()', url_for(:back) end - # TODO: missing test cases def test_button_to_with_straight_url assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com") end @@ -539,6 +538,22 @@ class UrlHelperTest < ActiveSupport::TestCase assert mail_to("david@loudthinking.com").html_safe? end + def test_mail_to_with_block + assert_dom_equal %{<a href="mailto:me@example.com"><span>Email me</span></a>}, + mail_to('me@example.com') { content_tag(:span, 'Email me') } + end + + def test_mail_to_with_block_and_options + assert_dom_equal %{<a class="special" href="mailto:me@example.com?cc=ccaddress%40example.com"><span>Email me</span></a>}, + mail_to('me@example.com', cc: "ccaddress@example.com", class: "special") { content_tag(:span, 'Email me') } + end + + def test_mail_to_does_not_modify_html_options_hash + options = { class: 'special' } + mail_to 'me@example.com', 'ME!', options + assert_equal({ class: 'special' }, options) + end + def protect_against_forgery? self.request_forgery end @@ -597,7 +612,7 @@ class UrlHelperControllerTest < ActionController::TestCase render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>" end - def show_overriden_url_for + def show_overridden_url_for render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>" end @@ -634,8 +649,8 @@ class UrlHelperControllerTest < ActionController::TestCase assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body end - def test_overriden_url_for_shows_only_path - get :show_overriden_url_for + def test_overridden_url_for_shows_only_path + get :show_overridden_url_for assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body end @@ -685,7 +700,7 @@ class UrlHelperControllerTest < ActionController::TestCase assert_equal 'ok', @response.body end - def test_url_helper_can_be_overriden + def test_url_helper_can_be_overridden get :override_url_helper assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body end |