aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch')
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb220
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb263
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb84
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb37
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb132
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb170
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb340
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb50
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb86
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb126
-rw-r--r--actionpack/lib/action_dispatch/http/rack_cache.rb63
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb430
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb520
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb84
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb350
-rw-r--r--actionpack/lib/action_dispatch/journey.rb7
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb189
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb164
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb41
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb158
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/builder.rb78
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb36
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/simulator.rb49
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb120
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb140
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb199
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y50
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb31
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb198
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb203
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb156
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb102
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb81
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb71
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb268
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.css30
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.js134
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/index.html.erb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb36
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb681
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb205
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_locks.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb147
-rw-r--r--actionpack/lib/action_dispatch/middleware/executor.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb300
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb57
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb183
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb43
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb92
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb54
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb117
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb28
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb62
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb149
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb116
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb130
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb27
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb161
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb32
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb200
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb55
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb234
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb76
-rw-r--r--actionpack/lib/action_dispatch/routing.rb260
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb14
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb224
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb2266
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb352
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb201
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb889
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb69
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb218
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb147
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb49
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb59
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb31
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb96
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb27
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/assertion_response.rb47
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb24
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb107
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb222
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb652
-rw-r--r--actionpack/lib/action_dispatch/testing/request_encoder.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb50
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb71
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb53
106 files changed, 15313 insertions, 0 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
new file mode 100644
index 0000000000..a8febc32b3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -0,0 +1,220 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ module Cache
+ module Request
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE".freeze
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
+
+ def if_modified_since
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
+ Time.rfc2822(since) rescue nil
+ end
+ end
+
+ def if_none_match
+ get_header HTTP_IF_NONE_MATCH
+ end
+
+ def if_none_match_etags
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
+ end
+
+ def not_modified?(modified_at)
+ if_modified_since && modified_at && if_modified_since >= modified_at
+ end
+
+ def etag_matches?(etag)
+ if etag
+ validators = if_none_match_etags
+ validators.include?(etag) || validators.include?("*")
+ end
+ end
+
+ # Check response freshness (Last-Modified and ETag) against request
+ # If-Modified-Since and If-None-Match conditions. If both headers are
+ # supplied, both must match, or the request is not considered fresh.
+ def fresh?(response)
+ last_modified = if_modified_since
+ etag = if_none_match
+
+ return false unless last_modified || etag
+
+ success = true
+ success &&= not_modified?(response.last_modified) if last_modified
+ success &&= etag_matches?(response.etag) if etag
+ success
+ end
+ end
+
+ module Response
+ attr_reader :cache_control
+
+ def last_modified
+ if last = get_header(LAST_MODIFIED)
+ Time.httpdate(last)
+ end
+ end
+
+ def last_modified?
+ has_header? LAST_MODIFIED
+ end
+
+ def last_modified=(utc_time)
+ set_header LAST_MODIFIED, utc_time.httpdate
+ end
+
+ def date
+ if date_header = get_header(DATE)
+ Time.httpdate(date_header)
+ end
+ end
+
+ def date?
+ has_header? DATE
+ end
+
+ def date=(utc_time)
+ set_header DATE, utc_time.httpdate
+ end
+
+ # This method sets a weak ETag validator on the response so browsers
+ # and proxies may cache the response, keyed on the ETag. On subsequent
+ # requests, the If-None-Match header is set to the cached ETag. If it
+ # matches the current ETag, we can return a 304 Not Modified response
+ # with no body, letting the browser or proxy know that their cache is
+ # current. Big savings in request time and network bandwidth.
+ #
+ # Weak ETags are considered to be semantically equivalent but not
+ # byte-for-byte identical. This is perfect for browser caching of HTML
+ # pages where we don't care about exact equality, just what the user
+ # is viewing.
+ #
+ # Strong ETags are considered byte-for-byte identical. They allow a
+ # browser or proxy cache to support Range requests, useful for paging
+ # through a PDF file or scrubbing through a video. Some CDNs only
+ # support strong ETags and will ignore weak ETags entirely.
+ #
+ # Weak ETags are what we almost always need, so they're the default.
+ # Check out #strong_etag= to provide a strong ETag validator.
+ def etag=(weak_validators)
+ self.weak_etag = weak_validators
+ end
+
+ def weak_etag=(weak_validators)
+ set_header "ETag", generate_weak_etag(weak_validators)
+ end
+
+ def strong_etag=(strong_validators)
+ set_header "ETag", generate_strong_etag(strong_validators)
+ end
+
+ def etag?; etag; end
+
+ # True if an ETag is set and it's a weak validator (preceded with W/)
+ def weak_etag?
+ etag? && etag.starts_with?('W/"')
+ end
+
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
+ def strong_etag?
+ etag? && !weak_etag?
+ end
+
+ private
+
+ DATE = "Date".freeze
+ LAST_MODIFIED = "Last-Modified".freeze
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
+
+ def generate_weak_etag(validators)
+ "W/#{generate_strong_etag(validators)}"
+ end
+
+ def generate_strong_etag(validators)
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ end
+
+ def cache_control_segments
+ if cache_control = _cache_control
+ cache_control.delete(" ").split(",")
+ else
+ []
+ end
+ end
+
+ def cache_control_headers
+ cache_control = {}
+
+ cache_control_segments.each do |segment|
+ directive, argument = segment.split("=", 2)
+
+ if SPECIAL_KEYS.include? directive
+ key = directive.tr("-", "_")
+ cache_control[key.to_sym] = argument || true
+ else
+ cache_control[:extras] ||= []
+ cache_control[:extras] << segment
+ end
+ end
+
+ cache_control
+ end
+
+ def prepare_cache_control!
+ @cache_control = cache_control_headers
+ end
+
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
+ NO_CACHE = "no-cache".freeze
+ PUBLIC = "public".freeze
+ PRIVATE = "private".freeze
+ MUST_REVALIDATE = "must-revalidate".freeze
+
+ def handle_conditional_get!
+ # Normally default cache control setting is handled by ETag
+ # middleware. But, if an etag is already set, the middleware
+ # defaults to `no-cache` unless a default `Cache-Control` value is
+ # previously set. So, set a default one here.
+ if (etag? || last_modified?) && !self._cache_control
+ self._cache_control = DEFAULT_CACHE_CONTROL
+ end
+ end
+
+ def merge_and_normalize_cache_control!(cache_control)
+ control = {}
+ cc_headers = cache_control_headers
+ if extras = cc_headers.delete(:extras)
+ cache_control[:extras] ||= []
+ cache_control[:extras] += extras
+ cache_control[:extras].uniq!
+ end
+
+ control.merge! cc_headers
+ control.merge! cache_control
+
+ if control.empty?
+ # Let middleware handle default behavior
+ elsif control[:no_cache]
+ self._cache_control = NO_CACHE
+ if control[:extras]
+ self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
+ end
+ else
+ extras = control[:extras]
+ max_age = control[:max_age]
+
+ options = []
+ options << "max-age=#{max_age.to_i}" if max_age
+ options << (control[:public] ? PUBLIC : PRIVATE)
+ options << MUST_REVALIDATE if control[:must_revalidate]
+ options.concat(extras) if extras
+
+ self._cache_control = options.join(", ")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
new file mode 100644
index 0000000000..a3407c9698
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -0,0 +1,263 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class ContentSecurityPolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type".freeze
+ POLICY = "Content-Security-Policy".freeze
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ _, headers, _ = response = @app.call(env)
+
+ return response unless html_response?(headers)
+ return response if policy_present?(headers)
+
+ if policy = request.content_security_policy
+ if policy.directives["script-src"]
+ if nonce = request.content_security_policy_nonce
+ policy.directives["script-src"] << "'nonce-#{nonce}'"
+ end
+ end
+
+ headers[header_name(request)] = policy.build(request.controller_instance)
+ end
+
+ response
+ end
+
+ private
+
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def header_name(request)
+ if request.content_security_policy_report_only
+ POLICY_REPORT_ONLY
+ else
+ POLICY
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.content_security_policy".freeze
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
+
+ def content_security_policy
+ get_header(POLICY)
+ end
+
+ def content_security_policy=(policy)
+ set_header(POLICY, policy)
+ end
+
+ def content_security_policy_report_only
+ get_header(POLICY_REPORT_ONLY)
+ end
+
+ def content_security_policy_report_only=(value)
+ set_header(POLICY_REPORT_ONLY, value)
+ end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ def content_security_policy_nonce
+ if content_security_policy_nonce_generator
+ if nonce = get_header(NONCE)
+ nonce
+ else
+ set_header(NONCE, generate_content_security_policy_nonce)
+ end
+ end
+ end
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ unsafe_eval: "'unsafe-eval'",
+ unsafe_inline: "'unsafe-inline'",
+ none: "'none'",
+ http: "http:",
+ https: "https:",
+ data: "data:",
+ mediastream: "mediastream:",
+ blob: "blob:",
+ filesystem: "filesystem:",
+ report_sample: "'report-sample'",
+ strict_dynamic: "'strict-dynamic'"
+ }.freeze
+
+ DIRECTIVES = {
+ base_uri: "base-uri",
+ child_src: "child-src",
+ connect_src: "connect-src",
+ default_src: "default-src",
+ font_src: "font-src",
+ form_action: "form-action",
+ frame_ancestors: "frame-ancestors",
+ frame_src: "frame-src",
+ img_src: "img-src",
+ manifest_src: "manifest-src",
+ media_src: "media-src",
+ object_src: "object-src",
+ script_src: "script-src",
+ style_src: "style-src",
+ worker_src: "worker-src"
+ }.freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def block_all_mixed_content(enabled = true)
+ if enabled
+ @directives["block-all-mixed-content"] = true
+ else
+ @directives.delete("block-all-mixed-content")
+ end
+ end
+
+ def plugin_types(*types)
+ if types.first
+ @directives["plugin-types"] = types
+ else
+ @directives.delete("plugin-types")
+ end
+ end
+
+ def report_uri(uri)
+ @directives["report-uri"] = [uri]
+ end
+
+ def require_sri_for(*types)
+ if types.first
+ @directives["require-sri-for"] = types
+ else
+ @directives.delete("require-sri-for")
+ end
+ end
+
+ def sandbox(*values)
+ if values.empty?
+ @directives["sandbox"] = true
+ elsif values.first
+ @directives["sandbox"] = values
+ else
+ @directives.delete("sandbox")
+ end
+ end
+
+ def upgrade_insecure_requests(enabled = true)
+ if enabled
+ @directives["upgrade-insecure-requests"] = true
+ else
+ @directives.delete("upgrade-insecure-requests")
+ end
+ end
+
+ def build(context = nil)
+ build_directives(context).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ def resolve_source(source, context)
+ case source
+ when String
+ source
+ when Symbol
+ source.to_s
+ when Proc
+ if context.nil?
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
+ else
+ context.instance_exec(&source)
+ end
+ else
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
new file mode 100644
index 0000000000..ec86b8bc47
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/parameter_filter"
+
+module ActionDispatch
+ module Http
+ # Allows you to specify sensitive parameters which will be replaced from
+ # the request log by looking in the query string of the request and all
+ # sub-hashes of the params hash to filter. Filtering only certain sub-keys
+ # from a hash is possible by using the dot notation: 'credit_card.number'.
+ # If a block is given, each key and value of the params hash and all
+ # sub-hashes is passed to it, where the value or the key can be replaced using
+ # String#replace or similar method.
+ #
+ # env["action_dispatch.parameter_filter"] = [:password]
+ # => replaces the value to all keys matching /password/i with "[FILTERED]"
+ #
+ # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
+ # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
+ #
+ # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
+ # change { file: { code: "xxxx"} }
+ #
+ # env["action_dispatch.parameter_filter"] = -> (k, v) do
+ # v.reverse! if k =~ /secret/i
+ # end
+ # => reverses the value to all keys matching /secret/i
+ module FilterParameters
+ ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
+ NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
+ NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
+
+ def initialize
+ super
+ @filtered_parameters = nil
+ @filtered_env = nil
+ @filtered_path = nil
+ end
+
+ # Returns a hash of parameters with all sensitive data replaced.
+ def filtered_parameters
+ @filtered_parameters ||= parameter_filter.filter(parameters)
+ end
+
+ # Returns a hash of request.env with all sensitive data replaced.
+ def filtered_env
+ @filtered_env ||= env_filter.filter(@env)
+ end
+
+ # Reconstructs a path with all sensitive GET parameters replaced.
+ def filtered_path
+ @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
+ end
+
+ private
+
+ def parameter_filter # :doc:
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
+ return NULL_PARAM_FILTER
+ }
+ end
+
+ def env_filter # :doc:
+ user_key = fetch_header("action_dispatch.parameter_filter") {
+ return NULL_ENV_FILTER
+ }
+ parameter_filter_for(Array(user_key) + ENV_MATCH)
+ end
+
+ def parameter_filter_for(filters) # :doc:
+ ParameterFilter.new(filters)
+ end
+
+ KV_RE = "[^&;=]+"
+ PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
+ def filtered_query_string # :doc:
+ query_string.gsub(PAIR_RE) do |_|
+ parameter_filter.filter($1 => $2).first.join("=")
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
new file mode 100644
index 0000000000..25394fe5dd
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ module FilterRedirect
+ FILTERED = "[FILTERED]".freeze # :nodoc:
+
+ def filtered_location # :nodoc:
+ if location_filter_match?
+ FILTERED
+ else
+ location
+ end
+ end
+
+ private
+
+ def location_filters
+ if request
+ request.get_header("action_dispatch.redirect_filter") || []
+ else
+ []
+ end
+ end
+
+ def location_filter_match?
+ location_filters.any? do |filter|
+ if String === filter
+ location.include?(filter)
+ elsif Regexp === filter
+ location =~ filter
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
new file mode 100644
index 0000000000..c3c2a9d8c5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ # Provides access to the request's HTTP headers from the environment.
+ #
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
+ # headers["Content-Type"] # => "text/plain"
+ # headers["User-Agent"] # => "curl/7.43.0"
+ #
+ # Also note that when headers are mapped to CGI-like variables by the Rack
+ # server, both dashes and underscores are converted to underscores. This
+ # ambiguity cannot be resolved at this stage anymore. Both underscores and
+ # dashes have to be interpreted as if they were originally sent as dashes.
+ #
+ # # GET / HTTP/1.1
+ # # ...
+ # # User-Agent: curl/7.43.0
+ # # X_Custom_Header: token
+ #
+ # headers["X_Custom_Header"] # => nil
+ # headers["X-Custom-Header"] # => "token"
+ class Headers
+ CGI_VARIABLES = Set.new(%W[
+ AUTH_TYPE
+ CONTENT_LENGTH
+ CONTENT_TYPE
+ GATEWAY_INTERFACE
+ HTTPS
+ PATH_INFO
+ PATH_TRANSLATED
+ QUERY_STRING
+ REMOTE_ADDR
+ REMOTE_HOST
+ REMOTE_IDENT
+ REMOTE_USER
+ REQUEST_METHOD
+ SCRIPT_NAME
+ SERVER_NAME
+ SERVER_PORT
+ SERVER_PROTOCOL
+ SERVER_SOFTWARE
+ ]).freeze
+
+ HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
+
+ include Enumerable
+
+ def self.from_hash(hash)
+ new ActionDispatch::Request.new hash
+ end
+
+ def initialize(request) # :nodoc:
+ @req = request
+ end
+
+ # Returns the value for the given key mapped to @env.
+ def [](key)
+ @req.get_header env_name(key)
+ end
+
+ # Sets the given value for the key mapped to @env.
+ def []=(key, value)
+ @req.set_header env_name(key), value
+ end
+
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
+ def add(key, value)
+ @req.add_header env_name(key), value
+ end
+
+ def key?(key)
+ @req.has_header? env_name(key)
+ end
+ alias :include? :key?
+
+ DEFAULT = Object.new # :nodoc:
+
+ # Returns the value for the given key mapped to @env.
+ #
+ # If the key is not found and an optional code block is not provided,
+ # raises a <tt>KeyError</tt> exception.
+ #
+ # If the code block is provided, then it will be run and
+ # its result returned.
+ def fetch(key, default = DEFAULT)
+ @req.fetch_header(env_name(key)) do
+ return default unless default == DEFAULT
+ return yield if block_given?
+ raise KeyError, key
+ end
+ end
+
+ def each(&block)
+ @req.each_header(&block)
+ end
+
+ # Returns a new Http::Headers instance containing the contents of
+ # <tt>headers_or_env</tt> and the original instance.
+ def merge(headers_or_env)
+ headers = @req.dup.headers
+ headers.merge!(headers_or_env)
+ headers
+ end
+
+ # Adds the contents of <tt>headers_or_env</tt> to original instance
+ # entries; duplicate keys are overwritten with the values from
+ # <tt>headers_or_env</tt>.
+ def merge!(headers_or_env)
+ headers_or_env.each do |key, value|
+ @req.set_header env_name(key), value
+ end
+ end
+
+ def env; @req.env.dup; end
+
+ private
+
+ # Converts an HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
+ def env_name(key)
+ key = key.to_s
+ if key =~ HTTP_HEADER
+ key = key.upcase.tr("-", "_")
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+ end
+ key
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
new file mode 100644
index 0000000000..d7435fa8df
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+
+module ActionDispatch
+ module Http
+ module MimeNegotiation
+ extend ActiveSupport::Concern
+
+ included do
+ mattr_accessor :ignore_accept_header, default: false
+ end
+
+ # The MIME type of the HTTP request, such as Mime[:xml].
+ def content_mime_type
+ fetch_header("action_dispatch.request.content_type") do |k|
+ v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
+ Mime::Type.lookup($1.strip.downcase)
+ else
+ nil
+ end
+ set_header k, v
+ end
+ end
+
+ def content_type
+ content_mime_type && content_mime_type.to_s
+ end
+
+ def has_content_type? # :nodoc:
+ get_header "CONTENT_TYPE"
+ end
+
+ # Returns the accepted MIME type for the request.
+ def accepts
+ fetch_header("action_dispatch.request.accepts") do |k|
+ header = get_header("HTTP_ACCEPT").to_s.strip
+
+ v = if header.empty?
+ [content_mime_type]
+ else
+ Mime::Type.parse(header)
+ end
+ set_header k, v
+ end
+ end
+
+ # Returns the MIME type for the \format used in the request.
+ #
+ # GET /posts/5.xml | request.format => Mime[:xml]
+ # GET /posts/5.xhtml | request.format => Mime[:html]
+ # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
+ #
+ def format(view_path = [])
+ formats.first || Mime::NullType.instance
+ end
+
+ def formats
+ fetch_header("action_dispatch.request.formats") do |k|
+ params_readable = begin
+ parameters[:format]
+ rescue ActionController::BadRequest
+ false
+ end
+
+ v = if params_readable
+ Array(Mime[parameters[:format]])
+ elsif use_accept_header && valid_accept_header
+ accepts
+ elsif extension_format = format_from_path_extension
+ [extension_format]
+ elsif xhr?
+ [Mime[:js]]
+ else
+ [Mime[:html]]
+ end
+ set_header k, v
+ end
+ end
+
+ # Sets the \variant for template.
+ def variant=(variant)
+ variant = Array(variant)
+
+ if variant.all? { |v| v.is_a?(Symbol) }
+ @variant = ActiveSupport::ArrayInquirer.new(variant)
+ else
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \
+ "For security reasons, never directly set the variant to a user-provided value, " \
+ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
+ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
+ end
+ end
+
+ def variant
+ @variant ||= ActiveSupport::ArrayInquirer.new
+ end
+
+ # Sets the \format by string extension, which can be used to force custom formats
+ # that are not controlled by the extension.
+ #
+ # class ApplicationController < ActionController::Base
+ # before_action :adjust_format_for_iphone
+ #
+ # private
+ # def adjust_format_for_iphone
+ # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
+ # end
+ # end
+ def format=(extension)
+ parameters[:format] = extension.to_s
+ set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
+ end
+
+ # Sets the \formats by string extensions. This differs from #format= by allowing you
+ # to set multiple, ordered formats, which is useful when you want to have a fallback.
+ #
+ # In this example, the :iphone format will be used if it's available, otherwise it'll fallback
+ # to the :html format.
+ #
+ # class ApplicationController < ActionController::Base
+ # before_action :adjust_format_for_iphone_with_html_fallback
+ #
+ # private
+ # def adjust_format_for_iphone_with_html_fallback
+ # request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
+ # end
+ # end
+ def formats=(extensions)
+ parameters[:format] = extensions.first.to_s
+ set_header "action_dispatch.request.formats", extensions.collect { |extension|
+ Mime::Type.lookup_by_extension(extension)
+ }
+ end
+
+ # Returns the first MIME type that matches the provided array of MIME types.
+ def negotiate_mime(order)
+ formats.each do |priority|
+ if priority == Mime::ALL
+ return order.first
+ elsif order.include?(priority)
+ return priority
+ end
+ end
+
+ order.include?(Mime::ALL) ? format : nil
+ end
+
+ private
+
+ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+
+ def valid_accept_header # :doc:
+ (xhr? && (accept.present? || content_mime_type)) ||
+ (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
+ end
+
+ def use_accept_header # :doc:
+ !self.class.ignore_accept_header
+ end
+
+ def format_from_path_extension # :doc:
+ path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
+ if match = path && path.match(/\.(\w+)\z/)
+ Mime[match.captures.first]
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
new file mode 100644
index 0000000000..295539281f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -0,0 +1,340 @@
+# frozen_string_literal: true
+
+# -*- frozen-string-literal: true -*-
+
+require "singleton"
+require "active_support/core_ext/string/starts_ends_with"
+
+module Mime
+ class Mimes
+ include Enumerable
+
+ def initialize
+ @mimes = []
+ @symbols = nil
+ end
+
+ def each
+ @mimes.each { |x| yield x }
+ end
+
+ def <<(type)
+ @mimes << type
+ @symbols = nil
+ end
+
+ def delete_if
+ @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
+ end
+
+ def symbols
+ @symbols ||= map(&:to_sym)
+ end
+ end
+
+ SET = Mimes.new
+ EXTENSION_LOOKUP = {}
+ LOOKUP = {}
+
+ class << self
+ def [](type)
+ return type if type.is_a?(Type)
+ Type.lookup_by_extension(type)
+ end
+
+ def fetch(type)
+ return type if type.is_a?(Type)
+ EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
+ end
+ end
+
+ # Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
+ #
+ # class PostsController < ActionController::Base
+ # def show
+ # @post = Post.find(params[:id])
+ #
+ # respond_to do |format|
+ # format.html
+ # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
+ # format.xml { render xml: @post }
+ # end
+ # end
+ # end
+ class Type
+ attr_reader :symbol
+
+ @register_callbacks = []
+
+ # A simple helper class used in parsing the accept header.
+ class AcceptItem #:nodoc:
+ attr_accessor :index, :name, :q
+ alias :to_s :name
+
+ def initialize(index, name, q = nil)
+ @index = index
+ @name = name
+ q ||= 0.0 if @name == "*/*".freeze # Default wildcard match to end of list.
+ @q = ((q || 1.0).to_f * 100).to_i
+ end
+
+ def <=>(item)
+ result = item.q <=> @q
+ result = @index <=> item.index if result == 0
+ result
+ end
+ end
+
+ class AcceptList #:nodoc:
+ def self.sort!(list)
+ list.sort!
+
+ text_xml_idx = find_item_by_name list, "text/xml"
+ app_xml_idx = find_item_by_name list, Mime[:xml].to_s
+
+ # Take care of the broken text/xml entry by renaming or deleting it.
+ if text_xml_idx && app_xml_idx
+ app_xml = list[app_xml_idx]
+ text_xml = list[text_xml_idx]
+
+ app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
+ if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
+ list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
+ app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
+ end
+ list.delete_at(text_xml_idx) # Delete text_xml from the list.
+ elsif text_xml_idx
+ list[text_xml_idx].name = Mime[:xml].to_s
+ end
+
+ # Look for more specific XML-based types and sort them ahead of app/xml.
+ if app_xml_idx
+ app_xml = list[app_xml_idx]
+ idx = app_xml_idx
+
+ while idx < list.length
+ type = list[idx]
+ break if type.q < app_xml.q
+
+ if type.name.ends_with? "+xml"
+ list[app_xml_idx], list[idx] = list[idx], app_xml
+ app_xml_idx = idx
+ end
+ idx += 1
+ end
+ end
+
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
+ list
+ end
+
+ def self.find_item_by_name(array, name)
+ array.index { |item| item.name == name }
+ end
+ end
+
+ class << self
+ TRAILING_STAR_REGEXP = /^(text|application)\/\*/
+ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
+
+ def register_callback(&block)
+ @register_callbacks << block
+ end
+
+ def lookup(string)
+ LOOKUP[string] || Type.new(string)
+ end
+
+ def lookup_by_extension(extension)
+ EXTENSION_LOOKUP[extension.to_s]
+ end
+
+ # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
+ # rendering different HTML versions depending on the user agent, like an iPhone.
+ def register_alias(string, symbol, extension_synonyms = [])
+ register(string, symbol, [], extension_synonyms, true)
+ end
+
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
+ new_mime = Type.new(string, symbol, mime_type_synonyms)
+
+ SET << new_mime
+
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
+ ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
+
+ @register_callbacks.each do |callback|
+ callback.call(new_mime)
+ end
+ new_mime
+ end
+
+ def parse(accept_header)
+ if !accept_header.include?(",")
+ accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
+ parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
+ else
+ list, index = [], 0
+ accept_header.split(",").each do |header|
+ params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
+
+ next unless params
+ params.strip!
+ next if params.empty?
+
+ params = parse_trailing_star(params) || [params]
+
+ params.each do |m|
+ list << AcceptItem.new(index, m.to_s, q)
+ index += 1
+ end
+ end
+ AcceptList.sort! list
+ end
+ end
+
+ def parse_trailing_star(accept_header)
+ parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
+ end
+
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
+ # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
+ #
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
+ # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
+ def parse_data_with_trailing_star(type)
+ Mime::SET.select { |m| m =~ type }
+ end
+
+ # This method is opposite of register method.
+ #
+ # To unregister a MIME type:
+ #
+ # Mime::Type.unregister(:mobile)
+ def unregister(symbol)
+ symbol = symbol.downcase
+ if mime = Mime[symbol]
+ SET.delete_if { |v| v.eql?(mime) }
+ LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ end
+ end
+ end
+
+ attr_reader :hash
+
+ def initialize(string, symbol = nil, synonyms = [])
+ @symbol, @synonyms = symbol, synonyms
+ @string = string
+ @hash = [@string, @synonyms, @symbol].hash
+ end
+
+ def to_s
+ @string
+ end
+
+ def to_str
+ to_s
+ end
+
+ def to_sym
+ @symbol
+ end
+
+ def ref
+ symbol || to_s
+ end
+
+ def ===(list)
+ if list.is_a?(Array)
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
+ else
+ super
+ end
+ end
+
+ def ==(mime_type)
+ return false unless mime_type
+ (@synonyms + [ self ]).any? do |synonym|
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
+ end
+ end
+
+ def eql?(other)
+ super || (self.class == other.class &&
+ @string == other.string &&
+ @synonyms == other.synonyms &&
+ @symbol == other.symbol)
+ end
+
+ def =~(mime_type)
+ return false unless mime_type
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+ @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
+ end
+
+ def html?
+ symbol == :html || @string =~ /html/
+ end
+
+ def all?; false; end
+
+ protected
+
+ attr_reader :string, :synonyms
+
+ private
+
+ def to_ary; end
+ def to_a; end
+
+ def method_missing(method, *args)
+ if method.to_s.ends_with? "?"
+ method[0..-2].downcase.to_sym == to_sym
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method, include_private = false)
+ (method.to_s.ends_with? "?") || super
+ end
+ end
+
+ class AllType < Type
+ include Singleton
+
+ def initialize
+ super "*/*", :all
+ end
+
+ def all?; true; end
+ def html?; true; end
+ end
+
+ # ALL isn't a real MIME type, so we don't register it for lookup with the
+ # other concrete types. It's a wildcard match that we use for `respond_to`
+ # negotiation internals.
+ ALL = AllType.instance
+
+ class NullType
+ include Singleton
+
+ def nil?
+ true
+ end
+
+ def ref; end
+
+ private
+ def respond_to_missing?(method, _)
+ method.to_s.ends_with? "?"
+ end
+
+ def method_missing(method, *args)
+ false if method.to_s.ends_with? "?"
+ end
+ end
+end
+
+require "action_dispatch/http/mime_types"
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
new file mode 100644
index 0000000000..342e6de312
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+# Build list of Mime types for HTTP responses
+# https://www.iana.org/assignments/media-types/
+
+Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
+Mime::Type.register "text/plain", :text, [], %w(txt)
+Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
+Mime::Type.register "text/css", :css
+Mime::Type.register "text/calendar", :ics
+Mime::Type.register "text/csv", :csv
+Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
+
+Mime::Type.register "image/png", :png, [], %w(png)
+Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
+Mime::Type.register "image/gif", :gif, [], %w(gif)
+Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
+Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
+Mime::Type.register "image/svg+xml", :svg
+
+Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
+
+Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
+Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
+Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
+
+Mime::Type.register "video/webm", :webm, [], %w(webm)
+Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
+
+Mime::Type.register "font/otf", :otf, [], %w(otf)
+Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
+Mime::Type.register "font/woff", :woff, [], %w(woff)
+Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
+
+Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
+Mime::Type.register "application/rss+xml", :rss
+Mime::Type.register "application/atom+xml", :atom
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
+
+Mime::Type.register "multipart/form-data", :multipart_form
+Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
+
+# https://www.ietf.org/rfc/rfc4627.txt
+# http://www.json.org/JSONRequest.html
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+
+Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
+Mime::Type.register "application/zip", :zip, [], %w(zip)
+Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz)
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
new file mode 100644
index 0000000000..1d58964862
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+
+module ActionDispatch
+ module Http
+ class ParameterFilter
+ FILTERED = "[FILTERED]".freeze # :nodoc:
+
+ def initialize(filters = [])
+ @filters = filters
+ end
+
+ def filter(params)
+ compiled_filter.call(params)
+ end
+
+ private
+
+ def compiled_filter
+ @compiled_filter ||= CompiledFilter.compile(@filters)
+ end
+
+ class CompiledFilter # :nodoc:
+ def self.compile(filters)
+ return lambda { |params| params.dup } if filters.empty?
+
+ strings, regexps, blocks = [], [], []
+
+ filters.each do |item|
+ case item
+ when Proc
+ blocks << item
+ when Regexp
+ regexps << item
+ else
+ strings << Regexp.escape(item.to_s)
+ end
+ end
+
+ deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) }
+ deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) }
+
+ regexps << Regexp.new(strings.join("|".freeze), true) unless strings.empty?
+ deep_regexps << Regexp.new(deep_strings.join("|".freeze), true) unless deep_strings.empty?
+
+ new regexps, deep_regexps, blocks
+ end
+
+ attr_reader :regexps, :deep_regexps, :blocks
+
+ def initialize(regexps, deep_regexps, blocks)
+ @regexps = regexps
+ @deep_regexps = deep_regexps.any? ? deep_regexps : nil
+ @blocks = blocks
+ end
+
+ def call(original_params, parents = [])
+ filtered_params = original_params.class.new
+
+ original_params.each do |key, value|
+ parents.push(key) if deep_regexps
+ if regexps.any? { |r| key =~ r }
+ value = FILTERED
+ elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r }
+ value = FILTERED
+ elsif value.is_a?(Hash)
+ value = call(value, parents)
+ elsif value.is_a?(Array)
+ value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }
+ elsif blocks.any?
+ key = key.dup if key.duplicable?
+ value = value.dup if value.duplicable?
+ blocks.each { |b| b.call(key, value) }
+ end
+ parents.pop if deep_regexps
+
+ filtered_params[key] = value
+ end
+
+ filtered_params
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
new file mode 100644
index 0000000000..8d7431fd6b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ module Parameters
+ extend ActiveSupport::Concern
+
+ PARAMETERS_KEY = "action_dispatch.request.path_parameters"
+
+ DEFAULT_PARSERS = {
+ Mime[:json].symbol => -> (raw_post) {
+ data = ActiveSupport::JSON.decode(raw_post)
+ data.is_a?(Hash) ? data : { _json: data }
+ }
+ }
+
+ # Raised when raw data from the request cannot be parsed by the parser
+ # defined for request's content MIME type.
+ class ParseError < StandardError
+ def initialize
+ super($!.message)
+ end
+ end
+
+ included do
+ class << self
+ # Returns the parameter parsers.
+ attr_reader :parameter_parsers
+ end
+
+ self.parameter_parsers = DEFAULT_PARSERS
+ end
+
+ module ClassMethods
+ # Configure the parameter parser for a given MIME type.
+ #
+ # It accepts a hash where the key is the symbol of the MIME type
+ # and the value is a proc.
+ #
+ # original_parsers = ActionDispatch::Request.parameter_parsers
+ # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
+ # new_parsers = original_parsers.merge(xml: xml_parser)
+ # ActionDispatch::Request.parameter_parsers = new_parsers
+ def parameter_parsers=(parsers)
+ @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
+ end
+ end
+
+ # Returns both GET and POST \parameters in a single hash.
+ def parameters
+ params = get_header("action_dispatch.request.parameters")
+ return params if params
+
+ params = begin
+ request_parameters.merge(query_parameters)
+ rescue EOFError
+ query_parameters.dup
+ end
+ params.merge!(path_parameters)
+ params = set_binary_encoding(params, params[:controller], params[:action])
+ set_header("action_dispatch.request.parameters", params)
+ params
+ end
+ alias :params :parameters
+
+ def path_parameters=(parameters) #:nodoc:
+ delete_header("action_dispatch.request.parameters")
+
+ parameters = set_binary_encoding(parameters, parameters[:controller], parameters[:action])
+ # If any of the path parameters has an invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ Request::Utils.check_param_encoding(parameters)
+
+ set_header PARAMETERS_KEY, parameters
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
+ raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
+ end
+
+ # Returns a hash with the \parameters used to form the \path of the request.
+ # Returned hash keys are strings:
+ #
+ # {'action' => 'my_action', 'controller' => 'my_controller'}
+ def path_parameters
+ get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
+ end
+
+ private
+
+ def set_binary_encoding(params, controller, action)
+ return params unless controller && controller.valid_encoding?
+
+ if binary_params_for?(controller, action)
+ ActionDispatch::Request::Utils.each_param_value(params) do |param|
+ param.force_encoding ::Encoding::ASCII_8BIT
+ end
+ end
+ params
+ end
+
+ def binary_params_for?(controller, action)
+ controller_class_for(controller).binary_params_for?(action)
+ rescue NameError
+ false
+ end
+
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero? || content_mime_type.nil?
+
+ strategy = parsers.fetch(content_mime_type.symbol) { return yield }
+
+ begin
+ strategy.call(raw_post)
+ rescue # JSON or Ruby code block errors.
+ my_logger = logger || ActiveSupport::Logger.new($stderr)
+ my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
+
+ raise ParseError
+ end
+ end
+
+ def params_parsers
+ ActionDispatch::Request.parameter_parsers
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb
new file mode 100644
index 0000000000..3e2d01aea3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/rack_cache.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require "rack/cache"
+require "rack/cache/context"
+require "active_support/cache"
+
+module ActionDispatch
+ class RailsMetaStore < Rack::Cache::MetaStore
+ def self.resolve(uri)
+ new
+ end
+
+ def initialize(store = Rails.cache)
+ @store = store
+ end
+
+ def read(key)
+ if data = @store.read(key)
+ Marshal.load(data)
+ else
+ []
+ end
+ end
+
+ def write(key, value)
+ @store.write(key, Marshal.dump(value))
+ end
+
+ ::Rack::Cache::MetaStore::RAILS = self
+ end
+
+ class RailsEntityStore < Rack::Cache::EntityStore
+ def self.resolve(uri)
+ new
+ end
+
+ def initialize(store = Rails.cache)
+ @store = store
+ end
+
+ def exist?(key)
+ @store.exist?(key)
+ end
+
+ def open(key)
+ @store.read(key)
+ end
+
+ def read(key)
+ body = open(key)
+ body.join if body
+ end
+
+ def write(body)
+ buf = []
+ key, size = slurp(body) { |part| buf << part }
+ @store.write(key, buf)
+ [key, size]
+ end
+
+ ::Rack::Cache::EntityStore::RAILS = self
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
new file mode 100644
index 0000000000..3838b84a7a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -0,0 +1,430 @@
+# frozen_string_literal: true
+
+require "stringio"
+
+require "active_support/inflector"
+require "action_dispatch/http/headers"
+require "action_controller/metal/exceptions"
+require "rack/request"
+require "action_dispatch/http/cache"
+require "action_dispatch/http/mime_negotiation"
+require "action_dispatch/http/parameters"
+require "action_dispatch/http/filter_parameters"
+require "action_dispatch/http/upload"
+require "action_dispatch/http/url"
+require "active_support/core_ext/array/conversions"
+
+module ActionDispatch
+ class Request
+ include Rack::Request::Helpers
+ include ActionDispatch::Http::Cache::Request
+ include ActionDispatch::Http::MimeNegotiation
+ include ActionDispatch::Http::Parameters
+ include ActionDispatch::Http::FilterParameters
+ include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
+ include Rack::Request::Env
+
+ autoload :Session, "action_dispatch/request/session"
+ autoload :Utils, "action_dispatch/request/utils"
+
+ LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
+
+ ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
+ PATH_TRANSLATED REMOTE_HOST
+ REMOTE_IDENT REMOTE_USER REMOTE_ADDR
+ SERVER_NAME SERVER_PROTOCOL
+ ORIGINAL_SCRIPT_NAME
+
+ HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
+ HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
+ HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION
+ HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
+ SERVER_ADDR
+ ].freeze
+
+ ENV_METHODS.each do |env|
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset
+ get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze
+ end # end
+ METHOD
+ end
+
+ def self.empty
+ new({})
+ end
+
+ def initialize(env)
+ super
+ @method = nil
+ @request_method = nil
+ @remote_ip = nil
+ @original_fullpath = nil
+ @fullpath = nil
+ @ip = nil
+ end
+
+ def commit_cookie_jar! # :nodoc:
+ end
+
+ PASS_NOT_FOUND = Class.new { # :nodoc:
+ def self.action(_); self; end
+ def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
+ def self.binary_params_for?(action); false; end
+ }
+
+ def controller_class
+ params = path_parameters
+ params[:action] ||= "index"
+ controller_class_for(params[:controller])
+ end
+
+ def controller_class_for(name)
+ if name
+ controller_param = name.underscore
+ const_name = "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
+ else
+ PASS_NOT_FOUND
+ end
+ end
+
+ # Returns true if the request has a header matching the given key parameter.
+ #
+ # request.key? :ip_spoofing_check # => true
+ def key?(key)
+ has_header? key
+ end
+
+ # List of HTTP request methods from the following RFCs:
+ # Hypertext Transfer Protocol -- HTTP/1.1 (https://www.ietf.org/rfc/rfc2616.txt)
+ # HTTP Extensions for Distributed Authoring -- WEBDAV (https://www.ietf.org/rfc/rfc2518.txt)
+ # Versioning Extensions to WebDAV (https://www.ietf.org/rfc/rfc3253.txt)
+ # Ordered Collections Protocol (WebDAV) (https://www.ietf.org/rfc/rfc3648.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (https://www.ietf.org/rfc/rfc3744.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) SEARCH (https://www.ietf.org/rfc/rfc5323.txt)
+ # Calendar Extensions to WebDAV (https://www.ietf.org/rfc/rfc4791.txt)
+ # PATCH Method for HTTP (https://www.ietf.org/rfc/rfc5789.txt)
+ RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+ RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+ RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+ RFC3648 = %w(ORDERPATCH)
+ RFC3744 = %w(ACL)
+ RFC5323 = %w(SEARCH)
+ RFC4791 = %w(MKCALENDAR)
+ RFC5789 = %w(PATCH)
+
+ HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
+
+ HTTP_METHOD_LOOKUP = {}
+
+ # Populate the HTTP method lookup cache.
+ HTTP_METHODS.each { |method|
+ HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+ }
+
+ # Returns the HTTP \method that the application should see.
+ # In the case where the \method was overridden by a middleware
+ # (for instance, if a HEAD request was converted to a GET,
+ # or if a _method parameter was used to determine the \method
+ # the application should use), this \method returns the overridden
+ # value, not the original.
+ def request_method
+ @request_method ||= check_method(super)
+ end
+
+ def routes # :nodoc:
+ get_header("action_dispatch.routes".freeze)
+ end
+
+ def routes=(routes) # :nodoc:
+ set_header("action_dispatch.routes".freeze, routes)
+ end
+
+ def engine_script_name(_routes) # :nodoc:
+ get_header(_routes.env_key)
+ end
+
+ def engine_script_name=(name) # :nodoc:
+ set_header(routes.env_key, name.dup)
+ end
+
+ def request_method=(request_method) #:nodoc:
+ if check_method(request_method)
+ @request_method = set_header("REQUEST_METHOD", request_method)
+ end
+ end
+
+ def controller_instance # :nodoc:
+ get_header("action_controller.instance".freeze)
+ end
+
+ def controller_instance=(controller) # :nodoc:
+ set_header("action_controller.instance".freeze, controller)
+ end
+
+ def http_auth_salt
+ get_header "action_dispatch.http_auth_salt"
+ end
+
+ def show_exceptions? # :nodoc:
+ # We're treating `nil` as "unset", and we want the default setting to be
+ # `true`. This logic should be extracted to `env_config` and calculated
+ # once.
+ !(get_header("action_dispatch.show_exceptions".freeze) == false)
+ end
+
+ # Returns a symbol form of the #request_method.
+ def request_method_symbol
+ HTTP_METHOD_LOOKUP[request_method]
+ end
+
+ # Returns the original value of the environment's REQUEST_METHOD,
+ # even if it was overridden by middleware. See #request_method for
+ # more information.
+ def method
+ @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
+ end
+
+ # Returns a symbol form of the #method.
+ def method_symbol
+ HTTP_METHOD_LOOKUP[method]
+ end
+
+ # Provides access to the request's HTTP headers, for example:
+ #
+ # request.headers["Content-Type"] # => "text/plain"
+ def headers
+ @headers ||= Http::Headers.new(self)
+ end
+
+ # Early Hints is an HTTP/2 status code that indicates hints to help a client start
+ # making preparations for processing the final response.
+ #
+ # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers.
+ #
+ # The +send_early_hints+ method accepts a hash of links as follows:
+ #
+ # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ #
+ # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the
+ # Early Hints headers are included by default if supported.
+ def send_early_hints(links)
+ return unless env["rack.early_hints"]
+
+ env["rack.early_hints"].call(links)
+ end
+
+ # Returns a +String+ with the last requested path including their params.
+ #
+ # # get '/foo'
+ # request.original_fullpath # => '/foo'
+ #
+ # # get '/foo?bar'
+ # request.original_fullpath # => '/foo?bar'
+ def original_fullpath
+ @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
+ end
+
+ # Returns the +String+ full path including params of the last URL requested.
+ #
+ # # get "/articles"
+ # request.fullpath # => "/articles"
+ #
+ # # get "/articles?page=2"
+ # request.fullpath # => "/articles?page=2"
+ def fullpath
+ @fullpath ||= super
+ end
+
+ # Returns the original request URL as a +String+.
+ #
+ # # get "/articles?page=2"
+ # request.original_url # => "http://www.example.com/articles?page=2"
+ def original_url
+ base_url + original_fullpath
+ end
+
+ # The +String+ MIME type of the request.
+ #
+ # # get "/articles"
+ # request.media_type # => "application/x-www-form-urlencoded"
+ def media_type
+ content_mime_type.to_s
+ end
+
+ # Returns the content length of the request as an integer.
+ def content_length
+ super.to_i
+ end
+
+ # Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
+ # (case-insensitive), which may need to be manually added depending on the
+ # choice of JavaScript libraries and frameworks.
+ def xml_http_request?
+ get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
+ end
+ alias :xhr? :xml_http_request?
+
+ # Returns the IP address of client as a +String+.
+ def ip
+ @ip ||= super
+ end
+
+ # Returns the IP address of client as a +String+,
+ # usually set by the RemoteIp middleware.
+ def remote_ip
+ @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
+ end
+
+ def remote_ip=(remote_ip)
+ set_header "action_dispatch.remote_ip".freeze, remote_ip
+ end
+
+ ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
+
+ # Returns the unique request id, which is based on either the X-Request-Id header that can
+ # be generated by a firewall, load balancer, or web server or by the RequestId middleware
+ # (which sets the action_dispatch.request_id environment variable).
+ #
+ # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging.
+ # This relies on the Rack variable set by the ActionDispatch::RequestId middleware.
+ def request_id
+ get_header ACTION_DISPATCH_REQUEST_ID
+ end
+
+ def request_id=(id) # :nodoc:
+ set_header ACTION_DISPATCH_REQUEST_ID, id
+ end
+
+ alias_method :uuid, :request_id
+
+ # Returns the lowercase name of the HTTP server software.
+ def server_software
+ (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
+ end
+
+ # Read the request \body. This is useful for web services that need to
+ # work with raw requests directly.
+ def raw_post
+ unless has_header? "RAW_POST_DATA"
+ raw_post_body = body
+ set_header("RAW_POST_DATA", raw_post_body.read(content_length))
+ raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
+ end
+ get_header "RAW_POST_DATA"
+ 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 = get_header("RAW_POST_DATA")
+ raw_post = raw_post.dup.force_encoding(Encoding::BINARY)
+ StringIO.new(raw_post)
+ else
+ body_stream
+ end
+ end
+
+ # Determine whether the request body contains form-data by checking
+ # the request Content-Type for one of the media-types:
+ # "application/x-www-form-urlencoded" or "multipart/form-data". The
+ # list of form-data media types can be modified through the
+ # +FORM_DATA_MEDIA_TYPES+ array.
+ #
+ # A request body is not assumed to contain form-data when no
+ # Content-Type header is provided and the request_method is POST.
+ def form_data?
+ FORM_DATA_MEDIA_TYPES.include?(media_type)
+ end
+
+ def body_stream #:nodoc:
+ get_header("rack.input")
+ end
+
+ # TODO This should be broken apart into AD::Request::Session and probably
+ # be included by the session middleware.
+ def reset_session
+ if session && session.respond_to?(:destroy)
+ session.destroy
+ else
+ self.session = {}
+ end
+ end
+
+ def session=(session) #:nodoc:
+ Session.set self, session
+ end
+
+ def session_options=(options)
+ Session::Options.set self, options
+ end
+
+ # Override Rack's GET method to support indifferent access.
+ def GET
+ fetch_header("action_dispatch.request.query_parameters") do |k|
+ rack_query_params = super || {}
+ # Check for non UTF-8 parameter values, which would cause errors later
+ Request::Utils.check_param_encoding(rack_query_params)
+ set_header k, Request::Utils.normalize_encode_params(rack_query_params)
+ end
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
+ raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
+ end
+ alias :query_parameters :GET
+
+ # Override Rack's POST method to support indifferent access.
+ def POST
+ fetch_header("action_dispatch.request.request_parameters") do
+ pr = parse_formatted_parameters(params_parsers) do |params|
+ super || {}
+ end
+ self.request_parameters = Request::Utils.normalize_encode_params(pr)
+ end
+ rescue Http::Parameters::ParseError # one of the parse strategies blew up
+ self.request_parameters = Request::Utils.normalize_encode_params(super || {})
+ raise
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
+ raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
+ end
+ alias :request_parameters :POST
+
+ # Returns the authorization header regardless of whether it was specified directly or through one of the
+ # proxy alternatives.
+ def authorization
+ get_header("HTTP_AUTHORIZATION") ||
+ get_header("X-HTTP_AUTHORIZATION") ||
+ get_header("X_HTTP_AUTHORIZATION") ||
+ get_header("REDIRECT_X_HTTP_AUTHORIZATION")
+ end
+
+ # True if the request came from localhost, 127.0.0.1, or ::1.
+ def local?
+ LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
+ end
+
+ def request_parameters=(params)
+ raise if params.nil?
+ set_header("action_dispatch.request.request_parameters".freeze, params)
+ end
+
+ def logger
+ get_header("action_dispatch.logger".freeze)
+ end
+
+ def commit_flash
+ end
+
+ def ssl?
+ super || scheme == "wss".freeze
+ end
+
+ private
+ def check_method(name)
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
+ name
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
new file mode 100644
index 0000000000..7e50cb6d23
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -0,0 +1,520 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "action_dispatch/http/filter_redirect"
+require "action_dispatch/http/cache"
+require "monitor"
+
+module ActionDispatch # :nodoc:
+ # Represents an HTTP response generated by a controller action. Use it to
+ # retrieve the current state of the response, or customize the response. It can
+ # either represent a real HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a TestResponse (i.e. one that is generated
+ # from integration tests).
+ #
+ # \Response is mostly a Ruby on \Rails framework implementation detail, and
+ # should never be used directly in controllers. Controllers should use the
+ # methods defined in ActionController::Base instead. For example, if you want
+ # to set the HTTP response's content MIME type, then use
+ # ActionControllerBase#headers instead of Response#headers.
+ #
+ # Nevertheless, integration tests may want to inspect controller responses in
+ # more detail, and that's when \Response can be useful for application
+ # developers. Integration test methods such as
+ # ActionDispatch::Integration::Session#get and
+ # ActionDispatch::Integration::Session#post return objects of type
+ # TestResponse (which are of course also of type \Response).
+ #
+ # For example, the following demo integration test prints the body of the
+ # controller response to the console:
+ #
+ # class DemoControllerTest < ActionDispatch::IntegrationTest
+ # def test_print_root_path_to_console
+ # get('/')
+ # puts response.body
+ # end
+ # end
+ class Response
+ class Header < DelegateClass(Hash) # :nodoc:
+ def initialize(response, header)
+ @response = response
+ super(header)
+ end
+
+ def []=(k, v)
+ if @response.sending? || @response.sent?
+ raise ActionDispatch::IllegalStateError, "header already sent"
+ end
+
+ super
+ end
+
+ def merge(other)
+ self.class.new @response, __getobj__.merge(other)
+ end
+
+ def to_hash
+ __getobj__.dup
+ end
+ end
+
+ # The request that the response is responding to.
+ attr_accessor :request
+
+ # The HTTP status code.
+ attr_reader :status
+
+ # Get headers for this response.
+ attr_reader :header
+
+ alias_method :headers, :header
+
+ delegate :[], :[]=, to: :@header
+
+ def each(&block)
+ sending!
+ x = @stream.each(&block)
+ sent!
+ x
+ end
+
+ CONTENT_TYPE = "Content-Type".freeze
+ SET_COOKIE = "Set-Cookie".freeze
+ LOCATION = "Location".freeze
+ NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
+
+ cattr_accessor :default_charset, default: "utf-8"
+ cattr_accessor :default_headers
+
+ include Rack::Response::Helpers
+ # Aliasing these off because AD::Http::Cache::Response defines them.
+ alias :_cache_control :cache_control
+ alias :_cache_control= :cache_control=
+
+ include ActionDispatch::Http::FilterRedirect
+ include ActionDispatch::Http::Cache::Response
+ include MonitorMixin
+
+ class Buffer # :nodoc:
+ def initialize(response, buf)
+ @response = response
+ @buf = buf
+ @closed = false
+ @str_body = nil
+ end
+
+ def body
+ @str_body ||= begin
+ buf = "".dup
+ each { |chunk| buf << chunk }
+ buf
+ end
+ end
+
+ def write(string)
+ raise IOError, "closed stream" if closed?
+
+ @str_body = nil
+ @response.commit!
+ @buf.push string
+ end
+
+ def each(&block)
+ if @str_body
+ return enum_for(:each) unless block_given?
+
+ yield @str_body
+ else
+ each_chunk(&block)
+ end
+ end
+
+ def abort
+ end
+
+ def close
+ @response.commit!
+ @closed = true
+ end
+
+ def closed?
+ @closed
+ end
+
+ private
+
+ def each_chunk(&block)
+ @buf.each(&block)
+ end
+ end
+
+ def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
+ header = merge_default_headers(header, default_headers)
+ new status, header, body
+ end
+
+ def self.merge_default_headers(original, default)
+ default.respond_to?(:merge) ? default.merge(original) : original
+ end
+
+ # The underlying body, as a streamable object.
+ attr_reader :stream
+
+ def initialize(status = 200, header = {}, body = [])
+ super()
+
+ @header = Header.new(self, header)
+
+ self.body, self.status = body, status
+
+ @cv = new_cond
+ @committed = false
+ @sending = false
+ @sent = false
+
+ prepare_cache_control!
+
+ yield self if block_given?
+ end
+
+ def has_header?(key); headers.key? key; end
+ def get_header(key); headers[key]; end
+ def set_header(key, v); headers[key] = v; end
+ def delete_header(key); headers.delete key; end
+
+ def await_commit
+ synchronize do
+ @cv.wait_until { @committed }
+ end
+ end
+
+ def await_sent
+ synchronize { @cv.wait_until { @sent } }
+ end
+
+ def commit!
+ synchronize do
+ before_committed
+ @committed = true
+ @cv.broadcast
+ end
+ end
+
+ def sending!
+ synchronize do
+ before_sending
+ @sending = true
+ @cv.broadcast
+ end
+ end
+
+ def sent!
+ synchronize do
+ @sent = true
+ @cv.broadcast
+ end
+ end
+
+ def sending?; synchronize { @sending }; end
+ def committed?; synchronize { @committed }; end
+ def sent?; synchronize { @sent }; end
+
+ # Sets the HTTP status code.
+ def status=(status)
+ @status = Rack::Utils.status_code(status)
+ end
+
+ # Sets the HTTP content type.
+ def content_type=(content_type)
+ return unless content_type
+ new_header_info = parse_content_type(content_type.to_s)
+ prev_header_info = parsed_content_type_header
+ charset = new_header_info.charset || prev_header_info.charset
+ charset ||= self.class.default_charset unless prev_header_info.mime_type
+ set_content_type new_header_info.mime_type, charset
+ end
+
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
+
+ def content_type
+ parsed_content_type_header.mime_type
+ end
+
+ def sending_file=(v)
+ if true == v
+ self.charset = false
+ end
+ end
+
+ # Sets the HTTP character set. In case of +nil+ parameter
+ # it sets the charset to +default_charset+.
+ #
+ # response.charset = 'utf-16' # => 'utf-16'
+ # response.charset = nil # => 'utf-8'
+ def charset=(charset)
+ content_type = parsed_content_type_header.mime_type
+ if false == charset
+ set_content_type content_type, nil
+ else
+ set_content_type content_type, charset || self.class.default_charset
+ end
+ end
+
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ def charset
+ header_info = parsed_content_type_header
+ header_info.charset || self.class.default_charset
+ end
+
+ # The response code of the request.
+ def response_code
+ @status
+ end
+
+ # 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
+ alias_method :status_message, :message
+
+ # Returns the content of the response as a string. This contains the contents
+ # of any calls to <tt>render</tt>.
+ def body
+ @stream.body
+ end
+
+ def write(string)
+ @stream.write string
+ end
+
+ # Allows you to manually set or override the response body.
+ def body=(body)
+ if body.respond_to?(:to_path)
+ @stream = body
+ else
+ synchronize do
+ @stream = build_buffer self, munge_body_object(body)
+ end
+ end
+ end
+
+ # Avoid having to pass an open file handle as the response body.
+ # Rack::Sendfile will usually intercept the response and uses
+ # the path directly, so there is no reason to open the file.
+ class FileBody #:nodoc:
+ attr_reader :to_path
+
+ def initialize(path)
+ @to_path = path
+ end
+
+ def body
+ File.binread(to_path)
+ end
+
+ # Stream the file's contents if Rack::Sendfile isn't present.
+ def each
+ File.open(to_path, "rb") do |file|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+ end
+ end
+ end
+
+ # Send the file stored at +path+ as the response body.
+ def send_file(path)
+ commit!
+ @stream = FileBody.new(path)
+ end
+
+ def reset_body!
+ @stream = build_buffer(self, [])
+ end
+
+ def body_parts
+ parts = []
+ @stream.each { |x| parts << x }
+ parts
+ end
+
+ # The location header we'll be responding with.
+ alias_method :redirect_url, :location
+
+ def close
+ stream.close if stream.respond_to?(:close)
+ end
+
+ def abort
+ if stream.respond_to?(:abort)
+ stream.abort
+ elsif stream.respond_to?(:close)
+ # `stream.close` should really be reserved for a close from the
+ # other direction, but we must fall back to it for
+ # compatibility.
+ stream.close
+ end
+ end
+
+ # Turns the Response into a Rack-compatible array of the status, headers,
+ # and body. Allows explicit splatting:
+ #
+ # status, headers, body = *response
+ def to_a
+ commit!
+ rack_response @status, @header.to_hash
+ end
+ alias prepare! to_a
+
+ # Returns the response cookies, converted to a Hash of (name => value) pairs
+ #
+ # assert_equal 'AuthorOfNewPage', r.cookies['author']
+ def cookies
+ cookies = {}
+ if header = get_header(SET_COOKIE)
+ header = header.split("\n") if header.respond_to?(:to_str)
+ header.each do |cookie|
+ if pair = cookie.split(";").first
+ key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
+ cookies[key] = value
+ end
+ end
+ end
+ cookies
+ end
+
+ private
+
+ ContentTypeHeader = Struct.new :mime_type, :charset
+ NullContentTypeHeader = ContentTypeHeader.new nil, nil
+
+ def parse_content_type(content_type)
+ if content_type
+ type, charset = content_type.split(/;\s*charset=/)
+ type = nil if type && type.empty?
+ ContentTypeHeader.new(type, charset)
+ else
+ NullContentTypeHeader
+ end
+ end
+
+ # Small internal convenience method to get the parsed version of the current
+ # content type header.
+ def parsed_content_type_header
+ parse_content_type(get_header(CONTENT_TYPE))
+ end
+
+ def set_content_type(content_type, charset)
+ type = (content_type || "").dup
+ type << "; charset=#{charset.to_s.downcase}" if charset
+ set_header CONTENT_TYPE, type
+ end
+
+ def before_committed
+ return if committed?
+ assign_default_content_type_and_charset!
+ merge_and_normalize_cache_control!(@cache_control)
+ handle_conditional_get!
+ handle_no_content!
+ end
+
+ def before_sending
+ # Normally we've already committed by now, but it's possible
+ # (e.g., if the controller action tries to read back its own
+ # response) to get here before that. In that case, we must force
+ # an "early" commit: we're about to freeze the headers, so this is
+ # our last chance.
+ commit! unless committed?
+
+ headers.freeze
+ request.commit_cookie_jar! unless committed?
+ end
+
+ def build_buffer(response, body)
+ Buffer.new response, body
+ end
+
+ def munge_body_object(body)
+ body.respond_to?(:each) ? body : [body]
+ end
+
+ def assign_default_content_type_and_charset!
+ return if content_type
+
+ ct = parsed_content_type_header
+ set_content_type(ct.mime_type || Mime[:html].to_s,
+ ct.charset || self.class.default_charset)
+ end
+
+ class RackBody
+ def initialize(response)
+ @response = response
+ end
+
+ def each(*args, &block)
+ @response.each(*args, &block)
+ end
+
+ def close
+ # Rack "close" maps to Response#abort, and *not* Response#close
+ # (which is used when the controller's finished writing)
+ @response.abort
+ end
+
+ def body
+ @response.body
+ end
+
+ def respond_to?(method, include_private = false)
+ if method.to_s == "to_path"
+ @response.stream.respond_to?(method)
+ else
+ super
+ end
+ end
+
+ def to_path
+ @response.stream.to_path
+ end
+
+ def to_ary
+ nil
+ end
+ end
+
+ def handle_no_content!
+ if NO_CONTENT_CODES.include?(@status)
+ @header.delete CONTENT_TYPE
+ @header.delete "Content-Length"
+ end
+ end
+
+ def rack_response(status, header)
+ if NO_CONTENT_CODES.include?(status)
+ [status, header, []]
+ else
+ [status, header, RackBody.new(self)]
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
new file mode 100644
index 0000000000..0b162dc7f1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ # Models uploaded files.
+ #
+ # The actual file is accessible via the +tempfile+ accessor, though some
+ # of its interface is available directly for convenience.
+ #
+ # Uploaded files are temporary files whose lifespan is one request. When
+ # the object is finalized Ruby unlinks the file, so there is no need to
+ # clean them with a separate maintenance task.
+ class UploadedFile
+ # The basename of the file in the client.
+ attr_accessor :original_filename
+
+ # A string with the MIME type of the file.
+ attr_accessor :content_type
+
+ # A +Tempfile+ object with the actual uploaded file. Note that some of
+ # its interface is available directly.
+ attr_accessor :tempfile
+ alias :to_io :tempfile
+
+ # A string with the headers of the multipart request.
+ attr_accessor :headers
+
+ def initialize(hash) # :nodoc:
+ @tempfile = hash[:tempfile]
+ raise(ArgumentError, ":tempfile is required") unless @tempfile
+
+ if hash[:filename]
+ @original_filename = hash[:filename].dup
+
+ begin
+ @original_filename.encode!(Encoding::UTF_8)
+ rescue EncodingError
+ @original_filename.force_encoding(Encoding::UTF_8)
+ end
+ else
+ @original_filename = nil
+ end
+
+ @content_type = hash[:type]
+ @headers = hash[:head]
+ end
+
+ # Shortcut for +tempfile.read+.
+ def read(length = nil, buffer = nil)
+ @tempfile.read(length, buffer)
+ end
+
+ # Shortcut for +tempfile.open+.
+ def open
+ @tempfile.open
+ end
+
+ # Shortcut for +tempfile.close+.
+ def close(unlink_now = false)
+ @tempfile.close(unlink_now)
+ end
+
+ # Shortcut for +tempfile.path+.
+ def path
+ @tempfile.path
+ end
+
+ # Shortcut for +tempfile.rewind+.
+ def rewind
+ @tempfile.rewind
+ end
+
+ # Shortcut for +tempfile.size+.
+ def size
+ @tempfile.size
+ end
+
+ # Shortcut for +tempfile.eof?+.
+ def eof?
+ @tempfile.eof?
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
new file mode 100644
index 0000000000..35ba44005a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -0,0 +1,350 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+
+module ActionDispatch
+ module Http
+ module URL
+ IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+ HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
+ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
+
+ mattr_accessor :tld_length, default: 1
+
+ class << self
+ # Returns the domain part of a host given the domain level.
+ #
+ # # Top-level domain example
+ # extract_domain('www.example.com', 1) # => "example.com"
+ # # Second-level domain example
+ # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
+ def extract_domain(host, tld_length)
+ extract_domain_from(host, tld_length) if named_host?(host)
+ end
+
+ # Returns the subdomains of a host as an Array given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomains('www.example.com', 1) # => ["www"]
+ # # Second-level domain example
+ # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
+ def extract_subdomains(host, tld_length)
+ if named_host?(host)
+ extract_subdomains_from(host, tld_length)
+ else
+ []
+ end
+ end
+
+ # Returns the subdomains of a host as a String given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomain('www.example.com', 1) # => "www"
+ # # Second-level domain example
+ # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
+ def extract_subdomain(host, tld_length)
+ extract_subdomains(host, tld_length).join(".")
+ end
+
+ def url_for(options)
+ if options[:only_path]
+ path_for options
+ else
+ full_url_for options
+ end
+ end
+
+ def full_url_for(options)
+ host = options[:host]
+ protocol = options[:protocol]
+ port = options[:port]
+
+ unless host
+ raise ArgumentError, "Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true"
+ end
+
+ build_host_url(host, port, protocol, options, path_for(options))
+ end
+
+ def path_for(options)
+ path = options[:script_name].to_s.chomp("/".freeze)
+ path << options[:path] if options.key?(:path)
+
+ add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
+
+ path
+ end
+
+ private
+
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_, v| v.to_param.nil? }
+ query = params.to_query
+ path << "?#{query}" unless query.empty?
+ end
+
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
+ end
+
+ def extract_domain_from(host, tld_length)
+ host.split(".").last(1 + tld_length).join(".")
+ end
+
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split(".")
+ parts[0..-(tld_length + 2)]
+ end
+
+ def add_trailing_slash(path)
+ if path.include?("?")
+ path.sub!(/\?/, '/\&')
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
+ end
+
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
+ end
+
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
+
+ result = protocol.dup
+
+ if options[:user] && options[:password]
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ end
+
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
+
+ result.concat path
+ end
+
+ def named_host?(host)
+ IP_HOST_REGEXP !~ host
+ end
+
+ def normalize_protocol(protocol)
+ case protocol
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
+ else
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
+ end
+ end
+
+ def normalize_host(_host, options)
+ return _host unless named_host?(_host)
+
+ tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
+
+ host = "".dup
+ if subdomain == true
+ return _host if domain.nil?
+
+ host << extract_subdomains_from(_host, tld_length).join(".")
+ elsif subdomain
+ host << subdomain.to_param
+ end
+ host << "." unless host.empty?
+ host << (domain || extract_domain_from(_host, tld_length))
+ host
+ end
+
+ def normalize_port(port, protocol)
+ return unless port
+
+ case protocol
+ when "//" then yield port
+ when "https://"
+ yield port unless port.to_i == 443
+ else
+ yield port unless port.to_i == 80
+ end
+ end
+ end
+
+ def initialize
+ super
+ @protocol = nil
+ @port = nil
+ end
+
+ # Returns the complete URL used for this request.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.url # => "http://example.com"
+ def url
+ protocol + host_with_port + fullpath
+ end
+
+ # Returns 'https://' if this is an SSL request and 'http://' otherwise.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.protocol # => "http://"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req.protocol # => "https://"
+ def protocol
+ @protocol ||= ssl? ? "https://" : "http://"
+ end
+
+ # Returns the \host and port for this request, such as "example.com:8080".
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.raw_host_with_port # => "example.com"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.raw_host_with_port # => "example.com:80"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.raw_host_with_port # => "example.com:8080"
+ def raw_host_with_port
+ if forwarded = x_forwarded_host.presence
+ forwarded.split(/,\s?/).last
+ else
+ get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
+ end
+ end
+
+ # Returns the host for this request, such as "example.com".
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host # => "example.com"
+ def host
+ raw_host_with_port.sub(/:\d+$/, "".freeze)
+ end
+
+ # Returns a \host:\port string for this request, such as "example.com" or
+ # "example.com:8080". Port is only included if it is not a default port
+ # (80 or 443)
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.host_with_port # => "example.com"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.host_with_port # => "example.com"
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host_with_port # => "example.com:8080"
+ def host_with_port
+ "#{host}#{port_string}"
+ end
+
+ # Returns the port number of this request as an integer.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.port # => 80
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port # => 8080
+ def port
+ @port ||= begin
+ if raw_host_with_port =~ /:(\d+)$/
+ $1.to_i
+ else
+ standard_port
+ end
+ end
+ end
+
+ # Returns the standard \port number for this request's protocol.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port # => 80
+ def standard_port
+ case protocol
+ when "https://" then 443
+ else 80
+ end
+ end
+
+ # Returns whether this request is using the standard port
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.standard_port? # => true
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port? # => false
+ def standard_port?
+ port == standard_port
+ end
+
+ # Returns a number \port suffix like 8080 if the \port number of this request
+ # is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.optional_port # => nil
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.optional_port # => 8080
+ def optional_port
+ standard_port? ? nil : port
+ end
+
+ # Returns a string \port suffix, including colon, like ":8080" if the \port
+ # number of this request is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.port_string # => ""
+ #
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port_string # => ":8080"
+ def port_string
+ standard_port? ? "" : ":#{port}"
+ end
+
+ # Returns the requested port, such as 8080, based on SERVER_PORT
+ #
+ # req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
+ # req.server_port # => 80
+ #
+ # req = ActionDispatch::Request.new 'SERVER_PORT' => '8080'
+ # req.server_port # => 8080
+ def server_port
+ get_header("SERVER_PORT").to_i
+ end
+
+ # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
+ # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
+ def domain(tld_length = @@tld_length)
+ ActionDispatch::Http::URL.extract_domain(host, tld_length)
+ end
+
+ # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
+ # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
+ # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
+ # in "www.rubyonrails.co.uk".
+ def subdomains(tld_length = @@tld_length)
+ ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
+ end
+
+ # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
+ # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
+ # such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt>
+ # in "www.rubyonrails.co.uk".
+ def subdomain(tld_length = @@tld_length)
+ ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb
new file mode 100644
index 0000000000..2852efa6ae
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router"
+require "action_dispatch/journey/gtg/builder"
+require "action_dispatch/journey/gtg/simulator"
+require "action_dispatch/journey/nfa/builder"
+require "action_dispatch/journey/nfa/simulator"
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
new file mode 100644
index 0000000000..0f04839d9b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+require "action_controller/metal/exceptions"
+
+module ActionDispatch
+ # :stopdoc:
+ module Journey
+ # The Formatter class is used for formatting URLs. For example, parameters
+ # passed to +url_for+ in Rails will eventually call Formatter#generate.
+ class Formatter
+ attr_reader :routes
+
+ def initialize(routes)
+ @routes = routes
+ @cache = nil
+ end
+
+ def generate(name, options, path_parameters, parameterize = nil)
+ constraints = path_parameters.merge(options)
+ missing_keys = nil
+
+ match_route(name, constraints) do |route|
+ parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
+
+ # Skip this route unless a name has been provided or it is a
+ # standard Rails route since we can't determine whether an options
+ # hash passed to url_for matches a Rack application or a redirect.
+ next unless name || route.dispatcher?
+
+ missing_keys = missing_keys(route, parameterized_parts)
+ next if missing_keys && !missing_keys.empty?
+ params = options.dup.delete_if do |key, _|
+ parameterized_parts.key?(key) || route.defaults.key?(key)
+ end
+
+ defaults = route.defaults
+ required_parts = route.required_parts
+
+ route.parts.reverse_each do |key|
+ break if defaults[key].nil? && parameterized_parts[key].present?
+ next if parameterized_parts[key].to_s != defaults[key].to_s
+ break if required_parts.include?(key)
+
+ parameterized_parts.delete(key)
+ end
+
+ return [route.format(parameterized_parts), params]
+ end
+
+ unmatched_keys = (missing_keys || []) & constraints.keys
+ missing_keys = (missing_keys || []) - unmatched_keys
+
+ message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}".dup
+ message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
+ message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
+
+ raise ActionController::UrlGenerationError, message
+ end
+
+ def clear
+ @cache = nil
+ end
+
+ private
+
+ def extract_parameterized_parts(route, options, recall, parameterize = nil)
+ parameterized_parts = recall.merge(options)
+
+ keys_to_keep = route.parts.reverse_each.drop_while { |part|
+ !options.key?(part) || (options[part] || recall[part]).nil?
+ } | route.required_parts
+
+ parameterized_parts.delete_if do |bad_key, _|
+ !keys_to_keep.include?(bad_key)
+ end
+
+ if parameterize
+ parameterized_parts.each do |k, v|
+ parameterized_parts[k] = parameterize.call(k, v)
+ end
+ end
+
+ parameterized_parts.keep_if { |_, v| v }
+ parameterized_parts
+ end
+
+ def named_routes
+ routes.named_routes
+ end
+
+ def match_route(name, options)
+ if named_routes.key?(name)
+ yield named_routes[name]
+ else
+ routes = non_recursive(cache, options)
+
+ supplied_keys = options.each_with_object({}) do |(k, v), h|
+ h[k.to_s] = true if v
+ end
+
+ hash = routes.group_by { |_, r| r.score(supplied_keys) }
+
+ hash.keys.sort.reverse_each do |score|
+ break if score < 0
+
+ hash[score].sort_by { |i, _| i }.each do |_, route|
+ yield route
+ end
+ end
+ end
+ end
+
+ def non_recursive(cache, options)
+ routes = []
+ queue = [cache]
+
+ while queue.any?
+ c = queue.shift
+ routes.concat(c[:___routes]) if c.key?(:___routes)
+
+ options.each do |pair|
+ queue << c[pair] if c.key?(pair)
+ end
+ end
+
+ routes
+ end
+
+ module RegexCaseComparator
+ DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
+ DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
+
+ def self.===(regex)
+ DEFAULT_INPUT == regex
+ end
+ end
+
+ # Returns an array populated with missing keys if any are present.
+ def missing_keys(route, parts)
+ missing_keys = nil
+ tests = route.path.requirements
+ route.required_parts.each { |key|
+ case tests[key]
+ when nil
+ unless parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ when RegexCaseComparator
+ unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ else
+ unless /\A#{tests[key]}\Z/ === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ end
+ }
+ missing_keys
+ end
+
+ def possibles(cache, options, depth = 0)
+ cache.fetch(:___routes) { [] } + options.find_all { |pair|
+ cache.key?(pair)
+ }.flat_map { |pair|
+ possibles(cache[pair], options, depth + 1)
+ }
+ end
+
+ def build_cache
+ root = { ___routes: [] }
+ routes.routes.each_with_index do |route, i|
+ leaf = route.required_defaults.inject(root) do |h, tuple|
+ h[tuple] ||= {}
+ end
+ (leaf[:___routes] ||= []) << [i, route]
+ end
+ root
+ end
+
+ def cache
+ @cache ||= build_cache
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
new file mode 100644
index 0000000000..44c31053cb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/gtg/transition_table"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class Builder # :nodoc:
+ DUMMY = Nodes::Dummy.new
+
+ attr_reader :root, :ast, :endpoints
+
+ def initialize(root)
+ @root = root
+ @ast = Nodes::Cat.new root, DUMMY
+ @followpos = nil
+ end
+
+ def transition_table
+ dtrans = TransitionTable.new
+ marked = {}
+ state_id = Hash.new { |h, k| h[k] = h.length }
+
+ start = firstpos(root)
+ dstates = [start]
+ until dstates.empty?
+ s = dstates.shift
+ next if marked[s]
+ marked[s] = true # mark s
+
+ s.group_by { |state| symbol(state) }.each do |sym, ps|
+ u = ps.flat_map { |l| followpos(l) }
+ next if u.empty?
+
+ if u.uniq == [DUMMY]
+ from = state_id[s]
+ to = state_id[Object.new]
+ dtrans[from, to] = sym
+
+ dtrans.add_accepting(to)
+ ps.each { |state| dtrans.add_memo(to, state.memo) }
+ else
+ dtrans[state_id[s], state_id[u]] = sym
+
+ if u.include?(DUMMY)
+ to = state_id[u]
+
+ accepting = ps.find_all { |l| followpos(l).include?(DUMMY) }
+
+ accepting.each { |accepting_state|
+ dtrans.add_memo(to, accepting_state.memo)
+ }
+
+ dtrans.add_accepting(state_id[u])
+ end
+ end
+
+ dstates << u
+ end
+ end
+
+ dtrans
+ end
+
+ def nullable?(node)
+ case node
+ when Nodes::Group
+ true
+ when Nodes::Star
+ true
+ when Nodes::Or
+ node.children.any? { |c| nullable?(c) }
+ when Nodes::Cat
+ nullable?(node.left) && nullable?(node.right)
+ when Nodes::Terminal
+ !node.left
+ when Nodes::Unary
+ nullable?(node.left)
+ else
+ raise ArgumentError, "unknown nullable: %s" % node.class.name
+ end
+ end
+
+ def firstpos(node)
+ case node
+ when Nodes::Star
+ firstpos(node.left)
+ when Nodes::Cat
+ if nullable?(node.left)
+ firstpos(node.left) | firstpos(node.right)
+ else
+ firstpos(node.left)
+ end
+ when Nodes::Or
+ node.children.flat_map { |c| firstpos(c) }.uniq
+ when Nodes::Unary
+ firstpos(node.left)
+ when Nodes::Terminal
+ nullable?(node) ? [] : [node]
+ else
+ raise ArgumentError, "unknown firstpos: %s" % node.class.name
+ end
+ end
+
+ def lastpos(node)
+ case node
+ when Nodes::Star
+ firstpos(node.left)
+ when Nodes::Or
+ node.children.flat_map { |c| lastpos(c) }.uniq
+ when Nodes::Cat
+ if nullable?(node.right)
+ lastpos(node.left) | lastpos(node.right)
+ else
+ lastpos(node.right)
+ end
+ when Nodes::Terminal
+ nullable?(node) ? [] : [node]
+ when Nodes::Unary
+ lastpos(node.left)
+ else
+ raise ArgumentError, "unknown lastpos: %s" % node.class.name
+ end
+ end
+
+ def followpos(node)
+ followpos_table[node]
+ end
+
+ private
+
+ def followpos_table
+ @followpos ||= build_followpos
+ end
+
+ def build_followpos
+ table = Hash.new { |h, k| h[k] = [] }
+ @ast.each do |n|
+ case n
+ when Nodes::Cat
+ lastpos(n.left).each do |i|
+ table[i] += firstpos(n.right)
+ end
+ when Nodes::Star
+ lastpos(n).each do |i|
+ table[i] += firstpos(n)
+ end
+ end
+ end
+ table
+ end
+
+ def symbol(edge)
+ case edge
+ when Journey::Nodes::Symbol
+ edge.regexp
+ else
+ edge.left
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
new file mode 100644
index 0000000000..2ee4f5c30c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "strscan"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class MatchData # :nodoc:
+ attr_reader :memos
+
+ def initialize(memos)
+ @memos = memos
+ end
+ end
+
+ class Simulator # :nodoc:
+ attr_reader :tt
+
+ def initialize(transition_table)
+ @tt = transition_table
+ end
+
+ def memos(string)
+ input = StringScanner.new(string)
+ state = [0]
+ while sym = input.scan(%r([/.?]|[^/.?]+))
+ state = tt.move(state, sym)
+ end
+
+ acceptance_states = state.find_all { |s|
+ tt.accepting? s
+ }
+
+ return yield if acceptance_states.empty?
+
+ acceptance_states.flat_map { |x| tt.memo(x) }.compact
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
new file mode 100644
index 0000000000..ea647e051a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class TransitionTable # :nodoc:
+ include Journey::NFA::Dot
+
+ attr_reader :memos
+
+ def initialize
+ @regexp_states = {}
+ @string_states = {}
+ @accepting = {}
+ @memos = Hash.new { |h, k| h[k] = [] }
+ end
+
+ def add_accepting(state)
+ @accepting[state] = true
+ end
+
+ def accepting_states
+ @accepting.keys
+ end
+
+ def accepting?(state)
+ @accepting[state]
+ end
+
+ def add_memo(idx, memo)
+ @memos[idx] << memo
+ end
+
+ def memo(idx)
+ @memos[idx]
+ end
+
+ def eclosure(t)
+ Array(t)
+ end
+
+ def move(t, a)
+ return [] if t.empty?
+
+ regexps = []
+
+ t.map { |s|
+ if states = @regexp_states[s]
+ regexps.concat states.map { |re, v| re === a ? v : nil }
+ end
+
+ if states = @string_states[s]
+ states[a]
+ end
+ }.compact.concat regexps
+ end
+
+ def as_json(options = nil)
+ simple_regexp = Hash.new { |h, k| h[k] = {} }
+
+ @regexp_states.each do |from, hash|
+ hash.each do |re, to|
+ simple_regexp[from][re.source] = to
+ end
+ end
+
+ {
+ regexp_states: simple_regexp,
+ string_states: @string_states,
+ accepting: @accepting
+ }
+ end
+
+ def to_svg
+ svg = IO.popen("dot -Tsvg", "w+") { |f|
+ f.write(to_dot)
+ f.close_write
+ f.readlines
+ }
+ 3.times { svg.shift }
+ svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
+ end
+
+ def visualizer(paths, title = "FSM")
+ viz_dir = File.join __dir__, "..", "visualizer"
+ fsm_js = File.read File.join(viz_dir, "fsm.js")
+ fsm_css = File.read File.join(viz_dir, "fsm.css")
+ erb = File.read File.join(viz_dir, "index.html.erb")
+ states = "function tt() { return #{to_json}; }"
+
+ fun_routes = paths.sample(3).map do |ast|
+ ast.map { |n|
+ case n
+ when Nodes::Symbol
+ case n.left
+ when ":id" then rand(100).to_s
+ when ":format" then %w{ xml json }.sample
+ else
+ "omg"
+ end
+ when Nodes::Terminal then n.symbol
+ else
+ nil
+ end
+ }.compact.join
+ end
+
+ stylesheets = [fsm_css]
+ svg = to_svg
+ javascripts = [states, fsm_js]
+
+ fun_routes = fun_routes
+ stylesheets = stylesheets
+ svg = svg
+ javascripts = javascripts
+
+ require "erb"
+ template = ERB.new erb
+ template.result(binding)
+ end
+
+ def []=(from, to, sym)
+ to_mappings = states_hash_for(sym)[from] ||= {}
+ to_mappings[sym] = to
+ end
+
+ def states
+ ss = @string_states.keys + @string_states.values.flat_map(&:values)
+ rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
+ (ss + rs).uniq
+ end
+
+ def transitions
+ @string_states.flat_map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ } + @regexp_states.flat_map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ }
+ end
+
+ private
+
+ def states_hash_for(sym)
+ case sym
+ when String
+ @string_states
+ when Regexp
+ @regexp_states
+ else
+ raise ArgumentError, "unknown symbol: %s" % sym.class
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
new file mode 100644
index 0000000000..d22302e101
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/transition_table"
+require "action_dispatch/journey/gtg/transition_table"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class Visitor < Visitors::Visitor # :nodoc:
+ def initialize(tt)
+ @tt = tt
+ @i = -1
+ end
+
+ def visit_CAT(node)
+ left = visit(node.left)
+ right = visit(node.right)
+
+ @tt.merge(left.last, right.first)
+
+ [left.first, right.last]
+ end
+
+ def visit_GROUP(node)
+ from = @i += 1
+ left = visit(node.left)
+ to = @i += 1
+
+ @tt.accepting = to
+
+ @tt[from, left.first] = nil
+ @tt[left.last, to] = nil
+ @tt[from, to] = nil
+
+ [from, to]
+ end
+
+ def visit_OR(node)
+ from = @i += 1
+ children = node.children.map { |c| visit(c) }
+ to = @i += 1
+
+ children.each do |child|
+ @tt[from, child.first] = nil
+ @tt[child.last, to] = nil
+ end
+
+ @tt.accepting = to
+
+ [from, to]
+ end
+
+ def terminal(node)
+ from_i = @i += 1 # new state
+ to_i = @i += 1 # new state
+
+ @tt[from_i, to_i] = node
+ @tt.accepting = to_i
+ @tt.add_memo(to_i, node.memo)
+
+ [from_i, to_i]
+ end
+ end
+
+ class Builder # :nodoc:
+ def initialize(ast)
+ @ast = ast
+ end
+
+ def transition_table
+ tt = TransitionTable.new
+ Visitor.new(tt).accept(@ast)
+ tt
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
new file mode 100644
index 0000000000..56e9e3c83d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ module Dot # :nodoc:
+ def to_dot
+ edges = transitions.map { |from, sym, to|
+ " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
+ }
+
+ # memo_nodes = memos.values.flatten.map { |n|
+ # label = n
+ # if Journey::Route === n
+ # label = "#{n.verb.source} #{n.path.spec}"
+ # end
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
+ # }
+ # memo_edges = memos.flat_map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ # }.uniq
+
+ <<-eodot
+digraph nfa {
+ rankdir=LR;
+ node [shape = doublecircle];
+ #{accepting_states.join ' '};
+ node [shape = circle];
+#{edges.join "\n"}
+}
+ eodot
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
new file mode 100644
index 0000000000..8efe48d91c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "strscan"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class MatchData # :nodoc:
+ attr_reader :memos
+
+ def initialize(memos)
+ @memos = memos
+ end
+ end
+
+ class Simulator # :nodoc:
+ attr_reader :tt
+
+ def initialize(transition_table)
+ @tt = transition_table
+ end
+
+ def simulate(string)
+ input = StringScanner.new(string)
+ state = tt.eclosure(0)
+ until input.eos?
+ sym = input.scan(%r([/.?]|[^/.?]+))
+
+ # FIXME: tt.eclosure is not needed for the GTG
+ state = tt.eclosure(tt.move(state, sym))
+ end
+
+ acceptance_states = state.find_all { |s|
+ tt.accepting?(tt.eclosure(s).sort.last)
+ }
+
+ return if acceptance_states.empty?
+
+ memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact
+
+ MatchData.new(memos)
+ end
+
+ alias :=~ :simulate
+ alias :match :simulate
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
new file mode 100644
index 0000000000..fe55861507
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class TransitionTable # :nodoc:
+ include Journey::NFA::Dot
+
+ attr_accessor :accepting
+ attr_reader :memos
+
+ def initialize
+ @table = Hash.new { |h, f| h[f] = {} }
+ @memos = {}
+ @accepting = nil
+ @inverted = nil
+ end
+
+ def accepting?(state)
+ accepting == state
+ end
+
+ def accepting_states
+ [accepting]
+ end
+
+ def add_memo(idx, memo)
+ @memos[idx] = memo
+ end
+
+ def memo(idx)
+ @memos[idx]
+ end
+
+ def []=(i, f, s)
+ @table[f][i] = s
+ end
+
+ def merge(left, right)
+ @memos[right] = @memos.delete(left)
+ @table[right] = @table.delete(left)
+ end
+
+ def states
+ (@table.keys + @table.values.flat_map(&:keys)).uniq
+ end
+
+ # Returns set of NFA states to which there is a transition on ast symbol
+ # +a+ from some state +s+ in +t+.
+ def following_states(t, a)
+ Array(t).flat_map { |s| inverted[s][a] }.uniq
+ end
+
+ # Returns set of NFA states to which there is a transition on ast symbol
+ # +a+ from some state +s+ in +t+.
+ def move(t, a)
+ Array(t).map { |s|
+ inverted[s].keys.compact.find_all { |sym|
+ sym === a
+ }.map { |sym| inverted[s][sym] }
+ }.flatten.uniq
+ end
+
+ def alphabet
+ inverted.values.flat_map(&:keys).compact.uniq.sort_by(&:to_s)
+ end
+
+ # Returns a set of NFA states reachable from some NFA state +s+ in set
+ # +t+ on nil-transitions alone.
+ def eclosure(t)
+ stack = Array(t)
+ seen = {}
+ children = []
+
+ until stack.empty?
+ s = stack.pop
+ next if seen[s]
+
+ seen[s] = true
+ children << s
+
+ stack.concat(inverted[s][nil])
+ end
+
+ children.uniq
+ end
+
+ def transitions
+ @table.flat_map { |to, hash|
+ hash.map { |from, sym| [from, sym, to] }
+ }
+ end
+
+ private
+
+ def inverted
+ return @inverted if @inverted
+
+ @inverted = Hash.new { |h, from|
+ h[from] = Hash.new { |j, s| j[s] = [] }
+ }
+
+ @table.each { |to, hash|
+ hash.each { |from, sym|
+ if sym
+ sym = Nodes::Symbol === sym ? sym.regexp : sym.left
+ end
+
+ @inverted[from][sym] << to
+ }
+ }
+
+ @inverted
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
new file mode 100644
index 0000000000..32f632800c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/visitors"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module Nodes # :nodoc:
+ class Node # :nodoc:
+ include Enumerable
+
+ attr_accessor :left, :memo
+
+ def initialize(left)
+ @left = left
+ @memo = nil
+ end
+
+ def each(&block)
+ Visitors::Each::INSTANCE.accept(self, block)
+ end
+
+ def to_s
+ Visitors::String::INSTANCE.accept(self, "")
+ end
+
+ def to_dot
+ Visitors::Dot::INSTANCE.accept(self)
+ end
+
+ def to_sym
+ name.to_sym
+ end
+
+ def name
+ -left.tr("*:", "")
+ end
+
+ def type
+ raise NotImplementedError
+ end
+
+ def symbol?; false; end
+ def literal?; false; end
+ def terminal?; false; end
+ def star?; false; end
+ def cat?; false; end
+ def group?; false; end
+ end
+
+ class Terminal < Node # :nodoc:
+ alias :symbol :left
+ def terminal?; true; end
+ end
+
+ class Literal < Terminal # :nodoc:
+ def literal?; true; end
+ def type; :LITERAL; end
+ end
+
+ class Dummy < Literal # :nodoc:
+ def initialize(x = Object.new)
+ super
+ end
+
+ def literal?; false; end
+ end
+
+ %w{ Symbol Slash Dot }.each do |t|
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
+ class #{t} < Terminal;
+ def type; :#{t.upcase}; end
+ end
+ eoruby
+ end
+
+ class Symbol < Terminal # :nodoc:
+ attr_accessor :regexp
+ alias :symbol :regexp
+ attr_reader :name
+
+ DEFAULT_EXP = /[^\.\/\?]+/
+ def initialize(left)
+ super
+ @regexp = DEFAULT_EXP
+ @name = -left.tr("*:", "")
+ end
+
+ def default_regexp?
+ regexp == DEFAULT_EXP
+ end
+
+ def symbol?; true; end
+ end
+
+ class Unary < Node # :nodoc:
+ def children; [left] end
+ end
+
+ class Group < Unary # :nodoc:
+ def type; :GROUP; end
+ def group?; true; end
+ end
+
+ class Star < Unary # :nodoc:
+ def star?; true; end
+ def type; :STAR; end
+
+ def name
+ left.name.tr "*:", ""
+ end
+ end
+
+ class Binary < Node # :nodoc:
+ attr_accessor :right
+
+ def initialize(left, right)
+ super(left)
+ @right = right
+ end
+
+ def children; [left, right] end
+ end
+
+ class Cat < Binary # :nodoc:
+ def cat?; true; end
+ def type; :CAT; end
+ end
+
+ class Or < Node # :nodoc:
+ attr_reader :children
+
+ def initialize(children)
+ @children = children
+ end
+
+ def type; :OR; end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
new file mode 100644
index 0000000000..e002755bcf
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -0,0 +1,199 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by Racc 1.4.14
+# from Racc grammar file "".
+#
+
+require 'racc/parser.rb'
+
+# :stopdoc:
+
+require "action_dispatch/journey/parser_extras"
+module ActionDispatch
+ module Journey
+ class Parser < Racc::Parser
+##### State transition tables begin ###
+
+racc_action_table = [
+ 13, 15, 14, 7, 19, 16, 8, 19, 13, 15,
+ 14, 7, 17, 16, 8, 13, 15, 14, 7, 21,
+ 16, 8, 13, 15, 14, 7, 24, 16, 8 ]
+
+racc_action_check = [
+ 2, 2, 2, 2, 22, 2, 2, 2, 19, 19,
+ 19, 19, 1, 19, 19, 7, 7, 7, 7, 17,
+ 7, 7, 0, 0, 0, 0, 20, 0, 0 ]
+
+racc_action_pointer = [
+ 20, 12, -2, nil, nil, nil, nil, 13, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 19, nil, 6,
+ 20, nil, -5, nil, nil ]
+
+racc_action_default = [
+ -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
+ -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
+ -19, 25, -8, -9, -7 ]
+
+racc_goto_table = [
+ 1, 22, 18, 23, nil, nil, nil, 20 ]
+
+racc_goto_check = [
+ 1, 2, 1, 3, nil, nil, nil, 1 ]
+
+racc_goto_pointer = [
+ nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
+ nil ]
+
+racc_goto_default = [
+ nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
+ 12 ]
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 2, 11, :_reduce_1,
+ 1, 11, :_reduce_2,
+ 1, 11, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 3, 15, :_reduce_7,
+ 3, 13, :_reduce_8,
+ 3, 13, :_reduce_9,
+ 1, 16, :_reduce_10,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 19, :_reduce_15,
+ 1, 17, :_reduce_16,
+ 1, 18, :_reduce_17,
+ 1, 20, :_reduce_18 ]
+
+racc_reduce_n = 19
+
+racc_shift_n = 25
+
+racc_token_table = {
+ false => 0,
+ :error => 1,
+ :SLASH => 2,
+ :LITERAL => 3,
+ :SYMBOL => 4,
+ :LPAREN => 5,
+ :RPAREN => 6,
+ :DOT => 7,
+ :STAR => 8,
+ :OR => 9 }
+
+racc_nt_base = 10
+
+racc_use_result_var = false
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+ "$end",
+ "error",
+ "SLASH",
+ "LITERAL",
+ "SYMBOL",
+ "LPAREN",
+ "RPAREN",
+ "DOT",
+ "STAR",
+ "OR",
+ "$start",
+ "expressions",
+ "expression",
+ "or",
+ "terminal",
+ "group",
+ "star",
+ "symbol",
+ "literal",
+ "slash",
+ "dot" ]
+
+Racc_debug_parser = false
+
+##### State transition tables end #####
+
+# reduce 0 omitted
+
+def _reduce_1(val, _values)
+ Cat.new(val.first, val.last)
+end
+
+def _reduce_2(val, _values)
+ val.first
+end
+
+# reduce 3 omitted
+
+# reduce 4 omitted
+
+# reduce 5 omitted
+
+# reduce 6 omitted
+
+def _reduce_7(val, _values)
+ Group.new(val[1])
+end
+
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
+end
+
+# reduce 11 omitted
+
+# reduce 12 omitted
+
+# reduce 13 omitted
+
+# reduce 14 omitted
+
+def _reduce_15(val, _values)
+ Slash.new(val.first)
+end
+
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
+end
+
+def _reduce_17(val, _values)
+ Literal.new(val.first)
+end
+
+def _reduce_18(val, _values)
+ Dot.new(val.first)
+end
+
+def _reduce_none(val, _values)
+ val[0]
+end
+
+ end # class Parser
+ end # module Journey
+ end # module ActionDispatch
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
new file mode 100644
index 0000000000..f9b1a7a958
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -0,0 +1,50 @@
+class ActionDispatch::Journey::Parser
+ options no_result_var
+token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
+
+rule
+ expressions
+ : expression expressions { Cat.new(val.first, val.last) }
+ | expression { val.first }
+ | or
+ ;
+ expression
+ : terminal
+ | group
+ | star
+ ;
+ group
+ : LPAREN expressions RPAREN { Group.new(val[1]) }
+ ;
+ or
+ : expression OR expression { Or.new([val.first, val.last]) }
+ | expression OR or { Or.new([val.first, val.last]) }
+ ;
+ star
+ : STAR { Star.new(Symbol.new(val.last)) }
+ ;
+ terminal
+ : symbol
+ | literal
+ | slash
+ | dot
+ ;
+ slash
+ : SLASH { Slash.new(val.first) }
+ ;
+ symbol
+ : SYMBOL { Symbol.new(val.first) }
+ ;
+ literal
+ : LITERAL { Literal.new(val.first) }
+ ;
+ dot
+ : DOT { Dot.new(val.first) }
+ ;
+
+end
+
+---- header
+# :stopdoc:
+
+require "action_dispatch/journey/parser_extras"
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
new file mode 100644
index 0000000000..18ec6c9b9b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/scanner"
+require "action_dispatch/journey/nodes/node"
+
+module ActionDispatch
+ # :stopdoc:
+ module Journey
+ class Parser < Racc::Parser
+ include Journey::Nodes
+
+ def self.parse(string)
+ new.parse string
+ end
+
+ def initialize
+ @scanner = Scanner.new
+ end
+
+ def parse(string)
+ @scanner.scan_setup(string)
+ do_parse
+ end
+
+ def next_token
+ @scanner.next_token
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
new file mode 100644
index 0000000000..537f479ee5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module Path # :nodoc:
+ class Pattern # :nodoc:
+ attr_reader :spec, :requirements, :anchored
+
+ def self.from_string(string)
+ build(string, {}, "/.?", true)
+ end
+
+ def self.build(path, requirements, separators, anchored)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, requirements, separators, anchored
+ end
+
+ def initialize(ast, requirements, separators, anchored)
+ @spec = ast
+ @requirements = requirements
+ @separators = separators
+ @anchored = anchored
+
+ @names = nil
+ @optional_names = nil
+ @required_names = nil
+ @re = nil
+ @offsets = nil
+ end
+
+ def build_formatter
+ Visitors::FormatBuilder.new.accept(spec)
+ end
+
+ def eager_load!
+ required_names
+ offsets
+ to_regexp
+ nil
+ end
+
+ def ast
+ @spec.find_all(&:symbol?).each do |node|
+ re = @requirements[node.to_sym]
+ node.regexp = re if re
+ end
+
+ @spec.find_all(&:star?).each do |node|
+ node = node.left
+ node.regexp = @requirements[node.to_sym] || /(.+)/
+ end
+
+ @spec
+ end
+
+ def names
+ @names ||= spec.find_all(&:symbol?).map(&:name)
+ end
+
+ def required_names
+ @required_names ||= names - optional_names
+ end
+
+ def optional_names
+ @optional_names ||= spec.find_all(&:group?).flat_map { |group|
+ group.find_all(&:symbol?)
+ }.map(&:name).uniq
+ end
+
+ class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
+ def initialize(separator, matchers)
+ @separator = separator
+ @matchers = matchers
+ @separator_re = "([^#{separator}]+)"
+ super()
+ end
+
+ def accept(node)
+ %r{\A#{visit node}\Z}
+ end
+
+ def visit_CAT(node)
+ [visit(node.left), visit(node.right)].join
+ end
+
+ def visit_SYMBOL(node)
+ node = node.to_sym
+
+ return @separator_re unless @matchers.key?(node)
+
+ re = @matchers[node]
+ "(#{Regexp.union(re)})"
+ end
+
+ def visit_GROUP(node)
+ "(?:#{visit node.left})?"
+ end
+
+ def visit_LITERAL(node)
+ Regexp.escape(node.left)
+ end
+ alias :visit_DOT :visit_LITERAL
+
+ def visit_SLASH(node)
+ node.left
+ end
+
+ def visit_STAR(node)
+ re = @matchers[node.left.to_sym] || ".+"
+ "(#{re})"
+ end
+
+ def visit_OR(node)
+ children = node.children.map { |n| visit n }
+ "(?:#{children.join(?|)})"
+ end
+ end
+
+ class UnanchoredRegexp < AnchoredRegexp # :nodoc:
+ def accept(node)
+ %r{\A#{visit node}}
+ end
+ end
+
+ class MatchData # :nodoc:
+ attr_reader :names
+
+ def initialize(names, offsets, match)
+ @names = names
+ @offsets = offsets
+ @match = match
+ end
+
+ def captures
+ Array.new(length - 1) { |i| self[i + 1] }
+ end
+
+ def [](x)
+ idx = @offsets[x - 1] + x
+ @match[idx]
+ end
+
+ def length
+ @offsets.length
+ end
+
+ def post_match
+ @match.post_match
+ end
+
+ def to_s
+ @match.to_s
+ end
+ end
+
+ def match(other)
+ return unless match = to_regexp.match(other)
+ MatchData.new(names, offsets, match)
+ end
+ alias :=~ :match
+
+ def source
+ to_regexp.source
+ end
+
+ def to_regexp
+ @re ||= regexp_visitor.new(@separators, @requirements).accept spec
+ end
+
+ private
+
+ def regexp_visitor
+ @anchored ? AnchoredRegexp : UnanchoredRegexp
+ end
+
+ def offsets
+ return @offsets if @offsets
+
+ @offsets = [0]
+
+ spec.find_all(&:symbol?).each do |node|
+ node = node.to_sym
+
+ if @requirements.key?(node)
+ re = /#{Regexp.union(@requirements[node])}|/
+ @offsets.push((re.match("").length - 1) + @offsets.last)
+ else
+ @offsets << @offsets.last
+ end
+ end
+
+ @offsets
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
new file mode 100644
index 0000000000..8165709a3d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # :stopdoc:
+ module Journey
+ class Route
+ attr_reader :app, :path, :defaults, :name, :precedence
+
+ attr_reader :constraints, :internal
+ alias :conditions :constraints
+
+ module VerbMatchers
+ VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
+ VERBS.each do |v|
+ class_eval <<-eoc, __FILE__, __LINE__ + 1
+ class #{v}
+ def self.verb; name.split("::").last; end
+ def self.call(req); req.#{v.downcase}?; end
+ end
+ eoc
+ end
+
+ class Unknown
+ attr_reader :verb
+
+ def initialize(verb)
+ @verb = verb
+ end
+
+ def call(request); @verb === request.request_method; end
+ end
+
+ class All
+ def self.call(_); true; end
+ def self.verb; ""; end
+ end
+
+ VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
+ klass = const_get verb
+ hash[verb] = klass
+ hash[verb.downcase] = klass
+ hash[verb.downcase.to_sym] = klass
+ end
+ end
+
+ def self.verb_matcher(verb)
+ VerbMatchers::VERB_TO_CLASS.fetch(verb) do
+ VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
+ end
+ end
+
+ def self.build(name, app, path, constraints, required_defaults, defaults)
+ request_method_match = verb_matcher(constraints.delete(:request_method))
+ new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
+ end
+
+ ##
+ # +path+ is a path constraint.
+ # +constraints+ is a hash of constraints to be applied to this route.
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false)
+ @name = name
+ @app = app
+ @path = path
+
+ @request_method_match = request_method_match
+ @constraints = constraints
+ @defaults = defaults
+ @required_defaults = nil
+ @_required_defaults = required_defaults
+ @required_parts = nil
+ @parts = nil
+ @decorated_ast = nil
+ @precedence = precedence
+ @path_formatter = @path.build_formatter
+ @internal = internal
+ end
+
+ def eager_load!
+ path.eager_load!
+ ast
+ parts
+ required_defaults
+ nil
+ end
+
+ def ast
+ @decorated_ast ||= begin
+ decorated_ast = path.ast
+ decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
+ decorated_ast
+ end
+ end
+
+ # Needed for `rails routes`. Picks up succinctly defined requirements
+ # for a route, for example route
+ #
+ # get 'photo/:id', :controller => 'photos', :action => 'show',
+ # :id => /[A-Z]\d{5}/
+ #
+ # will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
+ # as requirements.
+ def requirements
+ @defaults.merge(path.requirements).delete_if { |_, v|
+ /.+?/ == v
+ }
+ end
+
+ def segments
+ path.names
+ end
+
+ def required_keys
+ required_parts + required_defaults.keys
+ end
+
+ def score(supplied_keys)
+ required_keys = path.required_names
+
+ required_keys.each do |k|
+ return -1 unless supplied_keys.include?(k)
+ end
+
+ score = 0
+ path.names.each do |k|
+ score += 1 if supplied_keys.include?(k)
+ end
+
+ score + (required_defaults.length * 2)
+ end
+
+ def parts
+ @parts ||= segments.map(&:to_sym)
+ end
+ alias :segment_keys :parts
+
+ def format(path_options)
+ @path_formatter.evaluate path_options
+ end
+
+ def required_parts
+ @required_parts ||= path.required_names.map(&:to_sym)
+ end
+
+ def required_default?(key)
+ @_required_defaults.include?(key)
+ end
+
+ def required_defaults
+ @required_defaults ||= @defaults.dup.delete_if do |k, _|
+ parts.include?(k) || !required_default?(k)
+ end
+ end
+
+ def glob?
+ !path.spec.grep(Nodes::Star).empty?
+ end
+
+ def dispatcher?
+ @app.dispatcher?
+ end
+
+ def matches?(request)
+ match_verb(request) &&
+ constraints.all? { |method, value|
+ case value
+ when Regexp, String
+ value === request.send(method).to_s
+ when Array
+ value.include?(request.send(method))
+ when TrueClass
+ request.send(method).present?
+ when FalseClass
+ request.send(method).blank?
+ else
+ value === request.send(method)
+ end
+ }
+ end
+
+ def ip
+ constraints[:ip] || //
+ end
+
+ def requires_matching_verb?
+ !@request_method_match.all? { |x| x == VerbMatchers::All }
+ end
+
+ def verb
+ verbs.join("|")
+ end
+
+ private
+ def verbs
+ @request_method_match.map(&:verb)
+ end
+
+ def match_verb(request)
+ @request_method_match.any? { |m| m.call request }
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
new file mode 100644
index 0000000000..30af3ff930
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router/utils"
+require "action_dispatch/journey/routes"
+require "action_dispatch/journey/formatter"
+
+before = $-w
+$-w = false
+require "action_dispatch/journey/parser"
+$-w = before
+
+require "action_dispatch/journey/route"
+require "action_dispatch/journey/path/pattern"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class RoutingError < ::StandardError # :nodoc:
+ end
+
+ attr_accessor :routes
+
+ def initialize(routes)
+ @routes = routes
+ end
+
+ def eager_load!
+ # Eagerly trigger the simulator's initialization so
+ # it doesn't happen during a request cycle.
+ simulator
+ nil
+ end
+
+ def serve(req)
+ find_routes(req).each do |match, parameters, route|
+ set_params = req.path_parameters
+ path_info = req.path_info
+ script_name = req.script_name
+
+ unless route.path.anchored
+ req.script_name = (script_name.to_s + match.to_s).chomp("/")
+ req.path_info = match.post_match
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
+ end
+
+ parameters = route.defaults.merge parameters.transform_values { |val|
+ val.dup.force_encoding(::Encoding::UTF_8)
+ }
+
+ req.path_parameters = set_params.merge parameters
+
+ status, headers, body = route.app.serve(req)
+
+ if "pass" == headers["X-Cascade"]
+ req.script_name = script_name
+ req.path_info = path_info
+ req.path_parameters = set_params
+ next
+ end
+
+ return [status, headers, body]
+ end
+
+ [404, { "X-Cascade" => "pass" }, ["Not Found"]]
+ end
+
+ def recognize(rails_req)
+ find_routes(rails_req).each do |match, parameters, route|
+ unless route.path.anchored
+ rails_req.script_name = match.to_s
+ rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
+ end
+
+ parameters = route.defaults.merge parameters
+ yield(route, parameters)
+ end
+ end
+
+ def visualizer
+ tt = GTG::Builder.new(ast).transition_table
+ groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
+ asts = groups.values.map(&:first)
+ tt.visualizer(asts)
+ end
+
+ private
+
+ def partitioned_routes
+ routes.partition { |r|
+ r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
+ }
+ end
+
+ def ast
+ routes.ast
+ end
+
+ def simulator
+ routes.simulator
+ end
+
+ def custom_routes
+ routes.custom_routes
+ end
+
+ def filter_routes(path)
+ return [] unless ast
+ simulator.memos(path) { [] }
+ end
+
+ def find_routes(req)
+ routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
+ r.path.match(req.path_info)
+ }
+
+ routes =
+ if req.head?
+ match_head_routes(routes, req)
+ else
+ match_routes(routes, req)
+ end
+
+ routes.sort_by!(&:precedence)
+
+ routes.map! { |r|
+ match_data = r.path.match(req.path_info)
+ path_parameters = {}
+ match_data.names.zip(match_data.captures) { |name, val|
+ path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
+ }
+ [match_data, path_parameters, r]
+ }
+ end
+
+ def match_head_routes(routes, req)
+ verb_specific_routes = routes.select(&:requires_matching_verb?)
+ head_routes = match_routes(verb_specific_routes, req)
+
+ if head_routes.empty?
+ begin
+ req.request_method = "GET"
+ match_routes(routes, req)
+ ensure
+ req.request_method = "HEAD"
+ end
+ else
+ head_routes
+ end
+ end
+
+ def match_routes(routes, req)
+ routes.select { |r| r.matches?(req) }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
new file mode 100644
index 0000000000..df3f79a407
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class Utils # :nodoc:
+ # Normalizes URI path.
+ #
+ # Strips off trailing slash and ensures there is a leading slash.
+ # Also converts downcase URL encoded string to uppercase.
+ #
+ # normalize_path("/foo") # => "/foo"
+ # normalize_path("/foo/") # => "/foo"
+ # normalize_path("foo") # => "/foo"
+ # normalize_path("") # => "/"
+ # normalize_path("/%ab") # => "/%AB"
+ def self.normalize_path(path)
+ path ||= ""
+ encoding = path.encoding
+ path = "/#{path}".dup
+ path.squeeze!("/".freeze)
+ path.sub!(%r{/+\Z}, "".freeze)
+ path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
+ path = "/".dup if path == "".freeze
+ path.force_encoding(encoding)
+ path
+ end
+
+ # URI path and fragment escaping
+ # https://tools.ietf.org/html/rfc3986
+ class UriEncoder # :nodoc:
+ ENCODE = "%%%02X".freeze
+ US_ASCII = Encoding::US_ASCII
+ UTF_8 = Encoding::UTF_8
+ EMPTY = "".dup.force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
+
+ ALPHA = "a-zA-Z".freeze
+ DIGIT = "0-9".freeze
+ UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
+ SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze
+
+ ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
+
+ FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
+ SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
+ PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
+
+ def escape_fragment(fragment)
+ escape(fragment, FRAGMENT)
+ end
+
+ def escape_path(path)
+ escape(path, PATH)
+ end
+
+ def escape_segment(segment)
+ escape(segment, SEGMENT)
+ end
+
+ def unescape_uri(uri)
+ encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
+ uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
+ end
+
+ private
+ def escape(component, pattern)
+ component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
+ end
+
+ def percent_encode(unsafe)
+ safe = EMPTY.dup
+ unsafe.each_byte { |b| safe << DEC2HEX[b] }
+ safe
+ end
+ end
+
+ ENCODER = UriEncoder.new
+
+ def self.escape_path(path)
+ ENCODER.escape_path(path.to_s)
+ end
+
+ def self.escape_segment(segment)
+ ENCODER.escape_segment(segment.to_s)
+ end
+
+ def self.escape_fragment(fragment)
+ ENCODER.escape_fragment(fragment.to_s)
+ end
+
+ # Replaces any escaped sequences with their unescaped representations.
+ #
+ # uri = "/topics?title=Ruby%20on%20Rails"
+ # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
+ def self.unescape_uri(uri)
+ ENCODER.unescape_uri(uri)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
new file mode 100644
index 0000000000..639c063495
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Journey # :nodoc:
+ # The Routing table. Contains all routes for a system. Routes can be
+ # added to the table by calling Routes#add_route.
+ class Routes # :nodoc:
+ include Enumerable
+
+ attr_reader :routes, :custom_routes, :anchored_routes
+
+ def initialize
+ @routes = []
+ @ast = nil
+ @anchored_routes = []
+ @custom_routes = []
+ @simulator = nil
+ end
+
+ def empty?
+ routes.empty?
+ end
+
+ def length
+ routes.length
+ end
+ alias :size :length
+
+ def last
+ routes.last
+ end
+
+ def each(&block)
+ routes.each(&block)
+ end
+
+ def clear
+ routes.clear
+ anchored_routes.clear
+ custom_routes.clear
+ end
+
+ def partition_route(route)
+ if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
+ anchored_routes << route
+ else
+ custom_routes << route
+ end
+ end
+
+ def ast
+ @ast ||= begin
+ asts = anchored_routes.map(&:ast)
+ Nodes::Or.new(asts) unless asts.empty?
+ end
+ end
+
+ def simulator
+ @simulator ||= begin
+ gtg = GTG::Builder.new(ast).transition_table
+ GTG::Simulator.new(gtg)
+ end
+ end
+
+ def add_route(name, mapping)
+ route = mapping.make_route name, routes.length
+ routes << route
+ partition_route(route)
+ clear_cache!
+ route
+ end
+
+ private
+
+ def clear_cache!
+ @ast = nil
+ @simulator = nil
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
new file mode 100644
index 0000000000..2a075862e9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "strscan"
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Scanner # :nodoc:
+ def initialize
+ @ss = nil
+ end
+
+ def scan_setup(str)
+ @ss = StringScanner.new(str)
+ end
+
+ def eos?
+ @ss.eos?
+ end
+
+ def pos
+ @ss.pos
+ end
+
+ def pre_match
+ @ss.pre_match
+ end
+
+ def next_token
+ return if @ss.eos?
+
+ until token = scan || @ss.eos?; end
+ token
+ end
+
+ private
+
+ # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
+ # see: https://bugs.ruby-lang.org/issues/13077
+ def dedup_scan(regex)
+ r = @ss.scan(regex)
+ r ? -r : nil
+ end
+
+ def scan
+ case
+ # /
+ when @ss.skip(/\//)
+ [:SLASH, "/"]
+ when @ss.skip(/\(/)
+ [:LPAREN, "("]
+ when @ss.skip(/\)/)
+ [:RPAREN, ")"]
+ when @ss.skip(/\|/)
+ [:OR, "|"]
+ when @ss.skip(/\./)
+ [:DOT, "."]
+ when text = dedup_scan(/:\w+/)
+ [:SYMBOL, text]
+ when text = dedup_scan(/\*\w+/)
+ [:STAR, text]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
+ text.tr! "\\", ""
+ [:LITERAL, -text]
+ # any char
+ when text = dedup_scan(/./)
+ [:LITERAL, text]
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
new file mode 100644
index 0000000000..3395471a85
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # :stopdoc:
+ module Journey
+ class Format
+ ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
+ ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
+
+ Parameter = Struct.new(:name, :escaper) do
+ def escape(value); escaper.call value; end
+ end
+
+ def self.required_path(symbol)
+ Parameter.new symbol, ESCAPE_PATH
+ end
+
+ def self.required_segment(symbol)
+ Parameter.new symbol, ESCAPE_SEGMENT
+ end
+
+ def initialize(parts)
+ @parts = parts
+ @children = []
+ @parameters = []
+
+ parts.each_with_index do |object, i|
+ case object
+ when Journey::Format
+ @children << i
+ when Parameter
+ @parameters << i
+ end
+ end
+ end
+
+ def evaluate(hash)
+ parts = @parts.dup
+
+ @parameters.each do |index|
+ param = parts[index]
+ value = hash[param.name]
+ return "".freeze unless value
+ parts[index] = param.escape value
+ end
+
+ @children.each { |index| parts[index] = parts[index].evaluate(hash) }
+
+ parts.join
+ end
+ end
+
+ module Visitors # :nodoc:
+ class Visitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ def accept(node)
+ visit(node)
+ end
+
+ private
+
+ def visit(node)
+ send(DISPATCH_CACHE[node.type], node)
+ end
+
+ def binary(node)
+ visit(node.left)
+ visit(node.right)
+ end
+ def visit_CAT(n); binary(n); end
+
+ def nary(node)
+ node.children.each { |c| visit(c) }
+ end
+ def visit_OR(n); nary(n); end
+
+ def unary(node)
+ visit(node.left)
+ end
+ def visit_GROUP(n); unary(n); end
+ def visit_STAR(n); unary(n); end
+
+ def terminal(node); end
+ def visit_LITERAL(n); terminal(n); end
+ def visit_SYMBOL(n); terminal(n); end
+ def visit_SLASH(n); terminal(n); end
+ def visit_DOT(n); terminal(n); end
+
+ private_instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
+ class FunctionalVisitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ def accept(node, seed)
+ visit(node, seed)
+ end
+
+ def visit(node, seed)
+ send(DISPATCH_CACHE[node.type], node, seed)
+ end
+
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
+ def visit_CAT(n, seed); binary(n, seed); end
+
+ def nary(node, seed)
+ node.children.inject(seed) { |s, c| visit(c, s) }
+ end
+ def visit_OR(n, seed); nary(n, seed); end
+
+ def unary(node, seed)
+ visit(node.left, seed)
+ end
+ def visit_GROUP(n, seed); unary(n, seed); end
+ def visit_STAR(n, seed); unary(n, seed); end
+
+ def terminal(node, seed); seed; end
+ def visit_LITERAL(n, seed); terminal(n, seed); end
+ def visit_SYMBOL(n, seed); terminal(n, seed); end
+ def visit_SLASH(n, seed); terminal(n, seed); end
+ def visit_DOT(n, seed); terminal(n, seed); end
+
+ instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
+ class FormatBuilder < Visitor # :nodoc:
+ def accept(node); Journey::Format.new(super); end
+ def terminal(node); [node.left]; end
+
+ def binary(node)
+ visit(node.left) + visit(node.right)
+ end
+
+ def visit_GROUP(n); [Journey::Format.new(unary(n))]; end
+
+ def visit_STAR(n)
+ [Journey::Format.required_path(n.left.to_sym)]
+ end
+
+ def visit_SYMBOL(n)
+ symbol = n.to_sym
+ if symbol == :controller
+ [Journey::Format.required_path(symbol)]
+ else
+ [Journey::Format.required_segment(symbol)]
+ end
+ end
+ end
+
+ # Loop through the requirements AST.
+ class Each < FunctionalVisitor # :nodoc:
+ def visit(node, block)
+ block.call(node)
+ super
+ end
+
+ INSTANCE = new
+ end
+
+ class String < FunctionalVisitor # :nodoc:
+ private
+
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
+
+ def nary(node, seed)
+ last_child = node.children.last
+ node.children.inject(seed) { |s, c|
+ string = visit(c, s)
+ string << "|" unless last_child == c
+ string
+ }
+ end
+
+ def terminal(node, seed)
+ seed + node.left
+ end
+
+ def visit_GROUP(node, seed)
+ visit(node.left, seed.dup << "(") << ")"
+ end
+
+ INSTANCE = new
+ end
+
+ class Dot < FunctionalVisitor # :nodoc:
+ def initialize
+ @nodes = []
+ @edges = []
+ end
+
+ def accept(node, seed = [[], []])
+ super
+ nodes, edges = seed
+ <<-eodot
+ digraph parse_tree {
+ size="8,5"
+ node [shape = none];
+ edge [dir = none];
+ #{nodes.join "\n"}
+ #{edges.join("\n")}
+ }
+ eodot
+ end
+
+ private
+
+ def binary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
+ super
+ end
+
+ def nary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
+ super
+ end
+
+ def unary(node, seed)
+ seed.last << "#{node.object_id} -> #{node.left.object_id};"
+ super
+ end
+
+ def visit_GROUP(node, seed)
+ seed.first << "#{node.object_id} [label=\"()\"];"
+ super
+ end
+
+ def visit_CAT(node, seed)
+ seed.first << "#{node.object_id} [label=\"○\"];"
+ super
+ end
+
+ def visit_STAR(node, seed)
+ seed.first << "#{node.object_id} [label=\"*\"];"
+ super
+ end
+
+ def visit_OR(node, seed)
+ seed.first << "#{node.object_id} [label=\"|\"];"
+ super
+ end
+
+ def terminal(node, seed)
+ value = node.left
+
+ seed.first << "#{node.object_id} [label=\"#{value}\"];"
+ seed
+ end
+ INSTANCE = new
+ end
+ end
+ end
+ # :startdoc:
+end
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
new file mode 100644
index 0000000000..403e16a7bb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
@@ -0,0 +1,30 @@
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif;
+ margin: 0;
+}
+
+h1 {
+ font-size: 2.0em; font-weight: bold; text-align: center;
+ color: white; background-color: black;
+ padding: 5px 0;
+ margin: 0 0 20px;
+}
+
+h2 {
+ text-align: center;
+ display: none;
+ font-size: 0.5em;
+}
+
+.clearfix {display: inline-block; }
+.input { overflow: show;}
+.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
+.instruction p { padding: 0 0 5px; }
+.instruction li { padding: 0 10px 5px; }
+
+.form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px}
+.form p, .form form { text-align: center }
+.form form {padding: 0 10px 5px; }
+.form .fun_routes { font-size: 0.9em;}
+.form .fun_routes a { margin: 0 5px 0 0; }
+
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.js b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js
new file mode 100644
index 0000000000..d9bcaef928
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js
@@ -0,0 +1,134 @@
+function tokenize(input, callback) {
+ while(input.length > 0) {
+ callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]);
+ input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, '');
+ }
+}
+
+var graph = d3.select("#chart-2 svg");
+var svg_edges = {};
+var svg_nodes = {};
+
+graph.selectAll("g.edge").each(function() {
+ var node = d3.select(this);
+ var index = node.select("title").text().split("->");
+ var left = parseInt(index[0]);
+ var right = parseInt(index[1]);
+
+ if(!svg_edges[left]) { svg_edges[left] = {} }
+ svg_edges[left][right] = node;
+});
+
+graph.selectAll("g.node").each(function() {
+ var node = d3.select(this);
+ var index = parseInt(node.select("title").text());
+ svg_nodes[index] = node;
+});
+
+function reset_graph() {
+ for(var key in svg_edges) {
+ for(var mkey in svg_edges[key]) {
+ var node = svg_edges[key][mkey];
+ var path = node.select("path");
+ var arrow = node.select("polygon");
+ path.style("stroke", "black");
+ arrow.style("stroke", "black").style("fill", "black");
+ }
+ }
+
+ for(var key in svg_nodes) {
+ var node = svg_nodes[key];
+ node.select('ellipse').style("fill", "white");
+ node.select('polygon').style("fill", "white");
+ }
+ return false;
+}
+
+function highlight_edge(from, to) {
+ var node = svg_edges[from][to];
+ var path = node.select("path");
+ var arrow = node.select("polygon");
+
+ path
+ .transition().duration(500)
+ .style("stroke", "green");
+
+ arrow
+ .transition().duration(500)
+ .style("stroke", "green").style("fill", "green");
+}
+
+function highlight_state(index, color) {
+ if(!color) { color = "green"; }
+
+ svg_nodes[index].select('ellipse')
+ .style("fill", "white")
+ .transition().duration(500)
+ .style("fill", color);
+}
+
+function highlight_finish(index) {
+ svg_nodes[index].select('polygon')
+ .style("fill", "while")
+ .transition().duration(500)
+ .style("fill", "blue");
+}
+
+function match(input) {
+ reset_graph();
+ var table = tt();
+ var states = [0];
+ var regexp_states = table['regexp_states'];
+ var string_states = table['string_states'];
+ var accepting = table['accepting'];
+
+ highlight_state(0);
+
+ tokenize(input, function(token) {
+ var new_states = [];
+ for(var key in states) {
+ var state = states[key];
+
+ if(string_states[state] && string_states[state][token]) {
+ var new_state = string_states[state][token];
+ highlight_edge(state, new_state);
+ highlight_state(new_state);
+ new_states.push(new_state);
+ }
+
+ if(regexp_states[state]) {
+ for(var key in regexp_states[state]) {
+ var re = new RegExp("^" + key + "$");
+ if(re.test(token)) {
+ var new_state = regexp_states[state][key];
+ highlight_edge(state, new_state);
+ highlight_state(new_state);
+ new_states.push(new_state);
+ }
+ }
+ }
+ }
+
+ if(new_states.length == 0) {
+ return;
+ }
+ states = new_states;
+ });
+
+ for(var key in states) {
+ var state = states[key];
+ if(accepting[state]) {
+ for(var mkey in svg_edges[state]) {
+ if(!regexp_states[mkey] && !string_states[mkey]) {
+ highlight_edge(state, mkey);
+ highlight_finish(mkey);
+ }
+ }
+ } else {
+ highlight_state(state, "red");
+ }
+ }
+
+ return false;
+}
+
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
new file mode 100644
index 0000000000..9b28a65200
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= title %></title>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css">
+ <style>
+ <% stylesheets.each do |style| %>
+ <%= style %>
+ <% end %>
+ </style>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script>
+ </head>
+ <body>
+ <div id="wrapper">
+ <h1>Routes FSM with NFA simulation</h1>
+ <div class="instruction form">
+ <p>
+ Type a route in to the box and click "simulate".
+ </p>
+ <form onsubmit="return match(this.route.value);">
+ <input type="text" size="30" name="route" value="/articles/new" />
+ <button>simulate</button>
+ <input type="reset" value="reset" onclick="return reset_graph();"/>
+ </form>
+ <p class="fun_routes">
+ Some fun routes to try:
+ <% fun_routes.each do |path| %>
+ <a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));">
+ <%= path %>
+ </a>
+ <% end %>
+ </p>
+ </div>
+ <div class='chart' id='chart-2'>
+ <%= svg %>
+ </div>
+ <div class="instruction">
+ <p>
+ This is a FSM for a system that has the following routes:
+ </p>
+ <ul>
+ <% paths.each do |route| %>
+ <li><%= route %></li>
+ <% end %>
+ </ul>
+ </div>
+ </div>
+ <% javascripts.each do |js| %>
+ <script><%= js %></script>
+ <% end %>
+ </body>
+</html>
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
new file mode 100644
index 0000000000..5b2ad36dd5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # Provides callbacks to be executed before and after dispatching the request.
+ class Callbacks
+ include ActiveSupport::Callbacks
+
+ define_callbacks :call
+
+ class << self
+ def before(*args, &block)
+ set_callback(:call, :before, *args, &block)
+ end
+
+ def after(*args, &block)
+ set_callback(:call, :after, *args, &block)
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ error = nil
+ result = run_callbacks :call do
+ begin
+ @app.call(env)
+ rescue => error
+ end
+ end
+ raise error if error
+ result
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
new file mode 100644
index 0000000000..c45d947904
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -0,0 +1,681 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "active_support/key_generator"
+require "active_support/message_verifier"
+require "active_support/json"
+require "rack/utils"
+
+module ActionDispatch
+ class Request
+ def cookie_jar
+ fetch_header("action_dispatch.cookies".freeze) do
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
+ end
+ end
+
+ # :stopdoc:
+ prepend Module.new {
+ def commit_cookie_jar!
+ cookie_jar.commit!
+ end
+ }
+
+ def have_cookie_jar?
+ has_header? "action_dispatch.cookies".freeze
+ end
+
+ def cookie_jar=(jar)
+ set_header "action_dispatch.cookies".freeze, jar
+ end
+
+ def key_generator
+ get_header Cookies::GENERATOR_KEY
+ end
+
+ def signed_cookie_salt
+ get_header Cookies::SIGNED_COOKIE_SALT
+ end
+
+ def encrypted_cookie_salt
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
+ end
+
+ def encrypted_signed_cookie_salt
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
+ end
+
+ def authenticated_encrypted_cookie_salt
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
+ end
+
+ def use_authenticated_cookie_encryption
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
+ end
+
+ def encrypted_cookie_cipher
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
+ end
+
+ def signed_cookie_digest
+ get_header Cookies::SIGNED_COOKIE_DIGEST
+ end
+
+ def secret_token
+ get_header Cookies::SECRET_TOKEN
+ end
+
+ def secret_key_base
+ get_header Cookies::SECRET_KEY_BASE
+ end
+
+ def cookies_serializer
+ get_header Cookies::COOKIES_SERIALIZER
+ end
+
+ def cookies_digest
+ get_header Cookies::COOKIES_DIGEST
+ end
+
+ def cookies_rotations
+ get_header Cookies::COOKIES_ROTATIONS
+ end
+
+ # :startdoc:
+ end
+
+ # \Cookies are read and written through ActionController#cookies.
+ #
+ # The cookies being read are the ones received along with the request, the cookies
+ # being written will be sent out with the response. Reading a cookie does not get
+ # the cookie object itself back, just the value it holds.
+ #
+ # Examples of writing:
+ #
+ # # Sets a simple session cookie.
+ # # This cookie will be deleted when the user's browser is closed.
+ # cookies[:user_name] = "david"
+ #
+ # # Cookie values are String based. Other data types need to be serialized.
+ # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
+ #
+ # # Sets a cookie that expires in 1 hour.
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
+ #
+ # # Sets a cookie that expires at a specific time.
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
+ #
+ # # Sets a signed cookie, which prevents users from tampering with its value.
+ # # It can be read using the signed method `cookies.signed[:name]`
+ # cookies.signed[:user_id] = current_user.id
+ #
+ # # Sets an encrypted cookie value before sending it to the client which
+ # # prevent users from reading and tampering with its value.
+ # # It can be read using the encrypted method `cookies.encrypted[:name]`
+ # cookies.encrypted[:discount] = 45
+ #
+ # # Sets a "permanent" cookie (which expires in 20 years from now).
+ # cookies.permanent[:login] = "XJ-122"
+ #
+ # # You can also chain these methods:
+ # cookies.signed.permanent[:login] = "XJ-122"
+ #
+ # Examples of reading:
+ #
+ # cookies[:user_name] # => "david"
+ # cookies.size # => 2
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
+ # cookies.signed[:login] # => "XJ-122"
+ # cookies.encrypted[:discount] # => 45
+ #
+ # Example for deleting:
+ #
+ # cookies.delete :user_name
+ #
+ # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
+ #
+ # cookies[:name] = {
+ # value: 'a yummy cookie',
+ # expires: 1.year,
+ # domain: 'domain.com'
+ # }
+ #
+ # cookies.delete(:name, domain: 'domain.com')
+ #
+ # The option symbols for setting cookies are:
+ #
+ # * <tt>:value</tt> - The cookie's value.
+ # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
+ # of the application.
+ # * <tt>:domain</tt> - The domain for which this cookie applies so you can
+ # restrict to the domain level. If you use a schema like www.example.com
+ # and want to share session with user.example.com set <tt>:domain</tt>
+ # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
+ # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
+ #
+ # domain: nil # Does not set cookie domain. (default)
+ # domain: :all # Allow the cookie for the top most level
+ # # domain and subdomains.
+ # domain: %w(.example.com .example.org) # Allow the cookie
+ # # for concrete domain names.
+ #
+ # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
+ # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
+ # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
+ # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
+ # Default is +false+.
+ # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
+ # only HTTP. Defaults to +false+.
+ class Cookies
+ HTTP_HEADER = "Set-Cookie".freeze
+ GENERATOR_KEY = "action_dispatch.key_generator".freeze
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
+ COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
+
+ # Cookies can typically store 4096 bytes.
+ MAX_COOKIE_SIZE = 4096
+
+ # Raised when storing more than 4K of session data.
+ CookieOverflow = Class.new StandardError
+
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
+ module ChainedCookieJars
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
+ #
+ # cookies.permanent[:prefers_open_id] = true
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ #
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
+ #
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
+ #
+ # cookies.permanent.signed[:remember_me] = current_user.id
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ def permanent
+ @permanent ||= PermanentCookieJar.new(self)
+ end
+
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
+ # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
+ #
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
+ # legacy cookies signed with the old key generator will be transparently upgraded.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
+ #
+ # Example:
+ #
+ # cookies.signed[:discount] = 45
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
+ #
+ # cookies.signed[:discount] # => 45
+ def signed
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
+ end
+
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
+ # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
+ #
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
+ # legacy cookies signed with the old key generator will be transparently upgraded.
+ #
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
+ #
+ # Example:
+ #
+ # cookies.encrypted[:discount] = 45
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
+ #
+ # cookies.encrypted[:discount] # => 45
+ def encrypted
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
+ end
+
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
+ def signed_or_encrypted
+ @signed_or_encrypted ||=
+ if request.secret_key_base.present?
+ encrypted
+ else
+ signed
+ end
+ end
+
+ private
+
+ def upgrade_legacy_signed_cookies?
+ request.secret_token.present? && request.secret_key_base.present?
+ end
+
+ def upgrade_legacy_hmac_aes_cbc_cookies?
+ request.secret_key_base.present? &&
+ request.encrypted_signed_cookie_salt.present? &&
+ request.encrypted_cookie_salt.present? &&
+ request.use_authenticated_cookie_encryption
+ end
+
+ def encrypted_cookie_cipher
+ request.encrypted_cookie_cipher || "aes-256-gcm"
+ end
+
+ def signed_cookie_digest
+ request.signed_cookie_digest || "SHA1"
+ end
+ end
+
+ class CookieJar #:nodoc:
+ include Enumerable, ChainedCookieJars
+
+ # This regular expression is used to split the levels of a domain.
+ # The top level domain can be any string without a period or
+ # **.**, ***.** style TLDs like co.uk or com.au
+ #
+ # www.example.co.uk gives:
+ # $& => example.co.uk
+ #
+ # example.com gives:
+ # $& => example.com
+ #
+ # lots.of.subdomains.example.local gives:
+ # $& => example.local
+ DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
+
+ def self.build(req, cookies)
+ new(req).tap do |hash|
+ hash.update(cookies)
+ end
+ end
+
+ attr_reader :request
+
+ def initialize(request)
+ @set_cookies = {}
+ @delete_cookies = {}
+ @request = request
+ @cookies = {}
+ @committed = false
+ end
+
+ def committed?; @committed; end
+
+ def commit!
+ @committed = true
+ @set_cookies.freeze
+ @delete_cookies.freeze
+ end
+
+ def each(&block)
+ @cookies.each(&block)
+ end
+
+ # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
+ def [](name)
+ @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
+ alias :has_key? :key?
+
+ # Returns the cookies as Hash.
+ alias :to_hash :to_h
+
+ def update(other_hash)
+ @cookies.update other_hash.stringify_keys
+ self
+ end
+
+ def update_cookies_from_jar
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
+
+ @cookies.update set_cookies if set_cookies
+ end
+
+ def to_header
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
+ end
+
+ def handle_options(options) # :nodoc:
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
+ options[:path] ||= "/"
+
+ if options[:domain] == :all || options[:domain] == "all"
+ # If there is a provided tld length then we use it otherwise default domain regexp.
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
+
+ # If host is not ip and matches domain regexp.
+ # (ip confirms to domain regexp so we explicitly check for ip)
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
+ ".#{$&}"
+ end
+ elsif options[:domain].is_a? Array
+ # If host matches one of the supplied domains without a dot in front of it.
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
+ end
+ end
+
+ # Sets the cookie named +name+. The second argument may be the cookie's
+ # value or a hash of options as documented above.
+ def []=(name, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ value = options[:value]
+ else
+ value = options
+ options = { value: value }
+ end
+
+ handle_options(options)
+
+ if @cookies[name.to_s] != value || options[:expires]
+ @cookies[name.to_s] = value
+ @set_cookies[name.to_s] = options
+ @delete_cookies.delete(name.to_s)
+ end
+
+ value
+ end
+
+ # Removes the cookie on the client machine by setting the value to an empty string
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
+ # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
+ def delete(name, options = {})
+ return unless @cookies.has_key? name.to_s
+
+ options.symbolize_keys!
+ handle_options(options)
+
+ value = @cookies.delete(name.to_s)
+ @delete_cookies[name.to_s] = options
+ value
+ end
+
+ # Whether the given cookie is to be deleted by this CookieJar.
+ # Like <tt>[]=</tt>, you can pass in an options hash to test if a
+ # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
+ def deleted?(name, options = {})
+ options.symbolize_keys!
+ handle_options(options)
+ @delete_cookies[name.to_s] == options
+ end
+
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
+ def clear(options = {})
+ @cookies.each_key { |k| delete(k, options) }
+ end
+
+ def write(headers)
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
+ headers[HTTP_HEADER] = header
+ end
+ end
+
+ mattr_accessor :always_write_cookie, default: false
+
+ private
+
+ def escape(string)
+ ::Rack::Utils.escape(string)
+ end
+
+ def make_set_cookie_header(header)
+ header = @set_cookies.inject(header) { |m, (k, v)|
+ if write_cookie?(v)
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
+ else
+ m
+ end
+ }
+ @delete_cookies.inject(header) { |m, (k, v)|
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
+ }
+ end
+
+ def write_cookie?(cookie)
+ request.ssl? || !cookie[:secure] || always_write_cookie
+ end
+ end
+
+ class AbstractCookieJar # :nodoc:
+ include ChainedCookieJars
+
+ def initialize(parent_jar)
+ @parent_jar = parent_jar
+ end
+
+ def [](name)
+ if data = @parent_jar[name.to_s]
+ parse name, data
+ end
+ end
+
+ def []=(name, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ else
+ options = { value: options }
+ end
+
+ commit(options)
+ @parent_jar[name] = options
+ end
+
+ protected
+ def request; @parent_jar.request; end
+
+ private
+ def expiry_options(options)
+ if options[:expires].respond_to?(:from_now)
+ { expires_in: options[:expires] }
+ else
+ { expires_at: options[:expires] }
+ end
+ end
+
+ def parse(name, data); data; end
+ def commit(options); end
+ end
+
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
+ private
+ def commit(options)
+ options[:expires] = 20.years.from_now
+ end
+ end
+
+ class JsonSerializer # :nodoc:
+ def self.load(value)
+ ActiveSupport::JSON.decode(value)
+ end
+
+ def self.dump(value)
+ ActiveSupport::JSON.encode(value)
+ end
+ end
+
+ module SerializedCookieJars # :nodoc:
+ MARSHAL_SIGNATURE = "\x04\x08".freeze
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
+
+ protected
+ def needs_migration?(value)
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ end
+
+ def serialize(value)
+ serializer.dump(value)
+ end
+
+ def deserialize(name)
+ rotate = false
+ value = yield -> { rotate = true }
+
+ if value
+ case
+ when needs_migration?(value)
+ self[name] = Marshal.load(value)
+ when rotate
+ self[name] = serializer.load(value)
+ else
+ serializer.load(value)
+ end
+ end
+ end
+
+ def serializer
+ serializer = request.cookies_serializer || :marshal
+ case serializer
+ when :marshal
+ Marshal
+ when :json, :hybrid
+ JsonSerializer
+ else
+ serializer
+ end
+ end
+
+ def digest
+ request.cookies_digest || "SHA1"
+ end
+ end
+
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
+ include SerializedCookieJars
+
+ def initialize(parent_jar)
+ super
+
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
+
+ request.cookies_rotations.signed.each do |*secrets, **options|
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
+
+ if upgrade_legacy_signed_cookies?
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
+ end
+ end
+
+ private
+ def parse(name, signed_message)
+ deserialize(name) do |rotate|
+ @verifier.verified(signed_message, on_rotation: rotate)
+ end
+ end
+
+ def commit(options)
+ options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
+
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
+ end
+
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
+ include SerializedCookieJars
+
+ def initialize(parent_jar)
+ super
+
+ if request.use_authenticated_cookie_encryption
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
+ else
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
+ end
+
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
+
+ if upgrade_legacy_hmac_aes_cbc_cookies?
+ legacy_cipher = "aes-256-cbc"
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
+ end
+
+ if upgrade_legacy_signed_cookies?
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
+ end
+ end
+
+ private
+ def parse(name, encrypted_message)
+ deserialize(name) do |rotate|
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
+ end
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
+ parse_legacy_signed_message(name, encrypted_message)
+ end
+
+ def commit(options)
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
+
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
+
+ def parse_legacy_signed_message(name, legacy_signed_message)
+ if defined?(@legacy_verifier)
+ deserialize(name) do |rotate|
+ rotate.call
+
+ @legacy_verifier.verified(legacy_signed_message)
+ end
+ end
+ end
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+
+ status, headers, body = @app.call(env)
+
+ if request.have_cookie_jar?
+ cookie_jar = request.cookie_jar
+ unless cookie_jar.committed?
+ cookie_jar.write(headers)
+ if headers[HTTP_HEADER].respond_to?(:join)
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ end
+ end
+ end
+
+ [status, headers, body]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
new file mode 100644
index 0000000000..511306eb0e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
+require "action_dispatch/routing/inspector"
+require "action_view"
+require "action_view/base"
+
+require "pp"
+
+module ActionDispatch
+ # This middleware is responsible for logging exceptions and
+ # showing a debugging page in case the request is local.
+ class DebugExceptions
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
+
+ class DebugView < ActionView::Base
+ def debug_params(params)
+ clean_params = params.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ if clean_params.empty?
+ "None"
+ else
+ PP.pp(clean_params, "".dup, 200)
+ end
+ end
+
+ def debug_headers(headers)
+ if headers.present?
+ headers.inspect.gsub(",", ",\n")
+ else
+ "None"
+ end
+ end
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end
+
+ def render(*)
+ logger = ActionView::Base.logger
+
+ if logger && logger.respond_to?(:silence)
+ logger.silence { super }
+ else
+ super
+ end
+ end
+ end
+
+ def initialize(app, routes_app = nil, response_format = :default)
+ @app = app
+ @routes_app = routes_app
+ @response_format = response_format
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ _, headers, body = response = @app.call(env)
+
+ if headers["X-Cascade"] == "pass"
+ body.close if body.respond_to?(:close)
+ raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
+ end
+
+ response
+ rescue Exception => exception
+ raise exception unless request.show_exceptions?
+ render_exception(request, exception)
+ end
+
+ private
+
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ log_error(request, wrapper)
+
+ if request.get_header("action_dispatch.show_detailed_exceptions")
+ content_type = request.formats.first
+
+ if api_request?(content_type)
+ render_for_api_request(content_type, wrapper)
+ else
+ render_for_browser_request(request, wrapper)
+ end
+ else
+ raise exception
+ end
+ end
+
+ def render_for_browser_request(request, wrapper)
+ template = create_template(request, wrapper)
+ file = "rescues/#{wrapper.rescue_template}"
+
+ if request.xhr?
+ body = template.render(template: file, layout: false, formats: [:text])
+ format = "text/plain"
+ else
+ body = template.render(template: file, layout: "rescues/layout")
+ format = "text/html"
+ end
+ render(wrapper.status_code, body, format)
+ end
+
+ def render_for_api_request(content_type, wrapper)
+ body = {
+ status: wrapper.status_code,
+ error: Rack::Utils::HTTP_STATUS_CODES.fetch(
+ wrapper.status_code,
+ Rack::Utils::HTTP_STATUS_CODES[500]
+ ),
+ exception: wrapper.exception.inspect,
+ traces: wrapper.traces
+ }
+
+ to_format = "to_#{content_type.to_sym}"
+
+ if content_type && body.respond_to?(to_format)
+ formatted_body = body.public_send(to_format)
+ format = content_type
+ else
+ formatted_body = body.to_json
+ format = Mime[:json]
+ end
+
+ render(wrapper.status_code, formatted_body, format)
+ end
+
+ def create_template(request, wrapper)
+ traces = wrapper.traces
+
+ trace_to_show = "Application Trace"
+ if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error"
+ trace_to_show = "Full Trace"
+ end
+
+ if source_to_show = traces[trace_to_show].first
+ source_to_show_id = source_to_show[:id]
+ end
+
+ DebugView.new([RESCUES_TEMPLATE_PATH],
+ request: request,
+ exception: wrapper.exception,
+ traces: traces,
+ show_source_idx: source_to_show_id,
+ trace_to_show: trace_to_show,
+ routes_inspector: routes_inspector(wrapper.exception),
+ source_extracts: wrapper.source_extracts,
+ line_number: wrapper.line_number,
+ file: wrapper.file
+ )
+ end
+
+ def render(status, body, format)
+ [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
+ end
+
+ def log_error(request, wrapper)
+ logger = logger(request)
+ return unless logger
+
+ exception = wrapper.exception
+
+ trace = wrapper.application_trace
+ trace = wrapper.framework_trace if trace.empty?
+
+ ActiveSupport::Deprecation.silence do
+ logger.fatal " "
+ logger.fatal "#{exception.class} (#{exception.message}):"
+ log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
+ logger.fatal " "
+ log_array logger, trace
+ end
+ end
+
+ def log_array(logger, array)
+ if logger.formatter && logger.formatter.respond_to?(:tags_text)
+ logger.fatal array.join("\n#{logger.formatter.tags_text}")
+ else
+ logger.fatal array.join("\n")
+ end
+ end
+
+ def logger(request)
+ request.logger || ActionView::Base.logger || stderr_logger
+ end
+
+ def stderr_logger
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
+ end
+
+ def routes_inspector(exception)
+ if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
+ ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
+ end
+ end
+
+ def api_request?(content_type)
+ @response_format == :api && !content_type.html?
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
new file mode 100644
index 0000000000..03760438f7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # This middleware can be used to diagnose deadlocks in the autoload interlock.
+ #
+ # To use it, insert it near the top of the middleware stack, using
+ # <tt>config/application.rb</tt>:
+ #
+ # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
+ #
+ # After restarting the application and re-triggering the deadlock condition,
+ # <tt>/rails/locks</tt> will show a summary of all threads currently known to
+ # the interlock, which lock level they are holding or awaiting, and their
+ # current backtrace.
+ #
+ # Generally a deadlock will be caused by the interlock conflicting with some
+ # other external lock or blocking I/O call. These cannot be automatically
+ # identified, but should be visible in the displayed backtraces.
+ #
+ # NOTE: The formatting and content of this middleware's output is intended for
+ # human consumption, and should be expected to change between releases.
+ #
+ # This middleware exposes operational details of the server, with no access
+ # control. It should only be enabled when in use, and removed thereafter.
+ class DebugLocks
+ def initialize(app, path = "/rails/locks")
+ @app = app
+ @path = path
+ end
+
+ def call(env)
+ req = ActionDispatch::Request.new env
+
+ if req.get?
+ path = req.path_info.chomp("/".freeze)
+ if path == @path
+ return render_details(req)
+ end
+ end
+
+ @app.call(env)
+ end
+
+ private
+ def render_details(req)
+ threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads|
+ # The Interlock itself comes to a complete halt as long as this block
+ # is executing. That gives us a more consistent picture of everything,
+ # but creates a pretty strong Observer Effect.
+ #
+ # Most directly, that means we need to do as little as possible in
+ # this block. More widely, it means this middleware should remain a
+ # strictly diagnostic tool (to be used when something has gone wrong),
+ # and not for any sort of general monitoring.
+
+ raw_threads.each.with_index do |(thread, info), idx|
+ info[:index] = idx
+ info[:backtrace] = thread.backtrace
+ end
+
+ raw_threads
+ end
+
+ str = threads.map do |thread, info|
+ if info[:exclusive]
+ lock_state = "Exclusive".dup
+ elsif info[:sharing] > 0
+ lock_state = "Sharing".dup
+ lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
+ else
+ lock_state = "No lock".dup
+ end
+
+ if info[:waiting]
+ lock_state << " (yielded share)"
+ end
+
+ msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup
+
+ if info[:sleeper]
+ msg << " Waiting in #{info[:sleeper]}"
+ msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil?
+ msg << "\n"
+
+ if info[:compatible]
+ compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect }
+ msg << " may be pre-empted for: #{compat.join(', ')}\n"
+ end
+
+ blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) }
+ msg << " blocked by: #{blockers.map { |i| i[:index] }.join(', ')}\n" if blockers.any?
+ end
+
+ blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) }
+ msg << " blocking: #{blockees.map { |i| i[:index] }.join(', ')}\n" if blockees.any?
+
+ msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
+ end.join("\n\n---\n\n\n")
+
+ [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
+ end
+
+ def blocked_by?(victim, blocker, all_threads)
+ return false if victim.equal?(blocker)
+
+ case victim[:sleeper]
+ when :start_sharing
+ blocker[:exclusive] ||
+ (!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false))
+ when :start_exclusive
+ blocker[:sharing] > 0 ||
+ blocker[:exclusive] ||
+ (blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose]))
+ when :yield_shares
+ blocker[:exclusive]
+ when :stop_exclusive
+ blocker[:exclusive] ||
+ victim[:compatible] &&
+ victim[:compatible].include?(blocker[:purpose]) &&
+ all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
new file mode 100644
index 0000000000..d1b4508378
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "rack/utils"
+
+module ActionDispatch
+ class ExceptionWrapper
+ cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
+ "ActionController::RoutingError" => :not_found,
+ "AbstractController::ActionNotFound" => :not_found,
+ "ActionController::MethodNotAllowed" => :method_not_allowed,
+ "ActionController::UnknownHttpMethod" => :method_not_allowed,
+ "ActionController::NotImplemented" => :not_implemented,
+ "ActionController::UnknownFormat" => :not_acceptable,
+ "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
+ "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
+ "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
+ "ActionController::BadRequest" => :bad_request,
+ "ActionController::ParameterMissing" => :bad_request,
+ "Rack::QueryParser::ParameterTypeError" => :bad_request,
+ "Rack::QueryParser::InvalidParameterError" => :bad_request
+ )
+
+ cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
+ "ActionView::MissingTemplate" => "missing_template",
+ "ActionController::RoutingError" => "routing_error",
+ "AbstractController::ActionNotFound" => "unknown_action",
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
+ "ActionView::Template::Error" => "template_error"
+ )
+
+ attr_reader :backtrace_cleaner, :exception, :line_number, :file
+
+ def initialize(backtrace_cleaner, exception)
+ @backtrace_cleaner = backtrace_cleaner
+ @exception = original_exception(exception)
+
+ expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
+ end
+
+ def rescue_template
+ @@rescue_templates[@exception.class.name]
+ end
+
+ def status_code
+ self.class.status_code_for_exception(@exception.class.name)
+ end
+
+ def application_trace
+ clean_backtrace(:silent)
+ end
+
+ def framework_trace
+ clean_backtrace(:noise)
+ end
+
+ def full_trace
+ clean_backtrace(:all)
+ end
+
+ def traces
+ application_trace_with_ids = []
+ framework_trace_with_ids = []
+ full_trace_with_ids = []
+
+ full_trace.each_with_index do |trace, idx|
+ trace_with_id = { id: idx, trace: trace }
+
+ if application_trace.include?(trace)
+ application_trace_with_ids << trace_with_id
+ else
+ framework_trace_with_ids << trace_with_id
+ end
+
+ full_trace_with_ids << trace_with_id
+ end
+
+ {
+ "Application Trace" => application_trace_with_ids,
+ "Framework Trace" => framework_trace_with_ids,
+ "Full Trace" => full_trace_with_ids
+ }
+ end
+
+ def self.status_code_for_exception(class_name)
+ Rack::Utils.status_code(@@rescue_responses[class_name])
+ end
+
+ def source_extracts
+ backtrace.map do |trace|
+ file, line_number = extract_file_and_line_number(trace)
+
+ {
+ code: source_fragment(file, line_number),
+ line_number: line_number
+ }
+ end
+ end
+
+ private
+
+ def backtrace
+ Array(@exception.backtrace)
+ end
+
+ def original_exception(exception)
+ if @@rescue_responses.has_key?(exception.cause.class.name)
+ exception.cause
+ else
+ exception
+ end
+ end
+
+ def clean_backtrace(*args)
+ if backtrace_cleaner
+ backtrace_cleaner.clean(backtrace, *args)
+ else
+ backtrace
+ end
+ end
+
+ def source_fragment(path, line)
+ return unless Rails.respond_to?(:root) && Rails.root
+ full_path = Rails.root.join(path)
+ if File.exist?(full_path)
+ File.open(full_path, "r") do |file|
+ start = [line - 3, 0].max
+ lines = file.each_line.drop(start).take(6)
+ Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
+ end
+ end
+ end
+
+ def extract_file_and_line_number(trace)
+ # Split by the first colon followed by some digits, which works for both
+ # Windows and Unix path styles.
+ file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
+ [file, line.to_i]
+ end
+
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb
new file mode 100644
index 0000000000..129b18d3d9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/executor.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require "rack/body_proxy"
+
+module ActionDispatch
+ class Executor
+ def initialize(app, executor)
+ @app, @executor = app, executor
+ end
+
+ def call(env)
+ state = @executor.run!
+ begin
+ response = @app.call(env)
+ returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
+ ensure
+ state.complete! unless returned
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
new file mode 100644
index 0000000000..3e11846778
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -0,0 +1,300 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+
+module ActionDispatch
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
+ # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
+ # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
+ # then expose the flash to its template. Actually, that exposure is automatically done.
+ #
+ # class PostsController < ActionController::Base
+ # def create
+ # # save post
+ # flash[:notice] = "Post successfully created"
+ # redirect_to @post
+ # end
+ #
+ # def show
+ # # doesn't need to assign the flash notice to the template, that's done automatically
+ # end
+ # end
+ #
+ # show.html.erb
+ # <% if flash[:notice] %>
+ # <div class="notice"><%= flash[:notice] %></div>
+ # <% end %>
+ #
+ # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
+ #
+ # flash.alert = "You must be logged in"
+ # flash.notice = "Post successfully created"
+ #
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
+ # use sanitize helper.
+ #
+ # Just remember: They'll be gone by the time the next action has been performed.
+ #
+ # See docs on the FlashHash class for more details about the flash.
+ class Flash
+ KEY = "action_dispatch.request.flash_hash".freeze
+
+ module RequestMethods
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
+ end
+
+ def flash=(flash)
+ set_header Flash::KEY, flash
+ end
+
+ def flash_hash # :nodoc:
+ get_header Flash::KEY
+ end
+
+ def commit_flash # :nodoc:
+ session = self.session || {}
+ flash_hash = self.flash_hash
+
+ if flash_hash && (flash_hash.present? || session.key?("flash"))
+ session["flash"] = flash_hash.to_session_value
+ self.flash = flash_hash.dup
+ end
+
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
+ session.key?("flash") && session["flash"].nil?
+ session.delete("flash")
+ end
+ end
+
+ def reset_session # :nodoc
+ super
+ self.flash = nil
+ end
+ end
+
+ class FlashNow #:nodoc:
+ attr_accessor :flash
+
+ def initialize(flash)
+ @flash = flash
+ end
+
+ def []=(k, v)
+ k = k.to_s
+ @flash[k] = v
+ @flash.discard(k)
+ v
+ end
+
+ def [](k)
+ @flash[k.to_s]
+ end
+
+ # Convenience accessor for <tt>flash.now[:alert]=</tt>.
+ def alert=(message)
+ self[:alert] = message
+ end
+
+ # Convenience accessor for <tt>flash.now[:notice]=</tt>.
+ def notice=(message)
+ self[:notice] = message
+ end
+ end
+
+ class FlashHash
+ include Enumerable
+
+ def self.from_session_value(value) #:nodoc:
+ case value
+ when FlashHash # Rails 3.1, 3.2
+ flashes = value.instance_variable_get(:@flashes)
+ if discard = value.instance_variable_get(:@used)
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ when Hash # Rails 4.0
+ flashes = value["flashes"]
+ if discard = value["discard"]
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ else
+ new
+ end
+ end
+
+ # Builds a hash containing the flashes to keep for the next request.
+ # If there are none to keep, returns +nil+.
+ def to_session_value #:nodoc:
+ flashes_to_keep = @flashes.except(*@discard)
+ return nil if flashes_to_keep.empty?
+ { "discard" => [], "flashes" => flashes_to_keep }
+ end
+
+ def initialize(flashes = {}, discard = []) #:nodoc:
+ @discard = Set.new(stringify_array(discard))
+ @flashes = flashes.stringify_keys
+ @now = nil
+ end
+
+ def initialize_copy(other)
+ if other.now_is_loaded?
+ @now = other.now.dup
+ @now.flash = self
+ end
+ super
+ end
+
+ def []=(k, v)
+ k = k.to_s
+ @discard.delete k
+ @flashes[k] = v
+ end
+
+ def [](k)
+ @flashes[k.to_s]
+ end
+
+ def update(h) #:nodoc:
+ @discard.subtract stringify_array(h.keys)
+ @flashes.update h.stringify_keys
+ self
+ end
+
+ def keys
+ @flashes.keys
+ end
+
+ def key?(name)
+ @flashes.key? name.to_s
+ end
+
+ def delete(key)
+ key = key.to_s
+ @discard.delete key
+ @flashes.delete key
+ self
+ end
+
+ def to_hash
+ @flashes.dup
+ end
+
+ def empty?
+ @flashes.empty?
+ end
+
+ def clear
+ @discard.clear
+ @flashes.clear
+ end
+
+ def each(&block)
+ @flashes.each(&block)
+ end
+
+ alias :merge! :update
+
+ def replace(h) #:nodoc:
+ @discard.clear
+ @flashes.replace h.stringify_keys
+ self
+ end
+
+ # Sets a flash that will not be available to the next action, only to the current.
+ #
+ # flash.now[:message] = "Hello current action"
+ #
+ # This method enables you to use the flash as a central messaging system in your app.
+ # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
+ # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
+ # vanish when the current action is done.
+ #
+ # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
+ #
+ # Also, brings two convenience accessors:
+ #
+ # flash.now.alert = "Beware now!"
+ # # Equivalent to flash.now[:alert] = "Beware now!"
+ #
+ # flash.now.notice = "Good luck now!"
+ # # Equivalent to flash.now[:notice] = "Good luck now!"
+ def now
+ @now ||= FlashNow.new(self)
+ end
+
+ # Keeps either the entire current flash or a specific flash entry available for the next action:
+ #
+ # flash.keep # keeps the entire flash
+ # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
+ def keep(k = nil)
+ k = k.to_s if k
+ @discard.subtract Array(k || keys)
+ k ? self[k] : self
+ end
+
+ # Marks the entire flash or a single flash entry to be discarded by the end of the current action:
+ #
+ # flash.discard # discard the entire flash at the end of the current action
+ # flash.discard(:warning) # discard only the "warning" entry at the end of the current action
+ def discard(k = nil)
+ k = k.to_s if k
+ @discard.merge Array(k || keys)
+ k ? self[k] : self
+ end
+
+ # Mark for removal entries that were kept, and delete unkept ones.
+ #
+ # This method is called automatically by filters, so you generally don't need to care about it.
+ def sweep #:nodoc:
+ @discard.each { |k| @flashes.delete k }
+ @discard.replace @flashes.keys
+ end
+
+ # Convenience accessor for <tt>flash[:alert]</tt>.
+ def alert
+ self[:alert]
+ end
+
+ # Convenience accessor for <tt>flash[:alert]=</tt>.
+ def alert=(message)
+ self[:alert] = message
+ end
+
+ # Convenience accessor for <tt>flash[:notice]</tt>.
+ def notice
+ self[:notice]
+ end
+
+ # Convenience accessor for <tt>flash[:notice]=</tt>.
+ def notice=(message)
+ self[:notice] = message
+ end
+
+ protected
+ def now_is_loaded?
+ @now
+ end
+
+ private
+ def stringify_array(array) # :doc:
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
+ end
+ end
+
+ def self.new(app) app; end
+ end
+
+ class Request
+ prepend Flash::RequestMethods
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
new file mode 100644
index 0000000000..3feb3a19f3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # When called, this middleware renders an error page. By default if an HTML
+ # response is expected it will render static error pages from the <tt>/public</tt>
+ # directory. For example when this middleware receives a 500 response it will
+ # render the template found in <tt>/public/500.html</tt>.
+ # If an internationalized locale is set, this middleware will attempt to render
+ # the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template
+ # is not found it will fall back on <tt>/public/500.html</tt>.
+ #
+ # When a request with a content type other than HTML is made, this middleware
+ # will attempt to convert error information into the appropriate response type.
+ class PublicExceptions
+ attr_accessor :public_path
+
+ def initialize(public_path)
+ @public_path = public_path
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+ status = request.path_info[1..-1].to_i
+ content_type = request.formats.first
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
+
+ render(status, content_type, body)
+ end
+
+ private
+
+ def render(status, content_type, body)
+ format = "to_#{content_type.to_sym}" if content_type
+ if format && body.respond_to?(format)
+ render_format(status, content_type, body.public_send(format))
+ else
+ render_html(status)
+ end
+ end
+
+ def render_format(status, content_type, body)
+ [status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
+ "Content-Length" => body.bytesize.to_s }, [body]]
+ end
+
+ def render_html(status)
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
+
+ if found || File.exist?(path)
+ render_format(status, "text/html", File.read(path))
+ else
+ [404, { "X-Cascade" => "pass" }, []]
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
new file mode 100644
index 0000000000..8bb3ba7504
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader
+ # callbacks, intended to assist with code reloading during development.
+ #
+ # By default, ActionDispatch::Reloader is included in the middleware stack
+ # only in the development environment; specifically, when +config.cache_classes+
+ # is false.
+ class Reloader < Executor
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
new file mode 100644
index 0000000000..35158f9062
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+require "ipaddr"
+
+module ActionDispatch
+ # This middleware calculates the IP address of the remote client that is
+ # making the request. It does this by checking various headers that could
+ # contain the address, and then picking the last-set address that is not
+ # on the list of trusted IPs. This follows the precedent set by e.g.
+ # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
+ # with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
+ # by @gingerlime. A more detailed explanation of the algorithm is given
+ # at GetIp#calculate_ip.
+ #
+ # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
+ # requires. Some Rack servers simply drop preceding headers, and only report
+ # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
+ # then you should test your Rack server to make sure your data is good.
+ #
+ # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
+ # This middleware assumes that there is at least one proxy sitting around
+ # and setting headers with the client's remote IP address. If you don't use
+ # a proxy, because you are hosted on e.g. Heroku without SSL, any client can
+ # claim to have any IP address by setting the X-Forwarded-For header. If you
+ # care about that, then you need to explicitly drop or ignore those headers
+ # sometime before this middleware runs.
+ class RemoteIp
+ class IpSpoofAttackError < StandardError; end
+
+ # The default trusted IPs list simply includes IP addresses that are
+ # guaranteed by the IP specification to be private addresses. Those will
+ # not be the ultimate client IP in production, and so are discarded. See
+ # https://en.wikipedia.org/wiki/Private_network for details.
+ TRUSTED_PROXIES = [
+ "127.0.0.1", # localhost IPv4
+ "::1", # localhost IPv6
+ "fc00::/7", # private IPv6 range fc00::/7
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
+ ].map { |proxy| IPAddr.new(proxy) }
+
+ attr_reader :check_ip, :proxies
+
+ # Create a new +RemoteIp+ middleware instance.
+ #
+ # The +ip_spoofing_check+ option is on by default. When on, an exception
+ # is raised if it looks like the client is trying to lie about its own IP
+ # address. It makes sense to turn off this check on sites aimed at non-IP
+ # clients (like WAP devices), or behind proxies that set headers in an
+ # incorrect or confusing way (like AWS ELB).
+ #
+ # The +custom_proxies+ argument can take an Array of string, IPAddr, or
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
+ # single string, IPAddr, or Regexp object is provided, it will be used in
+ # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
+ # want in the middle (or at the beginning) of the X-Forwarded-For list,
+ # with your proxy servers after it. If your proxies aren't removed, pass
+ # them in via the +custom_proxies+ parameter. That way, the middleware will
+ # ignore those IP addresses, and return the one that you want.
+ def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
+ @app = app
+ @check_ip = ip_spoofing_check
+ @proxies = if custom_proxies.blank?
+ TRUSTED_PROXIES
+ elsif custom_proxies.respond_to?(:any?)
+ custom_proxies
+ else
+ Array(custom_proxies) + TRUSTED_PROXIES
+ end
+ end
+
+ # Since the IP address may not be needed, we store the object here
+ # without calculating the IP to keep from slowing down the majority of
+ # requests. For those requests that do need to know the IP, the
+ # GetIp#calculate_ip method will calculate the memoized client IP address.
+ def call(env)
+ req = ActionDispatch::Request.new env
+ req.remote_ip = GetIp.new(req, check_ip, proxies)
+ @app.call(req.env)
+ end
+
+ # The GetIp class exists as a way to defer processing of the request data
+ # into an actual IP address. If the ActionDispatch::Request#remote_ip method
+ # is called, this class will calculate the value and then memoize it.
+ class GetIp
+ def initialize(req, check_ip, proxies)
+ @req = req
+ @check_ip = check_ip
+ @proxies = proxies
+ end
+
+ # Sort through the various IP address headers, looking for the IP most
+ # likely to be the address of the actual remote client making this
+ # request.
+ #
+ # REMOTE_ADDR will be correct if the request is made directly against the
+ # Ruby process, on e.g. Heroku. When the request is proxied by another
+ # server like HAProxy or NGINX, the IP address that made the original
+ # request will be put in an X-Forwarded-For header. If there are multiple
+ # proxies, that header may contain a list of IPs. Other proxy services
+ # set the Client-Ip header instead, so we check that too.
+ #
+ # As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
+ # while the first IP in the list is likely to be the "originating" IP,
+ # it could also have been set by the client maliciously.
+ #
+ # In order to find the first address that is (probably) accurate, we
+ # take the list of IPs, remove known and trusted proxies, and then take
+ # the last address left, which was presumably set by one of those proxies.
+ def calculate_ip
+ # Set by the Rack web server, this is a single value.
+ remote_addr = ips_from(@req.remote_addr).last
+
+ # Could be a CSV list and/or repeated headers that were concatenated.
+ client_ips = ips_from(@req.client_ip).reverse
+ forwarded_ips = ips_from(@req.x_forwarded_for).reverse
+
+ # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
+ # If they are both set, it means that either:
+ #
+ # 1) This request passed through two proxies with incompatible IP header
+ # conventions.
+ # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+
+ # (whichever the proxy servers weren't using) themselves.
+ #
+ # Either way, there is no way for us to determine which header is the
+ # right one after the fact. Since we have no idea, if we are concerned
+ # about IP spoofing we need to give up and explode. (If you're not
+ # concerned about IP spoofing you can turn the +ip_spoofing_check+
+ # option off.)
+ should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
+ if should_check_ip && !forwarded_ips.include?(client_ips.last)
+ # We don't know which came from the proxy, and which from the user
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
+ end
+
+ # We assume these things about the IP headers:
+ #
+ # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
+ # - Client-Ip is propagated from the outermost proxy, or is blank
+ # - REMOTE_ADDR will be the IP that made the request to Rack
+ ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
+
+ # If every single IP option is in the trusted list, just return REMOTE_ADDR
+ filter_proxies(ips).first || remote_addr
+ end
+
+ # Memoizes the value returned by #calculate_ip and returns it for
+ # ActionDispatch::Request to use.
+ def to_s
+ @ip ||= calculate_ip
+ end
+
+ private
+
+ def ips_from(header) # :doc:
+ return [] unless header
+ # Split the comma-separated list into an array of strings.
+ ips = header.strip.split(/[,\s]+/)
+ ips.select do |ip|
+ begin
+ # Only return IPs that are valid according to the IPAddr#new method.
+ range = IPAddr.new(ip).to_range
+ # We want to make sure nobody is sneaking a netmask in.
+ range.begin == range.end
+ rescue ArgumentError
+ nil
+ end
+ end
+ end
+
+ def filter_proxies(ips) # :doc:
+ ips.reject do |ip|
+ @proxies.any? { |proxy| proxy === ip }
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
new file mode 100644
index 0000000000..da2871b551
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "securerandom"
+require "active_support/core_ext/string/access"
+
+module ActionDispatch
+ # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
+ # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
+ # the same id to the client via the X-Request-Id header.
+ #
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
+ # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
+ # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
+ #
+ # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
+ # from multiple pieces of the stack.
+ class RequestId
+ X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ req = ActionDispatch::Request.new env
+ req.request_id = make_request_id(req.x_request_id)
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
+ end
+
+ private
+ def make_request_id(request_id)
+ if request_id.presence
+ request_id.gsub(/[^\w\-@]/, "".freeze).first(255)
+ else
+ internal_request_id
+ end
+ end
+
+ def internal_request_id
+ SecureRandom.uuid
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
new file mode 100644
index 0000000000..5b0be96223
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "rack/utils"
+require "rack/request"
+require "rack/session/abstract/id"
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/request/session"
+
+module ActionDispatch
+ module Session
+ class SessionRestoreError < StandardError #:nodoc:
+ def initialize
+ super("Session contains objects whose class definition isn't available.\n" \
+ "Remember to require the classes for all objects kept in the session.\n" \
+ "(Original exception: #{$!.message} [#{$!.class}])\n")
+ set_backtrace $!.backtrace
+ end
+ end
+
+ module Compatibility
+ def initialize(app, options = {})
+ options[:key] ||= "_session_id"
+ super
+ end
+
+ def generate_sid
+ sid = SecureRandom.hex(16)
+ sid.encode!(Encoding::UTF_8)
+ sid
+ end
+
+ private
+
+ def initialize_sid # :doc:
+ @default_options.delete(:sidbits)
+ @default_options.delete(:secure_random)
+ end
+
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
+ end
+
+ module StaleSessionCheck
+ def load_session(env)
+ stale_session_check! { super }
+ end
+
+ def extract_session_id(env)
+ stale_session_check! { super }
+ end
+
+ def stale_session_check!
+ yield
+ rescue ArgumentError => argument_error
+ if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
+ begin
+ # Note that the regexp does not allow $1 to end with a ':'.
+ $1.constantize
+ rescue LoadError, NameError
+ raise ActionDispatch::Session::SessionRestoreError
+ end
+ retry
+ else
+ raise
+ end
+ end
+ end
+
+ module SessionObject # :nodoc:
+ def prepare_session(req)
+ Request::Session.create(self, req, @default_options)
+ end
+
+ def loaded_session?(session)
+ !session.is_a?(Request::Session) || session.loaded?
+ end
+ end
+
+ class AbstractStore < Rack::Session::Abstract::Persisted
+ include Compatibility
+ include StaleSessionCheck
+ include SessionObject
+
+ private
+
+ def set_cookie(request, session_id, cookie)
+ request.cookie_jar[key] = cookie
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
new file mode 100644
index 0000000000..a6d965a644
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
+
+module ActionDispatch
+ module Session
+ # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
+ # if you don't store critical data in your sessions and you don't need them to live for extended periods
+ # of time.
+ #
+ # ==== Options
+ # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
+ # By default, the <tt>:expires_in</tt> option of the cache is used.
+ class CacheStore < AbstractStore
+ def initialize(app, options = {})
+ @cache = options[:cache] || Rails.cache
+ options[:expire_after] ||= @cache.options[:expires_in]
+ super
+ end
+
+ # Get a session from the cache.
+ def find_session(env, sid)
+ unless sid && (session = @cache.read(cache_key(sid)))
+ sid, session = generate_sid, {}
+ end
+ [sid, session]
+ end
+
+ # Set a session in the cache.
+ def write_session(env, sid, session, options)
+ key = cache_key(sid)
+ if session
+ @cache.write(key, session, expires_in: options[:expire_after])
+ else
+ @cache.delete(key)
+ end
+ sid
+ end
+
+ # Remove a session from the cache.
+ def delete_session(env, sid, options)
+ @cache.delete(cache_key(sid))
+ generate_sid
+ end
+
+ private
+ # Turn the session id into a cache key.
+ def cache_key(sid)
+ "_session_id:#{sid}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
new file mode 100644
index 0000000000..4ea96196d3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "action_dispatch/middleware/session/abstract_store"
+require "rack/session/cookie"
+
+module ActionDispatch
+ module Session
+ # This cookie-based session store is the Rails default. It is
+ # dramatically faster than the alternatives.
+ #
+ # Sessions typically contain at most a user_id and flash message; both fit
+ # within the 4K cookie size limit. A CookieOverflow exception is raised if
+ # you attempt to store more than 4K of data.
+ #
+ # The cookie jar used for storage is automatically configured to be the
+ # best possible option given your application's configuration.
+ #
+ # If you only have secret_token set, your cookies will be signed, but
+ # not encrypted. This means a user cannot alter their +user_id+ without
+ # knowing your app's secret key, but can easily read their +user_id+. This
+ # was the default for Rails 3 apps.
+ #
+ # Your cookies will be encrypted using your apps secret_key_base. This
+ # goes a step further than signed cookies in that encrypted cookies cannot
+ # be altered or read by users. This is the default starting in Rails 4.
+ #
+ # Configure your session store in <tt>config/initializers/session_store.rb</tt>:
+ #
+ # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
+ #
+ # By default, your secret key base is derived from your application name in
+ # the test and development environments. In all other environments, it is stored
+ # encrypted in the <tt>config/credentials.yml.enc</tt> file.
+ #
+ # If your application was not updated to Rails 5.2 defaults, the secret_key_base
+ # will be found in the old <tt>config/secrets.yml</tt> file.
+ #
+ # Note that changing your secret_key_base will invalidate all existing session.
+ # Additionally, you should take care to make sure you are not relying on the
+ # ability to decode signed cookies generated by your app in external
+ # applications or JavaScript before changing it.
+ #
+ # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
+ # options described there can be used to customize the session cookie that
+ # is generated. For example:
+ #
+ # Rails.application.config.session_store :cookie_store, expire_after: 14.days
+ #
+ # would set the session cookie to expire automatically 14 days after creation.
+ # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
+ # <tt>:httponly</tt>.
+ class CookieStore < AbstractStore
+ def initialize(app, options = {})
+ super(app, options.merge!(cookie_only: true))
+ end
+
+ def delete_session(req, session_id, options)
+ new_sid = generate_sid unless options[:drop]
+ # Reset hash and Assign the new session id
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
+ new_sid
+ end
+
+ def load_session(req)
+ stale_session_check! do
+ data = unpacked_cookie_data(req)
+ data = persistent_session_id!(data)
+ [data["session_id"], data]
+ end
+ end
+
+ private
+
+ def extract_session_id(req)
+ stale_session_check! do
+ unpacked_cookie_data(req)["session_id"]
+ end
+ end
+
+ def unpacked_cookie_data(req)
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ v = stale_session_check! do
+ if data = get_cookie(req)
+ data.stringify_keys!
+ end
+ data || {}
+ end
+ req.set_header k, v
+ end
+ end
+
+ def persistent_session_id!(data, sid = nil)
+ data ||= {}
+ data["session_id"] ||= sid || generate_sid
+ data
+ end
+
+ def write_session(req, sid, session_data, options)
+ session_data["session_id"] = sid
+ session_data
+ end
+
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
+ end
+
+ def get_cookie(req)
+ cookie_jar(req)[@key]
+ end
+
+ def cookie_jar(request)
+ request.cookie_jar.signed_or_encrypted
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
new file mode 100644
index 0000000000..914df3a2b1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
+begin
+ require "rack/session/dalli"
+rescue LoadError => e
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+end
+
+module ActionDispatch
+ module Session
+ # A session store that uses MemCache to implement storage.
+ #
+ # ==== Options
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
+ class MemCacheStore < Rack::Session::Dalli
+ include Compatibility
+ include StaleSessionCheck
+ include SessionObject
+
+ def initialize(app, options = {})
+ options[:expire_after] ||= options[:expires]
+ super
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
new file mode 100644
index 0000000000..3c88afd4d3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
+
+module ActionDispatch
+ # This middleware rescues any exception returned by the application
+ # and calls an exceptions app that will wrap it in a format for the end user.
+ #
+ # The exceptions app should be passed as parameter on initialization
+ # of ShowExceptions. Every time there is an exception, ShowExceptions will
+ # store the exception in env["action_dispatch.exception"], rewrite the
+ # PATH_INFO to the exception status code and call the Rack app.
+ #
+ # If the application returns a "X-Cascade" pass response, this middleware
+ # will send an empty response as result with the correct status code.
+ # If any exception happens inside the exceptions app, this middleware
+ # catches the exceptions and returns a FAILSAFE_RESPONSE.
+ class ShowExceptions
+ FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
+ ["500 Internal Server Error\n" \
+ "If you are the administrator of this website, then please read this web " \
+ "application's log file and/or the web server's log file to find out what " \
+ "went wrong."]]
+
+ def initialize(app, exceptions_app)
+ @app = app
+ @exceptions_app = exceptions_app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new env
+ @app.call(env)
+ rescue Exception => exception
+ if request.show_exceptions?
+ render_exception(request, exception)
+ else
+ raise exception
+ end
+ end
+
+ private
+
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+ status = wrapper.status_code
+ request.set_header "action_dispatch.exception", wrapper.exception
+ request.set_header "action_dispatch.original_path", request.path_info
+ request.path_info = "/#{status}"
+ response = @exceptions_app.call(request.env)
+ response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
+ rescue Exception => failsafe_error
+ $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
+ FAILSAFE_RESPONSE
+ end
+
+ def pass_response(status)
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
new file mode 100644
index 0000000000..ef633aadc6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
+ # requests:
+ #
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
+ # to modify the destination URL
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
+ # <tt>redirect: false</tt> to disable this feature.
+ #
+ # Requests can opt-out of redirection with +exclude+:
+ #
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ #
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
+ # must not be sent along with +http://+ requests. Enabled by default. Set
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
+ #
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
+ # this site as TLS-only and automatically redirect non-TLS requests.
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
+ #
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
+ #
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
+ # required to qualify for browser preload lists is 18 weeks. Defaults to
+ # 180 days (recommended).
+ #
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
+ # to all subdomains. This protects your cookies from interception by a
+ # vulnerable site on a subdomain. Defaults to +true+.
+ #
+ # * +preload+: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.org to submit your site for inclusion.
+ # Defaults to +false+.
+ #
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
+ # <tt>hsts: { expires: 0 }</tt>.
+ class SSL
+ # :stopdoc:
+
+ # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
+ # and greater than the 18-week requirement for browser preload lists.
+ HSTS_EXPIRES_IN = 15552000
+
+ def self.default_hsts_options
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
+ end
+
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
+ @app = app
+
+ @redirect = redirect
+
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
+ @secure_cookies = secure_cookies
+
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
+ end
+
+ def call(env)
+ request = Request.new env
+
+ if request.ssl?
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers if @secure_cookies
+ end
+ else
+ return redirect_to_https request unless @exclude.call(request)
+ @app.call(env)
+ end
+ end
+
+ private
+ def set_hsts_header!(headers)
+ headers["Strict-Transport-Security".freeze] ||= @hsts_header
+ end
+
+ def normalize_hsts_options(options)
+ case options
+ # Explicitly disabling HSTS clears the existing setting from browsers
+ # by setting expiry to 0.
+ when false
+ self.class.default_hsts_options.merge(expires: 0)
+ # Default to enabled, with default options.
+ when nil, true
+ self.class.default_hsts_options
+ else
+ self.class.default_hsts_options.merge(options)
+ end
+ end
+
+ # https://tools.ietf.org/html/rfc6797#section-6.1
+ def build_hsts_header(hsts)
+ value = "max-age=#{hsts[:expires].to_i}".dup
+ value << "; includeSubDomains" if hsts[:subdomains]
+ value << "; preload" if hsts[:preload]
+ value
+ end
+
+ def flag_cookies_as_secure!(headers)
+ if cookies = headers["Set-Cookie".freeze]
+ cookies = cookies.split("\n".freeze)
+
+ headers["Set-Cookie".freeze] = cookies.map { |cookie|
+ if cookie !~ /;\s*secure\s*(;|$)/i
+ "#{cookie}; secure"
+ else
+ cookie
+ end
+ }.join("\n".freeze)
+ end
+ end
+
+ def redirect_to_https(request)
+ [ @redirect.fetch(:status, redirection_status(request)),
+ { "Content-Type" => "text/html",
+ "Location" => https_location_for(request) },
+ @redirect.fetch(:body, []) ]
+ end
+
+ def redirection_status(request)
+ if request.get? || request.head?
+ 301 # Issue a permanent redirect via a GET request.
+ else
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
+ end
+ end
+
+ def https_location_for(request)
+ host = @redirect[:host] || request.host
+ port = @redirect[:port] || request.port
+
+ location = "https://#{host}".dup
+ location << ":#{port}" if port != 80 && port != 443
+ location << request.fullpath
+ location
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
new file mode 100644
index 0000000000..b82f8aa3a3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "active_support/inflector/methods"
+require "active_support/dependencies"
+
+module ActionDispatch
+ class MiddlewareStack
+ class Middleware
+ attr_reader :args, :block, :klass
+
+ def initialize(klass, args, block)
+ @klass = klass
+ @args = args
+ @block = block
+ end
+
+ def name; klass.name; end
+
+ def ==(middleware)
+ case middleware
+ when Middleware
+ klass == middleware.klass
+ when Class
+ klass == middleware
+ end
+ end
+
+ def inspect
+ if klass.is_a?(Class)
+ klass.to_s
+ else
+ klass.class.to_s
+ end
+ end
+
+ def build(app)
+ klass.new(app, *args, &block)
+ end
+ end
+
+ include Enumerable
+
+ attr_accessor :middlewares
+
+ def initialize(*args)
+ @middlewares = []
+ yield(self) if block_given?
+ end
+
+ def each
+ @middlewares.each { |x| yield x }
+ end
+
+ def size
+ middlewares.size
+ end
+
+ def last
+ middlewares.last
+ end
+
+ def [](i)
+ middlewares[i]
+ end
+
+ def unshift(klass, *args, &block)
+ middlewares.unshift(build_middleware(klass, args, block))
+ end
+
+ def initialize_copy(other)
+ self.middlewares = other.middlewares.dup
+ end
+
+ def insert(index, klass, *args, &block)
+ index = assert_index(index, :before)
+ middlewares.insert(index, build_middleware(klass, args, block))
+ end
+
+ alias_method :insert_before, :insert
+
+ def insert_after(index, *args, &block)
+ index = assert_index(index, :after)
+ insert(index + 1, *args, &block)
+ end
+
+ def swap(target, *args, &block)
+ index = assert_index(target, :before)
+ insert(index, *args, &block)
+ middlewares.delete_at(index + 1)
+ end
+
+ def delete(target)
+ middlewares.delete_if { |m| m.klass == target }
+ end
+
+ def use(klass, *args, &block)
+ middlewares.push(build_middleware(klass, args, block))
+ end
+
+ def build(app = Proc.new)
+ middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
+ end
+
+ private
+
+ def assert_index(index, where)
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
+ i
+ end
+
+ def build_middleware(klass, args, block)
+ Middleware.new(klass, args, block)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
new file mode 100644
index 0000000000..23492e14eb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require "rack/utils"
+require "active_support/core_ext/uri"
+
+module ActionDispatch
+ # This middleware returns a file's contents from disk in the body response.
+ # When initialized, it can accept optional HTTP headers, which will be set
+ # when a response containing a file's contents is delivered.
+ #
+ # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
+ # where the base path is in the +root+ directory. For example, if the +root+
+ # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
+ # +assets/application.js+ will return a response with the contents of a file
+ # located at +public/assets/application.js+ if the file exists. If the file
+ # does not exist, a 404 "File not Found" response will be returned.
+ class FileHandler
+ def initialize(root, index: "index", headers: {})
+ @root = root.chomp("/")
+ @file_server = ::Rack::File.new(@root, headers)
+ @index = index
+ end
+
+ # Takes a path to a file. If the file is found, has valid encoding, and has
+ # correct read permissions, the return value is a URI-escaped string
+ # representing the filename. Otherwise, false is returned.
+ #
+ # Used by the +Static+ class to check the existence of a valid file
+ # in the server's +public/+ directory (see Static#call).
+ def match?(path)
+ path = ::Rack::Utils.unescape_path path
+ return false unless ::Rack::Utils.valid_path? path
+ path = ::Rack::Utils.clean_path_info path
+
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
+
+ if match = paths.detect { |p|
+ path = File.join(@root, p.dup.force_encoding(Encoding::UTF_8))
+ begin
+ File.file?(path) && File.readable?(path)
+ rescue SystemCallError
+ false
+ end
+
+ }
+ return ::Rack::Utils.escape_path(match)
+ end
+ end
+
+ def call(env)
+ serve(Rack::Request.new(env))
+ end
+
+ def serve(request)
+ path = request.path_info
+ gzip_path = gzip_file_path(path)
+
+ if gzip_path && gzip_encoding_accepted?(request)
+ request.path_info = gzip_path
+ status, headers, body = @file_server.call(request.env)
+ if status == 304
+ return [status, headers, body]
+ end
+ headers["Content-Encoding"] = "gzip"
+ headers["Content-Type"] = content_type(path)
+ else
+ status, headers, body = @file_server.call(request.env)
+ end
+
+ headers["Vary"] = "Accept-Encoding" if gzip_path
+
+ return [status, headers, body]
+ ensure
+ request.path_info = path
+ end
+
+ private
+ def ext
+ ::ActionController::Base.default_static_extension
+ end
+
+ def content_type(path)
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
+ end
+
+ def gzip_encoding_accepted?(request)
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
+ end
+
+ def gzip_file_path(path)
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
+ gzip_path = "#{path}.gz"
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
+ gzip_path
+ else
+ false
+ end
+ end
+ end
+
+ # This middleware will attempt to return the contents of a file's body from
+ # disk in the response. If a file is not found on disk, the request will be
+ # delegated to the application stack. This middleware is commonly initialized
+ # to serve assets from a server's +public/+ directory.
+ #
+ # This middleware verifies the path to ensure that only files
+ # living in the root directory can be rendered. A request cannot
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
+ # requests will result in a file being returned.
+ class Static
+ def initialize(app, path, index: "index", headers: {})
+ @app = app
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
+ end
+
+ def call(env)
+ req = Rack::Request.new env
+
+ if req.get? || req.head?
+ path = req.path_info.chomp("/".freeze)
+ if match = @file_handler.match?(path)
+ req.path_info = match
+ return @file_handler.serve(req)
+ end
+ end
+
+ @app.call(req.env)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
new file mode 100644
index 0000000000..49b1e83551
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -0,0 +1,22 @@
+<% unless @exception.blamed_files.blank? %>
+ <% if (hide = @exception.blamed_files.length > 8) %>
+ <a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
+ <% end %>
+ <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
+<% end %>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
+ <div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
+</div>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
+ <div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
+</div>
+
+<h2 style="margin-top: 30px">Response</h2>
+<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
new file mode 100644
index 0000000000..396768ecee
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
@@ -0,0 +1,23 @@
+<%
+ clean_params = @request.filtered_parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end unless self.class.method_defined?(:debug_hash)
+%>
+
+Request parameters
+<%= request_dump %>
+
+Session dump
+<%= debug_hash @request.session %>
+
+Env dump
+<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %>
+
+Response headers
+<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
new file mode 100644
index 0000000000..e7b913bbe4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
@@ -0,0 +1,27 @@
+<% @source_extracts.each_with_index do |source_extract, index| %>
+ <% if source_extract[:code] %>
+ <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
+<span><%= line_number -%></span>
+ <% end %>
+ </pre>
+ </td>
+<td width="100%">
+<pre>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
+</pre>
+</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb
new file mode 100644
index 0000000000..23a9c7ba3f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb
@@ -0,0 +1,8 @@
+<% @source_extracts.first(3).each do |source_extract| %>
+<% if source_extract[:code] %>
+Extracted source (around line #<%= source_extract[:line_number] %>):
+
+<% source_extract[:code].each do |line, source| -%>
+<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%>
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
new file mode 100644
index 0000000000..ab57b11c7d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -0,0 +1,52 @@
+<% names = @traces.keys %>
+
+<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
+
+<div id="traces">
+ <% names.each do |name| %>
+ <%
+ show = "show('#{name.gsub(/\s/, '-')}');"
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
+ %>
+ <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
+ <% end %>
+
+ <% @traces.each do |name, trace| %>
+ <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
+ <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
+ </div>
+ <% end %>
+
+ <script type="text/javascript">
+ var traceFrames = document.getElementsByClassName('trace-frames');
+ var selectedFrame, currentSource = document.getElementById('frame-source-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
+ }
+ }
+ </script>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
new file mode 100644
index 0000000000..c0b53068f7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -0,0 +1,9 @@
+Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
+
+<% @traces.each do |name, trace| %>
+<% if trace.any? %>
+<%= name %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
+
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
new file mode 100644
index 0000000000..f154021ae6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -0,0 +1,16 @@
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
new file mode 100644
index 0000000000..603de54b8b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
@@ -0,0 +1,9 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
new file mode 100644
index 0000000000..e1b129ccc5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -0,0 +1,21 @@
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
+
+<div id="container">
+ <h2>
+ <%= h @exception.message %>
+ <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+ <br />To resolve this issue run: bin/rails active_storage:install
+ <% end %>
+ </h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
new file mode 100644
index 0000000000..033518cf8a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -0,0 +1,13 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+To resolve this issue run: bin/rails active_storage:install
+<% end %>
+
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
new file mode 100644
index 0000000000..39ea25bdfc
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Action Controller: Exception caught</title>
+ <style>
+ body {
+ background-color: #FAFAFA;
+ color: #333;
+ margin: 0px;
+ }
+
+ body, p, ol, ul, td {
+ font-family: helvetica, verdana, arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ font-size: 11px;
+ white-space: pre-wrap;
+ }
+
+ pre.box {
+ border: 1px solid #EEE;
+ padding: 10px;
+ margin: 0px;
+ width: 958px;
+ }
+
+ header {
+ color: #F0F0F0;
+ background: #C52F24;
+ padding: 0.5em 1.5em;
+ }
+
+ h1 {
+ margin: 0.2em 0;
+ line-height: 1.1em;
+ font-size: 2em;
+ }
+
+ h2 {
+ color: #C52F24;
+ line-height: 25px;
+ }
+
+ .details {
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ margin: 1em 0px;
+ display: block;
+ width: 978px;
+ }
+
+ .summary {
+ padding: 8px 15px;
+ border-bottom: 1px solid #D0D0D0;
+ display: block;
+ }
+
+ .details pre {
+ margin: 5px;
+ border: none;
+ }
+
+ #container {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 1.5em;
+ }
+
+ .source * {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ .source {
+ border: 1px solid #D9D9D9;
+ background: #ECECEC;
+ width: 978px;
+ }
+
+ .source pre {
+ padding: 10px 0px;
+ border: none;
+ }
+
+ .source .data {
+ font-size: 80%;
+ overflow: auto;
+ background-color: #FFF;
+ }
+
+ .info {
+ padding: 0.5em;
+ }
+
+ .source .data .line_numbers {
+ background-color: #ECECEC;
+ color: #AAA;
+ padding: 1em .5em;
+ border-right: 1px solid #DDD;
+ text-align: right;
+ }
+
+ .line {
+ padding-left: 10px;
+ white-space: pre;
+ }
+
+ .line:hover {
+ background-color: #F6F6F6;
+ }
+
+ .line.active {
+ background-color: #FFCCCC;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ a { color: #980905; }
+ a:visited { color: #666; }
+ a.trace-frames { color: #666; }
+ a:hover { color: #C52F24; }
+ a.trace-frames.selected { color: #C52F24 }
+
+ <%= yield :style %>
+ </style>
+
+ <script>
+ var toggle = function(id) {
+ var s = document.getElementById(id).style;
+ s.display = s.display == 'none' ? 'block' : 'none';
+ return false;
+ }
+ var show = function(id) {
+ document.getElementById(id).style.display = 'block';
+ }
+ var hide = function(id) {
+ document.getElementById(id).style.display = 'none';
+ }
+ var toggleTrace = function() {
+ return toggle('blame_trace');
+ }
+ var toggleSessionDump = function() {
+ return toggle('session_dump');
+ }
+ var toggleEnvDump = function() {
+ return toggle('env_dump');
+ }
+ </script>
+</head>
+<body>
+
+<%= yield %>
+
+</body>
+</html>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
new file mode 100644
index 0000000000..2a65fd06ad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -0,0 +1,11 @@
+<header>
+ <h1>Template is missing</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
new file mode 100644
index 0000000000..ae62d9eb02
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
@@ -0,0 +1,3 @@
+Template is missing
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
new file mode 100644
index 0000000000..55dd5ddc7b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -0,0 +1,32 @@
+<header>
+ <h1>Routing Error</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+ <% unless @exception.failures.empty? %>
+ <p>
+ <h2>Failure reasons:</h2>
+ <ol>
+ <% @exception.failures.each do |route, reason| %>
+ <li><code><%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %></li>
+ <% end %>
+ </ol>
+ </p>
+ <% end %>
+
+ <%= render template: "rescues/_trace" %>
+
+ <% if @routes_inspector %>
+ <h2>
+ Routes
+ </h2>
+
+ <p>
+ Routes match in priority from top to bottom
+ </p>
+
+ <%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
+ <% end %>
+
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
new file mode 100644
index 0000000000..f6e4dac1f3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
@@ -0,0 +1,11 @@
+Routing Error
+
+<%= @exception.message %>
+<% unless @exception.failures.empty? %>
+Failure reasons:
+<% @exception.failures.each do |route, reason| %>
+ - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %>
+<% end %>
+<% end %>
+
+<%= render template: "rescues/_trace", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
new file mode 100644
index 0000000000..5060da9369
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -0,0 +1,20 @@
+<header>
+ <h1>
+ <%= @exception.cause.class.to_s %> in
+ <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+ </h1>
+</header>
+
+<div id="container">
+ <p>
+ Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised:
+ </p>
+ <pre><code><%= h @exception.message %></code></pre>
+
+ <%= render template: "rescues/_source" %>
+
+ <p><%= @exception.sub_template_message %></p>
+
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
new file mode 100644
index 0000000000..78d52acd96
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -0,0 +1,7 @@
+<%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+
+Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
+<%= @exception.message %>
+<%= @exception.sub_template_message %>
+<%= render template: "rescues/_trace", format: :text %>
+<%= render template: "rescues/_request_and_response", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
new file mode 100644
index 0000000000..259fb2bb3b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
@@ -0,0 +1,6 @@
+<header>
+ <h1>Unknown action</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
new file mode 100644
index 0000000000..83973addcb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
@@ -0,0 +1,3 @@
+Unknown action
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
new file mode 100644
index 0000000000..6e995c85c1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
@@ -0,0 +1,16 @@
+<tr class='route_row' data-helper='path'>
+ <td data-route-name='<%= route[:name] %>'>
+ <% if route[:name].present? %>
+ <%= route[:name] %><span class='helper'>_path</span>
+ <% end %>
+ </td>
+ <td>
+ <%= route[:verb] %>
+ </td>
+ <td data-route-path='<%= route[:path] %>'>
+ <%= route[:path] %>
+ </td>
+ <td>
+ <%=simple_format route[:reqs] %>
+ </td>
+</tr>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
new file mode 100644
index 0000000000..1fa0691303
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -0,0 +1,200 @@
+<% content_for :style do %>
+ #route_table {
+ margin: 0;
+ border-collapse: collapse;
+ }
+
+ #route_table thead tr {
+ border-bottom: 2px solid #ddd;
+ }
+
+ #route_table thead tr.bottom {
+ border-bottom: none;
+ }
+
+ #route_table thead tr.bottom th {
+ padding: 10px 0;
+ line-height: 15px;
+ }
+
+ #route_table thead tr.bottom th input#search {
+ -webkit-appearance: textfield;
+ }
+
+ #route_table tbody tr {
+ border-bottom: 1px solid #ddd;
+ }
+
+ #route_table tbody tr:nth-child(odd) {
+ background: #f2f2f2;
+ }
+
+ #route_table tbody.exact_matches,
+ #route_table tbody.fuzzy_matches {
+ background-color: LightGoldenRodYellow;
+ border-bottom: solid 2px SlateGrey;
+ }
+
+ #route_table tbody.exact_matches tr,
+ #route_table tbody.fuzzy_matches tr {
+ background: none;
+ border-bottom: none;
+ }
+
+ #route_table td {
+ padding: 4px 30px;
+ }
+
+ #path_search {
+ width: 80%;
+ font-size: inherit;
+ }
+<% end %>
+
+<table id='route_table' class='route_table'>
+ <thead>
+ <tr>
+ <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)" %>
+ </th>
+ <th><%# HTTP Verb %>
+ </th>
+ <th><%# Path %>
+ <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %>
+ </th>
+ <th><%# Controller#action %>
+ </th>
+ </tr>
+ </thead>
+ <tbody class='exact_matches' id='exact_matches'>
+ </tbody>
+ <tbody class='fuzzy_matches' id='fuzzy_matches'>
+ </tbody>
+ <tbody>
+ <%= yield %>
+ </tbody>
+</table>
+
+<script type='text/javascript'>
+ // support forEarch iterator on NodeList
+ NodeList.prototype.forEach = Array.prototype.forEach;
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ // Check if there are any matched results in a section
+ function checkNoMatch(section, noMatchText) {
+ if (section.children.length <= 1) {
+ section.innerHTML += noMatchText;
+ }
+ }
+
+ // get JSON from URL and invoke callback with result
+ function getJSON(url, success) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onload = function() {
+ if (this.status == 200)
+ success(JSON.parse(this.response));
+ };
+ xhr.send();
+ }
+
+ function delayedKeyup(input, callback) {
+ var timeout;
+ input.onkeyup = function(){
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(callback, 300);
+ }
+ }
+
+ // remove params or fragments
+ function sanitizePath(path) {
+ return path.replace(/[#?].*/, '');
+ }
+
+ var pathElements = document.querySelectorAll('#route_table [data-route-path]'),
+ searchElem = document.querySelector('#search'),
+ exactSection = document.querySelector('#exact_matches'),
+ fuzzySection = document.querySelector('#fuzzy_matches');
+
+ // Remove matches when no search value is present
+ searchElem.onblur = function(e) {
+ if (searchElem.value === "") {
+ exactSection.innerHTML = "";
+ fuzzySection.innerHTML = "";
+ }
+ }
+
+ // On key press perform a search for matching paths
+ delayedKeyup(searchElem, function() {
+ var path = sanitizePath(searchElem.value),
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>',
+ noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
+ noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
+
+ if (!path)
+ return searchElem.onblur();
+
+ getJSON('/rails/info/routes?path=' + path, function(matches){
+ // Clear out results section
+ exactSection.innerHTML = defaultExactMatch;
+ fuzzySection.innerHTML = defaultFuzzyMatch;
+
+ // Display exact matches and fuzzy matches
+ pathElements.forEach(function(elem) {
+ var elemPath = elem.getAttribute('data-route-path');
+
+ if (matches['exact'].indexOf(elemPath) != -1)
+ exactSection.appendChild(elem.parentNode.cloneNode(true));
+
+ if (matches['fuzzy'].indexOf(elemPath) != -1)
+ fuzzySection.appendChild(elem.parentNode.cloneNode(true));
+ })
+
+ // Display 'No Matches' message when no matches are found
+ checkNoMatch(exactSection, noExactMatch);
+ checkNoMatch(fuzzySection, noFuzzyMatch);
+ })
+ })
+ }
+
+ // Enables functionality to toggle between `_path` and `_url` helper suffixes
+ function setupRouteToggleHelperLinks() {
+
+ // Sets content for each element
+ function setValOn(elems, val) {
+ elems.forEach(function(elem) {
+ elem.innerHTML = val;
+ });
+ }
+
+ // Sets onClick event for each element
+ function onClick(elems, func) {
+ elems.forEach(function(elem) {
+ elem.onclick = func;
+ });
+ }
+
+ var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
+
+ onClick(toggleLinks, function(){
+ var helperTxt = this.getAttribute("data-route-helper"),
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
+
+ setValOn(helperElems, helperTxt);
+ });
+ }
+
+ setupMatchPaths();
+ setupRouteToggleHelperLinks();
+</script>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
new file mode 100644
index 0000000000..eb6fbca6ba
--- /dev/null
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "action_dispatch"
+require "active_support/messages/rotation_configuration"
+
+module ActionDispatch
+ class Railtie < Rails::Railtie # :nodoc:
+ config.action_dispatch = ActiveSupport::OrderedOptions.new
+ config.action_dispatch.x_sendfile_header = nil
+ config.action_dispatch.ip_spoofing_check = true
+ config.action_dispatch.show_exceptions = true
+ config.action_dispatch.tld_length = 1
+ config.action_dispatch.ignore_accept_header = false
+ config.action_dispatch.rescue_templates = {}
+ config.action_dispatch.rescue_responses = {}
+ config.action_dispatch.default_charset = nil
+ config.action_dispatch.rack_cache = false
+ config.action_dispatch.http_auth_salt = "http authentication"
+ config.action_dispatch.signed_cookie_salt = "signed cookie"
+ config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
+ config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
+ config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
+ config.action_dispatch.use_authenticated_cookie_encryption = false
+ config.action_dispatch.perform_deep_munge = true
+
+ config.action_dispatch.default_headers = {
+ "X-Frame-Options" => "SAMEORIGIN",
+ "X-XSS-Protection" => "1; mode=block",
+ "X-Content-Type-Options" => "nosniff",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none",
+ "Referrer-Policy" => "strict-origin-when-cross-origin"
+ }
+
+ config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
+
+ config.eager_load_namespaces << ActionDispatch
+
+ initializer "action_dispatch.configure" do |app|
+ ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
+ ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
+ ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
+ ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
+ ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers
+
+ ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
+ ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
+
+ config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
+ ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
+
+ ActionDispatch.test_app = app
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
new file mode 100644
index 0000000000..000847e193
--- /dev/null
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+
+module ActionDispatch
+ class Request
+ # Session is responsible for lazily loading the session from store.
+ class Session # :nodoc:
+ ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
+ ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
+
+ # Singleton object used to determine if an optional param wasn't specified.
+ Unspecified = Object.new
+
+ # Creates a session hash, merging the properties of the previous session if any.
+ def self.create(store, req, default_options)
+ session_was = find req
+ session = Request::Session.new(store, req)
+ session.merge! session_was if session_was
+
+ set(req, session)
+ Options.set(req, Request::Session::Options.new(store, default_options))
+ session
+ end
+
+ def self.find(req)
+ req.get_header ENV_SESSION_KEY
+ end
+
+ def self.set(req, session)
+ req.set_header ENV_SESSION_KEY, session
+ end
+
+ class Options #:nodoc:
+ def self.set(req, options)
+ req.set_header ENV_SESSION_OPTIONS_KEY, options
+ end
+
+ def self.find(req)
+ req.get_header ENV_SESSION_OPTIONS_KEY
+ end
+
+ def initialize(by, default_options)
+ @by = by
+ @delegate = default_options.dup
+ end
+
+ def [](key)
+ @delegate[key]
+ end
+
+ def id(req)
+ @delegate.fetch(:id) {
+ @by.send(:extract_session_id, req)
+ }
+ end
+
+ def []=(k, v); @delegate[k] = v; end
+ def to_hash; @delegate.dup; end
+ def values_at(*args); @delegate.values_at(*args); end
+ end
+
+ def initialize(by, req)
+ @by = by
+ @req = req
+ @delegate = {}
+ @loaded = false
+ @exists = nil # We haven't checked yet.
+ end
+
+ def id
+ options.id(@req)
+ end
+
+ def options
+ Options.find @req
+ end
+
+ def destroy
+ clear
+ options = self.options || {}
+ @by.send(:delete_session, @req, options.id(@req), options)
+
+ # Load the new sid to be written with the response.
+ @loaded = false
+ load_for_write!
+ end
+
+ # Returns value of the key stored in the session or
+ # +nil+ if the given key is not found in the session.
+ def [](key)
+ load_for_read!
+ @delegate[key.to_s]
+ end
+
+ # Returns true if the session has the given key or false.
+ def has_key?(key)
+ load_for_read!
+ @delegate.key?(key.to_s)
+ end
+ alias :key? :has_key?
+ alias :include? :has_key?
+
+ # Returns keys of the session as Array.
+ def keys
+ load_for_read!
+ @delegate.keys
+ end
+
+ # Returns values of the session as Array.
+ def values
+ load_for_read!
+ @delegate.values
+ end
+
+ # Writes given value to given key of the session.
+ def []=(key, value)
+ load_for_write!
+ @delegate[key.to_s] = value
+ end
+
+ # Clears the session.
+ def clear
+ load_for_write!
+ @delegate.clear
+ end
+
+ # Returns the session as Hash.
+ def to_hash
+ load_for_read!
+ @delegate.dup.delete_if { |_, v| v.nil? }
+ end
+ alias :to_h :to_hash
+
+ # Updates the session with given Hash.
+ #
+ # session.to_hash
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"}
+ #
+ # session.update({ "foo" => "bar" })
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
+ #
+ # session.to_hash
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
+ def update(hash)
+ load_for_write!
+ @delegate.update stringify_keys(hash)
+ end
+
+ # Deletes given key from the session.
+ def delete(key)
+ load_for_write!
+ @delegate.delete key.to_s
+ end
+
+ # Returns value of the given key from the session, or raises +KeyError+
+ # if can't find the given key and no default value is set.
+ # Returns default value if specified.
+ #
+ # session.fetch(:foo)
+ # # => KeyError: key not found: "foo"
+ #
+ # session.fetch(:foo, :bar)
+ # # => :bar
+ #
+ # session.fetch(:foo) do
+ # :bar
+ # end
+ # # => :bar
+ def fetch(key, default = Unspecified, &block)
+ load_for_read!
+ if default == Unspecified
+ @delegate.fetch(key.to_s, &block)
+ else
+ @delegate.fetch(key.to_s, default, &block)
+ end
+ end
+
+ def inspect
+ if loaded?
+ super
+ else
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
+ end
+ end
+
+ def exists?
+ return @exists unless @exists.nil?
+ @exists = @by.send(:session_exists?, @req)
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ def empty?
+ load_for_read!
+ @delegate.empty?
+ end
+
+ def merge!(other)
+ load_for_write!
+ @delegate.merge!(other)
+ end
+
+ def each(&block)
+ to_hash.each(&block)
+ end
+
+ private
+
+ def load_for_read!
+ load! if !loaded? && exists?
+ end
+
+ def load_for_write!
+ load! unless loaded?
+ end
+
+ def load!
+ id, session = @by.load_session @req
+ options[:id] = id
+ @delegate.replace(stringify_keys(session))
+ @loaded = true
+ end
+
+ def stringify_keys(other)
+ other.each_with_object({}) { |(key, value), hash|
+ hash[key.to_s] = value
+ }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
new file mode 100644
index 0000000000..0ae464082d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ class Request
+ class Utils # :nodoc:
+ mattr_accessor :perform_deep_munge, default: true
+
+ def self.each_param_value(params, &block)
+ case params
+ when Array
+ params.each { |element| each_param_value(element, &block) }
+ when Hash
+ params.each_value { |value| each_param_value(value, &block) }
+ when String
+ block.call params
+ end
+ end
+
+ def self.normalize_encode_params(params)
+ if perform_deep_munge
+ NoNilParamEncoder.normalize_encode_params params
+ else
+ ParamEncoder.normalize_encode_params params
+ end
+ end
+
+ def self.check_param_encoding(params)
+ case params
+ when Array
+ params.each { |element| check_param_encoding(element) }
+ when Hash
+ params.each_value { |value| check_param_encoding(value) }
+ when String
+ unless params.valid_encoding?
+ # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
+ # ActionDispatch::Request#GET will re-raise as a BadRequest error.
+ raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
+ end
+ end
+ end
+
+ class ParamEncoder # :nodoc:
+ # Convert nested Hash to HashWithIndifferentAccess.
+ def self.normalize_encode_params(params)
+ case params
+ when Array
+ handle_array params
+ when Hash
+ if params.has_key?(:tempfile)
+ ActionDispatch::Http::UploadedFile.new(params)
+ else
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_hash[key] = normalize_encode_params(val)
+ end.with_indifferent_access
+ end
+ else
+ params
+ end
+ end
+
+ def self.handle_array(params)
+ params.map! { |el| normalize_encode_params(el) }
+ end
+ end
+
+ # Remove nils from the params hash.
+ class NoNilParamEncoder < ParamEncoder # :nodoc:
+ def self.handle_array(params)
+ list = super
+ list.compact!
+ list
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
new file mode 100644
index 0000000000..72f7407c6e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+
+module ActionDispatch
+ # The routing module provides URL rewriting in native Ruby. It's a way to
+ # redirect incoming requests to controllers and actions. This replaces
+ # mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
+ # Routes are defined in <tt>config/routes.rb</tt>.
+ #
+ # Think of creating routes as drawing a map for your requests. The map tells
+ # them where to go based on some predefined pattern:
+ #
+ # Rails.application.routes.draw do
+ # Pattern 1 tells some request to go to one place
+ # Pattern 2 tell them to go to another
+ # ...
+ # end
+ #
+ # The following symbols are special:
+ #
+ # :controller maps to your controller name
+ # :action maps to an action with your controllers
+ #
+ # Other names simply map to a parameter as in the case of <tt>:id</tt>.
+ #
+ # == Resources
+ #
+ # Resource routing allows you to quickly declare all of the common routes
+ # for a given resourceful controller. Instead of declaring separate routes
+ # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
+ # actions, a resourceful route declares them in a single line of code:
+ #
+ # resources :photos
+ #
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the profile of
+ # the currently logged in user. In this case, you can use a singular resource
+ # to map /profile (rather than /profile/:id) to the show action.
+ #
+ # resource :profile
+ #
+ # It's common to have resources that are logically children of other
+ # resources:
+ #
+ # resources :magazines do
+ # resources :ads
+ # end
+ #
+ # You may wish to organize groups of controllers under a namespace. Most
+ # commonly, you might group a number of administrative controllers under
+ # an +admin+ namespace. You would place these controllers under the
+ # <tt>app/controllers/admin</tt> directory, and you can group them together
+ # in your router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # Alternatively, you can add prefixes to your path without using a separate
+ # directory by using +scope+. +scope+ takes additional options which
+ # apply to all enclosed routes.
+ #
+ # scope path: "/cpanel", as: 'admin' do
+ # resources :posts, :comments
+ # end
+ #
+ # For more, see <tt>Routing::Mapper::Resources#resources</tt>,
+ # <tt>Routing::Mapper::Scoping#namespace</tt>, and
+ # <tt>Routing::Mapper::Scoping#scope</tt>.
+ #
+ # == Non-resourceful routes
+ #
+ # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
+ # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
+ #
+ # get 'post/:id' => 'posts#show'
+ # post 'post/:id' => 'posts#create_comment'
+ #
+ # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
+ # URL will route to the <tt>show</tt> action.
+ #
+ # If your route needs to respond to more than one HTTP method (or all methods) then using the
+ # <tt>:via</tt> option on <tt>match</tt> is preferable.
+ #
+ # match 'post/:id' => 'posts#show', via: [:get, :post]
+ #
+ # == Named routes
+ #
+ # Routes can be named by passing an <tt>:as</tt> option,
+ # allowing for easy reference within your source as +name_of_route_url+
+ # for the full URL and +name_of_route_path+ for the URI path.
+ #
+ # Example:
+ #
+ # # In config/routes.rb
+ # get '/login' => 'accounts#login', as: 'login'
+ #
+ # # With render, redirect_to, tests, etc.
+ # redirect_to login_url
+ #
+ # Arguments can be passed as well.
+ #
+ # redirect_to show_item_path(id: 25)
+ #
+ # Use <tt>root</tt> as a shorthand to name a route for the root path "/".
+ #
+ # # In config/routes.rb
+ # root to: 'blogs#index'
+ #
+ # # would recognize http://www.example.com/ as
+ # params = { controller: 'blogs', action: 'index' }
+ #
+ # # and provide these named routes
+ # root_url # => 'http://www.example.com/'
+ # root_path # => '/'
+ #
+ # Note: when using +controller+, the route is simply named after the
+ # method you call on the block parameter rather than map.
+ #
+ # # In config/routes.rb
+ # controller :blog do
+ # get 'blog/show' => :list
+ # get 'blog/delete' => :delete
+ # get 'blog/edit' => :edit
+ # end
+ #
+ # # provides named routes for show, delete, and edit
+ # link_to @article.title, blog_show_path(id: @article.id)
+ #
+ # == Pretty URLs
+ #
+ # Routes can generate pretty URLs. For example:
+ #
+ # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # year: /\d{4}/,
+ # month: /\d{1,2}/,
+ # day: /\d{1,2}/
+ # }
+ #
+ # Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
+ # maps to
+ #
+ # params = {year: '2005', month: '11', day: '06'}
+ #
+ # == Regular Expressions and parameters
+ # You can specify a regular expression to define a format for a parameter.
+ #
+ # controller 'geocode' do
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /\d{5}(-\d{4})?/
+ # }
+ # end
+ #
+ # Constraints can include the 'ignorecase' and 'extended syntax' regular
+ # expression modifiers:
+ #
+ # controller 'geocode' do
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /hx\d\d\s\d[a-z]{2}/i
+ # }
+ # end
+ #
+ # controller 'geocode' do
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /# Postalcode format
+ # \d{5} #Prefix
+ # (-\d{4})? #Suffix
+ # /x
+ # }
+ # end
+ #
+ # Using the multiline modifier will raise an +ArgumentError+.
+ # Encoding regular expression modifiers are silently ignored. The
+ # match will always use the default encoding or ASCII.
+ #
+ # == External redirects
+ #
+ # You can redirect any path to another path using the redirect helper in your router:
+ #
+ # get "/stories" => redirect("/posts")
+ #
+ # == Unicode character routes
+ #
+ # You can specify unicode character routes in your router:
+ #
+ # get "こんにちは" => "welcome#index"
+ #
+ # == Routing to Rack Applications
+ #
+ # Instead of a String, like <tt>posts#index</tt>, which corresponds to the
+ # index action in the PostsController, you can specify any Rack application
+ # as the endpoint for a matcher:
+ #
+ # get "/application.js" => Sprockets
+ #
+ # == Reloading routes
+ #
+ # You can reload routes if you feel you must:
+ #
+ # Rails.application.reload_routes!
+ #
+ # This will clear all named routes and reload config/routes.rb if the file has been modified from
+ # last load. To absolutely force reloading, use <tt>reload!</tt>.
+ #
+ # == Testing Routes
+ #
+ # The two main methods for testing your routes:
+ #
+ # === +assert_routing+
+ #
+ # def test_movie_route_properly_splits
+ # opts = {controller: "plugin", action: "checkout", id: "2"}
+ # assert_routing "plugin/checkout/2", opts
+ # end
+ #
+ # +assert_routing+ lets you test whether or not the route properly resolves into options.
+ #
+ # === +assert_recognizes+
+ #
+ # def test_route_has_options
+ # opts = {controller: "plugin", action: "show", id: "12"}
+ # assert_recognizes opts, "/plugins/show/12"
+ # end
+ #
+ # Note the subtle difference between the two: +assert_routing+ tests that
+ # a URL fits options while +assert_recognizes+ tests that a URL
+ # breaks into parameters properly.
+ #
+ # In tests you can simply pass the URL or named route to +get+ or +post+.
+ #
+ # def send_to_jail
+ # get '/jail'
+ # assert_response :success
+ # end
+ #
+ # def goes_to_login
+ # get login_url
+ # #...
+ # end
+ #
+ # == View a list of all your routes
+ #
+ # rails routes
+ #
+ # Target specific controllers by prefixing the command with <tt>-c</tt> option.
+ #
+ module Routing
+ extend ActiveSupport::Autoload
+
+ autoload :Mapper
+ autoload :RouteSet
+ autoload :RoutesProxy
+ autoload :UrlFor
+ autoload :PolymorphicRoutes
+
+ SEPARATORS = %w( / . ? ) #:nodoc:
+ HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
new file mode 100644
index 0000000000..24dced1efd
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Routing
+ class Endpoint # :nodoc:
+ def dispatcher?; false; end
+ def redirect?; false; end
+ def engine?; rack_app.respond_to?(:routes); end
+ def matches?(req); true; end
+ def app; self; end
+ def rack_app; app; end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
new file mode 100644
index 0000000000..22336c59b6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require "delegate"
+
+module ActionDispatch
+ module Routing
+ class RouteWrapper < SimpleDelegator
+ def endpoint
+ app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
+ end
+
+ def constraints
+ requirements.except(:controller, :action)
+ end
+
+ def rack_app
+ app.rack_app
+ end
+
+ def path
+ super.spec.to_s
+ end
+
+ def name
+ super.to_s
+ end
+
+ def reqs
+ @reqs ||= begin
+ reqs = endpoint
+ reqs += " #{constraints}" unless constraints.empty?
+ reqs
+ end
+ end
+
+ def controller
+ parts.include?(:controller) ? ":controller" : requirements[:controller]
+ end
+
+ def action
+ parts.include?(:action) ? ":action" : requirements[:action]
+ end
+
+ def internal?
+ internal
+ end
+
+ def engine?
+ app.engine?
+ end
+ end
+
+ ##
+ # This class is just used for displaying route information when someone
+ # executes `rails routes` or looks at the RoutingError page.
+ # People should not use this class.
+ class RoutesInspector # :nodoc:
+ def initialize(routes)
+ @engines = {}
+ @routes = routes
+ end
+
+ def format(formatter, filter = nil)
+ routes_to_display = filter_routes(normalize_filter(filter))
+ routes = collect_routes(routes_to_display)
+ if routes.none?
+ formatter.no_routes(collect_routes(@routes))
+ return formatter.result
+ end
+
+ formatter.header routes
+ formatter.section routes
+
+ @engines.each do |name, engine_routes|
+ formatter.section_title "Routes for #{name}"
+ formatter.section engine_routes
+ end
+
+ formatter.result
+ end
+
+ private
+
+ def normalize_filter(filter)
+ if filter.is_a?(Hash) && filter[:controller]
+ { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
+ elsif filter
+ { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+ end
+ end
+
+ def filter_routes(filter)
+ if filter
+ @routes.select do |route|
+ route_wrapper = RouteWrapper.new(route)
+ filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ end
+ else
+ @routes
+ end
+ end
+
+ def collect_routes(routes)
+ routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject(&:internal?).collect do |route|
+ collect_engine_routes(route)
+
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
+ end
+ end
+
+ def collect_engine_routes(route)
+ name = route.endpoint
+ return unless route.engine?
+ return if @engines[name]
+
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
+ end
+ end
+
+ class ConsoleFormatter
+ def initialize
+ @buffer = []
+ end
+
+ def result
+ @buffer.join("\n")
+ end
+
+ def section_title(title)
+ @buffer << "\n#{title}:"
+ end
+
+ def section(routes)
+ @buffer << draw_section(routes)
+ end
+
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
+
+ def no_routes(routes)
+ @buffer <<
+ if routes.none?
+ <<~MESSAGE
+ You don't have any routes defined!
+
+ Please add some routes in config/routes.rb.
+ MESSAGE
+ else
+ "No routes were found for this controller"
+ end
+ @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
+ end
+
+ private
+ def draw_section(routes)
+ header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
+ end
+
+ def draw_header(routes)
+ name_width, verb_width, path_width = widths(routes)
+
+ "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ end
+
+ def widths(routes)
+ [routes.map { |r| r[:name].length }.max || 0,
+ routes.map { |r| r[:verb].length }.max || 0,
+ routes.map { |r| r[:path].length }.max || 0]
+ end
+ end
+
+ class HtmlTableFormatter
+ def initialize(view)
+ @view = view
+ @buffer = []
+ end
+
+ def section_title(title)
+ @buffer << %(<tr><th colspan="4">#{title}</th></tr>)
+ end
+
+ def section(routes)
+ @buffer << @view.render(partial: "routes/route", collection: routes)
+ end
+
+ # The header is part of the HTML page, so we don't construct it here.
+ def header(routes)
+ end
+
+ def no_routes(*)
+ @buffer << <<~MESSAGE
+ <p>You don't have any routes defined!</p>
+ <ul>
+ <li>Please add some routes in <tt>config/routes.rb</tt>.</li>
+ <li>
+ For more information about routes, please see the Rails guide
+ <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
+ </li>
+ </ul>
+ MESSAGE
+ end
+
+ def result
+ @view.raw @view.render(layout: "routes/table") {
+ @view.raw @buffer.join("\n")
+ }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
new file mode 100644
index 0000000000..f3970d5445
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -0,0 +1,2266 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/regexp"
+require "action_dispatch/routing/redirection"
+require "action_dispatch/routing/endpoint"
+
+module ActionDispatch
+ module Routing
+ class Mapper
+ URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
+
+ class Constraints < Routing::Endpoint #:nodoc:
+ attr_reader :app, :constraints
+
+ SERVE = ->(app, req) { app.serve req }
+ CALL = ->(app, req) { app.call req.env }
+
+ def initialize(app, constraints, strategy)
+ # Unwrap Constraints objects. I don't actually think it's possible
+ # to pass a Constraints object to this constructor, but there were
+ # multiple places that kept testing children of this object. I
+ # *think* they were just being defensive, but I have no idea.
+ if app.is_a?(self.class)
+ constraints += app.constraints
+ app = app.app
+ end
+
+ @strategy = strategy
+
+ @app, @constraints, = app, constraints
+ end
+
+ def dispatcher?; @strategy == SERVE; end
+
+ def matches?(req)
+ @constraints.all? do |constraint|
+ (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
+ (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
+ end
+ end
+
+ def serve(req)
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
+
+ @strategy.call @app, req
+ end
+
+ private
+ def constraint_args(constraint, request)
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
+ end
+ end
+
+ class Mapping #:nodoc:
+ ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
+
+ attr_reader :requirements, :defaults
+ attr_reader :to, :default_controller, :default_action
+ attr_reader :required_defaults, :ast
+
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ options = scope[:options].merge(options) if scope[:options]
+
+ defaults = (scope[:defaults] || {}).dup
+ scope_constraints = scope[:constraints] || {}
+
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ end
+
+ def self.check_via(via)
+ if via.empty?
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
+ "If you want to expose your action to GET, use `get` in the router:\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise ArgumentError, msg
+ end
+ via
+ end
+
+ def self.normalize_path(path, format)
+ path = Mapper.normalize_path(path)
+
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
+ end
+ end
+
+ def self.optional_format?(path, format)
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
+ end
+
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
+ @defaults = defaults
+ @set = set
+
+ @to = to
+ @default_controller = controller
+ @default_action = default_action
+ @ast = ast
+ @anchor = anchor
+ @via = via
+ @internal = options.delete(:internal)
+
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
+
+ options = add_wildcard_options(options, formatted, ast)
+
+ options = normalize_options!(options, path_params, modyoule)
+
+ split_options = constraints(options, path_params)
+
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
+
+ if options_constraints.is_a?(Hash)
+ @defaults = Hash[options_constraints.find_all { |key, default|
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
+ }].merge @defaults
+ @blocks = blocks
+ constraints.merge! options_constraints
+ else
+ @blocks = blocks(options_constraints)
+ end
+
+ requirements, conditions = split_constraints path_params, constraints
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
+
+ formats = normalize_format(formatted)
+
+ @requirements = formats[:requirements].merge Hash[requirements]
+ @conditions = Hash[conditions]
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+
+ if path_params.include?(:action) && !@requirements.key?(:action)
+ @defaults[:action] ||= "index"
+ end
+
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
+ end
+
+ def make_route(name, precedence)
+ route = Journey::Route.new(name,
+ application,
+ path,
+ conditions,
+ required_defaults,
+ defaults,
+ request_method,
+ precedence,
+ @internal)
+
+ route
+ end
+
+ def application
+ app(@blocks)
+ end
+
+ def path
+ build_path @ast, requirements, @anchor
+ end
+
+ def conditions
+ build_conditions @conditions, @set.request_class
+ end
+
+ def build_conditions(current_conditions, request_class)
+ conditions = current_conditions.dup
+
+ conditions.keep_if do |k, _|
+ request_class.public_method_defined?(k)
+ end
+ end
+ private :build_conditions
+
+ def request_method
+ @via.map { |x| Journey::Route.verb_matcher(x) }
+ end
+ private :request_method
+
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
+
+ def build_path(ast, requirements, anchor)
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
+
+ # Find all the symbol nodes that are adjacent to literal nodes and alter
+ # the regexp so that Journey will partition them into custom routes.
+ ast.find_all { |node|
+ next unless node.cat?
+
+ if node.left.literal? && node.right.symbol?
+ symbol = node.right
+ elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
+ symbol = node.right.left
+ elsif node.left.symbol? && node.right.literal?
+ symbol = node.left
+ elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
+ symbol = node.left
+ else
+ next
+ end
+
+ if symbol
+ symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
+ end
+ }
+
+ pattern
+ end
+ private :build_path
+
+ private
+ def add_wildcard_options(options, formatted, path_ast)
+ # Add a constraint for wildcard route to make it non-greedy and match the
+ # optional format part of the route by default.
+ if formatted != false
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
+ hash[node.name.to_sym] ||= /.+?/
+ }.merge options
+ else
+ options
+ end
+ end
+
+ def normalize_options!(options, path_params, modyoule)
+ if path_params.include?(:controller)
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
+
+ # Add a default constraint for :controller path segments that matches namespaced
+ # controllers with default routes like :controller/:action/:id(.:format), e.g:
+ # GET /admin/products/show/1
+ # => { controller: 'admin/products', action: 'show', id: '1' }
+ options[:controller] ||= /.+?/
+ end
+
+ if to.respond_to?(:action) || to.respond_to?(:call)
+ options
+ else
+ to_endpoint = split_to to
+ controller = to_endpoint[0] || default_controller
+ action = to_endpoint[1] || default_action
+
+ controller = add_controller_module(controller, modyoule)
+
+ options.merge! check_controller_and_action(path_params, controller, action)
+ end
+ end
+
+ def split_constraints(path_params, constraints)
+ constraints.partition do |key, requirement|
+ path_params.include?(key) || key == :controller
+ end
+ end
+
+ def normalize_format(formatted)
+ case formatted
+ when true
+ { requirements: { format: /.+/ },
+ defaults: {} }
+ when Regexp
+ { requirements: { format: formatted },
+ defaults: { format: nil } }
+ when String
+ { requirements: { format: Regexp.compile(formatted) },
+ defaults: { format: formatted } }
+ else
+ { requirements: {}, defaults: {} }
+ end
+ end
+
+ def verify_regexp_requirements(requirements)
+ requirements.each do |requirement|
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
+ end
+
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ end
+ end
+ end
+
+ def normalize_defaults(options)
+ Hash[options.reject { |_, default| Regexp === default }]
+ end
+
+ def app(blocks)
+ if to.respond_to?(:action)
+ Routing::RouteSet::StaticDispatcher.new to
+ elsif to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
+ else
+ dispatcher(defaults.key?(:controller))
+ end
+ end
+
+ def check_controller_and_action(path_params, controller, action)
+ hash = check_part(:controller, controller, path_params, {}) do |part|
+ translate_controller(part) {
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems.".dup
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
+
+ raise ArgumentError, message
+ }
+ end
+
+ check_part(:action, action, path_params, hash) { |part|
+ part.is_a?(Regexp) ? part : part.to_s
+ }
+ end
+
+ def check_part(name, part, path_params, hash)
+ if part
+ hash[name] = yield(part)
+ else
+ unless path_params.include?(name)
+ message = "Missing :#{name} key on routes definition, please check your routes."
+ raise ArgumentError, message
+ end
+ end
+ hash
+ end
+
+ def split_to(to)
+ if to =~ /#/
+ to.split("#")
+ else
+ []
+ end
+ end
+
+ def add_controller_module(controller, modyoule)
+ if modyoule && !controller.is_a?(Regexp)
+ if controller =~ %r{\A/}
+ controller[1..-1]
+ else
+ [modyoule, controller].compact.join("/")
+ end
+ else
+ controller
+ end
+ end
+
+ def translate_controller(controller)
+ return controller if Regexp === controller
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
+
+ yield
+ end
+
+ def blocks(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
+ end
+ [callable_constraint]
+ end
+
+ def constraints(options, path_params)
+ options.group_by do |key, option|
+ if Regexp === option
+ :constraints
+ else
+ if path_params.include?(key)
+ :path_params
+ else
+ :required_defaults
+ end
+ end
+ end
+ end
+
+ def dispatcher(raise_on_name_error)
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
+ end
+ end
+
+ # Invokes Journey::Router::Utils.normalize_path and ensure that
+ # (:locale) becomes (/:locale) instead of /(:locale). Except
+ # for root cases, where the latter is the correct one.
+ def self.normalize_path(path)
+ path = Journey::Router::Utils.normalize_path(path)
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
+ path
+ end
+
+ def self.normalize_name(name)
+ normalize_path(name)[1..-1].tr("/", "_")
+ end
+
+ module Base
+ # Matches a URL pattern to one or more routes.
+ #
+ # You should not use the +match+ method in your router
+ # without specifying an HTTP method.
+ #
+ # If you want to expose your action to both GET and POST, use:
+ #
+ # # sets :controller, :action and :id in params
+ # match ':controller/:action/:id', via: [:get, :post]
+ #
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as URL
+ # query parameters and thus available through +params+ in an action.
+ #
+ # If you want to expose your action to GET, use +get+ in the router:
+ #
+ # Instead of:
+ #
+ # match ":controller/:action/:id"
+ #
+ # Do:
+ #
+ # get ":controller/:action/:id"
+ #
+ # Two of these symbols are special, +:controller+ maps to the controller
+ # and +:action+ to the controller's action. A pattern can also map
+ # wildcard segments (globs) to params:
+ #
+ # get 'songs/*category/:title', to: 'songs#show'
+ #
+ # # 'songs/rock/classic/stairway-to-heaven' sets
+ # # params[:category] = 'rock/classic'
+ # # params[:title] = 'stairway-to-heaven'
+ #
+ # To match a wildcard parameter, it must have a name assigned to it.
+ # Without a variable name to attach the glob parameter to, the route
+ # can't be parsed.
+ #
+ # When a pattern points to an internal route, the route's +:action+ and
+ # +:controller+ should be set in options or hash shorthand. Examples:
+ #
+ # match 'photos/:id' => 'photos#show', via: :get
+ # match 'photos/:id', to: 'photos#show', via: :get
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
+ #
+ # A pattern can also point to a +Rack+ endpoint i.e. anything that
+ # responds to +call+:
+ #
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
+ # match 'photos/:id', to: PhotoRackApp, via: :get
+ # # Yes, controller actions are just rack endpoints
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
+ #
+ # Because requesting various HTTP verbs with a single action has security
+ # implications, you must either specify the actions in
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
+ # instead +match+
+ #
+ # === Options
+ #
+ # Any options not seen here are passed on as params with the URL.
+ #
+ # [:controller]
+ # The route's controller.
+ #
+ # [:action]
+ # The route's action.
+ #
+ # [:param]
+ # Overrides the default resource identifier +:id+ (name of the
+ # dynamic segment used to generate the routes).
+ # You can access that segment from your controller using
+ # <tt>params[<:param>]</tt>.
+ # In your router:
+ #
+ # resources :users, param: :name
+ #
+ # The +users+ resource here will have the following routes generated for it:
+ #
+ # GET /users(.:format)
+ # POST /users(.:format)
+ # GET /users/new(.:format)
+ # GET /users/:name/edit(.:format)
+ # GET /users/:name(.:format)
+ # PATCH/PUT /users/:name(.:format)
+ # DELETE /users/:name(.:format)
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to construct a URL:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
+ #
+ # [:path]
+ # The path prefix for the routes.
+ #
+ # [:module]
+ # The namespace for :controller.
+ #
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
+ # # => Sekret::PostsController
+ #
+ # See <tt>Scoping#namespace</tt> for its scope equivalent.
+ #
+ # [:as]
+ # The name used to generate routing helpers.
+ #
+ # [:via]
+ # Allowed HTTP verb(s) for route.
+ #
+ # match 'path', to: 'c#a', via: :get
+ # match 'path', to: 'c#a', via: [:get, :post]
+ # match 'path', to: 'c#a', via: :all
+ #
+ # [:to]
+ # Points to a +Rack+ endpoint. Can be an object that responds to
+ # +call+ or a string representing a controller's action.
+ #
+ # match 'path', to: 'controller#action', via: :get
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
+ # match 'path', to: RackApp, via: :get
+ #
+ # [:on]
+ # Shorthand for wrapping routes in a specific RESTful context. Valid
+ # values are +:member+, +:collection+, and +:new+. Only use within
+ # <tt>resource(s)</tt> block. For example:
+ #
+ # resource :bar do
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
+ # end
+ #
+ # Is equivalent to:
+ #
+ # resource :bar do
+ # member do
+ # match 'foo', to: 'c#a', via: [:get, :post]
+ # end
+ # end
+ #
+ # [:constraints]
+ # Constrains parameters with a hash of regular expressions
+ # or an object that responds to <tt>matches?</tt>. In addition, constraints
+ # other than path can also be specified with any object
+ # that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
+ #
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
+ #
+ # match 'json_only', constraints: { format: 'json' }, via: :get
+ #
+ # class Whitelist
+ # def matches?(request) request.remote_ip == '1.2.3.4' end
+ # end
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
+ #
+ # See <tt>Scoping#constraints</tt> for more examples with its scope
+ # equivalent.
+ #
+ # [:defaults]
+ # Sets defaults for parameters
+ #
+ # # Sets params[:format] to 'jpg' by default
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
+ #
+ # See <tt>Scoping#defaults</tt> for its scope equivalent.
+ #
+ # [:anchor]
+ # Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
+ # false, the pattern matches any request prefixed with the given path.
+ #
+ # # Matches any request starting with 'path'
+ # match 'path', to: 'c#a', anchor: false, via: :get
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
+ def match(path, options = nil)
+ end
+
+ # Mount a Rack-based application to be used within the application.
+ #
+ # mount SomeRackApp, at: "some_route"
+ #
+ # Alternatively:
+ #
+ # mount(SomeRackApp => "some_route")
+ #
+ # For options, see +match+, as +mount+ uses it internally.
+ #
+ # All mounted applications come with routing helpers to access them.
+ # These are named after the class specified, so for the above example
+ # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
+ # To customize this helper's name, use the +:as+ option:
+ #
+ # mount(SomeRackApp => "some_route", as: "exciting")
+ #
+ # This will generate the +exciting_path+ and +exciting_url+ helpers
+ # which can be used to navigate to this mounted app.
+ def mount(app, options = nil)
+ if options
+ path = options.delete(:at)
+ elsif Hash === app
+ options = app
+ app, path = options.find { |k, _| k.respond_to?(:call) }
+ options.delete(app) if app
+ end
+
+ raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
+ raise ArgumentError, <<~MSG unless path
+ Must be called with mount point
+
+ mount SomeRackApp, at: "some_route"
+ or
+ mount(SomeRackApp => "some_route")
+ MSG
+
+ rails_app = rails_app? app
+ options[:as] ||= app_name(app, rails_app)
+
+ target_as = name_for_action(options[:as], path)
+ options[:via] ||= :all
+
+ match(path, options.merge(to: app, anchor: false, format: false))
+
+ define_generate_prefix(app, target_as) if rails_app
+ self
+ end
+
+ def default_url_options=(options)
+ @set.default_url_options = options
+ end
+ alias_method :default_url_options, :default_url_options=
+
+ def with_default_scope(scope, &block)
+ scope(scope) do
+ instance_exec(&block)
+ end
+ end
+
+ # Query if the following named route was already defined.
+ def has_named_route?(name)
+ @set.named_routes.key? name
+ end
+
+ private
+ def rails_app?(app)
+ app.is_a?(Class) && app < Rails::Railtie
+ end
+
+ def app_name(app, rails_app)
+ if rails_app
+ app.railtie_name
+ elsif app.is_a?(Class)
+ class_name = app.name
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
+ end
+ end
+
+ def define_generate_prefix(app, name)
+ _route = @set.named_routes.get name
+ _routes = @set
+
+ script_namer = ->(options) do
+ prefix_options = options.slice(*_route.segment_keys)
+ prefix_options[:relative_url_root] = "".freeze
+
+ if options[:_recall]
+ prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
+ end
+
+ # We must actually delete prefix segment keys to avoid passing them to next url_for.
+ _route.segment_keys.each { |k| options.delete(k) }
+ _routes.url_helpers.send("#{name}_path", prefix_options)
+ end
+
+ app.routes.define_mounted_helper(name, script_namer)
+
+ app.routes.extend Module.new {
+ def optimize_routes_generation?; false; end
+
+ define_method :find_script_name do |options|
+ if options.key? :script_name
+ super(options)
+ else
+ script_namer.call(options)
+ end
+ end
+ }
+ end
+ end
+
+ module HttpHelpers
+ # Define a route that only recognizes HTTP GET.
+ # For supported arguments, see match[rdoc-ref:Base#match]
+ #
+ # get 'bacon', to: 'food#bacon'
+ def get(*args, &block)
+ map_method(:get, args, &block)
+ end
+
+ # Define a route that only recognizes HTTP POST.
+ # For supported arguments, see match[rdoc-ref:Base#match]
+ #
+ # post 'bacon', to: 'food#bacon'
+ def post(*args, &block)
+ map_method(:post, args, &block)
+ end
+
+ # Define a route that only recognizes HTTP PATCH.
+ # For supported arguments, see match[rdoc-ref:Base#match]
+ #
+ # patch 'bacon', to: 'food#bacon'
+ def patch(*args, &block)
+ map_method(:patch, args, &block)
+ end
+
+ # Define a route that only recognizes HTTP PUT.
+ # For supported arguments, see match[rdoc-ref:Base#match]
+ #
+ # put 'bacon', to: 'food#bacon'
+ def put(*args, &block)
+ map_method(:put, args, &block)
+ end
+
+ # Define a route that only recognizes HTTP DELETE.
+ # For supported arguments, see match[rdoc-ref:Base#match]
+ #
+ # delete 'broccoli', to: 'food#broccoli'
+ def delete(*args, &block)
+ map_method(:delete, args, &block)
+ end
+
+ private
+ def map_method(method, args, &block)
+ options = args.extract_options!
+ options[:via] = method
+ match(*args, options, &block)
+ self
+ end
+ end
+
+ # You may wish to organize groups of controllers under a namespace.
+ # Most commonly, you might group a number of administrative controllers
+ # under an +admin+ namespace. You would place these controllers under
+ # the <tt>app/controllers/admin</tt> directory, and you can group them
+ # together in your router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # This will create a number of routes for each of the posts and comments
+ # controller. For <tt>Admin::PostsController</tt>, Rails will create:
+ #
+ # GET /admin/posts
+ # GET /admin/posts/new
+ # POST /admin/posts
+ # GET /admin/posts/1
+ # GET /admin/posts/1/edit
+ # PATCH/PUT /admin/posts/1
+ # DELETE /admin/posts/1
+ #
+ # If you want to route /posts (without the prefix /admin) to
+ # <tt>Admin::PostsController</tt>, you could use
+ #
+ # scope module: "admin" do
+ # resources :posts
+ # end
+ #
+ # or, for a single case
+ #
+ # resources :posts, module: "admin"
+ #
+ # If you want to route /admin/posts to +PostsController+
+ # (without the <tt>Admin::</tt> module prefix), you could use
+ #
+ # scope "/admin" do
+ # resources :posts
+ # end
+ #
+ # or, for a single case
+ #
+ # resources :posts, path: "/admin/posts"
+ #
+ # In each of these cases, the named routes remain the same as if you did
+ # not use scope. In the last case, the following paths map to
+ # +PostsController+:
+ #
+ # GET /admin/posts
+ # GET /admin/posts/new
+ # POST /admin/posts
+ # GET /admin/posts/1
+ # GET /admin/posts/1/edit
+ # PATCH/PUT /admin/posts/1
+ # DELETE /admin/posts/1
+ module Scoping
+ # Scopes a set of routes to the given default options.
+ #
+ # Take the following route definition as an example:
+ #
+ # scope path: ":account_id", as: "account" do
+ # resources :projects
+ # end
+ #
+ # This generates helpers such as +account_projects_path+, just like +resources+ does.
+ # The difference here being that the routes generated are like /:account_id/projects,
+ # rather than /accounts/:account_id/projects.
+ #
+ # === Options
+ #
+ # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
+ #
+ # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
+ # scope module: "admin" do
+ # resources :posts
+ # end
+ #
+ # # prefix the posts resource's requests with '/admin'
+ # scope path: "/admin" do
+ # resources :posts
+ # end
+ #
+ # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
+ # scope as: "sekret" do
+ # resources :posts
+ # end
+ def scope(*args)
+ options = args.extract_options!.dup
+ scope = {}
+
+ options[:path] = args.flatten.join("/") if args.any?
+ options[:constraints] ||= {}
+
+ unless nested_scope?
+ options[:shallow_path] ||= options[:path] if options.key?(:path)
+ options[:shallow_prefix] ||= options[:as] if options.key?(:as)
+ end
+
+ if options[:constraints].is_a?(Hash)
+ defaults = options[:constraints].select do |k, v|
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
+ end
+
+ options[:defaults] = defaults.merge(options[:defaults] || {})
+ else
+ block, options[:constraints] = options[:constraints], {}
+ end
+
+ if options.key?(:only) || options.key?(:except)
+ scope[:action_options] = { only: options.delete(:only),
+ except: options.delete(:except) }
+ end
+
+ if options.key? :anchor
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
+ end
+
+ @scope.options.each do |option|
+ if option == :blocks
+ value = block
+ elsif option == :options
+ value = options
+ else
+ value = options.delete(option) { POISON }
+ end
+
+ unless POISON == value
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ end
+ end
+
+ @scope = @scope.new scope
+ yield
+ self
+ ensure
+ @scope = @scope.parent
+ end
+
+ POISON = Object.new # :nodoc:
+
+ # Scopes routes to a specific controller
+ #
+ # controller "food" do
+ # match "bacon", action: :bacon, via: :get
+ # end
+ def controller(controller)
+ @scope = @scope.new(controller: controller)
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ # Scopes routes to a specific namespace. For example:
+ #
+ # namespace :admin do
+ # resources :posts
+ # end
+ #
+ # This generates the following routes:
+ #
+ # admin_posts GET /admin/posts(.:format) admin/posts#index
+ # admin_posts POST /admin/posts(.:format) admin/posts#create
+ # new_admin_post GET /admin/posts/new(.:format) admin/posts#new
+ # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
+ # admin_post GET /admin/posts/:id(.:format) admin/posts#show
+ # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
+ # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
+ #
+ # === Options
+ #
+ # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
+ # options all default to the name of the namespace.
+ #
+ # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
+ # <tt>Resources#resources</tt>.
+ #
+ # # accessible through /sekret/posts rather than /admin/posts
+ # namespace :admin, path: "sekret" do
+ # resources :posts
+ # end
+ #
+ # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
+ # namespace :admin, module: "sekret" do
+ # resources :posts
+ # end
+ #
+ # # generates +sekret_posts_path+ rather than +admin_posts_path+
+ # namespace :admin, as: "sekret" do
+ # resources :posts
+ # end
+ def namespace(path, options = {})
+ path = path.to_s
+
+ defaults = {
+ module: path,
+ as: options.fetch(:as, path),
+ shallow_path: options.fetch(:path, path),
+ shallow_prefix: options.fetch(:as, path)
+ }
+
+ path_scope(options.delete(:path) { path }) do
+ scope(defaults.merge!(options)) { yield }
+ end
+ end
+
+ # === Parameter Restriction
+ # Allows you to constrain the nested routes based on a set of rules.
+ # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
+ #
+ # constraints(id: /\d+\.\d+/) do
+ # resources :posts
+ # end
+ #
+ # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
+ # The +id+ parameter must match the constraint passed in for this example.
+ #
+ # You may use this to also restrict other parameters:
+ #
+ # resources :posts do
+ # constraints(post_id: /\d+\.\d+/) do
+ # resources :comments
+ # end
+ # end
+ #
+ # === Restricting based on IP
+ #
+ # Routes can also be constrained to an IP or a certain range of IP addresses:
+ #
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
+ # resources :posts
+ # end
+ #
+ # Any user connecting from the 192.168.* range will be able to see this resource,
+ # where as any user connecting outside of this range will be told there is no such route.
+ #
+ # === Dynamic request matching
+ #
+ # Requests to routes can be constrained based on specific criteria:
+ #
+ # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
+ # resources :iphones
+ # end
+ #
+ # You are able to move this logic out into a class if it is too complex for routes.
+ # This class must have a +matches?+ method defined on it which either returns +true+
+ # if the user should be given access to that route, or +false+ if the user should not.
+ #
+ # class Iphone
+ # def self.matches?(request)
+ # request.env["HTTP_USER_AGENT"] =~ /iPhone/
+ # end
+ # end
+ #
+ # An expected place for this code would be +lib/constraints+.
+ #
+ # This class is then used like this:
+ #
+ # constraints(Iphone) do
+ # resources :iphones
+ # end
+ def constraints(constraints = {})
+ scope(constraints: constraints) { yield }
+ end
+
+ # Allows you to set default parameters for a route, such as this:
+ # defaults id: 'home' do
+ # match 'scoped_pages/(:id)', to: 'pages#show'
+ # end
+ # Using this, the +:id+ parameter here will default to 'home'.
+ def defaults(defaults = {})
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ private
+ def merge_path_scope(parent, child)
+ Mapper.normalize_path("#{parent}/#{child}")
+ end
+
+ def merge_shallow_path_scope(parent, child)
+ Mapper.normalize_path("#{parent}/#{child}")
+ end
+
+ def merge_as_scope(parent, child)
+ parent ? "#{parent}_#{child}" : child
+ end
+
+ def merge_shallow_prefix_scope(parent, child)
+ parent ? "#{parent}_#{child}" : child
+ end
+
+ def merge_module_scope(parent, child)
+ parent ? "#{parent}/#{child}" : child
+ end
+
+ def merge_controller_scope(parent, child)
+ child
+ end
+
+ def merge_action_scope(parent, child)
+ child
+ end
+
+ def merge_via_scope(parent, child)
+ child
+ end
+
+ def merge_format_scope(parent, child)
+ child
+ end
+
+ def merge_path_names_scope(parent, child)
+ merge_options_scope(parent, child)
+ end
+
+ def merge_constraints_scope(parent, child)
+ merge_options_scope(parent, child)
+ end
+
+ def merge_defaults_scope(parent, child)
+ merge_options_scope(parent, child)
+ end
+
+ def merge_blocks_scope(parent, child)
+ merged = parent ? parent.dup : []
+ merged << child if child
+ merged
+ end
+
+ def merge_options_scope(parent, child)
+ (parent || {}).merge(child)
+ end
+
+ def merge_shallow_scope(parent, child)
+ child ? true : false
+ end
+
+ def merge_to_scope(parent, child)
+ child
+ end
+ end
+
+ # Resource routing allows you to quickly declare all of the common routes
+ # for a given resourceful controller. Instead of declaring separate routes
+ # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
+ # actions, a resourceful route declares them in a single line of code:
+ #
+ # resources :photos
+ #
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the profile of
+ # the currently logged in user. In this case, you can use a singular resource
+ # to map /profile (rather than /profile/:id) to the show action.
+ #
+ # resource :profile
+ #
+ # It's common to have resources that are logically children of other
+ # resources:
+ #
+ # resources :magazines do
+ # resources :ads
+ # end
+ #
+ # You may wish to organize groups of controllers under a namespace. Most
+ # commonly, you might group a number of administrative controllers under
+ # an +admin+ namespace. You would place these controllers under the
+ # <tt>app/controllers/admin</tt> directory, and you can group them together
+ # in your router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # By default the +:id+ parameter doesn't accept dots. If you need to
+ # use dots as part of the +:id+ parameter add a constraint which
+ # overrides this restriction, e.g:
+ #
+ # resources :articles, id: /[^\/]+/
+ #
+ # This allows any character other than a slash as part of your +:id+.
+ #
+ module Resources
+ # CANONICAL_ACTIONS holds all actions that does not need a prefix or
+ # a path appended since they fit properly in their scope level.
+ VALID_ON_OPTIONS = [:new, :collection, :member]
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
+ CANONICAL_ACTIONS = %w(index create new show update destroy)
+
+ class Resource #:nodoc:
+ attr_reader :controller, :path, :param
+
+ def initialize(entities, api_only, shallow, options = {})
+ @name = entities.to_s
+ @path = (options[:path] || @name).to_s
+ @controller = (options[:controller] || @name).to_s
+ @as = options[:as]
+ @param = (options[:param] || :id).to_sym
+ @options = options
+ @shallow = shallow
+ @api_only = api_only
+ @only = options.delete :only
+ @except = options.delete :except
+ end
+
+ def default_actions
+ if @api_only
+ [:index, :create, :show, :update, :destroy]
+ else
+ [:index, :create, :new, :show, :update, :destroy, :edit]
+ end
+ end
+
+ def actions
+ if @only
+ Array(@only).map(&:to_sym)
+ elsif @except
+ default_actions - Array(@except).map(&:to_sym)
+ else
+ default_actions
+ end
+ end
+
+ def name
+ @as || @name
+ end
+
+ def plural
+ @plural ||= name.to_s
+ end
+
+ def singular
+ @singular ||= name.to_s.singularize
+ end
+
+ alias :member_name :singular
+
+ # Checks for uncountable plurals, and appends "_index" if the plural
+ # and singular form are the same.
+ def collection_name
+ singular == plural ? "#{plural}_index" : plural
+ end
+
+ def resource_scope
+ controller
+ end
+
+ alias :collection_scope :path
+
+ def member_scope
+ "#{path}/:#{param}"
+ end
+
+ alias :shallow_scope :member_scope
+
+ def new_scope(new_path)
+ "#{path}/#{new_path}"
+ end
+
+ def nested_param
+ :"#{singular}_#{param}"
+ end
+
+ def nested_scope
+ "#{path}/:#{nested_param}"
+ end
+
+ def shallow?
+ @shallow
+ end
+
+ def singleton?; false; end
+ end
+
+ class SingletonResource < Resource #:nodoc:
+ def initialize(entities, api_only, shallow, options)
+ super
+ @as = nil
+ @controller = (options[:controller] || plural).to_s
+ @as = options[:as]
+ end
+
+ def default_actions
+ if @api_only
+ [:show, :create, :update, :destroy]
+ else
+ [:show, :create, :update, :destroy, :new, :edit]
+ end
+ end
+
+ def plural
+ @plural ||= name.to_s.pluralize
+ end
+
+ def singular
+ @singular ||= name.to_s
+ end
+
+ alias :member_name :singular
+ alias :collection_name :singular
+
+ alias :member_scope :path
+ alias :nested_scope :path
+
+ def singleton?; true; end
+ end
+
+ def resources_path_names(options)
+ @scope[:path_names].merge!(options)
+ end
+
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the
+ # profile of the currently logged in user. In this case, you can use
+ # a singular resource to map /profile (rather than /profile/:id) to
+ # the show action:
+ #
+ # resource :profile
+ #
+ # This creates six different routes in your application, all mapping to
+ # the +Profiles+ controller (note that the controller is named after
+ # the plural):
+ #
+ # GET /profile/new
+ # GET /profile
+ # GET /profile/edit
+ # PATCH/PUT /profile
+ # DELETE /profile
+ # POST /profile
+ #
+ # === Options
+ # Takes same options as resources[rdoc-ref:#resources]
+ def resource(*resources, &block)
+ options = resources.extract_options!.dup
+
+ if apply_common_behavior_for(:resource, resources, options, &block)
+ return self
+ end
+
+ with_scope_level(:resource) do
+ options = apply_action_options options
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
+
+ concerns(options[:concerns]) if options[:concerns]
+
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
+
+ set_member_mappings_for_resource
+
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
+ end
+ end
+
+ self
+ end
+
+ # In Rails, a resourceful route provides a mapping between HTTP verbs
+ # and URLs and controller actions. By convention, each action also maps
+ # to particular CRUD operations in a database. A single entry in the
+ # routing file, such as
+ #
+ # resources :photos
+ #
+ # creates seven different routes in your application, all mapping to
+ # the +Photos+ controller:
+ #
+ # GET /photos
+ # GET /photos/new
+ # POST /photos
+ # GET /photos/:id
+ # GET /photos/:id/edit
+ # PATCH/PUT /photos/:id
+ # DELETE /photos/:id
+ #
+ # Resources can also be nested infinitely by using this block syntax:
+ #
+ # resources :photos do
+ # resources :comments
+ # end
+ #
+ # This generates the following comments routes:
+ #
+ # GET /photos/:photo_id/comments
+ # GET /photos/:photo_id/comments/new
+ # POST /photos/:photo_id/comments
+ # GET /photos/:photo_id/comments/:id
+ # GET /photos/:photo_id/comments/:id/edit
+ # PATCH/PUT /photos/:photo_id/comments/:id
+ # DELETE /photos/:photo_id/comments/:id
+ #
+ # === Options
+ # Takes same options as match[rdoc-ref:Base#match] as well as:
+ #
+ # [:path_names]
+ # Allows you to change the segment component of the +edit+ and +new+ actions.
+ # Actions not specified are not changed.
+ #
+ # resources :posts, path_names: { new: "brand_new" }
+ #
+ # The above example will now change /posts/new to /posts/brand_new.
+ #
+ # [:path]
+ # Allows you to change the path prefix for the resource.
+ #
+ # resources :posts, path: 'postings'
+ #
+ # The resource and all segments will now route to /postings instead of /posts.
+ #
+ # [:only]
+ # Only generate routes for the given actions.
+ #
+ # resources :cows, only: :show
+ # resources :cows, only: [:show, :index]
+ #
+ # [:except]
+ # Generate all routes except for the given actions.
+ #
+ # resources :cows, except: :show
+ # resources :cows, except: [:show, :index]
+ #
+ # [:shallow]
+ # Generates shallow routes for nested resource(s). When placed on a parent resource,
+ # generates shallow routes for all nested resources.
+ #
+ # resources :posts, shallow: true do
+ # resources :comments
+ # end
+ #
+ # Is the same as:
+ #
+ # resources :posts do
+ # resources :comments, except: [:show, :edit, :update, :destroy]
+ # end
+ # resources :comments, only: [:show, :edit, :update, :destroy]
+ #
+ # This allows URLs for resources that otherwise would be deeply nested such
+ # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
+ # to be shortened to just <tt>/comments/1234</tt>.
+ #
+ # [:shallow_path]
+ # Prefixes nested shallow routes with the specified path.
+ #
+ # scope shallow_path: "sekret" do
+ # resources :posts do
+ # resources :comments, shallow: true
+ # end
+ # end
+ #
+ # The +comments+ resource here will have the following routes generated for it:
+ #
+ # post_comments GET /posts/:post_id/comments(.:format)
+ # post_comments POST /posts/:post_id/comments(.:format)
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
+ # edit_comment GET /sekret/comments/:id/edit(.:format)
+ # comment GET /sekret/comments/:id(.:format)
+ # comment PATCH/PUT /sekret/comments/:id(.:format)
+ # comment DELETE /sekret/comments/:id(.:format)
+ #
+ # [:shallow_prefix]
+ # Prefixes nested shallow route names with specified prefix.
+ #
+ # scope shallow_prefix: "sekret" do
+ # resources :posts do
+ # resources :comments, shallow: true
+ # end
+ # end
+ #
+ # The +comments+ resource here will have the following routes generated for it:
+ #
+ # post_comments GET /posts/:post_id/comments(.:format)
+ # post_comments POST /posts/:post_id/comments(.:format)
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
+ # sekret_comment GET /comments/:id(.:format)
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
+ # sekret_comment DELETE /comments/:id(.:format)
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
+ #
+ # === Examples
+ #
+ # # routes call <tt>Admin::PostsController</tt>
+ # resources :posts, module: "admin"
+ #
+ # # resource actions are at /admin/posts.
+ # resources :posts, path: "admin/posts"
+ def resources(*resources, &block)
+ options = resources.extract_options!.dup
+
+ if apply_common_behavior_for(:resources, resources, options, &block)
+ return self
+ end
+
+ with_scope_level(:resources) do
+ options = apply_action_options options
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
+
+ concerns(options[:concerns]) if options[:concerns]
+
+ collection do
+ get :index if parent_resource.actions.include?(:index)
+ post :create if parent_resource.actions.include?(:create)
+ end
+
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
+
+ set_member_mappings_for_resource
+ end
+ end
+
+ self
+ end
+
+ # To add a route to the collection:
+ #
+ # resources :photos do
+ # collection do
+ # get 'search'
+ # end
+ # end
+ #
+ # This will enable Rails to recognize paths such as <tt>/photos/search</tt>
+ # with GET, and route to the search action of +PhotosController+. It will also
+ # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
+ # route helpers.
+ def collection
+ unless resource_scope?
+ raise ArgumentError, "can't use collection outside resource(s) scope"
+ end
+
+ with_scope_level(:collection) do
+ path_scope(parent_resource.collection_scope) do
+ yield
+ end
+ end
+ end
+
+ # To add a member route, add a member block into the resource block:
+ #
+ # resources :photos do
+ # member do
+ # get 'preview'
+ # end
+ # end
+ #
+ # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
+ # preview action of +PhotosController+. It will also create the
+ # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
+ def member
+ unless resource_scope?
+ raise ArgumentError, "can't use member outside resource(s) scope"
+ end
+
+ with_scope_level(:member) do
+ if shallow?
+ shallow_scope {
+ path_scope(parent_resource.member_scope) { yield }
+ }
+ else
+ path_scope(parent_resource.member_scope) { yield }
+ end
+ end
+ end
+
+ def new
+ unless resource_scope?
+ raise ArgumentError, "can't use new outside resource(s) scope"
+ end
+
+ with_scope_level(:new) do
+ path_scope(parent_resource.new_scope(action_path(:new))) do
+ yield
+ end
+ end
+ end
+
+ def nested
+ unless resource_scope?
+ raise ArgumentError, "can't use nested outside resource(s) scope"
+ end
+
+ with_scope_level(:nested) do
+ if shallow? && shallow_nesting_depth >= 1
+ shallow_scope do
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
+ else
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
+ end
+ end
+
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
+ def namespace(path, options = {})
+ if resource_scope?
+ nested { super }
+ else
+ super
+ end
+ end
+
+ def shallow
+ @scope = @scope.new(shallow: true)
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def shallow?
+ !parent_resource.singleton? && @scope[:shallow]
+ end
+
+ # Matches a URL pattern to one or more routes.
+ # For more information, see match[rdoc-ref:Base#match].
+ #
+ # match 'path' => 'controller#action', via: :patch
+ # match 'path', to: 'controller#action', via: :post
+ # match 'path', 'otherpath', on: :member, via: :get
+ def match(path, *rest, &block)
+ if rest.empty? && Hash === path
+ options = path
+ path, to = options.find { |name, _value| name.is_a?(String) }
+
+ raise ArgumentError, "Route path not specified" if path.nil?
+
+ case to
+ when Symbol
+ options[:action] = to
+ when String
+ if to =~ /#/
+ options[:to] = to
+ else
+ options[:controller] = to
+ end
+ else
+ options[:to] = to
+ end
+
+ options.delete(path)
+ paths = [path]
+ else
+ options = rest.pop || {}
+ paths = [path] + rest
+ end
+
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
+ else
+ map_match(paths, options, &block)
+ end
+ end
+
+ # You can specify what Rails should route "/" to with the root method:
+ #
+ # root to: 'pages#main'
+ #
+ # For options, see +match+, as +root+ uses it internally.
+ #
+ # You can also pass a string which will expand
+ #
+ # root 'pages#main'
+ #
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
+ # because this means it will be matched first. As this is the most popular route
+ # of most Rails applications, this is beneficial.
+ def root(path, options = {})
+ if path.is_a?(String)
+ options[:to] = path
+ elsif path.is_a?(Hash) && options.empty?
+ options = path
+ else
+ raise ArgumentError, "must be called with a path and/or options"
+ end
+
+ if @scope.resources?
+ with_scope_level(:root) do
+ path_scope(parent_resource.path) do
+ match_root_route(options)
+ end
+ end
+ else
+ match_root_route(options)
+ end
+ end
+
+ private
+
+ def parent_resource
+ @scope[:scope_level_resource]
+ end
+
+ def apply_common_behavior_for(method, resources, options, &block)
+ if resources.length > 1
+ resources.each { |r| send(method, r, options, &block) }
+ return true
+ end
+
+ if options.delete(:shallow)
+ shallow do
+ send(method, resources.pop, options, &block)
+ end
+ return true
+ end
+
+ if resource_scope?
+ nested { send(method, resources.pop, options, &block) }
+ return true
+ end
+
+ options.keys.each do |k|
+ (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
+ end
+
+ scope_options = options.slice!(*RESOURCE_OPTIONS)
+ unless scope_options.empty?
+ scope(scope_options) do
+ send(method, resources.pop, options, &block)
+ end
+ return true
+ end
+
+ false
+ end
+
+ def apply_action_options(options)
+ return options if action_options? options
+ options.merge scope_action_options
+ end
+
+ def action_options?(options)
+ options[:only] || options[:except]
+ end
+
+ def scope_action_options
+ @scope[:action_options] || {}
+ end
+
+ def resource_scope?
+ @scope.resource_scope?
+ end
+
+ def resource_method_scope?
+ @scope.resource_method_scope?
+ end
+
+ def nested_scope?
+ @scope.nested?
+ end
+
+ def with_scope_level(kind) # :doc:
+ @scope = @scope.new_level(kind)
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def resource_scope(resource)
+ @scope = @scope.new(scope_level_resource: resource)
+
+ controller(resource.resource_scope) { yield }
+ ensure
+ @scope = @scope.parent
+ end
+
+ def nested_options
+ options = { as: parent_resource.member_name }
+ options[:constraints] = {
+ parent_resource.nested_param => param_constraint
+ } if param_constraint?
+
+ options
+ end
+
+ def shallow_nesting_depth
+ @scope.find_all { |node|
+ node.frame[:scope_level_resource]
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
+ end
+
+ def param_constraint?
+ @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
+ end
+
+ def param_constraint
+ @scope[:constraints][parent_resource.param]
+ end
+
+ def canonical_action?(action)
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ end
+
+ def shallow_scope
+ scope = { as: @scope[:shallow_prefix],
+ path: @scope[:shallow_path] }
+ @scope = @scope.new scope
+
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def path_for_action(action, path)
+ return "#{@scope[:path]}/#{path}" if path
+
+ if canonical_action?(action)
+ @scope[:path].to_s
+ else
+ "#{@scope[:path]}/#{action_path(action)}"
+ end
+ end
+
+ def action_path(name)
+ @scope[:path_names][name.to_sym] || name
+ end
+
+ def prefix_name_for_action(as, action)
+ if as
+ prefix = as
+ elsif !canonical_action?(action)
+ prefix = action
+ end
+
+ if prefix && prefix != "/" && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
+ end
+ end
+
+ def name_for_action(as, action)
+ prefix = prefix_name_for_action(as, action)
+ name_prefix = @scope[:as]
+
+ if parent_resource
+ return nil unless as || action
+
+ collection_name = parent_resource.collection_name
+ member_name = parent_resource.member_name
+ end
+
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
+ candidate = action_name.select(&:present?).join("_")
+
+ unless candidate.empty?
+ # If a name was not explicitly given, we check if it is valid
+ # and return nil in case it isn't. Otherwise, we pass the invalid name
+ # forward so the underlying router engine treats it and raises an exception.
+ if as.nil?
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
+ else
+ candidate
+ end
+ end
+ end
+
+ def set_member_mappings_for_resource # :doc:
+ member do
+ get :edit if parent_resource.actions.include?(:edit)
+ get :show if parent_resource.actions.include?(:show)
+ if parent_resource.actions.include?(:update)
+ patch :update
+ put :update
+ end
+ delete :destroy if parent_resource.actions.include?(:destroy)
+ end
+ end
+
+ def api_only? # :doc:
+ @set.api_only?
+ end
+
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
+
+ def map_match(paths, options)
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
+ end
+
+ if @scope[:to]
+ options[:to] ||= @scope[:to]
+ end
+
+ if @scope[:controller] && @scope[:action]
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
+ end
+
+ controller = options.delete(:controller) || @scope[:controller]
+ option_path = options.delete :path
+ to = options.delete :to
+ via = Mapping.check_via Array(options.delete(:via) {
+ @scope[:via]
+ })
+ formatted = options.delete(:format) { @scope[:format] }
+ anchor = options.delete(:anchor) { true }
+ options_constraints = options.delete(:constraints) || {}
+
+ path_types = paths.group_by(&:class)
+ path_types.fetch(String, []).each do |_path|
+ route_options = options.dup
+ if _path && option_path
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
+ end
+ to = get_to_from_path(_path, to, route_options[:action])
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
+ end
+
+ path_types.fetch(Symbol, []).each do |action|
+ route_options = options.dup
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
+ end
+
+ self
+ end
+
+ def get_to_from_path(path, to, action)
+ return to if to || action
+
+ path_without_format = path.sub(/\(\.:format\)$/, "")
+ if using_match_shorthand?(path_without_format)
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ else
+ nil
+ end
+ end
+
+ def using_match_shorthand?(path)
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
+ end
+
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ if on = options.delete(:on)
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ case @scope.scope_level
+ when :resources
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ when :resource
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ end
+ end
+ end
+
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ path = path_for_action(action, _path)
+ raise ArgumentError, "path is required" if path.blank?
+
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
+
+ if action =~ /^[\w\-\/]+$/
+ default_action ||= action.tr("-", "_") unless action.include?("/")
+ else
+ action = nil
+ end
+
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
+
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
+ ast = Journey::Parser.parse path
+
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ @set.add_route(mapping, as)
+ end
+
+ def match_root_route(options)
+ name = has_named_route?(name_for_action(:root, nil)) ? nil : :root
+ args = ["/", { as: name, via: :get }.merge!(options)]
+
+ match(*args)
+ end
+ end
+
+ # Routing Concerns allow you to declare common routes that can be reused
+ # inside others resources and routes.
+ #
+ # concern :commentable do
+ # resources :comments
+ # end
+ #
+ # concern :image_attachable do
+ # resources :images, only: :index
+ # end
+ #
+ # These concerns are used in Resources routing:
+ #
+ # resources :messages, concerns: [:commentable, :image_attachable]
+ #
+ # or in a scope or namespace:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ module Concerns
+ # Define a routing concern using a name.
+ #
+ # Concerns may be defined inline, using a block, or handled by
+ # another object, by passing that object as the second parameter.
+ #
+ # The concern object, if supplied, should respond to <tt>call</tt>,
+ # which will receive two parameters:
+ #
+ # * The current mapper
+ # * A hash of options which the concern object may use
+ #
+ # Options may also be used by concerns defined in a block by accepting
+ # a block parameter. So, using a block, you might do something as
+ # simple as limit the actions available on certain resources, passing
+ # standard resource options through the concern:
+ #
+ # concern :commentable do |options|
+ # resources :comments, options
+ # end
+ #
+ # resources :posts, concerns: :commentable
+ # resources :archived_posts do
+ # # Don't allow comments on archived posts
+ # concerns :commentable, only: [:index, :show]
+ # end
+ #
+ # Or, using a callable object, you might implement something more
+ # specific to your application, which would be out of place in your
+ # routes file.
+ #
+ # # purchasable.rb
+ # class Purchasable
+ # def initialize(defaults = {})
+ # @defaults = defaults
+ # end
+ #
+ # def call(mapper, options = {})
+ # options = @defaults.merge(options)
+ # mapper.resources :purchases
+ # mapper.resources :receipts
+ # mapper.resources :returns if options[:returnable]
+ # end
+ # end
+ #
+ # # routes.rb
+ # concern :purchasable, Purchasable.new(returnable: true)
+ #
+ # resources :toys, concerns: :purchasable
+ # resources :electronics, concerns: :purchasable
+ # resources :pets do
+ # concerns :purchasable, returnable: false
+ # end
+ #
+ # Any routing helpers can be used inside a concern. If using a
+ # callable, they're accessible from the Mapper that's passed to
+ # <tt>call</tt>.
+ def concern(name, callable = nil, &block)
+ callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
+ @concerns[name] = callable
+ end
+
+ # Use the named concerns
+ #
+ # resources :posts do
+ # concerns :commentable
+ # end
+ #
+ # Concerns also work in any routes helper that you want to use:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ def concerns(*args)
+ options = args.extract_options!
+ args.flatten.each do |name|
+ if concern = @concerns[name]
+ concern.call(self, options)
+ else
+ raise ArgumentError, "No concern named #{name} was found!"
+ end
+ end
+ end
+ end
+
+ module CustomUrls
+ # Define custom URL helpers that will be added to the application's
+ # routes. This allows you to override and/or replace the default behavior
+ # of routing helpers, e.g:
+ #
+ # direct :homepage do
+ # "http://www.rubyonrails.org"
+ # end
+ #
+ # direct :commentable do |model|
+ # [ model, anchor: model.dom_id ]
+ # end
+ #
+ # direct :main do
+ # { controller: "pages", action: "index", subdomain: "www" }
+ # end
+ #
+ # The return value from the block passed to +direct+ must be a valid set of
+ # arguments for +url_for+ which will actually build the URL string. This can
+ # be one of the following:
+ #
+ # * A string, which is treated as a generated URL
+ # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
+ # * An array, which is passed to +polymorphic_url+
+ # * An Active Model instance
+ # * An Active Model class
+ #
+ # NOTE: Other URL helpers can be called in the block but be careful not to invoke
+ # your custom URL helper again otherwise it will result in a stack overflow error.
+ #
+ # You can also specify default options that will be passed through to
+ # your URL helper definition, e.g:
+ #
+ # direct :browse, page: 1, size: 10 do |options|
+ # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
+ # end
+ #
+ # In this instance the +params+ object comes from the context in which the
+ # block is executed, e.g. generating a URL inside a controller action or a view.
+ # If the block is executed where there isn't a +params+ object such as this:
+ #
+ # Rails.application.routes.url_helpers.browse_path
+ #
+ # then it will raise a +NameError+. Because of this you need to be aware of the
+ # context in which you will use your custom URL helper when defining it.
+ #
+ # NOTE: The +direct+ method can't be used inside of a scope block such as
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
+ def direct(name, options = {}, &block)
+ unless @scope.root?
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
+ end
+
+ @set.add_url_helper(name, options, &block)
+ end
+
+ # Define custom polymorphic mappings of models to URLs. This alters the
+ # behavior of +polymorphic_url+ and consequently the behavior of
+ # +link_to+ and +form_for+ when passed a model instance, e.g:
+ #
+ # resource :basket
+ #
+ # resolve "Basket" do
+ # [:basket]
+ # end
+ #
+ # This will now generate "/basket" when a +Basket+ instance is passed to
+ # +link_to+ or +form_for+ instead of the standard "/baskets/:id".
+ #
+ # NOTE: This custom behavior only applies to simple polymorphic URLs where
+ # a single model instance is passed and not more complicated forms, e.g:
+ #
+ # # config/routes.rb
+ # resource :profile
+ # namespace :admin do
+ # resources :users
+ # end
+ #
+ # resolve("User") { [:profile] }
+ #
+ # # app/views/application/_menu.html.erb
+ # link_to "Profile", @current_user
+ # link_to "Profile", [:admin, @current_user]
+ #
+ # The first +link_to+ will generate "/profile" but the second will generate
+ # the standard polymorphic URL of "/admin/users/1".
+ #
+ # You can pass options to a polymorphic mapping - the arity for the block
+ # needs to be two as the instance is passed as the first argument, e.g:
+ #
+ # resolve "Basket", anchor: "items" do |basket, options|
+ # [:basket, options]
+ # end
+ #
+ # This generates the URL "/basket#items" because when the last item in an
+ # array passed to +polymorphic_url+ is a hash then it's treated as options
+ # to the URL helper that gets called.
+ #
+ # NOTE: The +resolve+ method can't be used inside of a scope block such as
+ # +namespace+ or +scope+ and will raise an error if it detects that it is.
+ def resolve(*args, &block)
+ unless @scope.root?
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
+ end
+
+ options = args.extract_options!
+ args = args.flatten(1)
+
+ args.each do |klass|
+ @set.add_polymorphic_mapping(klass, options, &block)
+ end
+ end
+ end
+
+ class Scope # :nodoc:
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
+ :controller, :action, :path_names, :constraints,
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
+
+ def initialize(hash, parent = NULL, scope_level = nil)
+ @hash = hash
+ @parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def null?
+ @hash.nil? && @parent.nil?
+ end
+
+ def root?
+ @parent.null?
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
+ end
+
+ def options
+ OPTIONS
+ end
+
+ def new(hash)
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(frame, self, level)
+ end
+
+ def [](key)
+ scope = find { |node| node.frame.key? key }
+ scope && scope.frame[key]
+ end
+
+ include Enumerable
+
+ def each
+ node = self
+ until node.equal? NULL
+ yield node
+ node = node.parent
+ end
+ end
+
+ def frame; @hash; end
+
+ NULL = Scope.new(nil, nil)
+ end
+
+ def initialize(set) #:nodoc:
+ @set = set
+ @scope = Scope.new(path_names: @set.resources_path_names)
+ @concerns = {}
+ end
+
+ include Base
+ include HttpHelpers
+ include Redirection
+ include Scoping
+ include Concerns
+ include Resources
+ include CustomUrls
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
new file mode 100644
index 0000000000..6da869c0c2
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -0,0 +1,352 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Routing
+ # Polymorphic URL helpers are methods for smart resolution to a named route call when
+ # given an Active Record model instance. They are to be used in combination with
+ # ActionController::Resources.
+ #
+ # These methods are useful when you want to generate the correct URL or path to a RESTful
+ # resource without having to know the exact type of the record in question.
+ #
+ # Nested resources and/or namespaces are also supported, as illustrated in the example:
+ #
+ # polymorphic_url([:admin, @article, @comment])
+ #
+ # results in:
+ #
+ # admin_article_comment_url(@article, @comment)
+ #
+ # == Usage within the framework
+ #
+ # Polymorphic URL helpers are used in a number of places throughout the \Rails framework:
+ #
+ # * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
+ # <tt>url_for(@article)</tt>;
+ # * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
+ # <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
+ # action;
+ # * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
+ # <tt>redirect_to(post)</tt> in your controllers;
+ # * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
+ # for feed entries.
+ #
+ # == Prefixed polymorphic helpers
+ #
+ # In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
+ # number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt>
+ # in options. Those are:
+ #
+ # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
+ # * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
+ #
+ # Example usage:
+ #
+ # edit_polymorphic_path(@post) # => "/posts/1/edit"
+ # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
+ #
+ # == Usage with mounted engines
+ #
+ # If you are using a mounted engine and you need to use a polymorphic_url
+ # pointing at the engine's routes, pass in the engine's route proxy as the first
+ # argument to the method. For example:
+ #
+ # polymorphic_url([blog, @post]) # calls blog.post_path(@post)
+ # form_for([blog, @post]) # => "/blog/posts/1"
+ #
+ module PolymorphicRoutes
+ # Constructs a call to a named RESTful route for the given record and returns the
+ # resulting URL string. For example:
+ #
+ # # calls post_url(post)
+ # polymorphic_url(post) # => "http://example.com/posts/1"
+ # polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
+ # polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
+ # polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
+ # polymorphic_url(Comment) # => "http://example.com/comments"
+ #
+ # ==== Options
+ #
+ # * <tt>:action</tt> - Specifies the action prefix for the named route:
+ # <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
+ # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
+ # Default is <tt>:url</tt>.
+ #
+ # Also includes all the options from <tt>url_for</tt>. These include such
+ # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
+ # is given below:
+ #
+ # polymorphic_url([blog, post], anchor: 'my_anchor')
+ # # => "http://example.com/blogs/1/posts/1#my_anchor"
+ # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
+ # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
+ #
+ # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
+ #
+ # ==== Functionality
+ #
+ # # an Article record
+ # polymorphic_url(record) # same as article_url(record)
+ #
+ # # a Comment record
+ # polymorphic_url(record) # same as comment_url(record)
+ #
+ # # it recognizes new records and maps to the collection
+ # record = Comment.new
+ # polymorphic_url(record) # same as comments_url()
+ #
+ # # the class of a record will also map to the collection
+ # polymorphic_url(Comment) # same as comments_url()
+ #
+ def polymorphic_url(record_or_hash_or_array, options = {})
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_url record, options
+ end
+
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array, options], false)
+ end
+
+ opts = options.dup
+ action = opts.delete :action
+ type = opts.delete(:routing_type) || :url
+
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
+ end
+
+ # Returns the path component of a URL for the given record. It uses
+ # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
+ def polymorphic_path(record_or_hash_or_array, options = {})
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_path record, options
+ end
+
+ if mapping = polymorphic_mapping(record_or_hash_or_array)
+ return mapping.call(self, [record_or_hash_or_array, options], true)
+ end
+
+ opts = options.dup
+ action = opts.delete :action
+ type = :path
+
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
+ end
+
+ %w(edit new).each do |action|
+ module_eval <<-EOT, __FILE__, __LINE__ + 1
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
+ EOT
+ end
+
+ private
+
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(action: action))
+ end
+
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(action: action))
+ end
+
+ def polymorphic_mapping(record)
+ if record.respond_to?(:to_model)
+ _routes.polymorphic_mappings[record.to_model.model_name.name]
+ else
+ _routes.polymorphic_mappings[record.class.name]
+ end
+ end
+
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { "path" => {}, "url" => {} }
+
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
+ end
+
+ def self.url; CACHE["url".freeze][nil]; end
+ def self.path; CACHE["path".freeze][nil]; end
+
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == "new"
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
+ end
+ end
+
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
+ end
+
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
+ end
+
+ def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
+ builder = get action, type
+
+ case record_or_hash_or_array
+ when Array
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ end
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ recipient = record_or_hash_or_array.shift
+ end
+
+ method, args = builder.handle_list record_or_hash_or_array
+ when String, Symbol
+ method, args = builder.handle_string record_or_hash_or_array
+ when Class
+ method, args = builder.handle_class record_or_hash_or_array
+
+ when nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ else
+ method, args = builder.handle_model record_or_hash_or_array
+ end
+
+ if options.empty?
+ recipient.send(method, *args)
+ else
+ recipient.send(method, *args, options)
+ end
+ end
+
+ attr_reader :suffix, :prefix
+
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
+ end
+
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
+
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
+
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
+
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
+
+ def handle_model(record)
+ args = []
+
+ model = record.to_model
+ named_route = if model.persisted?
+ args << model
+ get_method_for_string model.model_name.singular_route_key
+ else
+ get_method_for_class model
+ end
+
+ [named_route, args]
+ end
+
+ def handle_model_call(target, record)
+ if mapping = polymorphic_mapping(target, record)
+ mapping.call(target, [record], suffix == "path")
+ else
+ method, args = handle_model(record)
+ target.send(method, *args)
+ end
+ end
+
+ def handle_list(list)
+ record_list = list.dup
+ record = record_list.pop
+
+ args = []
+
+ route = record_list.map { |parent|
+ case parent
+ when Symbol, String
+ parent.to_s
+ when Class
+ args << parent
+ parent.model_name.singular_route_key
+ else
+ args << parent.to_model
+ parent.to_model.model_name.singular_route_key
+ end
+ }
+
+ route <<
+ case record
+ when Symbol, String
+ record.to_s
+ when Class
+ @key_strategy.call record.model_name
+ else
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.model_name
+ end
+ end
+
+ route << suffix
+
+ named_route = prefix + route.join("_")
+ [named_route, args]
+ end
+
+ private
+
+ def polymorphic_mapping(target, record)
+ if record.respond_to?(:to_model)
+ target._routes.polymorphic_mappings[record.to_model.model_name.name]
+ else
+ target._routes.polymorphic_mappings[record.class.name]
+ end
+ end
+
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ get_method_for_string name
+ end
+
+ def get_method_for_string(str)
+ "#{prefix}#{str}_#{suffix}"
+ end
+
+ [nil, "new", "edit"].each do |action|
+ CACHE["url"][action] = build action, "url"
+ CACHE["path"][action] = build action, "path"
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
new file mode 100644
index 0000000000..143a4b3d62
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "active_support/core_ext/uri"
+require "active_support/core_ext/array/extract_options"
+require "rack/utils"
+require "action_controller/metal/exceptions"
+require "action_dispatch/routing/endpoint"
+
+module ActionDispatch
+ module Routing
+ class Redirect < Endpoint # :nodoc:
+ attr_reader :status, :block
+
+ def initialize(status, block)
+ @status = status
+ @block = block
+ end
+
+ def redirect?; true; end
+
+ def call(env)
+ serve Request.new env
+ end
+
+ def serve(req)
+ uri = URI.parse(path(req.path_parameters, req))
+
+ unless uri.host
+ if relative_path?(uri.path)
+ uri.path = "#{req.script_name}/#{uri.path}"
+ elsif uri.path.empty?
+ uri.path = req.script_name.empty? ? "/" : req.script_name
+ end
+ end
+
+ uri.scheme ||= req.scheme
+ uri.host ||= req.host
+ uri.port ||= req.port unless req.standard_port?
+
+ req.commit_flash
+
+ body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
+
+ headers = {
+ "Location" => uri.to_s,
+ "Content-Type" => "text/html",
+ "Content-Length" => body.length.to_s
+ }
+
+ [ status, headers, [body] ]
+ end
+
+ def path(params, request)
+ block.call params, request
+ end
+
+ def inspect
+ "redirect(#{status})"
+ end
+
+ private
+ def relative_path?(path)
+ path && !path.empty? && path[0] != "/"
+ end
+
+ def escape(params)
+ Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }]
+ end
+
+ def escape_fragment(params)
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ end
+
+ def escape_path(params)
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }]
+ end
+ end
+
+ class PathRedirect < Redirect
+ URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
+
+ def path(params, request)
+ if block.match(URL_PARTS)
+ path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
+ query = interpolation_required?($2, params) ? $2 % escape(params) : $2
+ fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
+
+ "#{path}#{query}#{fragment}"
+ else
+ interpolation_required?(block, params) ? block % escape(params) : block
+ end
+ end
+
+ def inspect
+ "redirect(#{status}, #{block})"
+ end
+
+ private
+ def interpolation_required?(string, params)
+ !params.empty? && string && string.match(/%\{\w*\}/)
+ end
+ end
+
+ class OptionRedirect < Redirect # :nodoc:
+ alias :options :block
+
+ def path(params, request)
+ url_options = {
+ protocol: request.protocol,
+ host: request.host,
+ port: request.optional_port,
+ path: request.path,
+ params: request.query_parameters
+ }.merge! options
+
+ if !params.empty? && url_options[:path].match(/%\{\w*\}/)
+ url_options[:path] = (url_options[:path] % escape_path(params))
+ end
+
+ unless options[:host] || options[:domain]
+ if relative_path?(url_options[:path])
+ url_options[:path] = "/#{url_options[:path]}"
+ url_options[:script_name] = request.script_name
+ elsif url_options[:path].empty?
+ url_options[:path] = request.script_name.empty? ? "/" : ""
+ url_options[:script_name] = request.script_name
+ end
+ end
+
+ ActionDispatch::Http::URL.url_for url_options
+ end
+
+ def inspect
+ "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
+ end
+ end
+
+ module Redirection
+ # Redirect any path to another path:
+ #
+ # get "/stories" => redirect("/posts")
+ #
+ # This will redirect the user, while ignoring certain parts of the request, including query string, etc.
+ # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>.
+ #
+ # You can also use interpolation in the supplied redirect argument:
+ #
+ # get 'docs/:article', to: redirect('/wiki/%{article}')
+ #
+ # Note that if you return a path without a leading slash then the URL is prefixed with the
+ # current SCRIPT_NAME environment variable. This is typically '/' but may be different in
+ # a mounted engine or where the application is deployed to a subdirectory of a website.
+ #
+ # Alternatively you can use one of the other syntaxes:
+ #
+ # The block version of redirect allows for the easy encapsulation of any logic associated with
+ # the redirect in question. Either the params and request are supplied as arguments, or just
+ # params, depending of how many arguments your block accepts. A string is required as a
+ # return value.
+ #
+ # get 'jokes/:number', to: redirect { |params, request|
+ # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
+ # "http://#{request.host_with_port}/#{path}"
+ # }
+ #
+ # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
+ # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
+ #
+ # The options version of redirect allows you to supply only the parts of the URL which need
+ # to change, it also supports interpolation of the path similar to the first example.
+ #
+ # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
+ # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
+ # get '/stories', to: redirect(path: '/posts')
+ #
+ # This will redirect the user, while changing only the specified parts of the request,
+ # for example the +path+ option in the last example.
+ # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> respectively.
+ #
+ # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
+ # common redirect routes. The call method must accept two arguments, params and request, and return
+ # a string.
+ #
+ # get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
+ #
+ def redirect(*args, &block)
+ options = args.extract_options!
+ status = options.delete(:status) || 301
+ path = args.shift
+
+ return OptionRedirect.new(status, options) if options.any?
+ return PathRedirect.new(status, path) if String === path
+
+ block = path if path.respond_to? :call
+ raise ArgumentError, "redirection argument not supported" unless block
+ Redirect.new status, block
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
new file mode 100644
index 0000000000..a29a5a04ef
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -0,0 +1,889 @@
+# frozen_string_literal: true
+
+require "action_dispatch/journey"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/array/extract_options"
+require "action_controller/metal/exceptions"
+require "action_dispatch/http/request"
+require "action_dispatch/routing/endpoint"
+
+module ActionDispatch
+ module Routing
+ # :stopdoc:
+ class RouteSet
+ # Since the router holds references to many parts of the system
+ # like engines, controllers and the application itself, inspecting
+ # the route set can actually be really slow, therefore we default
+ # alias inspect to to_s.
+ alias inspect to_s
+
+ class Dispatcher < Routing::Endpoint
+ def initialize(raise_on_name_error)
+ @raise_on_name_error = raise_on_name_error
+ end
+
+ def dispatcher?; true; end
+
+ def serve(req)
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue ActionController::RoutingError
+ if @raise_on_name_error
+ raise
+ else
+ return [404, { "X-Cascade" => "pass" }, []]
+ end
+ end
+
+ private
+
+ def controller(req)
+ req.controller_class
+ rescue NameError => e
+ raise ActionController::RoutingError, e.message, e.backtrace
+ end
+
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
+ end
+ end
+
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
+ end
+
+ private
+
+ def controller(_); @controller_class; end
+ end
+
+ # A NamedRouteCollection instance is a collection of named routes, and also
+ # maintains an anonymous module that can be used to install helpers for the
+ # named routes.
+ class NamedRouteCollection
+ include Enumerable
+ attr_reader :routes, :url_helpers_module, :path_helpers_module
+ private :routes
+
+ def initialize
+ @routes = {}
+ @path_helpers = Set.new
+ @url_helpers = Set.new
+ @url_helpers_module = Module.new
+ @path_helpers_module = Module.new
+ end
+
+ def route_defined?(name)
+ key = name.to_sym
+ @path_helpers.include?(key) || @url_helpers.include?(key)
+ end
+
+ def helper_names
+ @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
+ end
+
+ def clear!
+ @path_helpers.each do |helper|
+ @path_helpers_module.send :remove_method, helper
+ end
+
+ @url_helpers.each do |helper|
+ @url_helpers_module.send :remove_method, helper
+ end
+
+ @routes.clear
+ @path_helpers.clear
+ @url_helpers.clear
+ end
+
+ def add(name, route)
+ key = name.to_sym
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ if routes.key? key
+ @path_helpers_module.send :undef_method, path_name
+ @url_helpers_module.send :undef_method, url_name
+ end
+ routes[key] = route
+ define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
+ define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN
+
+ @path_helpers << path_name
+ @url_helpers << url_name
+ end
+
+ def get(name)
+ routes[name.to_sym]
+ end
+
+ def key?(name)
+ return unless name
+ routes.key? name.to_sym
+ end
+
+ alias []= add
+ alias [] get
+ alias clear clear!
+
+ def each
+ routes.each { |name, route| yield name, route }
+ self
+ end
+
+ def names
+ routes.keys
+ end
+
+ def length
+ routes.length
+ end
+
+ # Given a +name+, defines name_path and name_url helpers.
+ # Used by 'direct', 'resolve', and 'polymorphic' route helpers.
+ def add_url_helper(name, defaults, &block)
+ helper = CustomUrlHelper.new(name, defaults, &block)
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ @path_helpers_module.module_eval do
+ redefine_method(path_name) do |*args|
+ helper.call(self, args, true)
+ end
+ end
+
+ @url_helpers_module.module_eval do
+ redefine_method(url_name) do |*args|
+ helper.call(self, args, false)
+ end
+ end
+
+ @path_helpers << path_name
+ @url_helpers << url_name
+
+ self
+ end
+
+ class UrlHelper
+ def self.create(route, options, route_name, url_strategy)
+ if optimize_helper?(route)
+ OptimizedUrlHelper.new(route, options, route_name, url_strategy)
+ else
+ new route, options, route_name, url_strategy
+ end
+ end
+
+ def self.optimize_helper?(route)
+ !route.glob? && route.path.requirements.empty?
+ end
+
+ attr_reader :url_strategy, :route_name
+
+ class OptimizedUrlHelper < UrlHelper
+ attr_reader :arg_size
+
+ def initialize(route, options, route_name, url_strategy)
+ super
+ @required_parts = @route.required_parts
+ @arg_size = @required_parts.size
+ end
+
+ def call(t, args, inner_options)
+ if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
+ options = t.url_options.merge @options
+ options[:path] = optimized_helper(args)
+
+ original_script_name = options.delete(:original_script_name)
+ script_name = t._routes.find_script_name(options)
+
+ if original_script_name
+ script_name = original_script_name + script_name
+ end
+
+ options[:script_name] = script_name
+
+ url_strategy.call options
+ else
+ super
+ end
+ end
+
+ private
+
+ def optimized_helper(args)
+ params = parameterize_args(args) do
+ raise_generation_error(args)
+ end
+
+ @route.format params
+ end
+
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
+
+ def parameterize_args(args)
+ params = {}
+ @arg_size.times { |i|
+ key = @required_parts[i]
+ value = args[i].to_param
+ yield key if value.nil? || value.empty?
+ params[key] = value
+ }
+ params
+ end
+
+ def raise_generation_error(args)
+ missing_keys = []
+ params = parameterize_args(args) { |missing_key|
+ missing_keys << missing_key
+ }
+ constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }]
+ message = "No route matches #{constraints.inspect}".dup
+ message << ", missing required keys: #{missing_keys.sort.inspect}"
+
+ raise ActionController::UrlGenerationError, message
+ end
+ end
+
+ def initialize(route, options, route_name, url_strategy)
+ @options = options
+ @segment_keys = route.segment_keys.uniq
+ @route = route
+ @url_strategy = url_strategy
+ @route_name = route_name
+ end
+
+ def call(t, args, inner_options)
+ controller_options = t.url_options
+ options = controller_options.merge @options
+ hash = handle_positional_args(controller_options,
+ inner_options || {},
+ args,
+ options,
+ @segment_keys)
+
+ t._routes.url_for(hash, route_name, url_strategy)
+ end
+
+ def handle_positional_args(controller_options, inner_options, args, result, path_params)
+ if args.size > 0
+ # take format into account
+ if path_params.include?(:format)
+ path_params_size = path_params.size - 1
+ else
+ path_params_size = path_params.size
+ end
+
+ if args.size < path_params_size
+ path_params -= controller_options.keys
+ path_params -= result.keys
+ else
+ path_params = path_params.dup
+ end
+ inner_options.each_key do |key|
+ path_params.delete(key)
+ end
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
+ end
+ 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(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
+ define_method(name) do |*args|
+ last = args.last
+ options = \
+ case last
+ when Hash
+ args.pop
+ when ActionController::Parameters
+ args.pop.to_h
+ end
+ helper.call self, args, options
+ end
+ end
+ end
+ end
+
+ # strategy for building urls to send to the client
+ PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
+ UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+
+ attr_accessor :formatter, :set, :named_routes, :default_scope, :router
+ attr_accessor :disable_clear_and_finalize, :resources_path_names
+ attr_accessor :default_url_options
+ attr_reader :env_key, :polymorphic_mappings
+
+ alias :routes :set
+
+ def self.default_resources_path_names
+ { new: "new", edit: "edit" }
+ end
+
+ def self.new_with_config(config)
+ route_set_config = DEFAULT_CONFIG
+
+ # engines apparently don't have this set
+ if config.respond_to? :relative_url_root
+ route_set_config.relative_url_root = config.relative_url_root
+ end
+
+ if config.respond_to? :api_only
+ route_set_config.api_only = config.api_only
+ end
+
+ new route_set_config
+ end
+
+ Config = Struct.new :relative_url_root, :api_only
+
+ DEFAULT_CONFIG = Config.new(nil, false)
+
+ def initialize(config = DEFAULT_CONFIG)
+ self.named_routes = NamedRouteCollection.new
+ self.resources_path_names = self.class.default_resources_path_names
+ self.default_url_options = {}
+
+ @config = config
+ @append = []
+ @prepend = []
+ @disable_clear_and_finalize = false
+ @finalized = false
+ @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
+
+ @set = Journey::Routes.new
+ @router = Journey::Router.new @set
+ @formatter = Journey::Formatter.new self
+ @polymorphic_mappings = {}
+ end
+
+ def eager_load!
+ router.eager_load!
+ routes.each(&:eager_load!)
+ nil
+ end
+
+ def relative_url_root
+ @config.relative_url_root
+ end
+
+ def api_only?
+ @config.api_only
+ end
+
+ def request_class
+ ActionDispatch::Request
+ end
+
+ def make_request(env)
+ request_class.new env
+ end
+ private :make_request
+
+ def draw(&block)
+ clear! unless @disable_clear_and_finalize
+ eval_block(block)
+ finalize! unless @disable_clear_and_finalize
+ nil
+ end
+
+ def append(&block)
+ @append << block
+ end
+
+ def prepend(&block)
+ @prepend << block
+ end
+
+ def eval_block(block)
+ mapper = Mapper.new(self)
+ if default_scope
+ mapper.with_default_scope(default_scope, &block)
+ else
+ mapper.instance_exec(&block)
+ end
+ end
+ private :eval_block
+
+ def finalize!
+ return if @finalized
+ @append.each { |blk| eval_block(blk) }
+ @finalized = true
+ end
+
+ def clear!
+ @finalized = false
+ named_routes.clear
+ set.clear
+ formatter.clear
+ @polymorphic_mappings.clear
+ @prepend.each { |blk| eval_block(blk) }
+ end
+
+ module MountedHelpers
+ extend ActiveSupport::Concern
+ include UrlFor
+ end
+
+ # Contains all the mounted helpers across different
+ # engines and the `main_app` helper for the application.
+ # You can include this in your classes if you want to
+ # access routes for other engines.
+ def mounted_helpers
+ MountedHelpers
+ end
+
+ def define_mounted_helper(name, script_namer = nil)
+ return if MountedHelpers.method_defined?(name)
+
+ routes = self
+ helpers = routes.url_helpers
+
+ MountedHelpers.class_eval do
+ define_method "_#{name}" do
+ RoutesProxy.new(routes, _routes_context, helpers, script_namer)
+ end
+ end
+
+ MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ def #{name}
+ @_#{name} ||= _#{name}
+ end
+ RUBY
+ end
+
+ def url_helpers(supports_path = true)
+ routes = self
+
+ Module.new do
+ extend ActiveSupport::Concern
+ include UrlFor
+
+ # Define url_for in the singleton level so one can do:
+ # Rails.application.routes.url_helpers.url_for(args)
+ proxy_class = Class.new do
+ include UrlFor
+ include routes.named_routes.path_helpers_module
+ include routes.named_routes.url_helpers_module
+
+ attr_reader :_routes
+
+ def initialize(routes)
+ @_routes = routes
+ end
+
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
+ end
+ end
+
+ @_proxy = proxy_class.new(routes)
+
+ class << self
+ def url_for(options)
+ @_proxy.url_for(options)
+ end
+
+ def full_url_for(options)
+ @_proxy.full_url_for(options)
+ end
+
+ def route_for(name, *args)
+ @_proxy.route_for(name, *args)
+ end
+
+ def optimize_routes_generation?
+ @_proxy.optimize_routes_generation?
+ end
+
+ def polymorphic_url(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_url(record_or_hash_or_array, options)
+ end
+
+ def polymorphic_path(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_path(record_or_hash_or_array, options)
+ end
+
+ def _routes; @_proxy._routes; end
+ def url_options; {}; end
+ end
+
+ url_helpers = routes.named_routes.url_helpers_module
+
+ # Make named_routes available in the module singleton
+ # as well, so one can do:
+ # Rails.application.routes.url_helpers.posts_path
+ extend url_helpers
+
+ # Any class that includes this module will get all
+ # named routes...
+ include url_helpers
+
+ if supports_path
+ path_helpers = routes.named_routes.path_helpers_module
+
+ include path_helpers
+ extend path_helpers
+ end
+
+ # plus a singleton class method called _routes ...
+ included do
+ redefine_singleton_method(:_routes) { routes }
+ end
+
+ # And an instance method _routes. Note that
+ # UrlFor (included in this module) add extra
+ # conveniences for working with @_routes.
+ define_method(:_routes) { @_routes || routes }
+
+ define_method(:_generate_paths_by_default) do
+ supports_path
+ end
+
+ private :_generate_paths_by_default
+ end
+ end
+
+ def empty?
+ routes.empty?
+ end
+
+ def add_route(mapping, name)
+ raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
+
+ if name && named_routes[name]
+ raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
+ "You may have defined two routes with the same name using the `:as` option, or " \
+ "you may be overriding a route already defined by a resource with the same naming. " \
+ "For the latter, you can restrict the routes created with `resources` as explained here: \n" \
+ "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
+ end
+
+ route = @set.add_route(name, mapping)
+ named_routes[name] = route if name
+
+ if route.segment_keys.include?(:controller)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :controller segment in a route is deprecated and
+ will be removed in Rails 6.0.
+ MSG
+ end
+
+ if route.segment_keys.include?(:action)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :action segment in a route is deprecated and
+ will be removed in Rails 6.0.
+ MSG
+ end
+
+ route
+ end
+
+ def add_polymorphic_mapping(klass, options, &block)
+ @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
+ end
+
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
+ end
+
+ class CustomUrlHelper
+ attr_reader :name, :defaults, :block
+
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
+ end
+
+ def call(t, args, only_path = false)
+ options = args.extract_options!
+ url = t.full_url_for(eval_block(t, args, options))
+
+ if only_path
+ "/" + url.partition(%r{(?<!/)/(?!/)}).last
+ else
+ url
+ end
+ end
+
+ private
+ def eval_block(t, args, options)
+ t.instance_exec(*args, merge_defaults(options), &block)
+ end
+
+ def merge_defaults(options)
+ defaults ? defaults.merge(options) : options
+ end
+ end
+
+ class Generator
+ PARAMETERIZE = lambda do |name, value|
+ if name == :controller
+ value
+ else
+ value.to_param
+ end
+ end
+
+ attr_reader :options, :recall, :set, :named_route
+
+ def initialize(named_route, options, recall, set)
+ @named_route = named_route
+ @options = options
+ @recall = recall
+ @set = set
+
+ normalize_options!
+ normalize_controller_action_id!
+ use_relative_controller!
+ normalize_controller!
+ end
+
+ def controller
+ @options[:controller]
+ end
+
+ def current_controller
+ @recall[:controller]
+ end
+
+ def use_recall_for(key)
+ if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
+ if !named_route_exists? || segment_keys.include?(key)
+ @options[key] = @recall[key]
+ end
+ end
+ end
+
+ def normalize_options!
+ # If an explicit :controller was given, always make :action explicit
+ # too, so that action expiry works as expected for things like
+ #
+ # generate({controller: 'content'}, {controller: 'content', action: 'show'})
+ #
+ # (the above is from the unit tests). In the above case, because the
+ # controller was explicitly given, but no action, the action is implied to
+ # be "index", not the recalled action of "show".
+
+ if options[:controller]
+ options[:action] ||= "index"
+ options[:controller] = options[:controller].to_s
+ end
+
+ if options.key?(:action)
+ options[:action] = (options[:action] || "index").to_s
+ end
+ end
+
+ # This pulls :controller, :action, and :id out of the recall.
+ # The recall key is only used if there is no key in the options
+ # or if the key in the options is identical. If any of
+ # :controller, :action or :id is not found, don't pull any
+ # more keys from the recall.
+ def normalize_controller_action_id!
+ use_recall_for(:controller) || return
+ use_recall_for(:action) || return
+ use_recall_for(:id)
+ end
+
+ # if the current controller is "foo/bar/baz" and controller: "baz/bat"
+ # is specified, the controller becomes "foo/baz/bat"
+ def use_relative_controller!
+ if !named_route && different_controller? && !controller.start_with?("/")
+ old_parts = current_controller.split("/")
+ size = controller.count("/") + 1
+ parts = old_parts[0...-size] << controller
+ @options[:controller] = parts.join("/")
+ end
+ end
+
+ # Remove leading slashes from controllers
+ def normalize_controller!
+ if controller
+ if controller.start_with?("/".freeze)
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
+ end
+ end
+
+ # Generates a path from routes, returns [path, params].
+ # If no route is generated the formatter will raise ActionController::UrlGenerationError
+ def generate
+ @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
+ end
+
+ def different_controller?
+ return false unless current_controller
+ controller.to_param != current_controller.to_param
+ end
+
+ private
+ def named_route_exists?
+ named_route && set.named_routes[named_route]
+ end
+
+ def segment_keys
+ set.named_routes[named_route].segment_keys
+ end
+ end
+
+ # Generate the path indicated by the arguments, and return an array of
+ # the keys that were not used to generate it.
+ def extra_keys(options, recall = {})
+ generate_extras(options, recall).last
+ end
+
+ def generate_extras(options, recall = {})
+ route_key = options.delete :use_route
+ path, params = generate(route_key, options, recall)
+ return path, params.keys
+ end
+
+ def generate(route_key, options, recall = {})
+ Generator.new(route_key, options, recall, self).generate
+ end
+ private :generate
+
+ RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
+ :trailing_slash, :anchor, :params, :only_path, :script_name,
+ :original_script_name, :relative_url_root]
+
+ def optimize_routes_generation?
+ default_url_options.empty?
+ end
+
+ def find_script_name(options)
+ options.delete(:script_name) || find_relative_url_root(options) || ""
+ end
+
+ def find_relative_url_root(options)
+ options.delete(:relative_url_root) || relative_url_root
+ end
+
+ def path_for(options, route_name = nil)
+ url_for(options, route_name, PATH)
+ end
+
+ # The +options+ argument must be a hash whose keys are *symbols*.
+ def url_for(options, route_name = nil, url_strategy = UNKNOWN)
+ options = default_url_options.merge options
+
+ user = password = nil
+
+ if options[:user] && options[:password]
+ user = options.delete :user
+ password = options.delete :password
+ end
+
+ recall = options.delete(:_recall) { {} }
+
+ original_script_name = options.delete(:original_script_name)
+ script_name = find_script_name options
+
+ if original_script_name
+ script_name = original_script_name + script_name
+ end
+
+ path_options = options.dup
+ RESERVED_OPTIONS.each { |ro| path_options.delete ro }
+
+ path, params = generate(route_name, path_options, recall)
+
+ if options.key? :params
+ params.merge! options[:params]
+ end
+
+ options[:path] = path
+ options[:script_name] = script_name
+ options[:params] = params
+ options[:user] = user
+ options[:password] = password
+
+ url_strategy.call options
+ end
+
+ def call(env)
+ req = make_request(env)
+ req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
+ @router.serve(req)
+ end
+
+ def recognize_path(path, environment = {})
+ method = (environment[:method] || "GET").to_s.upcase
+ path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
+ extras = environment[:extras] || {}
+
+ begin
+ env = Rack::MockRequest.env_for(path, method: method)
+ rescue URI::InvalidURIError => e
+ raise ActionController::RoutingError, e.message
+ end
+
+ req = make_request(env)
+ recognize_path_with_request(req, path, extras)
+ end
+
+ def recognize_path_with_request(req, path, extras, raise_on_missing: true)
+ @router.recognize(req) do |route, params|
+ params.merge!(extras)
+ params.each do |key, value|
+ if value.is_a?(String)
+ value = value.dup.force_encoding(Encoding::BINARY)
+ params[key] = URI.parser.unescape(value)
+ end
+ end
+ req.path_parameters = params
+ app = route.app
+ if app.matches?(req) && app.dispatcher?
+ begin
+ req.controller_class
+ rescue NameError
+ raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
+ end
+
+ return req.path_parameters
+ elsif app.matches?(req) && app.engine?
+ path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
+ return path_parameters if path_parameters
+ end
+ end
+
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
+ end
+ end
+ # :startdoc:
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
new file mode 100644
index 0000000000..587a72729c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+
+module ActionDispatch
+ module Routing
+ class RoutesProxy #:nodoc:
+ include ActionDispatch::Routing::UrlFor
+
+ attr_accessor :scope, :routes
+ alias :_routes :routes
+
+ def initialize(routes, scope, helpers, script_namer = nil)
+ @routes, @scope = routes, scope
+ @helpers = helpers
+ @script_namer = script_namer
+ end
+
+ def url_options
+ scope.send(:_with_routes, routes) do
+ scope.url_options
+ end
+ end
+
+ private
+ def respond_to_missing?(method, _)
+ super || @helpers.respond_to?(method)
+ end
+
+ def method_missing(method, *args)
+ if @helpers.respond_to?(method)
+ self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args)
+ options = args.extract_options!
+ options = url_options.merge((options || {}).symbolize_keys)
+
+ if @script_namer
+ options[:script_name] = merge_script_names(
+ options[:script_name],
+ @script_namer.call(options)
+ )
+ end
+
+ args << options
+ @helpers.#{method}(*args)
+ end
+ RUBY
+ public_send(method, *args)
+ else
+ super
+ end
+ end
+
+ # Keeps the part of the script name provided by the global
+ # context via ENV["SCRIPT_NAME"], which `mount` doesn't know
+ # about since it depends on the specific request, but use our
+ # script name resolver for the mount point dependent part.
+ def merge_script_names(previous_script_name, new_script_name)
+ return new_script_name unless previous_script_name
+
+ resolved_parts = new_script_name.count("/")
+ previous_parts = previous_script_name.count("/")
+ context_parts = previous_parts - resolved_parts + 1
+
+ (previous_script_name.split("/").slice(0, context_parts).join("/")) + new_script_name
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
new file mode 100644
index 0000000000..fa345dccdf
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Routing
+ # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
+ # is also possible: a URL can be generated from one of your routing definitions.
+ # URL generation functionality is centralized in this module.
+ #
+ # See ActionDispatch::Routing for general information about routing and routes.rb.
+ #
+ # <b>Tip:</b> If you need to generate URLs from your models or some other place,
+ # then ActionController::UrlFor is what you're looking for. Read on for
+ # an introduction. In general, this module should not be included on its own,
+ # as it is usually included by url_helpers (as in Rails.application.routes.url_helpers).
+ #
+ # == URL generation from parameters
+ #
+ # As you may know, some functions, such as ActionController::Base#url_for
+ # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
+ # of parameters. For example, you've probably had the chance to write code
+ # like this in one of your views:
+ #
+ # <%= link_to('Click here', controller: 'users',
+ # action: 'new', message: 'Welcome!') %>
+ # # => <a href="/users/new?message=Welcome%21">Click here</a>
+ #
+ # link_to, and all other functions that require URL generation functionality,
+ # actually use ActionController::UrlFor under the hood. And in particular,
+ # they use the ActionController::UrlFor#url_for method. One can generate
+ # the same path as the above example by using the following code:
+ #
+ # include UrlFor
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # only_path: true)
+ # # => "/users/new?message=Welcome%21"
+ #
+ # Notice the <tt>only_path: true</tt> part. This is because UrlFor has no
+ # information about the website hostname that your Rails app is serving. So if you
+ # want to include the hostname as well, then you must also pass the <tt>:host</tt>
+ # argument:
+ #
+ # include UrlFor
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # host: 'www.example.com')
+ # # => "http://www.example.com/users/new?message=Welcome%21"
+ #
+ # By default, all controllers and views have access to a special version of url_for,
+ # that already knows what the current hostname is. So if you use url_for in your
+ # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
+ # argument.
+ #
+ # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
+ # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for'
+ # in full. However, mailers don't have hostname information, and you still have to provide
+ # the +:host+ argument or set the default host that will be used in all mailers using the
+ # configuration option +config.action_mailer.default_url_options+. For more information on
+ # url_for in mailers read the ActionMailer#Base documentation.
+ #
+ #
+ # == URL generation for named routes
+ #
+ # UrlFor also allows one to access methods that have been auto-generated from
+ # named routes. For example, suppose that you have a 'users' resource in your
+ # <tt>config/routes.rb</tt>:
+ #
+ # resources :users
+ #
+ # This generates, among other things, the method <tt>users_path</tt>. By default,
+ # this method is accessible from your controllers, views and mailers. If you need
+ # to access this auto-generated method from other places (such as a model), then
+ # you can do that by including Rails.application.routes.url_helpers in your class:
+ #
+ # class User < ActiveRecord::Base
+ # include Rails.application.routes.url_helpers
+ #
+ # def base_uri
+ # user_path(self)
+ # end
+ # end
+ #
+ # User.find(1).base_uri # => "/users/1"
+ #
+ module UrlFor
+ extend ActiveSupport::Concern
+ include PolymorphicRoutes
+
+ included do
+ unless method_defined?(:default_url_options)
+ # Including in a class uses an inheritable hash. Modules get a plain hash.
+ if respond_to?(:class_attribute)
+ class_attribute :default_url_options
+ else
+ mattr_writer :default_url_options
+ end
+
+ self.default_url_options = {}
+ end
+
+ include(*_url_for_modules) if respond_to?(:_url_for_modules)
+ end
+
+ def initialize(*)
+ @_routes = nil
+ super
+ end
+
+ # Hook overridden in controller to add request information
+ # with +default_url_options+. Application logic should not
+ # go into url_options.
+ def url_options
+ default_url_options
+ end
+
+ # Generate a URL based on the options provided, default_url_options and the
+ # routes defined in routes.rb. The following options are supported:
+ #
+ # * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+.
+ # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
+ # * <tt>:host</tt> - Specifies the host the link should be targeted at.
+ # If <tt>:only_path</tt> is false, this option must be
+ # provided either explicitly, or via +default_url_options+.
+ # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+
+ # to split the subdomain from the host.
+ # If false, removes all subdomains from the host part of the link.
+ # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+
+ # to split the domain from the host.
+ # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if
+ # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to
+ # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
+ # * <tt>:port</tt> - Optionally specify the port to connect to.
+ # * <tt>:anchor</tt> - An anchor name to be appended to the path.
+ # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
+ # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
+ #
+ # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
+ # +url_for+ is forwarded to the Routes module.
+ #
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080'
+ # # => 'http://somehost.org:8080/tasks/testing'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true
+ # # => '/tasks/testing#ok'
+ # url_for controller: 'tasks', action: 'testing', trailing_slash: true
+ # # => 'http://somehost.org/tasks/testing/'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33'
+ # # => 'http://somehost.org/tasks/testing?number=33'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp"
+ # # => 'http://somehost.org/myapp/tasks/testing'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
+ # # => '/myapp/tasks/testing'
+ #
+ # Missing routes keys may be filled in from the current request's parameters
+ # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
+ # placed in the path). Given that the current action has been reached
+ # through <tt>GET /users/1</tt>:
+ #
+ # url_for(only_path: true) # => '/users/1'
+ # url_for(only_path: true, action: 'edit') # => '/users/1/edit'
+ # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
+ #
+ # Notice that no +:id+ parameter was provided to the first +url_for+ call
+ # and the helper used the one from the route's path. Any path parameter
+ # implicitly used by +url_for+ can always be overwritten like shown on the
+ # last +url_for+ calls.
+ def url_for(options = nil)
+ full_url_for(options)
+ end
+
+ def full_url_for(options = nil) # :nodoc:
+ case options
+ when nil
+ _routes.url_for(url_options.symbolize_keys)
+ when Hash, ActionController::Parameters
+ route_name = options.delete :use_route
+ merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options)
+ _routes.url_for(merged_url_options, route_name)
+ when String
+ options
+ when Symbol
+ HelperMethodBuilder.url.handle_string_call self, options
+ when Array
+ components = options.dup
+ polymorphic_url(components, components.extract_options!)
+ when Class
+ HelperMethodBuilder.url.handle_class_call self, options
+ else
+ HelperMethodBuilder.url.handle_model_call self, options
+ end
+ end
+
+ def route_for(name, *args) # :nodoc:
+ public_send(:"#{name}_url", *args)
+ end
+
+ protected
+
+ def optimize_routes_generation?
+ _routes.optimize_routes_generation? && default_url_options.empty?
+ end
+
+ private
+
+ def _with_routes(routes) # :doc:
+ old_routes, @_routes = @_routes, routes
+ yield
+ ensure
+ @_routes = old_routes
+ end
+
+ def _routes_context # :doc:
+ self
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
new file mode 100644
index 0000000000..f85f816bb9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+gem "capybara", "~> 2.15"
+
+require "capybara/dsl"
+require "capybara/minitest"
+require "action_controller"
+require "action_dispatch/system_testing/driver"
+require "action_dispatch/system_testing/browser"
+require "action_dispatch/system_testing/server"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
+require "action_dispatch/system_testing/test_helpers/undef_methods"
+
+module ActionDispatch
+ # = System Testing
+ #
+ # System tests let you test applications in the browser. Because system
+ # tests use a real browser experience, you can test all of your JavaScript
+ # easily from your test suite.
+ #
+ # To create a system test in your application, extend your test class
+ # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a
+ # base and allow you to configure the settings through your
+ # <tt>application_system_test_case.rb</tt> file that is generated with a new
+ # application or scaffold.
+ #
+ # Here is an example system test:
+ #
+ # require 'application_system_test_case'
+ #
+ # class Users::CreateTest < ApplicationSystemTestCase
+ # test "adding a new user" do
+ # visit users_path
+ # click_on 'New User'
+ #
+ # fill_in 'Name', with: 'Arya'
+ # click_on 'Create User'
+ #
+ # assert_text 'Arya'
+ # end
+ # end
+ #
+ # When generating an application or scaffold, an +application_system_test_case.rb+
+ # file will also be generated containing the base class for system testing.
+ # This is where you can change the driver, add Capybara settings, and other
+ # configuration for your system tests.
+ #
+ # require "test_helper"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
+ # end
+ #
+ # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the
+ # Selenium driver, with the Chrome browser, and a browser size of 1400x1400.
+ #
+ # Changing the driver configuration options is easy. Let's say you want to use
+ # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+
+ # file add the following:
+ #
+ # require "test_helper"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :firefox
+ # end
+ #
+ # +driven_by+ has a required argument for the driver name. The keyword
+ # arguments are +:using+ for the browser and +:screen_size+ to change the
+ # size of the browser screen. These two options are not applicable for
+ # headless drivers and will be silently ignored if passed.
+ #
+ # Headless browsers such as headless Chrome and headless Firefox are also supported.
+ # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+.
+ #
+ # To use a headless driver, like Poltergeist, update your Gemfile to use
+ # Poltergeist instead of Selenium and then declare the driver name in the
+ # +application_system_test_case.rb+ file. In this case, you would leave out
+ # the +:using+ option because the driver is headless, but you can still use
+ # +:screen_size+ to change the size of the browser screen, also you can use
+ # +:options+ to pass options supported by the driver. Please refer to your
+ # driver documentation to learn about supported options.
+ #
+ # require "test_helper"
+ # require "capybara/poltergeist"
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :poltergeist, screen_size: [1400, 1400], options:
+ # { js_errors: true }
+ # end
+ #
+ # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
+ # and Rails, any driver that is supported by Capybara is supported by system
+ # tests as long as you include the required gems and files.
+ class SystemTestCase < IntegrationTest
+ include Capybara::DSL
+ include Capybara::Minitest::Assertions
+ include SystemTesting::TestHelpers::SetupAndTeardown
+ include SystemTesting::TestHelpers::ScreenshotHelper
+ include SystemTesting::TestHelpers::UndefMethods
+
+ def initialize(*) # :nodoc:
+ super
+ self.class.driver.use
+ end
+
+ def self.start_application # :nodoc:
+ Capybara.app = Rack::Builder.new do
+ map "/" do
+ run Rails.application
+ end
+ end
+
+ SystemTesting::Server.new.run
+ end
+
+ class_attribute :driver, instance_accessor: false
+
+ # System Test configuration options
+ #
+ # The default settings are Selenium, using Chrome, with a screen size
+ # of 1400x1400.
+ #
+ # Examples:
+ #
+ # driven_by :poltergeist
+ #
+ # driven_by :selenium, screen_size: [800, 800]
+ #
+ # driven_by :selenium, using: :chrome
+ #
+ # driven_by :selenium, using: :headless_chrome
+ #
+ # driven_by :selenium, using: :firefox
+ #
+ # driven_by :selenium, using: :headless_firefox
+ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
+ self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
+ end
+
+ driven_by :selenium
+
+ ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
+ end
+
+ SystemTestCase.start_application
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
new file mode 100644
index 0000000000..10e6888ab3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Browser # :nodoc:
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def type
+ case name
+ when :headless_chrome
+ :chrome
+ when :headless_firefox
+ :firefox
+ else
+ name
+ end
+ end
+
+ def options
+ case name
+ when :headless_chrome
+ headless_chrome_browser_options
+ when :headless_firefox
+ headless_firefox_browser_options
+ end
+ end
+
+ private
+ def headless_chrome_browser_options
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.args << "--headless"
+ options.args << "--disable-gpu"
+
+ options
+ end
+
+ def headless_firefox_browser_options
+ options = Selenium::WebDriver::Firefox::Options.new
+ options.args << "-headless"
+
+ options
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
new file mode 100644
index 0000000000..5252ff6746
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Driver # :nodoc:
+ def initialize(name, **options)
+ @name = name
+ @browser = Browser.new(options[:using])
+ @screen_size = options[:screen_size]
+ @options = options[:options]
+ end
+
+ def use
+ register if registerable?
+
+ setup
+ end
+
+ private
+ def registerable?
+ [:selenium, :poltergeist, :webkit].include?(@name)
+ end
+
+ def register
+ Capybara.register_driver @name do |app|
+ case @name
+ when :selenium then register_selenium(app)
+ when :poltergeist then register_poltergeist(app)
+ when :webkit then register_webkit(app)
+ end
+ end
+ end
+
+ def browser_options
+ @options.merge(options: @browser.options).compact
+ end
+
+ def register_selenium(app)
+ Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver|
+ driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
+ end
+ end
+
+ def register_poltergeist(app)
+ Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size))
+ end
+
+ def register_webkit(app)
+ Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver|
+ driver.resize_window_to(driver.current_window_handle, *@screen_size)
+ end
+ end
+
+ def setup
+ Capybara.current_driver = @name
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb
new file mode 100644
index 0000000000..4fc1f33767
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Server # :nodoc:
+ class << self
+ attr_accessor :silence_puma
+ end
+
+ self.silence_puma = false
+
+ def run
+ setup
+ end
+
+ private
+ def setup
+ set_server
+ set_port
+ end
+
+ def set_server
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
+ end
+
+ def set_port
+ Capybara.always_include_port = true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
new file mode 100644
index 0000000000..df0c5d3f0e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ # Screenshot helper for system testing.
+ module ScreenshotHelper
+ # Takes a screenshot of the current page in the browser.
+ #
+ # +take_screenshot+ can be used at any point in your system tests to take
+ # a screenshot of the current state. This can be useful for debugging or
+ # automating visual testing.
+ #
+ # The screenshot will be displayed in your console, if supported.
+ #
+ # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
+ # control the output. Possible values are:
+ # * [+simple+ (default)] Only displays the screenshot path.
+ # This is the default value.
+ # * [+inline+] Display the screenshot in the terminal using the
+ # iTerm image protocol (https://iterm2.com/documentation-images.html).
+ # * [+artifact+] Display the screenshot in the terminal, using the terminal
+ # artifact format (https://buildkite.github.io/terminal/inline-images/).
+ def take_screenshot
+ save_image
+ puts display_image
+ end
+
+ # Takes a screenshot of the current page in the browser if the test
+ # failed.
+ #
+ # +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt>
+ # that is generated with the application. To take screenshots when a test
+ # fails add +take_failed_screenshot+ to the teardown block before clearing
+ # sessions.
+ def take_failed_screenshot
+ take_screenshot if failed? && supports_screenshot?
+ end
+
+ private
+ def image_name
+ failed? ? "failures_#{method_name}" : method_name
+ end
+
+ def image_path
+ @image_path ||= absolute_image_path.relative_path_from(Pathname.pwd).to_s
+ end
+
+ def absolute_image_path
+ Rails.root.join("tmp/screenshots/#{image_name}.png")
+ end
+
+ def save_image
+ page.save_screenshot(absolute_image_path)
+ end
+
+ def output_type
+ # Environment variables have priority
+ output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
+
+ # Default to outputting a path to the screenshot
+ output_type ||= "simple"
+
+ output_type
+ end
+
+ def display_image
+ message = "[Screenshot]: #{image_path}\n".dup
+
+ case output_type
+ when "artifact"
+ message << "\e]1338;url=artifact://#{absolute_image_path}\a\n"
+ when "inline"
+ name = inline_base64(File.basename(absolute_image_path))
+ image = inline_base64(File.read(absolute_image_path))
+ message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
+ end
+
+ message
+ end
+
+ def inline_base64(path)
+ Base64.encode64(path).gsub("\n", "")
+ end
+
+ def failed?
+ !passed? && !skipped?
+ end
+
+ def supports_screenshot?
+ Capybara.current_driver != :rack_test
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
new file mode 100644
index 0000000000..ffa85f4e14
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module SetupAndTeardown # :nodoc:
+ DEFAULT_HOST = "http://127.0.0.1"
+
+ def host!(host)
+ super
+ Capybara.app_host = host
+ end
+
+ def before_setup
+ host! DEFAULT_HOST
+ super
+ end
+
+ def after_teardown
+ take_failed_screenshot
+ Capybara.reset_sessions!
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
new file mode 100644
index 0000000000..d64be3b3d9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module UndefMethods # :nodoc:
+ extend ActiveSupport::Concern
+ included do
+ METHODS = %i(get post put patch delete).freeze
+
+ METHODS.each do |verb|
+ undef_method verb
+ end
+
+ def method_missing(method, *args, &block)
+ if METHODS.include?(method)
+ raise NoMethodError, "System tests cannot make direct requests via ##{method}; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information."
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb
new file mode 100644
index 0000000000..dc019db6ac
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ # This is a class that abstracts away an asserted response. It purposely
+ # does not inherit from Response because it doesn't need it. That means it
+ # does not have headers or a body.
+ class AssertionResponse
+ attr_reader :code, :name
+
+ GENERIC_RESPONSE_CODES = { # :nodoc:
+ success: "2XX",
+ missing: "404",
+ redirect: "3XX",
+ error: "5XX"
+ }
+
+ # Accepts a specific response status code as an Integer (404) or String
+ # ('404') or a response status range as a Symbol pseudo-code (:success,
+ # indicating any 200-299 status code).
+ def initialize(code_or_name)
+ if code_or_name.is_a?(Symbol)
+ @name = code_or_name
+ @code = code_from_name(code_or_name)
+ else
+ @name = name_from_code(code_or_name)
+ @code = code_or_name
+ end
+
+ raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
+ raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
+ end
+
+ def code_and_name
+ "#{code}: #{name}"
+ end
+
+ private
+
+ def code_from_name(name)
+ GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
+ end
+
+ def name_from_code(code)
+ GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
new file mode 100644
index 0000000000..08c2969685
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "rails-dom-testing"
+
+module ActionDispatch
+ module Assertions
+ autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
+ autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
+
+ extend ActiveSupport::Concern
+
+ include ResponseAssertions
+ include RoutingAssertions
+ include Rails::Dom::Testing::Assertions
+
+ def html_document
+ @html_document ||= if @response.content_type.to_s.end_with?("xml")
+ Nokogiri::XML::Document.parse(@response.body)
+ else
+ Nokogiri::HTML::Document.parse(@response.body)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
new file mode 100644
index 0000000000..98b1965d22
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Assertions
+ # A small suite of assertions that test responses from \Rails applications.
+ module ResponseAssertions
+ RESPONSE_PREDICATES = { # :nodoc:
+ success: :successful?,
+ missing: :not_found?,
+ redirect: :redirection?,
+ error: :server_error?,
+ }
+
+ # Asserts that the response is one of the following types:
+ #
+ # * <tt>:success</tt> - Status code was in the 200-299 range
+ # * <tt>:redirect</tt> - Status code was in the 300-399 range
+ # * <tt>:missing</tt> - Status code was 404
+ # * <tt>:error</tt> - Status code was in the 500-599 range
+ #
+ # You can also pass an explicit status number like <tt>assert_response(501)</tt>
+ # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
+ #
+ # # Asserts that the response was a redirection
+ # assert_response :redirect
+ #
+ # # Asserts that the response code was status code 401 (unauthorized)
+ # assert_response 401
+ def assert_response(type, message = nil)
+ message ||= generate_response_message(type)
+
+ if RESPONSE_PREDICATES.keys.include?(type)
+ assert @response.send(RESPONSE_PREDICATES[type]), message
+ else
+ assert_equal AssertionResponse.new(type).code, @response.response_code, message
+ end
+ end
+
+ # Asserts that the redirection options passed in match those of the redirect called in the latest action.
+ # This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
+ # match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
+ #
+ # # Asserts that the redirection was to the "index" action on the WeblogController
+ # assert_redirected_to controller: "weblog", action: "index"
+ #
+ # # Asserts that the redirection was to the named route login_url
+ # assert_redirected_to login_url
+ #
+ # # Asserts that the redirection was to the URL for @customer
+ # assert_redirected_to @customer
+ #
+ # # Asserts that the redirection matches the regular expression
+ # assert_redirected_to %r(\Ahttp://example.org)
+ def assert_redirected_to(options = {}, message = nil)
+ assert_response(:redirect, message)
+ return true if options === @response.location
+
+ redirect_is = normalize_argument_to_redirection(@response.location)
+ redirect_expected = normalize_argument_to_redirection(options)
+
+ message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
+ assert_operator redirect_expected, :===, redirect_is, message
+ end
+
+ private
+ # Proxy to to_param if the object will respond to it.
+ def parameterize(value)
+ value.respond_to?(:to_param) ? value.to_param : value
+ end
+
+ def normalize_argument_to_redirection(fragment)
+ if Regexp === fragment
+ fragment
+ else
+ handle = @controller || ActionController::Redirecting
+ handle._compute_redirect_to_location(@request, fragment)
+ end
+ end
+
+ def generate_response_message(expected, actual = @response.response_code)
+ "Expected response to be a <#{code_with_name(expected)}>,"\
+ " but was a <#{code_with_name(actual)}>"
+ .dup.concat(location_if_redirected).concat(response_body_if_short)
+ end
+
+ def response_body_if_short
+ return "" if @response.body.size > 500
+ "\nResponse body: #{@response.body}"
+ end
+
+ def location_if_redirected
+ return "" unless @response.redirection? && @response.location.present?
+ location = normalize_argument_to_redirection(@response.location)
+ " redirect to <#{location}>"
+ end
+
+ def code_with_name(code_or_name)
+ if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
+ code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
+ end
+
+ AssertionResponse.new(code_or_name).code_and_name
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
new file mode 100644
index 0000000000..5390581139
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require "uri"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/string/access"
+require "action_controller/metal/exceptions"
+
+module ActionDispatch
+ module Assertions
+ # Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
+ module RoutingAssertions
+ # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
+ # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
+ #
+ # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
+ # requiring a specific HTTP method. The hash should contain a :path with the incoming request path
+ # and a :method containing the required HTTP verb.
+ #
+ # # Asserts that POSTing to /items will call the create action on ItemsController
+ # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
+ #
+ # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
+ # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
+ # argument because appending the query string on the path directly will not work. For example:
+ #
+ # # Asserts that a path of '/items/list/1?view=print' returns the correct options
+ # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
+ #
+ # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
+ #
+ # # Check the default route (i.e., the index action)
+ # assert_recognizes({controller: 'items', action: 'index'}, 'items')
+ #
+ # # Test a specific action
+ # assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
+ #
+ # # Test an action with a parameter
+ # assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
+ #
+ # # Test a custom route
+ # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
+ def assert_recognizes(expected_options, path, extras = {}, msg = nil)
+ if path.is_a?(Hash) && path[:method].to_s == "all"
+ [:get, :post, :put, :delete].each do |method|
+ assert_recognizes(expected_options, path.merge(method: method), extras, msg)
+ end
+ else
+ request = recognized_request_for(path, extras, msg)
+
+ expected_options = expected_options.clone
+
+ expected_options.stringify_keys!
+
+ msg = message(msg, "") {
+ sprintf("The recognized options <%s> did not match <%s>, difference:",
+ request.path_parameters, expected_options)
+ }
+
+ assert_equal(expected_options, request.path_parameters, msg)
+ end
+ end
+
+ # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
+ # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
+ # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
+ #
+ # The +defaults+ parameter is unused.
+ #
+ # # Asserts that the default action is generated for a route with no action
+ # assert_generates "/items", controller: "items", action: "index"
+ #
+ # # Tests that the list action is properly routed
+ # assert_generates "/items/list", controller: "items", action: "list"
+ #
+ # # Tests the generation of a route with a parameter
+ # assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
+ #
+ # # Asserts that the generated route gives us our custom route
+ # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
+ def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
+ if expected_path =~ %r{://}
+ fail_on(URI::InvalidURIError, message) do
+ uri = URI.parse(expected_path)
+ expected_path = uri.path.to_s.empty? ? "/" : uri.path
+ end
+ else
+ expected_path = "/#{expected_path}" unless expected_path.first == "/"
+ end
+ # Load routes.rb if it hasn't been loaded.
+
+ options = options.clone
+ generated_path, query_string_keys = @routes.generate_extras(options, defaults)
+ found_extras = options.reject { |k, _| ! query_string_keys.include? k }
+
+ msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
+ assert_equal(extras, found_extras, msg)
+
+ msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
+ expected_path)
+ assert_equal(expected_path, generated_path, msg)
+ end
+
+ # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
+ # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
+ # and +assert_generates+ into one step.
+ #
+ # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
+ # +message+ parameter allows you to specify a custom error message to display upon failure.
+ #
+ # # Asserts a basic route: a controller with the default action (index)
+ # assert_routing '/home', controller: 'home', action: 'index'
+ #
+ # # Test a route generated with a specific controller, action, and parameter (id)
+ # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
+ #
+ # # Asserts a basic route (controller + default action), with an error message if it fails
+ # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
+ #
+ # # Tests a route, providing a defaults hash
+ # assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
+ #
+ # # Tests a route with an HTTP method
+ # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
+ def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
+ assert_recognizes(options, path, extras, message)
+
+ controller, default_controller = options[:controller], defaults[:controller]
+ if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
+ options[:controller] = "/#{controller}"
+ end
+
+ generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
+ assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
+ end
+
+ # A helper to make it easier to test different route configurations.
+ # This method temporarily replaces @routes with a new RouteSet instance.
+ #
+ # The new instance is yielded to the passed block. Typically the block
+ # will create some routes using <tt>set.draw { match ... }</tt>:
+ #
+ # with_routing do |set|
+ # set.draw do
+ # resources :users
+ # end
+ # assert_equal "/users", users_path
+ # end
+ #
+ def with_routing
+ old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
+ if defined?(@controller) && @controller
+ old_controller, @controller = @controller, @controller.clone
+ _routes = @routes
+
+ @controller.singleton_class.include(_routes.url_helpers)
+
+ if @controller.respond_to? :view_context_class
+ @controller.view_context_class = Class.new(@controller.view_context_class) do
+ include _routes.url_helpers
+ end
+ end
+ end
+ yield @routes
+ ensure
+ @routes = old_routes
+ if defined?(@controller) && @controller
+ @controller = old_controller
+ end
+ end
+
+ # ROUTES TODO: These assertions should really work in an integration context
+ def method_missing(selector, *args, &block)
+ if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
+ @controller.send(selector, *args, &block)
+ else
+ super
+ end
+ end
+
+ private
+ # Recognizes the route for a given path.
+ def recognized_request_for(path, extras = {}, msg)
+ if path.is_a?(Hash)
+ method = path[:method]
+ path = path[:path]
+ else
+ method = :get
+ end
+
+ request = ActionController::TestRequest.create @controller.class
+
+ if path =~ %r{://}
+ fail_on(URI::InvalidURIError, msg) do
+ uri = URI.parse(path)
+ request.env["rack.url_scheme"] = uri.scheme || "http"
+ request.host = uri.host if uri.host
+ request.port = uri.port if uri.port
+ request.path = uri.path.to_s.empty? ? "/" : uri.path
+ end
+ else
+ path = "/#{path}" unless path.first == "/"
+ request.path = path
+ end
+
+ request.request_method = method if method
+
+ params = fail_on(ActionController::RoutingError, msg) do
+ @routes.recognize_path(path, method: method, extras: extras)
+ end
+ request.path_parameters = params.with_indifferent_access
+
+ request
+ end
+
+ def fail_on(exception_class, message)
+ yield
+ rescue exception_class => e
+ raise Minitest::Assertion, message || e.message
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
new file mode 100644
index 0000000000..7171b6942c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -0,0 +1,652 @@
+# frozen_string_literal: true
+
+require "stringio"
+require "uri"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/object/try"
+require "rack/test"
+require "minitest"
+
+require "action_dispatch/testing/request_encoder"
+
+module ActionDispatch
+ module Integration #:nodoc:
+ module RequestHelpers
+ # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def get(path, **args)
+ process(:get, path, **args)
+ end
+
+ # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def post(path, **args)
+ process(:post, path, **args)
+ end
+
+ # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def patch(path, **args)
+ process(:patch, path, **args)
+ end
+
+ # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def put(path, **args)
+ process(:put, path, **args)
+ end
+
+ # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def delete(path, **args)
+ process(:delete, path, **args)
+ end
+
+ # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def head(path, *args)
+ process(:head, path, *args)
+ end
+
+ # Follow a single redirect response. If the last response was not a
+ # redirect, an exception will be raised. Otherwise, the redirect is
+ # performed on the location header.
+ def follow_redirect!
+ raise "not a redirect! #{status} #{status_message}" unless redirect?
+ get(response.location)
+ status
+ end
+ end
+
+ # An instance of this class represents a set of requests and responses
+ # performed sequentially by a test process. Because you can instantiate
+ # multiple sessions and run them side-by-side, you can also mimic (to some
+ # limited extent) multiple simultaneous users interacting with your system.
+ #
+ # Typically, you will instantiate a new session using
+ # IntegrationTest#open_session, rather than instantiating
+ # Integration::Session directly.
+ class Session
+ DEFAULT_HOST = "www.example.com"
+
+ include Minitest::Assertions
+ include TestProcess, RequestHelpers, Assertions
+
+ %w( status status_message headers body redirect? ).each do |method|
+ delegate method, to: :response, allow_nil: true
+ end
+
+ %w( path ).each do |method|
+ delegate method, to: :request, allow_nil: true
+ end
+
+ # The hostname used in the last request.
+ def host
+ @host || DEFAULT_HOST
+ end
+ attr_writer :host
+
+ # The remote_addr used in the last request.
+ attr_accessor :remote_addr
+
+ # The Accept header to send.
+ attr_accessor :accept
+
+ # A map of the cookies returned by the last response, and which will be
+ # sent with the next request.
+ def cookies
+ _mock_session.cookie_jar
+ end
+
+ # A reference to the controller instance used by the last request.
+ attr_reader :controller
+
+ # A reference to the request instance used by the last request.
+ attr_reader :request
+
+ # A reference to the response instance used by the last request.
+ attr_reader :response
+
+ # A running counter of the number of requests processed.
+ attr_accessor :request_count
+
+ include ActionDispatch::Routing::UrlFor
+
+ # Create and initialize a new Session instance.
+ def initialize(app)
+ super()
+ @app = app
+
+ reset!
+ end
+
+ def url_options
+ @url_options ||= default_url_options.dup.tap do |url_options|
+ url_options.reverse_merge!(controller.url_options) if controller
+
+ if @app.respond_to?(:routes)
+ url_options.reverse_merge!(@app.routes.default_url_options)
+ end
+
+ url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
+ end
+ end
+
+ # Resets the instance. This can be used to reset the state information
+ # in an existing session instance, so it can be used from a clean-slate
+ # condition.
+ #
+ # session.reset!
+ def reset!
+ @https = false
+ @controller = @request = @response = nil
+ @_mock_session = nil
+ @request_count = 0
+ @url_options = nil
+
+ self.host = DEFAULT_HOST
+ self.remote_addr = "127.0.0.1"
+ self.accept = "text/xml,application/xml,application/xhtml+xml," \
+ "text/html;q=0.9,text/plain;q=0.8,image/png," \
+ "*/*;q=0.5"
+
+ unless defined? @named_routes_configured
+ # the helpers are made protected by default--we make them public for
+ # easier access during testing and troubleshooting.
+ @named_routes_configured = true
+ end
+ end
+
+ # Specify whether or not the session should mimic a secure HTTPS request.
+ #
+ # session.https!
+ # session.https!(false)
+ def https!(flag = true)
+ @https = flag
+ end
+
+ # Returns +true+ if the session is mimicking a secure HTTPS request.
+ #
+ # if session.https?
+ # ...
+ # end
+ def https?
+ @https
+ end
+
+ # Performs the actual request.
+ #
+ # - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
+ # as a symbol.
+ # - +path+: The URI (as a String) on which you want to perform the
+ # request.
+ # - +params+: The HTTP parameters that you want to pass. This may
+ # be +nil+,
+ # a Hash, or a String that is appropriately encoded
+ # (<tt>application/x-www-form-urlencoded</tt> or
+ # <tt>multipart/form-data</tt>).
+ # - +headers+: Additional headers to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
+ # - +env+: Additional env to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
+ #
+ # This method is rarely used directly. Use +#get+, +#post+, or other standard
+ # HTTP methods in integration tests. +#process+ is only required when using a
+ # request method that doesn't have a method defined in the integration tests.
+ #
+ # This method returns the response status, after performing the request.
+ # Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
+ # then that object's <tt>@response</tt> instance variable will point to a Response object
+ # which one can use to inspect the details of the response.
+ #
+ # Example:
+ # process :get, '/author', params: { since: 201501011400 }
+ def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
+ request_encoder = RequestEncoder.encoder(as)
+ headers ||= {}
+
+ if method == :get && as == :json && params
+ headers["X-Http-Method-Override"] = "GET"
+ method = :post
+ end
+
+ if path =~ %r{://}
+ path = build_expanded_path(path) do |location|
+ https! URI::HTTPS === location if location.scheme
+
+ if url_host = location.host
+ default = Rack::Request::DEFAULT_PORTS[location.scheme]
+ url_host += ":#{location.port}" if default != location.port
+ host! url_host
+ end
+ end
+ end
+
+ hostname, port = host.split(":")
+
+ request_env = {
+ :method => method,
+ :params => request_encoder.encode_params(params),
+
+ "SERVER_NAME" => hostname,
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
+ "HTTPS" => https? ? "on" : "off",
+ "rack.url_scheme" => https? ? "https" : "http",
+
+ "REQUEST_URI" => path,
+ "HTTP_HOST" => host,
+ "REMOTE_ADDR" => remote_addr,
+ "CONTENT_TYPE" => request_encoder.content_type,
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
+ }
+
+ wrapped_headers = Http::Headers.from_hash({})
+ wrapped_headers.merge!(headers) if headers
+
+ if xhr
+ wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
+ wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
+ end
+
+ # This modifies the passed request_env directly.
+ if wrapped_headers.present?
+ Http::Headers.from_hash(request_env).merge!(wrapped_headers)
+ end
+ if env.present?
+ Http::Headers.from_hash(request_env).merge!(env)
+ end
+
+ session = Rack::Test::Session.new(_mock_session)
+
+ # NOTE: rack-test v0.5 doesn't build a default uri correctly
+ # Make sure requested path is always a full URI.
+ session.request(build_full_uri(path, request_env), request_env)
+
+ @request_count += 1
+ @request = ActionDispatch::Request.new(session.last_request.env)
+ response = _mock_session.last_response
+ @response = ActionDispatch::TestResponse.from_response(response)
+ @response.request = @request
+ @html_document = nil
+ @url_options = nil
+
+ @controller = @request.controller_instance
+
+ response.status
+ end
+
+ # Set the host name to use in the next request.
+ #
+ # session.host! "www.example.com"
+ alias :host! :host=
+
+ private
+ def _mock_session
+ @_mock_session ||= Rack::MockSession.new(@app, host)
+ end
+
+ def build_full_uri(path, env)
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ end
+
+ def build_expanded_path(path)
+ location = URI.parse(path)
+ yield location if block_given?
+ path = location.path
+ location.query ? "#{path}?#{location.query}" : path
+ end
+ end
+
+ module Runner
+ include ActionDispatch::Assertions
+
+ APP_SESSIONS = {}
+
+ attr_reader :app
+
+ def initialize(*args, &blk)
+ super(*args, &blk)
+ @integration_session = nil
+ end
+
+ def before_setup # :nodoc:
+ @app = nil
+ super
+ end
+
+ def integration_session
+ @integration_session ||= create_session(app)
+ end
+
+ # Reset the current session. This is useful for testing multiple sessions
+ # in a single test case.
+ def reset!
+ @integration_session = create_session(app)
+ end
+
+ def create_session(app)
+ klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
+ # If the app is a Rails app, make url_helpers available on the session.
+ # This makes app.url_for and app.foo_path available in the console.
+ if app.respond_to?(:routes)
+ include app.routes.url_helpers
+ include app.routes.mounted_helpers
+ end
+ }
+ klass.new(app)
+ end
+
+ def remove! # :nodoc:
+ @integration_session = nil
+ end
+
+ %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
+ define_method(method) do |*args|
+ # reset the html_document variable, except for cookies/assigns calls
+ unless method == "cookies" || method == "assigns"
+ @html_document = nil
+ end
+
+ integration_session.__send__(method, *args).tap do
+ copy_session_variables!
+ end
+ end
+ end
+
+ # Open a new session instance. If a block is given, the new session is
+ # yielded to the block before being returned.
+ #
+ # session = open_session do |sess|
+ # sess.extend(CustomAssertions)
+ # end
+ #
+ # By default, a single session is automatically created for you, but you
+ # can use this method to open multiple sessions that ought to be tested
+ # simultaneously.
+ def open_session
+ dup.tap do |session|
+ session.reset!
+ yield session if block_given?
+ end
+ end
+
+ # Copy the instance variables from the current session instance into the
+ # test instance.
+ def copy_session_variables! #:nodoc:
+ @controller = @integration_session.controller
+ @response = @integration_session.response
+ @request = @integration_session.request
+ end
+
+ def default_url_options
+ integration_session.default_url_options
+ end
+
+ def default_url_options=(options)
+ integration_session.default_url_options = options
+ end
+
+ private
+ def respond_to_missing?(method, _)
+ integration_session.respond_to?(method) || super
+ end
+
+ # Delegate unhandled messages to the current session instance.
+ def method_missing(method, *args, &block)
+ if integration_session.respond_to?(method)
+ integration_session.public_send(method, *args, &block).tap do
+ copy_session_variables!
+ end
+ else
+ super
+ end
+ end
+ end
+ end
+
+ # An integration test spans multiple controllers and actions,
+ # tying them all together to ensure they work together as expected. It tests
+ # more completely than either unit or functional tests do, exercising the
+ # entire stack, from the dispatcher to the database.
+ #
+ # At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
+ # using the get/post methods:
+ #
+ # require "test_helper"
+ #
+ # class ExampleTest < ActionDispatch::IntegrationTest
+ # fixtures :people
+ #
+ # def test_login
+ # # get the login page
+ # get "/login"
+ # assert_equal 200, status
+ #
+ # # post the login and follow through to the home page
+ # post "/login", params: { username: people(:jamis).username,
+ # password: people(:jamis).password }
+ # follow_redirect!
+ # assert_equal 200, status
+ # assert_equal "/home", path
+ # end
+ # end
+ #
+ # However, you can also have multiple session instances open per test, and
+ # even extend those instances with assertions and methods to create a very
+ # powerful testing DSL that is specific for your application. You can even
+ # reference any named routes you happen to have defined.
+ #
+ # require "test_helper"
+ #
+ # class AdvancedTest < ActionDispatch::IntegrationTest
+ # fixtures :people, :rooms
+ #
+ # def test_login_and_speak
+ # jamis, david = login(:jamis), login(:david)
+ # room = rooms(:office)
+ #
+ # jamis.enter(room)
+ # jamis.speak(room, "anybody home?")
+ #
+ # david.enter(room)
+ # david.speak(room, "hello!")
+ # end
+ #
+ # private
+ #
+ # module CustomAssertions
+ # def enter(room)
+ # # reference a named route, for maximum internal consistency!
+ # get(room_url(id: room.id))
+ # assert(...)
+ # ...
+ # end
+ #
+ # def speak(room, message)
+ # post "/say/#{room.id}", xhr: true, params: { message: message }
+ # assert(...)
+ # ...
+ # end
+ # end
+ #
+ # def login(who)
+ # open_session do |sess|
+ # sess.extend(CustomAssertions)
+ # who = people(who)
+ # sess.post "/login", params: { username: who.username,
+ # password: who.password }
+ # assert(...)
+ # end
+ # end
+ # end
+ #
+ # Another longer example would be:
+ #
+ # A simple integration test that exercises multiple controllers:
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # login via https
+ # https!
+ # get "/login"
+ # assert_response :success
+ #
+ # post "/login", params: { username: users(:david).username, password: users(:david).password }
+ # follow_redirect!
+ # assert_equal '/welcome', path
+ # assert_equal 'Welcome david!', flash[:notice]
+ #
+ # https!(false)
+ # get "/articles/all"
+ # assert_response :success
+ # assert_select 'h1', 'Articles'
+ # end
+ # end
+ #
+ # As you can see the integration test involves multiple controllers and
+ # exercises the entire stack from database to dispatcher. In addition you can
+ # have multiple session instances open simultaneously in a test and extend
+ # those instances with assertion methods to create a very powerful testing
+ # DSL (domain-specific language) just for your application.
+ #
+ # Here's an example of multiple sessions and custom DSL in an integration test
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # User david logs in
+ # david = login(:david)
+ # # User guest logs in
+ # guest = login(:guest)
+ #
+ # # Both are now available in different sessions
+ # assert_equal 'Welcome david!', david.flash[:notice]
+ # assert_equal 'Welcome guest!', guest.flash[:notice]
+ #
+ # # User david can browse site
+ # david.browses_site
+ # # User guest can browse site as well
+ # guest.browses_site
+ #
+ # # Continue with other assertions
+ # end
+ #
+ # private
+ #
+ # module CustomDsl
+ # def browses_site
+ # get "/products/all"
+ # assert_response :success
+ # assert_select 'h1', 'Products'
+ # end
+ # end
+ #
+ # def login(user)
+ # open_session do |sess|
+ # sess.extend(CustomDsl)
+ # u = users(user)
+ # sess.https!
+ # sess.post "/login", params: { username: u.username, password: u.password }
+ # assert_equal '/welcome', sess.path
+ # sess.https!(false)
+ # end
+ # end
+ # end
+ #
+ # See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
+ # use +get+, etc.
+ #
+ # === Changing the request encoding
+ #
+ # You can also test your JSON API easily by setting what the request should
+ # be encoded as:
+ #
+ # require "test_helper"
+ #
+ # class ApiTest < ActionDispatch::IntegrationTest
+ # test "creates articles" do
+ # assert_difference -> { Article.count } do
+ # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
+ # end
+ #
+ # assert_response :success
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
+ # end
+ # end
+ #
+ # The +as+ option passes an "application/json" Accept header (thereby setting
+ # the request format to JSON unless overridden), sets the content type to
+ # "application/json" and encodes the parameters as JSON.
+ #
+ # Calling +parsed_body+ on the response parses the response body based on the
+ # last response MIME type.
+ #
+ # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
+ # types you've registered, you can add your own encoders with:
+ #
+ # ActionDispatch::IntegrationTest.register_encoder :wibble,
+ # param_encoder: -> params { params.to_wibble },
+ # response_parser: -> body { body }
+ #
+ # Where +param_encoder+ defines how the params should be encoded and
+ # +response_parser+ defines how the response body should be parsed through
+ # +parsed_body+.
+ #
+ # Consult the Rails Testing Guide for more.
+
+ class IntegrationTest < ActiveSupport::TestCase
+ include TestProcess::FixtureFile
+
+ module UrlOptions
+ extend ActiveSupport::Concern
+ def url_options
+ integration_session.url_options
+ end
+ end
+
+ module Behavior
+ extend ActiveSupport::Concern
+
+ include Integration::Runner
+ include ActionController::TemplateAssertions
+
+ included do
+ include ActionDispatch::Routing::UrlFor
+ include UrlOptions # don't let UrlFor override the url_options method
+ ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
+ @@app = nil
+ end
+
+ module ClassMethods
+ def app
+ if defined?(@@app) && @@app
+ @@app
+ else
+ ActionDispatch.test_app
+ end
+ end
+
+ def app=(app)
+ @@app = app
+ end
+
+ def register_encoder(*args)
+ RequestEncoder.register_encoder(*args)
+ end
+ end
+
+ def app
+ super || self.class.app
+ end
+
+ def document_root_element
+ html_document.root
+ end
+ end
+
+ include Behavior
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb
new file mode 100644
index 0000000000..01246b7a2e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ class RequestEncoder # :nodoc:
+ class IdentityEncoder
+ def content_type; end
+ def accept_header; end
+ def encode_params(params); params; end
+ def response_parser; -> body { body }; end
+ end
+
+ @encoders = { identity: IdentityEncoder.new }
+
+ attr_reader :response_parser
+
+ def initialize(mime_name, param_encoder, response_parser)
+ @mime = Mime[mime_name]
+
+ unless @mime
+ raise ArgumentError, "Can't register a request encoder for " \
+ "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
+ end
+
+ @response_parser = response_parser || -> body { body }
+ @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
+ end
+
+ def content_type
+ @mime.to_s
+ end
+
+ def accept_header
+ @mime.to_s
+ end
+
+ def encode_params(params)
+ @param_encoder.call(params)
+ end
+
+ def self.parser(content_type)
+ mime = Mime::Type.lookup(content_type)
+ encoder(mime ? mime.ref : nil).response_parser
+ end
+
+ def self.encoder(name)
+ @encoders[name] || @encoders[:identity]
+ end
+
+ def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
+ @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
+ end
+
+ register_encoder :json, response_parser: -> body { JSON.parse(body) }
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
new file mode 100644
index 0000000000..8ac50c730d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/middleware/flash"
+
+module ActionDispatch
+ module TestProcess
+ module FixtureFile
+ # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
+ #
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
+ #
+ # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
+ # This will not affect other platforms:
+ #
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
+ def fixture_file_upload(path, mime_type = nil, binary = false)
+ if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
+ !File.exist?(path)
+ path = File.join(self.class.fixture_path, path)
+ end
+ Rack::Test::UploadedFile.new(path, mime_type, binary)
+ end
+ end
+
+ include FixtureFile
+
+ def assigns(key = nil)
+ raise NoMethodError,
+ "assigns has been extracted to a gem. To continue using it,
+ add `gem 'rails-controller-testing'` to your Gemfile."
+ end
+
+ def session
+ @request.session
+ end
+
+ def flash
+ @request.flash
+ end
+
+ def cookies
+ @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
+ end
+
+ def redirect_to_url
+ @response.redirect_url
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
new file mode 100644
index 0000000000..6c5b7af50e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "rack/utils"
+
+module ActionDispatch
+ class TestRequest < Request
+ DEFAULT_ENV = Rack::MockRequest.env_for("/",
+ "HTTP_HOST" => "test.host",
+ "REMOTE_ADDR" => "0.0.0.0",
+ "HTTP_USER_AGENT" => "Rails Testing",
+ )
+
+ # Create a new test request with default +env+ values.
+ def self.create(env = {})
+ env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
+ env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
+ new(default_env.merge(env))
+ end
+
+ def self.default_env
+ DEFAULT_ENV
+ end
+ private_class_method :default_env
+
+ def request_method=(method)
+ super(method.to_s.upcase)
+ end
+
+ def host=(host)
+ set_header("HTTP_HOST", host)
+ end
+
+ def port=(number)
+ set_header("SERVER_PORT", number.to_i)
+ end
+
+ def request_uri=(uri)
+ set_header("REQUEST_URI", uri)
+ end
+
+ def path=(path)
+ set_header("PATH_INFO", path)
+ end
+
+ def action=(action_name)
+ path_parameters[:action] = action_name.to_s
+ end
+
+ def if_modified_since=(last_modified)
+ set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
+ end
+
+ def if_none_match=(etag)
+ set_header("HTTP_IF_NONE_MATCH", etag)
+ end
+
+ def remote_addr=(addr)
+ set_header("REMOTE_ADDR", addr)
+ end
+
+ def user_agent=(user_agent)
+ set_header("HTTP_USER_AGENT", user_agent)
+ end
+
+ def accept=(mime_types)
+ delete_header("action_dispatch.request.accepts")
+ set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
new file mode 100644
index 0000000000..1e6b21f235
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "action_dispatch/testing/request_encoder"
+
+module ActionDispatch
+ # Integration test methods such as ActionDispatch::Integration::Session#get
+ # and ActionDispatch::Integration::Session#post return objects of class
+ # TestResponse, which represent the HTTP response results of the requested
+ # controller actions.
+ #
+ # See Response for more information on controller response objects.
+ class TestResponse < Response
+ def self.from_response(response)
+ new response.status, response.headers, response.body
+ end
+
+ def initialize(*) # :nodoc:
+ super
+ @response_parser = RequestEncoder.parser(content_type)
+ end
+
+ # Was the response successful?
+ def success?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The success? predicate is deprecated and will be removed in Rails 6.0.
+ Please use successful? as provided by Rack::Response::Helpers.
+ MSG
+ successful?
+ end
+
+ # Was the URL not found?
+ def missing?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The missing? predicate is deprecated and will be removed in Rails 6.0.
+ Please use not_found? as provided by Rack::Response::Helpers.
+ MSG
+ not_found?
+ end
+
+ # Was there a server-side error?
+ def error?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The error? predicate is deprecated and will be removed in Rails 6.0.
+ Please use server_error? as provided by Rack::Response::Helpers.
+ MSG
+ server_error?
+ end
+
+ def parsed_body
+ @parsed_body ||= @response_parser.call(body)
+ end
+ end
+end