Checkout of Instiki Trunk 1/21/2007.

This commit is contained in:
Jacques Distler 2007-01-22 07:43:50 -06:00
commit 69b62b6f33
1138 changed files with 139586 additions and 0 deletions

View file

@ -0,0 +1,320 @@
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
require File.dirname(__FILE__) + "/vendor/html-scanner/html/document"
module Test #:nodoc:
module Unit #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt>: Status code was 200
# * <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 code number as the type, like assert_response(501)
def assert_response(type, message = nil)
clean_backtrace do
if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
assert_block("") { true } # to count the assertion
elsif type.is_a?(Fixnum) && @response.response_code == type
assert_block("") { true } # to count the assertion
else
assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false }
end
end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial,
# such that assert_redirected_to(:controller => "weblog") will also match the redirection of
# redirect_to(:controller => "weblog", :action => "show") and so on.
def assert_redirected_to(options = {}, message=nil)
clean_backtrace do
assert_response(:redirect, message)
if options.is_a?(String)
msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
[u, (p[0..0] == '/') ? p : '/' + p]
end.flatten
assert_equal(eurl, url, msg) if eurl && url
assert_equal(epath, path, msg) if epath && path
else
@response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)#{', difference: <?>' if @response_diff}",
@response.redirected_to || @response.redirect_url, @response_diff)
assert_block(msg) do
if options.is_a?(Symbol)
@response.redirected_to == options
else
options.keys.all? do |k|
if k == :controller then options[k] == ActionController::Routing.controller_relative_to(@response.redirected_to[k], @controller.class.controller_path)
else options[k] == (@response.redirected_to[k].respond_to?(:to_param) ? @response.redirected_to[k].to_param : @response.redirected_to[k] unless @response.redirected_to[k].nil?)
end
end
end
end
end
end
end
# Asserts that the request was rendered with the appropriate template file.
def assert_template(expected = nil, message=nil)
clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
!@response.rendered_with_file?
else
expected == rendered
end
end
end
end
# Asserts that the routing of the given path was handled correctly and that the parsed options match.
def assert_recognizes(expected_options, path, extras={}, message=nil)
clean_backtrace do
path = "/#{path}" unless path[0..0] == '/'
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request.path = path
ActionController::Routing::Routes.recognize!(request)
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
expected_options.stringify_keys!
msg = build_message(message, "The recognized options <?> did not match <?>",
request.path_parameters, expected_options)
assert_block(msg) { request.path_parameters == expected_options }
end
end
# Asserts that the provided options can be used to generate the provided path.
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
clean_backtrace do
expected_path = "/#{expected_path}" unless expected_path[0] == ?/
# Load routes.rb if it hasn't been loaded.
ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras)
found_extras = options.reject {|k, v| ! extra_keys.include? k}
msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
assert_block(msg) { found_extras == extras }
msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
expected_path)
assert_block(msg) { expected_path == generated_path }
end
end
# Asserts that path and options match both ways; in other words, the URL generated from
# options is the same as path, and also that the options recognized from path are the same as options
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
assert_generates(path, options, defaults, extras, message)
end
# Asserts that there is a tag/node/element in the body of the response
# that meets all of the given conditions. The +conditions+ parameter must
# be a hash of any of the following keys (all are optional):
#
# * <tt>:tag</tt>: the node type must match the corresponding value
# * <tt>:attributes</tt>: a hash. The node's attributes must match the
# corresponding values in the hash.
# * <tt>:parent</tt>: a hash. The node's parent must match the
# corresponding hash.
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts
# the keys:
# * <tt>:count</tt>: either a number or a range which must equal (or
# include) the number of children that match.
# * <tt>:less_than</tt>: the number of matching children must be less
# than this number.
# * <tt>:greater_than</tt>: the number of matching children must be
# greater than this number.
# * <tt>:only</tt>: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
# * <tt>:content</tt>: the textual content of the node must match the
# given value. This will not match HTML tags in the body of a
# tag--only text.
#
# Conditions are matched using the following algorithm:
#
# * if the condition is a string, it must be a substring of the value.
# * if the condition is a regexp, it must match the value.
# * if the condition is a number, the value must match number.to_s.
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# Usage:
#
# # assert that there is a "span" tag
# assert_tag :tag => "span"
#
# # assert that there is a "span" tag with id="x"
# assert_tag :tag => "span", :attributes => { :id => "x" }
#
# # assert that there is a "span" tag using the short-hand
# assert_tag :span
#
# # assert that there is a "span" tag with id="x" using the short-hand
# assert_tag :span, :attributes => { :id => "x" }
#
# # assert that there is a "span" inside of a "div"
# assert_tag :tag => "span", :parent => { :tag => "div" }
#
# # assert that there is a "span" somewhere inside a table
# assert_tag :tag => "span", :ancestor => { :tag => "table" }
#
# # assert that there is a "span" with at least one "em" child
# assert_tag :tag => "span", :child => { :tag => "em" }
#
# # assert that there is a "span" containing a (possibly nested)
# # "strong" tag.
# assert_tag :tag => "span", :descendant => { :tag => "strong" }
#
# # assert that there is a "span" containing between 2 and 4 "em" tags
# # as immediate children
# assert_tag :tag => "span",
# :children => { :count => 2..4, :only => { :tag => "em" } }
#
# # get funky: assert that there is a "div", with an "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and containing a
# # "span" descendant that contains text matching /hello world/
# assert_tag :tag => "div",
# :ancestor => { :tag => "ul" },
# :parent => { :tag => "li",
# :attributes => { :class => "enum" } },
# :descendant => { :tag => "span",
# :child => /hello world/ }
#
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
# (like br and hr and such) but will not work correctly with tags
# that allow optional closing tags (p, li, td). <em>You must explicitly
# close all of your tags to use these assertions.</em>
def assert_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
# Identical to #assert_tag, but asserts that a matching tag does _not_
# exist. (See #assert_tag for a full discussion of the syntax.)
def assert_no_tag(*opts)
clean_backtrace do
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
end
end
# test 2 html strings to be equivalent, i.e. identical up to reordering of attributes
def assert_dom_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom == actual_dom }
end
end
# negated form of +assert_dom_equivalent+
def assert_dom_not_equal(expected, actual, message="")
clean_backtrace do
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
assert_block(full_message) { expected_dom != actual_dom }
end
end
# ensures that the passed record is valid by active record standards. returns the error messages if not
def assert_valid(record)
clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n")
end
end
def clean_backtrace(&block)
yield
rescue AssertionFailedError => e
path = File.expand_path(__FILE__)
raise AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
require 'benchmark'
module ActionController #:nodoc:
# The benchmarking module times the performance of actions and reports to the logger. If the Active Record
# package has been included, a separate timing section for database calls will be added as well.
module Benchmarking #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
alias_method :perform_action_without_benchmark, :perform_action
alias_method :perform_action, :perform_action_with_benchmark
alias_method :render_without_benchmark, :render
alias_method :render, :render_with_benchmark
end
end
module ClassMethods
# Log and benchmark the workings of a single block and silence whatever logging that may have happened inside it
# (unless <tt>use_silence</tt> is set to false).
#
# The benchmark is only recorded if the current level of the logger matches the <tt>log_level</tt>, which makes it
# easy to include benchmarking statements in production software that will remain inexpensive because the benchmark
# will only be conducted if the log level is low enough.
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
if logger && logger.level == log_level
result = nil
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
result
else
yield
end
end
# Silences the logger for the duration of the block.
def silence
old_logger_level, logger.level = logger.level, Logger::ERROR if logger
yield
ensure
logger.level = old_logger_level if logger
end
end
def render_with_benchmark(options = nil, deprecated_status = nil, &block)
unless logger
render_without_benchmark(options, deprecated_status, &block)
else
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
render_output = nil
@rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, deprecated_status, &block) }.real
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@rendering_runtime -= @db_rt_after_render
end
render_output
end
end
def perform_action_with_benchmark
unless logger
perform_action_without_benchmark
else
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if @rendering_runtime
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
end
end
private
def rendering_runtime(runtime)
" | Rendering: #{sprintf("%.5f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime * 100) / runtime)}%)"
end
def active_record_runtime(runtime)
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render
db_percentage = (db_runtime * 100) / runtime
" | DB: #{sprintf("%.5f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
end
end
end

View file

@ -0,0 +1,555 @@
require 'fileutils'
module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
# around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
#
# You can read more about each approach and the sweeping assistance by clicking the modules below.
#
# Note: To turn off all caching and sweeping, set Base.perform_caching = false.
module Caching
def self.included(base) #:nodoc:
base.send(:include, Pages, Actions, Fragments, Sweeping)
base.class_eval do
@@perform_caching = true
cattr_accessor :perform_caching
end
end
# Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
# can serve without going through the Action Pack. This can be as much as 100 times faster than going through the process of dynamically
# generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors
# are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit
# for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates.
#
# Specifying which actions to cache is done through the <tt>caches</tt> class method:
#
# class WeblogController < ActionController::Base
# caches_page :show, :new
# end
#
# This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic
# generation. This is how the web server is able pick up a cache file when it exists and otherwise let the request pass on to
# the Action Pack to generate it.
#
# Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
# is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends:
#
# class WeblogController < ActionController::Base
# def update
# List.update(params[:list][:id], params[:list])
# expire_page :action => "show", :id => params[:list][:id]
# redirect_to :action => "show", :id => params[:list][:id]
# end
# end
#
# Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
# expired.
#
# == Setting the cache directory
#
# The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root".
# For Rails, this directory has already been set to RAILS_ROOT + "/public".
#
# == Setting the cache extension
#
# By default, the cache extension is .html, which makes it easy for the cached files to be picked up by the web server. If you want
# something else, like .php or .shtml, just set Base.page_cache_extension.
module Pages
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
@@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
cattr_accessor :page_cache_directory
@@page_cache_extension = '.html'
cattr_accessor :page_cache_extension
end
end
module ClassMethods
# Expires the page that was cached with the +path+ as a key. Example:
# expire_page "/lists/show"
def expire_page(path)
return unless perform_caching
benchmark "Expired page: #{page_cache_file(path)}" do
File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
end
end
# Manually cache the +content+ in the key determined by +path+. Example:
# cache_page "I'm the cached content", "/lists/show"
def cache_page(content, path)
return unless perform_caching
benchmark "Cached page: #{page_cache_file(path)}" do
FileUtils.makedirs(File.dirname(page_cache_path(path)))
File.open(page_cache_path(path), "wb+") { |f| f.write(content) }
end
end
# Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
# matches the triggering url.
def caches_page(*actions)
return unless perform_caching
actions.each do |action|
class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
end
end
private
def page_cache_file(path)
name = ((path.empty? || path == "/") ? "/index" : URI.unescape(path))
name << page_cache_extension unless (name.split('/').last || name).include? '.'
return name
end
def page_cache_path(path)
page_cache_directory + page_cache_file(path)
end
end
# Expires the page that was cached with the +options+ as a key. Example:
# expire_page :controller => "lists", :action => "show"
def expire_page(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action })))
end
else
self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
end
end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
# If no options are provided, the current +options+ for this action is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = {})
return unless perform_caching && caching_allowed
self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
end
private
def caching_allowed
!@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400
end
end
# Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching,
# every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which
# allows for authentication and other restrictions on whether someone is allowed to see the cache. Example:
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
# caches_page :public
# caches_action :show, :feed
# end
#
# In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the
# show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches.
#
# Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both
# the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
# "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
# "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
module Actions
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
base.send(:attr_accessor, :rendered_action_cache)
end
module ClassMethods #:nodoc:
def caches_action(*actions)
return unless perform_caching
around_filter(ActionCacheFilter.new(*actions))
end
end
def expire_action(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
end
else
expire_fragment(url_for(options).split("://").last)
end
end
class ActionCacheFilter #:nodoc:
def initialize(*actions)
@actions = actions
end
def before(controller)
return unless @actions.include?(controller.action_name.intern)
if cache = controller.read_fragment(controller.url_for.split("://").last)
controller.rendered_action_cache = true
controller.send(:render_text, cache)
false
end
end
def after(controller)
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
end
end
end
# Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
# certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
# parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
#
# <b>Hello <%= @name %></b>
# <% cache do %>
# All the topics in the system:
# <%= render_collection_of_partials "topic", Topic.find_all %>
# <% end %>
#
# This cache will bind to the name of action that called it. So you would be able to invalidate it using
# <tt>expire_fragment(:controller => "topics", :action => "list")</tt> -- if that was the controller/action used. This is not too helpful
# if you need to cache multiple fragments per action or if the action itself is cached using <tt>caches_action</tt>. So instead we should
# qualify the name of the action used with something like:
#
# <% cache(:action => "list", :action_suffix => "all_topics") do %>
#
# That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
# fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
# to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
# <tt>expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")</tt>.
#
# == Fragment stores
#
# In order to use the fragment caching, you need to designate where the caches should be stored. This is done by assigning a fragment store
# of which there are four different kinds:
#
# * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and shares the fragments for
# all the web server processes running off the same application directory.
# * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its
# own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take
# up a lot of memory since each process keeps all the caches in memory.
# * DRbStore: Keeps the fragments in the memory of a separate, shared DRb process. This works for all environments and only keeps one cache
# around for all processes, but requires that you run and manage a separate DRb process.
# * MemCacheStore: Works like DRbStore, but uses Danga's MemCache instead.
# Requires the ruby-memcache library: gem install ruby-memcache.
#
# Configuration examples (MemoryStore is the default):
#
# ActionController::Base.fragment_cache_store = :memory_store
# ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
# ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192"
# ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
# ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
module Fragments
def self.append_features(base) #:nodoc:
super
base.class_eval do
@@fragment_cache_store = MemoryStore.new
cattr_reader :fragment_cache_store
def self.fragment_cache_store=(store_option)
store, *parameters = *([ store_option ].flatten)
@@fragment_cache_store = if store.is_a?(Symbol)
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
store_class = ActionController::Caching::Fragments.const_get(store_class_name)
store_class.new(*parameters)
else
store
end
end
end
end
def fragment_cache_key(name)
name.is_a?(Hash) ? url_for(name).split("://").last : name
end
# Called by CacheHelper#cache
def cache_erb_fragment(block, name = {}, options = nil)
unless perform_caching then block.call; return end
buffer = eval("_erbout", block.binding)
if cache = read_fragment(name, options)
buffer.concat(cache)
else
pos = buffer.length
block.call
write_fragment(name, buffer[pos..-1], options)
end
end
def write_fragment(name, content, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
self.class.benchmark "Cached fragment: #{key}" do
fragment_cache_store.write(key, content, options)
end
content
end
def read_fragment(name, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
self.class.benchmark "Fragment read: #{key}" do
fragment_cache_store.read(key, options)
end
end
# Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes"
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
# * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes} Ensure you do not specify start and finish in the regex (^$) because the actual filename matched looks like ./cache/filename/path.cache
def expire_fragment(name, options = nil)
return unless perform_caching
key = fragment_cache_key(name)
if key.is_a?(Regexp)
self.class.benchmark "Expired fragments matching: #{key.source}" do
fragment_cache_store.delete_matched(key, options)
end
else
self.class.benchmark "Expired fragment: #{key}" do
fragment_cache_store.delete(key, options)
end
end
end
# Deprecated -- just call expire_fragment with a regular expression
def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
expire_fragment(matcher, options)
end
class UnthreadedMemoryStore #:nodoc:
def initialize #:nodoc:
@data = {}
end
def read(name, options=nil) #:nodoc:
@data[name]
end
def write(name, value, options=nil) #:nodoc:
@data[name] = value
end
def delete(name, options=nil) #:nodoc:
@data.delete(name)
end
def delete_matched(matcher, options=nil) #:nodoc:
@data.delete_if { |k,v| k =~ matcher }
end
end
module ThreadSafety #:nodoc:
def read(name, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def write(name, value, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def delete(name, options=nil) #:nodoc:
@mutex.synchronize { super }
end
def delete_matched(matcher, options=nil) #:nodoc:
@mutex.synchronize { super }
end
end
class MemoryStore < UnthreadedMemoryStore #:nodoc:
def initialize #:nodoc:
super
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
MemoryStore.send(:include, ThreadSafety)
end
end
end
class DRbStore < MemoryStore #:nodoc:
attr_reader :address
def initialize(address = 'druby://localhost:9192')
super()
@address = address
@data = DRbObject.new(nil, address)
end
end
class MemCacheStore < MemoryStore #:nodoc:
attr_reader :addresses
def initialize(*addresses)
super()
addresses = addresses.flatten
addresses = ["localhost"] if addresses.empty?
@addresses = addresses
@data = MemCache.new(*addresses)
end
end
class UnthreadedFileStore #:nodoc:
attr_reader :cache_path
def initialize(cache_path)
@cache_path = cache_path
end
def write(name, value, options = nil) #:nodoc:
ensure_cache_path(File.dirname(real_file_path(name)))
File.open(real_file_path(name), "wb+") { |f| f.write(value) }
rescue => e
Base.logger.error "Couldn't create cache directory: #{name} (#{e.message})" if Base.logger
end
def read(name, options = nil) #:nodoc:
File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil
end
def delete(name, options) #:nodoc:
File.delete(real_file_path(name))
rescue SystemCallError => e
# If there's no cache, then there's nothing to complain about
end
def delete_matched(matcher, options) #:nodoc:
search_dir(@cache_path) do |f|
if f =~ matcher
begin
File.delete(f)
rescue Object => e
# If there's no cache, then there's nothing to complain about
end
end
end
end
private
def real_file_path(name)
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
end
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exists?(path)
end
def search_dir(dir, &callback)
Dir.foreach(dir) do |d|
next if d == "." || d == ".."
name = File.join(dir, d)
if File.directory?(name)
search_dir(name, &callback)
else
callback.call name
end
end
end
end
class FileStore < UnthreadedFileStore #:nodoc:
def initialize(cache_path)
super(cache_path)
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
FileStore.send(:include, ThreadSafety)
end
end
end
end
# Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
# They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
#
# class ListSweeper < ActionController::Caching::Sweeper
# observe List, Item
#
# def after_save(record)
# list = record.is_a?(List) ? record : record.list
# expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
# expire_action(:controller => "lists", :action => "all")
# list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
# end
# end
#
# The sweeper is assigned in the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
#
# class ListsController < ApplicationController
# caches_action :index, :show, :public, :feed
# cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
# end
#
# In the example above, four actions are cached and three actions are responsible for expiring those caches.
module Sweeping
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
module ClassMethods #:nodoc:
def cache_sweeper(*sweepers)
return unless perform_caching
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
sweepers.each do |sweeper|
observer(sweeper)
sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
if sweeper_instance.is_a?(Sweeper)
around_filter(sweeper_instance, :only => configuration[:only])
else
after_filter(sweeper_instance, :only => configuration[:only])
end
end
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
# ActiveRecord::Observer will mark this class as reloadable even though it should not be.
# However, subclasses of ActionController::Caching::Sweeper should be Reloadable
include Reloadable::Subclasses
def before(controller)
self.controller = controller
callback(:before)
end
def after(controller)
callback(:after)
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
send(controller_callback_method_name) if respond_to?(controller_callback_method_name)
send(action_callback_method_name) if respond_to?(action_callback_method_name)
end
def method_missing(method, *arguments)
return if @controller.nil?
@controller.send(method, *arguments)
end
end
end
end
end

View file

@ -0,0 +1,43 @@
require 'cgi'
require 'cgi/session'
require 'cgi/session/pstore'
require 'action_controller/cgi_ext/cgi_methods'
# Wrapper around the CGIMethods that have been secluded to allow testing without
# an instantiated CGI object
class CGI #:nodoc:
class << self
alias :escapeHTML_fail_on_nil :escapeHTML
def escapeHTML(string)
escapeHTML_fail_on_nil(string) unless string.nil?
end
end
# Returns a parameter hash including values from both the request (POST/GET)
# and the query string with the latter taking precedence.
def parameters
request_parameters.update(query_parameters)
end
def query_parameters
CGIMethods.parse_query_parameters(query_string)
end
def request_parameters
CGIMethods.parse_request_parameters(params, env_table)
end
def redirect(where)
header({
"Status" => "302 Moved",
"location" => "#{where}"
})
end
def session(parameters = nil)
parameters = {} if parameters.nil?
parameters['database_manager'] = CGI::Session::PStore
CGI::Session.new(self, parameters)
end
end

View file

@ -0,0 +1,217 @@
require 'cgi'
require 'action_controller/vendor/xml_simple'
require 'action_controller/vendor/xml_node'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
public
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
def CGIMethods.parse_query_parameters(query_string)
parsed_params = {}
query_string.split(/[&;]/).each { |p|
# Ignore repeated delimiters.
next if p.empty?
k, v = p.split('=',2)
v = nil if (v && v.empty?)
k = CGI.unescape(k) if k
v = CGI.unescape(v) if v
unless k.include?(?[)
parsed_params[k] = v
else
keys = split_key(k)
last_key = keys.pop
last_key = keys.pop if (use_array = last_key.empty?)
parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
if use_array then (parent[last_key] ||= []) << v
else parent[last_key] = v
end
end
}
parsed_params
end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
# "Somewhere cool!" are translated into a full hash hierarchy, like
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
def CGIMethods.parse_request_parameters(params)
parsed_params = {}
for key, value in params
value = [value] if key =~ /.*\[\]$/
unless key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parsed_params[key] = get_typed_value(value[0])
else
build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
end
end
parsed_params
end
def self.parse_formatted_request_parameters(mime_type, raw_post_data)
params = case strategy = ActionController::Base.param_parsers[mime_type]
when Proc
strategy.call(raw_post_data)
when :xml_simple
raw_post_data.blank? ? nil :
typecast_xml_value(XmlSimple.xml_in(raw_post_data,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__'))
when :yaml
YAML.load(raw_post_data)
when :xml_node
node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node }
end
dasherize_keys(params || {})
rescue Object => e
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type }
end
def self.typecast_xml_value(value)
case value
when Hash
if value.has_key?("__content__")
content = translate_xml_entities(value["__content__"])
case value["type"]
when "integer" then content.to_i
when "boolean" then content == "true"
when "datetime" then Time.parse(content)
when "date" then Date.parse(content)
else content
end
else
value.empty? ? nil : value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v)
h
end
end
when Array
value.map! { |i| typecast_xml_value(i) }
case value.length
when 0 then nil
when 1 then value.first
else value
end
else
raise "can't typecast #{value.inspect}"
end
end
private
def self.translate_xml_entities(value)
value.gsub(/&lt;/, "<").
gsub(/&gt;/, ">").
gsub(/&quot;/, '"').
gsub(/&apos;/, "'").
gsub(/&amp;/, "&")
end
def self.dasherize_keys(params)
case params.class.to_s
when "Hash"
params.inject({}) do |h,(k,v)|
h[k.to_s.tr("-", "_")] = dasherize_keys(v)
h
end
when "Array"
params.map { |v| dasherize_keys(v) }
else
params
end
end
# Splits the given key into several pieces. Example keys are 'name', 'person[name]',
# 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
# 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
def CGIMethods.split_key(key)
if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
keys = [$1]
keys.concat($2[1..-2].split(']['))
keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
keys
else
[key]
end
end
def CGIMethods.get_typed_value(value)
# test most frequent case first
if value.is_a?(String)
value
elsif value.respond_to?(:content_type) && ! value.content_type.blank?
# Uploaded file
unless value.respond_to?(:full_original_filename)
class << value
alias_method :full_original_filename, :original_filename
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
md.captures.first
else
File.basename full_original_filename
end
end
end
end
# Return the same value after overriding original_filename.
value
elsif value.respond_to?(:read)
# Value as part of a multipart request
value.read
elsif value.class == Array
value.collect { |v| CGIMethods.get_typed_value(v) }
else
# other value (neither string nor a multipart request)
value.to_s
end
end
PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
def CGIMethods.get_levels(key)
all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
if main.nil?
[]
elsif trailing
[key]
elsif bracketed
[main] + bracketed.slice(1...-1).split('][')
else
[main]
end
end
def CGIMethods.build_deep_hash(value, hash, levels)
if levels.length == 0
value
elsif hash.nil?
{ levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
else
hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
end
end
end

View file

@ -0,0 +1,125 @@
CGI.module_eval { remove_const "Cookie" }
class CGI #:nodoc:
# This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
# It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
# http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
class Cookie < DelegateClass(Array)
# Create a new CGI::Cookie object.
#
# The contents of the cookie can be specified as a +name+ and one
# or more +value+ arguments. Alternatively, the contents can
# be specified as a single hash argument. The possible keywords of
# this hash are as follows:
#
# name:: the name of the cookie. Required.
# value:: the cookie's value or list of values.
# path:: the path for which this cookie applies. Defaults to the
# base directory of the CGI script.
# domain:: the domain for which this cookie applies.
# expires:: the time at which this cookie expires, as a +Time+ object.
# secure:: whether this cookie is a secure cookie or not (default to
# false). Secure cookies are only transmitted to HTTPS
# servers.
#
# These keywords correspond to attributes of the cookie object.
def initialize(name = '', *value)
if name.kind_of?(String)
@name = name
@value = Array(value)
@domain = nil
@expires = nil
@secure = false
@path = nil
else
@name = name['name']
@value = Array(name['value'])
@domain = name['domain']
@expires = name['expires']
@secure = name['secure'] || false
@path = name['path']
end
unless @name
raise ArgumentError, "`name' required"
end
# simple support for IE
unless @path
%r|^(.*/)|.match(ENV['SCRIPT_NAME'])
@path = ($1 or '')
end
super(@value)
end
def __setobj__(obj)
@_dc_obj = obj
end
attr_accessor("name", "value", "path", "domain", "expires")
attr_reader("secure")
# Set whether the Cookie is a secure cookie or not.
#
# +val+ must be a boolean.
def secure=(val)
@secure = val if val == true or val == false
@secure
end
# Convert the Cookie to its string representation.
def to_s
buf = ""
buf << @name << '='
if @value.kind_of?(String)
buf << CGI::escape(@value)
else
buf << @value.collect{|v| CGI::escape(v) }.join("&")
end
if @domain
buf << '; domain=' << @domain
end
if @path
buf << '; path=' << @path
end
if @expires
buf << '; expires=' << CGI::rfc1123_date(@expires)
end
if @secure == true
buf << '; secure'
end
buf
end
# Parse a raw cookie string into a hash of cookie-name=>Cookie
# pairs.
#
# cookies = CGI::Cookie::parse("raw_cookie_string")
# # { "name1" => cookie1, "name2" => cookie2, ... }
#
def self.parse(raw_cookie)
cookies = Hash.new([])
if raw_cookie
raw_cookie.split(/; ?/).each do |pairs|
name, values = pairs.split('=',2)
next unless name and values
name = CGI::unescape(name)
values = values.split('&').collect!{|v| CGI::unescape(v) }
unless cookies.has_key?(name)
cookies[name] = new(name, *values)
end
end
end
cookies
end
end # class Cookie
end

View file

@ -0,0 +1,74 @@
class CGI #:nodoc:
# Add @request.env['RAW_POST_DATA'] for the vegans.
module QueryExtension
# Initialize the data from the query.
#
# Handles multipart forms (in particular, forms that involve file uploads).
# Reads query parameters in the @params field, and cookies into @cookies.
def initialize_query()
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
#fix some strange request environments
if method = env_table['REQUEST_METHOD']
method = method.to_s.downcase.intern
else
method = :get
end
if method == :post && (boundary = multipart_form_boundary)
@multipart = true
@params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
else
@multipart = false
@params = CGI::parse(read_query_params(method) || "")
end
end
private
unless defined?(MULTIPART_FORM_BOUNDARY_RE)
MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
end
def multipart_form_boundary
MULTIPART_FORM_BOUNDARY_RE.match(env_table['CONTENT_TYPE']).to_a.pop
end
if defined? MOD_RUBY
def read_params_from_query
Apache::request.args || ''
end
else
def read_params_from_query
# fixes CGI querystring parsing for lighttpd
env_qs = env_table['QUERY_STRING']
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
uri.split('?', 2)[1] || ''
else
env_qs
end
end
end
def read_params_from_post
stdinput.binmode if stdinput.respond_to?(:binmode)
content = stdinput.read(Integer(env_table['CONTENT_LENGTH'])) || ''
# fix for Safari Ajax postings that always append \000
content.chop! if content[-1] == 0
content.gsub! /&_=$/, ''
env_table['RAW_POST_DATA'] = content.freeze
end
def read_query_params(method)
case method
when :get
read_params_from_query
when :post, :put
read_params_from_post
when :cmd
read_from_cmdline
else # when :head, :delete, :options
read_params_from_query
end
end
end # module QueryExtension
end

View file

@ -0,0 +1,207 @@
require 'action_controller/cgi_ext/cgi_ext'
require 'action_controller/cgi_ext/cookie_performance_fix'
require 'action_controller/cgi_ext/raw_post_data_fix'
module ActionController #:nodoc:
class Base
# Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
#
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session.
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
# * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
# of the request, or automatically generated for a new session.
# * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# an ArgumentError is raised.
# * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
# indefinitely.
# * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
# server.
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
# * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options)
end
def process_cgi(cgi, session_options = {}) #:nodoc:
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
end
end
class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi, :session_options
DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::PStore,
:prefix => "ruby_sess.",
:session_path => "/"
} unless const_defined?(:DEFAULT_SESSION_OPTIONS)
def initialize(cgi, session_options = {})
@cgi = cgi
@session_options = session_options
@env = @cgi.send(:env_table)
super()
end
def query_string
if (qs = @cgi.query_string) && !qs.empty?
qs
elsif uri = @env['REQUEST_URI']
parts = uri.split('?')
parts.shift
parts.join('?')
else
@env['QUERY_STRING'] || ''
end
end
def query_parameters
(qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs)
end
def request_parameters
@request_parameters ||=
if ActionController::Base.param_parsers.has_key?(content_type)
CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA'])
else
CGIMethods.parse_request_parameters(@cgi.params)
end
end
def cookies
@cgi.cookies.freeze
end
def host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
elsif http_host = env['HTTP_HOST']
http_host
elsif server_name = env['SERVER_NAME']
server_name
else
"#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
def host
host_with_port[/^[^:]+/]
end
def port
if host_with_port =~ /:(\d+)$/
$1.to_i
else
standard_port
end
end
def session
unless @session
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if session_options_with_string_keys['new_session'] == true
@session = new_session
else
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
end
@session['__valid_session']
end
end
end
@session
end
def reset_session
@session.delete if CGI::Session === @session
@session = new_session
end
def method_missing(method_id, *arguments)
@cgi.send(method_id, *arguments) rescue super
end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
end
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module (\w+)}
begin
Module.const_missing($1)
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<end_msg
Session contains objects whose class definition isn\'t available.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
end
retry
else
raise
end
end
def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).inject({}) { |options, (k,v)| options[k.to_s] = v; options }
end
end
class CgiResponse < AbstractResponse #:nodoc:
def initialize(cgi)
@cgi = cgi
super()
end
def out(output = $stdout)
convert_content_type!(@headers)
output.binmode if output.respond_to?(:binmode)
output.sync = false if output.respond_to?(:sync=)
begin
output.write(@cgi.header(@headers))
if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
return
elsif @body.respond_to?(:call)
@body.call(self, output)
else
output.write(@body)
end
output.flush if output.respond_to?(:flush)
rescue Errno::EPIPE => e
# lost connection to the FCGI process -- ignore the output, then
end
end
private
def convert_content_type!(headers)
if header = headers.delete("Content-Type")
headers["type"] = header
end
if header = headers.delete("Content-type")
headers["type"] = header
end
if header = headers.delete("content-type")
headers["type"] = header
end
end
end
end

View file

@ -0,0 +1,235 @@
module ActionController
module CodeGeneration #:nodoc:
class GenerationError < StandardError #:nodoc:
end
class Source #:nodoc:
attr_reader :lines, :indentation_level
IndentationString = ' '
def initialize
@lines, @indentation_level = [], 0
end
def line(line)
@lines << (IndentationString * @indentation_level + line)
end
alias :<< :line
def indent
@indentation_level += 1
yield
ensure
@indentation_level -= 1
end
def to_s() lines.join("\n") end
end
class CodeGenerator #:nodoc:
attr_accessor :source, :locals
def initialize(source = nil)
@locals = []
@source = source || Source.new
end
BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
Keywords = BeginKeywords + ResumeKeywords
def method_missing(keyword, *text)
if Keywords.include? keyword
if ResumeKeywords.include? keyword
raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
source.lines.pop # Remove the 'end'
end
line "#{keyword} #{text.join ' '}"
begin source.indent { yield(self.dup) }
ensure line 'end'
end
else
super(keyword, *text)
end
end
def line(*args) self.source.line(*args) end
alias :<< :line
def indent(*args, &block) source(*args, &block) end
def to_s() source.to_s end
def share_locals_with(other)
other.locals = self.locals = (other.locals | locals)
end
FieldsToDuplicate = [:locals]
def dup
copy = self.class.new(source)
self.class::FieldsToDuplicate.each do |sym|
value = self.send(sym)
value = value.dup unless value.nil? || value.is_a?(Numeric)
copy.send("#{sym}=", value)
end
return copy
end
end
class RecognitionGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@results, @constants = {}, {}
@depth = 0
@move_ahead = nil
@finish_statement = Proc.new {|hash_expr| hash_expr}
end
def if_next_matches(string, &block)
test = Routing.test_condition(next_segment(true), string)
self.if(test, &block)
end
def move_forward(places = 1)
dup = self.dup
dup.depth += 1
dup.move_ahead = places
yield dup
end
def next_segment(assign_inline = false, default = nil)
if locals.include?(segment_name)
code = segment_name
else
code = "#{segment_name} = #{path_name}[#{index_name}]"
if assign_inline
code = "(#{code})"
else
line(code)
code = segment_name
end
locals << segment_name
end
code = "(#{code} || #{default.inspect})" if default
return code.to_s
end
def segment_name() "segment#{depth}".to_sym end
def path_name() :path end
def index_name
move_ahead, @move_ahead = @move_ahead, nil
move_ahead ? "index += #{move_ahead}" : 'index'
end
def continue
dup = self.dup
dup.before << dup.current
dup.current = dup.after.shift
dup.go
end
def go
if current then current.write_recognition(self)
else self.finish
end
end
def result(key, expression, delay = false)
unless delay
line "#{key}_value = #{expression}"
expression = "#{key}_value"
end
results[key] = expression
end
def constant_result(key, object)
constants[key] = object
end
def finish(ensure_traversal_finished = true)
pairs = []
(results.keys + constants.keys).uniq.each do |key|
pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
end
hash_expr = "{#{pairs.join(', ')}}"
statement = finish_statement.call(hash_expr)
if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
else self << statement
end
end
end
class GenerationGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :segments]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@segments = []
end
def hash_name() 'hash' end
def local_name(key) "#{key}_value" end
def hash_value(key, assign = true, default = nil)
if locals.include?(local_name(key)) then code = local_name(key)
else
code = "hash[#{key.to_sym.inspect}]"
if assign
code = "(#{local_name(key)} = #{code})"
locals << local_name(key)
end
end
code = "(#{code} || (#{default.inspect}))" if default
return code
end
def expire_for_keys(*keys)
return if keys.empty?
conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
end
def add_segment(*segments)
d = dup
d.segments.concat segments
yield d
end
def go
if current then current.write_generation(self)
else self.finish
end
end
def continue
d = dup
d.before << d.current
d.current = d.after.shift
d.go
end
def finish
line %("/#{segments.join('/')}")
end
def check_conditions(conditions)
tests = []
generator = nil
conditions.each do |key, condition|
tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
generator = self.dup unless generator
end
return tests.join(' && ')
end
end
end
end

View file

@ -0,0 +1,186 @@
module ActionController #:nodoc:
# Components allow you to call other actions for their rendered response while executing another action. You can either delegate
# the entire response rendering or you can mix a partial response in with your other content.
#
# class WeblogController < ActionController::Base
# # Performs a method and then lets hello_world output its render
# def delegate_action
# do_other_stuff_before_hello_world
# render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
# end
# end
#
# class GreeterController < ActionController::Base
# def hello_world
# render :text => "#{params[:person]} says, Hello World!"
# end
# end
#
# The same can be done in a view to do a partial rendering:
#
# Let's see a greeting:
# <%= render_component :controller => "greeter", :action => "hello_world" %>
#
# It is also possible to specify the controller as a class constant, bypassing the inflector
# code to compute the controller class at runtime:
#
# <%= render_component :controller => GreeterController, :action => "hello_world" %>
#
# == When to use components
#
# Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
# conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
# reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
# across many applications at once.
#
# So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
module Components
def self.included(base) #:nodoc:
base.send :include, InstanceMethods
base.extend(ClassMethods)
base.helper do
def render_component(options)
@controller.send(:render_component_as_string, options)
end
end
# If this controller was instantiated to process a component request,
# +parent_controller+ points to the instantiator of this controller.
base.send :attr_accessor, :parent_controller
base.class_eval do
alias_method :process_cleanup_without_components, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_components
alias_method :set_session_options_without_components, :set_session_options
alias_method :set_session_options, :set_session_options_with_components
alias_method :flash_without_components, :flash
alias_method :flash, :flash_with_components
alias_method :component_request?, :parent_controller
end
end
module ClassMethods
# Track parent controller to identify component requests
def process_with_components(request, response, parent_controller = nil) #:nodoc:
controller = new
controller.parent_controller = parent_controller
controller.process(request, response)
end
# Set the template root to be one directory behind the root dir of the controller. Examples:
# /code/weblog/components/admin/users_controller.rb with Admin::UsersController
# will use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/users/
#
# /code/weblog/components/admin/parties/users_controller.rb with Admin::Parties::UsersController
# will also use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/parties/users/
def uses_component_template_root
path_of_calling_controller = File.dirname(caller[0].split(/:\d+:/).first)
path_of_controller_root = path_of_calling_controller.sub(/#{controller_path.split("/")[0..-2]}$/, "") # " (for ruby-mode)
self.template_root = path_of_controller_root
end
end
module InstanceMethods
# Extracts the action_name from the request parameters and performs that action.
def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
flash.discard if component_request?
process_without_components(request, response, method, *arguments)
end
protected
# Renders the component specified as the response for the current method
def render_component(options) #:doc:
component_logging(options) do
render_text(component_response(options, true).body, response.headers["Status"])
end
end
# Returns the component response as a string
def render_component_as_string(options) #:doc:
component_logging(options) do
response = component_response(options, false)
if redirected = response.redirected_to
render_component_as_string(redirected)
else
response.body
end
end
end
def flash_with_components(refresh = false) #:nodoc:
if @flash.nil? || refresh
@flash =
if @parent_controller
@parent_controller.flash
else
flash_without_components
end
end
@flash
end
private
def component_response(options, reuse_response)
klass = component_class(options)
request = request_for_component(klass.controller_name, options)
response = reuse_response ? @response : @response.dup
klass.process_with_components(request, response, self)
end
# determine the controller class for the component request
def component_class(options)
if controller = options[:controller]
controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
else
self.class
end
end
# Create a new request object based on the current request.
# The new request inherits the session from the current request,
# bypassing any session options set for the component controller's class
def request_for_component(controller_name, options)
request = @request.dup
request.session = @request.session
request.instance_variable_set(
:@parameters,
(options[:params] || {}).with_indifferent_access.update(
"controller" => controller_name, "action" => options[:action], "id" => options[:id]
)
)
request
end
def component_logging(options)
if logger
logger.info "Start rendering component (#{options.inspect}): "
result = yield
logger.info "\n\nEnd of component rendering"
result
else
yield
end
end
def set_session_options_with_components(request)
set_session_options_without_components(request) unless component_request?
end
def process_cleanup_with_components
process_cleanup_without_components unless component_request?
end
end
end
end

View file

@ -0,0 +1,77 @@
module ActionController #:nodoc:
# Cookies are read and written through ActionController#cookies. The cookies being read are what were received along with the request,
# the cookies being written are what will be sent out with the response. Cookies are read by value (so you won't get the cookie object
# itself back -- just the value it holds). Examples for writing:
#
# cookies[:user_name] = "david" # => Will set a simple session cookie
# cookies[:login] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour
#
# Examples for reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
#
# Example for deleting:
#
# cookies.delete :user_name
#
# All the option symbols for setting cookies are:
#
# * <tt>value</tt> - the cookie's value or list of values (as an array).
# * <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.
# * <tt>expires</tt> - the time at which this cookie expires, as a +Time+ object.
# * <tt>secure</tt> - whether this cookie is a secure cookie or not (default to false).
# Secure cookies are only transmitted to HTTPS servers.
module Cookies
protected
# Returns the cookie container, which operates as described above.
def cookies
CookieJar.new(self)
end
# Deprecated cookie writer method
def cookie(*options)
@response.headers["cookie"] << CGI::Cookie.new(*options)
end
end
class CookieJar < Hash #:nodoc:
def initialize(controller)
@controller, @cookies = controller, controller.instance_variable_get("@cookies")
super()
update(@cookies)
end
# Returns the value of the cookie by +name+ -- or nil if no such cookie exists. You set new cookies using either the cookie method
# or cookies[]= (for simple name/value cookies without options).
def [](name)
@cookies[name.to_s].value.first if @cookies[name.to_s] && @cookies[name.to_s].respond_to?(:value)
end
def []=(name, options)
if options.is_a?(Hash)
options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
options["name"] = name.to_s
else
options = { "name" => name.to_s, "value" => options }
end
set_cookie(options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past
def delete(name)
set_cookie("name" => name.to_s, "value" => "", "expires" => Time.at(0))
end
private
def set_cookie(options) #:doc:
options["path"] = "/" unless options["path"]
cookie = CGI::Cookie.new(options)
@controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
@controller.response.headers["cookie"] << cookie
end
end
end

View file

@ -0,0 +1,83 @@
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Dependencies control what classes are needed for the controller to run its course. This is an alternative to doing explicit
# +require+ statements that bring a number of benefits. It's more succinct, communicates what type of dependency we're talking about,
# can trigger special behavior (as in the case of +observer+), and enables Rails to be clever about reloading in cached environments
# like FCGI. Example:
#
# class ApplicationController < ActionController::Base
# model :account, :company, :person, :project, :category
# helper :access_control
# service :notifications, :billings
# observer :project_change_observer
# end
#
# Please note that a controller like ApplicationController will automatically attempt to require_dependency on a model of its
# singuralized name and a helper of its name. If nothing is found, no error is raised. This is especially useful for concrete
# controllers like PostController:
#
# class PostController < ApplicationController
# # model :post (already required)
# # helper :post (already required)
# end
#
# Also note, that if the models follow the pattern of just 1 class per file in the form of MyClass => my_class.rb, then these
# classes don't have to be required as Active Support will auto-require them.
module ClassMethods #:nodoc:
# Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
# backend for modelling entity classes.
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
# Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
# Action Mailer service or a Payment Gateway service.
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
# Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
# automatically have .instance called on them to make them active on assignment.
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
# Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
# <tt>ApplicationController.dependencies_on(:model)</tt> would return <tt>[:account, :company, :person, :project, :category]</tt>
def dependencies_on(layer)
read_inheritable_attribute("#{layer}_dependencies")
end
def depend_on(layer, dependencies) #:nodoc:
write_inheritable_array("#{layer}_dependencies", dependencies)
end
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_dependency(dependency.to_s)
rescue LoadError => e
raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
rescue Object => exception
exception.blame_file! "=> #{layer} #{dependency}.rb"
raise
end
end
end
end
end
end

View file

@ -0,0 +1,204 @@
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
module Test #:nodoc:
module Unit #:nodoc:
module Assertions
def assert_success(message=nil) #:nodoc:
assert_response(:success, message)
end
def assert_redirect(message=nil) #:nodoc:
assert_response(:redirect, message)
end
def assert_rendered_file(expected=nil, message=nil) #:nodoc:
assert_template(expected, message)
end
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the session <?>", key, @response.session)
assert_block(msg) { @response.has_session_object?(key) }
end
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the session <?>", key, @response.session)
assert_block(msg) { !@response.has_session_object?(key) }
end
def assert_session_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, @response.session[key])
assert_block(msg) { expected == @response.session[key] }
end
# -- cookie assertions ---------------------------------------------------
def assert_no_cookie(key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
msg = build_message(message, "<?> not expected in cookies['?']", actual, key)
assert_block(msg) { actual.nil? or actual.empty? }
end
def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
actual = actual.first if actual
msg = build_message(message, "<?> expected in cookies['?'] but was <?>", expected, key, actual)
assert_block(msg) { expected == actual }
end
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the flash <?>", key, @response.flash)
assert_block(msg) { @response.has_flash_object?(key) }
end
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the flash <?>", key, @response.flash)
assert_block(msg) { !@response.has_flash_object?(key) }
end
# ensure the flash exists
def assert_flash_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash does not exist <?>", @response.session['flash'] )
assert_block(msg) { @response.has_flash? }
end
# ensure the flash does not exist
def assert_flash_not_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash exists <?>", @response.flash)
assert_block(msg) { !@response.has_flash? }
end
# ensure the flash is empty but existent
def assert_flash_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is not empty <?>", @response.flash)
assert_block(msg) { !@response.has_flash_with_contents? }
end
# ensure the flash is not empty
def assert_flash_not_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is empty")
assert_block(msg) { @response.has_flash_with_contents? }
end
def assert_flash_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, @response.flash[key])
assert_block(msg) { expected == @response.flash[key] }
end
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> is not the redirected location <?>", url, @response.redirect_url)
assert_block(msg) { @response.redirect_url == url }
end
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> was not found in the location: <?>", pattern, @response.redirect_url)
assert_block(msg) { @response.redirect_url_match?(pattern) }
end
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not a template object", key )
assert_block(msg) { @response.has_template_object?(key) }
end
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil) #:nodoc:
msg = build_message(message, "<?> is a template object <?>", key, @response.template_objects[key])
assert_block(msg) { !@response.has_template_object?(key) }
end
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, @response.template.assigns[key.to_s])
assert_block(msg) { expected == @response.template.assigns[key.to_s] }
end
alias_method :assert_assigned_equal, :assert_template_equal
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil) #:nodoc:
xml, matches = REXML::Document.new(@response.body), []
xml.elements.each(expression) { |e| matches << e.text }
if matches.empty? then
msg = build_message(message, "<?> not found in document", expression)
flunk(msg)
return
elsif matches.length < 2 then
matches = matches.first
end
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
assert_block(msg) { matches == expected }
end
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
assert_template_has(key)
record = @response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
end

View file

@ -0,0 +1,17 @@
module ActionController
class Base
protected
# Deprecated in favor of calling redirect_to directly with the path.
def redirect_to_path(path) #:nodoc:
redirect_to(path)
end
# Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass
# true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through
# just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to.
def redirect_to_url(url, permanently = false) #:nodoc:
headers["Status"] = "301 Moved Permanently" if permanently
redirect_to(url)
end
end
end

View file

@ -0,0 +1,34 @@
module ActionController
class AbstractRequest
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def post_format
case content_type.to_s
when 'application/xml'
:xml
when 'application/x-yaml'
:yaml
else
:url_encoded
end
end
# Is this a POST request formatted as XML or YAML?
def formatted_post?
post? && (post_format == :yaml || post_format == :xml)
end
# Is this a POST request formatted as XML?
def xml_post?
post? && post_format == :xml
end
# Is this a POST request formatted as YAML?
def yaml_post?
post? && post_format == :yaml
end
end
end

View file

@ -0,0 +1,444 @@
module ActionController #:nodoc:
module Filters #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send(:include, ActionController::Filters::InstanceMethods)
end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed.
#
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt>
# to halt the processing before the intended action is processed by returning false or performing a redirect or render.
# This is especially useful for filters like authentication where you're not interested in allowing the action to be
# performed if the proper credentials are not in order.
#
# == Filter inheritance
#
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
# affecting the superclass. For example:
#
# class BankController < ActionController::Base
# before_filter :audit
#
# private
# def audit
# # record the action and parameters in an audit log
# end
# end
#
# class VaultController < BankController
# before_filter :verify_credentials
#
# private
# def verify_credentials
# # make sure the user is allowed into the vault
# end
# end
#
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# verify_credentials and the intended action are never called.
#
# == Filter types
#
# A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first
# is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of
# the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form.
#
# Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
# are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
#
# class OutputCompressionFilter
# def self.filter(controller)
# controller.response.body = compress(controller.response.body)
# end
# end
#
# class NewspaperController < ActionController::Base
# after_filter OutputCompressionFilter
# end
#
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
# manipulate them as it sees fit.
#
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# Or just as a quick test. It works like this:
#
# class WeblogController < ActionController::Base
# before_filter { |controller| false if controller.params["stop_action"] }
# end
#
# As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
# This means that the block has access to both the request and response objects complete with convenience methods for params,
# session, template, and assigns. Note: The inline method doesn't strictly have to be a block; any object that responds to call
# and returns 1 or -1 on arity will do (such as a Proc or an Method object).
#
# == Filter chain ordering
#
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
# just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
# can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the
# beginning of their respective chain and executed before the rest. For example:
#
# class ShoppingController
# before_filter :verify_open_shop
#
# class CheckoutController
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
#
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# is open or not.
#
# You may pass multiple filter arguments of each type as well as a filter block.
# If a block is given, it is treated as the last argument.
#
# == Around filters
#
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle
# both the before and after call. That's especially useful when you need to keep state active between the before and after,
# such as the example of a benchmark filter below:
#
# class WeblogController < ActionController::Base
# around_filter BenchmarkingFilter.new
#
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed
# def index
# end
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed
# end
#
# class BenchmarkingFilter
# def initialize
# @runtime
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# end
# end
#
# == Filter chain skipping
#
# Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
# subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
# they would like to be relieved of. Examples
#
# class ApplicationController < ActionController::Base
# before_filter :authenticate
# end
#
# class WeblogController < ApplicationController
# # will run the :authenticate filter
# end
#
# class SignupController < ApplicationController
# # will not run the :authenticate filter
# skip_before_filter :authenticate
# end
#
# == Filter conditions
#
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
# of which accept an arbitrary number of method references. For example:
#
# class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete
# before_filter :authorize, :only => [ :edit, :delete ]
#
# private
# def authorize
# # redirect to login unless authenticated
# end
# end
#
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
#
# class UserPreferences < ActionController::Base
# before_filter(:except => :new) { # some proc ... }
# # ...
# end
#
module ClassMethods
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
# on this controller are performed.
def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('before', filters)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
# on this controller are performed.
def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain('before', filters)
end
# Short-hand for append_before_filter since that's the most common of the two.
alias :before_filter :append_before_filter
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
# on this controller are performed.
def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('after', filters)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
# on this controller are performed.
def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters)
end
# Short-hand for append_after_filter since that's the most common of the two.
alias :after_filter :append_after_filter
# The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
#
# B#before
# A#before
# A#after
# B#after
def append_around_filter(*filters)
conditions = extract_conditions!(filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
append_before_filter(conditions || {}) { |c| filter.before(c) }
prepend_after_filter(conditions || {}) { |c| filter.after(c) }
end
end
# The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
#
# A#before
# B#before
# B#after
# A#after
def prepend_around_filter(*filters)
for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter)
prepend_before_filter { |c| filter.before(c) }
append_after_filter { |c| filter.after(c) }
end
end
# Short-hand for append_around_filter since that's the most common of the two.
alias :around_filter :append_around_filter
# Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters.
def skip_before_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ])
end
end
end
# Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy.
#
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters.
def skip_after_filter(*filters)
if conditions = extract_conditions!(filters)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ])
end
end
end
# Returns all the before filters for this class and all its ancestors.
def before_filters #:nodoc:
@before_filters ||= read_inheritable_attribute("before_filters") || []
end
# Returns all the after filters for this class and all its ancestors.
def after_filters #:nodoc:
@after_filters ||= read_inheritable_attribute("after_filters") || []
end
# Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc:
@included_actions ||= read_inheritable_attribute("included_actions") || {}
end
# Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc:
@excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
end
private
def append_filter_to_chain(condition, filters)
write_inheritable_array("#{condition}_filters", filters)
end
def prepend_filter_to_chain(condition, filters)
old_filters = read_inheritable_attribute("#{condition}_filters") || []
write_inheritable_attribute("#{condition}_filters", filters + old_filters)
end
def ensure_filter_responds_to_before_and_after(filter)
unless filter.respond_to?(:before) && filter.respond_to?(:after)
raise ActionControllerError, "Filter object must respond to both before and after"
end
end
def extract_conditions!(filters)
return nil unless filters.last.is_a? Hash
filters.pop
end
def add_action_conditions(filters, conditions)
return unless conditions
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included
write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded
end
def condition_hash(filters, *actions)
filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
end
def remove_contradicting_conditions!(filters, conditions)
return unless conditions[:only]
filters.each do |filter|
next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
[*conditions[:only]].each do |conditional_action|
conditional_action = conditional_action.to_s
included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action)
end
end
end
end
module InstanceMethods # :nodoc:
def self.included(base)
base.class_eval do
alias_method :perform_action_without_filters, :perform_action
alias_method :perform_action, :perform_action_with_filters
alias_method :process_without_filters, :process
alias_method :process, :process_with_filters
alias_method :process_cleanup_without_filters, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_filters
end
end
def perform_action_with_filters
before_action_result = before_action
unless before_action_result == false || performed?
perform_action_without_filters
after_action
end
@before_filter_chain_aborted = (before_action_result == false)
end
def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
@before_filter_chain_aborted = false
process_without_filters(request, response, method, *arguments)
end
# Calls all the defined before-filter filters, which are added by using "before_filter :method".
# If any of the filters return false, no more filters will be executed and the action is aborted.
def before_action #:doc:
call_filters(self.class.before_filters)
end
# Calls all the defined after-filter filters, which are added by using "after_filter :method".
# If any of the filters return false, no more filters will be executed.
def after_action #:doc:
call_filters(self.class.after_filters)
end
private
def call_filters(filters)
filters.each do |filter|
next if action_exempted?(filter)
filter_result = case
when filter.is_a?(Symbol)
self.send(filter)
when filter_block?(filter)
filter.call(self)
when filter_class?(filter)
filter.filter(self)
else
raise(
ActionControllerError,
'Filters need to be either a symbol, proc/method, or class implementing a static filter method'
)
end
if filter_result == false
logger.info "Filter chain halted as [#{filter}] returned false" if logger
return false
end
end
end
def filter_block?(filter)
filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1)
end
def filter_class?(filter)
filter.respond_to?('filter')
end
def action_exempted?(filter)
case
when ia = self.class.included_actions[filter]
!ia.include?(action_name)
when ea = self.class.excluded_actions[filter]
ea.include?(action_name)
end
end
def process_cleanup_with_filters
if @before_filter_chain_aborted
close_session
else
process_cleanup_without_filters
end
end
end
end
end

View file

@ -0,0 +1,178 @@
module ActionController #:nodoc:
# The flash provides a way to pass temporary objects 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] = "Successfully created"</tt> before redirecting to a display action that can then expose
# the flash to its template. Actually, that exposure is automatically done. Example:
#
# class WeblogController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Successfully created post"
# redirect_to :action => "display", :params => { :id => post.id }
# end
#
# def display
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# display.rhtml
# <% if @flash[:notice] %><div class="notice"><%= @flash[:notice] %></div><% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
# as you like at a time too. 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.
module Flash
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
alias_method :assign_shortcuts_without_flash, :assign_shortcuts
alias_method :assign_shortcuts, :assign_shortcuts_with_flash
alias_method :process_cleanup_without_flash, :process_cleanup
alias_method :process_cleanup, :process_cleanup_with_flash
end
end
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
end
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
@flash[k]
end
end
class FlashHash < Hash
def initialize #:nodoc:
super
@used = {}
end
def []=(k, v) #:nodoc:
keep(k)
super
end
def update(h) #:nodoc:
h.keys.each{ |k| discard(k) }
super
end
alias :merge! :update
def replace(h) #:nodoc:
@used = {}
super
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>.
def 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)
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action
#
# flash.keep # keep entire flash available for the next action
# flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
def discard(k=nil)
use(k)
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:
keys.each do |k|
unless @used[k]
use(k)
else
delete(k)
@used.delete(k)
end
end
(@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
end
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
# use('msg') # marks the "msg" entry as used
# use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
def use(k=nil, v=true)
unless k.nil?
@used[k] = v
else
keys.each{|key| use key, v }
end
end
end
module InstanceMethods #:nodoc:
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
end
def process_cleanup_with_flash
flash.sweep if @session
process_cleanup_without_flash
end
protected
# 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.
# Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc:
if @flash.nil? || refresh
@flash =
if @session.is_a?(Hash)
# @session is a Hash, if sessions are disabled
# we don't put the flash in the session in this case
FlashHash.new
else
# otherwise, @session is a CGI::Session or a TestSession
# so make sure it gets retrieved from/saved to session storage after request processing
@session["flash"] ||= FlashHash.new
end
end
@flash
end
# deprecated. use <tt>flash.keep</tt> instead
def keep_flash #:doc:
warn 'keep_flash is deprecated; use flash.keep instead.'
flash.keep
end
end
end
end

View file

@ -0,0 +1,134 @@
module ActionController #:nodoc:
module Helpers #:nodoc:
def self.append_features(base)
super
# Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module
base.master_helper_module = Module.new
# Extend base with class methods to declare helpers.
base.extend(ClassMethods)
base.class_eval do
# Wrap inherited to create a new master helper module for subclasses.
class << self
alias_method :inherited_without_helper, :inherited
alias_method :inherited, :inherited_with_helper
end
end
end
# The template helpers serve to relieve the templates from including the same inline code again and again. It's a
# set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
# Active Records (ActiveRecordHelper) that's available to all templates by default.
#
# It's also really easy to make your own helpers and it's much encouraged to keep the template files free
# from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
# (often the common helpers) as they're used by the specific application.
#
# module MyHelper
# def hello_world() "hello world" end
# end
#
# MyHelper can now be included in a controller, like this:
#
# class MyController < ActionController::Base
# helper :my_helper
# end
#
# ...and, same as above, used in any template rendered from MyController, like this:
#
# Let's hear what the helper has to say: <tt><%= hello_world %></tt>
module ClassMethods
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
# available to the templates.
def add_template_helper(helper_module) #:nodoc:
master_helper_module.send(:include, helper_module)
end
# Declare a helper:
# helper :foo
# requires 'foo_helper' and includes FooHelper in the template class.
# helper FooHelper
# includes FooHelper in the template class.
# helper { def foo() "#{bar} is the very best" end }
# evaluates the block in the template class, adding method #foo.
# helper(:three, BlindHelper) { def mice() 'mice' end }
# does all three.
def helper(*args, &block)
args.flatten.each do |arg|
case arg
when Module
add_template_helper(arg)
when String, Symbol
file_name = arg.to_s.underscore + '_helper'
class_name = file_name.camelize
begin
require_dependency(file_name)
rescue LoadError => load_error
requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
raise LoadError.new(msg).copy_blame!(load_error)
end
add_template_helper(class_name.constantize)
else
raise ArgumentError, 'helper expects String, Symbol, or Module argument'
end
end
# Evaluate block in template class if given.
master_helper_module.module_eval(&block) if block_given?
end
# Declare a controller method as a helper. For example,
# helper_method :link_to
# def link_to(name, options) ... end
# makes the link_to controller method available in the view.
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block)
controller.send(%(#{method}), *args, &block)
end
end_eval
end
end
# Declare a controller attribute as a helper. For example,
# helper_attr :name
# attr_accessor :name
# makes the name and name= controller methods available in the view.
# The is a convenience wrapper for helper_method.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
private
def default_helper_module!
module_name = name.sub(/Controller$|$/, 'Helper')
module_path = module_name.split('::').map { |m| m.underscore }.join('/')
require_dependency module_path
helper module_name.constantize
rescue LoadError
logger.debug("#{name}: missing default helper path #{module_path}") if logger
rescue NameError
logger.debug("#{name}: missing default helper module #{module_name}") if logger
end
def inherited_with_helper(child)
inherited_without_helper(child)
begin
child.master_helper_module = Module.new
child.master_helper_module.send :include, master_helper_module
child.send :default_helper_module!
rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
end
end
end
end
end

View file

@ -0,0 +1,528 @@
require 'dispatcher'
require 'stringio'
require 'uri'
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
# performed sequentially by some virtual user. Becase 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
include Test::Unit::Assertions
include ActionController::TestProcess
# The integer HTTP status code of the last request.
attr_reader :status
# The status message that accompanied the status code of the last request.
attr_reader :status_message
# The URI of the last request.
attr_reader :path
# The hostname used in the last request.
attr_accessor :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.
attr_reader :cookies
# A map of the headers returned by the last response.
attr_reader :headers
# 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
# Create an initialize a new Session instance.
def initialize
reset!
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!
@status = @path = @headers = nil
@result = @status_message = nil
@https = false
@cookies = {}
@controller = @request = @response = nil
self.host = "www.example.com"
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 @named_routes_configured
# install the named routes in this session instance.
klass = class<<self; self; end
Routing::NamedRoutes.install(klass)
# the helpers are made protected by default--we make them public for
# easier access during testing and troubleshooting.
klass.send(:public, *Routing::NamedRoutes::Helpers)
@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
# Return +true+ if the session is mimicing a secure HTTPS request.
#
# if session.https?
# ...
# end
def https?
@https
end
# Set the host name to use in the next request.
#
# session.host! "www.example.com"
def host!(name)
@host = name
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(interpret_uri(headers["location"].first))
status
end
# Performs a GET request, following any subsequent redirect. Note that
# the redirects are followed until the response is not a redirect--this
# means you may run into an infinite loop if your redirect loops back to
# itself.
def get_via_redirect(path, args={})
get path, args
follow_redirect! while redirect?
status
end
# Performs a POST request, following any subsequent redirect. This is
# vulnerable to infinite loops, the same as #get_via_redirect.
def post_via_redirect(path, args={})
post path, args
follow_redirect! while redirect?
status
end
# Returns +true+ if the last response was a redirect.
def redirect?
status/100 == 3
end
# Performs a GET request with the given parameters. The parameters may
# be +nil+, a Hash, or a string that is appropriately encoded
# (application/x-www-form-urlencoded or multipart/form-data). The headers
# should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed.
def get(path, parameters=nil, headers=nil)
process :get, path, parameters, headers
end
# Performs a POST request with the given parameters. The parameters may
# be +nil+, a Hash, or a string that is appropriately encoded
# (application/x-www-form-urlencoded or multipart/form-data). The headers
# should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed.
def post(path, parameters=nil, headers=nil)
process :post, path, parameters, headers
end
# Performs an XMLHttpRequest request with the given parameters, mimicing
# the request environment created by the Prototype library. The parameters
# may be +nil+, a Hash, or a string that is appropriately encoded
# (application/x-www-form-urlencoded or multipart/form-data). The headers
# should be a hash. The keys will automatically be upcased, with the
# prefix 'HTTP_' added if needed.
def xml_http_request(path, parameters=nil, headers=nil)
headers = (headers || {}).merge("X-Requested-With" => "XMLHttpRequest")
post(path, parameters, headers)
end
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
controller ? controller.url_for(options) : generic_url_rewriter.rewrite(options)
end
private
class MockCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table
def initialize(env, input=nil)
self.env_table = env
self.stdinput = StringIO.new(input || "")
self.stdoutput = StringIO.new
super()
end
end
# Tailors the session based on the given URI, setting the HTTPS value
# and the hostname.
def interpret_uri(path)
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
host! location.host if location.host
location.query ? "#{location.path}?#{location.query}" : location.path
end
# Performs the actual request.
def process(method, path, parameters=nil, headers=nil)
data = requestify(parameters)
path = interpret_uri(path) if path =~ %r{://}
path = "/#{path}" unless path[0] == ?/
@path = path
env = {}
if method == :get
env["QUERY_STRING"] = data
data = nil
end
env.update(
"REQUEST_METHOD" => method.to_s.upcase,
"REQUEST_URI" => path,
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"SERVER_PORT" => (https? ? "443" : "80"),
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"CONTENT_LENGTH" => data ? data.length.to_s : nil,
"HTTP_COOKIE" => encode_cookies,
"HTTPS" => https? ? "on" : "off",
"HTTP_ACCEPT" => accept
)
(headers || {}).each do |key, value|
key = key.to_s.upcase.gsub(/-/, "_")
key = "HTTP_#{key}" unless env.has_key?(key) || env =~ /^X|HTTP/
env[key] = value
end
unless ActionController::Base.respond_to?(:clear_last_instantiation!)
ActionController::Base.send(:include, ControllerCapture)
end
ActionController::Base.clear_last_instantiation!
cgi = MockCGI.new(env, data)
Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)
@result = cgi.stdoutput.string
@controller = ActionController::Base.last_instantiation
@request = @controller.request
@response = @controller.response
# Decorate the response with the standard behavior of the TestResponse
# so that things like assert_response can be used in integration
# tests.
@response.extend(TestResponseBehavior)
parse_result
return status
end
# Parses the result of the response and extracts the various values,
# like cookies, status, headers, etc.
def parse_result
headers, result_body = @result.split(/\r\n\r\n/, 2)
@headers = Hash.new { |h,k| h[k] = [] }
headers.each_line do |line|
key, value = line.strip.split(/:\s*/, 2)
@headers[key.downcase] << value
end
(@headers['set-cookie'] || [] ).each do |string|
name, value = string.match(/^(.*?)=(.*?);/)[1,2]
@cookies[name] = value
end
@status, @status_message = @headers["status"].first.split(/ /)
@status = @status.to_i
end
# Encode the cookies hash in a format suitable for passing to a
# request.
def encode_cookies
cookies.inject("") do |string, (name, value)|
string << "#{name}=#{value}; "
end
end
# Get a temporarly URL writer object
def generic_url_rewriter
cgi = MockCGI.new('REQUEST_METHOD' => "GET",
'QUERY_STRING' => "",
"REQUEST_URI" => "/",
"HTTP_HOST" => host,
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off")
ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {})
end
def name_with_prefix(prefix, name)
prefix ? "#{prefix}[#{name}]" : name.to_s
end
# Convert the given parameters to a request string. The parameters may
# be a string, +nil+, or a Hash.
def requestify(parameters, prefix=nil)
if Hash === parameters
return nil if parameters.empty?
parameters.map { |k,v| requestify(v, name_with_prefix(prefix, k)) }.join("&")
elsif Array === parameters
parameters.map { |v| requestify(v, name_with_prefix(prefix, "")) }.join("&")
elsif prefix.nil?
parameters
else
"#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}"
end
end
end
# A module used to extend ActionController::Base, so that integration tests
# can capture the controller used to satisfy a request.
module ControllerCapture #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class <<self
alias_method :new_without_capture, :new
alias_method :new, :new_with_capture
end
end
end
module ClassMethods #:nodoc:
mattr_accessor :last_instantiation
def clear_last_instantiation!
self.last_instantiation = nil
end
def new_with_capture(*args)
self.last_instantiation ||= new_without_capture(*args)
end
end
end
end
# An IntegrationTest is one that 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 IntegrationTest and write your tests
# using the get/post methods:
#
# require "#{File.dirname(__FILE__)}/test_helper"
#
# class ExampleTest < ActionController::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", :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 "#{File.dirname(__FILE__)}/test_helper"
#
# class AdvancedTest < ActionController::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)
# xml_http_request "/say/#{room.id}", :message => message
# assert(...)
# ...
# end
# end
#
# def login(who)
# open_session do |sess|
# sess.extend(CustomAssertions)
# who = people(who)
# sess.post "/login", :username => who.username,
# :password => who.password
# assert(...)
# end
# end
# end
class IntegrationTest < Test::Unit::TestCase
# Work around a bug in test/unit caused by the default test being named
# as a symbol (:default_test), which causes regex test filters
# (like "ruby test.rb -n /foo/") to fail because =~ doesn't work on
# symbols.
def initialize(name) #:nodoc:
super(name.to_s)
end
# Work around test/unit's requirement that every subclass of TestCase have
# at least one test method. Note that this implementation extends to all
# subclasses, as well, so subclasses of IntegrationTest may also exist
# without any test methods.
def run(*args) #:nodoc:
return if @method_name == "default_test"
super
end
# Because of how use_instantiated_fixtures and use_transactional_fixtures
# are defined, we need to treat them as special cases. Otherwise, users
# would potentially have to set their values for both Test::Unit::TestCase
# ActionController::IntegrationTest, since by the time the value is set on
# TestCase, IntegrationTest has already been defined and cannot inherit
# changes to those variables. So, we make those two attributes copy-on-write.
class << self
def use_transactional_fixtures=(flag) #:nodoc:
@_use_transactional_fixtures = true
@use_transactional_fixtures = flag
end
def use_instantiated_fixtures=(flag) #:nodoc:
@_use_instantiated_fixtures = true
@use_instantiated_fixtures = flag
end
def use_transactional_fixtures #:nodoc:
@_use_transactional_fixtures ?
@use_transactional_fixtures :
superclass.use_transactional_fixtures
end
def use_instantiated_fixtures #:nodoc:
@_use_instantiated_fixtures ?
@use_instantiated_fixtures :
superclass.use_instantiated_fixtures
end
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@integration_session = open_session
end
%w(get post cookies assigns xml_http_request).each do |method|
define_method(method) do |*args|
reset! unless @integration_session
returning @integration_session.send(method, *args) 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
session = Integration::Session.new
# delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result }
self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_")
next unless respond_to?(name)
extras.send(:define_method, name) { |*args| delegate.send(name, *args) }
end
# delegate add_assertion to the test case
extras.send(:define_method, :add_assertion) { test_result.add_assertion }
session.extend(extras)
session.delegate = self
session.test_result = @_result
yield session if block_given?
session
end
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
return unless @integration_session
%w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.send(var))
end
end
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
reset! unless @integration_session
returning @integration_session.send(sym, *args, &block) do
copy_session_variables!
end
end
end
end

View file

@ -0,0 +1,310 @@
module ActionController #:nodoc:
module Layout #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout
class << self
alias_method :inherited_without_layout, :inherited
alias_method :inherited, :inherited_with_layout
end
end
end
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
# <%= render "shared/header" %>
# Hello World
# <%= render "shared/footer" %>
#
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
#
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
# that the header and footer are only mentioned in one place, like this:
#
# <!-- The header part of this layout -->
# <%= yield %>
# <!-- The footer part of this layout -->
#
# And then you have content pages that look like this:
#
# hello world
#
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
# like this:
#
# <!-- The header part of this layout -->
# hello world
# <!-- The footer part of this layout -->
#
# == Accessing shared variables
#
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
# references that won't materialize before rendering time:
#
# <h1><%= @page_title %></h1>
# <%= yield %>
#
# ...and content pages that fulfill these references _at_ rendering time:
#
# <% @page_title = "Welcome" %>
# Off-world colonies offers you a chance to start a new life
#
# The result after rendering is:
#
# <h1>Welcome</h1>
# Off-world colonies offers you a chance to start a new life
#
# == Automatic layout assignment
#
# If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
# set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
# <tt>app/views/layouts/weblog.rhtml</tt> or <tt>app/views/layouts/weblog.rxml</tt> exists then it will be automatically set as
# the layout for your WeblogController. You can create a layout with the name <tt>application.rhtml</tt> or <tt>application.rxml</tt>
# and this will be set as the default controller if there is no layout with the same name as the current controller and there is
# no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
# assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.rhtml</tt>.
# Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
# Explicitly setting the layout in a parent class, though, will not override the child class's layout assignement if the child
# class has a layout with the same name.
#
# == Inheritance for layouts
#
# Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
#
# class BankController < ActionController::Base
# layout "bank_standard"
#
# class InformationController < BankController
#
# class VaultController < BankController
# layout :access_level_layout
#
# class EmployeeController < BankController
# layout nil
#
# The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
#
# == Types of layouts
#
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
#
# The method reference is the preferred approach to variable layouts and is used like this:
#
# class WeblogController < ActionController::Base
# layout :writers_and_readers
#
# def index
# # fetching posts
# end
#
# private
# def writers_and_readers
# logged_in? ? "writer_layout" : "reader_layout"
# end
#
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
# is logged in or not.
#
# If you want to use an inline method, such as a proc, do something like this:
#
# class WeblogController < ActionController::Base
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
#
# Of course, the most common way of specifying a layout is still just as a plain template name:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
#
# == Conditional layouts
#
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard", :except => :rss
#
# # ...
#
# end
#
# This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
# around the rendered view.
#
# Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
# #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
#
# == Using a different layout in the action render call
#
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
# Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
# This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
# qualified template and layout names as this example shows:
#
# class WeblogController < ActionController::Base
# def help
# render :action => "help/index", :layout => "help"
# end
# end
#
# As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
# as the third.
#
# NOTE: The old notation for rendering the view from a layout was to expose the magic <tt>@content_for_layout</tt> instance
# variable. The preferred notation now is to use <tt>yield</tt>, as documented above.
module ClassMethods
# If a layout is specified, all rendered actions will have their result rendered
# when the layout<tt>yield</tt>'s. This layout can itself depend on instance variables assigned during action
# performance and have access to them as any normal template would.
def layout(template_name, conditions = {})
add_layout_conditions(conditions)
write_inheritable_attribute "layout", template_name
end
def layout_conditions #:nodoc:
@layout_conditions ||= read_inheritable_attribute("layout_conditions")
end
def default_layout #:nodoc:
@default_layout ||= read_inheritable_attribute("layout")
end
private
def inherited_with_layout(child)
inherited_without_layout(child)
child.send :include, Reloadable
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
end
def layout_list
Dir.glob("#{template_root}/layouts/**/*")
end
def add_layout_conditions(conditions)
write_inheritable_hash "layout_conditions", normalize_conditions(conditions)
end
def normalize_conditions(conditions)
conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
end
def layout_directory_exists_cache
@@layout_directory_exists_cache ||= Hash.new do |h, dirname|
h[dirname] = File.directory? dirname
end
end
end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
def active_layout(passed_layout = nil)
layout = passed_layout || self.class.default_layout
active_layout = case layout
when String then layout
when Symbol then send(layout)
when Proc then layout.call(self)
end
# Explicitly passed layout names with slashes are looked up relative to the template root,
# but auto-discovered layouts derived from a nested controller will contain a slash, though be relative
# to the 'layouts' directory so we have to check the file system to infer which case the layout name came from.
if active_layout
if active_layout.include?('/') && ! layout_directory?(active_layout)
active_layout
else
"layouts/#{active_layout}"
end
end
end
def render_with_a_layout(options = nil, deprecated_status = nil, deprecated_layout = nil, &block) #:nodoc:
template_with_options = options.is_a?(Hash)
if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout))
options = options.merge :layout => false if template_with_options
logger.info("Rendering #{options} within #{layout}") if logger
if template_with_options
content_for_layout = render_with_no_layout(options, &block)
deprecated_status = options[:status] || deprecated_status
else
content_for_layout = render_with_no_layout(options, deprecated_status, &block)
end
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
render_text(@template.render_file(layout, true), deprecated_status)
else
render_with_no_layout(options, deprecated_status, &block)
end
end
private
def apply_layout?(template_with_options, options)
return false if options == :update
template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout?
end
def candidate_for_layout?(options)
(options.has_key?(:layout) && options[:layout] != false) ||
options.values_at(:text, :xml, :file, :inline, :partial, :nothing).compact.empty? &&
!template_exempt_from_layout?(default_template_name(options[:action] || options[:template]))
end
def pick_layout(template_with_options, options, deprecated_layout)
if deprecated_layout
deprecated_layout
elsif template_with_options
case layout = options[:layout]
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout?
else
active_layout(layout)
end
else
active_layout if action_has_layout?
end
end
def action_has_layout?
if conditions = self.class.layout_conditions
case
when only = conditions[:only]
only.include?(action_name)
when except = conditions[:except]
!except.include?(action_name)
else
true
end
else
true
end
end
# Does a layout directory for this class exist?
# we cache this info in a class level hash
def layout_directory?(layout_name)
template_path = File.join(self.class.view_root, 'layouts', layout_name)
dirname = File.dirname(template_path)
self.class.send(:layout_directory_exists_cache)[dirname]
end
end
end

View file

@ -0,0 +1,52 @@
module ActionController
# Macros are class-level calls that add pre-defined actions to the controller based on the parameters passed in.
# Currently, they're used to bridge the JavaScript macros, like autocompletion and in-place editing, with the controller
# backing.
module Macros
module AutoComplete #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# Example:
#
# # Controller
# class BlogController < ApplicationController
# auto_complete_for :post, :title
# end
#
# # View
# <%= text_field_with_auto_complete :post, title %>
#
# By default, auto_complete_for limits the results to 10 entries,
# and sorts by the given field.
#
# auto_complete_for takes a third parameter, an options hash to
# the find method used to search for the records:
#
# auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
#
# For help on defining text input fields with autocompletion,
# see ActionView::Helpers::JavaScriptHelper.
#
# For more examples, see script.aculo.us:
# * http://script.aculo.us/demos/ajax/autocompleter
# * http://script.aculo.us/demos/ajax/autocompleter_customized
module ClassMethods
def auto_complete_for(object, method, options = {})
define_method("auto_complete_for_#{object}_#{method}") do
find_options = {
:conditions => [ "LOWER(#{method}) LIKE ?", '%' + params[object][method].downcase + '%' ],
:order => "#{method} ASC",
:limit => 10 }.merge!(options)
@items = object.to_s.camelize.constantize.find(:all, find_options)
render :inline => "<%= auto_complete_result @items, '#{method}' %>"
end
end
end
end
end
end

View file

@ -0,0 +1,32 @@
module ActionController
module Macros
module InPlaceEditing #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# Example:
#
# # Controller
# class BlogController < ApplicationController
# in_place_edit_for :post, :title
# end
#
# # View
# <%= in_place_editor_field :post, 'title' %>
#
# For help on defining an in place editor in the browser,
# see ActionView::Helpers::JavaScriptHelper.
module ClassMethods
def in_place_edit_for(object, attribute, options = {})
define_method("set_#{object}_#{attribute}") do
@item = object.to_s.camelize.constantize.find(params[:id])
@item.update_attribute(attribute, params[:value])
render :text => @item.send(attribute)
end
end
end
end
end
end

View file

@ -0,0 +1,169 @@
module ActionController #:nodoc:
module MimeResponds #:nodoc:
def self.included(base)
base.send(:include, ActionController::MimeResponds::InstanceMethods)
end
module InstanceMethods
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
#
# def list
# @people = Person.find(:all)
# end
#
# Here's the same action, with web-service support baked in:
#
# def list
# @people = Person.find(:all)
#
# respond_to do |wants|
# wants.html
# wants.xml { render :xml => @people.to_xml }
# end
# end
#
# What that says is, "if the client wants HTML in response to this action, just respond as we
# would have before, but if the client wants XML, return them the list of people in XML format."
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
#
# Supposing you have an action that adds a new person, optionally creating their company
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def add
# @company = Company.find_or_create_by_name(params[:company][:name])
# @person = @company.people.create(params[:person])
#
# redirect_to(person_list_url)
# end
#
# Here's the same action, with web-service support baked in:
#
# def add
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |wants|
# wants.html { redirect_to(person_list_url) }
# wants.js
# wants.xml { render :xml => @person.to_xml(:include => @company) }
# end
# end
#
# If the client wants HTML, we just redirect them back to the person list. If they want Javascript
# (wants.js), then it is an RJS request and we render the RJS template associated with this action.
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
# include the persons company in the rendered XML, so you get something like this:
#
# <person>
# <id>...</id>
# ...
# <company>
# <id>...</id>
# <name>...</name>
# ...
# </company>
# </person>
#
# Note, however, the extra bit at the top of that action:
#
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by_name(company[:name])
#
# This is because the incoming XML document (if a web-service request is in process) can only contain a
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
#
# person[name]=...&person[company][name]=...&...
#
# And, like this (xml-encoded):
#
# <person>
# <name>...</name>
# <company>
# <name>...</name>
# </company>
# </person>
#
# In other words, we make the request so that it operates on a single entity—a person. Then, in the action,
# we extract the company data from the request, find or create the company, and then create the new person
# with the remaining data.
#
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
# in a single request (i.e., by wrapping them all in a single root note), but if you just go with the flow
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
# environment.rb as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block
block ||= lambda { |responder| types.each { |type| responder.send(type) } }
responder = Responder.new(block.binding)
block.call(responder)
responder.respond
end
end
class Responder #:nodoc:
DEFAULT_BLOCKS = {
:html => 'Proc.new { render }',
:js => 'Proc.new { render :action => "#{action_name}.rjs" }',
:xml => 'Proc.new { render :action => "#{action_name}.rxml" }'
}
def initialize(block_binding)
@block_binding = block_binding
@mime_type_priority = eval("request.accepts", block_binding)
@order = []
@responses = {}
end
def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
@order << mime_type
if block_given?
@responses[mime_type] = block
else
@responses[mime_type] = eval(DEFAULT_BLOCKS[mime_type.to_sym], @block_binding)
end
end
for mime_type in %w( all html js xml rss atom yaml )
eval <<-EOT
def #{mime_type}(&block)
custom(Mime::#{mime_type.upcase}, &block)
end
EOT
end
def any(*args, &block)
args.each { |type| send(type, &block) }
end
def respond
for priority in @mime_type_priority
if priority == Mime::ALL
@responses[@order.first].call
return
else
if priority === @order
@responses[priority].call
return # mime type match found, be happy and return
end
end
end
if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
else
eval 'render(:nothing => true, :status => "406 Not Acceptable")', @block_binding
end
end
end
end
end

View file

@ -0,0 +1,142 @@
module Mime
class Type #:nodoc:
# A simple helper class used in parsing the accept header
class AcceptItem #:nodoc:
attr_accessor :order, :name, :q
def initialize(order, name, q=nil)
@order = order
@name = name.strip
q ||= 0.0 if @name == "*/*" # default "*/*" to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
def to_s
@name
end
def <=>(item)
result = item.q <=> q
result = order <=> item.order if result == 0
result
end
def ==(item)
name == (item.respond_to?(:name) ? item.name : item)
end
end
class << self
def lookup(string)
LOOKUP[string]
end
def parse(accept_header)
# keep track of creation order to keep the subsequent sort stable
index = 0
list = accept_header.split(/,/).
map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort!
# Take care of the broken text/xml entry by renaming or deleting it
text_xml = list.index("text/xml")
app_xml = list.index("application/xml")
if text_xml && app_xml
# set the q value to the max of the two
list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
# make sure app_xml is ahead of text_xml in the list
if app_xml > text_xml
list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
app_xml, text_xml = text_xml, app_xml
end
# delete text_xml from the list
list.delete_at(text_xml)
elsif text_xml
list[text_xml].name = "application/xml"
end
# Look for more specific xml-based types and sort them ahead of app/xml
if app_xml
idx = app_xml
app_xml_type = list[app_xml]
while(idx < list.length)
type = list[idx]
break if type.q < app_xml_type.q
if type.name =~ /\+xml$/
list[app_xml], list[idx] = list[idx], list[app_xml]
app_xml = idx
end
idx += 1
end
end
list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
list
end
end
def initialize(string, symbol = nil, synonyms = [])
@symbol, @synonyms = symbol, synonyms
@string = string
end
def to_s
@string
end
def to_str
to_s
end
def to_sym
@symbol || @string.to_sym
end
def ===(list)
if list.is_a?(Array)
(@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
else
super
end
end
def ==(mime_type)
(@synonyms + [ self ]).any? { |synonym| synonym.to_s == mime_type.to_s } if mime_type
end
end
ALL = Type.new "*/*", :all
HTML = Type.new "text/html", :html, %w( application/xhtml+xml )
JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript )
XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml )
RSS = Type.new "application/rss+xml", :rss
ATOM = Type.new "application/atom+xml", :atom
YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml )
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) }
LOOKUP["*/*"] = ALL
LOOKUP["text/html"] = HTML
LOOKUP["application/xhtml+xml"] = HTML
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/javascript"] = JS
LOOKUP["application/javascript"] = JS
LOOKUP["application/x-javascript"] = JS
LOOKUP["text/yaml"] = YAML
LOOKUP["application/x-yaml"] = YAML
LOOKUP["application/rss+xml"] = RSS
LOOKUP["application/atom+xml"] = ATOM
end

View file

@ -0,0 +1,401 @@
module ActionController
# === Action Pack pagination for Active Record collections
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt>
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined
# by the <tt>params[:page]</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
# and <tt>@people</tt> for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:parameter => 'page'
}
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by
# singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
#
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => options[:count])
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = @params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end

View file

@ -0,0 +1,257 @@
module ActionController
# Subclassing AbstractRequest makes these methods available to the request objects used in production and testing,
# CgiRequest and TestRequest
class AbstractRequest
cattr_accessor :relative_url_root
# Returns the hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
attr_reader :env
# Returns both GET and POST parameters in a single hash.
def parameters
@parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access
end
# Returns the HTTP request method as a lowercase symbol (:get, for example)
def method
@request_method ||= @env['REQUEST_METHOD'].downcase.to_sym
end
# Is this a GET request? Equivalent to request.method == :get
def get?
method == :get
end
# Is this a POST request? Equivalent to request.method == :post
def post?
method == :post
end
# Is this a PUT request? Equivalent to request.method == :put
def put?
method == :put
end
# Is this a DELETE request? Equivalent to request.method == :delete
def delete?
method == :delete
end
# Is this a HEAD request? Equivalent to request.method == :head
def head?
method == :head
end
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_type
@content_type ||=
begin
content_type = @env['CONTENT_TYPE'].to_s.downcase
if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml'
content_type = 'application/x-yaml'
when 'xml'
content_type = 'application/xml'
end
end
Mime::Type.lookup(content_type)
end
end
# Returns the accepted MIME type for the request
def accepts
@accepts ||=
if @env['HTTP_ACCEPT'].to_s.strip.empty?
[ content_type, Mime::ALL ]
else
Mime::Type.parse(@env['HTTP_ACCEPT'])
end
end
# Returns true if the request's "X-Requested-With" header contains
# "XMLHttpRequest". (The Prototype Javascript library sends this header with
# every Ajax request.)
def xml_http_request?
not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil?
end
alias xhr? :xml_http_request?
# Determine originating IP address. REMOTE_ADDR is the standard
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
# HTTP_X_FORWARDED_FOR are set by proxies so check for these before
# falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
# delimited list in the case of multiple chained proxies; the first is
# the originating IP.
def remote_ip
return @env['HTTP_CLIENT_IP'] if @env.include? 'HTTP_CLIENT_IP'
if @env.include? 'HTTP_X_FORWARDED_FOR' then
remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',').reject do |ip|
ip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
end
return remote_ips.first.strip unless remote_ips.empty?
end
@env['REMOTE_ADDR']
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 = 1)
return nil if !/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.match(host).nil? or host.nil?
host.split('.').last(1 + tld_length).join('.')
end
# Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org".
# You can specify a different <tt>tld_length</tt>, such as 2 to catch ["www"] instead of ["www", "rubyonrails"]
# in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1)
return [] unless host
parts = host.split('.')
parts[0..-(tld_length+2)]
end
# Receive the raw post data.
# This is useful for services such as REST, XMLRPC and SOAP
# which communicate over HTTP POST but don't use the traditional parameter format.
def raw_post
@env['RAW_POST_DATA']
end
# Returns the request URI correctly, taking into account the idiosyncracies
# of the various servers.
def request_uri
if uri = @env['REQUEST_URI']
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_uri.
else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs
end
uri
end
end
# Return 'https://' if this is an SSL request and 'http://' otherwise.
def protocol
ssl? ? 'https://' : 'http://'
end
# Is this an SSL request?
def ssl?
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
def path
path = (uri = request_uri) ? uri.split('?').first : ''
# Cut off the path to the installation directory if given
root = relative_url_root
path[0, root.length] = '' if root
path || ''
end
# Returns the path minus the web server relative installation directory.
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not
# Apache, this method returns an empty string.
def relative_url_root
@@relative_url_root ||= case
when @env["RAILS_RELATIVE_URL_ROOT"]
@env["RAILS_RELATIVE_URL_ROOT"]
when server_software == 'apache'
@env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
else
''
end
end
# Returns the port number of this request as an integer.
def port
@port_as_int ||= @env['SERVER_PORT'].to_i
end
# Returns the standard port number for this request's protocol
def standard_port
case protocol
when 'https://' then 443
else 80
end
end
# Returns a port suffix like ":8080" if the port number of this request
# is not the default HTTP port 80 or HTTPS port 443.
def port_string
(port == standard_port) ? '' : ":#{port}"
end
# Returns a host:port string for this request, such as example.com or
# example.com:8080.
def host_with_port
host + port_string
end
def path_parameters=(parameters) #:nodoc:
@path_parameters = parameters
@symbolized_path_parameters = @parameters = nil
end
# The same as <tt>path_parameters</tt> with explicitly symbolized keys
def symbolized_path_parameters
@symbolized_path_parameters ||= path_parameters.symbolize_keys
end
# Returns a hash with the parameters used to form the path of the request
#
# Example:
#
# {:action => 'my_action', :controller => 'my_controller'}
def path_parameters
@path_parameters ||= {}
end
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end
#--
# Must be implemented in the concrete request
#++
def query_parameters #:nodoc:
end
def request_parameters #:nodoc:
end
# Returns the host for this request, such as example.com.
def host
end
def cookies #:nodoc:
end
def session #:nodoc:
end
def session=(session) #:nodoc:
@session = session
end
def reset_session #:nodoc:
end
end
end

View file

@ -0,0 +1,139 @@
module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
# is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too
# could the decision on whether something is a public or a developer request.
#
# You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
module Rescue
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
base.class_eval do
alias_method :perform_action_without_rescue, :perform_action
alias_method :perform_action, :perform_action_with_rescue
end
end
module ClassMethods #:nodoc:
def process_with_exception(request, response, exception)
new.process(request, response, :rescue_action, exception)
end
end
protected
# Exception handler called when the performance of an action raises an exception.
def rescue_action(exception)
log_error(exception) if logger
erase_results if performed?
if consider_all_requests_local || local_request?
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end
end
# Overwrite to implement custom logging of errors. By default logs as fatal.
def log_error(exception) #:doc:
if ActionView::TemplateError === exception
logger.fatal(exception.to_s)
else
logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") +
"\n\n"
)
end
end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
def rescue_action_in_public(exception) #:doc:
case exception
when RoutingError, UnknownAction then
render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
else render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
end
end
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurrences than
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
# remotely.
def local_request? #:doc:
[@request.remote_addr, @request.remote_ip] == ["127.0.0.1"] * 2
end
# Renders a detailed diagnostics screen on action exceptions.
def rescue_action_locally(exception)
add_variables_to_assigns
@template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(__FILE__) + "/templates/rescues/")
@template.send(:assign_variables_from_controller)
@template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
@headers["Content-Type"] = "text/html"
render_file(rescues_path("layout"), response_code_for_rescue(exception))
end
private
def perform_action_with_rescue #:nodoc:
begin
perform_action_without_rescue
rescue Object => exception
if defined?(Breakpoint) && @params["BP-RETRY"]
msg = exception.backtrace.first
if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then
origin_file, origin_line = md[1], md[2].to_i
set_trace_func(lambda do |type, file, line, method, context, klass|
if file == origin_file and line == origin_line then
set_trace_func(nil)
@params["BP-RETRY"] = false
callstack = caller
callstack.slice!(0) if callstack.first["rescue.rb"]
file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
message = "Exception at #{file}:#{line}#{" in `#{method}'" if method}." # `´ ( for ruby-mode)
Breakpoint.handle_breakpoint(context, message, file, line)
end
end)
retry
end
end
rescue_action(exception)
end
end
def rescues_path(template_name)
File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
end
def template_path_for_local_rescue(exception)
rescues_path(
case exception
when MissingTemplate then "missing_template"
when RoutingError then "routing_error"
when UnknownAction then "unknown_action"
when ActionView::TemplateError then "template_error"
else "diagnostics"
end
)
end
def response_code_for_rescue(exception)
case exception
when UnknownAction, RoutingError then "404 Page Not Found"
else "500 Internal Error"
end
end
def clean_backtrace(exception)
exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line }
end
end
end

View file

@ -0,0 +1,17 @@
module ActionController
class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end
def redirect(to_url, permanently = false)
@headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
@headers["location"] = to_url
@body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
end
end
end

View file

@ -0,0 +1,716 @@
module ActionController
module Routing #:nodoc:
class << self
def expiry_hash(options, recall)
k = v = nil
expire_on = {}
options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))}
expire_on
end
def extract_parameter_value(parameter) #:nodoc:
CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s)
end
def controller_relative_to(controller, previous)
if controller.nil? then previous
elsif controller[0] == ?/ then controller[1..-1]
elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
else controller
end
end
def treat_hash(hash, keys_to_delete = [])
k = v = nil
hash.each do |k, v|
if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s
else
hash.delete k
keys_to_delete << k
end
end
hash
end
def test_condition(expression, condition)
case condition
when String then "(#{expression} == #{condition.inspect})"
when Regexp then
condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source
"(#{condition.inspect} =~ #{expression})"
when Array then
conds = condition.collect do |condition|
cond = test_condition(expression, condition)
(cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})"
end
"(#{conds.join(' || ')})"
when true then expression
when nil then "! #{expression}"
else
raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil"
end
end
end
class Component #:nodoc:
def dynamic?() false end
def optional?() false end
def key() nil end
def self.new(string, *args)
return super(string, *args) unless self == Component
case string
when ':controller' then ControllerComponent.new(:controller, *args)
when /^:(\w+)$/ then DynamicComponent.new($1, *args)
when /^\*(\w+)$/ then PathComponent.new($1, *args)
else StaticComponent.new(string, *args)
end
end
end
class StaticComponent < Component #:nodoc:
attr_reader :value
def initialize(value)
@value = value
end
def write_recognition(g)
g.if_next_matches(value) do |gp|
gp.move_forward {|gpp| gpp.continue}
end
end
def write_generation(g)
g.add_segment(value) {|gp| gp.continue }
end
end
class DynamicComponent < Component #:nodoc:
attr_reader :key, :default
attr_accessor :condition
def dynamic?() true end
def optional?() @optional end
def default=(default)
@optional = true
@default = default
end
def initialize(key, options = {})
@key = key.to_sym
@optional = false
default, @condition = options[:default], options[:condition]
self.default = default if options.key?(:default)
end
def default_check(g)
presence = "#{g.hash_value(key, !! default)}"
if default
"!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})"
else
"! #{presence}"
end
end
def write_generation(g)
wrote_dropout = write_dropout_generation(g)
write_continue_generation(g, wrote_dropout)
end
def write_dropout_generation(g)
return false unless optional? && g.after.all? {|c| c.optional?}
check = [default_check(g)]
gp = g.dup # Use another generator to write the conditions after the first &&
# We do this to ensure that the generator will not assume x_value is set. It will
# not be set if it follows a false condition -- for example, false && (x = 2)
check += gp.after.map {|c| c.default_check gp}
gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here
true
end
def write_continue_generation(g, use_else)
test = Routing.test_condition(g.hash_value(key, true, default), condition || true)
check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test]
g.send(*check) do |gp|
gp.expire_for_keys(key) unless gp.after.empty?
add_segments_to(gp) {|gpp| gpp.continue}
end
end
def add_segments_to(g)
g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp}
end
def recognition_check(g)
test_type = [true, nil].include?(condition) ? :presence : :constraint
prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : ''
check = prefix + Routing.test_condition(g.next_segment(true), condition || true)
g.if(check) {|gp| yield gp, test_type}
end
def write_recognition(g)
test_type = nil
recognition_check(g) do |gp, test_type|
assign_result(gp) {|gpp| gpp.continue}
end
if optional? && g.after.all? {|c| c.optional?}
call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"]
g.send(*call) do |gp|
assign_default(gp)
gp.after.each {|c| c.assign_default(gp)}
gp.finish(false)
end
end
end
def assign_result(g, with_default = false)
g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})"
g.move_forward {|gp| yield gp}
end
def assign_default(g)
g.constant_result key, default unless default.nil?
end
end
class ControllerComponent < DynamicComponent #:nodoc:
def key() :controller end
def add_segments_to(g)
g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp}
end
def recognition_check(g)
g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})"
g.if('controller_result') do |gp|
gp << 'controller_value, segments_to_controller = controller_result'
if condition
gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')"
gp.if(Routing.test_condition("controller_path", condition)) do |gpp|
gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint}
end
else
gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint}
end
end
end
def assign_result(g)
g.result key, 'controller_value'
yield g
end
def assign_default(g)
ControllerComponent.assign_controller(g, default)
end
class << self
def assign_controller(g, controller)
expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller"
g.result :controller, expr, true
end
def traverse_to_controller(segments, start_at = 0)
mod = ::Object
length = segments.length
index = start_at
mod_name = controller_name = segment = nil
while index < length
return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index])
index += 1
mod_name = segment.camelize
controller_name = "#{mod_name}Controller"
path_suffix = File.join(segments[start_at..(index - 1)])
next_mod = nil
# If the controller is already present, or if we load it, return it.
if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined
controller = mod.const_get(controller_name)
return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller?
return [controller, (index - start_at)]
end
# No controller? Look for the module
if mod.const_defined? mod_name
next_mod = mod.send(:const_get, mod_name)
next_mod = nil unless next_mod.is_a?(Module)
else
# Try to load a file that defines the module we want.
case attempt_load(mod, mod_name, path_suffix)
when :defined then next_mod = mod.const_get mod_name
when :dir then # We didn't find a file, but there's a dir.
next_mod = Module.new # So create a module for the directory
mod.send :const_set, mod_name, next_mod
else
return nil
end
end
mod = next_mod
return nil unless mod && mod.is_a?(Module)
end
nil
end
protected
def safe_load_paths #:nodoc:
if defined?(RAILS_ROOT)
$LOAD_PATH.select do |base|
base = File.expand_path(base)
extended_root = File.expand_path(RAILS_ROOT)
# Exclude all paths that are not nested within app, lib, or components.
base.match(/\A#{Regexp.escape(extended_root)}\/*(app|lib|components)\/[a-z]/) || base =~ %r{rails-[\d.]+/builtin}
end
else
$LOAD_PATH
end
end
def attempt_load(mod, const_name, path)
has_dir = false
safe_load_paths.each do |load_path|
full_path = File.join(load_path, path)
file_path = full_path + '.rb'
if File.file?(file_path) # Found a .rb file? Load it up
require_dependency(file_path)
return :defined if mod.const_defined? const_name
else
has_dir ||= File.directory?(full_path)
end
end
return (has_dir ? :dir : nil)
end
end
end
class PathComponent < DynamicComponent #:nodoc:
def optional?() true end
def default() [] end
def condition() nil end
def default=(value)
raise RoutingError, "All path components have an implicit default of []" unless value == []
end
def write_generation(g)
raise RoutingError, 'Path components must occur last' unless g.after.empty?
g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do
g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)"
g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish }
end
g.else { g.finish }
end
def write_recognition(g)
raise RoutingError, "Path components must occur last" unless g.after.empty?
start = g.index_name
start = "(#{start})" unless /^\w+$/ =~ start
value_expr = "#{g.path_name}[#{start}..-1] || []"
g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})"
g.finish(false)
end
class Result < ::Array #:nodoc:
def to_s() join '/' end
def self.new_escaped(strings)
new strings.collect {|str| CGI.unescape str}
end
end
end
class Route #:nodoc:
attr_accessor :components, :known
attr_reader :path, :options, :keys, :defaults
def initialize(path, options = {})
@path, @options = path, options
initialize_components path
defaults, conditions = initialize_hashes options.dup
@defaults = defaults.dup
configure_components(defaults, conditions)
add_default_requirements
initialize_keys
end
def inspect
"<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>"
end
def write_generation(generator = CodeGeneration::GenerationGenerator.new)
generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || [])
if known.empty? then generator.go
else
# Alter the conditions to allow :action => 'index' to also catch :action => nil
altered_known = known.collect do |k, v|
if k == :action && v== 'index' then [k, [nil, 'index']]
else [k, v]
end
end
generator.if(generator.check_conditions(altered_known)) {|gp| gp.go }
end
generator
end
def write_recognition(generator = CodeGeneration::RecognitionGenerator.new)
g = generator.dup
g.share_locals_with generator
g.before, g.current, g.after = [], components.first, (components[1..-1] || [])
known.each do |key, value|
if key == :controller then ControllerComponent.assign_controller(g, value)
else g.constant_result(key, value)
end
end
g.go
generator
end
def initialize_keys
@keys = (components.collect {|c| c.key} + known.keys).compact
@keys.freeze
end
def extra_keys(options)
options.keys - @keys
end
def matches_controller?(controller)
if known[:controller] then known[:controller] == controller
else
c = components.find {|c| c.key == :controller}
return false unless c
return c.condition.nil? || eval(Routing.test_condition('controller', c.condition))
end
end
protected
def initialize_components(path)
path = path.split('/') if path.is_a? String
path.shift if path.first.blank?
self.components = path.collect {|str| Component.new str}
end
def initialize_hashes(options)
path_keys = components.collect {|c| c.key }.compact
self.known = {}
defaults = options.delete(:defaults) || {}
conditions = options.delete(:require) || {}
conditions.update(options.delete(:requirements) || {})
options.each do |k, v|
if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v
else known[k] = v
end
end
[defaults, conditions]
end
def configure_components(defaults, conditions)
components.each do |component|
if defaults.key?(component.key) then component.default = defaults[component.key]
elsif component.key == :action then component.default = 'index'
elsif component.key == :id then component.default = nil
end
component.condition = conditions[component.key] if conditions.key?(component.key)
end
end
def add_default_requirements
component_keys = components.collect {|c| c.key}
known[:action] ||= 'index' unless component_keys.include? :action
end
end
class RouteSet #:nodoc:
attr_reader :routes, :categories, :controller_to_selector
def initialize
@routes = []
@generation_methods = Hash.new(:generate_default_path)
end
def generate(options, request_or_recall_hash = {})
recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters
use_recall = true
controller = options[:controller]
options[:action] ||= 'index' if controller
recall_controller = recall[:controller]
if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/))
recall = {} if controller && controller[0] == ?/
options[:controller] = Routing.controller_relative_to(controller, recall_controller)
end
options = recall.dup if options.empty? # XXX move to url_rewriter?
keys_to_delete = []
Routing.treat_hash(options, keys_to_delete)
merged = recall.merge(options)
keys_to_delete.each {|key| merged.delete key}
expire_on = Routing.expiry_hash(options, recall)
generate_path(merged, options, expire_on)
end
def generate_path(merged, options, expire_on)
send @generation_methods[merged[:controller]], merged, options, expire_on
end
def generate_default_path(*args)
write_generation
generate_default_path(*args)
end
def write_generation
method_sources = []
@generation_methods = Hash.new(:generate_default_path)
categorize_routes.each do |controller, routes|
next unless routes.length < @routes.length
ivar = controller.gsub('/', '__')
method_name = "generate_path_for_#{ivar}".to_sym
instance_variable_set "@#{ivar}", routes
code = generation_code_for(ivar, method_name).to_s
method_sources << code
filename = "generated_code/routing/generation_for_controller_#{controller}.rb"
eval(code, nil, filename)
@generation_methods[controller.to_s] = method_name
@generation_methods[controller.to_sym] = method_name
end
code = generation_code_for('routes', 'generate_default_path').to_s
eval(code, nil, 'generated_code/routing/generation.rb')
return (method_sources << code)
end
def recognize(request)
string_path = request.path
string_path.chomp! if string_path[0] == ?/
path = string_path.split '/'
path.shift
hash = recognize_path(path)
return recognition_failed(request) unless hash && hash['controller']
controller = hash['controller']
hash['controller'] = controller.controller_path
request.path_parameters = hash
controller.new
end
alias :recognize! :recognize
def recognition_failed(request)
raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}"
end
def write_recognition
g = generator = CodeGeneration::RecognitionGenerator.new
g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"}
g.def "self.recognize_path(path)" do
each do |route|
g << 'index = 0'
route.write_recognition(g)
end
end
eval g.to_s, nil, 'generated/routing/recognition.rb'
return g.to_s
end
def generation_code_for(ivar = 'routes', method_name = nil)
routes = instance_variable_get('@' + ivar)
key_ivar = "@keys_for_#{ivar}"
instance_variable_set(key_ivar, routes.collect {|route| route.keys})
g = generator = CodeGeneration::GenerationGenerator.new
g.def "self.#{method_name}(merged, options, expire_on)" do
g << 'unused_count = options.length + 1'
g << "unused_keys = keys = options.keys"
g << 'path = nil'
routes.each_with_index do |route, index|
g << "new_unused_keys = keys - #{key_ivar}[#{index}]"
g << 'new_path = ('
g.source.indent do
if index.zero?
g << "new_unused_count = new_unused_keys.length"
g << "hash = merged; not_expired = true"
route.write_generation(g.dup)
else
g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp|
gp << "hash = merged; not_expired = true"
route.write_generation(gp)
end
end
end
g.source.lines.last << ' )' # Add the closing brace to the end line
g.if 'new_path' do
g << 'return new_path, [] if new_unused_count.zero?'
g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count'
end
end
g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path"
g << "return path, unused_keys"
end
return g
end
def categorize_routes
@categorized_routes = by_controller = Hash.new(self)
known_controllers.each do |name|
set = by_controller[name] = []
each do |route|
set << route if route.matches_controller? name
end
end
@categorized_routes
end
def known_controllers
@routes.inject([]) do |known, route|
if (controller = route.known[:controller])
if controller.is_a?(Regexp)
known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word}
else known << controller
end
end
known
end.uniq
end
def reload
NamedRoutes.clear
if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb'))
else connect(':controller/:action/:id', :action => 'index', :id => nil)
end
NamedRoutes.install
end
def connect(*args)
new_route = Route.new(*args)
@routes << new_route
return new_route
end
def draw
old_routes = @routes
@routes = []
begin yield self
rescue
@routes = old_routes
raise
end
write_generation
write_recognition
end
def empty?() @routes.empty? end
def each(&block) @routes.each(&block) end
# Defines a new named route with the provided name and arguments.
# This method need only be used when you wish to use a name that a RouteSet instance
# method exists for, such as categories.
#
# For example, map.categories '/categories', :controller => 'categories' will not work
# due to RouteSet#categories.
def named_route(name, path, hash = {})
route = connect(path, hash)
NamedRoutes.name_route(route, name)
route
end
def method_missing(name, *args)
(1..2).include?(args.length) ? named_route(name, *args) : super(name, *args)
end
def extra_keys(options, recall = {})
generate(options.dup, recall).last
end
end
module NamedRoutes #:nodoc:
Helpers = []
class << self
def clear() Helpers.clear end
def hash_access_name(name)
"hash_for_#{name}_url"
end
def url_helper_name(name)
"#{name}_url"
end
def known_hash_for_route(route)
hash = route.known.symbolize_keys
route.defaults.each do |key, value|
hash[key.to_sym] ||= value if value
end
hash[:controller] = "/#{hash[:controller]}"
hash
end
def define_hash_access_method(route, name)
hash = known_hash_for_route(route)
define_method(hash_access_name(name)) do |*args|
args.first ? hash.merge(args.first) : hash
end
end
def name_route(route, name)
define_hash_access_method(route, name)
module_eval(%{def #{url_helper_name name}(options = {})
url_for(#{hash_access_name(name)}.merge(options))
end}, "generated/routing/named_routes/#{name}.rb")
protected url_helper_name(name), hash_access_name(name)
Helpers << url_helper_name(name).to_sym
Helpers << hash_access_name(name).to_sym
Helpers.uniq!
end
def install(cls = ActionController::Base)
cls.send :include, self
if cls.respond_to? :helper_method
Helpers.each do |helper_name|
cls.send :helper_method, helper_name
end
end
end
end
end
Routes = RouteSet.new
end
end

View file

@ -0,0 +1,190 @@
module ActionController
module Scaffolding # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
# for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
# with both controller logic and default templates that through introspection already know which fields to display
# and which input types to use. Example:
#
# class WeblogController < ActionController::Base
# scaffold :entry
# end
#
# This tiny piece of code will add all of the following methods to the controller:
#
# class WeblogController < ActionController::Base
# verify :method => :post, :only => [ :destroy, :create, :update ],
# :redirect_to => { :action => :list }
#
# def index
# list
# end
#
# def list
# @entries = Entry.find_all
# render_scaffold "list"
# end
#
# def show
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def destroy
# Entry.find(params[:id]).destroy
# redirect_to :action => "list"
# end
#
# def new
# @entry = Entry.new
# render_scaffold
# end
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
# flash[:notice] = "Entry was successfully created"
# redirect_to :action => "list"
# else
# render_scaffold('new')
# end
# end
#
# def edit
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def update
# @entry = Entry.find(params[:id])
# @entry.attributes = params[:entry]
#
# if @entry.save
# flash[:notice] = "Entry was successfully updated"
# redirect_to :action => "show", :id => @entry
# else
# render_scaffold('edit')
# end
# end
# end
#
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# and one action at a time while relying on the rest of the scaffolded templates and actions.
module ClassMethods
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
# one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
# and @post/@posts for the instance variables.
#
# It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
# instead of just list, show, and post. If suffix is used, then no index method is added.
def scaffold(model_id, options = {})
options.assert_valid_keys(:class_name, :suffix)
singular_name = model_id.to_s
class_name = options[:class_name] || singular_name.camelize
plural_name = singular_name.pluralize
suffix = options[:suffix] ? "_#{singular_name}" : ""
unless options[:suffix]
module_eval <<-"end_eval", __FILE__, __LINE__
def index
list
end
end_eval
end
module_eval <<-"end_eval", __FILE__, __LINE__
verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
:redirect_to => { :action => :list#{suffix} }
def list#{suffix}
@#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
render#{suffix}_scaffold "list#{suffix}"
end
def show#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def destroy#{suffix}
#{class_name}.find(params[:id]).destroy
redirect_to :action => "list#{suffix}"
end
def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end
def create#{suffix}
@#{singular_name} = #{class_name}.new(params[:#{singular_name}])
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully created"
redirect_to :action => "list#{suffix}"
else
render#{suffix}_scaffold('new')
end
end
def edit#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def update#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
@#{singular_name}.attributes = params[:#{singular_name}]
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully updated"
redirect_to :action => "show#{suffix}", :id => @#{singular_name}
else
render#{suffix}_scaffold('edit')
end
end
private
def render#{suffix}_scaffold(action=nil)
action ||= caller_method_name(caller)
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
if template_exists?("\#{self.class.controller_path}/\#{action}")
render_action(action)
else
@scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@scaffold_suffix = "#{suffix}"
add_instance_variables_to_assigns
@template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
if !active_layout.nil?
render_file(active_layout, nil, true)
else
render_file(scaffold_path("layout"))
end
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end
end
end
end

View file

@ -0,0 +1,333 @@
require 'cgi'
require 'cgi/session'
require 'digest/md5'
require 'base64'
class CGI
class Session
# Return this session's underlying Session instance. Useful for the DB-backed session stores.
def model
@dbman.model if @dbman
end
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record +Session+ class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of config/environment.rb:
# CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
# CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
# CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
# Note that setting the primary key to the session_id frees you from
# having a separate id column if you don't want it. However, you must
# set session.model.id = session.session_id by hand! A before_filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
# +CGI::Session::ActiveRecordStore.session_class = MySessionClass+
# You must implement these methods:
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data)
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class ActiveRecordStore
# The default Active Record class.
class Session < ActiveRecord::Base
# Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
before_save :marshal_data!
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
# Hook to set up sessid compatibility.
def find_by_session_id(session_id)
setup_sessid_compatibility!
find_by_session_id(session_id)
end
def marshal(data) Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end
def create_table!
connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
)
end_sql
end
def drop_table!
connection.execute "DROP TABLE #{table_name}"
end
private
# Compatibility with tables using sessid instead of session_id.
def setup_sessid_compatibility!
# Reset column info since it may be stale.
reset_column_information
if columns_hash['sessid']
def self.find_by_session_id(*args)
find_by_sessid(*args)
end
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
end
end
end
end
# Lazy-unmarshal session state.
def data
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
# Has the session been loaded yet?
def loaded?
!! @data
end
private
attr_writer :data
def marshal_data!
return false if !loaded?
write_attribute(@@data_column_name, self.class.marshal(self.data))
end
# Ensures that the data about to be stored in the database is not
# larger than the data storage column. Raises
# ActionController::SessionOverflowError.
def raise_on_session_data_overflow!
return false if !loaded?
limit = self.class.data_column_size_limit
if loaded? and limit and read_attribute(@@data_column_name).size > limit
raise ActionController::SessionOverflowError
end
end
end
# A barebones session store which duck-types with the default session
# store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
# are configurable class attributes. Marshaling and unmarshaling
# are implemented as class methods that you may override. By default,
# marshaling data is +Base64.encode64(Marshal.dump(data))+ and
# unmarshaling data is +Marshal.load(Base64.decode64(data))+.
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
# Use the ActiveRecord::Base.connection by default.
cattr_accessor :connection
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
# The session id field defaults to 'session_id'.
cattr_accessor :session_id_column
@@session_id_column = 'session_id'
# The data field defaults to 'data'.
cattr_accessor :data_column
@@data_column = 'data'
class << self
def connection
@@connection ||= ActiveRecord::Base.connection
end
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
def marshal(data) Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end
def create_table!
@@connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
#{@@connection.quote_column_name(data_column)} TEXT
)
end_sql
end
def drop_table!
@@connection.execute "DROP TABLE #{table_name}"
end
end
attr_reader :session_id
attr_writer :data
# Look for normal and marshaled data, self.find_by_session_id's way of
# telling us to postpone unmarshaling until the data is requested.
# We need to handle a normal data attribute in case of a new record.
def initialize(attributes)
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
@new_record = @marshaled_data.nil?
end
def new_record?
@new_record
end
# Lazy-unmarshal session state.
def data
unless @data
if @marshaled_data
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
else
@data = {}
end
end
@data
end
def loaded?
!! @data
end
def save
return false if !loaded?
marshaled_data = self.class.marshal(data)
if @new_record
@new_record = false
@@connection.update <<-end_sql, 'Create session'
INSERT INTO #{@@table_name} (
#{@@connection.quote_column_name(@@session_id_column)},
#{@@connection.quote_column_name(@@data_column)} )
VALUES (
#{@@connection.quote(session_id)},
#{@@connection.quote(marshaled_data)} )
end_sql
else
@@connection.update <<-end_sql, 'Update session'
UPDATE #{@@table_name}
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
def destroy
unless @new_record
@@connection.delete <<-end_sql, 'Destroy session'
DELETE FROM #{@@table_name}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
end
# The class used for session storage. Defaults to
# CGI::Session::ActiveRecordStore::Session.
cattr_accessor :session_class
self.session_class = Session
# Find or instantiate a session given a CGI::Session.
def initialize(session, option = nil)
session_id = session.session_id
unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
unless session.new_session
raise CGI::Session::NoSession, 'uninitialized session'
end
@session = @@session_class.new(:session_id => session_id, :data => {})
# session saving can be lazy again, because of improved component implementation
# therefore next line gets commented out:
# @session.save
end
end
# Access the underlying session model.
def model
@session
end
# Restore session state. The session model handles unmarshaling.
def restore
if @session
@session.data
end
end
# Save session store.
def update
if @session
ActiveRecord::Base.silence { @session.save }
end
end
# Save and close the session store.
def close
if @session
update
@session = nil
end
end
# Delete and close the session store.
def delete
if @session
ActiveRecord::Base.silence { @session.destroy }
@session = nil
end
end
protected
def logger
ActionController::Base.logger rescue nil
end
end
end
end

View file

@ -0,0 +1,32 @@
#!/usr/local/bin/ruby -w
# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
require 'drb'
session_hash = Hash.new
session_hash.instance_eval { @mutex = Mutex.new }
class <<session_hash
def []=(key, value)
@mutex.synchronize do
super(key, value)
end
end
def [](key)
@mutex.synchronize do
super(key)
end
end
def delete(key)
@mutex.synchronize do
super(key)
end
end
end
DRb.start_service('druby://127.0.0.1:9192', session_hash)
DRb.thread.join

View file

@ -0,0 +1,31 @@
require 'cgi'
require 'cgi/session'
require 'drb'
class CGI #:nodoc:all
class Session
class DRbStore
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
def initialize(session, option=nil)
@session_id = session.session_id
end
def restore
@h = @@session_data[@session_id] || {}
end
def update
@@session_data[@session_id] = @h
end
def close
update
end
def delete
@@session_data.delete(@session_id)
end
end
end
end

View file

@ -0,0 +1,101 @@
# cgi/session/memcached.rb - persistent storage of marshalled session data
#
# == Overview
#
# This file provides the CGI::Session::MemCache class, which builds
# persistence of storage data on top of the MemCache library. See
# cgi/session.rb for more details on session storage managers.
#
begin
require 'cgi/session'
require 'memcache'
class CGI
class Session
# MemCache-based session storage class.
#
# This builds upon the top-level MemCache class provided by the
# library file memcache.rb. Session data is marshalled and stored
# in a memcached cache.
class MemCacheStore
def check_id(id) #:nodoc:#
/[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
end
# Create a new CGI::Session::MemCache instance
#
# This constructor is used internally by CGI::Session. The
# user does not generally need to call it directly.
#
# +session+ is the session for which this instance is being
# created. The session id must only contain alphanumeric
# characters; automatically generated session ids observe
# this requirement.
#
# +options+ is a hash of options for the initializer. The
# following options are recognized:
#
# cache:: an instance of a MemCache client to use as the
# session cache.
#
# expires:: an expiry time value to use for session entries in
# the session cache. +expires+ is interpreted in seconds
# relative to the current time if its less than 60*60*24*30
# (30 days), or as an absolute Unix time (e.g., Time#to_i) if
# greater. If +expires+ is +0+, or not passed on +options+,
# the entry will never expire.
#
# This session's memcache entry will be created if it does
# not exist, or retrieved if it does.
def initialize(session, options = {})
id = session.session_id
unless check_id(id)
raise ArgumentError, "session_id '%s' is invalid" % id
end
@cache = options['cache'] || MemCache.new('localhost')
@expires = options['expires'] || 0
@session_key = "session:#{id}"
@session_data = {}
end
# Restore session state from the session's memcache entry.
#
# Returns the session state as a hash.
def restore
begin
@session_data = @cache[@session_key] || {}
rescue
@session_data = {}
end
end
# Save session state to the session's memcache entry.
def update
begin
@cache.set(@session_key, @session_data, @expires)
rescue
# Ignore session update failures.
end
end
# Update and close the session's memcache entry.
def close
update
end
# Delete the session's memcache entry.
def delete
begin
@cache.delete(@session_key)
rescue
# Ignore session delete failures.
end
@session_data = {}
end
end
end
end
rescue LoadError
# MemCache wasn't available so neither can the store be
end

View file

@ -0,0 +1,145 @@
require 'action_controller/session/drb_store'
require 'action_controller/session/mem_cache_store'
if Object.const_defined?(:ActiveRecord)
require 'action_controller/session/active_record_store'
end
module ActionController #:nodoc:
module SessionManagement #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send :alias_method, :process_without_session_management_support, :process
base.send :alias_method, :process, :process_with_session_management_support
base.send :alias_method, :process_cleanup_without_session_management_support, :process_cleanup
base.send :alias_method, :process_cleanup, :process_cleanup_with_session_management_support
end
module ClassMethods
# Set the session store to be used for keeping the session data between requests. The default is using the
# file system, but you can also specify one of the other included stores (:active_record_store, :drb_store,
# :mem_cache_store, or :memory_store) or use your own class.
def session_store=(store)
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] =
store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store
end
# Returns the session store class currently used.
def session_store
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
end
# Returns the hash used to configure the session. Example use:
#
# ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS
def session_options
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
end
# Specify how sessions ought to be managed for a subset of the actions on
# the controller. Like filters, you can specify <tt>:only</tt> and
# <tt>:except</tt> clauses to restrict the subset, otherwise options
# apply to all actions on this controller.
#
# The session options are inheritable, as well, so if you specify them in
# a parent controller, they apply to controllers that extend the parent.
#
# Usage:
#
# # turn off session management for all actions.
# session :off
#
# # turn off session management for all actions _except_ foo and bar.
# session :off, :except => %w(foo bar)
#
# # turn off session management for only the foo and bar actions.
# session :off, :only => %w(foo bar)
#
# # the session will only work over HTTPS, but only for the foo action
# session :only => :foo, :session_secure => true
#
# # the session will only be disabled for 'foo', and only if it is
# # requested as a web service
# session :off, :only => :foo,
# :if => Proc.new { |req| req.parameters[:ws] }
#
# All session options described for ActionController::Base.process_cgi
# are valid arguments.
def session(*args)
options = Hash === args.last ? args.pop : {}
options[:disabled] = true if !args.empty?
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
if options[:only] && options[:except]
raise ArgumentError, "only one of either :only or :except are allowed"
end
write_inheritable_array("session_options", [options])
end
def cached_session_options #:nodoc:
@session_options ||= read_inheritable_attribute("session_options") || []
end
def session_options_for(request, action) #:nodoc:
if (session_options = cached_session_options).empty?
{}
else
options = {}
action = action.to_s
session_options.each do |opts|
next if opts[:if] && !opts[:if].call(request)
if opts[:only] && opts[:only].include?(action)
options.merge!(opts)
elsif opts[:except] && !opts[:except].include?(action)
options.merge!(opts)
elsif !opts[:only] && !opts[:except]
options.merge!(opts)
end
end
if options.empty? then options
else
options.delete :only
options.delete :except
options.delete :if
options[:disabled] ? false : options
end
end
end
end
def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
set_session_options(request)
process_without_session_management_support(request, response, method, *arguments)
end
private
def set_session_options(request)
request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
end
def process_cleanup_with_session_management_support
process_cleanup_without_session_management_support
clear_persistent_model_associations
end
# Clear cached associations in session data so they don't overflow
# the database field. Only applies to ActiveRecordStore since there
# is not a standard way to iterate over session data.
def clear_persistent_model_associations #:doc:
if defined?(@session) && @session.instance_variables.include?('@data')
session_data = @session.instance_variable_get('@data')
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
end
end
end
end

View file

@ -0,0 +1,147 @@
module ActionController #:nodoc:
# Methods for sending files and streams to the browser instead of rendering.
module Streaming
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,
:disposition => 'attachment'.freeze,
:stream => true,
:buffer_size => 4096
}.freeze
protected
# Sends the file by streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes
# it feasible to send even large files.
#
# Be careful to sanitize the path parameter if it coming from a web
# page. send_file(params[:path]) allows a malicious user to
# download any file on your server.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to File.basename(path).
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:stream</tt> - whether to send the file to the user agent as it is read (true)
# or to read the entire file before sending (false). Defaults to true.
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
# a variety of quirks (especially when downloading over SSL).
#
# Simple download:
# send_file '/path/to.zip'
#
# Show a JPEG in the browser:
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
#
# Show a 404 page in the browser:
# send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description).
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
# http://www.mnot.net/cache_docs/ for an overview of web caching and
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {}) #:doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path)
send_file_headers! options
@performed_render = false
if options[:stream]
render :status => options[:status], :text => Proc.new { |response, output|
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
if output.respond_to?(:syswrite)
begin
while true
output.syswrite(file.sysread(len))
end
rescue EOFError
end
else
while buf = file.read(len)
output.write(buf)
end
end
end
}
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
end
end
# Send binary data to the user as a file download. May set content type, apparent file name,
# and specify whether to show data inline or download as an attachment.
#
# Options:
# * <tt>:filename</tt> - Suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# Valid values are 'inline' and 'attachment' (default).
#
# Generic data download:
# send_data buffer
#
# Download a dynamically-generated tarball:
# send_data generate_tgz('dir'), :filename => 'dir.tgz'
#
# Display an image Active Record in the browser:
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" unless logger.nil?
send_file_headers! options.merge(:length => data.size)
@performed_render = false
render :status => options[:status], :text => data
end
private
def send_file_headers!(options)
options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
[:length, :type, :disposition].each do |arg|
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
end
disposition = options[:disposition].dup || 'attachment'
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
@headers.update(
'Content-Length' => options[:length],
'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
@headers['Cache-Control'] = 'private' if @headers['Cache-Control'] == 'no-cache'
end
end
end

View file

@ -0,0 +1,44 @@
<% unless @exception.blamed_files.blank? %>
<% if (hide = @exception.blamed_files.length > 8) %>
<a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a>
<% end %>
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre>
<% end %>
<% if false %>
<br /><br />
<% begin %>
<%= form_tag(@request.request_uri, "method" => @request.method) %>
<input type="hidden" name="BP-RETRY" value="1" />
<% for key, values in @params %>
<% next if key == "BP-RETRY" %>
<% for value in Array(values) %>
<input type="hidden" name="<%= key %>" value="<%= value %>" />
<% end %>
<% end %>
<input type="submit" value="Retry with Breakpoint" />
</form>
<% rescue Exception => e %>
<%=h "Couldn't render breakpoint link due to #{e.class} #{e.message}" %>
<% end %>
<% end %>
<%
request_parameters_without_action = @request.parameters.clone
request_parameters_without_action.delete("action")
request_parameters_without_action.delete("controller")
request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n")
%>
<h2 style="margin-top: 30px">Request</h2>
<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p>
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div>
<h2 style="margin-top: 30px">Response</h2>
<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/>

View file

@ -0,0 +1,26 @@
<%
traces = [
["Application Trace", @exception.application_backtrace],
["Framework Trace", @exception.framework_backtrace],
["Full Trace", @exception.clean_backtrace]
]
names = traces.collect {|name, trace| name}
%>
<p><code>RAILS_ROOT: <%= defined?(RAILS_ROOT) ? RAILS_ROOT : "unset" %></code></p>
<div id="traces">
<% names.each do |name| -%>
<%
show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';"
hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"}
%>
<a href="#" onclick="<%= hide %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
<% end -%>
<% traces.each do |name, trace| -%>
<div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;">
<pre><code><%= trace.join "\n" %></code></pre>
</div>
<% end -%>
</div>

View file

@ -0,0 +1,11 @@
<h1>
<%=h @exception.class.to_s %>
<% if @request.parameters['controller'] %>
in <%=h @request.parameters['controller'].humanize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %>
<% end %>
</h1>
<pre><%=h @exception.clean_message %></pre>
<%= render_file(@rescues_path + "/_trace.rhtml", false) %>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>

View file

@ -0,0 +1,29 @@
<html>
<head>
<title>Action Controller: Exception caught</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
</style>
</head>
<body>
<%= @contents %>
</body>
</html>

View file

@ -0,0 +1,2 @@
<h1>Template is missing</h1>
<p><%=h @exception.message %></p>

View file

@ -0,0 +1,10 @@
<h1>Routing Error</h1>
<p><pre><%=h @exception.message %></pre></p>
<% unless @exception.failures.empty? %><p>
<h2>Failure reasons:</h2>
<ol>
<% @exception.failures.each do |route, reason| %>
<li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li>
<% end %>
</ol>
</p><% end %>

View file

@ -0,0 +1,21 @@
<h1>
<%=h @exception.original_exception.class.to_s %> in
<%=h @request.parameters["controller"].capitalize if @request.parameters["controller"]%>#<%=h @request.parameters["action"] %>
</h1>
<p>
Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised:
<pre><code><%=h @exception.message %></code></pre>
</p>
<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>):
<pre><code><%=h @exception.source_extract %></code></pre></p>
<p><%=h @exception.sub_template_message %></p>
<% @real_exception = @exception
@exception = @exception.original_exception || @exception %>
<%= render_file(@rescues_path + "/_trace.rhtml", false) %>
<% @exception = @real_exception %>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>

View file

@ -0,0 +1,2 @@
<h1>Unknown action</h1>
<p><%=h @exception.message %></p>

View file

@ -0,0 +1,7 @@
<h1>Editing <%= @scaffold_singular_name %></h1>
<%= error_messages_for(@scaffold_singular_name) %>
<%= form(@scaffold_singular_name, :action => "update#{@scaffold_suffix}") %>
<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -0,0 +1,69 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Scaffolding</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
</style>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1,27 @@
<h1>Listing <%= @scaffold_plural_name %></h1>
<table>
<tr>
<% for column in @scaffold_class.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>
<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
<tr>
<% for column in @scaffold_class.content_columns %>
<td><%= entry.send(column.name) %></td>
<% end %>
<td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :post => true} %></td>
</tr>
<% end %>
</table>
<%= link_to "Previous page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous %>
<%= link_to "Next page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.next } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.next %>
<br />
<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>

View file

@ -0,0 +1,6 @@
<h1>New <%= @scaffold_singular_name %></h1>
<%= error_messages_for(@scaffold_singular_name) %>
<%= form(@scaffold_singular_name, :action => "create#{@scaffold_suffix}") %>
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -0,0 +1,9 @@
<% for column in @scaffold_class.content_columns %>
<p>
<b><%= column.human_name %>:</b>
<%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
</p>
<% end %>
<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -0,0 +1,482 @@
require File.dirname(__FILE__) + '/assertions'
require File.dirname(__FILE__) + '/deprecated_assertions'
module ActionController #:nodoc:
class Base
# Process a test request called with a +TestRequest+ object.
def self.process_test(request)
new.process_test(request)
end
def process_test(request) #:nodoc:
process(request, TestResponse.new)
end
def process_with_test(*args)
returning process_without_test(*args) do
add_variables_to_assigns
end
end
alias_method :process_without_test, :process
alias_method :process, :process_with_test
end
class TestRequest < AbstractRequest #:nodoc:
attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session, :env
attr_accessor :host
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {}
@request_parameters = request_parameters || {}
@session = session || TestSession.new
initialize_containers
initialize_default_values
super()
end
def reset_session
@session = {}
end
def raw_post
if raw_post = env['RAW_POST_DATA']
raw_post
else
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
params.delete(k.to_sym)
end
params.map { |k,v| [ CGI.escape(k.to_s), CGI.escape(v.to_s) ].join('=') }.sort.join('&')
end
end
def port=(number)
@env["SERVER_PORT"] = number.to_i
@port_as_int = nil
end
def action=(action_name)
@query_parameters.update({ "action" => action_name })
@parameters = nil
end
# Used to check AbstractRequest's request_uri functionality.
# Disables the use of @path and @request_uri so superclass can handle those.
def set_REQUEST_URI(value)
@env["REQUEST_URI"] = value
@request_uri = nil
@path = nil
end
def request_uri=(uri)
@request_uri = uri
@path = uri.split("?").first
end
def remote_addr=(addr)
@env['REMOTE_ADDR'] = addr
end
def remote_addr
@env['REMOTE_ADDR']
end
def request_uri
@request_uri || super()
end
def path
@path || super()
end
def assign_parameters(controller_path, action, parameters)
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
non_path_parameters = get? ? query_parameters : request_parameters
parameters.each do |key, value|
if value.is_a? Fixnum
value = value.to_s
elsif value.is_a? Array
value = ActionController::Routing::PathComponent::Result.new(value)
end
if extra_keys.include?(key.to_sym)
non_path_parameters[key] = value
else
path_parameters[key.to_s] = value
end
end
end
def recycle!
self.request_parameters = {}
self.query_parameters = {}
self.path_parameters = {}
@request_method, @accepts, @content_type = nil, nil, nil
end
private
def initialize_containers
@env, @cookies = {}, {}
end
def initialize_default_values
@host = "test.host"
@request_uri = "/"
self.remote_addr = "0.0.0.0"
@env["SERVER_PORT"] = 80
@env['REQUEST_METHOD'] = "GET"
end
end
# A refactoring of TestResponse to allow the same behavior to be applied
# to the "real" CgiResponse class in integration tests.
module TestResponseBehavior #:nodoc:
# the response code of the request
def response_code
headers['Status'][0,3].to_i rescue 0
end
# returns a String to ensure compatibility with Net::HTTPResponse
def code
headers['Status'].to_s.split(' ')[0]
end
def message
headers['Status'].to_s.split(' ',2)[1]
end
# was the response successful?
def success?
response_code == 200
end
# was the URL not found?
def missing?
response_code == 404
end
# were we redirected?
def redirect?
(300..399).include?(response_code)
end
# was there a server-side error?
def error?
(500..599).include?(response_code)
end
alias_method :server_error?, :error?
# returns the redirection location or nil
def redirect_url
redirect? ? headers['location'] : nil
end
# does the redirect location match this regexp pattern?
def redirect_url_match?( pattern )
return false if redirect_url.nil?
p = Regexp.new(pattern) if pattern.class == String
p = pattern if pattern.class == Regexp
return false if p.nil?
p.match(redirect_url) != nil
end
# returns the template path of the file which was used to
# render this response (or nil)
def rendered_file(with_controller=false)
unless template.first_render.nil?
unless with_controller
template.first_render
else
template.first_render.split('/').last || template.first_render
end
end
end
# was this template rendered by a file?
def rendered_with_file?
!rendered_file.nil?
end
# a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!)
def flash
session['flash'] || {}
end
# do we have a flash?
def has_flash?
!session['flash'].empty?
end
# do we have a flash that has contents?
def has_flash_with_contents?
!flash.empty?
end
# does the specified flash object exist?
def has_flash_object?(name=nil)
!flash[name].nil?
end
# does the specified object exist in the session?
def has_session_object?(name=nil)
!session[name].nil?
end
# a shortcut to the template.assigns
def template_objects
template.assigns || {}
end
# does the specified template object exist?
def has_template_object?(name=nil)
!template_objects[name].nil?
end
# Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs
# Example:
#
# assert_equal ['AuthorOfNewPage'], r.cookies['author'].value
def cookies
headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash }
end
# Returns binary content (downloadable file), converted to a String
def binary_content
raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc)
require 'stringio'
sio = StringIO.new
begin
$stdout = sio
body.call
ensure
$stdout = STDOUT
end
sio.rewind
sio.read
end
end
class TestResponse < AbstractResponse #:nodoc:
include TestResponseBehavior
end
class TestSession #:nodoc:
def initialize(attributes = {})
@attributes = attributes
end
def [](key)
@attributes[key]
end
def []=(key, value)
@attributes[key] = value
end
def session_id
""
end
def update() end
def close() end
def delete() @attributes = {} end
end
# Essentially generates a modified Tempfile object similar to the object
# you'd get from the standard library CGI module in a multipart
# request. This means you can use an ActionController::TestUploadedFile
# object in the params of a test request in order to simulate
# a file upload.
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
class TestUploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_reader :content_type
def initialize(path, content_type = 'text/plain')
raise "file does not exist" unless File.exist?(path)
@content_type = content_type
@original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
@tempfile = Tempfile.new(@original_filename)
FileUtils.copy_file(path, @tempfile.path)
end
def path #:nodoc:
@tempfile.path
end
alias local_path path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.send(method_name, *args, &block)
end
end
module TestProcess
def self.included(base)
# execute the request simulating a specific http method and set/volley the response
%w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil)
@request.env['REQUEST_METHOD'] = "#{method.upcase}" if @request
process(action, parameters, session, flash)
end
EOV
end
end
# execute the request and set/volley the response
def process(action, parameters = nil, session = nil, flash = nil)
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(controller request response).each do |iv_name|
raise "@#{iv_name} is nil: make sure you set it in your test's setup method." if instance_variable_get("@#{iv_name}").nil?
end
@request.recycle!
@html_document = nil
@request.env['REQUEST_METHOD'] ||= "GET"
@request.action = action.to_s
parameters ||= {}
@request.assign_parameters(@controller.class.controller_path, action.to_s, parameters)
@request.session = ActionController::TestSession.new(session) unless session.nil?
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
build_request_uri(action, parameters)
@controller.process(@request, @response)
end
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
returning self.send(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
end
end
alias xhr :xml_http_request
def follow_redirect
if @response.redirected_to[:controller]
raise "Can't follow redirects outside of current controller (#{@response.redirected_to[:controller]})"
end
get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys)
end
def assigns(key = nil)
if key.nil?
@response.template.assigns
else
@response.template.assigns[key.to_s]
end
end
def session
@response.session
end
def flash
@response.flash
end
def cookies
@response.cookies
end
def redirect_to_url
@response.redirect_url
end
def build_request_uri(action, parameters)
unless @request.env['REQUEST_URI']
options = @controller.send(:rewrite_options, parameters)
options.update(:only_path => true, :action => action)
url = ActionController::UrlRewriter.new(@request, parameters)
@request.set_REQUEST_URI(url.rewrite(options))
end
end
def html_document
@html_document ||= HTML::Document.new(@response.body)
end
def find_tag(conditions)
html_document.find(conditions)
end
def find_all_tag(conditions)
html_document.find_all(conditions)
end
def method_missing(selector, *args)
return @controller.send(selector, *args) if ActionController::Routing::NamedRoutes::Helpers.include?(selector)
return super
end
# Shortcut for ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type). Example:
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
def fixture_file_upload(path, mime_type = nil)
ActionController::TestUploadedFile.new(
Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path,
mime_type
)
end
# A helper to make it easier to test different route configurations.
# This method temporarily replaces ActionController::Routing::Routes
# with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using map.draw { map.connect ... }:
#
# with_routing do |set|
# set.draw { set.connect ':controller/:id/:action' }
# assert_equal(
# ['/content/10/show', {}],
# set.generate(:controller => 'content', :id => 10, :action => 'show')
# )
# end
#
def with_routing
real_routes = ActionController::Routing::Routes
ActionController::Routing.send :remove_const, :Routes
temporary_routes = ActionController::Routing::RouteSet.new
ActionController::Routing.send :const_set, :Routes, temporary_routes
yield temporary_routes
ensure
if ActionController::Routing.const_defined? :Routes
ActionController::Routing.send(:remove_const, :Routes)
end
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end
end
end
module Test
module Unit
class TestCase #:nodoc:
include ActionController::TestProcess
end
end
end

View file

@ -0,0 +1,74 @@
module ActionController
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters)
@request, @parameters = request, parameters
end
def rewrite(options = {})
rewrite_url(rewrite_path(options), options)
end
def to_str
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}"
end
alias_method :to_s, :to_str
private
def rewrite_url(path, options)
rewritten_url = ""
unless options[:only_path]
rewritten_url << (options[:protocol] || @request.protocol)
rewritten_url << (options[:host] || @request.host_with_port)
end
rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << path
rewritten_url << '/' if options[:trailing_slash]
rewritten_url << "##{options[:anchor]}" if options[:anchor]
rewritten_url
end
def rewrite_path(options)
options = options.symbolize_keys
options.update(options[:params].symbolize_keys) if options[:params]
if (overwrite = options.delete(:overwrite_params))
options.update(@parameters.symbolize_keys)
options.update(overwrite)
end
RESERVED_OPTIONS.each {|k| options.delete k}
path, extra_keys = Routing::Routes.generate(options.dup, @request) # Warning: Routes will mutate and violate the options hash
path << build_query_string(options, extra_keys) unless extra_keys.empty?
path
end
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
# be added as a path element instead of a regular parameter pair.
def build_query_string(hash, only_keys = nil)
elements = []
query_string = ""
only_keys ||= hash.keys
only_keys.each do |key|
value = hash[key]
key = CGI.escape key.to_s
if value.class == Array
key << '[]'
else
value = [ value ]
end
value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" }
end
query_string << ("?" + elements.join("&")) unless elements.empty?
query_string
end
end
end

View file

@ -0,0 +1,64 @@
require File.dirname(__FILE__) + '/tokenizer'
require File.dirname(__FILE__) + '/node'
module HTML #:nodoc:
# A top-level HTMl document. You give it a body of text, and it will parse that
# text into a tree of nodes.
class Document #:nodoc:
# The root of the parsed document.
attr_reader :root
# Create a new Document from the given text.
def initialize(text, strict=false, xml=false)
tokenizer = Tokenizer.new(text)
@root = Node.new(nil)
node_stack = [ @root ]
while token = tokenizer.next
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token)
node_stack.last.children << node unless node.tag? && node.closing == :close
if node.tag?
if node_stack.length > 1 && node.closing == :close
if node_stack.last.name == node.name
node_stack.pop
else
open_start = node_stack.last.position - 20
open_start = 0 if open_start < 0
close_start = node.position - 20
close_start = 0 if close_start < 0
msg = <<EOF.strip
ignoring attempt to close #{node_stack.last.name} with #{node.name}
opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
closed at byte #{node.position}, line #{node.line}
attributes at open: #{node_stack.last.attributes.inspect}
text around open: #{text[open_start,40].inspect}
text around close: #{text[close_start,40].inspect}
EOF
strict ? raise(msg) : warn(msg)
end
elsif !node.childless?(xml) && node.closing != :close
node_stack.push node
end
end
end
end
# Search the tree for (and return) the first node that matches the given
# conditions. The conditions are interpreted differently for different node
# types, see HTML::Text#find and HTML::Tag#find.
def find(conditions)
@root.find(conditions)
end
# Search the tree for (and return) all nodes that match the given
# conditions. The conditions are interpreted differently for different node
# types, see HTML::Text#find and HTML::Tag#find.
def find_all(conditions)
@root.find_all(conditions)
end
end
end

View file

@ -0,0 +1,533 @@
require 'strscan'
module HTML #:nodoc:
class Conditions < Hash #:nodoc:
def initialize(hash)
super()
hash = { :content => hash } unless Hash === hash
hash = keys_to_symbols(hash)
hash.each do |k,v|
case k
when :tag, :content then
# keys are valid, and require no further processing
when :attributes then
hash[k] = keys_to_strings(v)
when :parent, :child, :ancestor, :descendant, :sibling, :before,
:after
hash[k] = Conditions.new(v)
when :children
hash[k] = v = keys_to_symbols(v)
v.each do |k,v2|
case k
when :count, :greater_than, :less_than
# keys are valid, and require no further processing
when :only
v[k] = Conditions.new(v2)
else
raise "illegal key #{k.inspect} => #{v2.inspect}"
end
end
else
raise "illegal key #{k.inspect} => #{v.inspect}"
end
end
update hash
end
private
def keys_to_strings(hash)
hash.keys.inject({}) do |h,k|
h[k.to_s] = hash[k]
h
end
end
def keys_to_symbols(hash)
hash.keys.inject({}) do |h,k|
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
h[k.to_sym] = hash[k]
h
end
end
end
# The base class of all nodes, textual and otherwise, in an HTML document.
class Node #:nodoc:
# The array of children of this node. Not all nodes have children.
attr_reader :children
# The parent node of this node. All nodes have a parent, except for the
# root node.
attr_reader :parent
# The line number of the input where this node was begun
attr_reader :line
# The byte position in the input where this node was begun
attr_reader :position
# Create a new node as a child of the given parent.
def initialize(parent, line=0, pos=0)
@parent = parent
@children = []
@line, @position = line, pos
end
# Return a textual representation of the node.
def to_s
s = ""
@children.each { |child| s << child.to_s }
s
end
# Return false (subclasses must override this to provide specific matching
# behavior.) +conditions+ may be of any type.
def match(conditions)
false
end
# Search the children of this node for the first node for which #find
# returns non +nil+. Returns the result of the #find call that succeeded.
def find(conditions)
conditions = validate_conditions(conditions)
@children.each do |child|
node = child.find(conditions)
return node if node
end
nil
end
# Search for all nodes that match the given conditions, and return them
# as an array.
def find_all(conditions)
conditions = validate_conditions(conditions)
matches = []
matches << self if match(conditions)
@children.each do |child|
matches.concat child.find_all(conditions)
end
matches
end
# Returns +false+. Subclasses may override this if they define a kind of
# tag.
def tag?
false
end
def validate_conditions(conditions)
Conditions === conditions ? conditions : Conditions.new(conditions)
end
def ==(node)
return false unless self.class == node.class && children.size == node.children.size
equivalent = true
children.size.times do |i|
equivalent &&= children[i] == node.children[i]
end
equivalent
end
class <<self
def parse(parent, line, pos, content, strict=true)
if content !~ /^<\S/
Text.new(parent, line, pos, content)
else
scanner = StringScanner.new(content)
unless scanner.skip(/</)
if strict
raise "expected <"
else
return Text.new(parent, line, pos, content)
end
end
if scanner.skip(/!\[CDATA\[/)
scanner.scan_until(/\]\]>/)
return CDATA.new(parent, line, pos, scanner.pre_match)
end
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
name.downcase!
unless closing
scanner.skip(/\s*/)
attributes = {}
while attr = scanner.scan(/[-\w:]+/)
value = true
if scanner.scan(/\s*=\s*/)
if delim = scanner.scan(/['"]/)
value = ""
while text = scanner.scan(/[^#{delim}\\]+|./)
case text
when "\\" then
value << text
value << scanner.getch
when delim
break
else value << text
end
end
else
value = scanner.scan(/[^\s>\/]+/)
end
end
attributes[attr.downcase] = value
scanner.skip(/\s*/)
end
closing = ( scanner.scan(/\//) ? :self : nil )
end
unless scanner.scan(/\s*>/)
if strict
raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
else
# throw away all text until we find what we're looking for
scanner.skip_until(/>/) or scanner.terminate
end
end
Tag.new(parent, line, pos, name, attributes, closing)
end
end
end
end
# A node that represents text, rather than markup.
class Text < Node #:nodoc:
attr_reader :content
# Creates a new text node as a child of the given parent, with the given
# content.
def initialize(parent, line, pos, content)
super(parent, line, pos)
@content = content
end
# Returns the content of this node.
def to_s
@content
end
# Returns +self+ if this node meets the given conditions. Text nodes support
# conditions of the following kinds:
#
# * if +conditions+ is a string, it must be a substring of the node's
# content
# * if +conditions+ is a regular expression, it must match the node's
# content
# * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
# is either a string or a regexp, and which is interpreted as described
# above.
def find(conditions)
match(conditions) && self
end
# Returns non-+nil+ if this node meets the given conditions, or +nil+
# otherwise. See the discussion of #find for the valid conditions.
def match(conditions)
case conditions
when String
@content.index(conditions)
when Regexp
@content =~ conditions
when Hash
conditions = validate_conditions(conditions)
# Text nodes only have :content, :parent, :ancestor
unless (conditions.keys - [:content, :parent, :ancestor]).empty?
return false
end
match(conditions[:content])
else
nil
end
end
def ==(node)
return false unless super
content == node.content
end
end
# A CDATA node is simply a text node with a specialized way of displaying
# itself.
class CDATA < Text #:nodoc:
def to_s
"<![CDATA[#{super}]>"
end
end
# A Tag is any node that represents markup. It may be an opening tag, a
# closing tag, or a self-closing tag. It has a name, and may have a hash of
# attributes.
class Tag < Node #:nodoc:
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
attr_reader :closing
# Either +nil+, or a hash of attributes for this node.
attr_reader :attributes
# The name of this tag.
attr_reader :name
# Create a new node as a child of the given parent, using the given content
# to describe the node. It will be parsed and the node name, attributes and
# closing status extracted.
def initialize(parent, line, pos, name, attributes, closing)
super(parent, line, pos)
@name = name
@attributes = attributes
@closing = closing
end
# A convenience for obtaining an attribute of the node. Returns +nil+ if
# the node has no attributes.
def [](attr)
@attributes ? @attributes[attr] : nil
end
# Returns non-+nil+ if this tag can contain child nodes.
def childless?(xml = false)
return false if xml && @closing.nil?
!@closing.nil? ||
@name =~ /^(img|br|hr|link|meta|area|base|basefont|
col|frame|input|isindex|param)$/ox
end
# Returns a textual representation of the node
def to_s
if @closing == :close
"</#{@name}>"
else
s = "<#{@name}"
@attributes.each do |k,v|
s << " #{k}"
s << "='#{v.gsub(/'/,"\\\\'")}'" if String === v
end
s << " /" if @closing == :self
s << ">"
@children.each { |child| s << child.to_s }
s << "</#{@name}>" if @closing != :self && !@children.empty?
s
end
end
# If either the node or any of its children meet the given conditions, the
# matching node is returned. Otherwise, +nil+ is returned. (See the
# description of the valid conditions in the +match+ method.)
def find(conditions)
match(conditions) && self || super
end
# Returns +true+, indicating that this node represents an HTML tag.
def tag?
true
end
# Returns +true+ if the node meets any of the given conditions. The
# +conditions+ parameter must be a hash of any of the following keys
# (all are optional):
#
# * <tt>:tag</tt>: the node name must match the corresponding value
# * <tt>:attributes</tt>: a hash. The node's values must match the
# corresponding values in the hash.
# * <tt>:parent</tt>: a hash. The node's parent must match the
# corresponding hash.
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
# must meet the criteria described by the hash.
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
# meet the criteria described by the hash.
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
# must meet the criteria described by the hash.
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
# meet the criteria described by the hash.
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
# the criteria described by the hash, and at least one sibling must match.
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
# keys:
# ** <tt>:count</tt>: either a number or a range which must equal (or
# include) the number of children that match.
# ** <tt>:less_than</tt>: the number of matching children must be less than
# this number.
# ** <tt>:greater_than</tt>: the number of matching children must be
# greater than this number.
# ** <tt>:only</tt>: another hash consisting of the keys to use
# to match on the children, and only matching children will be
# counted.
#
# Conditions are matched using the following algorithm:
#
# * if the condition is a string, it must be a substring of the value.
# * if the condition is a regexp, it must match the value.
# * if the condition is a number, the value must match number.to_s.
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# Usage:
#
# # test if the node is a "span" tag
# node.match :tag => "span"
#
# # test if the node's parent is a "div"
# node.match :parent => { :tag => "div" }
#
# # test if any of the node's ancestors are "table" tags
# node.match :ancestor => { :tag => "table" }
#
# # test if any of the node's immediate children are "em" tags
# node.match :child => { :tag => "em" }
#
# # test if any of the node's descendants are "strong" tags
# node.match :descendant => { :tag => "strong" }
#
# # test if the node has between 2 and 4 span tags as immediate children
# node.match :children => { :count => 2..4, :only => { :tag => "span" } }
#
# # get funky: test to see if the node is a "div", has a "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and whether or not it has
# # a "span" descendant that contains # text matching /hello world/:
# node.match :tag => "div",
# :ancestor => { :tag => "ul" },
# :parent => { :tag => "li",
# :attributes => { :class => "enum" } },
# :descendant => { :tag => "span",
# :child => /hello world/ }
def match(conditions)
conditions = validate_conditions(conditions)
# check content of child nodes
if conditions[:content]
if children.empty?
return false unless match_condition("", conditions[:content])
else
return false unless children.find { |child| child.match(conditions[:content]) }
end
end
# test the name
return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
# test attributes
(conditions[:attributes] || {}).each do |key, value|
return false unless match_condition(self[key], value)
end
# test parent
return false unless parent.match(conditions[:parent]) if conditions[:parent]
# test children
return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
# test ancestors
if conditions[:ancestor]
return false unless catch :found do
p = self
throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
end
end
# test descendants
if conditions[:descendant]
return false unless children.find do |child|
# test the child
child.match(conditions[:descendant]) ||
# test the child's descendants
child.match(:descendant => conditions[:descendant])
end
end
# count children
if opts = conditions[:children]
matches = children.select do |c|
c.match(/./) or
(c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
end
matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
opts.each do |key, value|
next if key == :only
case key
when :count
if Integer === value
return false if matches.length != value
else
return false unless value.include?(matches.length)
end
when :less_than
return false unless matches.length < value
when :greater_than
return false unless matches.length > value
else raise "unknown count condition #{key}"
end
end
end
# test siblings
if conditions[:sibling] || conditions[:before] || conditions[:after]
siblings = parent ? parent.children : []
self_index = siblings.index(self)
if conditions[:sibling]
return false unless siblings.detect do |s|
s != self && s.match(conditions[:sibling])
end
end
if conditions[:before]
return false unless siblings[self_index+1..-1].detect do |s|
s != self && s.match(conditions[:before])
end
end
if conditions[:after]
return false unless siblings[0,self_index].detect do |s|
s != self && s.match(conditions[:after])
end
end
end
true
end
def ==(node)
return false unless super
return false unless closing == node.closing && self.name == node.name
attributes == node.attributes
end
private
# Match the given value to the given condition.
def match_condition(value, condition)
case condition
when String
value && value == condition
when Regexp
value && value.match(condition)
when Numeric
value == condition.to_s
when true
!value.nil?
when false, nil
value.nil?
else
false
end
end
end
end

View file

@ -0,0 +1,105 @@
require 'strscan'
module HTML #:nodoc:
# A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
# token is a string. Each string represents either "text", or an HTML element.
#
# This currently assumes valid XHTML, which means no free < or > characters.
#
# Usage:
#
# tokenizer = HTML::Tokenizer.new(text)
# while token = tokenizer.next
# p token
# end
class Tokenizer #:nodoc:
# The current (byte) position in the text
attr_reader :position
# The current line number
attr_reader :line
# Create a new Tokenizer for the given text.
def initialize(text)
@scanner = StringScanner.new(text)
@position = 0
@line = 0
@current_line = 1
end
# Return the next token in the sequence, or +nil+ if there are no more tokens in
# the stream.
def next
return nil if @scanner.eos?
@position = @scanner.pos
@line = @current_line
if @scanner.check(/<\S/)
update_current_line(scan_tag)
else
update_current_line(scan_text)
end
end
private
# Treat the text at the current position as a tag, and scan it. Supports
# comments, doctype tags, and regular tags, and ignores less-than and
# greater-than characters within quoted strings.
def scan_tag
tag = @scanner.getch
if @scanner.scan(/!--/) # comment
tag << @scanner.matched
tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
elsif @scanner.scan(/!\[CDATA\[/)
tag << @scanner.matched
tag << @scanner.scan_until(/\]\]>/)
elsif @scanner.scan(/!/) # doctype
tag << @scanner.matched
tag << consume_quoted_regions
else
tag << consume_quoted_regions
end
tag
end
# Scan all text up to the next < character and return it.
def scan_text
"#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
end
# Counts the number of newlines in the text and updates the current line
# accordingly.
def update_current_line(text)
text.scan(/\r?\n/) { @current_line += 1 }
end
# Skips over quoted strings, so that less-than and greater-than characters
# within the strings are ignored.
def consume_quoted_regions
text = ""
loop do
match = @scanner.scan_until(/['"<>]/) or break
delim = @scanner.matched
if delim == "<"
match = match.chop
@scanner.pos -= 1
end
text << match
break if delim == "<" || delim == ">"
# consume the quoted region
while match = @scanner.scan_until(/[\\#{delim}]/)
text << match
break if @scanner.matched == delim
text << @scanner.getch # skip the escaped character
end
end
text
end
end
end

View file

@ -0,0 +1,11 @@
module HTML #:nodoc:
module Version #:nodoc:
MAJOR = 0
MINOR = 5
TINY = 3
STRING = [ MAJOR, MINOR, TINY ].join(".")
end
end

View file

@ -0,0 +1,97 @@
require 'rexml/document'
# SimpleXML like xml parser. Written by leon breet from the ruby on rails Mailing list
class XmlNode #:nodoc:
attr :node
def initialize(node, options = {})
@node = node
@children = {}
@raise_errors = options[:raise_errors]
end
def self.from_xml(xml_or_io)
document = REXML::Document.new(xml_or_io)
if document.root
XmlNode.new(document.root)
else
XmlNode.new(document)
end
end
def node_encoding
@node.encoding
end
def node_name
@node.name
end
def node_value
@node.text
end
def node_value=(value)
@node.text = value
end
def xpath(expr)
matches = nil
REXML::XPath.each(@node, expr) do |element|
matches ||= XmlNodeList.new
matches << (@children[element] ||= XmlNode.new(element))
end
matches
end
def method_missing(name, *args)
name = name.to_s
nodes = nil
@node.each_element(name) do |element|
nodes ||= XmlNodeList.new
nodes << (@children[element] ||= XmlNode.new(element))
end
nodes
end
def <<(node)
if node.is_a? REXML::Node
child = node
elsif node.respond_to? :node
child = node.node
end
@node.add_element child
@children[child] ||= XmlNode.new(child)
end
def [](name)
@node.attributes[name.to_s]
end
def []=(name, value)
@node.attributes[name.to_s] = value
end
def to_s
@node.to_s
end
def to_i
to_s.to_i
end
end
class XmlNodeList < Array #:nodoc:
def [](i)
i.is_a?(String) ? super(0)[i] : super(i)
end
def []=(i, value)
i.is_a?(String) ? self[0][i] = value : super(i, value)
end
def method_missing(name, *args)
name = name.to_s
self[0].__send__(name, *args)
end
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
module ActionController #:nodoc:
module Verification #:nodoc:
def self.append_features(base) #:nodoc:
super
base.extend(ClassMethods)
end
# This module provides a class-level method for specifying that certain
# actions are guarded against being called without certain prerequisites
# being met. This is essentially a special kind of before_filter.
#
# An action may be guarded against being invoked without certain request
# parameters being set, or without certain session values existing.
#
# When a verification is violated, values may be inserted into the flash, and
# a specified redirection is triggered.
#
# Usage:
#
# class GlobalController < ActionController::Base
# # prevent the #update_settings action from being invoked unless
# # the 'admin_privileges' request parameter exists.
# verify :params => "admin_privileges", :only => :update_post,
# :redirect_to => { :action => "settings" }
#
# # disallow a post from being updated if there was no information
# # submitted with the post, and if there is no active post in the
# # session, and if there is no "note" key in the flash.
# verify :params => "post", :session => "post", "flash" => "note",
# :only => :update_post,
# :add_flash => { "alert" => "Failed to create your message" },
# :redirect_to => :category_url
#
module ClassMethods
# Verify the given actions so that if certain prerequisites are not met,
# the user is redirected to a different action. The +options+ parameter
# is a hash consisting of the following key/value pairs:
#
# * <tt>:params</tt>: a single key or an array of keys that must
# be in the <tt>params</tt> hash in order for the action(s) to be safely
# called.
# * <tt>:session</tt>: a single key or an array of keys that must
# be in the @session in order for the action(s) to be safely called.
# * <tt>:flash</tt>: a single key or an array of keys that must
# be in the flash in order for the action(s) to be safely called.
# * <tt>:method</tt>: a single key or an array of keys--any one of which
# must match the current request method in order for the action(s) to
# be safely called. (The key should be a symbol: <tt>:get</tt> or
# <tt>:post</tt>, for example.)
# * <tt>:xhr</tt>: true/false option to ensure that the request is coming
# from an Ajax call or not.
# * <tt>:add_flash</tt>: a hash of name/value pairs that should be merged
# into the session's flash if the prerequisites cannot be satisfied.
# * <tt>:redirect_to</tt>: the redirection parameters to be used when
# redirecting if the prerequisites cannot be satisfied.
# * <tt>:render</tt>: the render parameters to be used when
# the prerequisites cannot be satisfied.
# * <tt>:only</tt>: only apply this verification to the actions specified
# in the associated array (may also be a single value).
# * <tt>:except</tt>: do not apply this verification to the actions
# specified in the associated array (may also be a single value).
def verify(options={})
filter_opts = { :only => options[:only], :except => options[:except] }
before_filter(filter_opts) do |c|
c.send :verify_action, options
end
end
end
def verify_action(options) #:nodoc:
prereqs_invalid =
[*options[:params] ].find { |v| @params[v].nil? } ||
[*options[:session]].find { |v| @session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? }
if !prereqs_invalid && options[:method]
prereqs_invalid ||=
[*options[:method]].all? { |v| @request.method != v.to_sym }
end
prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil?
if prereqs_invalid
flash.update(options[:add_flash]) if options[:add_flash]
unless performed?
render(options[:render]) if options[:render]
redirect_to(options[:redirect_to]) if options[:redirect_to]
end
return false
end
true
end
private :verify_action
end
end