diff options
Diffstat (limited to 'actionpack')
78 files changed, 1295 insertions, 284 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index e65b73aa66..dc98fb583c 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -4,6 +4,199 @@ *Alessandro Diaferia* +* Allow an absolute controller path inside a module scope. Fixes #12777. + + Example: + + namespace :foo do + # will route to BarController without the namespace. + get '/special', to: '/bar#index' + end + + +* Unique the segment keys array for non-optimized url helpers + + In Rails 3.2 you only needed pass an argument for dynamic segment once so + unique the segment keys array to match the number of args. Since the number + of args is less than required parts the non-optimized code path is selected. + This means to benefit from optimized url generation the arg needs to be + specified as many times as it appears in the path. + + Fixes #12808. + + *Andrew White* + +* Show full route constraints in error message + + When an optimized helper fails to generate, show the full route constraints + in the error message. Previously it would only show the contraints that were + required as part of the path. + + Fixes #13592. + + *Andrew White* + +* Use a custom route visitor for optimized url generation. Fixes #13349. + + *Andrew White* + +* Allow engine root relative redirects using an empty string. + + Example: + + # application routes.rb + mount BlogEngine => '/blog' + + # engine routes.rb + get '/welcome' => redirect('') + + This now redirects to the path `/blog`, whereas before it would redirect + to the application root path. In the case of a path redirect or a custom + redirect if the path returned contains a host then the path is treated as + absolute. Similarly for option redirects, if the options hash returned + contains a `:host` or `:domain` key then the path is treated as absolute. + + Fixes #7977. + + *Andrew White* + +* Fix `Encoding::CompatibilityError` when public path is UTF-8 + + In #5337 we forced the path encoding to ASCII-8BIT to prevent static file handling + from blowing up before an application has had chance to deal with possibly invalid + urls. However this has a negative side effect of making it an incompatible encoding + if the application's public path has UTF-8 characters in it. + + To work around the problem we check to see if the path has a valid encoding once + it has been unescaped. If it is not valid then we can return early since it will + not match any file anyway. + + Fixes #13518. + + *Andrew White* + +* `ActionController::Parameters#permit!` permits hashes in array values. + + *Xavier Noria* + +* Converts hashes in arrays of unfiltered params to unpermitted params. + + Fixes #13382. + + *Xavier Noria* + +* New config option to opt out of params "deep munging" that was used to + address security vulnerability CVE-2013-0155. In your app config: + + config.action_dispatch.perform_deep_munge = false + + Take care to understand the security risk involved before disabling this. + [Read more.](https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI) + + *Bernard Potocki* + +* `rake routes` shows routes defined under assets prefix. + + *Ryunosuke SATO* + +* Extend cross-site request forgery (CSRF) protection to GET requests with + JavaScript responses, protecting apps from cross-origin `<script>` tags. + + *Jeremy Kemper* + +* Fix generating a path for engine inside a resources block. + + Fixes #8533. + + *Piotr Sarnacki* + +* Add `Mime::Type.register "text/vcard", :vcf` to the default list of mime types. + + *DHH* + +* Remove deprecated `ActionController::RecordIdentifier`, use + `ActionView::RecordIdentifier` instead. + + *kennyj* + +* Fix regression when using `ActionView::Helpers::TranslationHelper#translate` with + `options[:raise]`. + + This regression was introduced at ec16ba75a5493b9da972eea08bae630eba35b62f. + + *Shota Fukumori (sora_h)* + +* Introducing Variants + + We often want to render different html/json/xml templates for phones, + tablets, and desktop browsers. Variants make it easy. + + The request variant is a specialization of the request format, like `:tablet`, + `:phone`, or `:desktop`. + + You can set the variant in a `before_action`: + + request.variant = :tablet if request.user_agent =~ /iPad/ + + Respond to variants in the action just like you respond to formats: + + respond_to do |format| + format.html do |html| + html.tablet # renders app/views/projects/show.html+tablet.erb + html.phone { extra_setup; render ... } + end + end + + Provide separate templates for each format and variant: + + app/views/projects/show.html.erb + app/views/projects/show.html+tablet.erb + app/views/projects/show.html+phone.erb + + You can also simplify the variants definition using the inline syntax: + + respond_to do |format| + format.js { render "trash" } + format.html.phone { redirect_to progress_path } + format.html.none { render "trash" } + end + + Variants also support common `any`/`all` block that formats have. + + It works for both inline: + + respond_to do |format| + format.html.any { render text: "any" } + format.html.phone { render text: "phone" } + end + + and block syntax: + + respond_to do |format| + format.html do |variant| + variant.any(:tablet, :phablet){ render text: "any" } + variant.phone { render text: "phone" } + end + end + + *Łukasz Strzałkowski* + +* Fix render of localized templates without an explicit format using wrong + content header and not passing correct formats to template due to the + introduction of the `NullType` for mimes. + + Templates like `hello.it.erb` were subject to this issue. + + Fixes #13064. + + *Angelo Capilleri*, *Carlos Antonio da Silva* + +* Try to escape each part of a url correctly when using a redirect route. + + Fixes #13110. + + *Andrew White* + * Better error message for typos in assert_response argument. When the response type argument to `assert_response` is not a known @@ -27,9 +220,7 @@ * Add `session#fetch` method - fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch), - with the exception that the returned value is always saved into the session. - + fetch behaves like [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch). It returns a value from the hash for the given key. If the key can’t be found, there are several options: diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 5c668d9624..d58dd9ed9b 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2013 David Heinemeier Hansson +Copyright (c) 2004-2014 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 8a85bf346a..1d6009bab8 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'rack', '~> 1.5.2' s.add_dependency 'rack-test', '~> 0.6.2' + s.add_dependency 'actionview', version - s.add_development_dependency 'actionview', version s.add_development_dependency 'activemodel', version end diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 09b9e7ddf0..ddd56b354a 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -23,7 +23,17 @@ module AbstractController protected def method_missing(symbol, &block) - mime_constant = Mime.const_get(symbol.upcase) + const_name = symbol.upcase + + unless Mime.const_defined?(const_name) + raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ + "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ + "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ + "be sure to nest your variant response within a format response: " \ + "format.html { |html| html.tablet { ... } }" + end + + mime_constant = Mime.const_get(const_name) if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index fb8f40cb9b..7be61d94c9 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' +require 'action_view/view_paths' require 'set' module AbstractController @@ -13,6 +14,7 @@ module AbstractController module Rendering extend ActiveSupport::Concern + include ActionView::ViewPaths # Normalize arguments, options and then delegates render_to_body and # sticks the result in self.response_body. @@ -20,7 +22,7 @@ module AbstractController def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) - _process_format(rendered_format) + _process_format(rendered_format) if rendered_format self.response_body end @@ -45,7 +47,7 @@ module AbstractController def render_to_body(options = {}) end - # Return Content-Type of rendered content + # Returns Content-Type of rendered content # :api: public def rendered_format Mime::TEXT @@ -102,6 +104,8 @@ module AbstractController # :api: private def _normalize_render(*args, &block) options = _normalize_args(*args, &block) + #TODO: remove defined? when we restore AP <=> AV dependency + options[:variant] = request.variant if defined?(request) && request.variant.present? _normalize_options(options) options end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 417d2efec2..50bc26a80f 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -50,7 +50,7 @@ module ActionController end # Common Active Support usage in Action Controller -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/load_error' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/name_error' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c84776ab7a..c0f10da23a 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,21 +1,8 @@ +require 'action_view' require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" module ActionController - # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>. - # Modules needs to be included in particluar order. First we need to have <tt>AbstractController::Rendering</tt> included, - # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that - # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly - # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of - # <tt>ActionController::Base</tt> which includes <tt>ActionController::Rendering</tt>. If we include <tt>ActionView::Rendering</tt> - # beetween them to perserve the required order, we can simply do this by: - # - # ActionController::Base.superclass.send(:include, ActionView::Rendering) - # - metal = Class.new(Metal) do - include AbstractController::Rendering - end - # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed # on request and then either it renders a template or redirects to another action. An action is defined as a public method # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. @@ -99,7 +86,7 @@ module ActionController # or you can remove the entire session with +reset_session+. # # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted. - # This prevents the user from tampering with the session but also allows him to see its contents. + # This prevents the user from tampering with the session but also allows them to see its contents. # # Do not put secret information in cookie-based sessions! # @@ -174,7 +161,7 @@ module ActionController # render action: "overthere" # won't be called if monkeys is nil # end # - class Base < metal + class Base < Metal abstract! # We document the request and response methods here because albeit they are @@ -214,6 +201,7 @@ module ActionController end MODULES = [ + AbstractController::Rendering, AbstractController::Translation, AbstractController::AssetPaths, @@ -221,6 +209,7 @@ module ActionController HideActions, UrlFor, Redirecting, + ActionView::Layouts, Rendering, Renderers::All, ConditionalGet, diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 424473801d..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,6 +1,6 @@ module ActionController module Head - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index d3aa8f90c5..b0e164bc57 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -67,7 +67,7 @@ module ActionController private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 09ea4919c2..33014b97ca 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -48,7 +48,7 @@ module ActionController # the server will receive a +Last-Event-ID+ header with value equal to +id+. # # After setting an option in the constructor of the SSE object, all future - # SSEs sent accross the stream will use those options unless overridden. + # SSEs sent across the stream will use those options unless overridden. # # Example Usage: # diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 84ade41036..d5e08b7034 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,6 +181,61 @@ module ActionController #:nodoc: # end # end # + # Formats can have different variants. + # + # The request variant is a specialization of the request format, like <tt>:tablet</tt>, + # <tt>:phone</tt>, or <tt>:desktop</tt>. + # + # We often want to render different html/json/xml templates for phones, + # tablets, and desktop browsers. Variants make it easy. + # + # You can set the variant in a +before_action+: + # + # request.variant = :tablet if request.user_agent =~ /iPad/ + # + # Respond to variants in the action just like you respond to formats: + # + # respond_to do |format| + # format.html do |variant| + # variant.tablet # renders app/views/projects/show.html+tablet.erb + # variant.phone { extra_setup; render ... } + # variant.none { special_setup } # executed only if there is no variant set + # end + # end + # + # Provide separate templates for each format and variant: + # + # app/views/projects/show.html.erb + # app/views/projects/show.html+tablet.erb + # app/views/projects/show.html+phone.erb + # + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: + # + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end + # + # Variants also support common `any`/`all` block that formats have. + # + # It works for both inline: + # + # respond_to do |format| + # format.html.any { render text: "any" } + # format.html.phone { render text: "phone" } + # end + # + # and block syntax: + # + # respond_to do |format| + # format.html do |variant| + # variant.any(:tablet, :phablet){ render text: "any" } + # variant.phone { render text: "phone" } + # end + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) @@ -260,7 +315,7 @@ module ActionController #:nodoc: # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling + # directly, e.g. for an xml request, the response is equivalent to calling # <code>render xml: resource</code>. # # === Nested resources @@ -321,8 +376,10 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " \ - "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? + if self.class.mimes_for_respond_to.empty? + raise "In order to use respond_with, first you need to declare the " \ + "formats your controller responds to in the class level." + end if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! @@ -360,7 +417,7 @@ module ActionController #:nodoc: # is available. def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) + collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? format = collector.negotiate_format(request) @@ -398,9 +455,11 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :format - def initialize(mimes) + def initialize(mimes, variant = nil) @responses = {} - mimes.each { |mime| send(mime) } + @variant = variant + + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -414,16 +473,63 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end def response - @responses.fetch(format, @responses[Mime::ALL]) + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block + response + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) #call format block with variants collector + variant_collector.variant + end end def negotiate_format(request) @format = request.negotiate_mime(@responses.keys) end + + class VariantCollector #:nodoc: + def initialize(variant = nil) + @variant = variant + @variants = {} + end + + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + key = if @variant.nil? + :none + elsif @variants.has_key?(@variant) + @variant + else + :any + end + + @variants[key] + end + end end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ab14a61b97..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # redirect_to({ action: 'atom' }, alert: "Something serious happened") # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 62a3844b04..6c7b4652d4 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -43,7 +43,7 @@ module ActionController end # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are :json, :js, :xml. + # Default keys are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new # Adds a new renderer to call within controller actions. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd64b1f812..c88074d4c6 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,14 +5,24 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + class InvalidCrossOriginRequest < ActionControllerError #:nodoc: + end + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these - # should be idempotent. + # authentication scheme there anyway). + # + # GET requests are not protected since they don't have side effects like writing + # to the database and don't leak sensitive information. JavaScript requests are + # an exception: a third-party site can use a <script> tag to reference a JavaScript + # URL on your site. When your JavaScript response loads on their site, it executes. + # With carefully crafted JavaScript on their end, sensitive data in your JavaScript + # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or + # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if # you're building an API you'll need something like: @@ -65,17 +75,16 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # + # class ApplicationController < ActionController::Base + # protect_from_forgery + # end + # # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable csrf protection on controller-by-controller basis: - # + # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # - # It can also be disabled for specific controller actions: - # - # skip_before_action :verify_authenticity_token, except: [:create] - # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. @@ -89,6 +98,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -169,18 +179,61 @@ module ActionController #:nodoc: end protected + # The actual before_action that is used to verify the CSRF token. + # Don't override this directly. Provide your own forgery protection + # strategy instead. If you override, you'll disable same-origin + # `<script>` verification. + # + # Lean on the protect_from_forgery declaration to mark which actions are + # due for same-origin request verification. If protect_from_forgery is + # enabled on an action, this before_action flags its after_action to + # verify that JavaScript responses are for XHR requests, ensuring they + # follow the browser's same-origin policy. + def verify_authenticity_token + mark_for_same_origin_verification! + + if !verified_request? + logger.warn "Can't verify CSRF token authenticity" if logger + handle_unverified_request + end + end + def handle_unverified_request forgery_protection_strategy.new(self).handle_unverified_request end - # The actual before_action that is used. Modify this to change how you handle unverified requests. - def verify_authenticity_token - unless verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger - handle_unverified_request + CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ + "<script> tag on another site requested protected JavaScript. " \ + "If you know what you're doing, go ahead and disable forgery " \ + "protection on this action to permit cross-origin JavaScript embedding." + private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + + # If `verify_authenticity_token` was run (indicating that we have + # forgery protection enabled for this request) then also verify that + # we aren't serving an unauthorized cross-origin response. + def verify_same_origin_request + if marked_for_same_origin_verification? && non_xhr_javascript_response? + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end + # GET requests are checked for cross-origin JavaScript after rendering. + def mark_for_same_origin_verification! + @marked_for_same_origin_verification = request.get? + end + + # If the `verify_authenticity_token` before_action ran, verify that + # JavaScript responses are only served to same-origin GET requests. + def marked_for_same_origin_verification? + @marked_for_same_origin_verification ||= false + end + + # Check for cross-origin JavaScript responses. + def non_xhr_javascript_response? + content_type =~ %r(\Atext/javascript) && !request.xhr? + end + # Returns true or false if a request is verified. Checks: # # * is it a GET or HEAD request? Gets should be safe and idempotent diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index b4ba169e8f..e24b56fa91 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -270,7 +270,7 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # Check whether the neceessary Renderer is available + # Check whether the necessary Renderer is available def has_renderer? Renderers::RENDERERS.include?(format) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b4948d99a8..48a916f2b1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -284,14 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - value = super - # Don't rely on +convert_hashes_to_parameters+ - # so as to not mutate via a +fetch+ - if value.is_a?(Hash) - value = self.class.new(value) - value.permit! if permitted? - end - value + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -329,12 +332,21 @@ module ActionController end private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0833e65d23..a2fc814221 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -3,6 +3,7 @@ require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" +require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc: diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 24a3d4741e..920e651b08 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 40bb060d52..c33ba201e1 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,6 +10,8 @@ module ActionDispatch self.ignore_accept_header = false end + attr_reader :variant + # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -48,7 +50,7 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats @@ -64,6 +66,18 @@ module ActionDispatch end end + # Sets the \variant for template. + def variant=(variant) + if variant.is_a? Symbol + @variant = variant + else + raise ArgumentError, "request.variant must be set to a Symbol, not a #{variant.class}. " \ + "For security reasons, never directly set the variant to a user-provided value, " \ + "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ + "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + end + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index ef144c3c76..3d2dd2d632 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,5 +1,6 @@ require 'set' -require 'active_support/core_ext/class/attribute_accessors' +require 'singleton' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' module Mime @@ -27,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.new + Type.lookup_by_extension(type) end def fetch(type) @@ -292,13 +293,13 @@ module Mime end class NullType + include Singleton + def nil? true end - def ref - nil - end + def ref; end def respond_to_missing?(method, include_private = false) method.to_s.ends_with? '?' diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index a6b3aee5e7..0e4da36038 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv +Mime::Type.register "text/vcard", :vcf Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 99b81c898f..1318c62fbe 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -271,7 +271,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {}) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -279,7 +279,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {}) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 5247e61a23..7b2655b2d8 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'monitor' module ActionDispatch # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 7764763791..4410c1b5d5 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -33,8 +33,8 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.inspect}" if name + message = "No route matches #{Hash[constraints.sort].inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" if name raise ActionController::UrlGenerationError, message end diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index bb4cbb00e2..430812fafe 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! # This file is automatically generated by Racc 1.4.9 -# from Racc grammer file "". +# from Racc grammar file "". # require 'racc/parser.rb' diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 9e66cab052..daade5bb74 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -77,12 +77,32 @@ module ActionDispatch end end - class OptimizedPath < String # :nodoc: + class OptimizedPath < Visitor # :nodoc: + def accept(node) + Array(visit(node)) + end + private - def visit_GROUP(node) - "" - end + def visit_CAT(node) + [visit(node.left), visit(node.right)].flatten + end + + def visit_SYMBOL(node) + node.left[1..-1].to_sym + end + + def visit_STAR(node) + visit(node.left) + end + + def visit_GROUP(node) + [] + end + + %w{ LITERAL SLASH DOT }.each do |t| + class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ + end end # Used for formatting urls (url_for) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 3ccd0c9ee8..fe110d7938 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -30,7 +30,7 @@ module ActionDispatch # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # 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. + # # The cookie is signed by your app's <tt>secrets.secret_key_base</tt> value. # # It can be read using the signed method <tt>cookies.signed[:name]</tt> # cookies.signed[:user_id] = current_user.id # @@ -117,10 +117,10 @@ module ActionDispatch # 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, + # If +secrets.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+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -140,10 +140,10 @@ module ActionDispatch # 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, + # If +secrets.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+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -409,7 +409,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and config.secret_key_base are both set. It reads + # config.secret_token and secrets.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: @@ -427,7 +427,7 @@ module ActionDispatch def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end @@ -465,7 +465,7 @@ module ActionDispatch end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # instead of EncryptedCookieJar if config.secret_token and secrets.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: diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 37bf9c8c9f..377f05c982 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,5 @@ require 'action_controller/metal/exceptions' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' module ActionDispatch class ExceptionWrapper diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index b9eb8036e9..1ebc189c28 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -15,8 +15,8 @@ module ActionDispatch # best possible option given your application's configuration. # # 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 + # not encrypted. This means a user cannot alter their +user_id+ without + # knowing your app's secret key, but can easily read their +user_id+. This # was the default for Rails 3 apps. # # If you have secret_key_base set, your cookies will be encrypted. This @@ -31,9 +31,10 @@ module ActionDispatch # # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/initializers/secret_token.rb: + # Configure your secret key in config/secrets.yml: # - # Myapp::Application.config.secret_key_base 'secret key' + # development: + # secret_key_base: 'secret key' # # To generate a secret key for an existing application, run `rake secret`. # diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c6a7d9c415..2764584fe9 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -11,9 +11,10 @@ module ActionDispatch end def match?(path) - path = path.dup + path = unescape_path(path) + return false unless path.valid_encoding? - full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path)) paths = "#{full_path}#{ext}" matches = Dir[paths] @@ -40,7 +41,6 @@ module ActionDispatch end def escape_glob_chars(path) - path.force_encoding('binary') if path.respond_to? :force_encoding path.gsub(/[*?{}\[\]]/, "\\\\\\&") end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 95461fa693..323873ba4b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -89,8 +89,8 @@ } // takes an array of elements with a data-regexp attribute and - // passes their their parent <tr> into the callback function - // if the regexp matchs a given path + // passes their parent <tr> into the callback function + // if the regexp matches a given path function eachElemsForPath(elems, path, func) { each(elems, function(e){ var reg = e.getAttribute("data-regexp"); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 2dfaab3587..ddeea24bb3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = 'signed cookie' config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie' config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie' + config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', @@ -28,6 +29,7 @@ module ActionDispatch initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 6d911a75f1..973627f106 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,6 +7,9 @@ module ActionDispatch ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + # Singleton object used to determine if an optional param wasn't specified + Unspecified = Object.new + def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -127,15 +130,12 @@ module ActionDispatch @delegate.delete key.to_s end - def fetch(key, default=nil) - if self.key?(key) - self[key] - elsif default - self[key] = default - elsif block_given? - self[key] = yield(key) + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @delegate.fetch(key.to_s, &block) else - raise KeyError + @delegate.fetch(key.to_s, default, &block) end end diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 8b43cdada8..a6dca9741c 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,9 +1,15 @@ module ActionDispatch class Request < Rack::Request class Utils # :nodoc: + + mattr_accessor :perform_deep_munge + self.perform_deep_munge = true + class << self # Remove nils from the params hash def deep_munge(hash) + return hash unless perform_deep_munge + hash.each do |k, v| case v when Array diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 120bc54333..f612e91aef 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -69,7 +69,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} end def engine? diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 846a6345cb..18f37dc732 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -217,8 +218,12 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) - controller = [@scope[:module], controller].compact.join("/").presence + if @scope[:module] && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller = controller[1..-1] + else + controller = [@scope[:module], controller].compact.join("/").presence + end end if controller.is_a?(String) && controller =~ %r{\A/} @@ -502,11 +507,12 @@ module ActionDispatch raise "A rack application must be specified" unless path options[:as] ||= app_name(app) + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, options[:as]) + define_generate_prefix(app, target_as) self end @@ -545,11 +551,11 @@ module ActionDispatch _routes = @set app.routes.define_mounted_helper(name) app.routes.singleton_class.class_eval do - define_method :mounted? do + redefine_method :mounted? do true end - define_method :_generate_prefix do |options| + redefine_method :_generate_prefix do |options| prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3e54c7e71c..b08e62543b 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -26,14 +26,19 @@ module ActionDispatch end uri = URI.parse(path(req.symbolized_path_parameters, req)) + + unless uri.host + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + elsif uri.path.empty? + uri.path = req.script_name.empty? ? "/" : req.script_name + end + end + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? - if relative_path?(uri.path) - uri.path = "#{req.script_name}/#{uri.path}" - end - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) headers = { @@ -57,11 +62,33 @@ module ActionDispatch def relative_path?(path) path && !path.empty? && path[0] != '/' end + + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + end + + def escape_fragment(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + end + + def escape_path(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + end end class PathRedirect < Redirect + URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/ + def path(params, request) - (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params)) + if block.match(URL_PARTS) + path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1 + query = interpolation_required?($2, params) ? $2 % escape(params) : $2 + fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3 + + "#{path}#{query}#{fragment}" + else + interpolation_required?(block, params) ? block % escape(params) : block + end end def inspect @@ -69,8 +96,8 @@ module ActionDispatch end private - def escape(params) - Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + def interpolation_required?(string, params) + !params.empty? && string && string.match(/%\{\w*\}/) end end @@ -90,22 +117,22 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end - if relative_path?(url_options[:path]) - url_options[:path] = "/#{url_options[:path]}" - url_options[:script_name] = request.script_name + unless options[:host] || options[:domain] + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + elsif url_options[:path].empty? + url_options[:path] = request.script_name.empty? ? "/" : "" + url_options[:script_name] = request.script_name + end end - + ActionDispatch::Http::URL.url_for url_options end def inspect "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" end - - private - def escape_path(params) - Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] - end end module Redirection diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index b8abdabca5..a03fb4cee7 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -163,9 +163,10 @@ module ActionDispatch def initialize(route, options) super - @path_parts = @route.required_parts - @arg_size = @path_parts.size - @string_route = @route.optimized_path + @klass = Journey::Router::Utils + @required_parts = @route.required_parts + @arg_size = @required_parts.size + @optimized_path = @route.optimized_path end def call(t, args) @@ -182,43 +183,36 @@ module ActionDispatch private def optimized_helper(args) - path = @string_route.dup - klass = Journey::Router::Utils + params = Hash[parameterize_args(args)] + missing_keys = missing_keys(params) - @path_parts.zip(args) do |part, arg| - parameterized_arg = arg.to_param + unless missing_keys.empty? + raise_generation_error(params, missing_keys) + end - if parameterized_arg.nil? || parameterized_arg.empty? - raise_generation_error(args) - end + @optimized_path.map{ |segment| replace_segment(params, segment) }.join + end - # Replace each route parameter - # e.g. :id for regular parameter or *path for globbing - # with ruby string interpolation code - path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(parameterized_arg)) - end - path + def replace_segment(params, segment) + Symbol === segment ? @klass.escape_fragment(params[segment]) : segment end def optimize_routes_generation?(t) t.send(:optimize_routes_generation?) end - def raise_generation_error(args) - parts, missing_keys = [], [] - - @path_parts.zip(args) do |part, arg| - parameterized_arg = arg.to_param - - if parameterized_arg.nil? || parameterized_arg.empty? - missing_keys << part - end + def parameterize_args(args) + @required_parts.zip(args.map(&:to_param)) + end - parts << [part, arg] - end + def missing_keys(args) + args.select{ |part, arg| arg.nil? || arg.empty? }.keys + end - message = "No route matches #{Hash[parts].inspect}" - message << " missing required keys: #{missing_keys.inspect}" + def raise_generation_error(args, missing_keys) + constraints = Hash[@route.requirements.merge(args).sort] + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" raise ActionController::UrlGenerationError, message end @@ -226,7 +220,7 @@ module ActionDispatch def initialize(route, options) @options = options - @segment_keys = route.segment_keys + @segment_keys = route.segment_keys.uniq @route = route end @@ -361,7 +355,7 @@ module ActionDispatch include UrlFor end - # Contains all the mounted helpers accross different + # Contains all the mounted helpers across different # engines and the `main_app` helper for the application. # You can include this in your classes if you want to # access routes for other engines. diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 496682e8bd..f1f998d932 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -211,7 +211,7 @@ module ActionDispatch def fail_on(exception_class) yield rescue exception_class => e - raise MiniTest::Assertion, e.message + raise Minitest::Assertion, e.message end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9beb30307b..cc6b763093 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -137,7 +137,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include MiniTest::Assertions + include Minitest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index ad5acd8080..77f656d6f1 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index fd08f392aa..a51f6a434a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,7 +1,7 @@ module ActionPack # Returns the version of the currently loaded ActionPack as a Gem::Version def self.version - Gem::Version.new "4.1.0.beta" + Gem::Version.new "4.1.0.beta1" end module VERSION #:nodoc: diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index 5709ad0378..b1a5044399 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -37,7 +37,7 @@ module AbstractController test "does not register unknown mime types" do collector = MyCollector.new - assert_raise NameError do + assert_raise NoMethodError do collector.unknown end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index a0d90f7eee..37e993b4e5 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -43,6 +43,9 @@ Thread.abort_on_exception = true # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + # Register danish language for testing I18n.backend.store_translations 'da', {} I18n.backend.store_translations 'pt-BR', {} @@ -246,8 +249,6 @@ class Rack::TestCase < ActionDispatch::IntegrationTest end end -ActionController::Base.superclass.send(:include, ActionView::Layouts) - module ActionController class Base include ActionController::Testing @@ -333,7 +334,6 @@ class ThreadsController < ResourcesController; end class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end class ReviewsController < ResourcesController; end -class AuthorsController < ResourcesController; end class LogosController < ResourcesController; end class AccountsController < ResourcesController; end @@ -344,8 +344,6 @@ class PreferencesController < ResourcesController; end module Backoffice class ProductsController < ResourcesController; end - class TagsController < ResourcesController; end - class ManufacturersController < ResourcesController; end class ImagesController < ResourcesController; end module Admin diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 8eec98e916..5e64cae7e2 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -19,7 +19,7 @@ module ActionDispatch @response = FakeResponse.new sym assert_response sym - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } end @@ -29,11 +29,11 @@ module ActionDispatch @response = FakeResponse.new 400 assert_response 400 - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :unauthorized } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response 500 } end @@ -42,11 +42,11 @@ module ActionDispatch @response = FakeResponse.new 401 assert_response :unauthorized - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :ok } - assert_raises(MiniTest::Assertion) { + assert_raises(Minitest::Assertion) { assert_response :success } end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index ba4cffcd3e..b6b5a218cc 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -444,22 +444,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_assert_response_uses_exception_message @controller = AssertResponseWithUnexpectedErrorController.new - get :index + e = assert_raise RuntimeError, 'Expected non-success response' do + get :index + end assert_response :success - flunk 'Expected non-success response' - rescue RuntimeError => e - assert e.message.include?('FAIL') + assert_includes 'FAIL', e.message end def test_assert_response_failure_response_with_no_exception @controller = AssertResponseWithUnexpectedErrorController.new get :show - assert_response :success - flunk 'Expected non-success response' - rescue ActiveSupport::TestCase::Assertion - # success - rescue - flunk "assert_response failed to handle failure response with missing, but optional, exception." + assert_response 500 + assert_equal 'Boom', response.body end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 3b5d7ef446..d3efca5b6f 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -893,17 +893,6 @@ class ControllerWithFilterInstance < PostsController around_filter YieldingFilter.new, :only => :raises_after end -class ControllerWithFilterMethod < PostsController - class YieldingFilter < DefaultFilter - def around(controller) - yield - raise After - end - end - - around_filter YieldingFilter.new.method(:around), :only => :raises_after -end - class ControllerWithProcFilter < PostsController around_filter(:only => :no_raise) do |c,b| c.instance_variable_set(:"@before", true) diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 9f1c168209..52a0bc9aa3 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -21,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase def authenticate authenticate_or_request_with_http_digest("SuperSecret") do |username| - # Return the password + # Returns the password USERS[username] end end diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb index 6b02eedaed..c95ef8a0c7 100644 --- a/actionpack/test/controller/localized_templates_test.rb +++ b/actionpack/test/controller/localized_templates_test.rb @@ -34,4 +34,15 @@ class LocalizedTemplatesTest < ActionController::TestCase get :hello_world assert_equal "Gutten Tag", @response.body end + + def test_localized_template_has_correct_header_with_no_format_in_template_name + old_locale = I18n.locale + I18n.locale = :it + + get :hello_world + assert_equal "Ciao Mondo", @response.body + assert_equal "text/html", @response.content_type + ensure + I18n.locale = old_locale + end end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 774dabe105..84e4936f31 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -146,6 +146,106 @@ class RespondToController < ActionController::Base end end + def variant_with_implicit_rendering + end + + def variant_with_format_and_custom_render + request.variant = :mobile + + respond_to do |type| + type.html { render text: "mobile" } + end + end + + def multiple_variants_for_format + respond_to do |type| + type.html do |html| + html.tablet { render text: "tablet" } + html.phone { render text: "phone" } + end + end + end + + def variant_plus_none_for_format + respond_to do |format| + format.html do |variant| + variant.phone { render text: "phone" } + variant.none + end + end + end + + def variant_inline_syntax + respond_to do |format| + format.js { render text: "js" } + format.html.none { render text: "none" } + format.html.phone { render text: "phone" } + end + end + + def variant_inline_syntax_without_block + respond_to do |format| + format.js + format.html.none + format.html.phone + end + end + + def variant_any + respond_to do |format| + format.html do |variant| + variant.any(:tablet, :phablet){ render text: "any" } + variant.phone { render text: "phone" } + end + end + end + + def variant_any_any + respond_to do |format| + format.html do |variant| + variant.any { render text: "any" } + variant.phone { render text: "phone" } + end + end + end + + def variant_inline_any + respond_to do |format| + format.html.any(:tablet, :phablet){ render text: "any" } + format.html.phone { render text: "phone" } + end + end + + def variant_inline_any_any + respond_to do |format| + format.html.phone { render text: "phone" } + format.html.any { render text: "any" } + end + end + + def variant_any_implicit_render + respond_to do |format| + format.html.phone + format.html.any(:tablet, :phablet) + end + end + + def variant_any_with_none + respond_to do |format| + format.html.any(:none, :phone){ render text: "none or phone" } + end + end + + def format_any_variant_any + respond_to do |format| + format.html { render text: "HTML" } + format.any(:js, :xml) do |variant| + variant.phone{ render text: "phone" } + variant.any(:tablet, :phablet){ render text: "tablet" } + end + end + end + protected def set_layout case action_name @@ -490,4 +590,154 @@ class RespondToControllerTest < ActionController::TestCase get :using_defaults, :format => "invalidformat" end end + + def test_invalid_variant + @request.variant = :invalid + assert_raises(ActionView::MissingTemplate) do + get :variant_with_implicit_rendering + end + end + + def test_variant_not_set_regular_template_missing + assert_raises(ActionView::MissingTemplate) do + get :variant_with_implicit_rendering + end + end + + def test_variant_with_implicit_rendering + @request.variant = :mobile + get :variant_with_implicit_rendering + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_variant_with_format_and_custom_render + @request.variant = :phone + get :variant_with_format_and_custom_render + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_multiple_variants_for_format + @request.variant = :tablet + get :multiple_variants_for_format + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + end + + def test_no_variant_in_variant_setup + get :variant_plus_none_for_format + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + end + + def test_variant_inline_syntax + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + + get :variant_inline_syntax + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + + @request.variant = :phone + get :variant_inline_syntax + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_inline_syntax_without_block + @request.variant = :phone + get :variant_inline_syntax_without_block + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_any + @request.variant = :phone + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :tablet + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + @request.variant = :phablet + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_any + @request.variant = :phone + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :yolo + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any + @request.variant = :phone + get :variant_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :tablet + get :variant_inline_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + @request.variant = :phablet + get :variant_inline_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any_any + @request.variant = :phone + get :variant_inline_any_any + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + @request.variant = :yolo + get :variant_inline_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_implicit_render + @request.variant = :tablet + get :variant_any_implicit_render + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + + @request.variant = :phablet + get :variant_any_implicit_render + assert_equal "text/html", @response.content_type + assert_equal "phablet", @response.body + end + + def test_variant_any_with_none + get :variant_any_with_none + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + + @request.variant = :phone + get :variant_any_with_none + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + end + + def test_format_any_variant_any + @request.variant = :tablet + get :format_any_variant_any, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "tablet", @response.body + end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index b60c5f058d..33a91d72d9 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -8,9 +8,16 @@ class ParametersPermitTest < ActiveSupport::TestCase end setup do - @params = ActionController::Parameters.new({ person: { - age: "32", name: { first: "David", last: "Heinemeier Hansson" } - }}) + @params = ActionController::Parameters.new( + person: { + age: '32', + name: { + first: 'David', + last: 'Heinemeier Hansson' + }, + addresses: [{city: 'Chicago', state: 'Illinois'}] + } + ) @struct_fields = [] %w(0 1 12).each do |number| @@ -153,6 +160,18 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal nil, params[:foo] end + test 'hashes in array values get wrapped' do + params = ActionController::Parameters.new(foo: [{}, {}]) + params[:foo].each do |hash| + assert !hash.permitted? + end + end + + test 'arrays are converted at most once' do + params = ActionController::Parameters.new(foo: [{}]) + assert params[:foo].equal?(params[:foo]) + end + test "fetch doesnt raise ParameterMissing exception if there is a default" do assert_equal "monkey", @params.fetch(:foo, "monkey") assert_equal "monkey", @params.fetch(:foo) { "monkey" } @@ -221,6 +240,7 @@ class ParametersPermitTest < ActiveSupport::TestCase assert @params.permitted? assert @params[:person].permitted? assert @params[:person][:name].permitted? + assert @params[:person][:addresses][0].permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb index f070109b27..d550422a2f 100644 --- a/actionpack/test/controller/render_js_test.rb +++ b/actionpack/test/controller/render_js_test.rb @@ -22,7 +22,7 @@ class RenderJSTest < ActionController::TestCase tests TestController def test_render_vanilla_js - get :render_vanilla_js_hello + xhr :get, :render_vanilla_js_hello assert_equal "alert('hello')", @response.body assert_equal "text/javascript", @response.content_type end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 7c0a6bd67e..de8d1cbd9b 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase end def test_render_json_with_callback - get :render_json_hello_world_with_callback + xhr :get, :render_json_hello_world_with_callback assert_equal 'alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end def test_render_json_with_custom_content_type - get :render_json_with_custom_content_type + xhr :get, :render_json_with_custom_content_type assert_equal '{"hello":"world"}', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index f41287381a..26806fb03f 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -529,4 +529,4 @@ class HeadRenderTest < ActionController::TestCase assert_equal "something", @response.headers["X-Custom-Header"] assert_response :forbidden end -end
\ No newline at end of file +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 727db79241..1f5fc06410 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -52,18 +52,36 @@ module RequestForgeryProtectionActions render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" end + def same_origin_js + render js: 'foo();' + end + + def negotiate_same_origin + respond_to do |format| + format.js { same_origin_js } + end + end + + def cross_origin_js + same_origin_js + end + + def negotiate_cross_origin + negotiate_same_origin + end + def rescue_action(e) raise e end end # sample controllers class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :reset_session + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session end class RequestForgeryProtectionControllerUsingException < ActionController::Base include RequestForgeryProtectionActions - protect_from_forgery :only => %w(index meta), :with => :exception + protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception end class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base @@ -201,7 +219,7 @@ module RequestForgeryProtectionTests end def test_should_not_allow_post_without_token_irrespective_of_format - assert_blocked { post :index, :format=>'xml' } + assert_blocked { post :index, format: 'xml' } end def test_should_not_allow_patch_without_token @@ -271,6 +289,48 @@ module RequestForgeryProtectionTests end end + def test_should_only_allow_same_origin_js_get_with_xhr_header + assert_cross_origin_blocked { get :same_origin_js } + assert_cross_origin_blocked { get :same_origin_js, format: 'js' } + assert_cross_origin_blocked do + @request.accept = 'text/javascript' + get :negotiate_same_origin + end + + assert_cross_origin_not_blocked { xhr :get, :same_origin_js } + assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_same_origin + end + end + + # Allow non-GET requests since GET is all a remote <script> tag can muster. + def test_should_allow_non_get_js_without_xhr_header + assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token } + assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + post :negotiate_same_origin, custom_authenticity_token: @token + end + end + + def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled + assert_cross_origin_not_blocked { get :cross_origin_js } + assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + get :negotiate_cross_origin + end + + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js } + assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' } + assert_cross_origin_not_blocked do + @request.accept = 'text/javascript' + xhr :get, :negotiate_cross_origin + end + end + def assert_blocked session[:something_like_user_id] = 1 yield @@ -282,6 +342,16 @@ module RequestForgeryProtectionTests assert_nothing_raised { yield } assert_response :success end + + def assert_cross_origin_blocked + assert_raises(ActionController::InvalidCrossOriginRequest) do + yield + end + end + + def assert_cross_origin_not_blocked + assert_not_blocked { yield } + end end # OK let's get our test on @@ -305,13 +375,13 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController end end -class NullSessionDummyKeyGenerator - def generate_key(secret) - '03312270731a2ed0d11ed091c2338a06' +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + class NullSessionDummyKeyGenerator + def generate_key(secret) + '03312270731a2ed0d11ed091c2338a06' + end end -end -class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase def setup @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new end @@ -375,8 +445,8 @@ end class CustomAuthenticityParamControllerTest < ActionController::TestCase def setup - ActionController::Base.request_forgery_protection_token = :custom_token_name super + ActionController::Base.request_forgery_protection_token = :custom_token_name end def teardown diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 2c84e95c6e..df453a0251 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1833,11 +1833,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default')) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get)) assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post)) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) } - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) } assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar')) - assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') } + assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') } assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get)) assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get)) @@ -1916,11 +1916,4 @@ class RackMountIntegrationTests < ActiveSupport::TestCase end extras end - - def assert_raise(e) - result = yield - flunk "Did not raise #{e}, but returned #{result.inspect}" - rescue e - assert true - end end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 0326bf4562..4df2f8b98d 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -148,7 +148,7 @@ class SendFileTest < ActionController::TestCase } @controller.headers = {} - assert !@controller.send(:send_file_headers!, options) + assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) } end def test_send_file_headers_guess_type_from_extension diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 8a19129695..981cf2426e 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -31,21 +31,21 @@ class MimeTypeTest < ActiveSupport::TestCase test "parse text with trailing star at the beginning" do accept = "text/*, text/html, application/json, multipart/form-data" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star in the end" do accept = "text/html, application/json, multipart/form-data, text/*" - expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML] + expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end test "parse text with trailing star" do accept = "text/*" - expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON] + expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON] parsed = Mime::Type.parse(accept) assert_equal expect, parsed end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 30e95a0b75..683a4f01e2 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -5,7 +5,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest class FakeEngine def self.routes - Object.new + @routes ||= ActionDispatch::Routing::RouteSet.new end def self.call(env) @@ -27,12 +27,23 @@ class TestRoutingMount < ActionDispatch::IntegrationTest scope "/its_a" do mount SprocketsApp, :at => "/sprocket" end + + resources :users do + mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource + end end def app Router end + def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources + assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource), + "A mounted helper should be defined with a parent's prefix" + assert Router.named_routes.routes[:user_fake_mounted_at_resource], + "A named route should be defined with a parent's prefix" + end + def test_trailing_slash_is_not_removed_from_path_info get "/sprockets/omg/" assert_equal "/sprockets -- /omg/", response.body diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index e519fff51e..08501d19c0 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -32,12 +32,18 @@ module TestGenerationPrefix get "/conflicting_url", :to => "inside_engine_generating#conflicting" get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test - get "/relative_path_redirect", :to => redirect("foo") + get "/relative_path_root", :to => redirect("") + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_root", :to => redirect(:path => "") get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_root", :to => redirect { |params, request| "" } get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } - get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_path_root", :to => redirect("/") + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_root", :to => redirect(:path => "/") get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_root", :to => redirect { |params, request| "/" } get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end @@ -190,46 +196,64 @@ module TestGenerationPrefix assert_equal "engine", last_response.body end + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_root" + verify_redirect "http://example.org/awesome/blog" + end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_root" + verify_redirect "http://example.org/awesome/blog" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_root" + verify_redirect "http://example.org/awesome/blog" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/awesome/blog/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" end # Inside Application @@ -320,6 +344,17 @@ module TestGenerationPrefix path = engine_object.polymorphic_url(Post.new, :host => "www.example.com") assert_equal "http://www.example.com/awesome/blog/posts/1", path end + + private + def verify_redirect(url, status = 301) + assert_equal status, last_response.status + assert_equal url, last_response.headers["Location"] + assert_equal expected_redirect_body(url), last_response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end end class EngineMountedAtRoot < ActionDispatch::IntegrationTest @@ -332,12 +367,18 @@ module TestGenerationPrefix routes.draw do get "/posts/:id", :to => "posts#show", :as => :post - get "/relative_path_redirect", :to => redirect("foo") + get "/relative_path_root", :to => redirect("") + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_root", :to => redirect(:path => "") get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_root", :to => redirect { |params, request| "" } get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } - get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_path_root", :to => redirect("/") + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_root", :to => redirect(:path => "/") get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_root", :to => redirect { |params, request| "/" } get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end @@ -390,46 +431,75 @@ module TestGenerationPrefix assert_equal "/posts/1", last_response.body end + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/relative_path_root" + verify_redirect "http://example.org/" + end + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/relative_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/relative_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/relative_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/relative_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/relative_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/absolute_path_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/absolute_path_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/absolute_option_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/absolute_option_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body + verify_redirect "http://example.org/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_root" + verify_redirect "http://example.org/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/absolute_custom_redirect" - assert_equal 301, last_response.status - assert_equal "http://example.org/foo", last_response.headers["Location"] - assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body - end + verify_redirect "http://example.org/foo" + end + + private + def verify_redirect(url, status = 301) + assert_equal status, last_response.status + assert_equal url, last_response.headers["Location"] + assert_equal expected_redirect_body(url), last_response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end end end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb index f072a9f717..d82493140f 100644 --- a/actionpack/test/dispatch/request/query_string_parsing_test.rb +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -11,6 +11,17 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest head :ok end end + class EarlyParse + def initialize(app) + @app = app + end + + def call(env) + # Trigger a Rack parse so that env caches the query params + Rack::Request.new(env).params + @app.call(env) + end + end def teardown TestController.last_query_parameters = nil @@ -93,6 +104,21 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest assert_parses({"action" => ['1']}, "action[]=1&action[]") end + test "perform_deep_munge" do + ActionDispatch::Request::Utils.perform_deep_munge = false + begin + assert_parses({"action" => nil}, "action") + assert_parses({"action" => {"foo" => nil}}, "action[foo]") + assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]") + assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]") + assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]") + assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]") + assert_parses({"action" => ['1',nil]}, "action[]=1&action[]") + ensure + ActionDispatch::Request::Utils.perform_deep_munge = true + end + end + test "query string with empty key" do assert_parses( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, @@ -131,6 +157,10 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest set.draw do get ':action', :to => ::QueryStringParsingTest::TestController end + @app = self.class.build_app(set) do |middleware| + middleware.use(EarlyParse) + end + get "/parse", actual assert_response :ok diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index a244d1364c..df55fcc8bc 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -68,13 +68,12 @@ module ActionDispatch assert_equal '1', session.fetch(:one) assert_equal '2', session.fetch(:two, '2') - assert_equal '2', session.fetch(:two) + assert_nil session.fetch(:two, nil) assert_equal 'three', session.fetch(:three) {|el| el.to_s } - assert_equal 'three', session.fetch(:three) assert_raise KeyError do - session.fetch(:four) + session.fetch(:three) end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index f6de9748ca..f79fe47897 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -93,6 +93,14 @@ class RequestTest < ActiveSupport::TestCase assert_equal '1.1.1.1', request.remote_ip end + test "remote ip spoof protection ignores private addresses" do + request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.17.19.51', + 'HTTP_CLIENT_IP' => '172.17.19.51', + 'REMOTE_ADDR' => '1.1.1.1', + 'HTTP_X_BLUECOAT_VIA' => 'de462e07a2db325e' + assert_equal '1.1.1.1', request.remote_ip + end + test "remote ip v6" do request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334' assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip @@ -598,7 +606,7 @@ class RequestTest < ActiveSupport::TestCase 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) assert_equal [Mime::JS], request.formats - + request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8', 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) @@ -616,10 +624,10 @@ class RequestTest < ActiveSupport::TestCase test "format is not nil with unknown format" do request = stub_request request.expects(:parameters).at_least_once.returns({ format: :hello }) - assert_equal request.format.nil?, true - assert_equal request.format.html?, false - assert_equal request.format.xml?, false - assert_equal request.format.json?, false + assert request.format.nil? + assert_not request.format.html? + assert_not request.format.xml? + assert_not request.format.json? end test "formats with xhr request" do @@ -836,6 +844,19 @@ class RequestTest < ActiveSupport::TestCase end end + test "setting variant" do + request = stub_request + request.variant = :mobile + assert_equal :mobile, request.variant + end + + test "setting variant with non symbol value" do + request = stub_request + assert_raise ArgumentError do + request.variant = "mobile" + end + end + protected def stub_request(env = {}) diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index c8038bbd7c..18a52f13a7 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -203,6 +203,18 @@ module ActionDispatch assert_no_match(/\/sprockets/, output.first) end + def test_rake_routes_shows_route_defined_in_under_assets_prefix + output = draw do + scope '/sprockets' do + get '/foo' => 'foo#bar' + end + end + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " foo GET /sprockets/foo(.:format) foo#bar" + ], output + end + def test_redirect output = draw do get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" } diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 3e9e90a950..5a532dc38f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2864,6 +2864,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert !@request.params[:action].frozen? end + def test_multiple_positional_args_with_the_same_name + draw do + get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false + end + + expected_params = { + controller: 'downloads', + action: 'show', + id: '1' + } + + get '/downloads/1/1.tar' + assert_equal 'downloads#show', @response.body + assert_equal expected_params, @request.symbolized_path_parameters + assert_equal '/downloads/1/1.tar', download_path('1') + assert_equal '/downloads/1/1.tar', download_path('1', '1') + end + + def test_absolute_controller_namespace + draw do + namespace :foo do + get '/', to: '/bar#index', as: 'root' + end + end + + get '/foo' + assert_equal 'bar#index', @response.body + assert_equal '/foo', foo_root_path + end + private def draw(&block) @@ -3235,7 +3265,9 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest get "/foo/:id" => redirect("/foo/bar/%{id}") get "/bar/:id" => redirect(:path => "/foo/bar/%{id}") + get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}") get "/foo/bar/:id" => ok + get "/baz" => ok end end @@ -3251,6 +3283,14 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest verify_redirect "http://www.example.com/foo/bar/1%3E" end + test "path redirect escapes interpolated parameters correctly" do + get "/foo/1%201" + verify_redirect "http://www.example.com/foo/bar/1%201" + + get "/baz/1%201" + verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201" + end + private def verify_redirect(url, status=301) assert_equal status, @response.status @@ -3317,6 +3357,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } get '/foo' => ok, as: :foo get '/post(/:action(/:id))' => ok, as: :posts + get '/:foo/:foo_type/bars/:id' => ok, as: :bar + get '/projects/:id.:format' => ok, as: :project end end @@ -3339,6 +3381,16 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest assert_equal '/post', Routes.url_helpers.posts_path assert_equal '/post', posts_path end + + test 'segments with same prefix are replaced correctly' do + assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1') + assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1') + end + + test 'segments separated with a period are replaced correctly' do + assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json) + assert_equal '/projects/1.json', project_path(1, :json) + end end class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest @@ -3686,3 +3738,28 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest end end end + +class TestUrlGenerationErrors < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/products/:id" => 'products#show', :as => :product + end + end + + def app; Routes end + + include Routes.url_helpers + + test "url helpers raise a helpful error message whem generation fails" do + url, missing = { action: 'show', controller: 'products', id: nil }, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } + assert_equal message, error.message + end +end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index acccbcb2e6..d83461e52f 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -137,7 +137,7 @@ module StaticTests end def with_static_file(file) - path = "#{FIXTURE_LOAD_PATH}/public" + file + path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file File.open(path, "wb+") { |f| f.write(file) } yield file ensure @@ -149,11 +149,24 @@ class StaticTest < ActiveSupport::TestCase DummyApp = lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] } - App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60") def setup - @app = App + @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60") + end + + def public_path + "public" end include StaticTests end + +class StaticEncodingTest < StaticTest + def setup + @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, max-age=60") + end + + def public_path + "公共" + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index f919592d24..fdea27e2d2 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -66,7 +66,7 @@ module TestUrlGeneration assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://") end - test "port option overides the host" do + test "port option overrides the host" do assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080) end diff --git a/actionpack/test/fixtures/localized/hello_world.it.erb b/actionpack/test/fixtures/localized/hello_world.it.erb new file mode 100644 index 0000000000..9191fdc187 --- /dev/null +++ b/actionpack/test/fixtures/localized/hello_world.it.erb @@ -0,0 +1 @@ +Ciao Mondo
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb new file mode 100644 index 0000000000..e905d051bf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb @@ -0,0 +1 @@ +phablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb new file mode 100644 index 0000000000..65526af8cf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb @@ -0,0 +1 @@ +tablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb new file mode 100644 index 0000000000..cd222a4a49 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb @@ -0,0 +1 @@ +phone
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb new file mode 100644 index 0000000000..c86c3f3551 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb @@ -0,0 +1 @@ +none
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb new file mode 100644 index 0000000000..317801ad30 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb @@ -0,0 +1 @@ +mobile
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/bar.html b/actionpack/test/fixtures/公共/foo/bar.html new file mode 100644 index 0000000000..9a35646205 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/bar.html @@ -0,0 +1 @@ +/foo/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/baz.css b/actionpack/test/fixtures/公共/foo/baz.css new file mode 100644 index 0000000000..b5173fbef2 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/baz.css @@ -0,0 +1,3 @@ +body { +background: #000; +} diff --git a/actionpack/test/fixtures/公共/foo/index.html b/actionpack/test/fixtures/公共/foo/index.html new file mode 100644 index 0000000000..497a2e898f --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/index.html @@ -0,0 +1 @@ +/foo/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/こんにちは.html b/actionpack/test/fixtures/公共/foo/こんにちは.html new file mode 100644 index 0000000000..1df9166522 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/こんにちは.html @@ -0,0 +1 @@ +means hello in Japanese diff --git a/actionpack/test/fixtures/公共/index.html b/actionpack/test/fixtures/公共/index.html new file mode 100644 index 0000000000..525950ba6b --- /dev/null +++ b/actionpack/test/fixtures/公共/index.html @@ -0,0 +1 @@ +/index.html
\ No newline at end of file |