Browse files

Initial import.

  • Loading branch information...
0 parents commit 436c5c5974d3091f57e6d5f88381392bf224cdd3 @FooBarWidget committed Sep 3, 2008
279 lib/auto_redirection.rb
@@ -0,0 +1,279 @@
+# Copyright (c) 2008 Phusion
+# http://www.phusion.nl/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This library allows one to easily implement so-called "auto-redirections".
+#
+# Consider the following use cases:
+# 1. A person clicks on the 'Login' link from an arbitrary page. After logging in,
+# he is redirected back to the page where he originally clicked on 'Login'.
+# 2. A person posts a comment, but posting comments requires him to be logged in.
+# So he is redirected to the login page, and after a successful login, the
+# comment that he wanted to post before is now automatically posted. He is also
+# redirected back to the page where the form was.
+#
+# In all of these use cases, the visitor is automatically redirected back to a
+# certain place on the website, hence the name "auto-redirections".
+#
+# Use case 2 is especially interesting. The comment creation action is typically
+# a POST-only action, so the auto-redirecting with POST instead of GET must also
+# be possible.
+#
+# To implement these use cases, one must pass some information to the next
+# controller, so that it knows where to redirect the user to. auto_redirection
+# makes it easy to do this.
+#
+#
+# == Basic usage
+#
+# Let's consider use case 1. Suppose that you have:
+#
+# - a +BooksController+, and the visitor is only allowed to view a book if he's
+# logged in.
+# - a +LoginController+, which handles logins.
+#
+# When an anonymous visitor visits '/books/1', we want BooksController#show to
+# redirect him to the login page. After successfull login, we want
+# LoginController to redirect him back to '/books/1'.
+#
+# What we must do is to somehow tell LoginController that the visitor came from
+# '/books/1'. This example shows you how. Let's consider BooksController:
+#
+# class BooksController < ApplicationController
+# def show
+# if logged_in?
+# @book = Book.find(params[:id])
+# render(:action => 'show')
+# else
+# # User must be logged in to view this book.
+# redirect_to('/login/login_form')
+# end
+# end
+# end
+#
+# Let's also consider LoginController and how it should be modified. When a
+# login is successful, instead of calling +redirect_to+ on a hardcoded location,
+# call +auto_redirect+:
+#
+# class LoginController < ApplicationController
+# def process_login
+# if User.authenticate(params[:username], params[:password])
+# # Login successful! Redirect user back to where he came from.
+# flash[:message] = "You are now logged in."
+#
+# # redirect_to('/books') # <--- commented out!
+# auto_redirect # <--- replaced with this!
+# else
+# flash[:message] = "Wrong username or password!"
+# render(:action => 'login_form')
+# end
+# end
+# end
+#
+# +auto_redirect+ will take care of redirecting the browser back to where it was,
+# before the login page was accessed. But how does it know where to redirect to?
+# The answer: almost every browser sends the "Referer" HTTP header, which tells the
+# web server where the browser was. +auto_redirect+ makes use of that information.
+#
+# <b>Note:</b> Unlike <tt>redirect_to :back</tt>, +auto_redirect+ will redirect
+# the browser to +root_path+ if there's no redirection information.
+#
+# === Passing current redirection information via a form
+#
+# There is a problem however. Suppose that the user typed in the wrong password and
+# is redirected back to the login page once again. Now the browser will send the URL
+# of the login page as the referer! That's obviously undesirable: after login,
+# we want to redirect the browser back to where it was *before* the login page was
+# accessed.
+#
+# What we're supposed to do now is to tell LoginController what the original
+# Referer was, before the login page was accessed. To do this, we insert a
+# little piece of information into the login page's form:
+#
+# <% form_tag('/login/process_login') do %>
+# <%= pass_redirection_information %> <!-- Added! -->
+#
+# Username: <input type="text" name="username"><br>
+# Password: <input type="password" name="password"><br>
+# <input type="submit" value="Login!">
+# <% end %>
+#
+# The +pass_redirection_information+ view helper saves the initial referer into
+# a hidden field called '_redirection_information'. +auto_redirect+ will use that
+# information instead of the "Referer" header whenever possible.
+#
+# That's it, we're done. :) So in summary, one must:
+# - use +auto_redirect+, which redirect the user back to where he came from,
+# according to any redirection information that has been received.
+# - in the view, pass the current redirection information to the next controller
+# action by using the +pass_redirection_information+ view helper.
+#
+#
+# == Handling non-GET requests
+#
+# Use case 2 is a bit different. We can't rely on the "Referer" HTTP header, because
+# upon redirecting back, we want the original POST request parameters to be sent as
+# well. POST parameters are not included in the "Referer" HTTP header.
+#
+# Suppose that you've changed your LoginController and login view template, as
+# described in 'Basic Usage'. And suppose you also have a CommentsController,
+# which requires the user to be logged in before he can post a comment.
+#
+# What we must do now is to tell LoginController not only that the visitor came
+# from CommentsController#create, but also what its HTTP method and POST
+# parameters were. To do this, we must use the +save_redirection_information+
+# method, which saves this information into a flash entry called
+# "_redirection_information", which is another place that the auto_redirection
+# library looks at for retrieving redirection (in addition to the "Referer" HTTP
+# header and the "_redirection_information" HTTP parameter).
+#
+# class CommentsController < ApplicationController
+# def create
+# if logged_in?
+# comment = Comment.create!(params[:comment])
+# redirect_to(comment)
+# else
+# # Tell LoginController that we came from CommentsController#create,
+# # and what our request parameters were.
+# save_redirection_information
+# redirect_to('/login/login_form')
+# end
+# end
+# end
+#
+# Now that LoginController's +auto_redirect+ call knows the correct redirection
+# information, it will take care of the rest.
+#
+# === Nested redirects
+#
+# Suppose that there are two places on your website that have a comments form:
+# '/books/(id)' and '/reviews/(id)'. And the comments form currently looks like
+# this:
+#
+# <% form_for(@comment) do |f| %>
+# <%= f.text_area :contents %>
+# <%= submit_tag 'Post comment' %>
+# <% end %>
+#
+# When a visitor posts a comment via CommentsController#create, after a login
+# he will be redirected back to CommentsController#create. But we also want
+# CommentsController#create to redirect back to '/books/(id)' or '/reviews/(id)',
+# depending on where the visitor came from. In other words, we want to be able
+# to *nest* redirection information.
+#
+# Right now, CommentsController will always redirect to '/comments/id)' after
+# having created a comments. So we change it a little:
+#
+# class CommentsController < ApplicationController
+# def create
+# if logged_in?
+# comment = Comment.create!(params[:comment])
+# if !attempt_auto_redirect # <-- changed!
+# redirect_to(comment) # <-- changed!
+# end # <-- changed!
+# else
+# # Tell LoginController that we came from CommentsController#create,
+# # and what our request parameters were.
+# save_redirection_information
+# redirect_to('/login/login_form')
+# end
+# end
+# end
+#
+# Now, CommentsController will redirect using whatever auto-redirection information
+# it has received. If no auto-redirection information is given (i.e.
+# +attempt_auto_redirect+ returns false) then it returns the visitor to
+# '/comments/(id)'.
+#
+#
+# === Saving POST auto-redirection information without a session
+#
+# The flash is not available if sessions are disabled. In that case, you have to pass
+# auto-redirection information via a GET parameter, like this:
+#
+# redirect_to('/login/login_form', :auto_redirect_to => current_request)
+#
+# The +current_request+ method returns auto-redirection information for the
+# current request.
+#
+# == Security
+#
+# Auto-redirection information is encrypted, so it cannot be read or tampered with
+# by third parties. Be sure to set a custom encryption key instead of leaving
+# the key at the default value. For example, put this in your environment.rb:
+#
+# AutoRedirection.encryption_key = "my secret key"
+#
+# <b>Tip:</b> use 'rake secret' to generate a random key.
+module AutoRedirection
+ # The key to use for encrypting auto-redirection information.
+ mattr_accessor :encryption_key
+ @@encryption_key = "e1cd3bf04d0a24b2a9760d95221c3dee"
+
+ # Whether this library's view helper methods should output XHTML (instead
+ # of regular HTML). Default: true.
+ mattr_accessor :xhtml
+ @@xhtml = true
+
+ # Whether to enable debugging. Default: true.
+ mattr_accessor :debug
+ class << self
+ alias debug? debug
+ end
+ @@debug = true
+
+ # A view template for redirecting the browser back to a place, while
+ # sending a POST request.
+ TEMPLATE_FOR_POST_REDIRECTION = %q{
+ <% form_tag(@form_action, { :method => @redirection_information.method, :id => 'form' }) do %>
+ <div class="nested_redirection_information">
+ <%= hidden_field_tag('_redirection_information', @nested_redirection_information) if @nested_redirection_information %>
+ </div>
+ <noscript>
+ <input type="submit" value="Click here to continue." />
+ </noscript>
+ <div id="message" style="display: none">
+ <h2>Your request is being processed...</h2>
+ <input type="submit" value="Click here if you are not redirected within 5 seconds." />
+ </div>
+ <% end %>
+ <script type="text/javascript">
+ //<![CDATA[
+ document.getElementById('form').submit();
+ setTimeout(function() {
+ //# If server doesn't respond within 1 second, then
+ //# display a wait message.
+ document.getElementById('message').style.display = 'block';
+ }, 1000);
+ // ]]>
+ </script>
+ }
+end
+
+require 'auto_redirection/redirection_information'
+require 'auto_redirection/controller_extensions'
+require 'auto_redirection/view_helpers'
+require 'auto_redirection/marshal_extensions'
+require 'auto_redirection/encryption'
+
+ActionController::Base.send(:include, AutoRedirection::ControllerExtensions)
+ActionView::Base.send(:include, AutoRedirection::ViewHelpers)
+ActionController::UploadedFile.send(:include, AutoRedirection::MarshalExtensions)
+
153 lib/auto_redirection/controller_extensions.rb
@@ -0,0 +1,153 @@
+# Copyright (c) 2008 Phusion
+# http://www.phusion.nl/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+module AutoRedirection
+
+module ControllerExtensions
+protected
+ # Saves redirection information into the flash, so that the next
+ # controller action may use this information.
+ #
+ # +location+ may either be +:here+, or a String containing an URL.
+ def save_redirection_information(location = :here)
+ case location
+ when :here
+ parameters = params
+ current_redirection_info = get_redirection_information
+ if current_redirection_info
+ parameters = params.merge(:_redirection_information =>
+ current_redirection_info.marshal)
+ else
+ parameters = params
+ end
+
+ redirection_info_to_pass = ControllerRedirectionInformation.new(
+ controller_path, action_name, parameters, request.method)
+ flash[:_redirection_information] = redirection_info_to_pass.marshal(true, false)
+ logger.debug("Auto-Redirection: saving redirection information " <<
+ "for: #{controller_path}/#{action_name} (#{request.method})")
+ when String
+ info = UrlRedirectionInformation.new(location)
+ flash[:_redirection_information] = info.marshal(true, false)
+ logger.debug("Auto-Redirection: saving redirection information " <<
+ "for: #{location}")
+ else
+ raise ArgumentError, "Unknown location #{location.inspect}."
+ end
+ end
+
+ # Returns auto-redirection information for the current request.
+ def current_request
+ @_current_request ||= begin
+ info = {
+ 'controller' => controller_path,
+ 'action' => action_name,
+ 'method' => request.method,
+ 'params' => params
+ }
+ Encryption.encrypt(Marshal.dump(info))
+ end
+ end
+
+ # The current request may contain redirection information.
+ # If auto-redirection information is given, then this method will redirect
+ # the HTTP client to that location (by calling +redirect_to+) and return true.
+ # Otherwise, it will return false.
+ #
+ # Redirection information is obtained from the following sources, in
+ # the specified order:
+ # 1. The <tt>_redirection_information</tt> request parameter.
+ # 2. The <tt>_redirection_information</tt> flash entry.
+ # 3. The "Referer" HTTP header.
+ def attempt_auto_redirect
+ info = get_redirection_information
+ if info.nil?
+ return false
+ end
+
+ # The page where we're redirecting to might have redirection information
+ # as well. So we save that information to flash[:auto_redirect_to] to
+ # allow nested auto-redirections.
+ if info.method == :get
+ if info.is_a?(UrlRedirectionInformation)
+ logger.debug("Auto-Redirection: redirect to URL: #{info.url}")
+ redirect_to info.url
+ else
+ args = info.params.merge(
+ :controller => info.controller,
+ :action => info.action
+ )
+ logger.debug("Auto-Redirection: redirecting to: " <<
+ "#{info.controller}/#{info.action} (get), " <<
+ "parameters: #{info.params.inspect}")
+ redirect_to args
+ end
+ else
+ @redirection_information = info
+ @form_action = info.params.merge(
+ 'controller' => info.controller,
+ 'action' => info.action
+ )
+
+ # We want to put nested redirection information a hidden field
+ # so that the data can be posted in the POST body instead of
+ # the HTTP request query string.
+ @nested_redirection_information = @form_action['_redirection_information']
+ @form_action.delete('_redirection_information')
+
+ logger.debug("Auto-Redirection: redirecting to: " <<
+ "#{info.controller}/#{info.action} (#{info.method}), " <<
+ "parameters: #{info.params.inspect}")
+ render :inline => TEMPLATE_FOR_POST_REDIRECTION, :layout => false
+ end
+ return true
+ end
+
+ # Try to redirect the browser by calling +attempt_auto_redirect+. If that
+ # method returns false, then the browser will be redirected to +root_path+
+ # instead.
+ def auto_redirect
+ if !attempt_auto_redirect
+ redirect_to root_path
+ end
+ end
+
+private
+ # Retrieve the redirection information that has been passed to the current
+ # controller action. Returns nil if no redirection information has been passed.
+ def get_redirection_information
+ if !@_redirection_information_given
+ if params.has_key?(:_redirection_information)
+ info = RedirectionInformation.load(params[:_redirection_information])
+ elsif flash.has_key?(:_redirection_information)
+ info = RedirectionInformation.load(flash[:_redirection_information], true, false)
+ elsif request.headers["Referer"]
+ info = UrlRedirectionInformation.new(request.headers["Referer"])
+ else
+ info = nil
+ end
+ @_redirection_information_given = true
+ @_redirection_information = info
+ end
+ return @_redirection_information
+ end
+end
+
+end # module AutoRedirections
110 lib/auto_redirection/encryption.rb
@@ -0,0 +1,110 @@
+# Copyright (c) 2008 Phusion
+# http://www.phusion.nl/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'openssl'
+require 'digest/sha2'
+
+module AutoRedirection
+
+# Convenience module for encrypting data. Properties:
+# - AES-CBC will be used for encryption.
+# - A cryptographic hash will be inserted so that the decryption method
+# can check whether the data has been tampered with.
+class Encryption
+ SIGNATURE_SIZE = 512 / 8 # Size of a binary SHA-512 hash.
+
+ # Encrypts the given data, which may be an arbitrary string.
+ #
+ # If +ascii7+ is true, then the encrypted data will be returned, in a
+ # format that's ASCII-7 compliant and URL-friendly (i.e. doesn't
+ # need to be URL-escaped).
+ #
+ # Otherwise, the encrypted data in binary format will be returned.
+ def self.encrypt(data, ascii7 = true)
+ signature = Digest::SHA512.digest(data)
+ encrypted_data = aes(:encrypt, AutoRedirection.encryption_key, signature << data)
+ if ascii7
+ return encode_base64_url(encrypted_data)
+ else
+ return encrypted_data
+ end
+ end
+
+ # Decrypt the given data, which was encrypted by the +encrypt+ method.
+ #
+ # The +ascii7+ parameter specifies whether +encrypt+ was called with
+ # its +ascii7+ argument set to true.
+ #
+ # If +data+ is nil, then nil will be returned. Otherwise, it must
+ # be a String.
+ #
+ # Returns the decrypted data as a String, or nil if the data has been
+ # corrupted or tampered with.
+ def self.decrypt(data, ascii7 = true)
+ if data.nil?
+ return nil
+ end
+ if ascii7
+ data = decode_base64_url(data)
+ end
+ decrypted_data = aes(:decrypt, AutoRedirection.encryption_key, data)
+ if decrypted_data.size < SIGNATURE_SIZE
+ return nil
+ end
+ signature = decrypted_data.slice!(0, SIGNATURE_SIZE)
+ if Digest::SHA512.digest(decrypted_data) != signature
+ return nil
+ end
+ return decrypted_data
+ rescue OpenSSL::CipherError
+ return nil
+ end
+
+ # Encode the given data with "modified Base64 for URL". See
+ # http://tinyurl.com/5tcnra for details.
+ def self.encode_base64_url(data)
+ data = [data].pack("m")
+ data.gsub!('+', '-')
+ data.gsub!('/', '_')
+ data.gsub!(/(=*\n\Z|\n*)/, '')
+ return data
+ end
+
+ # Encode the given data, which is in "modified Base64 for URL" format.
+ # This method never raises an exception, but will return invalid data
+ # if +data+ is not in a valid format.
+ def self.decode_base64_url(data)
+ data = data.gsub('-', '+')
+ data.gsub!('_', '/')
+ padding_size = 4 - (data.size % 4)
+ data << ('=' * padding_size) << "\n"
+ return data.unpack("m*").first
+ end
+
+private
+ def self.aes(m, k, t)
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc').send(m)
+ cipher.key = Digest::SHA256.digest(k)
+ return cipher.update(t) << cipher.final
+ end
+end
+
+end # AutoRedirection
34 lib/auto_redirection/marshal_extensions.rb
@@ -0,0 +1,34 @@
+# Copyright (c) 2008 Phusion
+# http://www.phusion.nl/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+module AutoRedirection
+
+module MarshalExtensions
+ def marshal_dump
+ return nil
+ end
+
+ def marshal_load(data)
+ return nil
+ end
+end
+
+end # module AutoRedirection
84 lib/auto_redirection/redirection_information.rb
@@ -0,0 +1,84 @@
+module AutoRedirection
+
+class RedirectionInformation
+ def self.load(data, encrypted = true, ascii7 = true)
+ if encrypted
+ data = Encryption.decrypt(data, ascii7)
+ end
+ info = Marshal.load(data)
+ if info[:url]
+ return UrlRedirectionInformation.new(info[:url])
+ else
+ return ControllerRedirectionInformation.new(
+ info[:controller],
+ info[:action],
+ info[:params],
+ info[:method])
+ end
+ end
+
+ def marshal(encrypt = true, ascii7 = true)
+ data = yield
+ if encrypt
+ data = Encryption.encrypt(data, ascii7)
+ end
+ return data
+ end
+end
+
+class UrlRedirectionInformation < RedirectionInformation
+ attr_accessor :url
+
+ def initialize(url = nil)
+ @url = url
+ end
+
+ def method
+ return :get
+ end
+
+ def marshal(encrypt = true, ascii7 = true)
+ super do
+ Marshal.dump({
+ :method => :get,
+ :url => url
+ })
+ end
+ end
+
+ def ==(other)
+ return other.is_a?(UrlRedirectionInformation) && other.url == url
+ end
+end
+
+class ControllerRedirectionInformation < RedirectionInformation
+ attr_accessor :controller, :action, :params, :method
+
+ def initialize(controller_path, action_name, params = {}, method = :get)
+ @controller = controller_path
+ @action = action_name
+ @params = params || {}
+ @method = method || :get
+ end
+
+ def marshal(encrypt = true, ascii7 = true)
+ super do
+ Marshal.dump({
+ :method => @method,
+ :controller => @controller,
+ :action => @action,
+ :params => @params
+ })
+ end
+ end
+
+ def ==(other)
+ return other.is_a?(ControllerRedirectionInformation) &&
+ other.controller == controller &&
+ other.action == action &&
+ other.params == params &&
+ other.method == method
+ end
+end
+
+end # module AutoRedirection
79 lib/auto_redirection/view_helpers.rb
@@ -0,0 +1,79 @@
+# Copyright (c) 2008 Phusion
+# http://www.phusion.nl/
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+module AutoRedirection
+
+module ViewHelpers
+ # Returns a hidden field tag, which contains information about where the
+ # form action should redirect the browser to when it's done.
+ #
+ # The page to redirect to is retrieved from the redirection information
+ # that the current controller action has received. If there is no such
+ # information, then this method will return +nil+.
+ def pass_redirection_information
+ return render_redirection_information(get_redirection_information)
+ end
+
+ def auto_redirect_to(location, params = {})
+ # TODO: merge this with 'auto_redirect_to' in ControllerExtensions.
+ case location
+ when :here
+ info = {
+ 'controller' => controller.controller_path,
+ 'action' => controller.action_name,
+ 'method' => controller.request.method,
+ 'params' => controller.params.merge(params)
+ }
+ logger.debug("Auto-Redirection: saving redirection information " <<
+ "for: #{controller.controller_path}/#{controller.action_name}" <<
+ " (#{request.method}), parameters: #{info['params'].inspect}")
+ else
+ raise ArgumentError, "Unknown location '#{location}'."
+ end
+ return render_redirection_information(info)
+ end
+
+private
+ def get_redirection_information
+ return controller.send(:get_redirection_information)
+ end
+
+ def render_redirection_information(info)
+ if info
+ RAILS_DEFAULT_LOGGER.info(info.inspect)
+ value = h(info.marshal)
+ html = %Q{<input type="hidden" name="_redirection_information" value="#{value}"}
+ if AutoRedirection.xhtml
+ html << " /"
+ end
+ html << ">"
+ if AutoRedirection.debug?
+ # Value intentionally not escaped.
+ html << "\n<!-- #{info.inspect} -->"
+ end
+ return html
+ else
+ return nil
+ end
+ end
+end
+
+end # module AutoRedirection
57 test/abstract_unit.rb
@@ -0,0 +1,57 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
+require 'rubygems'
+require 'stringio'
+require 'test/unit'
+
+gem 'rails', '~> 2.1.0'
+require 'action_controller'
+require 'action_controller/cgi_ext'
+require 'action_controller/test_process'
+require 'action_controller/integration'
+require 'action_view/test_case'
+
+require 'auto_redirection'
+
+# Show backtraces for deprecated behavior for quicker cleanup.
+ActiveSupport::Deprecation.debug = true
+
+RAILS_DEFAULT_LOGGER = Logger.new(STDOUT)
+RAILS_DEFAULT_LOGGER.level = Logger::WARN
+ActionController::Base.session_store = :memory_store
+ActionController::Base.logger = RAILS_DEFAULT_LOGGER
+ActionController::Routing::Routes.draw do |map|
+ map.connect 'test/:action', :controller => 'test'
+ map.connect 'users/:action', :controller => 'users'
+
+ map.connect 'books/:action/:id', :controller => 'books'
+ map.connect 'comments/:action', :controller => 'comments'
+ map.connect 'login/:action', :controller => 'login'
+end
+
+class Test::Unit::TestCase
+ def self.test(name, &block)
+ @@test_counts ||= {}
+ @@test_counts[self.class.to_s] ||= 0
+ @@test_counts[self.class.to_s] += 1
+ test_name = sprintf("test %03d: %s", @@test_counts[self.class.to_s], name.squish).to_sym
+ defined = instance_method(test_name) rescue false
+ raise "#{test_name} is already defined in #{self}" if defined
+ define_method(test_name, &block)
+ end
+end
+
+module TestHelpers
+private
+ def form_action
+ return CGI.unescapeHTML(css_select("#form").first['action'])
+ end
+
+ def parse_form_action
+ uri = URI.parse(form_action)
+ return [uri.path, CGI.parse(uri.query || "")]
+ end
+
+ def input_value(css_rule = "input")
+ return CGI.unescapeHTML(css_select(css_rule).first['value'])
+ end
+end
279 test/controller_methods_test.rb
@@ -0,0 +1,279 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+require 'uri'
+
+class TestController < ActionController::Base
+ def action_attempt_auto_redirect
+ if !attempt_auto_redirect
+ render :text => 'false'
+ end
+ end
+
+ def action_save_redirection_information
+ save_redirection_information
+ render :nothing => true
+ end
+
+ def action_pass_redirection_information
+ render :inline => '<%= pass_redirection_information %>'
+ end
+end
+
+class UsersController < ActionController::Base
+end
+
+class SimpleRedirectionTest < ActionController::TestCase
+ include AutoRedirection
+ include TestHelpers
+ tests TestController
+
+ test "attempt_auto_redirect returns false if there's no redirection information" do
+ get :action_attempt_auto_redirect
+ assert_equal 'false', @response.body
+ end
+
+
+ ##### URL GET redirections #####
+
+ test "attempt_auto_redirect redirects to the URL given by the Referer HTTP header" do
+ @controller.request.headers["Referer"] = '/foo'
+ get :action_attempt_auto_redirect
+ assert_redirected_to '/foo'
+ end
+
+ test "attempt_auto_redirect redirects to the URL given by the
+ _redirection_information HTTP parameter" do
+ info = UrlRedirectionInformation.new('http://www.google.com')
+ get(:action_attempt_auto_redirect, :_redirection_information => info.marshal)
+ assert_redirected_to 'http://www.google.com'
+ end
+
+ test "attempt_auto_redirect redirects to the URL given by the
+ _redirection_information flash" do
+ info = UrlRedirectionInformation.new('http://www.google.com')
+ get(:action_attempt_auto_redirect, nil, nil,
+ :_redirection_information => info.marshal(true, false))
+ assert_redirected_to 'http://www.google.com'
+ end
+
+
+ ##### Non-URL (controller information) GET redirections #####
+
+ test "attempt_auto_redirect redirects to the non-URL location given by the
+ _redirection_information HTTP parameter" do
+ info = ControllerRedirectionInformation.new('users', 'show')
+ get(:action_attempt_auto_redirect, :_redirection_information => info.marshal)
+ assert_redirected_to '/users/show'
+ end
+
+ test "attempt_auto_redirect redirects to the non-URL location given by the
+ _redirection_information HTTP flash" do
+ info = ControllerRedirectionInformation.new('users', 'show')
+ get(:action_attempt_auto_redirect, nil, nil,
+ :_redirection_information => info.marshal(true, false))
+ assert_redirected_to '/users/show'
+ end
+
+
+ ##### Non-GET redirections #####
+
+ test "attempt_auto_redirect renders a page with a form when the redirection
+ information does not pertain a GET request" do
+ info = ControllerRedirectionInformation.new('users', 'create', nil, :post)
+ get(:action_attempt_auto_redirect, :_redirection_information => info.marshal)
+ assert_response :ok
+ assert_select 'form'
+ end
+
+ test "the form that attempt_auto_redirect renders contains the correct form action" do
+ info = ControllerRedirectionInformation.new('users', 'create',
+ { :hello => "world", :foo => "bar" }, :post)
+ get(:action_attempt_auto_redirect, :_redirection_information => info.marshal)
+ path, query = parse_form_action
+ assert_equal '/users/create', path
+ assert_equal({ "hello" => ["world"], "foo" => ["bar"] }, query)
+ end
+
+ test "action_save_redirection_information saves the current request details into the flash" do
+ post(:action_save_redirection_information, :hello => "world", :foo => ["bar", "baz"])
+ info = RedirectionInformation.load(@response.flash[:_redirection_information], true, false)
+ assert_kind_of ControllerRedirectionInformation, info
+ assert_equal :post, info.method
+ assert_equal({
+ 'controller' => @controller.controller_path,
+ 'action' => "action_save_redirection_information",
+ 'hello' => "world",
+ 'foo' => ["bar", "baz"]
+ }, info.params)
+ end
+
+
+ ##### View helpers #####
+
+ test "the pass_redirection_information view helper returns nil if there's no redirection information" do
+ get(:action_pass_redirection_information)
+ assert_equal [], css_select("input")
+ end
+
+ test "the pass_redirection_information view helper saves the passed redirection information" do
+ info = ControllerRedirectionInformation.new('users', 'create',
+ { :hello => "world", :foo => "bar" }, :post)
+ get(:action_pass_redirection_information, :_redirection_information => info.marshal)
+ info2 = RedirectionInformation.load(input_value)
+ assert_equal info, info2
+ end
+
+ test "the pass_redirection_information view helper saves the HTTP referer" do
+ @controller.request.headers["Referer"] = '/foo'
+ get(:action_pass_redirection_information)
+ info = RedirectionInformation.load(input_value)
+ assert_kind_of UrlRedirectionInformation, info
+ assert_equal '/foo', info.url
+ end
+end
+
+
+class ApplicationController < ActionController::Base
+private
+ def logged_in?
+ return session[:logged_in]
+ end
+end
+
+class BooksController < ApplicationController
+ def show
+ render :inline => %q{
+ <% form_tag('/comments/create', :method => 'post') do %>
+ <input type="text" name="summary">
+ <input type="submit" value="Submit">
+ <% end %>
+ }
+ end
+end
+
+class CommentsController < ApplicationController
+ def create
+ if logged_in?
+ if !attempt_auto_redirect
+ render :text => "Comment '#{params[:summary]}' created!"
+ end
+ else
+ save_redirection_information
+ redirect_to '/login/login_form'
+ end
+ end
+end
+
+class LoginController < ApplicationController
+ def login_form
+ render :inline => %q{
+ <% form_tag('/login/process_login') do %>
+ <div class="redirection_info">
+ <%= pass_redirection_information %>
+ </div>
+ <input type="password" name="password">
+ <input type="submit" value="Login">
+ <% end %>
+ }
+ end
+
+ def process_login
+ if params[:password] == "secret"
+ session[:logged_in] = true
+ auto_redirect
+ else
+ login_form
+ end
+ end
+end
+
+class NestedRedirectionTest < ActionController::IntegrationTest
+ include AutoRedirection
+ include TestHelpers
+
+ test "scenario with nested redirects" do
+ # Visitor is at a book page.
+ get "/books/show/1"
+ assert_response :ok
+ book_redirection_info = UrlRedirectionInformation.new('/books/show/1')
+
+ # He posts a comment.
+ post("/comments/create", { :summary => "hi" }, :HTTP_REFERER => "/books/show/1")
+ # And gets redirected to the login page.
+ assert_redirected_to '/login/login_form'
+
+ # He loads the login page.
+ get('/login/login_form', nil, :HTTP_REFERER => '/login/login_form')
+ # There should be redirection information in the form that tells the
+ # process_login action that he came from /comments/create.
+ assert_response :ok
+ value = css_select('.redirection_info input').first['value']
+ info = RedirectionInformation.load(value)
+ assert_kind_of ControllerRedirectionInformation, info
+ assert_equal 'comments', info.controller
+ assert_equal 'create', info.action
+ assert_equal :post, info.method
+ assert_equal({
+ "summary" => "hi",
+ "controller" => "comments",
+ "action" => "create",
+ # This happens to include information which tells
+ # /comments/create that the visitor came from
+ # /books/show/1
+ "_redirection_information" => book_redirection_info.marshal
+ }, info.params)
+
+ # He logs in but entered the wrong password.
+ post("/login/process_login", {
+ :password => 'wrong',
+ :_redirection_information => value
+ }, :HTTP_REFERER => "/login/login_form")
+ assert_response :ok
+ # There should still be redirection information in the form that
+ # tells the process_login action that he came from /comments/create.
+ assert_response :ok
+ value = input_value('.redirection_info input')
+ info = RedirectionInformation.load(value)
+ assert_kind_of ControllerRedirectionInformation, info
+ assert_equal 'comments', info.controller
+ assert_equal 'create', info.action
+ assert_equal :post, info.method
+ assert_equal({
+ "summary" => "hi",
+ "controller" => "comments",
+ "action" => "create",
+ # This happens to include information which tells
+ # /comments/create that the visitor came from
+ # /books/show/1
+ "_redirection_information" => book_redirection_info.marshal
+ }, info.params)
+
+ # He logs in, this time with the correct password.
+ post("/login/process_login", {
+ :password => 'secret',
+ :_redirection_information => value
+ }, :HTTP_REFERER => "/login/login_form")
+ # The process_login page will render a form for POSTing to
+ # /comments/create, with the correct parameters.
+ assert_response :ok
+ path, query = parse_form_action
+ assert_equal '/comments/create', path
+ assert_equal({ "summary" => ["hi"] }, query)
+
+ # A hidden field contains the original redirection information,
+ # which tells /comments/create that the visitor came from
+ # /books/show/1.
+ value = input_value(".nested_redirection_information input")
+ assert_equal book_redirection_info.marshal, value
+
+ # The form gets auto-submitted by JavaScript.
+ post("/comments/create", {
+ :summary => "hi",
+ :_redirection_information => value
+ },
+ :HTTP_REFERER => "/login/process_login")
+
+ # /comments/create creates the comment and redirects us back to
+ # /books/show/1.
+ assert_redirected_to '/books/show/1'
+ end
+end

0 comments on commit 436c5c5

Please sign in to comment.