diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-08-14 16:31:14 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-08-14 16:31:14 +0100 |
commit | 2ebe8d275efa53af967b09ad66dab68acc1aed98 (patch) | |
tree | b0b3e42c277b213788b55558aef3579f4e242300 /actionpack/lib | |
parent | 73ef94e9675ef6db85f18f1e3c70bf6ddfc1260a (diff) | |
parent | 8cb14ee1203c9ed380c4b192e8730757a52d43cb (diff) | |
download | rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.tar.gz rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.tar.bz2 rails-2ebe8d275efa53af967b09ad66dab68acc1aed98.zip |
Merge commit 'mainstream/master'
Conflicts:
actionpack/lib/action_controller/request.rb
actionpack/lib/action_controller/resources.rb
Diffstat (limited to 'actionpack/lib')
21 files changed, 685 insertions, 463 deletions
diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb index 765225ae24..e2e8bbdc71 100644 --- a/actionpack/lib/action_controller/assertions/response_assertions.rb +++ b/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -87,11 +87,11 @@ module ActionController # def assert_template(expected = nil, message=nil) clean_backtrace do - rendered = @response.rendered_template + rendered = @response.rendered_template.to_s msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) assert_block(msg) do if expected.nil? - @response.rendered_template.nil? + @response.rendered_template.blank? else rendered.to_s.match(expected) end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5689a9825e..0fdbcbd26f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -428,11 +428,7 @@ module ActionController #:nodoc: # By default, all methods defined in ActionController::Base and included modules are hidden. # More methods can be hidden using <tt>hide_actions</tt>. def hidden_actions - unless read_inheritable_attribute(:hidden_actions) - write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map { |m| m.to_s }) - end - - read_inheritable_attribute(:hidden_actions) + read_inheritable_attribute(:hidden_actions) || write_inheritable_attribute(:hidden_actions, []) end # Hide each of the given methods from being callable as actions. @@ -1199,7 +1195,7 @@ module ActionController #:nodoc: end def perform_action - if self.class.action_methods.include?(action_name) + if action_methods.include?(action_name) send(action_name) default_render unless performed? elsif respond_to? :method_missing @@ -1208,7 +1204,7 @@ module ActionController #:nodoc: elsif template_exists? && template_public? default_render else - raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.to_a.sort.to_sentence}", caller + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller end end @@ -1234,7 +1230,15 @@ module ActionController #:nodoc: end def self.action_methods - @action_methods ||= Set.new(public_instance_methods.map { |m| m.to_s }) - hidden_actions + @action_methods ||= + # All public instance methods of this class, including ancestors + public_instance_methods(true).map { |m| m.to_s }.to_set - + # Except for public instance methods of Base and its ancestors + Base.public_instance_methods(true).map { |m| m.to_s } + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false).map { |m| m.to_s } - + # And always exclude explicitly hidden actions + hidden_actions end def add_variables_to_assigns diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb index 8bc5e4c3a7..0ca27b30db 100644 --- a/actionpack/lib/action_controller/cgi_process.rb +++ b/actionpack/lib/action_controller/cgi_process.rb @@ -43,7 +43,7 @@ module ActionController #:nodoc: :session_path => "/", # available to all paths in app :session_key => "_session_id", :cookie_only => true - } unless const_defined?(:DEFAULT_SESSION_OPTIONS) + } def initialize(cgi, session_options = {}) @cgi = cgi @@ -61,53 +61,14 @@ module ActionController #:nodoc: end end - # The request body is an IO input stream. If the RAW_POST_DATA environment - # variable is already set, wrap it in a StringIO. - def body - if raw_post = env['RAW_POST_DATA'] - raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) - StringIO.new(raw_post) - else - @cgi.stdinput - end - end - - def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) - end - - def request_parameters - @request_parameters ||= parse_formatted_request_parameters + def body_stream #:nodoc: + @cgi.stdinput end def cookies @cgi.cookies.freeze end - def host_with_port_without_standard_port_handling - if forwarded = env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - elsif http_host = env['HTTP_HOST'] - http_host - elsif server_name = env['SERVER_NAME'] - server_name - else - "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - def host - host_with_port_without_standard_port_handling.sub(/:\d+$/, '') - end - - def port - if host_with_port_without_standard_port_handling =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - def session unless defined?(@session) if @session_options == false diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb index 10dc0cc45b..1d7236f18a 100644 --- a/actionpack/lib/action_controller/filters.rb +++ b/actionpack/lib/action_controller/filters.rb @@ -109,16 +109,17 @@ module ActionController #:nodoc: update_options! options end + # override these to return true in appropriate subclass def before? - self.class == BeforeFilter + false end def after? - self.class == AfterFilter + false end def around? - self.class == AroundFilter + false end # Make sets of strings from :only/:except options @@ -170,6 +171,10 @@ module ActionController #:nodoc: :around end + def around? + true + end + def call(controller, &block) if should_run_callback?(controller) method = filter_responds_to_before_and_after? ? around_proc : self.method @@ -212,6 +217,10 @@ module ActionController #:nodoc: :before end + def before? + true + end + def call(controller, &block) super if controller.send!(:performed?) @@ -224,6 +233,10 @@ module ActionController #:nodoc: def type :after end + + def after? + true + end end # Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do diff --git a/actionpack/lib/action_controller/headers.rb b/actionpack/lib/action_controller/headers.rb index 7239438c49..139669c66f 100644 --- a/actionpack/lib/action_controller/headers.rb +++ b/actionpack/lib/action_controller/headers.rb @@ -1,31 +1,33 @@ +require 'active_support/memoizable' + module ActionController module Http class Headers < ::Hash - - def initialize(constructor = {}) - if constructor.is_a?(Hash) + extend ActiveSupport::Memoizable + + def initialize(*args) + if args.size == 1 && args[0].is_a?(Hash) super() - update(constructor) + update(args[0]) else - super(constructor) + super end end - + def [](header_name) if include?(header_name) - super + super else - super(normalize_header(header_name)) + super(env_name(header_name)) end end - - + private - # Takes an HTTP header name and returns it in the - # format - def normalize_header(header_name) + # Converts a HTTP header name to an environment variable name. + def env_name(header_name) "HTTP_#{header_name.upcase.gsub(/-/, '_')}" end + memoize :env_name end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb index 7e0a6b091e..dcbcf8bc1d 100644 --- a/actionpack/lib/action_controller/rack_process.rb +++ b/actionpack/lib/action_controller/rack_process.rb @@ -3,7 +3,7 @@ require 'action_controller/session/cookie_store' module ActionController #:nodoc: class RackRequest < AbstractRequest #:nodoc: - attr_accessor :env, :session_options + attr_accessor :session_options attr_reader :cgi class SessionFixationAttempt < StandardError #:nodoc: @@ -15,7 +15,7 @@ module ActionController #:nodoc: :session_path => "/", # available to all paths in app :session_key => "_session_id", :cookie_only => true - } unless const_defined?(:DEFAULT_SESSION_OPTIONS) + } def initialize(env, session_options = DEFAULT_SESSION_OPTIONS) @session_options = session_options @@ -30,35 +30,21 @@ module ActionController #:nodoc: SERVER_NAME SERVER_PROTOCOL HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING - HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_HOST + HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| define_method(env.sub(/^HTTP_/n, '').downcase) do @env[env] end end - # The request body is an IO input stream. If the RAW_POST_DATA environment - # variable is already set, wrap it in a StringIO. - def body - if raw_post = env['RAW_POST_DATA'] - StringIO.new(raw_post) - else - @env['rack.input'] - end + def body_stream #:nodoc: + @env['rack.input'] end def key?(key) @env.key?(key) end - def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) - end - - def request_parameters - @request_parameters ||= parse_formatted_request_parameters - end - def cookies return {} unless @env["HTTP_COOKIE"] @@ -70,34 +56,6 @@ module ActionController #:nodoc: @env["rack.request.cookie_hash"] end - def host_with_port_without_standard_port_handling - if forwarded = @env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - elsif http_host = @env['HTTP_HOST'] - http_host - elsif server_name = @env['SERVER_NAME'] - server_name - else - "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - def host - host_with_port_without_standard_port_handling.sub(/:\d+$/, '') - end - - def port - if host_with_port_without_standard_port_handling =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - - def remote_addr - @env['REMOTE_ADDR'] - end - def server_port @env['SERVER_PORT'].to_i end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 60ff75fe2c..185518761d 100644 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -2,35 +2,35 @@ require 'tempfile' require 'stringio' require 'strscan' -module ActionController - # HTTP methods which are accepted by default. - ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options )) +require 'active_support/memoizable' +module ActionController # CgiRequest and TestRequest provide concrete implementations. class AbstractRequest + extend ActiveSupport::Memoizable + def self.relative_url_root=(*args) ActiveSupport::Deprecation.warn( "ActionController::AbstractRequest.relative_url_root= has been renamed." + "You can now set it with config.action_controller.relative_url_root=", caller) end - # The hash of CGI-like environment variables for this request, such as - # - # { 'SERVER_PROTOCOL' => 'HTTP/1.1', 'HTTP_ACCEPT_LANGUAGE' => 'en-us', ... } + HTTP_METHODS = %w(get head put post delete options) + HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + + # The hash of environment variables for this request, + # such as { 'RAILS_ENV' => 'production' }. attr_reader :env # The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>. # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS. def request_method - @request_method ||= begin - method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase - if ACCEPTED_HTTP_METHODS.include?(method) - method.to_sym - else - raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}" - end - end + method = @env['REQUEST_METHOD'] + method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank? + + HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") end + memoize :request_method # The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>. # Note, HEAD is returned as <tt>:get</tt> since the two are functionally @@ -69,34 +69,60 @@ module ActionController # # request.headers["Content-Type"] # => "text/plain" def headers - @headers ||= ActionController::Http::Headers.new(@env) + ActionController::Http::Headers.new(@env) end + memoize :headers # Returns the content length of the request as an integer. def content_length - @content_length ||= env['CONTENT_LENGTH'].to_i + @env['CONTENT_LENGTH'].to_i end + memoize :content_length # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - @content_type ||= Mime::Type.lookup(content_type_without_parameters) + Mime::Type.lookup(content_type_without_parameters) end + memoize :content_type # Returns the accepted MIME type for the request. def accepts - @accepts ||= - begin - header = @env['HTTP_ACCEPT'].to_s.strip + header = @env['HTTP_ACCEPT'].to_s.strip - if header.empty? - [content_type, Mime::ALL].compact - else - Mime::Type.parse(header) - end - end + if header.empty? + [content_type, Mime::ALL].compact + else + Mime::Type.parse(header) + end + end + memoize :accepts + + def if_modified_since + if since = env['HTTP_IF_MODIFIED_SINCE'] + Time.rfc2822(since) + end + end + memoize :if_modified_since + + def if_none_match + env['HTTP_IF_NONE_MATCH'] + end + + def not_modified?(modified_at) + if_modified_since && modified_at && if_modified_since >= modified_at + end + + def etag_matches?(etag) + if_none_match && if_none_match == etag + end + + # Check response freshness (Last-Modified and ETag) against request + # If-Modified-Since and If-None-Match conditions. + def fresh?(response) + not_modified?(response.last_modified) || etag_matches?(response.etag) end # Returns the Mime type for the \format used in the request. @@ -105,7 +131,7 @@ module ActionController # GET /posts/5.xhtml | request.format => Mime::HTML # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> def format - @format ||= begin + @format ||= if parameters[:format] Mime::Type.lookup_by_extension(parameters[:format]) elsif ActionController::Base.use_accept_header @@ -115,7 +141,6 @@ module ActionController else Mime::Type.lookup_by_extension("html") end - end end @@ -203,22 +228,26 @@ EOM @env['REMOTE_ADDR'] end + memoize :remote_ip # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end + memoize :server_software # Returns the complete URL used for this request. def url protocol + host_with_port + request_uri end + memoize :url # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol ssl? ? 'https://' : 'http://' end + memoize :protocol # Is this an SSL request? def ssl? @@ -226,19 +255,36 @@ EOM end # Returns the \host for this request, such as "example.com". + def raw_host_with_port + if forwarded = env["HTTP_X_FORWARDED_HOST"] + forwarded.split(/,\s?/).last + else + env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + end + end + + # Returns the host for this request, such as example.com. def host + raw_host_with_port.sub(/:\d+$/, '') end + memoize :host # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". def host_with_port - @host_with_port ||= host + port_string + "#{host}#{port_string}" end + memoize :host_with_port # Returns the port number of this request as an integer. def port - @port_as_int ||= @env['SERVER_PORT'].to_i + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end end + memoize :port # Returns the standard \port number for this request's protocol. def standard_port @@ -251,7 +297,7 @@ EOM # Returns a \port suffix like ":8080" if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. def port_string - (port == standard_port) ? '' : ":#{port}" + port == standard_port ? '' : ":#{port}" end # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify @@ -280,6 +326,7 @@ EOM @env['QUERY_STRING'] || '' end end + memoize :query_string # Returns the request URI, accounting for server idiosyncrasies. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. @@ -289,21 +336,23 @@ EOM (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri else # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. - script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) - uri = @env['PATH_INFO'] - uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil? - unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty? - uri << '?' << env_qs + uri = @env['PATH_INFO'].to_s + + if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) + uri = uri.sub(/#{script_filename}\//, '') end - if uri.nil? + env_qs = @env['QUERY_STRING'].to_s + uri += "?#{env_qs}" unless env_qs.empty? + + if uri.blank? @env.delete('REQUEST_URI') - uri else @env['REQUEST_URI'] = uri end end end + memoize :request_uri # Returns the interpreted \path to requested resource after all the installation # directory of this application was taken into account. @@ -314,6 +363,7 @@ EOM path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '') path || '' end + memoize :path # Read the request \body. This is useful for web services that need to # work with raw requests directly. @@ -350,19 +400,41 @@ EOM @path_parameters ||= {} end + # The request body is an IO input stream. If the RAW_POST_DATA environment + # variable is already set, wrap it in a StringIO. + def body + if raw_post = env['RAW_POST_DATA'] + raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) + StringIO.new(raw_post) + else + body_stream + end + end - #-- - # Must be implemented in the concrete request - #++ + def remote_addr + @env['REMOTE_ADDR'] + end - # The request \body as an IO input stream. - def body + def referrer + @env['HTTP_REFERER'] end + alias referer referrer + - def query_parameters #:nodoc: + def query_parameters + @query_parameters ||= self.class.parse_query_parameters(query_string) end - def request_parameters #:nodoc: + def request_parameters + @request_parameters ||= parse_formatted_request_parameters + end + + + #-- + # Must be implemented in the concrete request + #++ + + def body_stream #:nodoc: end def cookies #:nodoc: @@ -389,8 +461,9 @@ EOM # The raw content type string with its parameters stripped off. def content_type_without_parameters - @content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters) + self.class.extract_content_type_without_parameters(content_type_with_parameters) end + memoize :content_type_without_parameters private def content_type_from_legacy_post_data_format_header diff --git a/actionpack/lib/action_controller/resources.rb b/actionpack/lib/action_controller/resources.rb index 77b329b70e..becf6b0b63 100644 --- a/actionpack/lib/action_controller/resources.rb +++ b/actionpack/lib/action_controller/resources.rb @@ -481,8 +481,7 @@ module ActionController resource.collection_methods.each do |method, actions| actions.each do |action| action_options = action_options_for(action, resource, method) - map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) - map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options) + map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) end end end @@ -495,18 +494,15 @@ module ActionController index_route_name << "_index" end - map.named_route(index_route_name, resource.path, index_action_options) - map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options) + map_named_routes(map, index_route_name, resource.path, index_action_options) create_action_options = action_options_for("create", resource) - map.connect(resource.path, create_action_options) - map.connect("#{resource.path}.:format", create_action_options) + map_unnamed_routes(map, resource.path, create_action_options) end def map_default_singleton_actions(map, resource) create_action_options = action_options_for("create", resource) - map.connect(resource.path, create_action_options) - map.connect("#{resource.path}.:format", create_action_options) + map_unnamed_routes(map, resource.path, create_action_options) end def map_new_actions(map, resource) @@ -514,11 +510,9 @@ module ActionController actions.each do |action| action_options = action_options_for(action, resource, method) if action == :new - map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) - map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options) + map_named_routes(map, "new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) else - map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) - map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options) + map_named_routes(map, "#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) end end end @@ -532,22 +526,28 @@ module ActionController action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) action_path ||= Base.resources_path_names[action] || action - map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options) - map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}.:format",action_options) + map_named_routes(map, "#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action_path}", action_options) end end show_action_options = action_options_for("show", resource) - map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) - map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options) + map_named_routes(map, "#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) update_action_options = action_options_for("update", resource) - map.connect(resource.member_path, update_action_options) - map.connect("#{resource.member_path}.:format", update_action_options) + map_unnamed_routes(map, resource.member_path, update_action_options) destroy_action_options = action_options_for("destroy", resource) - map.connect(resource.member_path, destroy_action_options) - map.connect("#{resource.member_path}.:format", destroy_action_options) + map_unnamed_routes(map, resource.member_path, destroy_action_options) + end + + def map_unnamed_routes(map, path_without_format, options) + map.connect(path_without_format, options) + map.connect("#{path_without_format}.:format", options) + end + + def map_named_routes(map, name, path_without_format, options) + map.named_route(name, path_without_format, options) + map.named_route("formatted_#{name}", "#{path_without_format}.:format", options) end def add_conditions_for(conditions, method) @@ -574,4 +574,4 @@ end class ActionController::Routing::RouteSet::Mapper include ActionController::Resources -end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index da352b6993..a85fad0d39 100644 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -37,12 +37,20 @@ module ActionController # :nodoc: attr_accessor :body # The headers of the response, as a Hash. It maps header names to header values. attr_accessor :headers - attr_accessor :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout + attr_accessor :session, :cookies, :assigns, :template, :layout + attr_accessor :redirected_to, :redirected_to_method_params def initialize @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] end + def status; headers['Status'] end + def status=(status) headers['Status'] = status end + + def location; headers['Location'] end + def location=(url) headers['Location'] = url end + + # Sets the HTTP response's content MIME type. For example, in the controller # you could write this: # @@ -70,35 +78,29 @@ module ActionController # :nodoc: charset.blank? ? nil : charset.strip.split("=")[1] end - def redirect(to_url, response_status) - self.headers["Status"] = response_status - self.headers["Location"] = to_url + def last_modified + Time.rfc2822(headers['Last-Modified']) + end - self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>" + def last_modified=(utc_time) + headers['Last-Modified'] = utc_time.httpdate end - def prepare! - handle_conditional_get! - convert_content_type! - set_content_length! + def etag; headers['ETag'] end + def etag=(etag) + headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}") end - # Sets the Last-Modified response header. Returns whether it's older than - # the If-Modified-Since request header. - def last_modified!(utc_time) - headers['Last-Modified'] ||= utc_time.httpdate - if request && since = request.headers['HTTP_IF_MODIFIED_SINCE'] - utc_time <= Time.rfc2822(since) - end + def redirect(url, status) + self.status = status + self.location = url + self.body = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>" end - # Sets the ETag response header. Returns whether it matches the - # If-None-Match request header. - def etag!(tag) - headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}") - if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] - true - end + def prepare! + handle_conditional_get! + convert_content_type! + set_content_length! end private @@ -106,15 +108,15 @@ module ActionController # :nodoc: if nonempty_ok_response? set_conditional_cache_control! - if etag!(body) - headers['Status'] = '304 Not Modified' + self.etag ||= body + if request && request.etag_matches?(etag) + self.status = '304 Not Modified' self.body = '' end end end def nonempty_ok_response? - status = headers['Status'] ok = !status || status[0..2] == '200' ok && body.is_a?(String) && !body.empty? end diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 75784c3b78..9d4b740a44 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -2,7 +2,8 @@ module ActionController module Routing class Segment #:nodoc: RESERVED_PCHAR = ':@&=+$,;' - UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" + UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze # TODO: Convert :is_optional accessor to read only attr_accessor :is_optional diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb index b477c1f7da..5bf7503f04 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -129,7 +129,7 @@ class CGI::Session::CookieStore private # Marshal a session hash into safe cookie data. Include an integrity hash. def marshal(session) - data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop + data = ActiveSupport::Base64.encode64s(Marshal.dump(session)) "#{data}--#{generate_digest(data)}" end diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 66675aaa13..0c705207e3 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -23,7 +23,7 @@ module ActionController #:nodoc: class TestRequest < AbstractRequest #:nodoc: attr_accessor :cookies, :session_options - attr_accessor :query_parameters, :request_parameters, :path, :session, :env + attr_accessor :query_parameters, :request_parameters, :path, :session attr_accessor :host, :user_agent def initialize(query_parameters = nil, request_parameters = nil, session = nil) @@ -42,7 +42,7 @@ module ActionController #:nodoc: end # Wraps raw_post in a StringIO. - def body + def body_stream #:nodoc: StringIO.new(raw_post) end @@ -54,7 +54,7 @@ module ActionController #:nodoc: def port=(number) @env["SERVER_PORT"] = number.to_i - @port_as_int = nil + port(true) end def action=(action_name) @@ -68,6 +68,8 @@ module ActionController #:nodoc: @env["REQUEST_URI"] = value @request_uri = nil @path = nil + request_uri(true) + path(true) end def request_uri=(uri) @@ -77,21 +79,26 @@ module ActionController #:nodoc: def accept=(mime_types) @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + accepts(true) end - def remote_addr=(addr) - @env['REMOTE_ADDR'] = addr + def if_modified_since=(last_modified) + @env["HTTP_IF_MODIFIED_SINCE"] = last_modified end - def remote_addr - @env['REMOTE_ADDR'] + def if_none_match=(etag) + @env["HTTP_IF_NONE_MATCH"] = etag end - def request_uri + def remote_addr=(addr) + @env['REMOTE_ADDR'] = addr + end + + def request_uri(*args) @request_uri || super end - def path + def path(*args) @path || super end @@ -113,17 +120,13 @@ module ActionController #:nodoc: end end @parameters = nil # reset TestRequest#parameters to use the new path_parameters - end - + end + def recycle! self.request_parameters = {} self.query_parameters = {} self.path_parameters = {} - @request_method, @accepts, @content_type = nil, nil, nil - end - - def referer - @env["HTTP_REFERER"] + unmemoize_all end private @@ -448,10 +451,13 @@ module ActionController #:nodoc: end def method_missing(selector, *args) - return @controller.send!(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector) - return super + if ActionController::Routing::Routes.named_routes.helpers.include?(selector) + @controller.send(selector, *args) + else + super + end end - + # Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>: # # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index bdcb1dc246..ad59d92086 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -300,6 +300,8 @@ module ActionView #:nodoc: # # => 'users/legacy.rhtml' # def pick_template(template_path) + return template_path if template_path.respond_to?(:render) + path = template_path.sub(/^\//, '') if m = path.match(/(.*)\.(\w+)$/) template_file_name, template_file_extension = m[1], m[2] @@ -343,7 +345,8 @@ module ActionView #:nodoc: ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller) end - if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/") + if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && + template_path.is_a?(String) && !template_path.include?("/") raise ActionViewError, <<-END_ERROR Due to changes in ActionMailer, you need to provide the mailer_name along with the template name. diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 769eada120..c2b4f51c9c 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -463,7 +463,7 @@ module ActionView end private - COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence!.threadsafe! + COMPUTED_PUBLIC_PATHS = ActiveSupport::Cache::MemoryStore.new.silence! # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL @@ -618,6 +618,11 @@ module ActionView def write_asset_file_contents(joined_asset_path, asset_paths) FileUtils.mkdir_p(File.dirname(joined_asset_path)) File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) } + + # Set mtime to the latest of the combined files to allow for + # consistent ETag without a shared filesystem. + mt = asset_paths.map { |p| File.mtime(File.join(ASSETS_DIR, p)) }.max + File.utime(mt, mt, joined_asset_path) end def collect_asset_files(*path) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index c7a1d40ff2..953a2a9f86 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -13,9 +13,6 @@ module ActionView # the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of # "date[month]". module DateHelper - include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') - # Reports the approximate distance in time between two Time or Date objects or integers as seconds. # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs # Distances are reported based on the following table: @@ -52,7 +49,7 @@ module ActionView # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year - # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years + # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years # # to_time = Time.now + 6.years + 19.days # distance_of_time_in_words(from_time, to_time, true) # => over 6 years @@ -109,19 +106,36 @@ module ActionView alias_method :distance_of_time_in_words_to_now, :time_ago_in_words # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based - # attribute (identified by +method+) on an object assigned to the template (identified by +object+). It's - # possible to tailor the selects through the +options+ hash, which accepts all the keys that each of the - # individual select builders do (like <tt>:use_month_numbers</tt> for select_month) as well as a range of discard - # options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set - # to true, they'll drop the respective select. Discarding the month select will also automatically discard the - # day select. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an - # array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. Symbols may be omitted - # and the respective select is not included. - # - # Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of <tt>:year</tt>, - # <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, and <tt>:second</tt>. - # - # Passing <tt>:disabled => true</tt> as part of the +options+ will make elements inaccessible for change. + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). You can + # the output in the +options+ hash. + # + # ==== Options + # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g. + # "2" instead of "February"). + # * <tt>:use_short_month</tt> - Set to true if you want to use the abbreviated month name instead of the full + # name (e.g. "Feb" instead of "February"). + # * <tt>:add_month_number</tt> - Set to true if you want to show both, the month's number and name (e.g. + # "2 - February" instead of "February"). + # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names. + # Note: You can also use Rails' new i18n functionality for this. + # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing). + # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>. + # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>. + # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day + # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the + # first of the given month in order to not create invalid dates like 31 February. + # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month + # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true. + # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year + # as a hidden field instead of showing a select field. + # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> do + # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective + # select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in + # the respective locale (e.g. [:year, :month, :day] in the en-US locale that ships with Rails). + # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty + # dates. + # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. # @@ -165,9 +179,9 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options) end - # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified - # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+). - # You can include the seconds with <tt>:include_seconds</tt>. + # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a + # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by + # +object+). You can include the seconds with <tt>:include_seconds</tt>. # # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option # <tt>:ignore_date</tt> is set to +true+. @@ -178,7 +192,8 @@ module ActionView # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute # time_select("post", "sunrise") # - # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute + # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted + # # attribute # time_select("order", "submitted") # # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute @@ -210,7 +225,8 @@ module ActionView # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute + # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on + # # attribute # datetime_select("post", "written_on") # # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the @@ -230,12 +246,12 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options) end - # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. - # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of - # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, - # it will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt>, - # <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to control visual display of - # the elements. + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the + # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with + # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not + # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add + # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to + # control visual display of the elements. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -270,14 +286,13 @@ module ActionView # select_datetime(my_date_time, :prefix => 'payday') # def select_datetime(datetime = Time.current, options = {}, html_options = {}) - separator = options[:datetime_separator] || '' - select_date(datetime, options, html_options) + separator + select_time(datetime, options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_datetime end # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of - # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it - # will be appended onto the <tt>:order</tt> passed in. + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, + # it will be appended onto the <tt>:order</tt> passed in. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # @@ -307,12 +322,7 @@ module ActionView # select_date(my_date, :prefix => 'payday') # def select_date(date = Date.current, options = {}, html_options = {}) - options.reverse_merge!(:order => [], :date_separator => '') - [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) } - - options[:order].inject([]) { |s, o| - s << self.send("select_#{o}", date, options, html_options) - }.join(options[:date_separator]) + DateTimeSelector.new(date, options, html_options).select_date end # Returns a set of html select-tags (one for hour and minute) @@ -343,9 +353,7 @@ module ActionView # select_time(my_time, :time_separator => ':', :include_seconds => true) # def select_time(datetime = Time.current, options = {}, html_options = {}) - separator = options[:time_separator] || '' - select_hour(datetime, options, html_options) + separator + select_minute(datetime, options, html_options) + - (options[:include_seconds] ? separator + select_second(datetime, options, html_options) : '') + DateTimeSelector.new(datetime, options, html_options).select_time end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. @@ -366,15 +374,12 @@ module ActionView # select_second(my_time, :field_name => 'interval') # def select_second(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' - options[:use_hidden] ? - (options[:include_seconds] ? _date_hidden_html(options[:field_name] || 'second', val, options) : '') : - _date_select_html(options[:field_name] || 'second', _date_build_options(val), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_second end # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. - # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected - # The <tt>minute</tt> can also be substituted for a minute number. + # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute + # selected. The <tt>minute</tt> can also be substituted for a minute number. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # # ==== Examples @@ -391,11 +396,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_minute(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' - options[:use_hidden] ? - _date_hidden_html(options[:field_name] || 'minute', val, options) : - _date_select_html(options[:field_name] || 'minute', - _date_build_options(val, :step => options[:minute_step]), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_minute end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. @@ -416,9 +417,7 @@ module ActionView # select_minute(my_time, :field_name => 'stride') # def select_hour(datetime, options = {}, html_options = {}) - val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'hour', val, options) : - _date_select_html(options[:field_name] || 'hour', _date_build_options(val, :end => 23), options, html_options) + DateTimeSelector.new(datetime, options, html_options).select_hour end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. @@ -439,11 +438,7 @@ module ActionView # select_day(my_time, :field_name => 'due') # def select_day(date, options = {}, html_options = {}) - val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' - options[:use_hidden] ? _date_hidden_html(options[:field_name] || 'day', val, options) : - _date_select_html(options[:field_name] || 'day', - _date_build_options(val, :start => 1, :end => 31, :leading_zeros => false), - options, html_options) + DateTimeSelector.new(date, options, html_options).select_day end # Returns a select tag with options for each of the months January through December with the current month @@ -481,36 +476,7 @@ module ActionView # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # def select_month(date, options = {}, html_options = {}) - locale = options[:locale] - - val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'month', val, options) - else - month_options = [] - month_names = options[:use_month_names] || begin - key = options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate key, :locale => locale - end - month_names.unshift(nil) if month_names.size < 13 - - 1.upto(12) do |month_number| - month_name = if options[:use_month_numbers] - month_number - elsif options[:add_month_numbers] - month_number.to_s + ' - ' + month_names[month_number] - else - month_names[month_number] - end - - month_options << ((val == month_number) ? - content_tag(:option, month_name, :value => month_number, :selected => "selected") : - content_tag(:option, month_name, :value => month_number) - ) - month_options << "\n" - end - _date_select_html(options[:field_name] || 'month', month_options.join, options, html_options) - end + DateTimeSelector.new(date, options, html_options).select_month end # Returns a select tag with options for each of the five years on each side of the current, which is selected. @@ -537,158 +503,369 @@ module ActionView # select_year(2006, :start_year => 2000, :end_year => 2010) # def select_year(date, options = {}, html_options = {}) - if !date || date == 0 + DateTimeSelector.new(date, options, html_options).select_year + end + end + + class DateTimeSelector #:nodoc: + extend ActiveSupport::Memoizable + include ActionView::Helpers::TagHelper + + DEFAULT_PREFIX = 'date'.freeze unless const_defined?('DEFAULT_PREFIX') + POSITION = { + :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 + }.freeze unless const_defined?('POSITION') + + def initialize(datetime, options = {}, html_options = {}) + @options = options.dup + @html_options = html_options.dup + @datetime = datetime + end + + def select_datetime + # TODO: Remove tag conditional + # Ideally we could just join select_date and select_date for the tag case + if @options[:tag] && @options[:ignore_date] + select_time + elsif @options[:tag] + order = date_order.dup + order -= [:hour, :minute, :second] + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + @options[:discard_minute] ||= true if @options[:discard_hour] + @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute] + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + order += [:hour, :minute, :second] unless @options[:discard_hour] + + build_selects_from_types(order) + else + "#{select_date}#{@options[:datetime_separator]}#{select_time}" + end + end + + def select_date + order = date_order.dup + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_hour] = true + @options[:discard_minute] = true + @options[:discard_second] = true + + @options[:discard_year] ||= true unless order.include?(:year) + @options[:discard_month] ||= true unless order.include?(:month) + @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day) + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are + # valid (otherwise it could be 31 and february wouldn't be a valid date) + if @options[:discard_day] && !@options[:discard_month] + @datetime = @datetime.change(:day => 1) + end + end + + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + + build_selects_from_types(order) + end + + def select_time + order = [] + + # TODO: Remove tag conditional + if @options[:tag] + @options[:discard_month] = true + @options[:discard_year] = true + @options[:discard_day] = true + @options[:discard_second] ||= true unless @options[:include_seconds] + + order += [:year, :month, :day] unless @options[:ignore_date] + end + + order += [:hour, :minute] + order << :second if @options[:include_seconds] + + build_selects_from_types(order) + end + + def select_second + if @options[:use_hidden] || @options[:discard_second] + build_hidden(:second, sec) if @options[:include_seconds] + else + build_options_and_select(:second, sec) + end + end + + def select_minute + if @options[:use_hidden] || @options[:discard_minute] + build_hidden(:minute, min) + else + build_options_and_select(:minute, min, :step => @options[:minute_step]) + end + end + + def select_hour + if @options[:use_hidden] || @options[:discard_hour] + build_hidden(:hour, hour) + else + build_options_and_select(:hour, hour, :end => 23) + end + end + + def select_day + if @options[:use_hidden] || @options[:discard_day] + build_hidden(:day, day) + else + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false) + end + end + + def select_month + if @options[:use_hidden] || @options[:discard_month] + build_hidden(:month, month) + else + month_options = [] + 1.upto(12) do |month_number| + options = { :value => month_number } + options[:selected] = "selected" if month == month_number + month_options << content_tag(:option, month_name(month_number), options) + "\n" + end + build_select(:month, month_options.join) + end + end + + def select_year + if !@datetime || @datetime == 0 val = '' middle_year = Date.today.year - elsif date.kind_of?(Fixnum) - val = middle_year = date else - val = middle_year = date.year + val = middle_year = year end - if options[:use_hidden] - _date_hidden_html(options[:field_name] || 'year', val, options) + if @options[:use_hidden] || @options[:discard_year] + build_hidden(:year, val) else - options[:start_year] ||= middle_year - 5 - options[:end_year] ||= middle_year + 5 - step = options[:start_year] < options[:end_year] ? 1 : -1 - - _date_select_html(options[:field_name] || 'year', - _date_build_options(val, - :start => options[:start_year], - :end => options[:end_year], - :step => step, - :leading_zeros => false - ), options, html_options) + options = {} + options[:start] = @options[:start_year] || middle_year - 5 + options[:end] = @options[:end_year] || middle_year + 5 + options[:step] = options[:start] < options[:end] ? 1 : -1 + options[:leading_zeros] = false + + build_options_and_select(:year, val, options) end end private - def _date_build_options(selected, options={}) - options.reverse_merge!(:start => 0, :end => 59, :step => 1, :leading_zeros => true) + %w( sec min hour day month year ).each do |method| + define_method(method) do + @datetime.kind_of?(Fixnum) ? @datetime : @datetime.send(method) if @datetime + end + end + + # Returns translated month names, but also ensures that a custom month + # name array has a leading nil element + def month_names + month_names = @options[:use_month_names] || translated_month_names + month_names.unshift(nil) if month_names.size < 13 + month_names + end + memoize :month_names + + # Returns translated month names + # => [nil, "January", "February", "March", + # "April", "May", "June", "July", + # "August", "September", "October", + # "November", "December"] + # + # If :use_short_month option is set + # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + def translated_month_names + begin + key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' + I18n.translate(key, :locale => @options[:locale]) + end + end + + # Lookup month name for number + # month_name(1) => "January" + # + # If :use_month_numbers option is passed + # month_name(1) => 1 + # + # If :add_month_numbers option is passed + # month_name(1) => "1 - January" + def month_name(number) + if @options[:use_month_numbers] + number + elsif @options[:add_month_numbers] + "#{number} - #{month_names[number]}" + else + month_names[number] + end + end + + def date_order + @options[:order] || translated_date_order + end + memoize :date_order + + def translated_date_order + begin + I18n.translate(:'date.order', :locale => @options[:locale]) || [] + end + end + + # Build full select tag from date type and options + def build_options_and_select(type, selected, options = {}) + build_select(type, build_options(selected, options)) + end + + # Build select option html from date value and options + # build_options(15, :start => 1, :end => 31) + # => "<option value="1">1</option> + # <option value=\"2\">2</option> + # <option value=\"3\">3</option>..." + def build_options(selected, options = {}) + start = options.delete(:start) || 0 + stop = options.delete(:end) || 59 + step = options.delete(:step) || 1 + leading_zeros = options.delete(:leading_zeros).nil? ? true : false select_options = [] - (options[:start] || 0).step((options[:end] || 59), options[:step] || 1) do |i| - value = options[:leading_zeros] ? sprintf("%02d", i) : i + start.step(stop, step) do |i| + value = leading_zeros ? sprintf("%02d", i) : i tag_options = { :value => value } tag_options[:selected] = "selected" if selected == i - select_options << content_tag(:option, value, tag_options) end select_options.join("\n") + "\n" end - def _date_select_html(type, html_options, options, select_tag_options = {}) - _date_name_and_id_from_options(options, type) - select_options = {:id => options[:id], :name => options[:name]} - select_options.merge!(:disabled => 'disabled') if options[:disabled] - select_options.merge!(select_tag_options) unless select_tag_options.empty? + # Builds select tag from date type and html select options + # build_select(:month, "<option value="1">January</option>...") + # => "<select id="post_written_on_2i" name="post[written_on(2i)]"> + # <option value="1">January</option>... + # </select>" + def build_select(type, select_options_as_html) + select_options = { + :id => input_id_from_type(type), + :name => input_name_from_type(type) + }.merge(@html_options) + select_options.merge!(:disabled => 'disabled') if @options[:disabled] + select_html = "\n" - select_html << content_tag(:option, '', :value => '') + "\n" if options[:include_blank] - select_html << html_options.to_s + select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] + select_html << select_options_as_html.to_s + content_tag(:select, select_html, select_options) + "\n" end - def _date_hidden_html(type, value, options) - _date_name_and_id_from_options(options, type) - hidden_html = tag(:input, :type => "hidden", :id => options[:id], :name => options[:name], :value => value) + "\n" + # Builds hidden input tag for date part and value + # build_hidden(:year, 2008) + # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />" + def build_hidden(type, value) + tag(:input, { + :type => "hidden", + :id => input_id_from_type(type), + :name => input_name_from_type(type), + :value => value + }) + "\n" end - def _date_name_and_id_from_options(options, type) - options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") - options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + # Returns the name attribute for the input tag + # => post[written_on(1i)] + def input_name_from_type(type) + prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX + prefix += "[#{@options[:index]}]" if @options[:index] + + field_name = @options[:field_name] || type + if @options[:include_position] + field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" + end + + @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]" + end + + # Returns the id attribute for the input tag + # => "post_written_on_1i" + def input_id_from_type(type) + input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + end + + # Given an ordering of datetime components, create the selection html + # and join them with their appropriate seperators + def build_selects_from_types(order) + select = '' + order.reverse.each do |type| + separator = separator(type) unless type == order.first # don't add on last field + select.insert(0, separator.to_s + send("select_#{type}").to_s) + end + select + end + + # Returns the separator for a given datetime component + def separator(type) + case type + when :month, :day + @options[:date_separator] + when :hour + (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + when :minute + @options[:time_separator] + when :second + @options[:include_seconds] ? @options[:time_separator] : "" + end end end class InstanceTag #:nodoc: - include DateHelper - def to_date_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_hour => true), html_options) + datetime_selector(options, html_options).select_date end def to_time_select_tag(options = {}, html_options = {}) - date_or_time_select(options.merge(:discard_year => true, :discard_month => true), html_options) + datetime_selector(options, html_options).select_time end def to_datetime_select_tag(options = {}, html_options = {}) - date_or_time_select(options, html_options) + datetime_selector(options, html_options).select_datetime end private - def date_or_time_select(options, html_options = {}) - locale = options[:locale] - - defaults = { :discard_type => true } - options = defaults.merge(options) - datetime = value(object) - datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] - - position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } - - order = options[:order] ||= I18n.translate(:'date.order', :locale => locale) - - # Discard explicit and implicit by not being included in the :order - discard = {} - discard[:year] = true if options[:discard_year] or !order.include?(:year) - discard[:month] = true if options[:discard_month] or !order.include?(:month) - discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day) - discard[:hour] = true if options[:discard_hour] - discard[:minute] = true if options[:discard_minute] or discard[:hour] - discard[:second] = true unless options[:include_seconds] && !discard[:minute] - - # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid - # (otherwise it could be 31 and february wouldn't be a valid date) - if datetime && discard[:day] && !discard[:month] - datetime = datetime.change(:day => 1) - end - - # Maintain valid dates by including hidden fields for discarded elements - [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } - - # Ensure proper ordering of :hour, :minute and :second - [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } - - date_or_time_select = '' - order.reverse.each do |param| - # Send hidden fields for discarded elements once output has started - # This ensures AR can reconstruct valid dates using ParseDate - next if discard[param] && (date_or_time_select.empty? || options[:ignore_date]) - - date_or_time_select.insert(0, - self.send("select_#{param}", - datetime, - options_with_prefix(position[param], options.merge(:use_hidden => discard[param])), - html_options)) - date_or_time_select.insert(0, - case param - when :hour then (discard[:year] && discard[:day] ? "" : " — ") - when :minute then " : " - when :second then options[:include_seconds] ? " : " : "" - else "" - end) - end - - date_or_time_select + def datetime_selector(options, html_options) + datetime = value(object) || default_datetime(options) + + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] ||= @auto_index + options[:datetime_separator] ||= ' — ' + options[:time_separator] ||= ' : ' + + DateTimeSelector.new(datetime, options.merge(:tag => true), html_options) end - def options_with_prefix(position, options) - prefix = "#{@object_name}" - if options[:index] - prefix << "[#{options[:index]}]" - elsif @auto_index - prefix << "[#{@auto_index}]" - end - options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]") - end + def default_datetime(options) + return if options[:include_blank] - def default_time_from_options(default) - case default + case options[:default] when nil Time.current when Date, Time - default + options[:default] else + default = options[:default].dup + # Rename :minute and :second to :min and :sec default[:min] ||= default[:minute] default[:sec] ||= default[:second] @@ -699,8 +876,11 @@ module ActionView default[key] ||= time.send(key) end - Time.utc_time(default[:year], default[:month], default[:day], default[:hour], default[:min], default[:sec]) - end + Time.utc_time( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end end end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 8c1dea2186..77f19b36a6 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -71,9 +71,9 @@ module ActionView def number_to_currency(number, options = {}) options.symbolize_keys! - defaults, currency = I18n.translate([:'number.format', :'number.currency.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(currency) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(currency) precision = options[:precision] || defaults[:precision] unit = options[:unit] || defaults[:unit] @@ -109,9 +109,9 @@ module ActionView def number_to_percentage(number, options = {}) options.symbolize_keys! - defaults, percentage = I18n.translate([:'number.format', :'number.percentage.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(percentage) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :raise => true) rescue {} + defaults = defaults.merge(percentage) precision = options[:precision] || defaults[:precision] separator = options[:separator] || defaults[:separator] @@ -151,7 +151,7 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults = I18n.translate(:'number.format', :locale => options[:locale]) || {} + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} unless args.empty? ActiveSupport::Deprecation.warn('number_with_delimiter takes an option hash ' + @@ -195,9 +195,10 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, precision_defaults = I18n.translate([:'number.format', :'number.precision.format'], - :locale => options[:locale]) || [{},{}] - defaults = defaults.merge(precision_defaults) + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], + :raise => true) rescue {} + defaults = defaults.merge(precision_defaults) unless args.empty? ActiveSupport::Deprecation.warn('number_with_precision takes an option hash ' + @@ -209,12 +210,14 @@ module ActionView separator ||= (options[:separator] || defaults[:separator]) delimiter ||= (options[:delimiter] || defaults[:delimiter]) - rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision - number_with_delimiter("%01.#{precision}f" % rounded_number, - :separator => separator, - :delimiter => delimiter) - rescue - number + begin + rounded_number = (Float(number) * (10 ** precision)).round.to_f / 10 ** precision + number_with_delimiter("%01.#{precision}f" % rounded_number, + :separator => separator, + :delimiter => delimiter) + rescue + number + end end STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze @@ -251,8 +254,8 @@ module ActionView options = args.extract_options! options.symbolize_keys! - defaults, human = I18n.translate([:'number.format', :'number.human.format'], - :locale => options[:locale]) || [{},{}] + defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} + human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {} defaults = defaults.merge(human) unless args.empty? @@ -272,13 +275,16 @@ module ActionView number /= 1024 ** exponent unit = STORAGE_UNITS[exponent] - number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter - ).sub(/(\d)(#{Regexp.escape(separator)}[1-9]*)?0+\z/, '\1') + " #{unit}" - rescue - number + begin + escaped_separator = Regexp.escape(separator) + number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter + ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}" + rescue + number + end end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 022edf23c8..f9096d0029 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -558,7 +558,7 @@ module ActionView [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port - (?:/(?:(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$])))*)* # path + (?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path (?:\?[\w\+@%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index f31502d99d..7ba42a3b72 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -442,7 +442,7 @@ module ActionView # # => <a href="mailto:me@domain.com">me@domain.com</a> # # mail_to "me@domain.com", "My email", :encode => "javascript" - # # => <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script> + # # => <script type="text/javascript">eval(decodeURIComponent('%64%6f%63...%27%29%3b'))</script> # # mail_to "me@domain.com", "My email", :encode => "hex" # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a> @@ -476,7 +476,7 @@ module ActionView "document.write('#{content_tag("a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c| string << sprintf("%%%x", c) end - "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>" + "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>" elsif encode == "hex" email_address_encoded = '' email_address_obfuscated.each_byte do |c| diff --git a/actionpack/lib/action_view/partials.rb b/actionpack/lib/action_view/partials.rb index eb74d4a4c7..894b88534c 100644 --- a/actionpack/lib/action_view/partials.rb +++ b/actionpack/lib/action_view/partials.rb @@ -146,7 +146,7 @@ module ActionView def find_partial_path(partial_path) if partial_path.include?('/') - "#{File.dirname(partial_path)}/_#{File.basename(partial_path)}" + File.join(File.dirname(partial_path), "_#{File.basename(partial_path)}") elsif respond_to?(:controller) "#{controller.class.controller_path}/_#{partial_path}" else diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 5fe1ca86f3..89ac500717 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -31,10 +31,10 @@ module ActionView view.send(:evaluate_assigns) view.send(:set_controller_content_type, mime_type) if respond_to?(:mime_type) - view.send(:execute, method(local_assigns), local_assigns) + view.send(:execute, method_name(local_assigns), local_assigns) end - def method(local_assigns) + def method_name(local_assigns) if local_assigns && local_assigns.any? local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}" end @@ -44,7 +44,7 @@ module ActionView private # Compile and evaluate the template's code (if necessary) def compile(local_assigns) - render_symbol = method(local_assigns) + render_symbol = method_name(local_assigns) @@mutex.synchronize do if recompile?(render_symbol) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index b281ff61f2..5dc6708431 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -22,6 +22,14 @@ module ActionView #:nodoc: end memoize :format_and_extension + def multipart? + format && format.include?('.') + end + + def content_type + format.gsub('.', '/') + end + def mime_type Mime::Type.lookup_by_extension(format) if format end @@ -84,7 +92,7 @@ module ActionView #:nodoc: # [base_path, name, format, extension] def split(file) if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) - if m[5] # Mulipart formats + if m[5] # Multipart formats [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] elsif m[4] # Single format [m[1], m[2], m[3], m[4]] |