diff options
author | Carl Lerche & Yehuda Katz <wycats@gmail.com> | 2009-04-13 15:18:45 -0700 |
---|---|---|
committer | Carl Lerche & Yehuda Katz <wycats@gmail.com> | 2009-04-13 15:18:45 -0700 |
commit | 906aebceedb95d8caa6db6314bc90f605bdfaf2b (patch) | |
tree | 5abc86bb6709b20df7cb5f4d1750b27c641dca4b /actionpack/lib/action_controller/base | |
parent | 2036d3ba75da1a0f3061bf5a33c89e2b2eaff420 (diff) | |
parent | c877857d59554d78dbf45f5f9fcaafb8badec4e2 (diff) | |
download | rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.gz rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.bz2 rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.zip |
Bring abstract_controller up to date with rails/master
Resolved all the conflicts since 2.3.0 -> HEAD. Following is a list
of commits that could not be applied cleanly or are obviated with the
abstract_controller refactor. They all need to be revisited to ensure
that fixes made in 2.3 do not reappear in 3.0:
2259ecf368e6a6715966f69216e3ee86bf1a82a7
AR not available
* This will be reimplemented with ActionORM or equivalent
06182ea02e92afad579998aa80144588e8865ac3
implicitly rendering a js response should not use the default layout
[#1844 state:resolved]
* This will be handled generically
893e9eb99504705419ad6edac14d00e71cef5f12
Improve view rendering performance in development mode and reinstate
template recompiling in production [#1909 state:resolved]
* We will need to reimplement rails-dev-boost on top of the refactor;
the changes here are very implementation specific and cannot be
cleanly applied. The following commits are implicated:
199e750d46c04970b5e7684998d09405648ecbd4
3942cb406e1d5db0ac00e03153809cc8dc4cc4db
f8ea9f85d4f1e3e6f3b5d895bef6b013aa4b0690
e3b166aab37ddc2fbab030b146eb61713b91bf55
ae9f258e03c9fd5088da12c1c6cd216cc89a01f7
44423126c6f6133a1d9cf1d0832b527e8711d40f
0cb020b4d6d838025859bd60fb8151c8e21b8e84
workaround for picking layouts based on wrong view_paths
[#1974 state:resolved]
* The specifics of this commit no longer apply. Since it is a two-line
commit, we will reimplement this change.
8c5cc66a831aadb159f3daaffa4208064c30af0e
make action_controller/layouts pick templates from the current instance's
view_paths instead of the class view_paths [#1974 state:resolved]
* This does not apply at all. It should be trivial to apply the feature
to the reimplemented ActionController::Base.
87e8b162463f13bd50d27398f020769460a770e3
fix HTML fallback for explicit templates [#2052 state:resolved]
* There were a number of patches related to this that simply compounded
each other. Basically none of them apply cleanly, and the underlying
issue needs to be revisited. After discussing the underlying problem
with Koz, we will defer these fixes for further discussion.
Diffstat (limited to 'actionpack/lib/action_controller/base')
8 files changed, 123 insertions, 40 deletions
diff --git a/actionpack/lib/action_controller/base/base.rb b/actionpack/lib/action_controller/base/base.rb index 7a745ea040..aea434ffa0 100644 --- a/actionpack/lib/action_controller/base/base.rb +++ b/actionpack/lib/action_controller/base/base.rb @@ -22,7 +22,7 @@ module ActionController #:nodoc: attr_reader :allowed_methods def initialize(*allowed_methods) - super("Only #{allowed_methods.to_sentence} requests are allowed.") + super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.") @allowed_methods = allowed_methods end @@ -394,7 +394,7 @@ module ActionController #:nodoc: # Return an array containing the names of public methods that have been marked hidden from the action processor. # By default, all methods defined in ActionController::Base and included modules are hidden. - # More methods can be hidden using <tt>hide_actions</tt>. + # More methods can be hidden using <tt>hide_action</tt>. def hidden_actions read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, []) end @@ -690,6 +690,11 @@ module ActionController #:nodoc: # request is considered stale and should be generated from scratch. Otherwise, # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. # + # Parameters: + # * <tt>:etag</tt> + # * <tt>:last_modified</tt> + # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). + # # Example: # # def show @@ -710,20 +715,34 @@ module ActionController #:nodoc: # Sets the etag, last_modified, or both on the response and renders a # "304 Not Modified" response if the request is already fresh. # + # Parameters: + # * <tt>:etag</tt> + # * <tt>:last_modified</tt> + # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). + # # Example: # # def show # @article = Article.find(params[:id]) - # fresh_when(:etag => @article, :last_modified => @article.created_at.utc) + # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true) # end # # This will render the show template if the request isn't sending a matching etag or # If-Modified-Since header and just a "304 Not Modified" response if there's a match. + # def fresh_when(options) - options.assert_valid_keys(:etag, :last_modified) + options.assert_valid_keys(:etag, :last_modified, :public) response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] + + if options[:public] + cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip } + cache_control.delete("private") + cache_control.delete("no-cache") + cache_control << "public" + response.headers["Cache-Control"] = cache_control.join(', ') + end if request.fresh?(response) head :not_modified @@ -735,15 +754,26 @@ module ActionController #:nodoc: # # Examples: # expires_in 20.minutes - # expires_in 3.hours, :private => false - # expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true + # expires_in 3.hours, :public => true + # expires in 3.hours, 'max-stale' => 5.hours, :public => true # # This method will overwrite an existing Cache-Control header. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. def expires_in(seconds, options = {}) #:doc: - cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) - cache_options.delete_if { |k,v| v.nil? or v == false } - cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} + cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip } + + cache_control << "max-age=#{seconds}" + cache_control.delete("no-cache") + if options[:public] + cache_control.delete("private") + cache_control << "public" + else + cache_control << "private" + end + + # This allows for additional headers to be passed through like 'max-stale' => 5.hours + cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} + response.headers["Cache-Control"] = cache_control.join(', ') end @@ -839,6 +869,7 @@ module ActionController #:nodoc: end end + # Returns true if a render or redirect has already been performed. def performed? @performed_render || @performed_redirect end @@ -857,6 +888,7 @@ module ActionController #:nodoc: @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}" end + # Returns the request URI used to get to the current location def complete_request_uri "#{request.protocol}#{request.host}#{request.request_uri}" end diff --git a/actionpack/lib/action_controller/base/cookies.rb b/actionpack/lib/action_controller/base/cookies.rb index 840ceb5abd..ca380e98d0 100644 --- a/actionpack/lib/action_controller/base/cookies.rb +++ b/actionpack/lib/action_controller/base/cookies.rb @@ -41,7 +41,7 @@ module ActionController #:nodoc: # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object. # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. # Default is +false+. - # * <tt>:http_only</tt> - Whether this cookie is accessible via scripting or + # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. module Cookies def self.included(base) diff --git a/actionpack/lib/action_controller/base/http_authentication.rb b/actionpack/lib/action_controller/base/http_authentication.rb index 5d915fda08..b6b5267c66 100644 --- a/actionpack/lib/action_controller/base/http_authentication.rb +++ b/actionpack/lib/action_controller/base/http_authentication.rb @@ -68,8 +68,11 @@ module ActionController # # Simple Digest example: # + # require 'digest/md5' # class PostsController < ApplicationController - # USERS = {"dhh" => "secret"} + # REALM = "SuperSecret" + # USERS = {"dhh" => "secret", #plain text password + # "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password # # before_filter :authenticate, :except => [:index] # @@ -83,14 +86,18 @@ module ActionController # # private # def authenticate - # authenticate_or_request_with_http_digest(realm) do |username| + # authenticate_or_request_with_http_digest(REALM) do |username| # USERS[username] # end # end # end # - # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password so the framework can appropriately - # hash it to check the user's credentials. Returning +nil+ will cause authentication to fail. + # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately + # hash to check the user's credentials. Returning +nil+ will cause authentication to fail. + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. # # On shared hosts, Apache sometimes doesn't pass authentication headers to # FCGI instances. If your environment matches this description and you cannot @@ -177,26 +184,37 @@ module ActionController end # Raises error unless the request credentials response value matches the expected value. + # First try the password as a ha1 digest password. If this fails, then try it as a plain + # text password. def validate_digest_response(request, realm, &password_procedure) credentials = decode_credentials_header(request) valid_nonce = validate_nonce(request, credentials[:nonce]) - if valid_nonce && realm == credentials[:realm] && opaque(request.session.session_id) == credentials[:opaque] + if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque] password = password_procedure.call(credentials[:username]) - expected = expected_response(request.env['REQUEST_METHOD'], request.url, credentials, password) - expected == credentials[:response] + + [true, false].any? do |password_is_ha1| + expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1) + expected == credentials[:response] + end end end # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ - def expected_response(http_method, uri, credentials, password) - ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead + # of a plain-text password. + def expected_response(http_method, uri, credentials, password, password_is_ha1=true) + ha1 = password_is_ha1 ? password : ha1(credentials, password) ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':')) ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':')) end - def encode_credentials(http_method, credentials, password) - credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password) + def ha1(credentials, password) + ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':')) + end + + def encode_credentials(http_method, credentials, password, password_is_ha1) + credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ') end @@ -213,8 +231,7 @@ module ActionController end def authentication_header(controller, realm) - session_id = controller.request.session.session_id - controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(session_id)}", opaque="#{opaque(session_id)}") + controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}") end def authentication_request(controller, realm, message = nil) @@ -252,23 +269,36 @@ module ActionController # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 # of this document. # - # The nonce is opaque to the client. - def nonce(session_id, time = Time.now) + # The nonce is opaque to the client. Composed of Time, and hash of Time with secret + # key from the Rails session secret generated upon creation of project. Ensures + # the time cannot be modifed by client. + def nonce(time = Time.now) t = time.to_i - hashed = [t, session_id] + hashed = [t, secret_key] digest = ::Digest::MD5.hexdigest(hashed.join(":")) Base64.encode64("#{t}:#{digest}").gsub("\n", '') end - def validate_nonce(request, value) + # Might want a shorter timeout depending on whether the request + # is a PUT or POST, and if client is browser or web service. + # Can be much shorter if the Stale directive is implemented. This would + # allow a user to use new nonce without prompting user again for their + # username and password. + def validate_nonce(request, value, seconds_to_timeout=5*60) t = Base64.decode64(value).split(":").first.to_i - nonce(request.session.session_id, t) == value && (t - Time.now.to_i).abs <= 10 * 60 + nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end - # Opaque based on digest of session_id - def opaque(session_id) - Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '') + # Opaque based on random generation - but changing each request? + def opaque() + ::Digest::MD5.hexdigest(secret_key) end + + # Set in /initializers/session_store.rb, and loaded even if sessions are not in use. + def secret_key + ActionController::Base.session_options[:secret] + end + end end end diff --git a/actionpack/lib/action_controller/base/redirect.rb b/actionpack/lib/action_controller/base/redirect.rb index 83af793978..2e92117e7c 100644 --- a/actionpack/lib/action_controller/base/redirect.rb +++ b/actionpack/lib/action_controller/base/redirect.rb @@ -49,7 +49,6 @@ module ActionController end response.redirected_to = options - logger.info("Redirected to #{options}") if logger && logger.info? case options # The scheme name consist of a letter followed by any combination of @@ -72,6 +71,7 @@ module ActionController def redirect_to_full_url(url, status) raise DoubleRenderError if performed? + logger.info("Redirected to #{url}") if logger && logger.info? response.redirect(url, interpret_status(status)) @performed_redirect = true end diff --git a/actionpack/lib/action_controller/base/render.rb b/actionpack/lib/action_controller/base/render.rb index abba059969..c4a3725079 100644 --- a/actionpack/lib/action_controller/base/render.rb +++ b/actionpack/lib/action_controller/base/render.rb @@ -197,7 +197,17 @@ module ActionController raise DoubleRenderError, "Can only render or redirect once per action" if performed? options = { :layout => true } if options.nil? - original, options = options, extra_options unless options.is_a?(Hash) + + # This handles render "string", render :symbol, and render object + # render string and symbol are handled by render_for_name + # render object becomes render :partial => object + unless options.is_a?(Hash) + if options.is_a?(String) || options.is_a?(Symbol) + original, options = options, extra_options + else + extra_options[:partial], options = options, extra_options + end + end layout_name = options.delete(:layout) @@ -300,6 +310,7 @@ module ActionController # of sending it as the response body to the browser. def render_to_string(options = nil, &block) #:doc: render(options, &block) + response.body ensure response.content_type = nil erase_render_results @@ -308,7 +319,7 @@ module ActionController # Clears the rendered results, allowing for another render to be performed. def erase_render_results #:nodoc: - response.body = nil + response.body = [] @performed_render = false end @@ -360,8 +371,9 @@ module ActionController def render_for_parts(parts, layout, options = {}) tmp = view_paths.find_by_parts(*parts) - layout = _pick_layout(*layout) unless tmp.exempt_from_layout? + layout = _pick_layout(*layout) unless tmp.exempt_from_layout? + render_for_text( @template._render_template_with_layout(tmp, layout, options, parts[3])) end diff --git a/actionpack/lib/action_controller/base/responder.rb b/actionpack/lib/action_controller/base/responder.rb index f83abb5a4b..989f82444b 100644 --- a/actionpack/lib/action_controller/base/responder.rb +++ b/actionpack/lib/action_controller/base/responder.rb @@ -20,6 +20,7 @@ module ActionController end end + # Returns a set of the methods defined as actions in your controller def action_methods self.class.action_methods end diff --git a/actionpack/lib/action_controller/base/streaming.rb b/actionpack/lib/action_controller/base/streaming.rb index e1786913a7..9f80f48c3d 100644 --- a/actionpack/lib/action_controller/base/streaming.rb +++ b/actionpack/lib/action_controller/base/streaming.rb @@ -1,5 +1,6 @@ module ActionController #:nodoc: - # Methods for sending files and streams to the browser instead of rendering. + # Methods for sending arbitrary data and for streaming files to the browser, + # instead of rendering. module Streaming DEFAULT_SEND_FILE_OPTIONS = { :type => 'application/octet-stream'.freeze, @@ -103,8 +104,11 @@ module ActionController #:nodoc: end end - # Send binary data to the user as a file download. May set content type, apparent file name, - # and specify whether to show data inline or download as an attachment. + # Sends the given binary data to the browser. This method is similar to + # <tt>render :text => data</tt>, but also allows you to specify whether + # the browser should display the response as a file attachment (i.e. in a + # download dialog) or as inline data. You may also set the content type, + # the apparent file name, and other things. # # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. @@ -127,6 +131,10 @@ module ActionController #:nodoc: # send_data image.data, :type => image.content_type, :disposition => 'inline' # # See +send_file+ for more information on HTTP Content-* headers and caching. + # + # <b>Tip:</b> if you want to stream large amounts of on-the-fly generated + # data to the browser, then use <tt>render :text => proc { ... }</tt> + # instead. See ActionController::Base#render for more information. def send_data(data, options = {}) #:doc: logger.info "Sending data #{options[:filename]}" if logger send_file_headers! options.merge(:length => data.size) @@ -152,7 +160,7 @@ module ActionController #:nodoc: end content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers - headers.update( + headers.merge!( 'Content-Length' => options[:length], 'Content-Type' => content_type, 'Content-Disposition' => disposition, diff --git a/actionpack/lib/action_controller/base/verification.rb b/actionpack/lib/action_controller/base/verification.rb index 7bf09ba6ea..c62b81b666 100644 --- a/actionpack/lib/action_controller/base/verification.rb +++ b/actionpack/lib/action_controller/base/verification.rb @@ -90,7 +90,7 @@ module ActionController #:nodoc: def verify_action(options) #:nodoc: if prereqs_invalid?(options) flash.update(options[:add_flash]) if options[:add_flash] - response.headers.update(options[:add_headers]) if options[:add_headers] + response.headers.merge!(options[:add_headers]) if options[:add_headers] apply_remaining_actions(options) unless performed? end end |