aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing/redirection.rb
blob: a87dfa40378258f7c89492e7f17e8690f4e7a991 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# 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.
      # `/stories`, `/stories?foo=bar`, etc all redirect to `/posts`.
      #
      # 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.
      # `/stories`, `/stories?foo=bar`, redirect to `/posts` and `/posts?foo=bar` 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