diff options
Diffstat (limited to 'actionpack/lib')
38 files changed, 539 insertions, 186 deletions
diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index b6c484d188..db48022b9f 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -1,5 +1,13 @@ module AbstractController module Translation + # Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>. + # + # When the given key starts with a period, it will be scoped by the current + # controller and action. So if you call <tt>translate(".foo")</tt> from + # <tt>PeopleController#index</tt>, it will convert the call to + # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive + # to translate many keys within the same controller / action and gives you a + # simple framework for scoping them consistently. def translate(*args) key = args.first if key.is_a?(String) && (key[0] == '.') @@ -11,6 +19,7 @@ module AbstractController end alias :t :translate + # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>. def localize(*args) I18n.localize(*args) end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index cf2cda039d..ea33d975ef 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -82,10 +82,6 @@ module ActionController end end - def caching_allowed? - request.get? && response.status == 200 - end - def view_cache_dependencies self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 832dec7b2a..143b3e0cbd 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' module ActionController diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index eddee08545..6e0cd51d8b 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActionController module ConditionalGet extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index d04fbae150..93568da9ef 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/extract_options' require 'abstract_controller/collector' module ActionController #:nodoc: diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 59b91a240e..e9031f3fac 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -88,7 +88,7 @@ module ActionController # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). # The protocol relative scheme starts with a double slash "//" - when %r{^(\w[\w+.-]*:|//).*} + when %r{\A(\w[\w+.-]*:|//).*} options when String request.protocol + request.host_with_port + options diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index c5db0cb0d4..17379cf7ac 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -102,15 +102,16 @@ module ActionController #:nodoc: # This is the method that defines the application behavior when a request is found to be unverified. def handle_unverified_request - request.session = NullSessionHash.new + request.session = NullSessionHash.new(request.env) request.env['action_dispatch.request.flash_hash'] = nil request.env['rack.session.options'] = { skip: true } request.env['action_dispatch.cookies'] = NullCookieJar.build(request) end class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc: - def initialize - super(nil, nil) + def initialize(env) + super(nil, env) + @data = {} @loaded = true end @@ -125,7 +126,7 @@ module ActionController #:nodoc: host = request.host secure = request.ssl? - new(key_generator, host, secure) + new(key_generator, host, secure, options_for_env({})) end def write(*) @@ -162,11 +163,11 @@ module ActionController #:nodoc: # Returns true or false if a request is verified. Checks: # - # * is it a GET request? Gets should be safe and idempotent + # * is it a GET or HEAD request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? - !protect_against_forgery? || request.get? || + !protect_against_forgery? || request.get? || request.head? || form_authenticity_token == params[request_forgery_protection_token] || form_authenticity_token == request.headers['X-CSRF-Token'] end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 2c96e03f55..7e720ca6f5 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' +require 'action_dispatch/http/upload' module ActionController # Raised when a required parameter is missing. @@ -184,6 +185,21 @@ module ActionController # permitted.has_key?(:age) # => true # permitted.has_key?(:role) # => false # + # Only permitted scalars pass the filter. For example, given + # + # params.permit(:name) + # + # +:name+ passes it is a key of +params+ whose associated value is of type + # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, + # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, or + # +ActionDispatch::Http::UploadedFile+. Otherwise, the key +:name+ is + # filtered out. + # + # You may declare that the parameter should be an array of permitted scalars + # by mapping it to an empty array: + # + # params.permit(tags: []) + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ @@ -230,33 +246,14 @@ module ActionController filters.flatten.each do |filter| case filter - when Symbol, String then - if has_key?(filter) - _value = self[filter] - params[filter] = _value unless Hash === _value - end - keys.grep(/\A#{Regexp.escape(filter)}\(\d+[if]?\)\z/) { |key| params[key] = self[key] } + when Symbol, String + permitted_scalar_filter(params, filter) when Hash then - filter = filter.with_indifferent_access - - self.slice(*filter.keys).each do |key, values| - return unless values - - key = key.to_sym - - params[key] = each_element(values) do |value| - # filters are a Hash, so we expect value to be a Hash too - next if filter.is_a?(Hash) && !value.is_a?(Hash) - - value = self.class.new(value) if !value.respond_to?(:permit) - - value.permit(*Array.wrap(filter[key])) - end - end + hash_filter(params, filter) end end - unpermitted_parameters!(params) + unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters params.permit! end @@ -352,6 +349,83 @@ module ActionController def unpermitted_keys(params) self.keys - params.keys - NEVER_UNPERMITTED_PARAMS end + + # + # --- Filtering ---------------------------------------------------------- + # + + # This is a white list of permitted scalar types that includes the ones + # supported in XML and JSON requests. + # + # This list is in particular used to filter ordinary requests, String goes + # as first element to quickly short-circuit the common case. + # + # If you modify this collection please update the API of +permit+ above. + PERMITTED_SCALAR_TYPES = [ + String, + Symbol, + NilClass, + Numeric, + TrueClass, + FalseClass, + Date, + Time, + # DateTimes are Dates, we document the type but avoid the redundant check. + StringIO, + IO, + ActionDispatch::Http::UploadedFile, + ] + + def permitted_scalar?(value) + PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)} + end + + def permitted_scalar_filter(params, key) + if has_key?(key) && permitted_scalar?(self[key]) + params[key] = self[key] + end + + keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| + if permitted_scalar?(self[k]) + params[k] = self[k] + end + end + end + + def array_of_permitted_scalars?(value) + if value.is_a?(Array) + value.all? {|element| permitted_scalar?(element)} + end + end + + def array_of_permitted_scalars_filter(params, key) + if has_key?(key) && array_of_permitted_scalars?(self[key]) + params[key] = self[key] + end + end + + EMPTY_ARRAY = [] + def hash_filter(params, filter) + filter = filter.with_indifferent_access + + # Slicing filters out non-declared keys. + slice(*filter.keys).each do |key, value| + return unless value + + if filter[key] == EMPTY_ARRAY + # Declaration { comment_ids: [] }. + array_of_permitted_scalars_filter(params, key) + else + # Declaration { user: :name } or { user: [:name, :age, { adress: ... }] }. + params[key] = each_element(value) do |element| + if element.is_a?(Hash) + element = self.class.new(element) unless element.respond_to?(:permit) + element.permit(*Array.wrap(filter[key])) + end + end + end + end + end end # == Strong \Parameters diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index d8206b573d..bba1f1e201 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/hash/keys' module ActionController module TemplateAssertions @@ -125,7 +126,11 @@ module ActionController if expected_partial = options[:partial] if expected_locals = options[:locals] if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/,'') + view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') + + partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view + assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg + msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, expected_locals, @_rendered_views.locals_for(view)] @@ -235,18 +240,39 @@ module ActionController end end + # Methods #destroy and #load! are overridden to avoid calling methods on the + # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS def initialize(session = {}) super(nil, nil) - replace(session.stringify_keys) + @id = SecureRandom.hex(16) + @data = stringify_keys(session) @loaded = true end def exists? true end + + def keys + @data.keys + end + + def values + @data.values + end + + def destroy + clear + end + + private + + def load! + @id + end end # Superclass for ActionController functional tests. Functional tests allow you to diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 9ab048b756..618e2f3033 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -47,7 +47,6 @@ module ActionDispatch autoload_under 'middleware' do autoload :RequestId - autoload :BestStandardsSupport autoload :Callbacks autoload :Cookies autoload :DebugExceptions diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 25edd196c3..446862aad0 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -55,7 +55,7 @@ module ActionDispatch # should really prevent that from happening def encode_params(params) if params.is_a?(String) - return params.force_encoding("UTF-8").encode! + return params.force_encoding(Encoding::UTF_8).encode! elsif !params.is_a?(Hash) return params end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 91cf4784db..e7e8905d7e 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -137,6 +137,7 @@ module ActionDispatch # :nodoc: @committed end + # Sets the HTTP status code. def status=(status) @status = Rack::Utils.status_code(status) end @@ -145,16 +146,24 @@ module ActionDispatch # :nodoc: @content_type = content_type.to_s end - # The response code of the request + # The response code of the request. def response_code @status end - # Returns a String to ensure compatibility with Net::HTTPResponse + # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>. def code @status.to_s end + # Returns the corresponding message for the current HTTP status code: + # + # response.status = 200 + # response.message # => "OK" + # + # response.status = 404 + # response.message # => "Not Found" + # def message Rack::Utils::HTTP_STATUS_CODES[@status] end @@ -172,6 +181,8 @@ module ActionDispatch # :nodoc: stream.to_path end + # Returns the content of the response as a string. This contains the contents + # of any calls to <tt>render</tt>. def body strings = [] each { |part| strings << part.to_s } @@ -180,6 +191,7 @@ module ActionDispatch # :nodoc: EMPTY = " " + # Allows you to manually set or override the response body. def body=(body) @blank = true if body == EMPTY diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 8a97248eb3..67cb7fbcb5 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -70,7 +70,7 @@ module ActionDispatch def encode_filename(filename) # Encode the filename in the utf8 encoding, unless it is nil - filename.force_encoding("UTF-8").encode! if filename + filename.force_encoding(Encoding::UTF_8).encode! if filename end end diff --git a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb deleted file mode 100644 index 94efeb79fa..0000000000 --- a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActionDispatch - class BestStandardsSupport - def initialize(app, type = true) - @app = app - - @header = case type - when true - "IE=Edge,chrome=1" - when :builtin - "IE=Edge" - when false - nil - end - end - - def call(env) - status, headers, body = @app.call(env) - - if headers["X-UA-Compatible"] && @header - unless headers["X-UA-Compatible"][@header] - headers["X-UA-Compatible"] << "," << @header.to_s - end - else - headers["X-UA-Compatible"] = @header - end - - [status, headers, body] - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 6ecbb03784..fff26bd1b2 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/key_generator' require 'active_support/message_verifier' module ActionDispatch @@ -110,13 +111,17 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ + def self.options_for_env(env) #:nodoc: + { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', + encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', + encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', + token_key: env[TOKEN_KEY] } + end + def self.build(request) env = request.env key_generator = env[GENERATOR_KEY] - options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT], - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT], - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT], - token_key: env[TOKEN_KEY] } + options = options_for_env env host = request.host secure = request.ssl? @@ -145,6 +150,10 @@ module ActionDispatch @cookies[name.to_s] end + def fetch(name, *args, &block) + @cookies.fetch(name.to_s, *args, &block) + end + def key?(name) @cookies.key?(name.to_s) end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 7c12590c49..84df55fd5a 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -26,7 +26,7 @@ module ActionDispatch def generate_sid sid = SecureRandom.hex(16) - sid.encode!('UTF-8') + sid.encode!(Encoding::UTF_8) sid end diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb index 400ae97d22..24e44f31ac 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb @@ -7,7 +7,7 @@ <td data-route-verb='<%= route[:verb] %>'> <%= route[:verb] %> </td> - <td data-route-path='<%= route[:path] %>'> + <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'> <%= route[:path] %> </td> <td data-route-reqs='<%= route[:reqs] %>'> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 9026c4eeb2..95461fa693 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -1,22 +1,58 @@ <% content_for :style do %> - #route_table td { padding: 0 30px; } - #route_table { margin: 0 auto 0; } + #route_table { + margin: 0 auto 0; + border-collapse: collapse; + } + + #route_table td { + padding: 0 30px; + } + + #route_table tr.bottom th { + padding-bottom: 10px; + line-height: 15px; + } + + #route_table .matched_paths { + background-color: LightGoldenRodYellow; + } + + #route_table .matched_paths { + border-bottom: solid 3px SlateGrey; + } + + #path_search { + width: 80%; + font-size: inherit; + } <% end %> <table id='route_table' class='route_table'> <thead> <tr> - <th>Helper<br /> + <th>Helper</th> + <th>HTTP Verb</th> + <th>Path</th> + <th>Controller#Action</th> + </tr> + <tr class='bottom'> + <th><%# Helper %> <%= link_to "Path", "#", 'data-route-helper' => '_path', title: "Returns a relative path (without the http or domain)" %> / <%= link_to "Url", "#", 'data-route-helper' => '_url', - title: "Returns an absolute url (with the http and domain)" %> + title: "Returns an absolute url (with the http and domain)" %> + </th> + <th><%# HTTP Verb %> + </th> + <th><%# Path %> + <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %> + </th> + <th><%# Controller#action %> </th> - <th>HTTP Verb</th> - <th>Path</th> - <th>Controller#Action</th> </tr> </thead> + <tbody class='matched_paths' id='matched_paths'> + </tbody> <tbody> <%= yield %> </tbody> @@ -25,7 +61,7 @@ <script type='text/javascript'> function each(elems, func) { if (!elems instanceof Array) { elems = [elems]; } - for (var i = elems.length; i--; ) { + for (var i = 0, len = elems.length; i < len; i++) { func(elems[i]); } } @@ -46,11 +82,63 @@ function setupRouteToggleHelperLinks() { var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); onClick(toggleLinks, function(){ - var helperTxt = this.getAttribute("data-route-helper"); - var helperElems = document.querySelectorAll('[data-route-name] span.helper'); + var helperTxt = this.getAttribute("data-route-helper"), + helperElems = document.querySelectorAll('[data-route-name] span.helper'); setValOn(helperElems, helperTxt); }); } + // takes an array of elements with a data-regexp attribute and + // passes their their parent <tr> into the callback function + // if the regexp matchs a given path + function eachElemsForPath(elems, path, func) { + each(elems, function(e){ + var reg = e.getAttribute("data-regexp"); + if (path.match(RegExp(reg))) { + func(e.parentNode.cloneNode(true)); + } + }) + } + + // Ensure path always starts with a slash "/" and remove params or fragments + function sanitizePath(path) { + var path = path.charAt(0) == '/' ? path : "/" + path; + return path.replace(/\#.*|\?.*/, ''); + } + + // Enables path search functionality + function setupMatchPaths() { + var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), + pathElem = document.querySelector('#path_search'), + selectedSection = document.querySelector('#matched_paths'), + noMatchText = '<tr><th colspan="4">None</th></tr>'; + + + // Remove matches if no path is present + pathElem.onblur = function(e) { + if (pathElem.value === "") selectedSection.innerHTML = ""; + } + + // On key press perform a search for matching paths + pathElem.onkeyup = function(e){ + var path = sanitizePath(pathElem.value), + defaultText = '<tr><th colspan="4">Paths Matching (' + path + '):</th></tr>'; + + // Clear out results section + selectedSection.innerHTML= defaultText; + + // Display matches if they exist + eachElemsForPath(regexpElems, path, function(e){ + selectedSection.appendChild(e); + }); + + // If no match present, tell the user + if (selectedSection.innerHTML === defaultText) { + selectedSection.innerHTML = selectedSection.innerHTML + noMatchText; + } + } + } + + setupMatchPaths(); setupRouteToggleHelperLinks(); </script> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 5a835ae439..edf37bb9a5 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -6,7 +6,6 @@ module ActionDispatch config.action_dispatch.x_sendfile_header = nil config.action_dispatch.ip_spoofing_check = true config.action_dispatch.show_exceptions = true - config.action_dispatch.best_standards_support = true config.action_dispatch.tld_length = 1 config.action_dispatch.ignore_accept_header = false config.action_dispatch.rescue_templates = { } @@ -21,7 +20,8 @@ module ActionDispatch config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', - 'X-Content-Type-Options' => 'nosniff' + 'X-Content-Type-Options' => 'nosniff', + 'X-UA-Compatible' => 'chrome=1' } config.eager_load_namespaces << ActionDispatch diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index a05a23d953..7bc812fd22 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -63,6 +63,10 @@ module ActionDispatch @exists = nil # we haven't checked yet end + def id + options[:id] + end + def options Options.find @env end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 6c970f3024..bc6dd7145c 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -34,6 +34,23 @@ module ActionDispatch super.to_s end + def regexp + __getobj__.path.to_regexp + end + + def json_regexp + str = regexp.inspect. + sub('\\A' , '^'). + sub('\\Z' , '$'). + sub('\\z' , '$'). + sub(/^\// , ''). + sub(/\/[a-z]*$/ , ''). + gsub(/\(\?#.+\)/ , ''). + gsub(/\(\?-\w+:/ , '('). + gsub(/\s/ , '') + Regexp.new(str).source + end + def reqs @reqs ||= begin reqs = endpoint @@ -101,7 +118,11 @@ module ActionDispatch end.collect do |route| collect_engine_routes(route) - { name: route.name, verb: route.verb, path: route.path, reqs: route.reqs } + { name: route.name, + verb: route.verb, + path: route.path, + reqs: route.reqs, + regexp: route.json_regexp } end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 6d93f609a6..0a41ed0fcf 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/array/extract_options' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -245,6 +246,12 @@ module ActionDispatch raise ArgumentError, "missing :action" end + if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ + message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." + message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + raise ArgumentError, message + end + hash = {} hash[:controller] = controller unless controller.blank? hash[:action] = action unless action.blank? @@ -1403,9 +1410,10 @@ module ActionDispatch def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) + action = action.to_s.dup - if action.to_s =~ /^[\w\/]+$/ - options[:action] ||= action unless action.to_s.include?("/") + if action =~ /^[\w\/]+$/ + options[:action] ||= action unless action.include?("/") else action = nil end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index c72310cca3..ff86f87d49 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -4,6 +4,7 @@ require 'thread_safe' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' module ActionDispatch @@ -100,36 +101,16 @@ module ActionDispatch def initialize @routes = {} @helpers = [] - @module = Module.new do - protected - - def handle_positional_args(args, options, segment_keys) - inner_options = args.extract_options! - result = options.dup - - if args.size > 0 - keys = segment_keys - if args.size < keys.size - 1 # take format into account - keys -= self.url_options.keys if self.respond_to?(:url_options) - keys -= options.keys - end - result.merge!(Hash[keys.zip(args)]) - end - - result.merge!(inner_options) - end - end + @module = Module.new end def helper_names - self.module.instance_methods.map(&:to_s) + @helpers.map(&:to_s) end def clear! @helpers.each do |helper| - @module.module_eval do - remove_possible_method helper - end + @module.remove_possible_method helper end @routes.clear @@ -162,68 +143,126 @@ module ActionDispatch routes.length end - private + class UrlHelper # :nodoc: + def self.create(route, options) + if optimize_helper?(route) + OptimizedUrlHelper.new(route, options) + else + new route, options + end + end - def define_named_route_methods(name, route) - define_url_helper route, :"#{name}_path", - route.defaults.merge(:use_route => name, :only_path => true) - define_url_helper route, :"#{name}_url", - route.defaults.merge(:use_route => name, :only_path => false) + def self.optimize_helper?(route) + route.requirements.except(:controller, :action).empty? end - # Create a url helper allowing ordered parameters to be associated - # with corresponding dynamic segments, so you can do: - # - # foo_url(bar, baz, bang) - # - # Instead of: - # - # foo_url(bar: bar, baz: baz, bang: bang) - # - # Also allow options hash, so you can do: - # - # foo_url(bar, baz, bang, sort_by: 'baz') - # - def define_url_helper(route, name, options) - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{name} - def #{name}(*args) - if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? - options = #{options.inspect} - options.merge!(url_options) if respond_to?(:url_options) - options[:path] = "#{optimized_helper(route)}" - ActionDispatch::Http::URL.url_for(options) - else - url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect})) - end + class OptimizedUrlHelper < UrlHelper # :nodoc: + attr_reader :arg_size + + def initialize(route, options) + super + @path_parts = @route.required_parts + @arg_size = @path_parts.size + @string_route = string_route(route) + end + + def call(t, args) + if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) + @options.merge!(t.url_options) if t.respond_to?(:url_options) + @options[:path] = optimized_helper(args) + ActionDispatch::Http::URL.url_for(@options) + else + super end - END_EVAL + end + + private + + def string_route(route) + string_route = route.ast.to_s.dup + while string_route.gsub!(/\([^\)]*\)/, "") + true + end + string_route + end + + def optimized_helper(args) + path = @string_route.dup + klass = Journey::Router::Utils - helpers << name + @path_parts.zip(args) do |part, arg| + # Replace each route parameter + # e.g. :id for regular parameter or *path for globbing + # with ruby string interpolation code + path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(arg.to_param)) + end + path + end + + def optimize_routes_generation?(t) + t.send(:optimize_routes_generation?) + end end - # Clause check about when we need to generate an optimized helper. - def optimize_helper?(route) #:nodoc: - route.requirements.except(:controller, :action).empty? + def initialize(route, options) + @options = options + @segment_keys = route.segment_keys + @route = route + end + + def call(t, args) + t.url_for(handle_positional_args(t, args, @options, @segment_keys)) end - # Generates the interpolation to be used in the optimized helper. - def optimized_helper(route) - string_route = route.ast.to_s + def handle_positional_args(t, args, options, keys) + inner_options = args.extract_options! + result = options.dup - while string_route.gsub!(/\([^\)]*\)/, "") - true + if args.size > 0 + if args.size < keys.size - 1 # take format into account + keys -= t.url_options.keys if t.respond_to?(:url_options) + keys -= options.keys + end + result.merge!(Hash[keys.zip(args)]) end - route.required_parts.each_with_index do |part, i| - # Replace each route parameter - # e.g. :id for regular parameter or *path for globbing - # with ruby string interpolation code - string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") - end + result.merge!(inner_options) + end + end + + private + # Create a url helper allowing ordered parameters to be associated + # with corresponding dynamic segments, so you can do: + # + # foo_url(bar, baz, bang) + # + # Instead of: + # + # foo_url(bar: bar, baz: baz, bang: bang) + # + # Also allow options hash, so you can do: + # + # foo_url(bar, baz, bang, sort_by: 'baz') + # + def define_url_helper(route, name, options) + helper = UrlHelper.create(route, options.dup) - string_route + @module.remove_possible_method name + @module.module_eval do + define_method(name) do |*args| + helper.call self, args + end end + + helpers << name + end + + def define_named_route_methods(name, route) + define_url_helper route, :"#{name}_path", + route.defaults.merge(:use_route => name, :only_path => true) + define_url_helper route, :"#{name}_url", + route.defaults.merge(:use_route => name, :only_path => false) + end end attr_accessor :formatter, :set, :named_routes, :default_scope, :router diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index 73af5920ed..e2393d3799 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/extract_options' + module ActionDispatch module Routing class RoutesProxy #:nodoc: diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 11743e36f2..31e37893c6 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -182,6 +182,8 @@ module ActionView # width="30" and height="45", and "50" becomes width="50" and height="50". # <tt>:size</tt> will be ignored if the value is not in the correct format. # + # ==== Examples + # # image_tag("icon") # # => <img alt="Icon" src="/assets/icon" /> # image_tag("icon.png") @@ -212,10 +214,24 @@ module ActionView end # Returns a string suitable for an html image tag alt attribute. - # +src+ is meant to be an image file path. - # It removes the basename of the file path and the digest, if any. + # The +src+ argument is meant to be an image file path. + # The method removes the basename of the file path and the digest, + # if any. It also removes hyphens and underscores from file names and + # replaces them with spaces, returning a space-separated, titleized + # string. + # + # ==== Examples + # + # image_tag('rails.png') + # # => <img alt="Rails" src="/assets/rails.png" /> + # + # image_tag('hyphenated-file-name.png') + # # => <img alt="Hyphenated file name" src="/assets/hyphenated-file-name.png" /> + # + # image_tag('underscored_file_name.png') + # # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" /> def image_alt(src) - File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').capitalize + File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize end # Returns an html video tag for the +sources+. If +sources+ is a string, diff --git a/actionpack/lib/action_view/helpers/asset_url_helper.rb b/actionpack/lib/action_view/helpers/asset_url_helper.rb index 0affac41e8..71b78cf0b5 100644 --- a/actionpack/lib/action_view/helpers/asset_url_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_url_helper.rb @@ -132,8 +132,7 @@ module ActionView source = compute_asset_path(source, options) end - relative_url_root = (defined?(config.relative_url_root) && config.relative_url_root) || - (respond_to?(:request) && request.try(:script_name)) + relative_url_root = defined?(config.relative_url_root) && config.relative_url_root if relative_url_root source = "#{relative_url_root}#{source}" unless source.starts_with?("#{relative_url_root}/") end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 10748aacf4..61f939bff1 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -1,5 +1,6 @@ require 'date' require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/with_options' @@ -1040,14 +1041,38 @@ module ActionView end class FormBuilder + # Wraps ActionView::Helpers::DateHelper#date_select for form builders: + # + # <%= form_for @person do |f| %> + # <%= f.date_select :birth_date %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def date_select(method, options = {}, html_options = {}) @template.date_select(@object_name, method, objectify_options(options), html_options) end + # Wraps ActionView::Helpers::DateHelper#time_select for form builders: + # + # <%= form_for @race do |f| %> + # <%= f.time_select :average_lap %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def time_select(method, options = {}, html_options = {}) @template.time_select(@object_name, method, objectify_options(options), html_options) end + # Wraps ActionView::Helpers::DateHelper#datetime_select for form builders: + # + # <%= form_for @person do |f| %> + # <%= f.time_select :last_request_at %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def datetime_select(method, options = {}, html_options = {}) @template.datetime_select(@object_name, method, objectify_options(options), html_options) end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 4a31de715d..3dae1fc87a 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -8,7 +8,6 @@ require 'action_view/model_naming' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/string/inflections' module ActionView @@ -771,8 +770,8 @@ module ActionView # text_field(:post, :title, class: "create_input") # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> # - # text_field(:session, :user, onchange: "if $('#session_user').value == 'admin' { alert('Your login can not be admin!'); }") - # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange = "if $('#session_user').value == 'admin' { alert('Your login can not be admin!'); }"/> + # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }") + # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }"/> # # text_field(:snippet, :code, size: 20, class: 'code_input') # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> @@ -792,8 +791,8 @@ module ActionView # password_field(:account, :secret, class: "form_input", value: @account.secret) # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" /> # - # password_field(:user, :password, onchange: "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }") - # # => <input type="password" id="user_password" name="user[password]" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/> + # password_field(:user, :password, onchange: "if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }") + # # => <input type="password" id="user_password" name="user[password]" onchange="if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }"/> # # password_field(:account, :pin, size: 20, class: 'form_input') # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" /> diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index ae7bfd1ec6..8b3a37a853 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -2,6 +2,7 @@ require 'cgi' require 'erb' require 'action_view/helpers/form_helper' require 'active_support/core_ext/string/output_safety' +require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' module ActionView @@ -756,26 +757,74 @@ module ActionView end class FormBuilder + # Wraps ActionView::Helpers::FormOptionsHelper#select for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.select :person_id, Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def select(method, choices, options = {}, html_options = {}) @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options)) end + # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) end + # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders: + # + # <%= form_for @city do |f| %> + # <%= f.grouped_collection_select :country_id, :country_id, @continents, :countries, :name, :id, :name %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options)) end + # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders: + # + # <%= form_for @user do |f| %> + # <%= f.time_zone_select :time_zone, nil, include_blank: true %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) end + # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) end + # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders: + # + # <%= form_for @post do |f| %> + # <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %> + # <%= f.submit %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index cfdd7c77d8..878d3e0eda 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -13,8 +13,8 @@ module ActionView "'" => "\\'" } - JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' - JS_ESCAPE_MAP["\342\200\251".force_encoding('UTF-8').encode!] = '
' + JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '
' + JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '
' # Escapes carriage returns and single and double quotes for JavaScript segments. # diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 271a194913..f767957fa9 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -92,7 +92,7 @@ module ActionView # for each record. def content_tag_for_single_record(tag_name, record, prefix, options, &block) options = options ? options.dup : {} - options[:class] = "#{dom_class(record, prefix)} #{options[:class]}".rstrip + options[:class] = [ dom_class(record, prefix), options[:class] ].compact options[:id] = dom_id(record, prefix) if block_given? diff --git a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb index d27df45b5a..9655008fe2 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -13,13 +13,13 @@ module ActionView end end - def render + def render(&block) rendered_collection = render_collection do |item, value, text, default_html_options| default_html_options[:multiple] = true builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options) if block_given? - yield builder + @template_object.capture(builder, &block) else render_component(builder) end diff --git a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb index 81f2ecb2b3..893f4411e7 100644 --- a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -13,12 +13,12 @@ module ActionView end end - def render + def render(&block) render_collection do |item, value, text, default_html_options| builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options) if block_given? - yield builder + @template_object.capture(builder, &block) else render_component(builder) end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index bade121d44..5e20b557d8 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -514,7 +514,7 @@ module ActionView "in a #request method" end - return false unless request.get? + return false unless request.get? || request.head? url_string = url_for(options) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index b927c69260..f73d14c79b 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -80,8 +80,7 @@ module ActionView # problems with converting the user's data to # the <tt>default_internal</tt>. # - # To do so, simply raise the raise +WrongEncodingError+ - # as follows: + # To do so, simply raise +WrongEncodingError+ as follows: # # raise WrongEncodingError.new( # problematic_string, diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index b479f991bc..a89d51221e 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -17,7 +17,7 @@ module ActionView end def message - @string.force_encoding("BINARY") + @string.force_encoding(Encoding::ASCII_8BIT) "Your template was not saved as valid #{@encoding}. Please " \ "either specify #{@encoding} as the encoding for your template " \ "in your text editor, or mark the template with its " \ diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index afbbece90f..5aaafc15c1 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -81,7 +81,7 @@ module ActionView # wrong, we can still find an encoding tag # (<%# encoding %>) inside the String using a regular # expression - template_source = template.source.dup.force_encoding("BINARY") + template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) erb = template_source.gsub(ENCODING_TAG, '') encoding = $2 diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 4479da5bc4..463f192d0c 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -122,7 +122,7 @@ module ActionView class RenderedViewsCollection def initialize - @rendered_views ||= {} + @rendered_views ||= Hash.new { |hash, key| hash[key] = [] } end def add(view, locals) @@ -134,6 +134,10 @@ module ActionView @rendered_views[view] end + def rendered_views + @rendered_views.keys + end + def view_rendered?(view, expected_locals) locals_for(view).any? do |actual_locals| expected_locals.all? {|key, value| value == actual_locals[key] } |