diff options
Diffstat (limited to 'vendor/rails-2.0.2/actionpack/lib')
102 files changed, 29311 insertions, 0 deletions
diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller.rb new file mode 100755 index 000000000..e7a9eba56 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller.rb @@ -0,0 +1,79 @@ +#-- +# Copyright (c) 2004-2007 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +$:.unshift(File.dirname(__FILE__)) unless + $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) + +unless defined?(ActiveSupport) + begin + $:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib" + require 'active_support' + rescue LoadError + require 'rubygems' + gem 'activesupport' + end +end + +$:.unshift "#{File.dirname(__FILE__)}/action_controller/vendor/html-scanner" + +require 'action_controller/base' +require 'action_controller/request' +require 'action_controller/rescue' +require 'action_controller/benchmarking' +require 'action_controller/flash' +require 'action_controller/filters' +require 'action_controller/layout' +require 'action_controller/mime_responds' +require 'action_controller/helpers' +require 'action_controller/cookies' +require 'action_controller/cgi_process' +require 'action_controller/caching' +require 'action_controller/verification' +require 'action_controller/streaming' +require 'action_controller/session_management' +require 'action_controller/http_authentication' +require 'action_controller/components' +require 'action_controller/record_identifier' +require 'action_controller/request_forgery_protection' + +require 'action_view' +ActionController::Base.template_class = ActionView::Base + +ActionController::Base.class_eval do + include ActionController::Flash + include ActionController::Filters + include ActionController::Layout + include ActionController::Benchmarking + include ActionController::Rescue + include ActionController::MimeResponds + include ActionController::Helpers + include ActionController::Cookies + include ActionController::Caching + include ActionController::Verification + include ActionController::Streaming + include ActionController::SessionManagement + include ActionController::HttpAuthentication::Basic::ControllerMethods + include ActionController::Components + include ActionController::RecordIdentifier + include ActionController::RequestForgeryProtection +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions.rb new file mode 100644 index 000000000..5b9a2b71f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions.rb @@ -0,0 +1,69 @@ +require 'test/unit/assertions' + +module ActionController #: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 devised 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 + def self.included(klass) + %w(response selector tag dom routing model).each do |kind| + require "action_controller/assertions/#{kind}_assertions" + klass.module_eval { include const_get("#{kind.camelize}Assertions") } + end + end + + def clean_backtrace(&block) + yield + rescue Test::Unit::AssertionFailedError => error + framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions")) + error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path } + raise + end + end +end + +module Test #:nodoc: + module Unit #:nodoc: + class TestCase #:nodoc: + include ActionController::Assertions + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/dom_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/dom_assertions.rb new file mode 100644 index 000000000..5ffe5f188 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/dom_assertions.rb @@ -0,0 +1,39 @@ +module ActionController + module Assertions + module DomAssertions + # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) + # + # ==== Examples + # + # # assert that the referenced method generates the appropriate HTML string + # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") + # + 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 + + # The negated form of +assert_dom_equivalent+. + # + # ==== Examples + # + # # assert that the referenced method does not generate the specified HTML string + # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") + # + 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 + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/model_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/model_assertions.rb new file mode 100644 index 000000000..0b4313055 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/model_assertions.rb @@ -0,0 +1,19 @@ +module ActionController + module Assertions + module ModelAssertions + # Ensures that the passed record is valid by ActiveRecord standards and returns any error messages if it is not. + # + # ==== Examples + # + # # assert that a newly created record is valid + # model = Model.new + # assert_valid(model) + # + def assert_valid(record) + clean_backtrace do + assert record.valid?, record.errors.full_messages.join("\n") + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/response_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/response_assertions.rb new file mode 100644 index 000000000..42bd7fb3d --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -0,0 +1,166 @@ +require 'rexml/document' +require 'html/document' + +module ActionController + module Assertions + # A small suite of assertions that test responses from Rails applications. + module ResponseAssertions + # 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 number like assert_response(501) + # or its symbolic equivalent assert_response(:not_implemented). + # See ActionController::StatusCodes for a full list. + # + # ==== Examples + # + # # assert that the response was a redirection + # assert_response :redirect + # + # # assert that the response code was status code 401 (unauthorized) + # assert_response 401 + # + 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 + elsif type.is_a?(Symbol) && @response.response_code == ActionController::StatusCodes::SYMBOL_TO_STATUS_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. + # + # ==== Examples + # + # # assert that the redirection was to the "index" action on the WeblogController + # assert_redirected_to :controller => "weblog", :action => "index" + # + # # assert that the redirection was to the named route login_url + # assert_redirected_to login_url + # + def assert_redirected_to(options = {}, message=nil) + clean_backtrace do + assert_response(:redirect, message) + return true if options == @response.redirected_to + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + + begin + url = {} + original = { :expected => options, :actual => @response.redirected_to.is_a?(Symbol) ? @response.redirected_to : @response.redirected_to.dup } + original.each do |key, value| + if value.is_a?(Symbol) + value = @controller.respond_to?(value, true) ? @controller.send(value) : @controller.send("hash_for_#{value}_url") + end + + unless value.is_a?(Hash) + request = case value + when NilClass then nil + when /^\w+:\/\// then recognized_request_for(%r{^(\w+://.*?(/|$|\?))(.*)$} =~ value ? $3 : nil) + else recognized_request_for(value) + end + value = request.path_parameters if request + end + + if value.is_a?(Hash) # stringify 2 levels of hash keys + if name = value.delete(:use_route) + route = ActionController::Routing::Routes.named_routes[name] + value.update(route.parameter_shell) + end + + value.stringify_keys! + value.values.select { |v| v.is_a?(Hash) }.collect { |v| v.stringify_keys! } + if key == :expected && value['controller'] == @controller.controller_name && original[:actual].is_a?(Hash) + original[:actual].stringify_keys! + value.delete('controller') if original[:actual]['controller'].nil? || original[:actual]['controller'] == value['controller'] + end + end + + if value.respond_to?(:[]) && value['controller'] + value['controller'] = value['controller'].to_s + if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') + new_controller_path = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) + value['controller'] = new_controller_path if value['controller'] != new_controller_path && ActionController::Routing.possible_controllers.include?(new_controller_path) + end + value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash + end + url[key] = value + end + + @response_diff = url[:actual].diff(url[:expected]) if url[:actual] + msg = build_message(message, "expected a redirect to <?>, found one to <?>, a difference of <?> ", url[:expected], url[:actual], @response_diff) + + assert_block(msg) do + url[:expected].keys.all? do |k| + if k == :controller then url[:expected][k] == ActionController::Routing.controller_relative_to(url[:actual][k], @controller.class.controller_path) + else parameterize(url[:expected][k]) == parameterize(url[:actual][k]) + end + end + end + rescue ActionController::RoutingError # routing failed us, so match the strings only. + 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.first == '/') ? p : '/' + p] + end.flatten + + assert_equal(eurl, url, msg) if eurl && url + assert_equal(epath, path, msg) if epath && path + end + end + end + + # Asserts that the request was rendered with the appropriate template file. + # + # ==== Examples + # + # # assert that the "new" view template was rendered + # assert_template "new" + # + 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 + + private + # Recognizes the route for a given path. + def recognized_request_for(path, request_method = nil) + path = "/#{path}" unless path.first == '/' + + # Assume given controller + request = ActionController::TestRequest.new({}, {}, nil) + request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method + request.path = path + + ActionController::Routing::Routes.recognize(request) + request + end + + # Proxy to to_param if the object will respond to it. + def parameterize(value) + value.respond_to?(:to_param) ? value.to_param : value + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/routing_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/routing_assertions.rb new file mode 100644 index 000000000..9bff28324 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/routing_assertions.rb @@ -0,0 +1,143 @@ +module ActionController + module Assertions + # Suite of assertions to test routes generated by Rails and the handling of requests made to them. + module RoutingAssertions + # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) + # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. + # + # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes + # requiring a specific HTTP method. The hash should contain a :path with the incoming request path + # and a :method containing the required HTTP verb. + # + # # assert that POSTing to /items will call the create action on ItemsController + # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) + # + # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used + # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the + # extras argument, appending the query string on the path directly will not work. For example: + # + # # assert that a path of '/items/list/1?view=print' returns the correct options + # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) + # + # The +message+ parameter allows you to pass in an error message that is displayed upon failure. + # + # ==== Examples + # # Check the default route (i.e., the index action) + # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') + # + # # Test a specific action + # assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') + # + # # Test an action with a parameter + # assert_recognizes({:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1') + # + # # Test a custom route + # assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1') + # + # # Check a Simply RESTful generated route + # assert_recognizes(list_items_url, 'items/list') + def assert_recognizes(expected_options, path, extras={}, message=nil) + if path.is_a? Hash + request_method = path[:method] + path = path[:path] + else + request_method = nil + end + + clean_backtrace do + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + request = recognized_request_for(path, request_method) + + expected_options = expected_options.clone + extras.each_key { |key| expected_options.delete key } unless extras.nil? + + expected_options.stringify_keys! + routing_diff = expected_options.diff(request.path_parameters) + msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>", + request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) + assert_block(msg) { request.path_parameters == expected_options } + end + end + + # Asserts that the provided options can be used to generate the provided path. This is the inverse of #assert_recognizes. + # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in + # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures. + # + # The +defaults+ parameter is unused. + # + # ==== Examples + # # Asserts that the default action is generated for a route with no action + # assert_generates("/items", :controller => "items", :action => "index") + # + # # Tests that the list action is properly routed + # assert_generates("/items/list", :controller => "items", :action => "list") + # + # # Tests the generation of a route with a parameter + # assert_generates("/items/list/1", { :controller => "items", :action => "list", :id => "1" }) + # + # # Asserts that the generated route gives us our custom route + # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } + def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) + 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_extras(options, defaults) + 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, it verifies that <tt>path</tt> generates + # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines #assert_recognizes + # and #assert_generates into one step. + # + # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The + # +message+ parameter allows you to specify a custom error message to display upon failure. + # + # ==== Examples + # # Assert a basic route: a controller with the default action (index) + # assert_routing('/home', :controller => 'home', :action => 'index') + # + # # Test a route generated with a specific controller, action, and parameter (id) + # assert_routing('/entries/show/23', :controller => 'entries', :action => 'show', id => 23) + # + # # Assert a basic route (controller + default action), with an error message if it fails + # assert_routing('/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly') + # + # # Tests a route, providing a defaults hash + # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"} + 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 + + private + # Recognizes the route for a given path. + def recognized_request_for(path, request_method = nil) + path = "/#{path}" unless path.first == '/' + + # Assume given controller + request = ActionController::TestRequest.new({}, {}, nil) + request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method + request.path = path + + ActionController::Routing::Routes.recognize(request) + request + end + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/selector_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/selector_assertions.rb new file mode 100644 index 000000000..573405c0f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/selector_assertions.rb @@ -0,0 +1,640 @@ +#-- +# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) +# Under MIT and/or CC By license. +#++ + +require 'rexml/document' +require 'html/document' + +module ActionController + module Assertions + unless const_defined?(:NO_STRIP) + NO_STRIP = %w{pre script style textarea} + end + + # Adds the #assert_select method for use in Rails functional + # test cases, which can be used to make assertions on the response HTML of a controller + # action. You can also call #assert_select within another #assert_select to + # make assertions on elements selected by the enclosing assertion. + # + # Use #css_select to select elements without making an assertions, either + # from the response HTML or elements selected by the enclosing assertion. + # + # In addition to HTML responses, you can make the following assertions: + # * #assert_select_rjs -- Assertions on HTML content of RJS update and + # insertion operations. + # * #assert_select_encoded -- Assertions on HTML encoded inside XML, + # for example for dealing with feed item descriptions. + # * #assert_select_email -- Assertions on the HTML body of an e-mail. + # + # Also see HTML::Selector to learn how to use selectors. + module SelectorAssertions + # :call-seq: + # css_select(selector) => array + # css_select(element, selector) => array + # + # Select and return all matching elements. + # + # If called with a single argument, uses that argument as a selector + # to match all elements of the current page. Returns an empty array + # if no match is found. + # + # If called with two arguments, uses the first argument as the base + # element and the second argument as the selector. Attempts to match the + # base element and any of its children. Returns an empty array if no + # match is found. + # + # The selector may be a CSS selector expression (+String+), an expression + # with substitution values (+Array+) or an HTML::Selector object. + # + # ==== Examples + # # Selects all div tags + # divs = css_select("div") + # + # # Selects all paragraph tags and does something interesting + # pars = css_select("p") + # pars.each do |par| + # # Do something fun with paragraphs here... + # end + # + # # Selects all list items in unordered lists + # items = css_select("ul>li") + # + # # Selects all form tags and then all inputs inside the form + # forms = css_select("form") + # forms.each do |form| + # inputs = css_select(form, "input") + # ... + # end + # + def css_select(*args) + # See assert_select to understand what's going on here. + arg = args.shift + + if arg.is_a?(HTML::Node) + root = arg + arg = args.shift + elsif arg == nil + raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + matches = [] + + @selected.each do |selected| + subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) + subset.each do |match| + matches << match unless matches.any? { |m| m.equal?(match) } + end + end + + return matches + else + root = response_from_page_or_rjs + end + + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + selector.select(root) + end + + # :call-seq: + # assert_select(selector, equality?, message?) + # assert_select(element, selector, equality?, message?) + # + # An assertion that selects elements and makes one or more equality tests. + # + # If the first argument is an element, selects all matching elements + # starting from (and including) that element and all its children in + # depth-first order. + # + # If no element if specified, calling #assert_select will select from the + # response HTML. Calling #assert_select inside an #assert_select block will + # run the assertion for each element selected by the enclosing assertion. + # + # ==== Example + # assert_select "ol>li" do |elements| + # elements.each do |element| + # assert_select element, "li" + # end + # end + # + # Or for short: + # assert_select "ol>li" do + # assert_select "li" + # end + # + # The selector may be a CSS selector expression (+String+), an expression + # with substitution values, or an HTML::Selector object. + # + # === Equality Tests + # + # The equality test may be one of the following: + # * <tt>true</tt> -- Assertion is true if at least one element selected. + # * <tt>false</tt> -- Assertion is true if no element selected. + # * <tt>String/Regexp</tt> -- Assertion is true if the text value of at least + # one element matches the string or regular expression. + # * <tt>Integer</tt> -- Assertion is true if exactly that number of + # elements are selected. + # * <tt>Range</tt> -- Assertion is true if the number of selected + # elements fit the range. + # If no equality test specified, the assertion is true if at least one + # element selected. + # + # To perform more than one equality tests, use a hash with the following keys: + # * <tt>:text</tt> -- Narrow the selection to elements that have this text + # value (string or regexp). + # * <tt>:html</tt> -- Narrow the selection to elements that have this HTML + # content (string or regexp). + # * <tt>:count</tt> -- Assertion is true if the number of selected elements + # is equal to this value. + # * <tt>:minimum</tt> -- Assertion is true if the number of selected + # elements is at least this value. + # * <tt>:maximum</tt> -- Assertion is true if the number of selected + # elements is at most this value. + # + # If the method is called with a block, once all equality tests are + # evaluated the block is called with an array of all matched elements. + # + # ==== Examples + # + # # At least one form element + # assert_select "form" + # + # # Form element includes four input fields + # assert_select "form input", 4 + # + # # Page title is "Welcome" + # assert_select "title", "Welcome" + # + # # Page title is "Welcome" and there is only one title element + # assert_select "title", {:count=>1, :text=>"Welcome"}, + # "Wrong title or more than one title element" + # + # # Page contains no forms + # assert_select "form", false, "This page must contain no forms" + # + # # Test the content and style + # assert_select "body div.header ul.menu" + # + # # Use substitution values + # assert_select "ol>li#?", /item-\d+/ + # + # # All input fields in the form have a name + # assert_select "form input" do + # assert_select "[name=?]", /.+/ # Not empty + # end + def assert_select(*args, &block) + # Start with optional element followed by mandatory selector. + arg = args.shift + + if arg.is_a?(HTML::Node) + # First argument is a node (tag or text, but also HTML root), + # so we know what we're selecting from. + root = arg + arg = args.shift + elsif arg == nil + # This usually happens when passing a node/element that + # happens to be nil. + raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + root = HTML::Node.new(nil) + root.children.concat @selected + else + # Otherwise just operate on the response document. + root = response_from_page_or_rjs + end + + # First or second argument is the selector: string and we pass + # all remaining arguments. Array and we pass the argument. Also + # accepts selector itself. + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + # Next argument is used for equality tests. + equals = {} + case arg = args.shift + when Hash + equals = arg + when String, Regexp + equals[:text] = arg + when Integer + equals[:count] = arg + when Range + equals[:minimum] = arg.begin + equals[:maximum] = arg.end + when FalseClass + equals[:count] = 0 + when NilClass, TrueClass + equals[:minimum] = 1 + else raise ArgumentError, "I don't understand what you're trying to match" + end + + # By default we're looking for at least one match. + if equals[:count] + equals[:minimum] = equals[:maximum] = equals[:count] + else + equals[:minimum] = 1 unless equals[:minimum] + end + + # Last argument is the message we use if the assertion fails. + message = args.shift + #- message = "No match made with selector #{selector.inspect}" unless message + if args.shift + raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" + end + + matches = selector.select(root) + # If text/html, narrow down to those elements that match it. + content_mismatch = nil + if match_with = equals[:text] + matches.delete_if do |match| + text = "" + stack = match.children.reverse + while node = stack.pop + if node.tag? + stack.concat node.children.reverse + else + text << node.content + end + end + text.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) + content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text) + true + end + end + elsif match_with = equals[:html] + matches.delete_if do |match| + html = match.children.map(&:to_s).join + html.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) + content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html) + true + end + end + end + # Expecting foo found bar element only if found zero, not if + # found one but expecting two. + message ||= content_mismatch if matches.empty? + # Test minimum/maximum occurrence. + min, max = equals[:minimum], equals[:maximum] + message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.) + assert matches.size >= min, message if min + assert matches.size <= max, message if max + + # If a block is given call that block. Set @selected to allow + # nested assert_select, which can be nested several levels deep. + if block_given? && !matches.empty? + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + + # Returns all matches elements. + matches + end + + def count_description(min, max) #:nodoc: + pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} + + if min && max && (max != min) + "between #{min} and #{max} elements" + elsif min && !(min == 1 && max == 1) + "at least #{min} #{pluralize['element', min]}" + elsif max + "at most #{max} #{pluralize['element', max]}" + end + end + + # :call-seq: + # assert_select_rjs(id?) { |elements| ... } + # assert_select_rjs(statement, id?) { |elements| ... } + # assert_select_rjs(:insert, position, id?) { |elements| ... } + # + # Selects content from the RJS response. + # + # === Narrowing down + # + # With no arguments, asserts that one or more elements are updated or + # inserted by RJS statements. + # + # Use the +id+ argument to narrow down the assertion to only statements + # that update or insert an element with that identifier. + # + # Use the first argument to narrow down assertions to only statements + # of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>, + # <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and + # <tt>:insert_html</tt>. + # + # Use the argument <tt>:insert</tt> followed by an insertion position to narrow + # down the assertion to only statements that insert elements in that + # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt> + # and <tt>:after</tt>. + # + # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will + # be ignored as there is no HTML passed for this statement. + # + # === Using blocks + # + # Without a block, #assert_select_rjs merely asserts that the response + # contains one or more RJS statements that replace or update content. + # + # With a block, #assert_select_rjs also selects all elements used in + # these statements and passes them to the block. Nested assertions are + # supported. + # + # Calling #assert_select_rjs with no arguments and using nested asserts + # asserts that the HTML content is returned by one or more RJS statements. + # Using #assert_select directly makes the same assertion on the content, + # but without distinguishing whether the content is returned in an HTML + # or JavaScript. + # + # ==== Examples + # + # # Replacing the element foo. + # # page.replace 'foo', ... + # assert_select_rjs :replace, "foo" + # + # # Replacing with the chained RJS proxy. + # # page[:foo].replace ... + # assert_select_rjs :chained_replace, 'foo' + # + # # Inserting into the element bar, top position. + # assert_select_rjs :insert, :top, "bar" + # + # # Remove the element bar + # assert_select_rjs :remove, "bar" + # + # # Changing the element foo, with an image. + # assert_select_rjs "foo" do + # assert_select "img[src=/images/logo.gif"" + # end + # + # # RJS inserts or updates a list with four items. + # assert_select_rjs do + # assert_select "ol>li", 4 + # end + # + # # The same, but shorter. + # assert_select "ol>li", 4 + def assert_select_rjs(*args, &block) + rjs_type = nil + arg = args.shift + + # If the first argument is a symbol, it's the type of RJS statement we're looking + # for (update, replace, insertion, etc). Otherwise, we're looking for just about + # any RJS statement. + if arg.is_a?(Symbol) + rjs_type = arg + + if rjs_type == :insert + arg = args.shift + insertion = "insert_#{arg}".to_sym + raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] + statement = "(#{RJS_STATEMENTS[insertion]})" + else + raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type] + statement = "(#{RJS_STATEMENTS[rjs_type]})" + end + arg = args.shift + else + statement = "#{RJS_STATEMENTS[:any]}" + end + + # Next argument we're looking for is the element identifier. If missing, we pick + # any element. + if arg.is_a?(String) + id = Regexp.quote(arg) + arg = args.shift + else + id = "[^\"]*" + end + + pattern = + case rjs_type + when :chained_replace, :chained_replace_html + Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) + when :remove, :show, :hide, :toggle + Regexp.new("#{statement}\\(\"#{id}\"\\)") + else + Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) + end + + # Duplicate the body since the next step involves destroying it. + matches = nil + case rjs_type + when :remove, :show, :hide, :toggle + matches = @response.body.match(pattern) + else + @response.body.gsub(pattern) do |match| + html = unescape_rjs($2) + matches ||= [] + matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } + "" + end + end + + if matches + assert_block("") { true } # to count the assertion + if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + matches + else + # RJS statement not found. + flunk args.shift || "No RJS statement that replaces or inserts HTML content." + end + end + + # :call-seq: + # assert_select_encoded(element?) { |elements| ... } + # + # Extracts the content of an element, treats it as encoded HTML and runs + # nested assertion on it. + # + # You typically call this method within another assertion to operate on + # all currently selected elements. You can also pass an element or array + # of elements. + # + # The content of each element is un-encoded, and wrapped in the root + # element +encoded+. It then calls the block with all un-encoded elements. + # + # ==== Examples + # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) + # assert_select_feed :atom, 1.0 do + # # Select each entry item and then the title item + # assert_select "entry>title" do + # # Run assertions on the encoded title elements + # assert_select_encoded do + # assert_select "b" + # end + # end + # end + # + # + # # Selects all paragraph tags from within the description of an RSS feed + # assert_select_feed :rss, 2.0 do + # # Select description element of each feed item. + # assert_select "channel>item>description" do + # # Run assertions on the encoded elements. + # assert_select_encoded do + # assert_select "p" + # end + # end + # end + def assert_select_encoded(element = nil, &block) + case element + when Array + elements = element + when HTML::Node + elements = [element] + when nil + unless elements = @selected + raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" + end + else + raise ArgumentError, "Argument is optional, and may be node or array of nodes" + end + + fix_content = lambda do |node| + # Gets around a bug in the Rails 1.1 HTML parser. + node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } + end + + selected = elements.map do |element| + text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root + css_select(root, "encoded:root", &block)[0] + end + + begin + old_selected, @selected = @selected, selected + assert_select ":root", &block + ensure + @selected = old_selected + end + end + + # :call-seq: + # assert_select_email { } + # + # Extracts the body of an email and runs nested assertions on it. + # + # You must enable deliveries for this assertion to work, use: + # ActionMailer::Base.perform_deliveries = true + # + # ==== Examples + # + # assert_select_email do + # assert_select "h1", "Email alert" + # end + # + # assert_select_email do + # items = assert_select "ol>li" + # items.each do + # # Work with items here... + # end + # end + # + def assert_select_email(&block) + deliveries = ActionMailer::Base.deliveries + assert !deliveries.empty?, "No e-mail in delivery list" + + for delivery in deliveries + for part in delivery.parts + if part["Content-Type"].to_s =~ /^text\/html\W/ + root = HTML::Document.new(part.body).root + assert_select root, ":root", &block + end + end + end + end + + protected + unless const_defined?(:RJS_STATEMENTS) + RJS_STATEMENTS = { + :replace => /Element\.replace/, + :replace_html => /Element\.update/, + :chained_replace => /\.replace/, + :chained_replace_html => /\.update/, + :remove => /Element\.remove/, + :show => /Element\.show/, + :hide => /Element\.hide/, + :toggle => /Element\.toggle/ + } + RJS_INSERTIONS = [:top, :bottom, :before, :after] + RJS_INSERTIONS.each do |insertion| + RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}")) + end + RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") + RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion| + Regexp.quote("new Insertion.#{insertion.to_s.camelize}") + end.join('|')) + RJS_PATTERN_HTML = /"((\\"|[^"])*)"/ + RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", + Regexp::MULTILINE) + RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ + end + + # #assert_select and #css_select call this to obtain the content in the HTML + # page, or from all the RJS statements, depending on the type of response. + def response_from_page_or_rjs() + content_type = @response.content_type + + if content_type && content_type =~ /text\/javascript/ + body = @response.body.dup + root = HTML::Node.new(nil) + + while true + next if body.sub!(RJS_PATTERN_EVERYTHING) do |match| + html = unescape_rjs($3) + matches = HTML::Document.new(html).root.children.select { |n| n.tag? } + root.children.concat matches + "" + end + break + end + + root + else + html_document.root + end + end + + # Unescapes a RJS string. + def unescape_rjs(rjs_string) + # RJS encodes double quotes and line breaks. + unescaped= rjs_string.gsub('\"', '"') + unescaped.gsub!(/\\\//, '/') + unescaped.gsub!('\n', "\n") + unescaped.gsub!('\076', '>') + unescaped.gsub!('\074', '<') + # RJS encodes non-ascii characters. + unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')} + unescaped + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/tag_assertions.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/tag_assertions.rb new file mode 100644 index 000000000..4ac489520 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/assertions/tag_assertions.rb @@ -0,0 +1,130 @@ +require 'rexml/document' +require 'html/document' + +module ActionController + module Assertions + # Pair of assertions to testing elements in the HTML output of the response. + module TagAssertions + # 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+. + # + # === Examples + # + # # 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/ } + # + # <b>Please note</b>: #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.) + # + # === Examples + # # Assert that there is not a "div" containing a "p" + # assert_no_tag :tag => "div", :descendant => { :tag => "p" } + # + # # Assert that an unordered list is empty + # assert_no_tag :tag => "ul", :descendant => { :tag => "li" } + # + # # Assert that there is not a "p" tag with between 1 to 3 "img" tags + # # as immediate children + # assert_no_tag :tag => "p", + # :children => { :count => 1..3, :only => { :tag => "img" } } + 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 + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/base.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/base.rb new file mode 100755 index 000000000..ff77e036f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/base.rb @@ -0,0 +1,1295 @@ +require 'action_controller/mime_type' +require 'action_controller/request' +require 'action_controller/response' +require 'action_controller/routing' +require 'action_controller/resources' +require 'action_controller/url_rewriter' +require 'action_controller/status_codes' +require 'drb' +require 'set' + +module ActionController #:nodoc: + class ActionControllerError < StandardError #:nodoc: + end + + class SessionRestoreError < ActionControllerError #:nodoc: + end + + class MissingTemplate < ActionControllerError #:nodoc: + end + + class RenderError < ActionControllerError #:nodoc: + end + + class RoutingError < ActionControllerError #:nodoc: + attr_reader :failures + def initialize(message, failures=[]) + super(message) + @failures = failures + end + end + + class MethodNotAllowed < ActionControllerError #:nodoc: + attr_reader :allowed_methods + + def initialize(*allowed_methods) + super("Only #{allowed_methods.to_sentence} requests are allowed.") + @allowed_methods = allowed_methods + end + + def allowed_methods_header + allowed_methods.map { |method_symbol| method_symbol.to_s.upcase } * ', ' + end + + def handle_response!(response) + response.headers['Allow'] ||= allowed_methods_header + end + end + + class NotImplemented < MethodNotAllowed #:nodoc: + end + + class UnknownController < ActionControllerError #:nodoc: + end + + class UnknownAction < ActionControllerError #:nodoc: + end + + class MissingFile < ActionControllerError #:nodoc: + end + + class RenderError < ActionControllerError #:nodoc: + end + + class SessionOverflowError < ActionControllerError #:nodoc: + DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + + class DoubleRenderError < ActionControllerError #:nodoc: + DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"." + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + + class RedirectBackError < ActionControllerError #:nodoc: + DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + + class UnknownHttpMethod < ActionControllerError #:nodoc: + end + + # Action Controllers are the core of a web request in Rails. They are made up of one or more actions that are executed + # on request and then either render a template or redirect to another action. An action is defined as a public method + # on the controller, which will automatically be made accessible to the web-server through Rails Routes. + # + # A sample controller could look like this: + # + # class GuestBookController < ActionController::Base + # def index + # @entries = Entry.find(:all) + # end + # + # def sign + # Entry.create(params[:entry]) + # redirect_to :action => "index" + # end + # end + # + # Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action + # after executing code in the action. For example, the +index+ action of the +GuestBookController+ would render the + # template <tt>app/views/guestbook/index.erb</tt> by default after populating the <tt>@entries</tt> instance variable. + # + # Unlike index, the sign action will not render a template. After performing its main purpose (creating a + # new entry in the guest book), it initiates a redirect instead. This redirect works by returning an external + # "302 Moved" HTTP response that takes the user to the index action. + # + # The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # Most actions are variations of these themes. + # + # == Requests + # + # Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters. + # This value should hold the name of the action to be performed. Once the action has been identified, the remaining + # request parameters, the session (if one is available), and the full request with all the http headers are made available to + # the action through instance variables. Then the action is performed. + # + # The full request object is available with the request accessor and is primarily used to query for http headers. These queries + # are made by accessing the environment hash, like this: + # + # def server_ip + # location = request.env["SERVER_ADDR"] + # render :text => "This server hosted at #{location}" + # end + # + # == Parameters + # + # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method + # which returns a hash. For example, an action that was performed through <tt>/weblog/list?category=All&limit=5</tt> will include + # <tt>{ "category" => "All", "limit" => 5 }</tt> in params. + # + # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # + # <input type="text" name="post[name]" value="david"> + # <input type="text" name="post[address]" value="hyacintvej"> + # + # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. + # If the address input had been named "post[address][street]", the params would have included + # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. + # + # == Sessions + # + # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, + # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such + # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely + # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. + # + # You can place objects in the session by using the <tt>session</tt> method, which accesses a hash: + # + # session[:person] = Person.authenticate(user_name, password) + # + # And retrieved again through the same hash: + # + # Hello #{session[:person]} + # + # For removing objects from the session, you can either assign a single key to nil, like <tt>session[:person] = nil</tt>, or you can + # remove the entire session with reset_session. + # + # Sessions are stored in a browser cookie that's cryptographically signed, but unencrypted, by default. This prevents + # the user from tampering with the session but also allows him to see its contents. + # + # Do not put secret information in session! + # + # Other options for session storage are: + # + # ActiveRecordStore: sessions are stored in your database, which works better than PStore with multiple app servers and, + # unlike CookieStore, hides your session contents from the user. To use ActiveRecordStore, set + # + # config.action_controller.session_store = :active_record_store + # + # in your <tt>environment.rb</tt> and run <tt>rake db:sessions:create</tt>. + # + # MemCacheStore: sessions are stored as entries in your memcached cache. Set the session store type in <tt>environment.rb</tt>: + # + # config.action_controller.session_store = :mem_cache_store + # + # This assumes that memcached has been installed and configured properly. See the MemCacheStore docs for more information. + # + # == Responses + # + # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response + # object is generated automatically through the use of renders and redirects and requires no user intervention. + # + # == Renders + # + # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering + # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured. + # The controller passes objects to the view by assigning instance variables: + # + # def show + # @post = Post.find(params[:id]) + # end + # + # Which are then automatically available to the view: + # + # Title: <%= @post.title %> + # + # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use + # the manual rendering methods: + # + # def search + # @results = Search.find(params[:query]) + # case @results + # when 0 then render :action => "no_results" + # when 1 then render :action => "show" + # when 2..10 then render :action => "show_many" + # end + # end + # + # Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html. + # + # == Redirects + # + # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to a database, + # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to) + # a <tt>show</tt> action that we'll assume has already been created. The code might look like this: + # + # def create + # @entry = Entry.new(params[:entry]) + # if @entry.save + # # The entry was saved correctly, redirect to show + # redirect_to :action => 'show', :id => @entry.id + # else + # # things didn't go so well, do something else + # end + # end + # + # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method which is then executed. + # + # == Calling multiple redirects or renders + # + # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError: + # + # def do_something + # redirect_to :action => "elsewhere" + # render :action => "overthere" # raises DoubleRenderError + # end + # + # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution. + # + # def do_something + # redirect_to(:action => "elsewhere") and return if monkeys.nil? + # render :action => "overthere" # won't be called unless monkeys is nil + # end + # + class Base + DEFAULT_RENDER_STATUS_CODE = "200 OK" + + include StatusCodes + + # Determines whether the view has access to controller internals @request, @response, @session, and @template. + # By default, it does. + @@view_controller_internals = true + cattr_accessor :view_controller_internals + + # Protected instance variable cache + @@protected_variables_cache = nil + cattr_accessor :protected_variables_cache + + # Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets, + # and images to a dedicated asset server away from the main web server. Example: + # ActionController::Base.asset_host = "http://assets.example.com" + @@asset_host = "" + cattr_accessor :asset_host + + # All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors. + # When the application is ready to go public, this should be set to false, and the protected method <tt>local_request?</tt> + # should instead be implemented in the controller to determine when debugging screens should be shown. + @@consider_all_requests_local = true + cattr_accessor :consider_all_requests_local + + # Enable or disable the collection of failure information for RoutingErrors. + # This information can be extremely useful when tweaking custom routes, but is + # pointless once routes have been tested and verified. + @@debug_routes = true + cattr_accessor :debug_routes + + # Controls whether the application is thread-safe, so multi-threaded servers like WEBrick know whether to apply a mutex + # around the performance of each action. Action Pack and Active Record are by default thread-safe, but many applications + # may not be. Turned off by default. + @@allow_concurrency = false + cattr_accessor :allow_concurrency + + # Modern REST web services often need to submit complex data to the web application. + # The param_parsers hash lets you register handlers which will process the http body and add parameters to the + # <tt>params</tt> hash. These handlers are invoked for post and put requests. + # + # By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instantiated + # in the <tt>params</tt>. This allows XML requests to mask themselves as regular form submissions, so you can have one + # action serve both regular forms and web service requests. + # + # Example of doing your own parser for a custom content type: + # + # ActionController::Base.param_parsers[Mime::Type.lookup('application/atom+xml')] = Proc.new do |data| + # node = REXML::Document.new(post) + # { node.root.name => node.root } + # end + # + # Note: Up until release 1.1 of Rails, Action Controller would default to using XmlSimple configured to discard the + # root node for such requests. The new default is to keep the root, such that "<r><name>David</name></r>" results + # in params[:r][:name] for "David" instead of params[:name]. To get the old behavior, you can + # re-register XmlSimple as application/xml handler ike this: + # + # ActionController::Base.param_parsers[Mime::XML] = + # Proc.new { |data| XmlSimple.xml_in(data, 'ForceArray' => false) } + # + # A YAML parser is also available and can be turned on with: + # + # ActionController::Base.param_parsers[Mime::YAML] = :yaml + @@param_parsers = { Mime::MULTIPART_FORM => :multipart_form, + Mime::URL_ENCODED_FORM => :url_encoded_form, + Mime::XML => :xml_simple } + cattr_accessor :param_parsers + + # Controls the default charset for all renders. + @@default_charset = "utf-8" + cattr_accessor :default_charset + + # The logger is used for generating information on the action run-time (including benchmarking) if available. + # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. + cattr_accessor :logger + + # Determines which template class should be used by ActionController. + cattr_accessor :template_class + + # Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates. + cattr_accessor :ignore_missing_templates + + # Controls the resource action separator + @@resource_action_separator = "/" + cattr_accessor :resource_action_separator + + # Sets the token parameter name for RequestForgery. Calling #protect_from_forgery sets it to :authenticity_token by default + cattr_accessor :request_forgery_protection_token + + # Indicates whether or not optimise the generated named + # route helper methods + cattr_accessor :optimise_named_routes + self.optimise_named_routes = true + + # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. + class_inheritable_accessor :allow_forgery_protection + self.allow_forgery_protection = true + + # Holds the request object that's primarily used to get environment variables through access like + # <tt>request.env["REQUEST_URI"]</tt>. + attr_internal :request + + # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>params["post_id"]</tt> + # to get the post_id. No type casts are made, so all values are returned as strings. + attr_internal :params + + # Holds the response object that's primarily used to set additional HTTP headers through access like + # <tt>response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template + # has been rendered through response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output, + # such as a OutputCompressionFilter. + attr_internal :response + + # Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> to get the object tied to the "person" + # key. The session will hold any type of object as values, but the key should be a string or symbol. + attr_internal :session + + # Holds a hash of header names and values. Accessed like <tt>headers["Cache-Control"]</tt> to get the value of the Cache-Control + # directive. Values should always be specified as strings. + attr_internal :headers + + # Holds the hash of variables that are passed on to the template class to be made available to the view. This hash + # is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered. + attr_accessor :assigns + + # Returns the name of the action this controller is processing. + attr_accessor :action_name + + # Templates that are exempt from layouts + @@exempt_from_layout = Set.new([/\.rjs$/]) + + class << self + # Factory for the standard create, process loop where the controller is discarded after processing. + def process(request, response) #:nodoc: + new.process(request, response) + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController". + def controller_class_name + @controller_class_name ||= name.demodulize + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat". + def controller_name + @controller_name ||= controller_class_name.sub(/Controller$/, '').underscore + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "one_module/two_module/neat". + def controller_path + @controller_path ||= name.gsub(/Controller$/, '').underscore + end + + # Return an array containing the names of public methods that have been marked hidden from the action processor. + # By default, all methods defined in ActionController::Base and included modules are hidden. + # More methods can be hidden using <tt>hide_actions</tt>. + def hidden_actions + unless read_inheritable_attribute(:hidden_actions) + write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map(&:to_s)) + end + + read_inheritable_attribute(:hidden_actions) + end + + # Hide each of the given methods from being callable as actions. + def hide_action(*names) + write_inheritable_attribute(:hidden_actions, hidden_actions | names.map(&:to_s)) + end + + ## View load paths determine the bases from which template references can be made. So a call to + ## render("test/template") will be looked up in the view load paths array and the closest match will be + ## returned. + def view_paths + @view_paths || superclass.view_paths + end + + def view_paths=(value) + @view_paths = value + end + + # Adds a view_path to the front of the view_paths array. + # If the current class has no view paths, copy them from + # the superclass. This change will be visible for all future requests. + # + # ArticleController.prepend_view_path("views/default") + # ArticleController.prepend_view_path(["views/default", "views/custom"]) + # + def prepend_view_path(path) + @view_paths = superclass.view_paths.dup if @view_paths.nil? + view_paths.unshift(*path) + end + + # Adds a view_path to the end of the view_paths array. + # If the current class has no view paths, copy them from + # the superclass. This change will be visible for all future requests. + # + # ArticleController.append_view_path("views/default") + # ArticleController.append_view_path(["views/default", "views/custom"]) + # + def append_view_path(path) + @view_paths = superclass.view_paths.dup if @view_paths.nil? + view_paths.push(*path) + end + + # Replace sensitive parameter data from the request log. + # Filters parameters that have any of the arguments as a substring. + # Looks in all subhashes of the param hash for keys to filter. + # If a block is given, each key and value of the parameter hash and all + # subhashes is passed to it, the value or key + # can be replaced using String#replace or similar method. + # + # Examples: + # filter_parameter_logging + # => Does nothing, just slows the logging process down + # + # filter_parameter_logging :password + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # filter_parameter_logging :foo, "bar" + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i + # + # filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i, and + # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + def filter_parameter_logging(*filter_words, &block) + parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 + + define_method(:filter_parameters) do |unfiltered_parameters| + filtered_parameters = {} + + unfiltered_parameters.each do |key, value| + if key =~ parameter_filter + filtered_parameters[key] = '[FILTERED]' + elsif value.is_a?(Hash) + filtered_parameters[key] = filter_parameters(value) + elsif block_given? + key = key.dup + value = value.dup if value + yield key, value + filtered_parameters[key] = value + else + filtered_parameters[key] = value + end + end + + filtered_parameters + end + end + + # Don't render layouts for templates with the given extensions. + def exempt_from_layout(*extensions) + regexps = extensions.collect do |extension| + extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/ + end + @@exempt_from_layout.merge regexps + end + end + + public + # Extracts the action_name from the request parameters and performs that action. + def process(request, response, method = :perform_action, *arguments) #:nodoc: + initialize_template_class(response) + assign_shortcuts(request, response) + initialize_current_url + assign_names + forget_variables_added_to_assigns + + log_processing + send(method, *arguments) + + assign_default_content_type_and_charset + + response.request = request + response.prepare! unless component_request? + response + ensure + process_cleanup + end + + # Returns a URL that has been rewritten according to the options hash and the defined Routes. + # (For doing a complete redirect, use redirect_to). + # Â + # <tt>url_for</tt> is used to: + # Â + # All keys given to url_for are forwarded to the Route module, save for the following: + # * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path. For example, + # <tt>url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'</tt> + # will produce "/posts/show/10#comments". + # * <tt>:only_path</tt> -- if true, returns the relative URL (omitting the protocol, host name, and port) (<tt>false</tt> by default) + # * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this + # is currently not recommended since it breaks caching. + # * <tt>:host</tt> -- overrides the default (current) host if provided. + # * <tt>:protocol</tt> -- overrides the default (current) protocol if provided. + # * <tt>:port</tt> -- optionally specify the port to connect to. + # * <tt>:user</tt> -- Inline HTTP authentication (only plucked out if :password is also present). + # * <tt>:password</tt> -- Inline HTTP authentication (only plucked out if :user is also present). + # * <tt>:skip_relative_url_root</tt> -- if true, the url is not constructed using the relative_url_root of the request so the path + # will include the web server relative installation directory. + # + # The URL is generated from the remaining keys in the hash. A URL contains two key parts: the <base> and a query string. + # Routes composes a query string as the key/value pairs not included in the <base>. + # + # The default Routes setup supports a typical Rails path of "controller/action/id" where action and id are optional, with + # action defaulting to 'index' when not given. Here are some typical url_for statements and their corresponding URLs: + # + # url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent' + # url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts' + # url_for :controller => 'posts', :action => 'index', :port=>'8033' # => 'proto://host.com:8033/posts' + # url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10' + # url_for :controller => 'posts', :user => 'd', :password => '123' # => 'proto://d:123@host.com/posts' + # + # When generating a new URL, missing values may be filled in from the current request's parameters. For example, + # <tt>url_for :action => 'some_action'</tt> will retain the current controller, as expected. This behavior extends to + # other parameters, including <tt>:controller</tt>, <tt>:id</tt>, and any other parameters that are placed into a Route's + # path. + # Â + # The URL helpers such as <tt>url_for</tt> have a limited form of memory: when generating a new URL, they can look for + # missing values in the current request's parameters. Routes attempts to guess when a value should and should not be + # taken from the defaults. There are a few simple rules on how this is performed: + # + # * If the controller name begins with a slash, no defaults are used: <tt>url_for :controller => '/home'</tt> + # * If the controller changes, the action will default to index unless provided + # + # The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the + # route given by <tt>map.connect 'people/:last/:first/:action', :action => 'bio', :controller => 'people'</tt>. + # + # Suppose that the current URL is "people/hh/david/contacts". Let's consider a few different cases of URLs which are generated + # from this page. + # + # * <tt>url_for :action => 'bio'</tt> -- During the generation of this URL, default values will be used for the first and + # last components, and the action shall change. The generated URL will be, "people/hh/david/bio". + # * <tt>url_for :first => 'davids-little-brother'</tt> This generates the URL 'people/hh/davids-little-brother' -- note + # that this URL leaves out the assumed action of 'bio'. + # + # However, you might ask why the action from the current request, 'contacts', isn't carried over into the new URL. The + # answer has to do with the order in which the parameters appear in the generated path. In a nutshell, since the + # value that appears in the slot for <tt>:first</tt> is not equal to default value for <tt>:first</tt> we stop using + # defaults. On its own, this rule can account for much of the typical Rails URL behavior. + # Â + # Although a convenience, defaults can occasionally get in your way. In some cases a default persists longer than desired. + # The default may be cleared by adding <tt>:name => nil</tt> to <tt>url_for</tt>'s options. + # This is often required when writing form helpers, since the defaults in play may vary greatly depending upon where the + # helper is used from. The following line will redirect to PostController's default action, regardless of the page it is + # displayed on: + # + # url_for :controller => 'posts', :action => nil + # + # If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the + # :overwrite_params options. Say for your posts you have different views for showing and printing them. + # Then, in the show view, you get the URL for the print view like this + # + # url_for :overwrite_params => { :action => 'print' } + # + # This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt> + # would have slashed-off the path components after the changed action. + def url_for(options = nil) #:doc: + case options || {} + when String + options + when Hash + @url.rewrite(rewrite_options(options)) + else + polymorphic_url(options) + end + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController". + def controller_class_name + self.class.controller_class_name + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat". + def controller_name + self.class.controller_name + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "one_module/two_module/neat". + def controller_path + self.class.controller_path + end + + def session_enabled? + request.session_options && request.session_options[:disabled] != false + end + + self.view_paths = [] + + # View load paths for controller. + def view_paths + (@template || self.class).view_paths + end + + def view_paths=(value) + (@template || self.class).view_paths = value + end + + # Adds a view_path to the front of the view_paths array. + # This change affects the current request only. + # + # self.prepend_view_path("views/default") + # self.prepend_view_path(["views/default", "views/custom"]) + # + def prepend_view_path(path) + (@template || self.class).prepend_view_path(path) + end + + # Adds a view_path to the end of the view_paths array. + # This change affects the current request only. + # + # self.append_view_path("views/default") + # self.append_view_path(["views/default", "views/custom"]) + # + def append_view_path(path) + (@template || self.class).append_view_path(path) + end + + protected + # Renders the content that will be returned to the browser as the response body. + # + # === Rendering an action + # + # Action rendering is the most common form and the type used automatically by Action Controller when nothing else is + # specified. By default, actions are rendered within the current layout (if one exists). + # + # # Renders the template for the action "goal" within the current controller + # render :action => "goal" + # + # # Renders the template for the action "short_goal" within the current controller, + # # but without the current active layout + # render :action => "short_goal", :layout => false + # + # # Renders the template for the action "long_goal" within the current controller, + # # but with a custom layout + # render :action => "long_goal", :layout => "spectacular" + # + # === Rendering partials + # + # Partial rendering in a controller is most commonly used together with Ajax calls that only update one or a few elements on a page + # without reloading. Rendering of partials from the controller makes it possible to use the same partial template in + # both the full-page rendering (by calling it from within the template) and when sub-page updates happen (from the + # controller action responding to Ajax calls). By default, the current layout is not used. + # + # # Renders the same partial with a local variable. + # render :partial => "person", :locals => { :name => "david" } + # + # # Renders the partial, making @new_person available through + # # the local variable 'person' + # render :partial => "person", :object => @new_person + # + # # Renders a collection of the same partial by making each element + # # of @winners available through the local variable "person" as it + # # builds the complete response. + # render :partial => "person", :collection => @winners + # + # # Renders the same collection of partials, but also renders the + # # person_divider partial between each person partial. + # render :partial => "person", :collection => @winners, :spacer_template => "person_divider" + # + # # Renders a collection of partials located in a view subfolder + # # outside of our current controller. In this example we will be + # # rendering app/views/shared/_note.r(html|xml) Inside the partial + # # each element of @new_notes is available as the local var "note". + # render :partial => "shared/note", :collection => @new_notes + # + # # Renders the partial with a status code of 500 (internal error). + # render :partial => "broken", :status => 500 + # + # Note that the partial filename must also be a valid Ruby variable name, + # so e.g. 2005 and register-user are invalid. + # + # + # == Automatic etagging + # + # Rendering will automatically insert the etag header on 200 OK responses. The etag is calculated using MD5 of the + # response body. If a request comes in that has a matching etag, the response will be changed to a 304 Not Modified + # and the response body will be set to an empty string. No etag header will be inserted if it's already set. + # + # === Rendering a template + # + # Template rendering works just like action rendering except that it takes a path relative to the template root. + # The current layout is automatically applied. + # + # # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.erb) + # render :template => "weblog/show" + # + # === Rendering a file + # + # File rendering works just like action rendering except that it takes a filesystem path. By default, the path + # is assumed to be absolute, and the current layout is not applied. + # + # # Renders the template located at the absolute filesystem path + # render :file => "/path/to/some/template.erb" + # render :file => "c:/path/to/some/template.erb" + # + # # Renders a template within the current layout, and with a 404 status code + # render :file => "/path/to/some/template.erb", :layout => true, :status => 404 + # render :file => "c:/path/to/some/template.erb", :layout => true, :status => 404 + # + # # Renders a template relative to the template root and chooses the proper file extension + # render :file => "some/template", :use_full_path => true + # + # === Rendering text + # + # Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text + # rendering is not done within the active layout. + # + # # Renders the clear text "hello world" with status code 200 + # render :text => "hello world!" + # + # # Renders the clear text "Explosion!" with status code 500 + # render :text => "Explosion!", :status => 500 + # + # # Renders the clear text "Hi there!" within the current active layout (if one exists) + # render :text => "Hi there!", :layout => true + # + # # Renders the clear text "Hi there!" within the layout + # # placed in "app/views/layouts/special.r(html|xml)" + # render :text => "Hi there!", :layout => "special" + # + # The :text option can also accept a Proc object, which can be used to manually control the page generation. This should + # generally be avoided, as it violates the separation between code and content, and because almost everything that can be + # done with this method can also be done more cleanly using one of the other rendering methods, most notably templates. + # + # # Renders "Hello from code!" + # render :text => proc { |response, output| output.write("Hello from code!") } + # + # === Rendering JSON + # + # Rendering JSON sets the content type to application/json and optionally wraps the JSON in a callback. It is expected + # that the response will be parsed (or eval'd) for use as a data structure. + # + # # Renders '{"name": "David"}' + # render :json => {:name => "David"}.to_json + # + # It's not necessary to call <tt>to_json</tt> on the object you want to render, since <tt>render</tt> will + # automatically do that for you: + # + # # Also renders '{"name": "David"}' + # render :json => {:name => "David"} + # + # Sometimes the result isn't handled directly by a script (such as when the request comes from a SCRIPT tag), + # so the <tt>:callback</tt> option is provided for these cases. + # + # # Renders 'show({"name": "David"})' + # render :json => {:name => "David"}.to_json, :callback => 'show' + # + # === Rendering an inline template + # + # Rendering of an inline template works as a cross between text and action rendering where the source for the template + # is supplied inline, like text, but its interpreted with ERb or Builder, like action. By default, ERb is used for rendering + # and the current layout is not used. + # + # # Renders "hello, hello, hello, again" + # render :inline => "<%= 'hello, ' * 3 + 'again' %>" + # + # # Renders "<p>Good seeing you!</p>" using Builder + # render :inline => "xml.p { 'Good seeing you!' }", :type => :builder + # + # # Renders "hello david" + # render :inline => "<%= 'hello ' + name %>", :locals => { :name => "david" } + # + # === Rendering inline JavaScriptGenerator page updates + # + # In addition to rendering JavaScriptGenerator page updates with Ajax in RJS templates (see ActionView::Base for details), + # you can also pass the <tt>:update</tt> parameter to +render+, along with a block, to render page updates inline. + # + # render :update do |page| + # page.replace_html 'user_list', :partial => 'user', :collection => @users + # page.visual_effect :highlight, 'user_list' + # end + # + # === Rendering with status and location headers + # + # All renders take the :status and :location options and turn them into headers. They can even be used together: + # + # render :xml => post.to_xml, :status => :created, :location => post_url(post) + def render(options = nil, &block) #:doc: + raise DoubleRenderError, "Can only render or redirect once per action" if performed? + + if options.nil? + return render_for_file(default_template_name, nil, true) + else + if options == :update + options = { :update => true } + elsif !options.is_a?(Hash) + raise RenderError, "You called render with invalid options : #{options}" + end + end + + if content_type = options[:content_type] + response.content_type = content_type.to_s + end + + if location = options[:location] + response.headers["Location"] = url_for(location) + end + + if text = options[:text] + render_for_text(text, options[:status]) + + else + if file = options[:file] + render_for_file(file, options[:status], options[:use_full_path], options[:locals] || {}) + + elsif template = options[:template] + render_for_file(template, options[:status], true) + + elsif inline = options[:inline] + add_variables_to_assigns + render_for_text(@template.render_template(options[:type], inline, nil, options[:locals] || {}), options[:status]) + + elsif action_name = options[:action] + template = default_template_name(action_name.to_s) + if options[:layout] && !template_exempt_from_layout?(template) + render_with_a_layout(:file => template, :status => options[:status], :use_full_path => true, :layout => true) + else + render_with_no_layout(:file => template, :status => options[:status], :use_full_path => true) + end + + elsif xml = options[:xml] + response.content_type ||= Mime::XML + render_for_text(xml.respond_to?(:to_xml) ? xml.to_xml : xml, options[:status]) + + elsif json = options[:json] + json = json.to_json unless json.is_a?(String) + json = "#{options[:callback]}(#{json})" unless options[:callback].blank? + response.content_type ||= Mime::JSON + render_for_text(json, options[:status]) + + elsif partial = options[:partial] + partial = default_template_name if partial == true + add_variables_to_assigns + + if collection = options[:collection] + render_for_text( + @template.send!(:render_partial_collection, partial, collection, + options[:spacer_template], options[:locals]), options[:status] + ) + else + render_for_text( + @template.send!(:render_partial, partial, + ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]), options[:status] + ) + end + + elsif options[:update] + add_variables_to_assigns + @template.send! :evaluate_assigns + + generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block) + response.content_type = Mime::JS + render_for_text(generator.to_s) + + elsif options[:nothing] + # Safari doesn't pass the headers of the return if the response is zero length + render_for_text(" ", options[:status]) + + else + render_for_file(default_template_name, options[:status], true) + end + end + end + + # Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead + # of sending it as the response body to the browser. + def render_to_string(options = nil, &block) #:doc: + render(options, &block) + ensure + erase_render_results + forget_variables_added_to_assigns + reset_variables_added_to_assigns + end + + # Return a response that has no content (merely headers). The options + # argument is interpreted to be a hash of header names and values. + # This allows you to easily return a response that consists only of + # significant headers: + # + # head :created, :location => person_path(@person) + # + # It can also be used to return exceptional conditions: + # + # return head(:method_not_allowed) unless request.post? + # return head(:bad_request) unless valid_request? + # render + def head(*args) + if args.length > 2 + raise ArgumentError, "too many arguments to head" + elsif args.empty? + raise ArgumentError, "too few arguments to head" + end + options = args.extract_options! + status = interpret_status(args.shift || options.delete(:status) || :ok) + + options.each do |key, value| + headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + end + + render :nothing => true, :status => status + end + + + # Clears the rendered results, allowing for another render to be performed. + def erase_render_results #:nodoc: + response.body = nil + @performed_render = false + end + + # Clears the redirected results from the headers, resets the status to 200 and returns + # the URL that was used to redirect or nil if there was no redirected URL + # Note that +redirect_to+ will change the body of the response to indicate a redirection. + # The response body is not reset here, see +erase_render_results+ + def erase_redirect_results #:nodoc: + @performed_redirect = false + response.redirected_to = nil + response.redirected_to_method_params = nil + response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE + response.headers.delete('Location') + end + + # Erase both render and redirect results + def erase_results #:nodoc: + erase_render_results + erase_redirect_results + end + + def rewrite_options(options) #:nodoc: + if defaults = default_url_options(options) + defaults.merge(options) + else + options + end + end + + # Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in + # the form of a hash, just like the one you would use for url_for directly. Example: + # + # def default_url_options(options) + # { :project => @project.active? ? @project.url_name : "unknown" } + # end + # + # As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the + # urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set + # by this method. + def default_url_options(options) #:doc: + end + + # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: + # + # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. + # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. + # * <tt>String starting with protocol:// (like http://)</tt> - Is passed straight through as the target for redirection. + # * <tt>String not containing a protocol</tt> - The current protocol and host is prepended to the string. + # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. + # Short-hand for redirect_to(request.env["HTTP_REFERER"]) + # + # Examples: + # redirect_to :action => "show", :id => 5 + # redirect_to post + # redirect_to "http://www.rubyonrails.org" + # redirect_to "/images/screenshot.jpg" + # redirect_to articles_url + # redirect_to :back + # + # The redirection happens as a "302 Moved" header unless otherwise specified. + # + # Examples: + # redirect_to post_url(@post), :status=>:found + # redirect_to :action=>'atom', :status=>:moved_permanently + # redirect_to post_url(@post), :status=>301 + # redirect_to :action=>'atom', :status=>302 + # + # When using <tt>redirect_to :back</tt>, if there is no referrer, + # RedirectBackError will be raised. You may specify some fallback + # behavior for this case by rescuing RedirectBackError. + def redirect_to(options = {}, response_status = {}) #:doc: + + if options.is_a?(Hash) && options[:status] + status = options.delete(:status) + elsif response_status[:status] + status = response_status[:status] + else + status = 302 + end + + case options + when %r{^\w+://.*} + raise DoubleRenderError if performed? + logger.info("Redirected to #{options}") if logger && logger.info? + response.redirect(options, interpret_status(status)) + response.redirected_to = options + @performed_redirect = true + + when String + redirect_to(request.protocol + request.host_with_port + options, :status=>status) + + when :back + request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"], :status=>status) : raise(RedirectBackError) + + when Hash + redirect_to(url_for(options), :status=>status) + response.redirected_to = options + + else + redirect_to(url_for(options), :status=>status) + end + end + + # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that + # intermediate caches shouldn't cache the response. + # + # Examples: + # expires_in 20.minutes + # expires_in 3.hours, :private => false + # expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true + # + # This method will overwrite an existing Cache-Control header. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. + def expires_in(seconds, options = {}) #:doc: + cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) + cache_options.delete_if { |k,v| v.nil? or v == false } + cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} + response.headers["Cache-Control"] = cache_control.join(', ') + end + + # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or + # intermediate caches (like caching proxy servers). + def expires_now #:doc: + response.headers["Cache-Control"] = "no-cache" + end + + # Resets the session by clearing out all the objects stored within and initializing a new session object. + def reset_session #:doc: + request.reset_session + @_session = request.session + response.session = @_session + end + + + private + def render_for_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc: + add_variables_to_assigns + assert_existence_of_template_file(template_path) if use_full_path + logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger + render_for_text(@template.render_file(template_path, use_full_path, locals), status) + end + + def render_for_text(text = nil, status = nil, append_response = false) #:nodoc: + @performed_render = true + + response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE) + + if append_response + response.body ||= '' + response.body << text.to_s + else + response.body = text.is_a?(Proc) ? text : text.to_s + end + end + + def initialize_template_class(response) + unless @@template_class + raise "You must assign a template class through ActionController.template_class= before processing a request" + end + + response.template = ActionView::Base.new(view_paths, {}, self) + response.template.extend self.class.master_helper_module + response.redirected_to = nil + @performed_render = @performed_redirect = false + end + + def assign_shortcuts(request, response) + @_request, @_params, @_cookies = request, request.parameters, request.cookies + + @_response = response + @_response.session = request.session + + @_session = @_response.session + @template = @_response.template + @assigns = @_response.template.assigns + + @_headers = @_response.headers + end + + def initialize_current_url + @url = UrlRewriter.new(request, params.clone) + end + + def log_processing + if logger && logger.info? + logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]" + logger.info " Session ID: #{@_session.session_id}" if @_session and @_session.respond_to?(:session_id) + logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}" + end + end + + def default_render #:nodoc: + render + end + + def perform_action + if self.class.action_methods.include?(action_name) + send(action_name) + default_render unless performed? + elsif respond_to? :method_missing + method_missing action_name + default_render unless performed? + elsif template_exists? && template_public? + default_render + else + raise UnknownAction, "No action responded to #{action_name}", caller + end + end + + def performed? + @performed_render || @performed_redirect + end + + def assign_names + @action_name = (params['action'] || 'index') + end + + def assign_default_content_type_and_charset + response.content_type ||= Mime::HTML + response.charset ||= self.class.default_charset unless sending_file? + end + + def sending_file? + response.headers["Content-Transfer-Encoding"] == "binary" + end + + def action_methods + self.class.action_methods + end + + def self.action_methods + @action_methods ||= Set.new(public_instance_methods.map(&:to_s)) - hidden_actions + end + + def add_variables_to_assigns + unless @variables_added + add_instance_variables_to_assigns + add_class_variables_to_assigns if view_controller_internals + @variables_added = true + end + end + + def forget_variables_added_to_assigns + @variables_added = nil + end + + def reset_variables_added_to_assigns + @template.instance_variable_set("@assigns_added", nil) + end + + def add_instance_variables_to_assigns + @@protected_variables_cache ||= Set.new(protected_instance_variables) + instance_variables.each do |var| + next if @@protected_variables_cache.include?(var) + @assigns[var[1..-1]] = instance_variable_get(var) + end + end + + def add_class_variables_to_assigns + %w(view_paths logger template_class ignore_missing_templates).each do |cvar| + @assigns[cvar] = self.send(cvar) + end + end + + def protected_instance_variables + if view_controller_internals + %w(@assigns @performed_redirect @performed_render) + else + %w(@assigns @performed_redirect @performed_render + @_request @request @_response @response @_params @params + @_session @session @_cookies @cookies + @template @request_origin @parent_controller) + end + end + + def request_origin + # this *needs* to be cached! + # otherwise you'd get different results if calling it more than once + @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}" + end + + def complete_request_uri + "#{request.protocol}#{request.host}#{request.request_uri}" + end + + def close_session + @_session.close if @_session && @_session.respond_to?(:close) + end + + def template_exists?(template_name = default_template_name) + @template.file_exists?(template_name) + end + + def template_public?(template_name = default_template_name) + @template.file_public?(template_name) + end + + def template_exempt_from_layout?(template_name = default_template_name) + extension = @template && @template.pick_template_extension(template_name) + name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name + @@exempt_from_layout.any? { |ext| name_with_extension =~ ext } + end + + def assert_existence_of_template_file(template_name) + unless template_exists?(template_name) || ignore_missing_templates + full_template_path = template_name.include?('.') ? template_name : "#{template_name}.#{@template.template_format}.erb" + display_paths = view_paths.join(':') + template_type = (template_name =~ /layouts/i) ? 'layout' : 'template' + raise(MissingTemplate, "Missing #{template_type} #{full_template_path} in view path #{display_paths}") + end + end + + def default_template_name(action_name = self.action_name) + if action_name + action_name = action_name.to_s + if action_name.include?('/') && template_path_includes_controller?(action_name) + action_name = strip_out_controller(action_name) + end + end + "#{self.class.controller_path}/#{action_name}" + end + + def strip_out_controller(path) + path.split('/', 2).last + end + + def template_path_includes_controller?(path) + self.class.controller_path.split('/')[-1] == path.split('/')[0] + end + + def process_cleanup + close_session + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/benchmarking.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/benchmarking.rb new file mode 100644 index 000000000..5201d31b8 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/benchmarking.rb @@ -0,0 +1,94 @@ +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_chain :perform_action, :benchmark + alias_method_chain :render, :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 + + protected + def render_with_benchmark(options = nil, deprecated_status = nil, &block) + unless logger + render_without_benchmark(options, &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, &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 + + private + 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 defined?(@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) + response.headers["X-Runtime"] = sprintf("%.5f", runtime) + end + end + + def rendering_runtime(runtime) + percentage = @rendering_runtime * 100 / runtime + " | Rendering: %.5f (%d%%)" % [@rendering_runtime, percentage.to_i] + 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: %.5f (%d%%)" % [db_runtime, db_percentage.to_i] + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/caching.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/caching.rb new file mode 100644 index 000000000..e3e48f660 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/caching.rb @@ -0,0 +1,683 @@ +require 'fileutils' +require 'uri' +require 'set' + +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.class_eval do + include Pages, Actions, Fragments + + if defined? ActiveRecord + include Sweeping, SqlCache + end + + @@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.exist?(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 = actions.map(&:to_s) + after_filter { |c| c.cache_page if actions.include?(c.action_name) } + end + + private + def page_cache_file(path) + name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/')) + 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.is_a?(Hash) + 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 + else + self.class.expire_page(options) + 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 requested url is used. Example: + # cache_page "I'm the cached content", :controller => "lists", :action => "show" + def cache_page(content = nil, options = nil) + return unless perform_caching && caching_allowed + + path = case options + when Hash + url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) + when String + options + else + request.path + end + + self.class.cache_page(content || response.body, path) + end + + private + def caching_allowed + request.get? && response.headers['Status'].to_i == 200 + 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. + # + # Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt> + # are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that <tt>:action => 'lists'</tt> is not the same + # as <tt>:action => 'list', :format => :xml</tt>. + # + # You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy + # for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance. + # + # class ListsController < ApplicationController + # before_filter :authenticate, :except => :public + # caches_page :public + # caches_action :show, :cache_path => { :project => 1 } + # caches_action :show, :cache_path => Proc.new { |controller| + # controller.params[:user_id] ? + # controller.send(:user_list_url, c.params[:user_id], c.params[:id]) : + # controller.send(:list_url, c.params[:id]) } + # end + module Actions + def self.included(base) #:nodoc: + base.extend(ClassMethods) + base.class_eval do + attr_accessor :rendered_action_cache, :action_cache_path + alias_method_chain :protected_instance_variables, :action_caching + end + end + + module ClassMethods + # Declares that +actions+ should be cached. + # See ActionController::Caching::Actions for details. + def caches_action(*actions) + return unless perform_caching + around_filter(ActionCacheFilter.new(*actions)) + end + end + + def protected_instance_variables_with_action_caching + protected_instance_variables_without_action_caching + %w(@action_cache_path) + end + + def expire_action(options = {}) + return unless perform_caching + if options[:action].is_a?(Array) + options[:action].dup.each do |action| + expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }))) + end + else + expire_fragment(ActionCachePath.path_for(self, options)) + end + end + + class ActionCacheFilter #:nodoc: + def initialize(*actions, &block) + @options = actions.extract_options! + @actions = Set.new actions + end + + def before(controller) + return unless @actions.include?(controller.action_name.intern) + cache_path = ActionCachePath.new(controller, path_options_for(controller, @options)) + if cache = controller.read_fragment(cache_path.path) + controller.rendered_action_cache = true + set_content_type!(controller, cache_path.extension) + controller.send!(:render_for_text, cache) + false + else + controller.action_cache_path = cache_path + end + end + + def after(controller) + return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache || !caching_allowed(controller) + controller.write_fragment(controller.action_cache_path.path, controller.response.body) + end + + private + def set_content_type!(controller, extension) + controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension + end + + def path_options_for(controller, options) + ((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {} + end + + def caching_allowed(controller) + controller.request.get? && controller.response.headers['Status'].to_i == 200 + end + end + + class ActionCachePath + attr_reader :path, :extension + + class << self + def path_for(controller, options) + new(controller, options).path + end + end + + def initialize(controller, options = {}) + @extension = extract_extension(controller.request.path) + path = controller.url_for(options).split('://').last + normalize!(path) + add_extension!(path, @extension) + @path = URI.unescape(path) + end + + private + def normalize!(path) + path << 'index' if path[-1] == ?/ + end + + def add_extension!(path, extension) + path << ".#{extension}" if extension + end + + def extract_extension(file_path) + # Don't want just what comes after the last '.' to accommodate multi part extensions + # such as tar.gz. + file_path[/^[^.]+\.(.+)$/, 1] + 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 :partial => "topic", :collection => Topic.find(:all) %> + # <% end %> + # + # This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would + # be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>. + # + # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using + # <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like: + # + # <% cache(:action => "list", :action_suffix => "all_topics") do %> + # + # That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a + # different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique + # cache names that we can refer to when we need to expire the cache. + # + # The expiration call for this example is: + # + # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics") + # + # == Fragment stores + # + # By default, cached fragments are stored in memory. The available store options are: + # + # * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and allows all + # processes running from the same application directory to access the cached content. + # * 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.included(base) #:nodoc: + base.class_eval do + @@fragment_cache_store = MemoryStore.new + cattr_reader :fragment_cache_store + + # Defines the storage option for cached fragments + 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 + + # Given a name (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading, + # writing, or expiring a cached fragment. If the name is a hash, the generated name is the return + # value of url_for on that hash (without the protocol). + 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(ActionView::Base.erb_variable, 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 + + # Writes <tt>content</tt> to the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats) + 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 + + # Reads a cached fragment from the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats) + 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 + # Regexp expiration is only supported on caches that can iterate over + # all keys (unlike memcached). + 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 + + + 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.module_eval { 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 + + begin + require_library_or_gem 'memcache' + 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 + rescue LoadError + # MemCache wasn't available so neither can the store be + 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 SystemCallError => 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.exist?(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.module_eval { 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.included(base) #:nodoc: + base.extend(ClassMethods) + end + + module ClassMethods #:nodoc: + def cache_sweeper(*sweepers) + return unless perform_caching + configuration = sweepers.extract_options! + sweepers.each do |sweeper| + ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base) + 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 + + 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 + + protected + # gets the action cache path for the given options. + def action_path_for(options) + ActionController::Caching::Actions::ActionCachePath.path_for(controller, options) + end + + # Retrieve instance variables set in the controller. + def assigns(key) + controller.instance_variable_get("@#{key}") + 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, true) + send!(action_callback_method_name) if respond_to?(action_callback_method_name, true) + end + + def method_missing(method, *arguments) + return if @controller.nil? + @controller.send!(method, *arguments) + end + end + end + + module SqlCache + def self.included(base) #:nodoc: + if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache) + base.alias_method_chain :perform_action, :caching + end + end + + def perform_action_with_caching + ActiveRecord::Base.cache do + perform_action_without_caching + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext.rb new file mode 100644 index 000000000..f3b8c08d8 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext.rb @@ -0,0 +1,16 @@ +require 'action_controller/cgi_ext/stdinput' +require 'action_controller/cgi_ext/query_extension' +require 'action_controller/cgi_ext/cookie' +require 'action_controller/cgi_ext/session' + +class CGI #:nodoc: + include ActionController::CgiExt::Stdinput + + class << self + alias :escapeHTML_fail_on_nil :escapeHTML + + def escapeHTML(string) + escapeHTML_fail_on_nil(string) unless string.nil? + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/cookie.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/cookie.rb new file mode 100644 index 000000000..07d2f08d5 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/cookie.rb @@ -0,0 +1,106 @@ +CGI.module_eval { remove_const "Cookie" } + +# TODO: document how this differs from stdlib CGI::Cookie +class CGI #:nodoc: + class Cookie < DelegateClass(Array) + attr_accessor :name, :value, :path, :domain, :expires + attr_reader :secure, :http_only + + # 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. + # http_only:: whether this cookie can be accessed by client side scripts (e.g. document.cookie) or only over HTTP + # More details: http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx + # Defaults to false. + # 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 + @http_only = false + @path = nil + else + @name = name['name'] + @value = Array(name['value']) + @domain = name['domain'] + @expires = name['expires'] + @secure = name['secure'] || false + @http_only = name['http_only'] || false + @path = name['path'] + end + + raise ArgumentError, "`name' required" unless @name + + # simple support for IE + unless @path + %r|^(.*/)|.match(ENV['SCRIPT_NAME']) + @path = ($1 or '') + end + + super(@value) + end + + # Set whether the Cookie is a secure cookie or not. + def secure=(val) + @secure = val == true + end + + # Set whether the Cookie is an HTTP only cookie or not. + def http_only=(val) + @http_only = val == true + end + + # Convert the Cookie to its string representation. + def to_s + buf = '' + buf << @name << '=' + buf << (@value.kind_of?(String) ? CGI::escape(@value) : @value.collect{|v| CGI::escape(v) }.join("&")) + buf << '; domain=' << @domain if @domain + buf << '; path=' << @path if @path + buf << '; expires=' << CGI::rfc1123_date(@expires) if @expires + buf << '; secure' if @secure + buf << '; HttpOnly' if @http_only + 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(/[;,]\s?/).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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/query_extension.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/query_extension.rb new file mode 100644 index 000000000..9620fd287 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/query_extension.rb @@ -0,0 +1,22 @@ +require 'cgi' + +class CGI #:nodoc: + module QueryExtension + # Remove the old initialize_query method before redefining it. + remove_method :initialize_query + + # Neuter CGI parameter parsing. + def initialize_query + # Fix some strange request environments. + env_table['REQUEST_METHOD'] ||= 'GET' + + # POST assumes missing Content-Type is application/x-www-form-urlencoded. + if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST' + env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' + end + + @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE']) + @params = {} + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/session.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/session.rb new file mode 100644 index 000000000..a01f17f9c --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/session.rb @@ -0,0 +1,73 @@ +require 'digest/md5' +require 'cgi/session' +require 'cgi/session/pstore' + +class CGI #:nodoc: + # * Expose the CGI instance to session stores. + # * Don't require 'digest/md5' whenever a new session id is generated. + class Session #:nodoc: + begin + require 'securerandom' + + # Generate a 32-character unique id using SecureRandom. + # This is used to generate session ids but may be reused elsewhere. + def self.generate_unique_id(constant = nil) + SecureRandom.hex(16) + end + rescue LoadError + # Generate an 32-character unique id based on a hash of the current time, + # a random number, the process id, and a constant string. This is used + # to generate session ids but may be reused elsewhere. + def self.generate_unique_id(constant = 'foobar') + md5 = Digest::MD5.new + now = Time.now + md5 << now.to_s + md5 << String(now.usec) + md5 << String(rand(0)) + md5 << String($$) + md5 << constant + md5.hexdigest + end + end + + # Make the CGI instance available to session stores. + attr_reader :cgi + attr_reader :dbman + alias_method :initialize_without_cgi_reader, :initialize + def initialize(cgi, options = {}) + @cgi = cgi + initialize_without_cgi_reader(cgi, options) + end + + private + # Create a new session id. + def create_new_id + @new_session = true + self.class.generate_unique_id + end + + # * Don't require 'digest/md5' whenever a new session is started. + class PStore #:nodoc: + def initialize(session, option={}) + dir = option['tmpdir'] || Dir::tmpdir + prefix = option['prefix'] || '' + id = session.session_id + md5 = Digest::MD5.hexdigest(id)[0,16] + path = dir+"/"+prefix+md5 + path.untaint + if File::exist?(path) + @hash = nil + else + unless session.new_session + raise CGI::Session::NoSession, "uninitialized session" + end + @hash = {} + end + @p = ::PStore.new(path) + @p.transaction do |p| + File.chmod(0600, p.path) + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/stdinput.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/stdinput.rb new file mode 100644 index 000000000..b0ca63ef2 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_ext/stdinput.rb @@ -0,0 +1,23 @@ +require 'cgi' + +module ActionController + module CgiExt + # Publicize the CGI's internal input stream so we can lazy-read + # request.body. Make it writable so we don't have to play $stdin games. + module Stdinput + def self.included(base) + base.class_eval do + remove_method :stdinput + attr_accessor :stdinput + end + + base.alias_method_chain :initialize, :stdinput + end + + def initialize_with_stdinput(type = nil, stdinput = $stdin) + @stdinput = stdinput + initialize_without_stdinput(type || 'query') + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_process.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_process.rb new file mode 100644 index 000000000..6a802aa8f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cgi_process.rb @@ -0,0 +1,221 @@ +require 'action_controller/cgi_ext' +require 'action_controller/session/cookie_store' + +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+ cookie, 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. + # * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from + # the query string or POST parameters. This protects against session fixation attacks. + 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 + class SessionFixationAttempt < StandardError; end #:nodoc: + + DEFAULT_SESSION_OPTIONS = { + :database_manager => CGI::Session::CookieStore, # store data in cookie + :prefix => "ruby_sess.", # prefix session file names + :session_path => "/", # available to all paths in app + :session_key => "_session_id", + :cookie_only => true + } 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 + qs = @cgi.query_string if @cgi.respond_to?(:query_string) + if !qs.blank? + qs + else + super + end + end + + # The request body is an IO input stream. If the RAW_POST_DATA environment + # variable is already set, wrap it in a StringIO. + def body + if raw_post = env['RAW_POST_DATA'] + StringIO.new(raw_post) + else + @cgi.stdinput + end + end + + def query_parameters + @query_parameters ||= self.class.parse_query_parameters(query_string) + end + + def request_parameters + @request_parameters ||= parse_formatted_request_parameters + end + + def cookies + @cgi.cookies.freeze + end + + def host_with_port_without_standard_port_handling + 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_without_standard_port_handling.sub(/:\d+$/, '') + end + + def port + if host_with_port_without_standard_port_handling =~ /:(\d+)$/ + $1.to_i + else + standard_port + end + end + + def session + unless defined?(@session) + if @session_options == false + @session = Hash.new + else + stale_session_check! do + if cookie_only? && query_parameters[session_options_with_string_keys['session_key']] + raise SessionFixationAttempt + end + case value = session_options_with_string_keys['new_session'] + when true + @session = new_session + when false + begin + @session = CGI::Session.new(@cgi, session_options_with_string_keys) + # CGI::Session raises ArgumentError if 'new_session' == false + # and no session cookie or query param is present. + rescue ArgumentError + @session = Hash.new + end + when nil + @session = CGI::Session.new(@cgi, session_options_with_string_keys) + else + raise ArgumentError, "Invalid new_session option: #{value}" + end + @session['__valid_session'] + end + end + end + @session + end + + def reset_session + @session.delete if defined?(@session) && @session.is_a?(CGI::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 cookie_only? + session_options_with_string_keys['cookie_only'] + end + + def stale_session_check! + yield + rescue ArgumentError => argument_error + if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} + begin + # Note that the regexp does not allow $1 to end with a ':' + $1.constantize + rescue LoadError, NameError => 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).stringify_keys + end + end + + class CgiResponse < AbstractResponse #:nodoc: + def initialize(cgi) + @cgi = cgi + super() + end + + def out(output = $stdout) + 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) + # Flush the output now in case the @body Proc uses + # #syswrite. + output.flush if output.respond_to?(:flush) + @body.call(self, output) + else + output.write(@body) + end + + output.flush if output.respond_to?(:flush) + rescue Errno::EPIPE, Errno::ECONNRESET + # lost connection to parent process, ignore output + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/components.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/components.rb new file mode 100644 index 000000000..7f7ecfff7 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/components.rb @@ -0,0 +1,165 @@ +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.class_eval do + include InstanceMethods + extend ClassMethods + + 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. + attr_accessor :parent_controller + + alias_method_chain :process_cleanup, :components + alias_method_chain :set_session_options, :components + alias_method_chain :flash, :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 + 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_for_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 !defined?(@_flash) || refresh + @_flash = + if defined?(@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) + new_response = reuse_response ? response : response.dup + + klass.process_with_components(request, new_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) + new_request = request.dup + new_request.session = request.session + + new_request.instance_variable_set( + :@parameters, + (options[:params] || {}).with_indifferent_access.update( + "controller" => controller_name, "action" => options[:action], "id" => options[:id] + ) + ) + + new_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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/cookies.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/cookies.rb new file mode 100644 index 000000000..19847c695 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/cookies.rb @@ -0,0 +1,84 @@ +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 => 1.hour.from_now } + # # => 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. + # * <tt>http_only</tt> - whether this cookie is accessible via scripting or only HTTP (defaults to false). + + module Cookies + def self.included(base) + base.helper_method :cookies + end + + protected + # Returns the cookie container, which operates as described above. + def cookies + CookieJar.new(self) + end + end + + class CookieJar < Hash #:nodoc: + def initialize(controller) + @controller, @cookies = controller, controller.request.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 cookies[]= + # (for simple name/value cookies without options). + def [](name) + cookie = @cookies[name.to_s] + if cookie && cookie.respond_to?(:value) + cookie.size > 1 ? cookie.value : cookie.value[0] + end + 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. Like []=, you can pass in an options + # hash to delete cookies with extra data such as a +path+. + def delete(name, options = {}) + options.stringify_keys! + set_cookie(options.merge("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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/dispatcher.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/dispatcher.rb new file mode 100644 index 000000000..c8656e4ba --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/dispatcher.rb @@ -0,0 +1,195 @@ +module ActionController + # Dispatches requests to the appropriate controller and takes care of + # reloading the app after each request when Dependencies.load? is true. + class Dispatcher + class << self + # Backward-compatible class method takes CGI-specific args. Deprecated + # in favor of Dispatcher.new(output, request, response).dispatch. + def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout) + new(output).dispatch_cgi(cgi, session_options) + end + + # Declare a block to be called before each dispatch. + # Run in the order declared. + def before_dispatch(*method_names, &block) + callbacks[:before].concat method_names + callbacks[:before] << block if block_given? + end + + # Declare a block to be called after each dispatch. + # Run in reverse of the order declared. + def after_dispatch(*method_names, &block) + callbacks[:after].concat method_names + callbacks[:after] << block if block_given? + end + + # Add a preparation callback. Preparation callbacks are run before every + # request in development mode, and before the first request in production + # mode. + # + # An optional identifier may be supplied for the callback. If provided, + # to_prepare may be called again with the same identifier to replace the + # existing callback. Passing an identifier is a suggested practice if the + # code adding a preparation block may be reloaded. + def to_prepare(identifier = nil, &block) + # Already registered: update the existing callback + if identifier + if callback = callbacks[:prepare].assoc(identifier) + callback[1] = block + else + callbacks[:prepare] << [identifier, block] + end + else + callbacks[:prepare] << block + end + end + + # If the block raises, send status code as a last-ditch response. + def failsafe_response(fallback_output, status, originating_exception = nil) + yield + rescue Exception => exception + begin + log_failsafe_exception(status, originating_exception || exception) + body = failsafe_response_body(status) + fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}" + nil + rescue Exception => failsafe_error # Logger or IO errors + $stderr.puts "Error during failsafe response: #{failsafe_error}" + $stderr.puts "(originally #{originating_exception})" if originating_exception + end + end + + private + def failsafe_response_body(status) + error_path = "#{error_file_path}/#{status.to_s[0..3]}.html" + + if File.exist?(error_path) + File.read(error_path) + else + "<html><body><h1>#{status}</h1></body></html>" + end + end + + def log_failsafe_exception(status, exception) + message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: #{status}\n" + message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception + failsafe_logger.fatal message + end + + def failsafe_logger + if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil? + ::RAILS_DEFAULT_LOGGER + else + Logger.new($stderr) + end + end + end + + cattr_accessor :error_file_path + self.error_file_path = "#{::RAILS_ROOT}/public" if defined? ::RAILS_ROOT + + cattr_accessor :callbacks + self.callbacks = Hash.new { |h, k| h[k] = [] } + + cattr_accessor :unprepared + self.unprepared = true + + + before_dispatch :reload_application + before_dispatch :prepare_application + after_dispatch :flush_logger + after_dispatch :cleanup_application + + if defined? ActiveRecord + to_prepare :activerecord_instantiate_observers do + ActiveRecord::Base.instantiate_observers + end + end + + def initialize(output, request = nil, response = nil) + @output, @request, @response = output, request, response + end + + def dispatch + run_callbacks :before + handle_request + rescue Exception => exception + failsafe_rescue exception + ensure + run_callbacks :after, :reverse_each + end + + def dispatch_cgi(cgi, session_options) + if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new } + @request = CgiRequest.new(cgi, session_options) + @response = CgiResponse.new(cgi) + dispatch + end + rescue Exception => exception + failsafe_rescue exception + end + + def reload_application + if Dependencies.load? + Routing::Routes.reload + self.unprepared = true + end + end + + def prepare_application(force = false) + begin + require_dependency 'application' unless defined?(::ApplicationController) + rescue LoadError => error + raise unless error.message =~ /application\.rb/ + end + + ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord) + + if unprepared || force + run_callbacks :prepare + self.unprepared = false + end + end + + # Cleanup the application by clearing out loaded classes so they can + # be reloaded on the next request without restarting the server. + def cleanup_application(force = false) + if Dependencies.load? || force + ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) + Dependencies.clear + ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) + end + end + + def flush_logger + RAILS_DEFAULT_LOGGER.flush if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush) + end + + protected + def handle_request + @controller = Routing::Routes.recognize(@request) + @controller.process(@request, @response).out(@output) + end + + def run_callbacks(kind, enumerator = :each) + callbacks[kind].send!(enumerator) do |callback| + case callback + when Proc; callback.call(self) + when String, Symbol; send!(callback) + when Array; callback[1].call(self) + else raise ArgumentError, "Unrecognized callback #{callback.inspect}" + end + end + end + + def failsafe_rescue(exception) + self.class.failsafe_response(@output, '500 Internal Server Error', exception) do + if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base + @controller.process_with_exception(@request, @response, exception).out(@output) + else + raise exception + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/filters.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/filters.rb new file mode 100644 index 000000000..d7fb27617 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/filters.rb @@ -0,0 +1,767 @@ +module ActionController #:nodoc: + module Filters #:nodoc: + def self.included(base) + base.class_eval do + extend ClassMethods + include ActionController::Filters::InstanceMethods + end + 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). + # + # == Filter inheritance + # + # Controller inheritance hierarchies share filters downwards, but subclasses can also add or skip 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 renders or redirects, 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| head(400) 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). + # + # Please note that around_filters function a little differently than the normal before and after filters with regard to filter + # types. Please see the section dedicated to around_filters below. + # + # == 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 < ActionController::Base + # before_filter :verify_open_shop + # + # class CheckoutController < ShoppingController + # 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 renders or redirects, 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 + # + # Around filters wrap an action, executing code both before and after. + # They may be declared as method references, blocks, or objects responding + # to #filter or to both #before and #after. + # + # To use a method as an around_filter, pass a symbol naming the Ruby method. + # Yield (or block.call) within the method to run the action. + # + # around_filter :catch_exceptions + # + # private + # def catch_exceptions + # yield + # rescue => exception + # logger.debug "Caught exception! #{exception}" + # raise + # end + # + # To use a block as an around_filter, pass a block taking as args both + # the controller and the action block. You can't call yield directly from + # an around_filter block; explicitly call the action block instead: + # + # around_filter do |controller, action| + # logger.debug "before #{controller.action_name}" + # action.call + # logger.debug "after #{controller.action_name}" + # end + # + # To use a filter object with around_filter, pass an object responding + # to :filter or both :before and :after. With a filter method, yield to + # the block as above: + # + # around_filter BenchmarkingFilter + # + # class BenchmarkingFilter + # def self.filter(controller, &block) + # Benchmark.measure(&block) + # end + # end + # + # With before and after methods: + # + # around_filter Authorizer.new + # + # class Authorizer + # # This will run before the action. Redirecting aborts the action. + # def before(controller) + # unless user.authorized? + # redirect_to(login_url) + # end + # end + # + # # This will run after the action if and only if before did not render or redirect. + # def after(controller) + # end + # end + # + # If the filter has before and after methods, the before method will be + # called before the action. If before renders or redirects, the filter chain is + # halted and after will not be run. See Filter Chain Halting below for + # an example. + # + # == Filter chain skipping + # + # Declaring a filter on a base class conveniently applies to its subclasses, + # but sometimes a subclass should skip some of its superclass' filters: + # + # class ApplicationController < ActionController::Base + # before_filter :authenticate + # around_filter :catch_exceptions + # end + # + # class WeblogController < ApplicationController + # # Will run the :authenticate and :catch_exceptions filters. + # end + # + # class SignupController < ApplicationController + # # Skip :authenticate, run :catch_exceptions. + # skip_before_filter :authenticate + # end + # + # class ProjectsController < ApplicationController + # # Skip :catch_exceptions, run :authenticate. + # skip_filter :catch_exceptions + # end + # + # class ClientsController < ApplicationController + # # Skip :catch_exceptions and :authenticate unless action is index. + # skip_filter :catch_exceptions, :authenticate, :except => :index + # end + # + # == Filter conditions + # + # Filters may be limited to specific actions by declaring the actions to + # include or exclude. Both options accept single actions (:only => :index) + # or arrays of actions (:except => [:foo, :bar]). + # + # class Journal < ActionController::Base + # # Require authentication for edit and delete. + # before_filter :authorize, :only => [:edit, :delete] + # + # # Passing options to a filter with a block. + # around_filter(:except => :index) do |controller, action_block| + # results = Profiler.run(&action_block) + # controller.response.sub! "</body>", "#{results}</body>" + # end + # + # private + # def authorize + # # Redirect to login unless authenticated. + # end + # end + # + # == Filter Chain Halting + # + # <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request + # before a controller action is run. This is useful, for example, to deny + # access to unauthenticated users or to redirect from http to https. + # Simply call render or redirect. After filters will not be executed if the filter + # chain is halted. + # + # Around filters halt the request unless the action block is called. + # Given these filters + # after_filter :after + # around_filter :around + # before_filter :before + # + # The filter chain will look like: + # + # ... + # . \ + # . #around (code before yield) + # . . \ + # . . #before (actual filter code is run) + # . . . \ + # . . . execute controller action + # . . . / + # . . ... + # . . / + # . #around (code after yield) + # . / + # #after (actual filter code is run, unless the around filter does not yield) + # + # If #around returns before yielding, #after will still not be run. The #before + # filter and controller action will not be run. If #before renders or redirects, + # the second half of #around and will still run but #after and the + # action will not. If #around fails to yield, #after will not be run. + module ClassMethods + # The passed <tt>filters</tt> will be appended to the filter_chain and + # will execute before the action on this controller is performed. + def append_before_filter(*filters, &block) + append_filter_to_chain(filters, :before, &block) + end + + # The passed <tt>filters</tt> will be prepended to the filter_chain and + # will execute before the action on this controller is performed. + def prepend_before_filter(*filters, &block) + prepend_filter_to_chain(filters, :before, &block) + end + + # Shorthand for append_before_filter since it's the most common. + alias :before_filter :append_before_filter + + # The passed <tt>filters</tt> will be appended to the array of filters + # that run _after_ actions on this controller are performed. + def append_after_filter(*filters, &block) + append_filter_to_chain(filters, :after, &block) + end + + # The passed <tt>filters</tt> will be prepended to the array of filters + # that run _after_ actions on this controller are performed. + def prepend_after_filter(*filters, &block) + prepend_filter_to_chain(filters, :after, &block) + end + + # Shorthand for append_after_filter since it's the most common. + alias :after_filter :append_after_filter + + + # If you append_around_filter A.new, B.new, the filter chain looks like + # + # B#before + # A#before + # # run the action + # A#after + # B#after + # + # With around filters which yield to the action block, #before and #after + # are the code before and after the yield. + def append_around_filter(*filters, &block) + filters, conditions = extract_conditions(filters, &block) + filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter| + append_filter_to_chain([filter, conditions]) + end + end + + # If you prepend_around_filter A.new, B.new, the filter chain looks like: + # + # A#before + # B#before + # # run the action + # B#after + # A#after + # + # With around filters which yield to the action block, #before and #after + # are the code before and after the yield. + def prepend_around_filter(*filters, &block) + filters, conditions = extract_conditions(filters, &block) + filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter| + prepend_filter_to_chain([filter, conditions]) + end + end + + # Shorthand for append_around_filter since it's the most common. + 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) + skip_filter_in_chain(*filters, &:before?) + 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) + skip_filter_in_chain(*filters, &:after?) + end + + # Removes the specified filters from the filter chain. This only works for method reference (symbol) + # filters, not procs. This method is different from skip_after_filter and skip_before_filter in that + # it will match any before, after or yielding around filter. + # + # 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_filter(*filters) + skip_filter_in_chain(*filters) + end + + # Returns an array of Filter objects for this controller. + def filter_chain + read_inheritable_attribute("filter_chain") || [] + end + + # Returns all the before filters for this class and all its ancestors. + # This method returns the actual filter that was assigned in the controller to maintain existing functionality. + def before_filters #:nodoc: + filter_chain.select(&:before?).map(&:filter) + end + + # Returns all the after filters for this class and all its ancestors. + # This method returns the actual filter that was assigned in the controller to maintain existing functionality. + def after_filters #:nodoc: + filter_chain.select(&:after?).map(&:filter) + 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 + + # Find a filter in the filter_chain where the filter method matches the _filter_ param + # and (optionally) the passed block evaluates to true (mostly used for testing before? + # and after? on the filter). Useful for symbol filters. + # + # The object of type Filter is passed to the block when yielded, not the filter itself. + def find_filter(filter, &block) #:nodoc: + filter_chain.select { |f| f.filter == filter && (!block_given? || yield(f)) }.first + end + + # Returns true if the filter is excluded from the given action + def filter_excluded_from_action?(filter,action) #:nodoc: + case + when ia = included_actions[filter] + !ia.include?(action) + when ea = excluded_actions[filter] + ea.include?(action) + end + end + + # Filter class is an abstract base class for all filters. Handles all of the included/excluded actions but + # contains no logic for calling the actual filters. + class Filter #:nodoc: + attr_reader :filter, :included_actions, :excluded_actions + + def initialize(filter) + @filter = filter + end + + def type + :around + end + + def before? + type == :before + end + + def after? + type == :after + end + + def around? + type == :around + end + + def run(controller) + raise ActionControllerError, 'No filter type: Nothing to do here.' + end + + def call(controller, &block) + run(controller) + end + end + + # Abstract base class for filter proxies. FilterProxy objects are meant to mimic the behaviour of the old + # before_filter and after_filter by moving the logic into the filter itself. + class FilterProxy < Filter #:nodoc: + def filter + @filter.filter + end + end + + class BeforeFilterProxy < FilterProxy #:nodoc: + def type + :before + end + + def run(controller) + # only filters returning false are halted. + @filter.call(controller) + if controller.send!(:performed?) + controller.send!(:halt_filter_chain, @filter, :rendered_or_redirected) + end + end + + def call(controller) + yield unless run(controller) + end + end + + class AfterFilterProxy < FilterProxy #:nodoc: + def type + :after + end + + def run(controller) + @filter.call(controller) + end + + def call(controller) + yield + run(controller) + end + end + + class SymbolFilter < Filter #:nodoc: + def call(controller, &block) + controller.send!(@filter, &block) + end + end + + class ProcFilter < Filter #:nodoc: + def call(controller) + @filter.call(controller) + rescue LocalJumpError # a yield from a proc... no no bad dog. + raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.') + end + end + + class ProcWithCallFilter < Filter #:nodoc: + def call(controller, &block) + @filter.call(controller, block) + rescue LocalJumpError # a yield from a proc... no no bad dog. + raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.') + end + end + + class MethodFilter < Filter #:nodoc: + def call(controller, &block) + @filter.call(controller, &block) + end + end + + class ClassFilter < Filter #:nodoc: + def call(controller, &block) + @filter.filter(controller, &block) + end + end + + class ClassBeforeFilter < Filter #:nodoc: + def call(controller, &block) + @filter.before(controller) + end + end + + class ClassAfterFilter < Filter #:nodoc: + def call(controller, &block) + @filter.after(controller) + end + end + + protected + def append_filter_to_chain(filters, filter_type = :around, &block) + pos = find_filter_append_position(filters, filter_type) + update_filter_chain(filters, filter_type, pos, &block) + end + + def prepend_filter_to_chain(filters, filter_type = :around, &block) + pos = find_filter_prepend_position(filters, filter_type) + update_filter_chain(filters, filter_type, pos, &block) + end + + def update_filter_chain(filters, filter_type, pos, &block) + new_filters = create_filters(filters, filter_type, &block) + new_chain = filter_chain.insert(pos, new_filters).flatten + write_inheritable_attribute('filter_chain', new_chain) + end + + def find_filter_append_position(filters, filter_type) + # appending an after filter puts it at the end of the call chain + # before and around filters go before the first after filter in the chain + unless filter_type == :after + filter_chain.each_with_index do |f,i| + return i if f.after? + end + end + return -1 + end + + def find_filter_prepend_position(filters, filter_type) + # prepending a before or around filter puts it at the front of the call chain + # after filters go before the first after filter in the chain + if filter_type == :after + filter_chain.each_with_index do |f,i| + return i if f.after? + end + return -1 + end + return 0 + end + + def create_filters(filters, filter_type, &block) #:nodoc: + filters, conditions = extract_conditions(filters, &block) + filters.map! { |filter| find_or_create_filter(filter, filter_type) } + update_conditions(filters, conditions) + filters + end + + def find_or_create_filter(filter, filter_type) + if found_filter = find_filter(filter) { |f| f.type == filter_type } + found_filter + else + f = class_for_filter(filter, filter_type).new(filter) + # apply proxy to filter if necessary + case filter_type + when :before + BeforeFilterProxy.new(f) + when :after + AfterFilterProxy.new(f) + else + f + end + end + end + + # The determination of the filter type was once done at run time. + # This method is here to extract as much logic from the filter run time as possible + def class_for_filter(filter, filter_type) #:nodoc: + case + when filter.is_a?(Symbol) + SymbolFilter + when filter.respond_to?(:call) + if filter.is_a?(Method) + MethodFilter + elsif filter.arity == 1 + ProcFilter + else + ProcWithCallFilter + end + when filter.respond_to?(:filter) + ClassFilter + when filter.respond_to?(:before) && filter_type == :before + ClassBeforeFilter + when filter.respond_to?(:after) && filter_type == :after + ClassAfterFilter + else + raise(ActionControllerError, 'A filter must be a Symbol, Proc, Method, or object responding to filter, after or before.') + end + end + + def extract_conditions(*filters, &block) #:nodoc: + filters.flatten! + conditions = filters.extract_options! + filters << block if block_given? + return filters, conditions + end + + def update_conditions(filters, conditions) + return if conditions.empty? + if conditions[:only] + write_inheritable_hash('included_actions', condition_hash(filters, conditions[:only])) + elsif conditions[:except] + write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except])) + end + end + + def condition_hash(filters, *actions) + actions = actions.flatten.map(&:to_s) + filters.inject({}) { |h,f| h.update( f => (actions.blank? ? nil : actions)) } + end + + def skip_filter_in_chain(*filters, &test) #:nodoc: + filters, conditions = extract_conditions(filters) + filters.map! { |f| block_given? ? find_filter(f, &test) : find_filter(f) } + filters.compact! + + if conditions.empty? + delete_filters_in_chain(filters) + else + remove_actions_from_included_actions!(filters,conditions[:only] || []) + conditions[:only], conditions[:except] = conditions[:except], conditions[:only] + update_conditions(filters,conditions) + end + end + + def remove_actions_from_included_actions!(filters,*actions) + actions = actions.flatten.map(&:to_s) + updated_hash = filters.inject(read_inheritable_attribute('included_actions')||{}) do |hash,filter| + ia = (hash[filter] || []) - actions + ia.empty? ? hash.delete(filter) : hash[filter] = ia + hash + end + write_inheritable_attribute('included_actions', updated_hash) + end + + def delete_filters_in_chain(filters) #:nodoc: + write_inheritable_attribute('filter_chain', filter_chain.reject { |f| filters.include?(f) }) + end + + def filter_responds_to_before_and_after(filter) #:nodoc: + filter.respond_to?(:before) && filter.respond_to?(:after) + end + + def proxy_before_and_after_filter(filter) #:nodoc: + return filter unless filter_responds_to_before_and_after(filter) + Proc.new do |controller, action| + filter.before(controller) + + if controller.send!(:performed?) + controller.send!(:halt_filter_chain, filter, :rendered_or_redirected) + else + begin + action.call + ensure + filter.after(controller) + end + end + end + end + end + + module InstanceMethods # :nodoc: + def self.included(base) + base.class_eval do + alias_method_chain :perform_action, :filters + alias_method_chain :process, :filters + end + end + + protected + + def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: + @before_filter_chain_aborted = false + process_without_filters(request, response, method, *arguments) + end + + def perform_action_with_filters + call_filters(self.class.filter_chain, 0, 0) + end + + private + + def call_filters(chain, index, nesting) + index = run_before_filters(chain, index, nesting) + aborted = @before_filter_chain_aborted + perform_action_without_filters unless performed? || aborted + return index if nesting != 0 || aborted + run_after_filters(chain, index) + end + + def skip_excluded_filters(chain, index) + while (filter = chain[index]) && self.class.filter_excluded_from_action?(filter, action_name) + index = index.next + end + [filter, index] + end + + def run_before_filters(chain, index, nesting) + while chain[index] + filter, index = skip_excluded_filters(chain, index) + break unless filter # end of call chain reached + + case filter.type + when :before + filter.run(self) # invoke before filter + index = index.next + break if @before_filter_chain_aborted + when :around + yielded = false + + filter.call(self) do + yielded = true + # all remaining before and around filters will be run in this call + index = call_filters(chain, index.next, nesting.next) + end + + halt_filter_chain(filter, :did_not_yield) unless yielded + + break + else + break # no before or around filters left + end + end + + index + end + + def run_after_filters(chain, index) + seen_after_filter = false + + while chain[index] + filter, index = skip_excluded_filters(chain, index) + break unless filter # end of call chain reached + + case filter.type + when :after + seen_after_filter = true + filter.run(self) # invoke after filter + else + # implementation error or someone has mucked with the filter chain + raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" if seen_after_filter + end + + index = index.next + end + + index.next + end + + def halt_filter_chain(filter, reason) + @before_filter_chain_aborted = true + logger.info "Filter chain halted as [#{filter.inspect}] #{reason}." if logger + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/flash.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/flash.rb new file mode 100644 index 000000000..692168f23 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/flash.rb @@ -0,0 +1,177 @@ +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.erb + # <% 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.class_eval do + include InstanceMethods + alias_method_chain :assign_shortcuts, :flash + alias_method_chain :process_cleanup, :flash + alias_method_chain :reset_session, :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| keep(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.discard # discard the entire flash at the end of the current action + # flash.discard(:warning) # discard only the "warning" entry at the end of the current action + def discard(k = nil) + 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 + + # clean up after keys that could have been left over by calling reject! or shift on the flash + (@used.keys - keys).each{ |k| @used.delete(k) } + 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: + protected + def reset_session_with_flash + reset_session_without_flash + remove_instance_variable(:@_flash) + flash(:refresh) + end + + # 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 !defined?(@_flash) || refresh + @_flash = + if session.is_a?(Hash) + # don't put flash in session if disabled + 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 + + private + 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 + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/helpers.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/helpers.rb new file mode 100644 index 000000000..81b8ff975 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/helpers.rb @@ -0,0 +1,204 @@ +# FIXME: helper { ... } is broken on Ruby 1.9 +module ActionController #:nodoc: + module Helpers #:nodoc: + HELPERS_DIR = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers") + + def self.included(base) + # 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_chain :inherited, :helper + end + end + end + + # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+, + # +numbers+ and +ActiveRecord+ objects, to name a few. These helpers are available to all templates + # by default. + # + # In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to + # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will + # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically + # include <tt>MyHelper</tt>. + # + # Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any + # controller which inherits from it. + # + # ==== Examples + # The +to_s+ method from the +Time+ class can be wrapped in a helper method to display a custom message if + # the Time object is blank: + # + # module FormattedTimeHelper + # def format_time(time, format=:long, blank_message=" ") + # time.blank? ? blank_message : time.to_s(format) + # end + # end + # + # +FormattedTimeHelper+ can now be included in a controller, using the +helper+ class method: + # + # class EventsController < ActionController::Base + # helper FormattedTimeHelper + # def index + # @events = Event.find(:all) + # end + # end + # + # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called: + # + # <% @events.each do |event| -%> + # <p> + # <% format_time(event.time, :short, "N/A") %> | <%= event.name %> + # </p> + # <% end -%> + # + # Finally, assuming we have two event instances, one which has a time and one which does not, + # the output might look like this: + # + # 23 Aug 11:30 | Carolina Railhawks Soccer Match + # N/A | Carolina Railhaws Training Workshop + # + 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.module_eval { include helper_module } + end + + # The +helper+ class method can take a series of helper module names, a block, or both. + # + # * <tt>*args</tt>: One or more +Modules+, +Strings+ or +Symbols+, or the special symbol <tt>:all</tt>. + # * <tt>&block</tt>: A block defining helper methods. + # + # ==== Examples + # When the argument is a +String+ or +Symbol+, the method will provide the "_helper" suffix, require the file + # and include the module in the template class. The second form illustrates how to include custom helpers + # when working with namespaced controllers, or other cases where the file containing the helper definition is not + # in one of Rails' standard load paths: + # helper :foo # => requires 'foo_helper' and includes FooHelper + # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper + # + # When the argument is a +Module+, it will be included directly in the template class. + # helper FooHelper # => includes FooHelper + # + # When the argument is the symbol <tt>:all</tt>, the controller will include all helpers from + # <tt>app/helpers/**/*.rb</tt> under +RAILS_ROOT+. + # helper :all + # + # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available + # to the template. + # # One line + # helper { def hello() "Hello, world!" end } + # # Multi-line + # helper do + # def foo(bar) + # "#{bar} is the very best" + # end + # end + # + # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of + # +symbols+, +strings+, +modules+ and blocks. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when Module + add_template_helper(arg) + when :all + helper(all_application_helpers) + 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.message).to_a[1] + if requiree == file_name + msg = "Missing helper file helpers/#{file_name}.rb" + raise LoadError.new(msg).copy_blame!(load_error) + else + raise + end + end + + add_template_helper(class_name.constantize) + else + raise ArgumentError, "helper expects String, Symbol, or Module argument (was: #{args.inspect})" + 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, the following + # makes the +current_user+ controller method available to the view: + # class ApplicationController < ActionController::Base + # helper_method :current_user + # def current_user + # @current_user ||= User.find(session[:user]) + # end + # end + 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 + + # Declares helper accessors for controller attributes. For example, the + # following adds new +name+ and <tt>name=</tt> instance methods to a + # controller and makes them available to the view: + # helper_attr :name + # attr_accessor :name + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + + private + def default_helper_module! + unless name.blank? + module_name = name.sub(/Controller$|$/, 'Helper') + module_path = module_name.split('::').map { |m| m.underscore }.join('/') + require_dependency module_path + helper module_name.constantize + end + rescue MissingSourceFile => e + raise unless e.is_missing? module_path + rescue NameError => e + raise unless e.missing_name? module_name + 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 + + # Extract helper names from files in app/helpers/**/*.rb + def all_application_helpers + extract = /^#{Regexp.quote(HELPERS_DIR)}\/?(.*)_helper.rb$/ + Dir["#{HELPERS_DIR}/**/*_helper.rb"].map { |file| file.sub extract, '\1' } + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/http_authentication.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/http_authentication.rb new file mode 100644 index 000000000..18a503c3a --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/http_authentication.rb @@ -0,0 +1,126 @@ +require 'base64' + +module ActionController + module HttpAuthentication + # Makes it dead easy to do HTTP Basic authentication. + # + # Simple Basic example: + # + # class PostsController < ApplicationController + # USER_NAME, PASSWORD = "dhh", "secret" + # + # before_filter :authenticate, :except => [ :index ] + # + # def index + # render :text => "Everyone can see me!" + # end + # + # def edit + # render :text => "I'm only accessible if you know the password" + # end + # + # private + # def authenticate + # authenticate_or_request_with_http_basic do |user_name, password| + # user_name == USER_NAME && password == PASSWORD + # end + # end + # end + # + # + # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, + # the regular HTML interface is protected by a session approach: + # + # class ApplicationController < ActionController::Base + # before_filter :set_account, :authenticate + # + # protected + # def set_account + # @account = Account.find_by_url_name(request.subdomains.first) + # end + # + # def authenticate + # case request.format + # when Mime::XML, Mime::ATOM + # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } + # @current_user = user + # else + # request_http_basic_authentication + # end + # else + # if session_authenticated? + # @current_user = @account.users.find(session[:authenticated][:user_id]) + # else + # redirect_to(login_url) and return false + # end + # end + # end + # end + # + # + # In your integration tests, you can do something like this: + # + # def test_access_granted_from_xml + # get( + # "/notes/1.xml", nil, + # :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) + # ) + # + # assert_equal 200, status + # end + # + # + # On shared hosts, Apache sometimes doesn't pass authentication headers to + # FCGI instances. If your environment matches this description and you cannot + # authenticate, try this rule in public/.htaccess (replace the plain one): + # + # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] + module Basic + extend self + + module ControllerMethods + def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + end + + def authenticate_with_http_basic(&login_procedure) + HttpAuthentication::Basic.authenticate(self, &login_procedure) + end + + def request_http_basic_authentication(realm = "Application") + HttpAuthentication::Basic.authentication_request(self, realm) + end + end + + def authenticate(controller, &login_procedure) + unless authorization(controller.request).blank? + login_procedure.call(*user_name_and_password(controller.request)) + end + end + + def user_name_and_password(request) + decode_credentials(request).split(/:/, 2) + end + + def authorization(request) + request.env['HTTP_AUTHORIZATION'] || + request.env['X-HTTP_AUTHORIZATION'] || + request.env['X_HTTP_AUTHORIZATION'] || + request.env['REDIRECT_X_HTTP_AUTHORIZATION'] + end + + def decode_credentials(request) + Base64.decode64(authorization(request).split.last || '') + end + + def encode_credentials(user_name, password) + "Basic #{Base64.encode64("#{user_name}:#{password}")}" + end + + def authentication_request(controller, realm) + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + controller.send! :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/integration.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/integration.rb new file mode 100644 index 000000000..25fb1b93a --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/integration.rb @@ -0,0 +1,581 @@ +require 'dispatcher' +require 'stringio' +require 'uri' +require 'action_controller/test_process' + +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::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 + + # A running counter of the number of requests processed. + attr_accessor :request_count + + # Create and 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 + @request_count = 0 + + 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 defined? @named_routes_configured + # install the named routes in this session instance. + klass = class<<self; self; end + Routing::Routes.install_helpers(klass) + + # the helpers are made protected by default--we make them public for + # easier access during testing and troubleshooting. + klass.module_eval { public *Routing::Routes.named_routes.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 request using the specified method, 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 request_via_redirect(http_method, path, parameters = nil, headers = nil) + send(http_method, path, parameters, headers) + follow_redirect! while redirect? + status + end + + # Performs a GET request, following any subsequent redirect. + # See #request_via_redirect() for more information. + def get_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:get, path, parameters, headers) + end + + # Performs a POST request, following any subsequent redirect. + # See #request_via_redirect() for more information. + def post_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:post, path, parameters, headers) + end + + # Performs a PUT request, following any subsequent redirect. + # See #request_via_redirect() for more information. + def put_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:put, path, parameters, headers) + end + + # Performs a DELETE request, following any subsequent redirect. + # See #request_via_redirect() for more information. + def delete_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:delete, path, parameters, headers) + 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. + # + # You can also perform POST, PUT, DELETE, and HEAD requests with #post, + # #put, #delete, and #head. + def get(path, parameters = nil, headers = nil) + process :get, path, parameters, headers + end + + # Performs a POST request with the given parameters. See get() for more details. + def post(path, parameters = nil, headers = nil) + process :post, path, parameters, headers + end + + # Performs a PUT request with the given parameters. See get() for more details. + def put(path, parameters = nil, headers = nil) + process :put, path, parameters, headers + end + + # Performs a DELETE request with the given parameters. See get() for more details. + def delete(path, parameters = nil, headers = nil) + process :delete, path, parameters, headers + end + + # Performs a HEAD request with the given parameters. See get() for more details. + def head(path, parameters = nil, headers = nil) + process :head, path, parameters, headers + end + + # Performs an XMLHttpRequest request with the given parameters, mirroring + # a request from the Prototype library. + # + # The request_method is :get, :post, :put, :delete or :head; the + # parameters are +nil+, a hash, or a url-encoded or multipart string; + # the headers are a hash. Keys are automatically upcased and prefixed + # with 'HTTP_' if not already. + def xml_http_request(request_method, path, parameters = nil, headers = nil) + headers ||= {} + headers['X-Requested-With'] = 'XMLHttpRequest' + headers['Accept'] ||= 'text/javascript, text/html, application/xml, text/xml, */*' + + process(request_method, path, parameters, headers) + end + alias xhr :xml_http_request + + # 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 StubCGI < CGI #:nodoc: + attr_accessor :stdinput, :stdoutput, :env_table + + def initialize(env, stdinput = nil) + self.env_table = env + self.stdoutput = StringIO.new + + super + + @stdinput = stdinput.is_a?(IO) ? stdinput : StringIO.new(stdinput || '') + 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) || key =~ /^HTTP_/ + env[key] = value + end + + unless ActionController::Base.respond_to?(:clear_last_instantiation!) + ActionController::Base.module_eval { include ControllerCapture } + end + + ActionController::Base.clear_last_instantiation! + + cgi = StubCGI.new(env, data) + Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput) + @result = cgi.stdoutput.string + @request_count += 1 + + @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) + + @html_document = nil + + 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 temporary URL writer object + def generic_url_rewriter + cgi = StubCGI.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_chain :new, :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) + controller = new_without_capture(*args) + self.last_instantiation ||= controller + controller + end + end + end + + module Runner + # 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 put head delete cookies assigns + xml_http_request get_via_redirect post_via_redirect).each do |method| + define_method(method) do |*args| + reset! unless @integration_session + # reset the html_document variable, but only for new get/post calls + @html_document = nil unless %w(cookies assigns).include?(method) + 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 } + if self.class.respond_to?(:fixture_table_names) + 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 + 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 + + # 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 + include Integration::Runner + + # 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 + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/layout.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/layout.rb new file mode 100644 index 000000000..63a68c108 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/layout.rb @@ -0,0 +1,326 @@ +module ActionController #:nodoc: + module Layout #:nodoc: + def self.included(base) + base.extend(ClassMethods) + base.class_eval do + # NOTE: Can't use alias_method_chain here because +render_without_layout+ is already + # defined as a publicly exposed method + alias_method :render_with_no_layout, :render + alias_method :render, :render_with_a_layout + + class << self + alias_method_chain :inherited, :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.erb</tt> or <tt>app/views/layouts/weblog.builder</tt> exists then it will be automatically set as + # the layout for your WeblogController. You can create a layout with the name <tt>application.erb</tt> or <tt>application.builder</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.erb</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 assignment 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 be looked for in +app/views/layouts/+. + # Otherwise, it will be looked up relative to the template root. + # + # == 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 = {}, auto = false) + add_layout_conditions(conditions) + write_inheritable_attribute "layout", template_name + write_inheritable_attribute "auto_layout", auto + end + + def layout_conditions #:nodoc: + @layout_conditions ||= read_inheritable_attribute("layout_conditions") + end + + def default_layout(format) #:nodoc: + layout = read_inheritable_attribute("layout") + return layout unless read_inheritable_attribute("auto_layout") + @default_layout ||= {} + @default_layout[format] ||= default_layout_with_format(format, layout) + @default_layout[format] + end + + def layout_list #:nodoc: + view_paths.collect do |path| + Dir["#{path}/layouts/**/*"] + end.flatten + end + + private + def inherited_with_layout(child) + inherited_without_layout(child) + unless child.name.blank? + layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '') + child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty? + end + 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 + + def default_layout_with_format(format, layout) + list = layout_list + if list.grep(%r{layouts/#{layout}\.#{format}(\.[a-z][0-9a-z]*)+$}).empty? + (!list.grep(%r{layouts/#{layout}\.([a-z][0-9a-z]*)+$}).empty? && format == :html) ? layout : nil + else + layout + 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(response.template.template_format) + 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 + + protected + def render_with_a_layout(options = nil, &block) #:nodoc: + template_with_options = options.is_a?(Hash) + + if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options)) + assert_existence_of_template_file(layout) + + options = options.merge :layout => false if template_with_options + logger.info("Rendering template within #{layout}") if logger + + content_for_layout = render_with_no_layout(options, &block) + erase_render_results + add_variables_to_assigns + @template.instance_variable_set("@content_for_layout", content_for_layout) + response.layout = layout + status = template_with_options ? options[:status] : nil + render_for_text(@template.render_file(layout, true), status) + else + render_with_no_layout(options, &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, :json, :file, :inline, :partial, :nothing).compact.empty? && + !template_exempt_from_layout?(options[:template] || default_template_name(options[:action])) + end + + def pick_layout(template_with_options, options) + if 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) + view_paths.find do |path| + next unless template_path = Dir[File.join(path, 'layouts', layout_name) + ".*"].first + self.class.send!(:layout_directory_exists_cache)[File.dirname(template_path)] + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_responds.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_responds.rb new file mode 100644 index 000000000..4ba4e626e --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_responds.rb @@ -0,0 +1,170 @@ +module ActionController #:nodoc: + module MimeResponds #:nodoc: + def self.included(base) + base.module_eval do + include ActionController::MimeResponds::InstanceMethods + end + 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 index + # @people = Person.find(:all) + # end + # + # Here's the same action, with web-service support baked in: + # + # def index + # @people = Person.find(:all) + # + # respond_to do |format| + # format.html + # format.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 create + # @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 create + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # @person = @company.people.create(params[:person]) + # + # respond_to do |format| + # format.html { redirect_to(person_list_url) } + # format.js + # format.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 + # (format.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 person's 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's 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 both" unless types.any? ^ block + block ||= lambda { |responder| types.each { |type| responder.send(type) } } + responder = Responder.new(self) + block.call(responder) + responder.respond + end + end + + class Responder #:nodoc: + def initialize(controller) + @controller = controller + @request = controller.request + @response = controller.response + + @mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts) + + @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 + + @responses[mime_type] = Proc.new do + @response.template.template_format = mime_type.to_sym + @response.content_type = mime_type.to_s + block_given? ? block.call : @controller.send(:render, :action => @controller.action_name) + end + end + + def any(*args, &block) + args.each { |type| send(type, &block) } + end + + def method_missing(symbol, &block) + mime_constant = symbol.to_s.upcase + + if Mime::SET.include?(Mime.const_get(mime_constant)) + custom(Mime.const_get(mime_constant), &block) + else + super + end + end + + def respond + for priority in @mime_type_priority + if priority == Mime::ALL + @responses[@order.first].call + return + else + if @responses[priority] + @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 + @controller.send :head, :not_acceptable + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_type.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_type.rb new file mode 100644 index 000000000..ec9d2eea6 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_type.rb @@ -0,0 +1,163 @@ +module Mime + SET = [] + EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + + # Encapsulates the notion of a mime type. Can be used at render time, for example, with: + # + # class PostsController < ActionController::Base + # def show + # @post = Post.find(params[:id]) + # + # respond_to do |format| + # format.html + # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] } + # format.xml { render :xml => @people.to_xml } + # end + # end + # end + class Type + # 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 == Mime::ALL # default wilcard match 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 lookup_by_extension(extension) + EXTENSION_LOOKUP[extension] + end + + # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for + # rendering different HTML versions depending on the user agent, like an iPhone. + def register_alias(string, symbol, extension_synonyms = []) + register(string, symbol, [], extension_synonyms, true) + end + + def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) + Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) } + + SET << Mime.const_get(symbol.to_s.upcase) + + ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup + ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last } + end + + def parse(accept_header) + # keep track of creation order to keep the subsequent sort stable + list = [] + accept_header.split(/,/).each_with_index do |header, index| + params = header.split(/;\s*q=/) + list << AcceptItem.new(index, *params) unless params.empty? + end + list.sort! + + # Take care of the broken text/xml entry by renaming or deleting it + text_xml = list.index("text/xml") + app_xml = list.index(Mime::XML.to_s) + + 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 = Mime::XML.to_s + end + + # Look for more specific xml-based types and sort them ahead of app/xml + + if app_xml + idx = app_xml + 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 + + private + def method_missing(method, *args) + if method.to_s =~ /(\w+)\?$/ + mime_type = $1.downcase.to_sym + mime_type == @symbol || (mime_type == :html && @symbol == :all) + else + super + end + end + end +end + +require 'action_controller/mime_types' diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_types.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_types.rb new file mode 100644 index 000000000..71706b4c4 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/mime_types.rb @@ -0,0 +1,20 @@ +# Build list of Mime types for HTTP responses +# http://www.iana.org/assignments/media-types/ + +Mime::Type.register "*/*", :all +Mime::Type.register "text/plain", :text, [], %w(txt) +Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) +Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript ) +Mime::Type.register "text/css", :css +Mime::Type.register "text/calendar", :ics +Mime::Type.register "text/csv", :csv +Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) +Mime::Type.register "application/rss+xml", :rss +Mime::Type.register "application/atom+xml", :atom +Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ) + +Mime::Type.register "multipart/form-data", :multipart_form +Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form + +# http://www.ietf.org/rfc/rfc4627.txt +Mime::Type.register "application/json", :json, %w( text/x-json ) diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/polymorphic_routes.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/polymorphic_routes.rb new file mode 100644 index 000000000..94aefc9aa --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/polymorphic_routes.rb @@ -0,0 +1,88 @@ +module ActionController + module PolymorphicRoutes + def polymorphic_url(record_or_hash_or_array, options = {}) + record = extract_record(record_or_hash_or_array) + + namespace = extract_namespace(record_or_hash_or_array) + + args = case record_or_hash_or_array + when Hash; [ record_or_hash_or_array ] + when Array; record_or_hash_or_array.dup + else [ record_or_hash_or_array ] + end + + inflection = + case + when options[:action] == "new" + args.pop + :singular + when record.respond_to?(:new_record?) && record.new_record? + args.pop + :plural + else + :singular + end + + named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) + send!(named_route, *args) + end + + def polymorphic_path(record_or_hash_or_array) + polymorphic_url(record_or_hash_or_array, :routing_type => :path) + end + + %w(edit new formatted).each do |action| + module_eval <<-EOT, __FILE__, __LINE__ + def #{action}_polymorphic_url(record_or_hash) + polymorphic_url(record_or_hash, :action => "#{action}") + end + + def #{action}_polymorphic_path(record_or_hash) + polymorphic_url(record_or_hash, :action => "#{action}", :routing_type => :path) + end + EOT + end + + + private + def action_prefix(options) + options[:action] ? "#{options[:action]}_" : "" + end + + def routing_type(options) + "#{options[:routing_type] || "url"}" + end + + def build_named_route_call(records, namespace, inflection, options = {}) + records = Array.new([extract_record(records)]) unless records.is_a?(Array) + base_segment = "#{RecordIdentifier.send!("#{inflection}_class_name", records.pop)}_" + + method_root = records.reverse.inject(base_segment) do |string, name| + segment = "#{RecordIdentifier.send!("singular_class_name", name)}_" + segment << string + end + + action_prefix(options) + namespace + method_root + routing_type(options) + end + + def extract_record(record_or_hash_or_array) + case record_or_hash_or_array + when Array; record_or_hash_or_array.last + when Hash; record_or_hash_or_array[:id] + else record_or_hash_or_array + end + end + + def extract_namespace(record_or_hash_or_array) + returning "" do |namespace| + if record_or_hash_or_array.is_a?(Array) + record_or_hash_or_array.delete_if do |record_or_namespace| + if record_or_namespace.is_a?(String) || record_or_namespace.is_a?(Symbol) + namespace << "#{record_or_namespace.to_s}_" + end + end + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/record_identifier.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/record_identifier.rb new file mode 100644 index 000000000..bdf2753a7 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/record_identifier.rb @@ -0,0 +1,91 @@ +module ActionController + # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or + # Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate + # the view actions to a higher logical level. Example: + # + # # routes + # map.resources :posts + # + # # view + # <% div_for(post) do %> <div id="post_45" class="post"> + # <%= post.body %> What a wonderful world! + # <% end %> </div> + # + # # controller + # def destroy + # post = Post.find(params[:id]) + # post.destroy + # + # respond_to do |format| + # format.html { redirect_to(post) } # Calls polymorphic_url(post) which in turn calls post_url(post) + # format.js do + # # Calls: new Effect.fade('post_45'); + # render(:update) { |page| page[post].visual_effect(:fade) } + # end + # end + # end + # + # As the example above shows, you can stop caring to a large extent what the actual id of the post is. You just know + # that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming + # convention and allows you to write less code if you follow it. + module RecordIdentifier + extend self + + # Returns plural/singular for a record or class. Example: + # + # partial_path(post) # => "posts/post" + # partial_path(Person) # => "people/person" + def partial_path(record_or_class) + klass = class_from_record_or_class(record_or_class) + "#{klass.name.tableize}/#{klass.name.demodulize.underscore}" + end + + # The DOM class convention is to use the singular form of an object or class. Examples: + # + # dom_class(post) # => "post" + # dom_class(Person) # => "person" + # + # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class: + # + # dom_class(post, :edit) # => "edit_post" + # dom_class(Person, :edit) # => "edit_person" + def dom_class(record_or_class, prefix = nil) + [ prefix, singular_class_name(record_or_class) ].compact * '_' + end + + # The DOM class convention is to use the singular form of an object or class with the id following an underscore. + # If no id is found, prefix with "new_" instead. Examples: + # + # dom_class(Post.new(:id => 45)) # => "post_45" + # dom_class(Post.new) # => "new_post" + # + # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id: + # + # dom_class(Post.new(:id => 45), :edit) # => "edit_post_45" + def dom_id(record, prefix = nil) + prefix ||= 'new' unless record.id + [ prefix, singular_class_name(record), record.id ].compact * '_' + end + + # Returns the plural class name of a record or class. Examples: + # + # plural_class_name(post) # => "posts" + # plural_class_name(Highrise::Person) # => "highrise_people" + def plural_class_name(record_or_class) + singular_class_name(record_or_class).pluralize + end + + # Returns the singular class name of a record or class. Examples: + # + # singular_class_name(post) # => "post" + # singular_class_name(Highrise::Person) # => "highrise_person" + def singular_class_name(record_or_class) + class_from_record_or_class(record_or_class).name.underscore.tr('/', '_') + end + + private + def class_from_record_or_class(record_or_class) + record_or_class.is_a?(Class) ? record_or_class : record_or_class.class + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/request.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/request.rb new file mode 100755 index 000000000..19948da9c --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/request.rb @@ -0,0 +1,730 @@ +require 'tempfile' +require 'stringio' +require 'strscan' + +module ActionController + # HTTP methods which are accepted by default. + ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options )) + + # CgiRequest and TestRequest provide concrete implementations. + class AbstractRequest + cattr_accessor :relative_url_root + remove_method :relative_url_root + + # The hash of environment variables for this request, + # such as { 'RAILS_ENV' => 'production' }. + attr_reader :env + + # The true HTTP request method as a lowercase symbol, such as :get. + # UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS. + def request_method + @request_method ||= begin + method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase + if ACCEPTED_HTTP_METHODS.include?(method) + method.to_sym + else + raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}" + end + end + end + + # The HTTP request method as a lowercase symbol, such as :get. + # Note, HEAD is returned as :get since the two are functionally + # equivalent from the application's perspective. + def method + request_method == :head ? :get : request_method + end + + # Is this a GET (or HEAD) request? Equivalent to request.method == :get + def get? + method == :get + end + + # Is this a POST request? Equivalent to request.method == :post + def post? + request_method == :post + end + + # Is this a PUT request? Equivalent to request.method == :put + def put? + request_method == :put + end + + # Is this a DELETE request? Equivalent to request.method == :delete + def delete? + request_method == :delete + end + + # Is this a HEAD request? request.method sees HEAD as :get, so check the + # HTTP method directly. + def head? + request_method == :head + end + + def headers + @env + end + + def content_length + @content_length ||= env['CONTENT_LENGTH'].to_i + end + + # The MIME type of the HTTP request, such as Mime::XML. + # + # For backward compatibility, the post format is extracted from the + # X-Post-Data-Format HTTP header if present. + def content_type + @content_type ||= Mime::Type.lookup(content_type_without_parameters) + end + + # Returns the accepted MIME type for the request + def accepts + @accepts ||= + if @env['HTTP_ACCEPT'].to_s.strip.empty? + [ content_type, Mime::ALL ].compact # make sure content_type being nil is not included + else + Mime::Type.parse(@env['HTTP_ACCEPT']) + end + end + + # Returns the Mime type for the format used in the request. If there is no format available, the first of the + # accept types will be used. Examples: + # + # GET /posts/5.xml | request.format => Mime::XML + # GET /posts/5.xhtml | request.format => Mime::HTML + # GET /posts/5 | request.format => request.accepts.first (usually Mime::HTML for browsers) + def format + @format ||= parameters[:format] ? Mime::Type.lookup_by_extension(parameters[:format]) : accepts.first + end + + + # Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension. + # Example: + # + # class ApplicationController < ActionController::Base + # before_filter :adjust_format_for_iphone + # + # private + # def adjust_format_for_iphone + # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] + # end + # end + def format=(extension) + parameters[:format] = extension.to_s + format + 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? + !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i) + 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. + # + # Security note: do not use if IP spoofing is a concern for your + # application. Since remote_ip checks HTTP headers for addresses forwarded + # by proxies, the client may send any IP. remote_addr can't be spoofed but + # also doesn't work behind a proxy, since it's always the proxy's 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.strip =~ /^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 lowercase name of the HTTP server software. + def server_software + (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil + end + + + # Returns the complete URL used for this request + def url + protocol + host_with_port + request_uri + 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 host for this request, such as example.com. + def host + end + + # Returns a host:port string for this request, such as example.com or + # example.com:8080. + def host_with_port + @host_with_port ||= host + port_string + 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 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 unless named_host?(host) + + 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 named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + # Return the query string, accounting for server idiosyncracies. + def query_string + if uri = @env['REQUEST_URI'] + uri.split('?', 2)[1] || '' + else + @env['QUERY_STRING'] || '' + end + end + + # Return the request URI, accounting for server idiosyncracies. + # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. + def request_uri + if uri = @env['REQUEST_URI'] + # Remove domain, which webrick puts into the request_uri. + (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri + else + # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. + 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 + + if uri.nil? + @env.delete('REQUEST_URI') + uri + else + @env['REQUEST_URI'] = uri + end + end + 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.to_s : '' + + # Cut off the path to the installation directory if given + path.sub!(%r/^#{relative_url_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 + + + # Read the request body. This is useful for web services that need to + # work with raw requests directly. + def raw_post + unless env.include? 'RAW_POST_DATA' + env['RAW_POST_DATA'] = body.read(content_length) + body.rewind if body.respond_to?(:rewind) + end + env['RAW_POST_DATA'] + end + + # Returns both GET and POST parameters in a single hash. + def parameters + @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access + 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. + # Returned hash keys are strings. See <tt>symbolized_path_parameters</tt> for symbolized keys. + # + # Example: + # + # {'action' => 'my_action', 'controller' => 'my_controller'} + def path_parameters + @path_parameters ||= {} + end + + + #-- + # Must be implemented in the concrete request + #++ + + # The request body is an IO input stream. + def body + end + + def query_parameters #:nodoc: + end + + def request_parameters #:nodoc: + end + + def cookies #:nodoc: + end + + def session #:nodoc: + end + + def session=(session) #:nodoc: + @session = session + end + + def reset_session #:nodoc: + end + + protected + # The raw content type string. Use when you need parameters such as + # charset or boundary which aren't included in the content_type MIME type. + # Overridden by the X-POST_DATA_FORMAT header for backward compatibility. + def content_type_with_parameters + content_type_from_legacy_post_data_format_header || + env['CONTENT_TYPE'].to_s + end + + # The raw content type string with its parameters stripped off. + def content_type_without_parameters + @content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters) + end + + private + def content_type_from_legacy_post_data_format_header + if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] + case x_post_format.to_s.downcase + when 'yaml'; 'application/x-yaml' + when 'xml'; 'application/xml' + end + end + end + + def parse_formatted_request_parameters + return {} if content_length.zero? + + content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters) + + # Don't parse params for unknown requests. + return {} if content_type.blank? + + mime_type = Mime::Type.lookup(content_type) + strategy = ActionController::Base.param_parsers[mime_type] + + # Only multipart form parsing expects a stream. + body = (strategy && strategy != :multipart_form) ? raw_post : self.body + + case strategy + when Proc + strategy.call(body) + when :url_encoded_form + self.class.clean_up_ajax_request_body! body + self.class.parse_query_parameters(body) + when :multipart_form + self.class.parse_multipart_form_parameters(body, boundary, content_length, env) + when :xml_simple, :xml_node + body.blank? ? {} : Hash.from_xml(body).with_indifferent_access + when :yaml + YAML.load(body) + else + {} + end + rescue Exception => e # YAML, XML or Ruby code block errors + raise + { "body" => body, + "content_type" => content_type_with_parameters, + "content_length" => content_length, + "exception" => "#{e.message} (#{e.class})", + "backtrace" => e.backtrace } + end + + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + + class << self + def parse_query_parameters(query_string) + return {} if query_string.blank? + + pairs = query_string.split('&').collect do |chunk| + next if chunk.empty? + key, value = chunk.split('=', 2) + next if key.empty? + value = value.nil? ? nil : CGI.unescape(value) + [ CGI.unescape(key), value ] + end.compact + + UrlEncodedPairParser.new(pairs).result + end + + def parse_request_parameters(params) + parser = UrlEncodedPairParser.new + + params = params.dup + until params.empty? + for key, value in params + if key.blank? + params.delete key + elsif !key.include?('[') + # much faster to test for the most common case first (GET) + # and avoid the call to build_deep_hash + parser.result[key] = get_typed_value(value[0]) + params.delete key + elsif value.is_a?(Array) + parser.parse(key, get_typed_value(value.shift)) + params.delete key if value.empty? + else + raise TypeError, "Expected array, found #{value.inspect}" + end + end + end + + parser.result + end + + def parse_multipart_form_parameters(body, boundary, content_length, env) + parse_request_parameters(read_multipart(body, boundary, content_length, env)) + end + + def extract_multipart_boundary(content_type_with_parameters) + if content_type_with_parameters =~ MULTIPART_BOUNDARY + ['multipart/form-data', $1.dup] + else + extract_content_type_without_parameters(content_type_with_parameters) + end + end + + def extract_content_type_without_parameters(content_type_with_parameters) + $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/ + end + + def clean_up_ajax_request_body!(body) + body.chop! if body[-1] == 0 + body.gsub!(/&_=$/, '') + end + + + private + def get_typed_value(value) + case value + when String + value + when NilClass + '' + when Array + value.map { |v| get_typed_value(v) } + else + if value.is_a?(UploadedFile) + # Uploaded file + if value.original_filename + value + # Multipart param + else + result = value.read + value.rewind + result + end + # Unknown value, neither string nor multipart. + else + raise "Unknown form value: #{value.inspect}" + end + end + end + + + MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n + + EOL = "\015\012" + + def read_multipart(body, boundary, content_length, env) + params = Hash.new([]) + boundary = "--" + boundary + quoted_boundary = Regexp.quote(boundary, "n") + buf = "" + bufsize = 10 * 1024 + boundary_end="" + + # start multipart/form-data + body.binmode if defined? body.binmode + boundary_size = boundary.size + EOL.size + content_length -= boundary_size + status = body.read(boundary_size) + if nil == status + raise EOFError, "no content body" + elsif boundary + EOL != status + raise EOFError, "bad content body" + end + + loop do + head = nil + content = + if 10240 < content_length + UploadedTempfile.new("CGI") + else + UploadedStringIO.new + end + content.binmode if defined? content.binmode + + until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf) + + if (not head) and /#{EOL}#{EOL}/n.match(buf) + buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do + head = $1.dup + "" + end + next + end + + if head and ( (EOL + boundary + EOL).size < buf.size ) + content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] + buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" + end + + c = if bufsize < content_length + body.read(bufsize) + else + body.read(content_length) + end + if c.nil? || c.empty? + raise EOFError, "bad content body" + end + buf.concat(c) + content_length -= c.size + end + + buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do + content.print $1 + if "--" == $2 + content_length = -1 + end + boundary_end = $2.dup + "" + end + + content.rewind + + head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni + if filename = $1 || $2 + if /Mac/ni.match(env['HTTP_USER_AGENT']) and + /Mozilla/ni.match(env['HTTP_USER_AGENT']) and + (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) + filename = CGI.unescape(filename) + end + content.original_path = filename.dup + end + + head =~ /Content-Type: ([^\r]*)/ni + content.content_type = $1.dup if $1 + + head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni + name = $1.dup if $1 + + if params.has_key?(name) + params[name].push(content) + else + params[name] = [content] + end + break if buf.size == 0 + break if content_length == -1 + end + raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/ + + begin + body.rewind if body.respond_to?(:rewind) + rescue Errno::ESPIPE + # Handles exceptions raised by input streams that cannot be rewound + # such as when using plain CGI under Apache + end + + params + end + end + end + + class UrlEncodedPairParser < StringScanner #:nodoc: + attr_reader :top, :parent, :result + + def initialize(pairs = []) + super('') + @result = {} + pairs.each { |key, value| parse(key, value) } + end + + KEY_REGEXP = %r{([^\[\]=&]+)} + BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} + + # Parse the query string + def parse(key, value) + self.string = key + @top, @parent = result, nil + + # First scan the bare key + key = scan(KEY_REGEXP) or return + key = post_key_check(key) + + # Then scan as many nestings as present + until eos? + r = scan(BRACKETED_KEY_REGEXP) or return + key = self[1] + key = post_key_check(key) + end + + bind(key, value) + end + + private + # After we see a key, we must look ahead to determine our next action. Cases: + # + # [] follows the key. Then the value must be an array. + # = follows the key. (A value comes next) + # & or the end of string follows the key. Then the key is a flag. + # otherwise, a hash follows the key. + def post_key_check(key) + if scan(/\[\]/) # a[b][] indicates that b is an array + container(key, Array) + nil + elsif check(/\[[^\]]/) # a[b] indicates that a is a hash + container(key, Hash) + nil + else # End of key? We do nothing. + key + end + end + + # Add a container to the stack. + def container(key, klass) + type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) + value = bind(key, klass.new) + type_conflict! klass, value unless value.is_a?(klass) + push(value) + end + + # Push a value onto the 'stack', which is actually only the top 2 items. + def push(value) + @parent, @top = @top, value + end + + # Bind a key (which may be nil for items in an array) to the provided value. + def bind(key, value) + if top.is_a? Array + if key + if top[-1].is_a?(Hash) && ! top[-1].key?(key) + top[-1][key] = value + else + top << {key => value}.with_indifferent_access + push top.last + end + else + top << value + end + elsif top.is_a? Hash + key = CGI.unescape(key) + parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) + return top[key] ||= value + else + raise ArgumentError, "Don't know what to do: top is #{top.inspect}" + end + + return value + end + + def type_conflict!(klass, value) + raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value." + end + end + + module UploadedFile + def self.included(base) + base.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path + end + end + + # 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 + unless defined? @original_filename + @original_filename = + unless original_path.blank? + if original_path =~ /^(?:.*[:\\\/])?(.*)/m + $1 + else + File.basename original_path + end + end + end + @original_filename + end + end + + class UploadedStringIO < StringIO + include UploadedFile + end + + class UploadedTempfile < Tempfile + include UploadedFile + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/request_forgery_protection.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/request_forgery_protection.rb new file mode 100644 index 000000000..75f9c0b28 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/request_forgery_protection.rb @@ -0,0 +1,132 @@ +module ActionController #:nodoc: + class InvalidAuthenticityToken < ActionControllerError #:nodoc: + end + + module RequestForgeryProtection + def self.included(base) + base.class_eval do + class_inheritable_accessor :request_forgery_protection_options + self.request_forgery_protection_options = {} + helper_method :form_authenticity_token + helper_method :protect_against_forgery? + end + base.extend(ClassMethods) + end + + module ClassMethods + # Protect a controller's actions from CSRF attacks by ensuring that all forms are coming from the current web application, not + # a forged link from another site. This is done by embedding a token based on the session (which an attacker wouldn't know) in + # all forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only + # HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication + # scheme there anyway). Also, GET requests are not protected as these should be indempotent anyway. + # + # You turn this on with the #protect_from_forgery method, which will perform the check and raise + # an ActionController::InvalidAuthenticityToken if the token doesn't match what was expected. And it will add + # a _authenticity_token parameter to all forms that are automatically generated by Rails. You can customize the error message + # given through public/422.html. + # + # Learn more about CSRF (Cross-Site Request Forgery) attacks: + # + # * http://isc.sans.org/diary.html?storyid=1750 + # * http://en.wikipedia.org/wiki/Cross-site_request_forgery + # + # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. + # There are a few guidelines you should follow: + # + # * Keep your GET requests safe and idempotent. More reading material: + # * http://www.xml.com/pub/a/2002/04/24/deviant.html + # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 + # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look for "Expires: at end of session" + # + # If you need to construct a request yourself, but still want to take advantage of forgery protection, you can grab the + # authenticity_token using the form_authenticity_token helper method and make it part of the parameters yourself. + # + # Example: + # + # class FooController < ApplicationController + # # uses the cookie session store (then you don't need a separate :secret) + # protect_from_forgery :except => :index + # + # # uses one of the other session stores that uses a session_id value. + # protect_from_forgery :secret => 'my-little-pony', :except => :index + # + # # you can disable csrf protection on controller-by-controller basis: + # skip_before_filter :verify_authenticity_token + # end + # + # If you are upgrading from Rails 1.x, disable forgery protection to + # simplify your tests. Add this to config/environments/test.rb: + # + # # Disable request forgery protection in test environment + # config.action_controller.allow_forgery_protection = false + # + # Valid Options: + # + # * <tt>:only/:except</tt> - passed to the before_filter call. Set which actions are verified. + # * <tt>:secret</tt> - Custom salt used to generate the form_authenticity_token. + # Leave this off if you are using the cookie session store. + # * <tt>:digest</tt> - Message digest used for hashing. Defaults to 'SHA1' + def protect_from_forgery(options = {}) + self.request_forgery_protection_token ||= :authenticity_token + before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except) + request_forgery_protection_options.update(options) + end + end + + protected + # The actual before_filter that is used. Modify this to change how you handle unverified requests. + def verify_authenticity_token + verified_request? || raise(ActionController::InvalidAuthenticityToken) + end + + # Returns true or false if a request is verified. Checks: + # + # * is the format restricted? By default, only HTML and AJAX requests are checked. + # * is it a GET request? Gets should be safe and idempotent + # * Does the form_authenticity_token match the given _token value from the params? + def verified_request? + !protect_against_forgery? || + request.method == :get || + !verifiable_request_format? || + form_authenticity_token == params[request_forgery_protection_token] + end + + def verifiable_request_format? + request.format.html? || request.format.js? + end + + # Sets the token value for the current session. Pass a :secret option in #protect_from_forgery to add a custom salt to the hash. + def form_authenticity_token + @form_authenticity_token ||= if request_forgery_protection_options[:secret] + authenticity_token_from_session_id + elsif session.respond_to?(:dbman) && session.dbman.respond_to?(:generate_digest) + authenticity_token_from_cookie_session + elsif session.nil? + raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session." + else + raise InvalidAuthenticityToken, "No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)." + end + end + + # Generates a unique digest using the session_id and the CSRF secret. + def authenticity_token_from_session_id + key = if request_forgery_protection_options[:secret].respond_to?(:call) + request_forgery_protection_options[:secret].call(@session) + else + request_forgery_protection_options[:secret] + end + digest = request_forgery_protection_options[:digest] ||= 'SHA1' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(digest), key.to_s, session.session_id.to_s) + end + + # No secret was given, so assume this is a cookie session store. + def authenticity_token_from_cookie_session + session[:csrf_id] ||= CGI::Session.generate_unique_id + session.dbman.generate_digest(session[:csrf_id]) + end + + def protect_against_forgery? + allow_forgery_protection && request_forgery_protection_token + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/request_profiler.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/request_profiler.rb new file mode 100755 index 000000000..62f6e665f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/request_profiler.rb @@ -0,0 +1,138 @@ +require 'optparse' +require 'action_controller/integration' + +module ActionController + class RequestProfiler + # Wrap up the integration session runner. + class Sandbox + include Integration::Runner + + def self.benchmark(n, script) + new(script).benchmark(n) + end + + def initialize(script_path) + @quiet = false + define_run_method(File.read(script_path)) + reset! + end + + def benchmark(n) + @quiet = true + print ' ' + result = Benchmark.realtime do + n.times do |i| + run + print i % 10 == 0 ? 'x' : '.' + $stdout.flush + end + end + puts + result + ensure + @quiet = false + end + + def say(message) + puts " #{message}" unless @quiet + end + + private + def define_run_method(script) + instance_eval "def run; #{script}; end", __FILE__, __LINE__ + end + end + + + attr_reader :options + + def initialize(options = {}) + @options = default_options.merge(options) + end + + + def self.run(args = nil, options = {}) + profiler = new(options) + profiler.parse_options(args) if args + profiler.run + end + + def run + sandbox = Sandbox.new(options[:script]) + + puts 'Warming up once' + + elapsed = warmup(sandbox) + puts '%.2f sec, %d requests, %d req/sec' % [elapsed, sandbox.request_count, sandbox.request_count / elapsed] + puts "\n#{options[:benchmark] ? 'Benchmarking' : 'Profiling'} #{options[:n]}x" + + options[:benchmark] ? benchmark(sandbox) : profile(sandbox) + end + + def profile(sandbox) + load_ruby_prof + + results = RubyProf.profile { benchmark(sandbox) } + + show_profile_results results + results + end + + def benchmark(sandbox) + sandbox.request_count = 0 + elapsed = sandbox.benchmark(options[:n]).to_f + count = sandbox.request_count.to_i + puts '%.2f sec, %d requests, %d req/sec' % [elapsed, count, count / elapsed] + end + + def warmup(sandbox) + Benchmark.realtime { sandbox.run } + end + + def default_options + { :n => 100, :open => 'open %s &' } + end + + # Parse command-line options + def parse_options(args) + OptionParser.new do |opt| + opt.banner = "USAGE: #{$0} [options] [session script path]" + + opt.on('-n', '--times [0000]', 'How many requests to process. Defaults to 100.') { |v| options[:n] = v.to_i } + opt.on('-b', '--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = v } + opt.on('--open [CMD]', 'Command to open profile results. Defaults to "open %s &"') { |v| options[:open] = v } + opt.on('-h', '--help', 'Show this help') { puts opt; exit } + + opt.parse args + + if args.empty? + puts opt + exit + end + options[:script] = args.pop + end + end + + protected + def load_ruby_prof + begin + require 'ruby-prof' + #RubyProf.measure_mode = RubyProf::ALLOCATED_OBJECTS + rescue LoadError + abort '`gem install ruby-prof` to use the profiler' + end + end + + def show_profile_results(results) + File.open "#{RAILS_ROOT}/tmp/profile-graph.html", 'w' do |file| + RubyProf::GraphHtmlPrinter.new(results).print(file) + `#{options[:open] % file.path}` if options[:open] + end + + File.open "#{RAILS_ROOT}/tmp/profile-flat.txt", 'w' do |file| + RubyProf::FlatPrinter.new(results).print(file) + `#{options[:open] % file.path}` if options[:open] + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/rescue.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/rescue.rb new file mode 100644 index 000000000..b91115a93 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/rescue.rb @@ -0,0 +1,258 @@ +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. + # + # The default behavior for public exceptions is to render a static html file with the name of the error code thrown. If no such + # file exists, an empty response is sent with the correct status code. + # + # You can override what constitutes a local request by overriding the <tt>local_request?</tt> method in your own controller. + # Custom rescue behavior is achieved by overriding the <tt>rescue_action_in_public</tt> and <tt>rescue_action_locally</tt> methods. + module Rescue + LOCALHOST = '127.0.0.1'.freeze + + DEFAULT_RESCUE_RESPONSE = :internal_server_error + DEFAULT_RESCUE_RESPONSES = { + 'ActionController::RoutingError' => :not_found, + 'ActionController::UnknownAction' => :not_found, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity + } + + DEFAULT_RESCUE_TEMPLATE = 'diagnostics' + DEFAULT_RESCUE_TEMPLATES = { + 'ActionController::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + 'ActionController::UnknownAction' => 'unknown_action', + 'ActionView::TemplateError' => 'template_error' + } + + def self.included(base) #:nodoc: + base.cattr_accessor :rescue_responses + base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE) + base.rescue_responses.update DEFAULT_RESCUE_RESPONSES + + base.cattr_accessor :rescue_templates + base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE) + base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES + + base.class_inheritable_array :rescue_handlers + base.rescue_handlers = [] + + base.extend(ClassMethods) + base.class_eval do + alias_method_chain :perform_action, :rescue + end + end + + module ClassMethods + def process_with_exception(request, response, exception) #:nodoc: + new.process(request, response, :rescue_action, exception) + end + + # Rescue exceptions raised in controller actions. + # + # <tt>rescue_from</tt> receives a series of exception classes or class + # names, and a trailing :with option with the name of a method or a Proc + # object to be called to handle them. Alternatively a block can be given. + # + # Handlers that take one argument will be called with the exception, so + # that the exception can be inspected when dealing with it. + # + # Handlers are inherited. They are searched from right to left, from + # bottom to top, and up the hierarchy. The handler of the first class for + # which exception.is_a?(klass) holds true is the one invoked, if any. + # + # class ApplicationController < ActionController::Base + # rescue_from User::NotAuthorized, :with => :deny_access # self defined exception + # rescue_from ActiveRecord::RecordInvalid, :with => :show_errors + # + # rescue_from 'MyAppError::Base' do |exception| + # render :xml => exception, :status => 500 + # end + # + # protected + # def deny_access + # ... + # end + # + # def show_errors(exception) + # exception.record.new_record? ? ... + # end + # end + def rescue_from(*klasses, &block) + options = klasses.extract_options! + unless options.has_key?(:with) + block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.") + end + + klasses.each do |klass| + key = if klass.is_a?(Class) && klass <= Exception + klass.name + elsif klass.is_a?(String) + klass + else + raise(ArgumentError, "#{klass} is neither an Exception nor a String") + end + + # Order is important, we put the pair at the end. When dealing with an + # exception we will follow the documented order going from right to left. + rescue_handlers << [key, options[:with]] + end + 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? + + # Let the exception alter the response if it wants. + # For example, MethodNotAllowed sets the Allow header. + if exception.respond_to?(:handle_response!) + exception.handle_response!(response) + end + + 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: + ActiveSupport::Deprecation.silence do + 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 + end + + # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By + # default will call render_optional_error_file. Override this method to provide more user friendly error messages.s + def rescue_action_in_public(exception) #:doc: + render_optional_error_file response_code_for_rescue(exception) + end + + # Attempts to render a static error page based on the <tt>status_code</tt> thrown, + # or just return headers if no such file exists. For example, if a 500 error is + # being handled Rails will first attempt to render the file at <tt>public/500.html</tt>. + # If the file doesn't exist, the body of the response will be left empty. + def render_optional_error_file(status_code) + status = interpret_status(status_code) + path = "#{RAILS_ROOT}/public/#{status[0,3]}.html" + if File.exist?(path) + render :file => path, :status => status + else + head status + end + end + + # True if the request came from localhost, 127.0.0.1. Override this + # method if you wish to redefine the meaning of a local request to + # include remote IP addresses or other criteria. + def local_request? #:doc: + request.remote_addr == LOCALHOST and request.remote_ip == LOCALHOST + end + + # Render detailed diagnostics for unhandled exceptions rescued from + # a controller action. + def rescue_action_locally(exception) + add_variables_to_assigns + @template.instance_variable_set("@exception", exception) + @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub"))) + @template.send!(:assign_variables_from_controller) + + @template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false)) + + response.content_type = Mime::HTML + render_for_file(rescues_path("layout"), response_code_for_rescue(exception)) + end + + # Tries to rescue the exception by looking up and calling a registered handler. + def rescue_action_with_handler(exception) + if handler = handler_for_rescue(exception) + if handler.arity != 0 + handler.call(exception) + else + handler.call + end + true # don't rely on the return value of the handler + end + end + + private + def perform_action_with_rescue #:nodoc: + perform_action_without_rescue + rescue Exception => exception # errors from action performed + return if rescue_action_with_handler(exception) + + rescue_action(exception) + end + + def rescues_path(template_name) + "#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb" + end + + def template_path_for_local_rescue(exception) + rescues_path(rescue_templates[exception.class.name]) + end + + def response_code_for_rescue(exception) + rescue_responses[exception.class.name] + end + + def handler_for_rescue(exception) + # We go from right to left because pairs are pushed onto rescue_handlers + # as rescue_from declarations are found. + _, handler = *rescue_handlers.reverse.detect do |klass_name, handler| + # The purpose of allowing strings in rescue_from is to support the + # declaration of handler associations for exception classes whose + # definition is yet unknown. + # + # Since this loop needs the constants it would be inconsistent to + # assume they should exist at this point. An early raised exception + # could trigger some other handler and the array could include + # precisely a string whose corresponding constant has not yet been + # seen. This is why we are tolerant to unknown constants. + # + # Note that this tolerance only matters if the exception was given as + # a string, otherwise a NameError will be raised by the interpreter + # itself when rescue_from CONSTANT is executed. + klass = self.class.const_get(klass_name) rescue nil + klass ||= klass_name.constantize rescue nil + exception.is_a?(klass) if klass + end + + case handler + when Symbol + method(handler) + when Proc + handler.bind(self) + end + end + + def clean_backtrace(exception) + if backtrace = exception.backtrace + if defined?(RAILS_ROOT) + backtrace.map { |line| line.sub RAILS_ROOT, '' } + else + backtrace + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/resources.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/resources.rb new file mode 100644 index 000000000..8ec6b84a5 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/resources.rb @@ -0,0 +1,529 @@ +module ActionController + # == Overview + # + # ActionController::Resources are a way of defining RESTful resources. A RESTful resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a resource (the noun). + # + # Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # +GET+ Requests for a resource, no saving or editing of a resource should occur in a GET request + # +POST+ Creation of resources + # +PUT+ Editing of attributes on a resource + # +DELETE+ Deletion of a resource + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + module Resources + class Resource #:nodoc: + attr_reader :collection_methods, :member_methods, :new_methods + attr_reader :path_prefix, :name_prefix + attr_reader :plural, :singular + attr_reader :options + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + + @options = options + + arrange_actions + add_default_actions + set_prefixes + end + + def controller + @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" + end + + def requirements(with_id = false) + @requirements ||= @options[:requirements] || {} + @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } + + with_id ? @requirements.merge(@id_requirement) : @requirements + end + + def conditions + @conditions = @options[:conditions] || {} + end + + def path + @path ||= "#{path_prefix}/#{plural}" + end + + def new_path + @new_path ||= "#{path}/new" + end + + def member_path + @member_path ||= "#{path}/:id" + end + + def nesting_path_prefix + @nesting_path_prefix ||= "#{path}/:#{singular}_id" + end + + def nesting_name_prefix + "#{name_prefix}#{singular}_" + end + + def action_separator + @action_separator ||= Base.resource_action_separator + end + + def uncountable? + @singular.to_s == @plural.to_s + end + + protected + def arrange_actions + @collection_methods = arrange_actions_by_methods(options.delete(:collection)) + @member_methods = arrange_actions_by_methods(options.delete(:member)) + @new_methods = arrange_actions_by_methods(options.delete(:new)) + end + + def add_default_actions + add_default_action(member_methods, :get, :edit) + add_default_action(new_methods, :get, :new) + end + + def set_prefixes + @path_prefix = options.delete(:path_prefix) + @name_prefix = options.delete(:name_prefix) + end + + def arrange_actions_by_methods(actions) + (actions || {}).inject({}) do |flipped_hash, (key, value)| + (flipped_hash[value] ||= []) << key + flipped_hash + end + end + + def add_default_action(collection, method, action) + (collection[method] ||= []).unshift(action) + end + end + + class SingletonResource < Resource #:nodoc: + def initialize(entity, options) + @singular = @plural = entity + options[:controller] ||= @singular.to_s.pluralize + super + end + + alias_method :member_path, :path + alias_method :nesting_path_prefix, :path + end + + # Creates named routes for implementing verb-oriented controllers + # for a collection resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, #resources generates named routes for use in + # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of #url_for or methods that take #url_for parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # The #resources method accepts the following options to customize the resulting routes: + # * <tt>:collection</tt> - add named routes for other actions that operate on the collection. + # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt> + # or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of rss_messages_url. + # * <tt>:member</tt> - same as :collection, but for actions that operate on a specific member. + # * <tt>:new</tt> - same as :collection, but for actions that operate on the new resource action. + # * <tt>:controller</tt> - specify the controller name for the routes. + # * <tt>:singular</tt> - specify the singular name used in the member routes. + # * <tt>:requirements</tt> - set custom routing parameter requirements. + # * <tt>:conditions</tt> - specify custom routing recognition conditions. Resources sets the :method value for the method-specific routes. + # * <tt>:path_prefix</tt> - set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use resources like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest resources calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment resources work the same, but must now include a value for :article_id. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # * <tt>:name_prefix</tt> - define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use :name_prefix to override the generic named routes in a nested resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The #resources method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in + # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton resource. + # A singleton resource is global to its current context. For unnested singleton resources, + # the resource is global to the current user visiting the application, such as a user's + # /account profile. For nested singleton resources, the resource is global to its parent + # resource, such as a <tt>projects</tt> resource that <tt>has_one :project_manager</tt>. + # The <tt>project_manager</tt> should be mapped as a singleton resource under <tt>projects</tt>: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See map.resources for general conventions. These are the main differences: + # * A singular name is given to map.resource. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the :plural option. There is no :singular option. + # * No default index route is created for the singleton resource controller. + # * When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, #resource generates named routes for + # use in controllers and views. <tt>map.resource :account</tt> produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + def resource(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } + end + + private + def map_resource(entities, options = {}, &block) + resource = Resource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_collection_actions(map, resource) + map_default_collection_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + + map_associations(resource, options) + + if block_given? + with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block) + end + end + end + + def map_singleton_resource(entities, options = {}, &block) + resource = SingletonResource.new(entities, options) + + with_options :controller => resource.controller do |map| + map_collection_actions(map, resource) + map_default_singleton_actions(map, resource) + map_new_actions(map, resource) + map_member_actions(map, resource) + + map_associations(resource, options) + + if block_given? + with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block) + end + end + end + + def map_associations(resource, options) + path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" + name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" + + Array(options[:has_many]).each do |association| + resources(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace]) + end + + Array(options[:has_one]).each do |association| + resource(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace]) + end + end + + def map_collection_actions(map, resource) + resource.collection_methods.each do |method, actions| + actions.each do |action| + action_options = action_options_for(action, resource, method) + map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options) + map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options) + end + end + end + + def map_default_collection_actions(map, resource) + index_action_options = action_options_for("index", resource) + index_route_name = "#{resource.name_prefix}#{resource.plural}" + + if resource.uncountable? + index_route_name << "_index" + end + + map.named_route(index_route_name, resource.path, index_action_options) + map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options) + + create_action_options = action_options_for("create", resource) + map.connect(resource.path, create_action_options) + map.connect("#{resource.path}.:format", create_action_options) + end + + def map_default_singleton_actions(map, resource) + create_action_options = action_options_for("create", resource) + map.connect(resource.path, create_action_options) + map.connect("#{resource.path}.:format", create_action_options) + end + + def map_new_actions(map, resource) + resource.new_methods.each do |method, actions| + actions.each do |action| + action_options = action_options_for(action, resource, method) + if action == :new + map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options) + map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options) + else + map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options) + map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options) + end + end + end + end + + def map_member_actions(map, resource) + resource.member_methods.each do |method, actions| + actions.each do |action| + action_options = action_options_for(action, resource, method) + map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}", action_options) + map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format",action_options) + end + end + + show_action_options = action_options_for("show", resource) + map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options) + map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options) + + update_action_options = action_options_for("update", resource) + map.connect(resource.member_path, update_action_options) + map.connect("#{resource.member_path}.:format", update_action_options) + + destroy_action_options = action_options_for("destroy", resource) + map.connect(resource.member_path, destroy_action_options) + map.connect("#{resource.member_path}.:format", destroy_action_options) + end + + def add_conditions_for(conditions, method) + returning({:conditions => conditions.dup}) do |options| + options[:conditions][:method] = method unless method == :any + end + end + + def action_options_for(action, resource, method = nil) + default_options = { :action => action.to_s } + require_id = !resource.kind_of?(SingletonResource) + case default_options[:action] + when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) + when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) + when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) + when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) + when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) + else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements) + end + end + end +end + +class ActionController::Routing::RouteSet::Mapper + include ActionController::Resources +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/response.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/response.rb new file mode 100755 index 000000000..1d9f6676b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/response.rb @@ -0,0 +1,76 @@ +require 'digest/md5' + +module ActionController + class AbstractResponse #:nodoc: + DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } + attr_accessor :request + attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout + + def initialize + @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] + end + + def content_type=(mime_type) + self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type + end + + def content_type + content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0] + content_type.blank? ? nil : content_type + end + + def charset=(encoding) + self.headers["Content-Type"] = "#{content_type || Mime::HTML}; charset=#{encoding}" + end + + def charset + charset = String(headers["Content-Type"] || headers["type"]).split(";")[1] + charset.blank? ? nil : charset.strip.split("=")[1] + end + + def redirect(to_url, response_status) + self.headers["Status"] = response_status + self.headers["Location"] = to_url + + self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>" + end + + def prepare! + handle_conditional_get! + convert_content_type! + set_content_length! + end + + + private + def handle_conditional_get! + if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty? + self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}") + self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] + + if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag'] + self.headers['Status'] = '304 Not Modified' + self.body = '' + end + end + end + + def convert_content_type! + if content_type = headers.delete("Content-Type") + self.headers["type"] = content_type + end + if content_type = headers.delete("Content-type") + self.headers["type"] = content_type + end + if content_type = headers.delete("content-type") + self.headers["type"] = content_type + end + end + + # Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice + # for, say, a 2GB streaming file. + def set_content_length! + self.headers["Content-Length"] = body.size unless body.respond_to?(:call) + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/routing.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/routing.rb new file mode 100644 index 000000000..edd4683f6 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/routing.rb @@ -0,0 +1,1499 @@ +require 'cgi' +require 'uri' +require 'action_controller/polymorphic_routes' +require 'action_controller/routing_optimisation' + +class Object + def to_param + to_s + end +end + +class TrueClass + def to_param + self + end +end + +class FalseClass + def to_param + self + end +end + +class NilClass + def to_param + self + end +end + +class Regexp #:nodoc: + def number_of_captures + Regexp.new("|#{source}").match('').captures.length + end + + class << self + def optionalize(pattern) + case unoptionalize(pattern) + when /\A(.|\(.*\))\Z/ then "#{pattern}?" + else "(?:#{pattern})?" + end + end + + def unoptionalize(pattern) + [/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp| + return $1 if regexp =~ pattern + end + return pattern + end + end +end + +module ActionController + # == Routing + # + # The routing module provides URL rewriting in native Ruby. It's a way to + # redirect incoming requests to controllers and actions. This replaces + # mod_rewrite rules. Best of all, Rails' Routing works with any web server. + # Routes are defined in routes.rb in your RAILS_ROOT/config directory. + # + # Consider the following route, installed by Rails when you generate your + # application: + # + # map.connect ':controller/:action/:id' + # + # This route states that it expects requests to consist of a + # :controller followed by an :action that in turn is fed some :id. + # + # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up + # with: + # + # params = { :controller => 'blog', + # :action => 'edit', + # :id => '22' + # } + # + # Think of creating routes as drawing a map for your requests. The map tells + # them where to go based on some predefined pattern: + # + # ActionController::Routing::Routes.draw do |map| + # Pattern 1 tells some request to go to one place + # Pattern 2 tell them to go to another + # ... + # end + # + # The following symbols are special: + # + # :controller maps to your controller name + # :action maps to an action with your controllers + # + # Other names simply map to a parameter as in the case of +:id+. + # + # == Route priority + # + # Not all routes are created equally. Routes have priority defined by the + # order of appearance of the routes in the routes.rb file. The priority goes + # from top to bottom. The last route in that file is at the lowest priority + # and will be applied last. If no route matches, 404 is returned. + # + # Within blocks, the empty pattern is at the highest priority. + # In practice this works out nicely: + # + # ActionController::Routing::Routes.draw do |map| + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # end + # map.connect ':controller/:action/:view' + # end + # + # In this case, invoking blog controller (with an URL like '/blog/') + # without parameters will activate the 'list' action by default. + # + # == Defaults routes and default parameters + # + # Setting a default route is straightforward in Rails - you simply append a + # Hash at the end of your mapping to set any default parameters. + # + # Example: + # ActionController::Routing:Routes.draw do |map| + # map.connect ':controller/:action/:id', :controller => 'blog' + # end + # + # This sets up +blog+ as the default controller if no other is specified. + # This means visiting '/' would invoke the blog controller. + # + # More formally, you can define defaults in a route with the +:defaults+ key. + # + # map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' } + # + # == Named routes + # + # Routes can be named with the syntax <tt>map.name_of_route options</tt>, + # allowing for easy reference within your source as +name_of_route_url+ + # for the full URL and +name_of_route_path+ for the URI path. + # + # Example: + # # In routes.rb + # map.login 'login', :controller => 'accounts', :action => 'login' + # + # # With render, redirect_to, tests, etc. + # redirect_to login_url + # + # Arguments can be passed as well. + # + # redirect_to show_item_path(:id => 25) + # + # Use <tt>map.root</tt> as a shorthand to name a route for the root path "". + # + # # In routes.rb + # map.root :controller => 'blogs' + # + # # would recognize http://www.example.com/ as + # params = { :controller => 'blogs', :action => 'index' } + # + # # and provide these named routes + # root_url # => 'http://www.example.com/' + # root_path # => '' + # + # Note: when using +with_options+, the route is simply named after the + # method you call on the block parameter rather than map. + # + # # In routes.rb + # map.with_options :controller => 'blog' do |blog| + # blog.show '', :action => 'list' + # blog.delete 'delete/:id', :action => 'delete', + # blog.edit 'edit/:id', :action => 'edit' + # end + # + # # provides named routes for show, delete, and edit + # link_to @article.title, show_path(:id => @article.id) + # + # == Pretty URLs + # + # Routes can generate pretty URLs. For example: + # + # map.connect 'articles/:year/:month/:day', + # :controller => 'articles', + # :action => 'find_by_date', + # :year => /\d{4}/, + # :month => /\d{1,2}/, + # :day => /\d{1,2}/ + # + # # Using the route above, the url below maps to: + # # params = {:year => '2005', :month => '11', :day => '06'} + # # http://localhost:3000/articles/2005/11/06 + # + # == Regular Expressions and parameters + # You can specify a regular expression to define a format for a parameter. + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :postalcode => /\d{5}(-\d{4})?/ + # + # or, more formally: + # + # map.geocode 'geocode/:postalcode', :controller => 'geocode', + # :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ } + # + # == Route globbing + # + # Specifying <tt>*[string]</tt> as part of a rule like: + # + # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' + # + # will glob all remaining parts of the route that were not recognized earlier. This idiom + # must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in + # this case. + # + # == Route conditions + # + # With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>. + # + # * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>, + # <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>, + # <tt>:any</tt> means that any method can access the route. + # + # Example: + # + # map.connect 'post/:id', :controller => 'posts', :action => 'show', + # :conditions => { :method => :get } + # map.connect 'post/:id', :controller => 'posts', :action => 'create_comment', + # :conditions => { :method => :post } + # + # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same + # URL will route to the <tt>show</tt> action. + # + # == Reloading routes + # + # You can reload routes if you feel you must: + # + # ActionController::Routing::Routes.reload + # + # This will clear all named routes and reload routes.rb if the file has been modified from + # last load. To absolutely force reloading, use +reload!+. + # + # == Testing Routes + # + # The two main methods for testing your routes: + # + # === +assert_routing+ + # + # def test_movie_route_properly_splits + # opts = {:controller => "plugin", :action => "checkout", :id => "2"} + # assert_routing "plugin/checkout/2", opts + # end + # + # +assert_routing+ lets you test whether or not the route properly resolves into options. + # + # === +assert_recognizes+ + # + # def test_route_has_options + # opts = {:controller => "plugin", :action => "show", :id => "12"} + # assert_recognizes opts, "/plugins/show/12" + # end + # + # Note the subtle difference between the two: +assert_routing+ tests that + # a URL fits options while +assert_recognizes+ tests that a URL + # breaks into parameters properly. + # + # In tests you can simply pass the URL or named route to +get+ or +post+. + # + # def send_to_jail + # get '/jail' + # assert_response :success + # assert_template "jail/front" + # end + # + # def goes_to_login + # get login_url + # #... + # end + # + # == View a list of all your routes + # + # Run <tt>rake routes</tt>. + # + module Routing + SEPARATORS = %w( / . ? ) + + HTTP_METHODS = [:get, :head, :post, :put, :delete] + + ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set + + # The root paths which may contain controller files + mattr_accessor :controller_paths + self.controller_paths = [] + + # A helper module to hold URL related helpers. + module Helpers + include PolymorphicRoutes + end + + class << self + def with_controllers(names) + prior_controllers = @possible_controllers + use_controllers! names + yield + ensure + use_controllers! prior_controllers + end + + def normalize_paths(paths) + # do the hokey-pokey of path normalization... + paths = paths.collect do |path| + path = path. + gsub("//", "/"). # replace double / chars with a single + gsub("\\\\", "\\"). # replace double \ chars with a single + gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it + + # eliminate .. paths where possible + re = %r{\w+[/\\]\.\.[/\\]} + path.gsub!(%r{\w+[/\\]\.\.[/\\]}, "") while path.match(re) + path + end + + # start with longest path, first + paths = paths.uniq.sort_by { |path| - path.length } + end + + def possible_controllers + unless @possible_controllers + @possible_controllers = [] + + paths = controller_paths.select { |path| File.directory?(path) && path != "." } + + seen_paths = Hash.new {|h, k| h[k] = true; false} + normalize_paths(paths).each do |load_path| + Dir["#{load_path}/**/*_controller.rb"].collect do |path| + next if seen_paths[path.gsub(%r{^\.[/\\]}, "")] + + controller_name = path[(load_path.length + 1)..-1] + + controller_name.gsub!(/_controller\.rb\Z/, '') + @possible_controllers << controller_name + end + end + + # remove duplicates + @possible_controllers.uniq! + end + @possible_controllers + end + + def use_controllers!(controller_names) + @possible_controllers = controller_names + 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 + end + + class Route #:nodoc: + attr_accessor :segments, :requirements, :conditions, :optimise + + def initialize + @segments = [] + @requirements = {} + @conditions = {} + @optimise = true + end + + # Indicates whether the routes should be optimised with the string interpolation + # version of the named routes methods. + def optimise? + @optimise && ActionController::Base::optimise_named_routes + end + + def segment_keys + segments.collect do |segment| + segment.key if segment.respond_to? :key + end.compact + end + + # Write and compile a +generate+ method for this Route. + def write_generation + # Build the main body of the generation + body = "expired = false\n#{generation_extraction}\n#{generation_structure}" + + # If we have conditions that must be tested first, nest the body inside an if + body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements + args = "options, hash, expire_on = {}" + + # Nest the body inside of a def block, and then compile it. + raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + + # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash + # are the same as the keys that were recalled from the previous request. Thus, + # we can use the expire_on.keys to determine which keys ought to be used to build + # the query string. (Never use keys from the recalled request when building the + # query string.) + + method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + + method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + raw_method + end + + # Build several lines of code that extract values from the options hash. If any + # of the values are missing or rejected then a return will be executed. + def generation_extraction + segments.collect do |segment| + segment.extraction_code + end.compact * "\n" + end + + # Produce a condition expression that will check the requirements of this route + # upon generation. + def generation_requirements + requirement_conditions = requirements.collect do |key, req| + if req.is_a? Regexp + value_regexp = Regexp.new "\\A#{req.source}\\Z" + "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" + else + "hash[:#{key}] == #{req.inspect}" + end + end + requirement_conditions * ' && ' unless requirement_conditions.empty? + end + + def generation_structure + segments.last.string_structure segments[0..-2] + end + + # Write and compile a +recognize+ method for this Route. + def write_recognition + # Create an if structure to extract the params from a match if it occurs. + body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" + body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" + + # Build the method declaration and compile it + method_decl = "def recognize(path, env={})\n#{body}\nend" + instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" + method_decl + end + + # Plugins may override this method to add other conditions, like checks on + # host, subdomain, and so forth. Note that changes here only affect route + # recognition, not generation. + def recognition_conditions + result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] + result << "conditions[:method] === env[:method]" if conditions[:method] + result + end + + # Build the regular expression pattern that will match this route. + def recognition_pattern(wrap = true) + pattern = '' + segments.reverse_each do |segment| + pattern = segment.build_pattern pattern + end + wrap ? ("\\A" + pattern + "\\Z") : pattern + end + + # Write the code to extract the parameters from a matched route. + def recognition_extraction + next_capture = 1 + extraction = segments.collect do |segment| + x = segment.match_extraction(next_capture) + next_capture += Regexp.new(segment.regexp_chunk).number_of_captures + x + end + extraction.compact + end + + # Write the real generation implementation and then resend the message. + def generate(options, hash, expire_on = {}) + write_generation + generate options, hash, expire_on + end + + def generate_extras(options, hash, expire_on = {}) + write_generation + generate_extras options, hash, expire_on + end + + # Generate the query string with any extra keys in the hash and append + # it to the given path, returning the new path. + def append_query_string(path, hash, query_keys=nil) + return nil unless path + query_keys ||= extra_keys(hash) + "#{path}#{build_query_string(hash, query_keys)}" + end + + # Determine which keys in the given hash are "extra". Extra keys are + # those that were not used to generate a particular route. The extra + # keys also do not include those recalled from the prior request, nor + # do they include any keys that were implied in the route (like a + # :controller that is required, but not explicitly used in the text of + # the route.) + def extra_keys(hash, recall={}) + (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys + end + + # Build a query string from the keys of the given hash. If +only_keys+ + # is given (as an array), only the keys indicated will be used to build + # the query string. The query string will correctly build array parameter + # values. + def build_query_string(hash, only_keys = nil) + elements = [] + + (only_keys || hash.keys).each do |key| + if value = hash[key] + elements << value.to_query(key) + end + end + + elements.empty? ? '' : "?#{elements.sort * '&'}" + end + + # Write the real recognition implementation and then resend the message. + def recognize(path, environment={}) + write_recognition + recognize path, environment + end + + # A route's parameter shell contains parameter values that are not in the + # route's path, but should be placed in the recognized hash. + # + # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: + # + # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ + # + def parameter_shell + @parameter_shell ||= returning({}) do |shell| + requirements.each do |key, requirement| + shell[key] = requirement unless requirement.is_a? Regexp + end + end + end + + # Return an array containing all the keys that are used in this route. This + # includes keys that appear inside the path, and keys that have requirements + # placed upon them. + def significant_keys + @significant_keys ||= returning [] do |sk| + segments.each { |segment| sk << segment.key if segment.respond_to? :key } + sk.concat requirements.keys + sk.uniq! + end + end + + # Return a hash of key/value pairs representing the keys in the route that + # have defaults, or which are specified by non-regexp requirements. + def defaults + @defaults ||= returning({}) do |hash| + segments.each do |segment| + next unless segment.respond_to? :default + hash[segment.key] = segment.default unless segment.default.nil? + end + requirements.each do |key,req| + next if Regexp === req || req.nil? + hash[key] = req + end + end + end + + def matches_controller_and_action?(controller, action) + unless defined? @matching_prepared + @controller_requirement = requirement_for(:controller) + @action_requirement = requirement_for(:action) + @matching_prepared = true + end + + (@controller_requirement.nil? || @controller_requirement === controller) && + (@action_requirement.nil? || @action_requirement === action) + end + + def to_s + @to_s ||= begin + segs = segments.inject("") { |str,s| str << s.to_s } + "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] + end + end + + protected + def requirement_for(key) + return requirements[key] if requirements.key? key + segments.each do |segment| + return segment.regexp if segment.respond_to?(:key) && segment.key == key + end + nil + end + + end + + class Segment #:nodoc: + RESERVED_PCHAR = ':@&=+$,;' + UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + + attr_accessor :is_optional + alias_method :optional?, :is_optional + + def initialize + self.is_optional = false + end + + def extraction_code + nil + end + + # Continue generating string for the prior segments. + def continue_string_structure(prior_segments) + if prior_segments.empty? + interpolation_statement(prior_segments) + else + new_priors = prior_segments[0..-2] + prior_segments.last.string_structure(new_priors) + end + end + + def interpolation_chunk + URI.escape(value, UNSAFE_PCHAR) + end + + # Return a string interpolation statement for this segment and those before it. + def interpolation_statement(prior_segments) + chunks = prior_segments.collect { |s| s.interpolation_chunk } + chunks << interpolation_chunk + "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" + end + + def string_structure(prior_segments) + optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) + end + + # Return an if condition that is true if all the prior segments can be generated. + # If there are no optional segments before this one, then nil is returned. + def all_optionals_available_condition(prior_segments) + optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact + optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" + end + + # Recognition + + def match_extraction(next_capture) + nil + end + + # Warning + + # Returns true if this segment is optional? because of a default. If so, then + # no warning will be emitted regarding this segment. + def optionality_implied? + false + end + end + + class StaticSegment < Segment #:nodoc: + attr_accessor :value, :raw + alias_method :raw?, :raw + + def initialize(value = nil) + super() + self.value = value + end + + def interpolation_chunk + raw? ? value : super + end + + def regexp_chunk + chunk = Regexp.escape(value) + optional? ? Regexp.optionalize(chunk) : chunk + end + + def build_pattern(pattern) + escaped = Regexp.escape(value) + if optional? && ! pattern.empty? + "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" + elsif optional? + Regexp.optionalize escaped + else + escaped + pattern + end + end + + def to_s + value + end + end + + class DividerSegment < StaticSegment #:nodoc: + def initialize(value = nil) + super(value) + self.raw = true + self.is_optional = true + end + + def optionality_implied? + true + end + end + + class DynamicSegment < Segment #:nodoc: + attr_accessor :key, :default, :regexp + + def initialize(key = nil, options = {}) + super() + self.key = key + self.default = options[:default] if options.key? :default + self.is_optional = true if options[:optional] || options.key?(:default) + end + + def to_s + ":#{key}" + end + + # The local variable name that the value of this segment will be extracted to. + def local_name + "#{key}_value" + end + + def extract_value + "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" + end + def value_check + if default # Then we know it won't be nil + "#{value_regexp.inspect} =~ #{local_name}" if regexp + elsif optional? + # If we have a regexp check that the value is not given, or that it matches. + # If we have no regexp, return nil since we do not require a condition. + "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp + else # Then it must be present, and if we have a regexp, it must match too. + "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" + end + end + def expiry_statement + "expired, hash = true, options if !expired && expire_on[:#{key}]" + end + + def extraction_code + s = extract_value + vc = value_check + s << "\nreturn [nil,nil] unless #{vc}" if vc + s << "\n#{expiry_statement}" + end + + def interpolation_chunk(value_code = "#{local_name}") + "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" + end + + def string_structure(prior_segments) + if optional? # We have a conditional to do... + # If we should not appear in the url, just write the code for the prior + # segments. This occurs if our value is the default value, or, if we are + # optional, if we have nil as our value. + "if #{local_name} == #{default.inspect}\n" + + continue_string_structure(prior_segments) + + "\nelse\n" + # Otherwise, write the code up to here + "#{interpolation_statement(prior_segments)}\nend" + else + interpolation_statement(prior_segments) + end + end + + def value_regexp + Regexp.new "\\A#{regexp.source}\\Z" if regexp + end + def regexp_chunk + regexp ? "(#{regexp.source})" : "([^#{Routing::SEPARATORS.join}]+)" + end + + def build_pattern(pattern) + chunk = regexp_chunk + chunk = "(#{chunk})" if Regexp.new(chunk).number_of_captures == 0 + pattern = "#{chunk}#{pattern}" + optional? ? Regexp.optionalize(pattern) : pattern + end + def match_extraction(next_capture) + # All non code-related keys (such as :id, :slug) are URI-unescaped as + # path parameters. + default_value = default ? default.inspect : nil + %[ + value = if (m = match[#{next_capture}]) + URI.unescape(m) + else + #{default_value} + end + params[:#{key}] = value if value + ] + end + + def optionality_implied? + [:action, :id].include? key + end + + end + + class ControllerSegment < DynamicSegment #:nodoc: + def regexp_chunk + possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } + "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" + end + + # Don't URI.escape the controller name since it may contain slashes. + def interpolation_chunk(value_code = "#{local_name}") + "\#{#{value_code}.to_s}" + end + + # Make sure controller names like Admin/Content are correctly normalized to + # admin/content + def extract_value + "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" + end + + def match_extraction(next_capture) + if default + "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'" + else + "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]" + end + end + end + + class PathSegment < DynamicSegment #:nodoc: + RESERVED_PCHAR = "#{Segment::RESERVED_PCHAR}/" + UNSAFE_PCHAR = Regexp.new("[^#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}]", false, 'N').freeze + + def interpolation_chunk(value_code = "#{local_name}") + "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::PathSegment::UNSAFE_PCHAR)}" + end + + def default + '' + end + + def default=(path) + raise RoutingError, "paths cannot have non-empty default values" unless path.blank? + end + + def match_extraction(next_capture) + "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" + end + + def regexp_chunk + regexp || "(.*)" + end + + def optionality_implied? + true + end + + class Result < ::Array #:nodoc: + def to_s() join '/' end + def self.new_escaped(strings) + new strings.collect {|str| URI.unescape str} + end + end + end + + class RouteBuilder #:nodoc: + attr_accessor :separators, :optional_separators + + def initialize + self.separators = Routing::SEPARATORS + self.optional_separators = %w( / ) + end + + def separator_pattern(inverted = false) + "[#{'^' if inverted}#{Regexp.escape(separators.join)}]" + end + + def interval_regexp + Regexp.new "(.*?)(#{separators.source}|$)" + end + + # Accepts a "route path" (a string defining a route), and returns the array + # of segments that corresponds to it. Note that the segment array is only + # partially initialized--the defaults and requirements, for instance, need + # to be set separately, via the #assign_route_options method, and the + # #optional? method for each segment will not be reliable until after + # #assign_route_options is called, as well. + def segments_for_route_path(path) + rest, segments = path, [] + + until rest.empty? + segment, rest = segment_for rest + segments << segment + end + segments + end + + # A factory method that returns a new segment instance appropriate for the + # format of the given string. + def segment_for(string) + segment = case string + when /\A:(\w+)/ + key = $1.to_sym + case key + when :controller then ControllerSegment.new(key) + else DynamicSegment.new key + end + when /\A\*(\w+)/ then PathSegment.new($1.to_sym, :optional => true) + when /\A\?(.*?)\?/ + returning segment = StaticSegment.new($1) do + segment.is_optional = true + end + when /\A(#{separator_pattern(:inverted)}+)/ then StaticSegment.new($1) + when Regexp.new(separator_pattern) then + returning segment = DividerSegment.new($&) do + segment.is_optional = (optional_separators.include? $&) + end + end + [segment, $~.post_match] + end + + # Split the given hash of options into requirement and default hashes. The + # segments are passed alongside in order to distinguish between default values + # and requirements. + def divide_route_options(segments, options) + options = options.dup + + if options[:namespace] + options[:controller] = "#{options[:path_prefix]}/#{options[:controller]}" + options.delete(:path_prefix) + options.delete(:name_prefix) + options.delete(:namespace) + end + + requirements = (options.delete(:requirements) || {}).dup + defaults = (options.delete(:defaults) || {}).dup + conditions = (options.delete(:conditions) || {}).dup + + path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact + options.each do |key, value| + hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements + hash[key] = value + end + + [defaults, requirements, conditions] + end + + # Takes a hash of defaults and a hash of requirements, and assigns them to + # the segments. Any unused requirements (which do not correspond to a segment) + # are returned as a hash. + def assign_route_options(segments, defaults, requirements) + route_requirements = {} # Requirements that do not belong to a segment + + segment_named = Proc.new do |key| + segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } + end + + requirements.each do |key, requirement| + segment = segment_named[key] + if segment + raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + segment.regexp = requirement + else + route_requirements[key] = requirement + end + end + + defaults.each do |key, default| + segment = segment_named[key] + raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment + segment.is_optional = true + segment.default = default.to_param if default + end + + assign_default_route_options(segments) + ensure_required_segments(segments) + route_requirements + end + + # Assign default options, such as 'index' as a default for :action. This + # method must be run *after* user supplied requirements and defaults have + # been applied to the segments. + def assign_default_route_options(segments) + segments.each do |segment| + next unless segment.is_a? DynamicSegment + case segment.key + when :action + if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index' + segment.default ||= 'index' + segment.is_optional = true + end + when :id + if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ '' + segment.is_optional = true + end + end + end + end + + # Makes sure that there are no optional segments that precede a required + # segment. If any are found that precede a required segment, they are + # made required. + def ensure_required_segments(segments) + allow_optional = true + segments.reverse_each do |segment| + allow_optional &&= segment.optional? + if !allow_optional && segment.optional? + unless segment.optionality_implied? + warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." + end + segment.is_optional = false + elsif allow_optional && segment.respond_to?(:default) && segment.default + # if a segment has a default, then it is optional + segment.is_optional = true + end + end + end + + # Construct and return a route with the given path and options. + def build(path, options) + # Wrap the path with slashes + path = "/#{path}" unless path[0] == ?/ + path = "#{path}/" unless path[-1] == ?/ + + path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix] + + segments = segments_for_route_path(path) + defaults, requirements, conditions = divide_route_options(segments, options) + requirements = assign_route_options(segments, defaults, requirements) + + route = Route.new + + route.segments = segments + route.requirements = requirements + route.conditions = conditions + + if !route.significant_keys.include?(:action) && !route.requirements[:action] + route.requirements[:action] = "index" + route.significant_keys << :action + end + + # Routes cannot use the current string interpolation method + # if there are user-supplied :requirements as the interpolation + # code won't raise RoutingErrors when generating + if options.key?(:requirements) || route.requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION + route.optimise = false + end + + if !route.significant_keys.include?(:controller) + raise ArgumentError, "Illegal route: the :controller must be specified!" + end + + route + end + end + + class RouteSet #:nodoc: + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + class Mapper #:doc: + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionController::Routing for an introduction to routes. + def connect(path, options = {}) + @set.add_route(path, options) + end + + # Creates a named route called "root" for matching the root level request. + def root(options = {}) + named_route("root", '', options) + end + + def named_route(name, path, options = {}) #:nodoc: + @set.add_named_route(name, path, options) + end + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + def namespace(name, options = {}, &block) + if options[:namespace] + with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) + else + with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + end + end + + def method_missing(route_name, *args, &proc) #:nodoc: + super unless args.length >= 1 && proc.nil? + @set.add_named_route(route_name, *args) + end + end + + # A NamedRouteCollection instance is a collection of named routes, and also + # maintains an anonymous module that can be used to install helpers for the + # named routes. + class NamedRouteCollection #:nodoc: + include Enumerable + include ActionController::Routing::Optimisation + attr_reader :routes, :helpers + + def initialize + clear! + end + + def clear! + @routes = {} + @helpers = [] + + @module ||= Module.new + @module.instance_methods.each do |selector| + @module.class_eval { remove_method selector } + end + end + + def add(name, route) + routes[name.to_sym] = route + define_named_route_methods(name, route) + end + + def get(name) + routes[name.to_sym] + end + + alias []= add + alias [] get + alias clear clear! + + def each + routes.each { |name, route| yield name, route } + self + end + + def names + routes.keys + end + + def length + routes.length + end + + def reset! + old_routes = routes.dup + clear! + old_routes.each do |name, route| + add(name, route) + end + end + + def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) + reset! if regenerate + Array(destinations).each do |dest| + dest.send! :include, @module + end + end + + private + def url_helper_name(name, kind = :url) + :"#{name}_#{kind}" + end + + def hash_access_name(name, kind = :url) + :"hash_for_#{name}_#{kind}" + end + + def define_named_route_methods(name, route) + {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| + hash = route.defaults.merge(:use_route => name).merge(opts) + define_hash_access route, name, kind, hash + define_url_helper route, name, kind, hash + end + end + + def define_hash_access(route, name, kind, options) + selector = hash_access_name(name, kind) + @module.module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(options = nil) + options ? #{options.inspect}.merge(options) : #{options.inspect} + end + protected :#{selector} + end_eval + helpers << selector + end + + def define_url_helper(route, name, kind, options) + selector = url_helper_name(name, kind) + # The segment keys used for positional paramters + + hash_access_method = hash_access_name(name, kind) + + # allow ordered parameters to be associated with corresponding + # dynamic segments, so you can do + # + # foo_url(bar, baz, bang) + # + # instead of + # + # foo_url(:bar => bar, :baz => baz, :bang => bang) + # + # Also allow options hash, so you can do + # + # foo_url(bar, baz, bang, :sort_by => 'baz') + # + @module.module_eval <<-end_eval # We use module_eval to avoid leaks + def #{selector}(*args) + #{generate_optimisation_block(route, kind)} + + opts = if args.empty? || Hash === args.first + args.first || {} + else + options = args.last.is_a?(Hash) ? args.pop : {} + args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| + h[k] = v + h + end + options.merge(args) + end + + url_for(#{hash_access_method}(opts)) + end + protected :#{selector} + end_eval + helpers << selector + end + end + + attr_accessor :routes, :named_routes + + def initialize + self.routes = [] + self.named_routes = NamedRouteCollection.new + end + + # Subclasses and plugins may override this method to specify a different + # RouteBuilder instance, so that other route DSL's can be created. + def builder + @builder ||= RouteBuilder.new + end + + def draw + clear! + yield Mapper.new(self) + install_helpers + end + + def clear! + routes.clear + named_routes.clear + @combined_regexp = nil + @routes_by_controller = nil + end + + def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) + Array(destinations).each { |d| d.module_eval { include Helpers } } + named_routes.install(destinations, regenerate_code) + end + + def empty? + routes.empty? + end + + def load! + Routing.use_controllers! nil # Clear the controller cache so we may discover new ones + clear! + load_routes! + install_helpers + end + + # reload! will always force a reload whereas load checks the timestamp first + alias reload! load! + + def reload + if @routes_last_modified && defined?(RAILS_ROOT) + mtime = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime + # if it hasn't been changed, then just return + return if mtime == @routes_last_modified + # if it has changed then record the new time and fall to the load! below + @routes_last_modified = mtime + end + load! + end + + def load_routes! + if defined?(RAILS_ROOT) && defined?(::ActionController::Routing::Routes) && self == ::ActionController::Routing::Routes + load File.join("#{RAILS_ROOT}/config/routes.rb") + @routes_last_modified = File.stat("#{RAILS_ROOT}/config/routes.rb").mtime + else + add_route ":controller/:action/:id" + end + end + + def add_route(path, options = {}) + route = builder.build(path, options) + routes << route + route + end + + def add_named_route(name, path, options = {}) + # TODO - is options EVER used? + name = options[:name_prefix] + name.to_s if options[:name_prefix] + named_routes[name.to_sym] = add_route(path, options) + end + + def options_as_params(options) + # If an explicit :controller was given, always make :action explicit + # too, so that action expiry works as expected for things like + # + # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + # + # (the above is from the unit tests). In the above case, because the + # controller was explicitly given, but no action, the action is implied to + # be "index", not the recalled action of "show". + # + # great fun, eh? + + options_as_params = options.clone + options_as_params[:action] ||= 'index' if options[:controller] + options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] + options_as_params + end + + def build_expiry(options, recall) + recall.inject({}) do |expiry, (key, recalled_value)| + expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) + expiry + end + end + + # Generate the path indicated by the arguments, and return an array of + # the keys that were not used to generate it. + def extra_keys(options, recall={}) + generate_extras(options, recall).last + end + + def generate_extras(options, recall={}) + generate(options, recall, :generate_extras) + end + + def generate(options, recall = {}, method=:generate) + named_route_name = options.delete(:use_route) + generate_all = options.delete(:generate_all) + if named_route_name + named_route = named_routes[named_route_name] + options = named_route.parameter_shell.merge(options) + end + + options = options_as_params(options) + expire_on = build_expiry(options, recall) + + if options[:controller] + options[:controller] = options[:controller].to_s + end + # if the controller has changed, make sure it changes relative to the + # current controller module, if any. In other words, if we're currently + # on admin/get, and the new controller is 'set', the new controller + # should really be admin/set. + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ + old_parts = recall[:controller].split('/') + new_parts = options[:controller].split('/') + parts = old_parts[0..-(new_parts.length + 1)] + new_parts + options[:controller] = parts.join('/') + end + + # drop the leading '/' on the controller name + options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ + merged = recall.merge(options) + + if named_route + path = named_route.generate(options, merged, expire_on) + if path.nil? + raise_named_route_error(options, named_route, named_route_name) + else + return path + end + else + merged[:action] ||= 'index' + options[:action] ||= 'index' + + controller = merged[:controller] + action = merged[:action] + + raise RoutingError, "Need controller and action!" unless controller && action + + if generate_all + # Used by caching to expire all paths for a resource + return routes.collect do |route| + route.send!(method, options, merged, expire_on) + end.compact + end + + # don't use the recalled keys when determining which routes to check + routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }] + + routes.each do |route| + results = route.send!(method, options, merged, expire_on) + return results if results && (!results.is_a?(Array) || results.first) + end + end + + raise RoutingError, "No route matches #{options.inspect}" + end + + # try to give a helpful error message when named route generation fails + def raise_named_route_error(options, named_route, named_route_name) + diff = named_route.requirements.diff(options) + unless diff.empty? + raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" + else + required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } + required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment + raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" + end + end + + def recognize(request) + params = recognize_path(request.path, extract_request_environment(request)) + request.path_parameters = params.with_indifferent_access + "#{params[:controller].camelize}Controller".constantize + end + + def recognize_path(path, environment={}) + routes.each do |route| + result = route.recognize(path, environment) and return result + end + + allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } } + + if environment[:method] && !HTTP_METHODS.include?(environment[:method]) + raise NotImplemented.new(*allows) + elsif !allows.empty? + raise MethodNotAllowed.new(*allows) + else + raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}" + end + end + + def routes_by_controller + @routes_by_controller ||= Hash.new do |controller_hash, controller| + controller_hash[controller] = Hash.new do |action_hash, action| + action_hash[action] = Hash.new do |key_hash, keys| + key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) + end + end + end + end + + def routes_for(options, merged, expire_on) + raise "Need controller and action!" unless controller && action + controller = merged[:controller] + merged = options if expire_on[:controller] + action = merged[:action] || 'index' + + routes_by_controller[controller][action][merged.keys] + end + + def routes_for_controller_and_action(controller, action) + selected = routes.select do |route| + route.matches_controller_and_action? controller, action + end + (selected.length == routes.length) ? routes : selected + end + + def routes_for_controller_and_action_and_keys(controller, action, keys) + selected = routes.select do |route| + route.matches_controller_and_action? controller, action + end + selected.sort_by do |route| + (keys - route.significant_keys).length + end + end + + # Subclasses and plugins may override this method to extract further attributes + # from the request, for use by route conditions and such. + def extract_request_environment(request) + { :method => request.method } + end + end + + Routes = RouteSet.new + + ::Inflector.module_eval do + def inflections_with_route_reloading(&block) + returning(inflections_without_route_reloading(&block)) { + ActionController::Routing::Routes.reload! if block_given? + } + end + + alias_method_chain :inflections, :route_reloading + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/routing_optimisation.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/routing_optimisation.rb new file mode 100644 index 000000000..ba4aeb4e8 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/routing_optimisation.rb @@ -0,0 +1,119 @@ +module ActionController + module Routing + # Much of the slow performance from routes comes from the + # complexity of expiry, :requirements matching, defaults providing + # and figuring out which url pattern to use. With named routes + # we can avoid the expense of finding the right route. So if + # they've provided the right number of arguments, and have no + # :requirements, we can just build up a string and return it. + # + # To support building optimisations for other common cases, the + # generation code is separated into several classes + module Optimisation + def generate_optimisation_block(route, kind) + return "" unless route.optimise? + OPTIMISERS.inject("") do |memo, klazz| + memo << klazz.new(route, kind).source_code + memo + end + end + + class Optimiser + attr_reader :route, :kind + def initialize(route, kind) + @route = route + @kind = kind + end + + def guard_condition + 'false' + end + + def generation_code + 'nil' + end + + def source_code + if applicable? + "return #{generation_code} if #{guard_condition}\n" + else + "\n" + end + end + + # Temporarily disabled :url optimisation pending proper solution to + # Issues around request.host etc. + def applicable? + true + end + end + + # Given a route: + # map.person '/people/:id' + # + # If the user calls person_url(@person), we can simply + # return a string like "/people/#{@person.to_param}" + # rather than triggering the expensive logic in url_for + class PositionalArguments < Optimiser + def guard_condition + number_of_arguments = route.segment_keys.size + # if they're using foo_url(:id=>2) it's one + # argument, but we don't want to generate /foos/id2 + if number_of_arguments == 1 + "defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)" + else + "defined?(request) && request && args.size == #{number_of_arguments}" + end + end + + def generation_code + elements = [] + idx = 0 + + if kind == :url + elements << '#{request.protocol}' + elements << '#{request.host_with_port}' + end + + elements << '#{request.relative_url_root if request.relative_url_root}' + + # The last entry in route.segments appears to # *always* be a + # 'divider segment' for '/' but we have assertions to ensure that + # we don't include the trailing slashes, so skip them. + (route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment| + if segment.is_a?(DynamicSegment) + elements << segment.interpolation_chunk("args[#{idx}].to_param") + idx += 1 + else + elements << segment.interpolation_chunk + end + end + %("#{elements * ''}") + end + end + + # This case is mostly the same as the positional arguments case + # above, but it supports additional query parameters as the last + # argument + class PositionalArgumentsWithAdditionalParams < PositionalArguments + def guard_condition + "defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)" + end + + # This case uses almost the same code as positional arguments, + # but add an args.last.to_query on the end + def generation_code + super.insert(-2, '?#{args.last.to_query}') + end + + # To avoid generating http://localhost/?host=foo.example.com we + # can't use this optimisation on routes without any segments + def applicable? + super && route.segment_keys.size > 0 + end + end + + OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams] + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session/active_record_store.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/active_record_store.rb new file mode 100644 index 000000000..14747c505 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/active_record_store.rb @@ -0,0 +1,336 @@ +require 'cgi' +require 'cgi/session' +require 'digest/md5' +require 'base64' + +class CGI + class Session + attr_reader :data + + # 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 + + attr_writer :data + + # Has the session been loaded yet? + def loaded? + !! @data + end + + private + + 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
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session/cookie_store.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/cookie_store.rb new file mode 100644 index 000000000..086f5a87a --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/cookie_store.rb @@ -0,0 +1,164 @@ +require 'cgi' +require 'cgi/session' +require 'base64' # to convert Marshal.dump to ASCII +require 'openssl' # to generate the HMAC message digest + +# This cookie-based session store is the Rails default. Sessions typically +# contain at most a user_id and flash message; both fit within the 4K cookie +# size limit. Cookie-based sessions are dramatically faster than the +# alternatives. +# +# If you have more than 4K of session data or don't want your data to be +# visible to the user, pick another session store. +# +# CookieOverflow is raised if you attempt to store more than 4K of data. +# TamperedWithCookie is raised if the data integrity check fails. +# +# A message digest is included with the cookie to ensure data integrity: +# a user cannot alter his user_id without knowing the secret key included in +# the hash. New apps are generated with a pregenerated secret in +# config/environment.rb. Set your own for old apps you're upgrading. +# +# Session options: +# :secret An application-wide key string or block returning a string +# called per generated digest. The block is called with the +# CGI::Session instance as an argument. It's important that the +# secret is not vulnerable to a dictionary attack. Therefore, +# you should choose a secret consisting of random numbers and +# letters and more than 30 characters. +# +# Example: :secret => '449fe2e7daee471bffae2fd8dc02313d' +# :secret => Proc.new { User.current_user.secret_key } +# +# :digest The message digest algorithm used to verify session integrity +# defaults to 'SHA1' but may be any digest provided by OpenSSL, +# such as 'MD5', 'RIPEMD160', 'SHA256', etc. +# +# To generate a secret key for an existing application, run +# `rake secret` and set the key in config/environment.rb +# +# Note that changing digest or secret invalidates all existing sessions! +class CGI::Session::CookieStore + # Cookies can typically store 4096 bytes. + MAX = 4096 + SECRET_MIN_LENGTH = 30 # characters + + # Raised when storing more than 4K of session data. + class CookieOverflow < StandardError; end + + # Raised when the cookie fails its integrity check. + class TamperedWithCookie < StandardError; end + + # Called from CGI::Session only. + def initialize(session, options = {}) + # The session_key option is required. + if options['session_key'].blank? + raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb' + end + + # The secret option is required. + ensure_secret_secure(options['secret']) + + # Keep the session and its secret on hand so we can read and write cookies. + @session, @secret = session, options['secret'] + + # Message digest defaults to SHA1. + @digest = options['digest'] || 'SHA1' + + # Default cookie options derived from session settings. + @cookie_options = { + 'name' => options['session_key'], + 'path' => options['session_path'], + 'domain' => options['session_domain'], + 'expires' => options['session_expires'], + 'secure' => options['session_secure'] + } + + # Set no_hidden and no_cookies since the session id is unused and we + # set our own data cookie. + options['no_hidden'] = true + options['no_cookies'] = true + end + + # To prevent users from using something insecure like "Password" we make sure that the + # secret they've provided is at least 30 characters in length. + def ensure_secret_secure(secret) + # There's no way we can do this check if they've provided a proc for the + # secret. + return true if secret.is_a?(Proc) + + if secret.blank? + raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb} + end + + if secret.length < SECRET_MIN_LENGTH + raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters} + end + end + + # Restore session data from the cookie. + def restore + @original = read_cookie + @data = unmarshal(@original) || {} + end + + # Wait until close to write the session data cookie. + def update; end + + # Write the session data cookie if it was loaded and has changed. + def close + if defined?(@data) && !@data.blank? + updated = marshal(@data) + raise CookieOverflow if updated.size > MAX + write_cookie('value' => updated) unless updated == @original + end + end + + # Delete the session data by setting an expired cookie with no data. + def delete + @data = nil + clear_old_cookie_value + write_cookie('value' => '', 'expires' => 1.year.ago) + end + + # Generate the HMAC keyed message digest. Uses SHA1 by default. + def generate_digest(data) + key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret + OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data) + end + + private + # Marshal a session hash into safe cookie data. Include an integrity hash. + def marshal(session) + data = Base64.encode64(Marshal.dump(session)).chop + CGI.escape "#{data}--#{generate_digest(data)}" + end + + # Unmarshal cookie data to a hash and verify its integrity. + def unmarshal(cookie) + if cookie + data, digest = CGI.unescape(cookie).split('--') + unless digest == generate_digest(data) + delete + raise TamperedWithCookie + end + Marshal.load(Base64.decode64(data)) + end + end + + # Read the session data cookie. + def read_cookie + @session.cgi.cookies[@cookie_options['name']].first + end + + # CGI likes to make you hack. + def write_cookie(options) + cookie = CGI::Cookie.new(@cookie_options.merge(options)) + @session.cgi.send :instance_variable_set, '@output_cookies', [cookie] + end + + # Clear cookie value so subsequent new_session doesn't reload old data. + def clear_old_cookie_value + @session.cgi.cookies[@cookie_options['name']].clear + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_server.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_server.rb new file mode 100644 index 000000000..6f90db674 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_server.rb @@ -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
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_store.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_store.rb new file mode 100644 index 000000000..4feb2636e --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/drb_store.rb @@ -0,0 +1,35 @@ +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 + + def data + @@session_data[@session_id] + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session/mem_cache_store.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/mem_cache_store.rb new file mode 100644 index 000000000..2f08af663 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session/mem_cache_store.rb @@ -0,0 +1,98 @@ +# 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_library_or_gem '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 it’s 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 = {} + # Add this key to the store if haven't done so yet + unless @cache.get(@session_key) + @cache.add(@session_key, @session_data, @expires) + end + end + + # Restore session state from the session's memcache entry. + # + # Returns the session state as a hash. + def restore + @session_data = @cache[@session_key] || {} + end + + # Save session state to the session's memcache entry. + def update + @cache.set(@session_key, @session_data, @expires) + end + + # Update and close the session's memcache entry. + def close + update + end + + # Delete the session's memcache entry. + def delete + @cache.delete(@session_key) + @session_data = {} + end + + def data + @session_data + end + + end + end + end +rescue LoadError + # MemCache wasn't available so neither can the store be +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/session_management.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/session_management.rb new file mode 100644 index 000000000..fabb6e7f6 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/session_management.rb @@ -0,0 +1,151 @@ +require 'action_controller/session/cookie_store' +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.class_eval do + extend ClassMethods + alias_method_chain :process, :session_management_support + alias_method_chain :process_cleanup, :session_management_support + end + end + + module ClassMethods + # Set the session store to be used for keeping the session data between requests. By default, sessions are stored + # in browser cookies (:cookie_store), but you can also specify one of the other included stores + # (:active_record_store, :p_store, drb_store, :mem_cache_store, or :memory_store) or your own custom 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] } + # + # # the session will be disabled for non html/ajax requests + # session :off, + # :if => Proc.new { |req| !(req.format.html? || req.format.js?) } + # + # All session options described for ActionController::Base.process_cgi + # are valid arguments. + def session(*args) + options = args.extract_options! + + 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 + + # So we can declare session options in the Rails initializer. + alias_method :session=, :session + + 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 + clear_persistent_model_associations + process_cleanup_without_session_management_support + 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.respond_to?(:data) + session_data = @_session.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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/status_codes.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/status_codes.rb new file mode 100644 index 000000000..4977c7949 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/status_codes.rb @@ -0,0 +1,88 @@ +module ActionController + module StatusCodes #:nodoc: + # Defines the standard HTTP status codes, by integer, with their + # corresponding default message texts. + # Source: http://www.iana.org/assignments/http-status-codes + STATUS_CODES = { + 100 => "Continue", + 101 => "Switching Protocols", + 102 => "Processing", + + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non-Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 207 => "Multi-Status", + 226 => "IM Used", + + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Found", + 303 => "See Other", + 304 => "Not Modified", + 305 => "Use Proxy", + 307 => "Temporary Redirect", + + 400 => "Bad Request", + 401 => "Unauthorized", + 402 => "Payment Required", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 407 => "Proxy Authentication Required", + 408 => "Request Timeout", + 409 => "Conflict", + 410 => "Gone", + 411 => "Length Required", + 412 => "Precondition Failed", + 413 => "Request Entity Too Large", + 414 => "Request-URI Too Long", + 415 => "Unsupported Media Type", + 416 => "Requested Range Not Satisfiable", + 417 => "Expectation Failed", + 422 => "Unprocessable Entity", + 423 => "Locked", + 424 => "Failed Dependency", + 426 => "Upgrade Required", + + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 505 => "HTTP Version Not Supported", + 507 => "Insufficient Storage", + 510 => "Not Extended" + } + + # Provides a symbol-to-fixnum lookup for converting a symbol (like + # :created or :not_implemented) into its corresponding HTTP status + # code (like 200 or 501). + SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)| + hash[message.gsub(/ /, "").underscore.to_sym] = code + hash + end + + # Given a status parameter, determine whether it needs to be converted + # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup + # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE + # hash to convert it. + def interpret_status(status) + case status + when Fixnum then + "#{status} #{STATUS_CODES[status]}".strip + when Symbol then + interpret_status(SYMBOL_TO_STATUS_CODE[status] || + "500 Unknown Status #{status.inspect}") + else + status.to_s + end + end + private :interpret_status + + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/streaming.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/streaming.rb new file mode 100644 index 000000000..42fe42983 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/streaming.rb @@ -0,0 +1,141 @@ +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'. + # * <tt>:url_based_filename</tt> - set to true if you want the browser guess the filename from + # the URL, which is necessary for i18n filenames on certain browsers + # (setting :filename overrides this option). + # + # 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) unless options[:url_based_filename] + 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| + while buf = file.read(len) + output.write(buf) + 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. + # Valid values are 'inline' and 'attachment' (default). + # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. + # + # 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].to_s.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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_request_and_response.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_request_and_response.erb new file mode 100644 index 000000000..64b34650b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_request_and_response.erb @@ -0,0 +1,24 @@ +<% 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 %> + +<% + clean_params = request.parameters.clone + clean_params.delete("action") + clean_params.delete("controller") + + request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") +%> + +<h2 style="margin-top: 30px">Request</h2> +<p><b>Parameters</b>: <pre><%=h request_dump %></pre></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> +<p><b>Headers</b>: <pre><%=h response ? response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_trace.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_trace.erb new file mode 100644 index 000000000..b322b0aaa --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/_trace.erb @@ -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>
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/diagnostics.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/diagnostics.erb new file mode 100644 index 000000000..032f945ed --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/diagnostics.erb @@ -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.erb", false) %> + +<%= render_file(@rescues_path + "/_request_and_response.erb", false) %> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/layout.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/layout.erb new file mode 100644 index 000000000..d38f3e67f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/layout.erb @@ -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>
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/missing_template.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/missing_template.erb new file mode 100644 index 000000000..dbfdf7694 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/missing_template.erb @@ -0,0 +1,2 @@ +<h1>Template is missing</h1> +<p><%=h @exception.message %></p> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/routing_error.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/routing_error.erb new file mode 100644 index 000000000..ccfa858cc --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/routing_error.erb @@ -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 %> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/template_error.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/template_error.erb new file mode 100644 index 000000000..eda64db3e --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/template_error.erb @@ -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.erb", false) %> +<% @exception = @real_exception %> + +<%= render_file(@rescues_path + "/_request_and_response.erb", false) %> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/unknown_action.erb b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/unknown_action.erb new file mode 100644 index 000000000..683379da1 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/templates/rescues/unknown_action.erb @@ -0,0 +1,2 @@ +<h1>Unknown action</h1> +<p><%=h @exception.message %></p> diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/test_case.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/test_case.rb new file mode 100644 index 000000000..d81cb5dae --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/test_case.rb @@ -0,0 +1,53 @@ +require 'active_support/test_case' + +module ActionController + class NonInferrableControllerError < ActionControllerError + def initialize(name) + super "Unable to determine the controller to test from #{name}. " + + "You'll need to specify it using 'tests YourController' in your " + + "test case definition" + end + end + + class TestCase < ActiveSupport::TestCase + @@controller_class = nil + class << self + def tests(controller_class) + self.controller_class = controller_class + end + + def controller_class=(new_class) + prepare_controller_class(new_class) + write_inheritable_attribute(:controller_class, new_class) + end + + def controller_class + if current_controller_class = read_inheritable_attribute(:controller_class) + current_controller_class + else + self.controller_class= determine_default_controller_class(name) + end + end + + def determine_default_controller_class(name) + name.sub(/Test$/, '').constantize + rescue NameError + raise NonInferrableControllerError.new(name) + end + + def prepare_controller_class(new_class) + new_class.class_eval do + def rescue_action(e) + raise e + end + end + end + end + + def setup + @controller = self.class.controller_class.new + @request = TestRequest.new + @response = TestResponse.new + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/test_process.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/test_process.rb new file mode 100644 index 000000000..885331159 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/test_process.rb @@ -0,0 +1,520 @@ +require 'action_controller/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_chain :process, :test + end + + class TestRequest < AbstractRequest #:nodoc: + attr_accessor :cookies, :session_options + attr_accessor :query_parameters, :request_parameters, :path, :session, :env + attr_accessor :host, :user_agent + + 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 = TestSession.new + end + + # Wraps raw_post in a StringIO. + def body + StringIO.new(raw_post) + end + + # Either the RAW_POST_DATA environment variable or the URL-encoded request + # parameters. + def raw_post + env['RAW_POST_DATA'] ||= url_encoded_request_parameters + 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 accept=(mime_types) + @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + 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::PathSegment::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 + @parameters = nil # reset TestRequest#parameters to use the new path_parameters + end + + def recycle! + self.request_parameters = {} + self.query_parameters = {} + self.path_parameters = {} + @request_method, @accepts, @content_type = nil, nil, nil + end + + def referer + @env["HTTP_REFERER"] + end + + private + def initialize_containers + @env, @cookies = {}, {} + end + + def initialize_default_values + @host = "test.host" + @request_uri = "/" + @user_agent = "Rails Testing" + self.remote_addr = "0.0.0.0" + @env["SERVER_PORT"] = 80 + @env['REQUEST_METHOD'] = "GET" + end + + def url_encoded_request_parameters + params = self.request_parameters.dup + + %w(controller action only_path).each do |k| + params.delete(k) + params.delete(k.to_sym) + end + + params.to_query + 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 + headers['Location'] + 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 + body.call(self, sio) + + sio.rewind + sio.read + end + end + + class TestResponse < AbstractResponse #:nodoc: + include TestResponseBehavior + end + + class TestSession #:nodoc: + attr_accessor :session_id + + def initialize(attributes = nil) + @session_id = '' + @attributes = attributes + @saved_attributes = nil + end + + def data + @attributes ||= @saved_attributes || {} + end + + def [](key) + data[key] + end + + def []=(key, value) + data[key] = value + end + + def update + @saved_attributes = @attributes + end + + def delete + @attributes = nil + end + + def close + update + delete + 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') + # + # Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows): + # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary) + require 'tempfile' + 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 = Mime::TEXT, binary = false) + raise "#{path} 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) + @tempfile.binmode if binary + 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 defined?(@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| + if !(instance_variables.include?(iv_name) || instance_variables.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." + end + 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 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 + redirected_controller = @response.redirected_to[:controller] + if redirected_controller && redirected_controller != @controller.controller_name + raise "Can't follow redirects outside of current controller (from #{@controller.controller_name} to #{redirected_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 + xml = @response.content_type =~ /xml$/ + @html_document ||= HTML::Document.new(@response.body, false, xml) + 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::Routes.named_routes.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') + # + # To upload binary files on Windows, pass :binary as the last parameter. This will not affect other platforms. + # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary) + def fixture_file_upload(path, mime_type = nil, binary = false) + ActionController::TestUploadedFile.new( + Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path, + mime_type, + binary + ) + 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 do |map| + # map.connect ':controller/:action/:id' + # assert_equal( + # ['/content/10/show', {}], + # map.generate(:controller => 'content', :id => 10, :action => 'show') + # end + # end + # end + # + def with_routing + real_routes = ActionController::Routing::Routes + ActionController::Routing.module_eval { remove_const :Routes } + + temporary_routes = ActionController::Routing::RouteSet.new + ActionController::Routing.module_eval { const_set :Routes, temporary_routes } + + yield temporary_routes + ensure + if ActionController::Routing.const_defined? :Routes + ActionController::Routing.module_eval { 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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/url_rewriter.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/url_rewriter.rb new file mode 100644 index 000000000..c650763fd --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/url_rewriter.rb @@ -0,0 +1,135 @@ +module ActionController + # Write URLs from arbitrary places in your codebase, such as your mailers. + # + # Example: + # + # class MyMailer + # include ActionController::UrlWriter + # default_url_options[:host] = 'www.basecamphq.com' + # + # def signup_url(token) + # url_for(:controller => 'signup', action => 'index', :token => token) + # end + # end + # + # In addition to providing +url_for+, named routes are also accessible after + # including UrlWriter. + module UrlWriter + # The default options for urls written by this writer. Typically a :host pair + # is provided. + mattr_accessor :default_url_options + self.default_url_options = {} + + def self.included(base) #:nodoc: + ActionController::Routing::Routes.install_helpers base + base.mattr_accessor :default_url_options + base.default_url_options ||= default_url_options + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * <tt>:only_path</tt> If true, the relative url is returned. Defaults to false. + # * <tt>:protocol</tt> The protocol to connect to. Defaults to 'http'. + # * <tt>:host</tt> Specifies the host the link should be targetted at. If <tt>:only_path</tt> is false, this option must be + # provided either explicitly, or via default_url_options. + # * <tt>:port</tt> Optionally specify the port to connect to. + # * <tt>:anchor</tt> An anchor name to be appended to the path. + # + # Any other key(:controller, :action, etc...) given to <tt>url_for</tt> is forwarded to the Routes module. + # + # Examples: + # + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + # + def url_for(options) + options = self.class.default_url_options.merge(options) + + url = '' + + unless options.delete :only_path + url << (options.delete(:protocol) || 'http') + url << '://' unless url.match("://") #dont add separator if its already been specified in :protocol + + raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] + + url << options.delete(:host) + url << ":#{options.delete(:port)}" if options.key?(:port) + else + # Delete the unused options to prevent their appearance in the query string + [:protocol, :host, :port].each { |k| options.delete k } + end + + anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options.key?(:anchor) + url << Routing::Routes.generate(options, {}) + url << anchor if anchor + + return url + end + end + + # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. + class UrlRewriter #:nodoc: + RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root] + def initialize(request, parameters) + @request, @parameters = request, parameters + end + + def rewrite(options = {}) + rewrite_url(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 + # Given a path and options, returns a rewritten URL string + def rewrite_url(options) + rewritten_url = "" + + unless options[:only_path] + rewritten_url << (options[:protocol] || @request.protocol) + rewritten_url << "://" unless rewritten_url.match("://") + rewritten_url << rewrite_authentication(options) + rewritten_url << (options[:host] || @request.host_with_port) + rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) + end + + path = rewrite_path(options) + rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root] + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "##{options[:anchor]}" if options[:anchor] + + rewritten_url + end + + # Given a Hash of options, generates a route + 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.symbolize_keys) + end + + RESERVED_OPTIONS.each { |k| options.delete(k) } + + # Generates the query string, too + Routing::Routes.generate(options, @request.symbolized_path_parameters) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@" + else + "" + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb new file mode 100644 index 000000000..607fd186b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb @@ -0,0 +1,68 @@ +require 'html/tokenizer' +require 'html/node' +require 'html/selector' +require 'html/sanitizer' + +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 + if node_stack.last.children.empty? + node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "") + end + 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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb new file mode 100644 index 000000000..472c5b2ba --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -0,0 +1,530 @@ +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.gsub(/<!\[CDATA\[/, '')) + 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 == 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}\"" 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.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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb new file mode 100644 index 000000000..1eb426aea --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -0,0 +1,173 @@ +module HTML + class Sanitizer + def sanitize(text, options = {}) + return text unless sanitizeable?(text) + tokenize(text, options).join + end + + def sanitizeable?(text) + !(text.nil? || text.empty? || !text.index("<")) + end + + protected + def tokenize(text, options) + tokenizer = HTML::Tokenizer.new(text) + result = [] + while token = tokenizer.next + node = Node.parse(nil, 0, 0, token, false) + process_node node, result, options + end + result + end + + def process_node(node, result, options) + result << node.to_s + end + end + + class FullSanitizer < Sanitizer + def sanitize(text, options = {}) + result = super + # strip any comments, and if they have a newline at the end (ie. line with + # only a comment) strip that too + result.gsub!(/<!--(.*?)-->[\n]?/m, "") if result + # Recurse - handle all dirty nested tags + result == text ? result : sanitize(result, options) + end + + def process_node(node, result, options) + result << node.to_s if node.class == HTML::Text + end + end + + class LinkSanitizer < FullSanitizer + cattr_accessor :included_tags, :instance_writer => false + self.included_tags = Set.new(%w(a href)) + + def sanitizeable?(text) + !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">"))) + end + + protected + def process_node(node, result, options) + result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) + end + end + + class WhiteListSanitizer < Sanitizer + [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, + :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| + class_inheritable_accessor attr, :instance_writer => false + end + + # A regular expression of the valid characters used to separate protocols like + # the ':' in 'http://foo.com' + self.protocol_separator = /:|(�*58)|(p)|(%|%)3A/ + + # Specifies a Set of HTML attributes that can have URIs. + self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) + + # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed + # to just escaping harmless tags like <font> + self.bad_tags = Set.new(%w(script)) + + # Specifies the default Set of tags that the #sanitize helper will allow unscathed. + self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub + sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dt dd abbr + acronym a img blockquote del ins)) + + # Specifies the default Set of html attributes that the #sanitize helper will leave + # in the allowed tag. + self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr)) + + # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. + self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto + feed svn urn aim rsync tag ssh sftp rtsp afs)) + + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. + self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse + border-color border-left-color border-right-color border-top-color clear color cursor direction display + elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height + overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation + speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space + width)) + + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. + self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center + collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal + nowrap olive pointer purple red right solid silver teal top transparent underline white yellow)) + + # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers. + self.shorthand_css_properties = Set.new(%w(background border margin padding)) + + # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute + def sanitize_css(style) + # disallow urls + style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ') + + # gauntlet + if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ || + style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$))*$/ + return '' + end + + clean = [] + style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val| + if allowed_css_properties.include?(prop.downcase) + clean << prop + ': ' + val + ';' + elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) + unless val.split().any? do |keyword| + !allowed_css_keywords.include?(keyword) && + keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/ + end + clean << prop + ': ' + val + ';' + end + end + end + clean.join(' ') + end + + protected + def tokenize(text, options) + options[:parent] = [] + options[:attributes] ||= allowed_attributes + options[:tags] ||= allowed_tags + super + end + + def process_node(node, result, options) + result << case node + when HTML::Tag + if node.closing == :close + options[:parent].shift + else + options[:parent].unshift node.name + end + + process_attributes_for node, options + + options[:tags].include?(node.name) ? node : nil + else + bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<") + end + end + + def process_attributes_for(node, options) + return unless node.attributes + node.attributes.keys.each do |attr_name| + value = node.attributes[attr_name].to_s + + if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value) + node.attributes.delete(attr_name) + else + node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(value) + end + end + end + + def contains_bad_protocols?(attr_name, value) + uri_attributes.include?(attr_name) && + (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first)) + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb new file mode 100644 index 000000000..1a3c77025 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb @@ -0,0 +1,828 @@ +#-- +# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) +# Under MIT and/or CC By license. +#++ + +module HTML + + # Selects HTML elements using CSS 2 selectors. + # + # The +Selector+ class uses CSS selector expressions to match and select + # HTML elements. + # + # For example: + # selector = HTML::Selector.new "form.login[action=/login]" + # creates a new selector that matches any +form+ element with the class + # +login+ and an attribute +action+ with the value <tt>/login</tt>. + # + # === Matching Elements + # + # Use the #match method to determine if an element matches the selector. + # + # For simple selectors, the method returns an array with that element, + # or +nil+ if the element does not match. For complex selectors (see below) + # the method returns an array with all matched elements, of +nil+ if no + # match found. + # + # For example: + # if selector.match(element) + # puts "Element is a login form" + # end + # + # === Selecting Elements + # + # Use the #select method to select all matching elements starting with + # one element and going through all children in depth-first order. + # + # This method returns an array of all matching elements, an empty array + # if no match is found + # + # For example: + # selector = HTML::Selector.new "input[type=text]" + # matches = selector.select(element) + # matches.each do |match| + # puts "Found text field with name #{match.attributes['name']}" + # end + # + # === Expressions + # + # Selectors can match elements using any of the following criteria: + # * <tt>name</tt> -- Match an element based on its name (tag name). + # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt> + # to match any element. + # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the + # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>. + # * <tt>.class</tt> -- Match an element based on its class name, all + # class names if more than one specified. + # * <tt>[attr]</tt> -- Match an element that has the specified attribute. + # * <tt>[attr=value]</tt> -- Match an element that has the specified + # attribute and value. (More operators are supported see below) + # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class, + # such as <tt>:nth-child</tt> and <tt>:empty</tt>. + # * <tt>:not(expr)</tt> -- Match an element that does not match the + # negation expression. + # + # When using a combination of the above, the element name comes first + # followed by identifier, class names, attributes, pseudo classes and + # negation in any order. Do not seprate these parts with spaces! + # Space separation is used for descendant selectors. + # + # For example: + # selector = HTML::Selector.new "form.login[action=/login]" + # The matched element must be of type +form+ and have the class +login+. + # It may have other classes, but the class +login+ is required to match. + # It must also have an attribute called +action+ with the value + # <tt>/login</tt>. + # + # This selector will match the following element: + # <form class="login form" method="post" action="/login"> + # but will not match the element: + # <form method="post" action="/logout"> + # + # === Attribute Values + # + # Several operators are supported for matching attributes: + # * <tt>name</tt> -- The element must have an attribute with that name. + # * <tt>name=value</tt> -- The element must have an attribute with that + # name and value. + # * <tt>name^=value</tt> -- The attribute value must start with the + # specified value. + # * <tt>name$=value</tt> -- The attribute value must end with the + # specified value. + # * <tt>name*=value</tt> -- The attribute value must contain the + # specified value. + # * <tt>name~=word</tt> -- The attribute value must contain the specified + # word (space separated). + # * <tt>name|=word</tt> -- The attribute value must start with specified + # word. + # + # For example, the following two selectors match the same element: + # #my_id + # [id=my_id] + # and so do the following two selectors: + # .my_class + # [class~=my_class] + # + # === Alternatives, siblings, children + # + # Complex selectors use a combination of expressions to match elements: + # * <tt>expr1 expr2</tt> -- Match any element against the second expression + # if it has some parent element that matches the first expression. + # * <tt>expr1 > expr2</tt> -- Match any element against the second expression + # if it is the child of an element that matches the first expression. + # * <tt>expr1 + expr2</tt> -- Match any element against the second expression + # if it immediately follows an element that matches the first expression. + # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression + # that comes after an element that matches the first expression. + # * <tt>expr1, expr2</tt> -- Match any element against the first expression, + # or against the second expression. + # + # Since children and sibling selectors may match more than one element given + # the first element, the #match method may return more than one match. + # + # === Pseudo classes + # + # Pseudo classes were introduced in CSS 3. They are most often used to select + # elements in a given position: + # * <tt>:root</tt> -- Match the element only if it is the root element + # (no parent element). + # * <tt>:empty</tt> -- Match the element only if it has no child elements, + # and no text content. + # * <tt>:only-child</tt> -- Match the element if it is the only child (element) + # of its parent element. + # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element) + # of its parent element and its type. + # * <tt>:first-child</tt> -- Match the element if it is the first child (element) + # of its parent element. + # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element) + # of its parent element of its type. + # * <tt>:last-child</tt> -- Match the element if it is the last child (element) + # of its parent element. + # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element) + # of its parent element of its type. + # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element) + # of its parent element. The value <tt>b</tt> specifies its index, starting with 1. + # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element) + # in each group of <tt>a</tt> child elements of its parent element. + # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element) + # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child + # elements of its parent element. + # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third). + # Same as <tt>:nth-child(2n+1)</tt>. + # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second, + # fourth). Same as <tt>:nth-child(2n+2)</tt>. + # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type. + # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child. + # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and + # only elements of its type. + # * <tt>:not(selector)</tt> -- Match the element only if the element does not + # match the simple selector. + # + # As you can see, <tt>:nth-child<tt> pseudo class and its varient can get quite + # tricky and the CSS specification doesn't do a much better job explaining it. + # But after reading the examples and trying a few combinations, it's easy to + # figure out. + # + # For example: + # table tr:nth-child(odd) + # Selects every second row in the table starting with the first one. + # + # div p:nth-child(4) + # Selects the fourth paragraph in the +div+, but not if the +div+ contains + # other elements, since those are also counted. + # + # div p:nth-of-type(4) + # Selects the fourth paragraph in the +div+, counting only paragraphs, and + # ignoring all other elements. + # + # div p:nth-of-type(-n+4) + # Selects the first four paragraphs, ignoring all others. + # + # And you can always select an element that matches one set of rules but + # not another using <tt>:not</tt>. For example: + # p:not(.post) + # Matches all paragraphs that do not have the class <tt>.post</tt>. + # + # === Substitution Values + # + # You can use substitution with identifiers, class names and element values. + # A substitution takes the form of a question mark (<tt>?</tt>) and uses the + # next value in the argument list following the CSS expression. + # + # The substitution value may be a string or a regular expression. All other + # values are converted to strings. + # + # For example: + # selector = HTML::Selector.new "#?", /^\d+$/ + # matches any element whose identifier consists of one or more digits. + # + # See http://www.w3.org/TR/css3-selectors/ + class Selector + + + # An invalid selector. + class InvalidSelectorError < StandardError #:nodoc: + end + + + class << self + + # :call-seq: + # Selector.for_class(cls) => selector + # + # Creates a new selector for the given class name. + def for_class(cls) + self.new([".?", cls]) + end + + + # :call-seq: + # Selector.for_id(id) => selector + # + # Creates a new selector for the given id. + def for_id(id) + self.new(["#?", id]) + end + + end + + + # :call-seq: + # Selector.new(string, [values ...]) => selector + # + # Creates a new selector from a CSS 2 selector expression. + # + # The first argument is the selector expression. All other arguments + # are used for value substitution. + # + # Throws InvalidSelectorError is the selector expression is invalid. + def initialize(selector, *values) + raise ArgumentError, "CSS expression cannot be empty" if selector.empty? + @source = "" + values = values[0] if values.size == 1 && values[0].is_a?(Array) + + # We need a copy to determine if we failed to parse, and also + # preserve the original pass by-ref statement. + statement = selector.strip.dup + + # Create a simple selector, along with negation. + simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) } + + @alternates = [] + @depends = nil + + # Alternative selector. + if statement.sub!(/^\s*,\s*/, "") + second = Selector.new(statement, values) + @alternates << second + # If there are alternate selectors, we group them in the top selector. + if alternates = second.instance_variable_get(:@alternates) + second.instance_variable_set(:@alternates, []) + @alternates.concat alternates + end + @source << " , " << second.to_s + # Sibling selector: create a dependency into second selector that will + # match element immediately following this one. + elsif statement.sub!(/^\s*\+\s*/, "") + second = next_selector(statement, values) + @depends = lambda do |element, first| + if element = next_element(element) + second.match(element, first) + end + end + @source << " + " << second.to_s + # Adjacent selector: create a dependency into second selector that will + # match all elements following this one. + elsif statement.sub!(/^\s*~\s*/, "") + second = next_selector(statement, values) + @depends = lambda do |element, first| + matches = [] + while element = next_element(element) + if subset = second.match(element, first) + if first && !subset.empty? + matches << subset.first + break + else + matches.concat subset + end + end + end + matches.empty? ? nil : matches + end + @source << " ~ " << second.to_s + # Child selector: create a dependency into second selector that will + # match a child element of this one. + elsif statement.sub!(/^\s*>\s*/, "") + second = next_selector(statement, values) + @depends = lambda do |element, first| + matches = [] + element.children.each do |child| + if child.tag? && subset = second.match(child, first) + if first && !subset.empty? + matches << subset.first + break + else + matches.concat subset + end + end + end + matches.empty? ? nil : matches + end + @source << " > " << second.to_s + # Descendant selector: create a dependency into second selector that + # will match all descendant elements of this one. Note, + elsif statement =~ /^\s+\S+/ && statement != selector + second = next_selector(statement, values) + @depends = lambda do |element, first| + matches = [] + stack = element.children.reverse + while node = stack.pop + next unless node.tag? + if subset = second.match(node, first) + if first && !subset.empty? + matches << subset.first + break + else + matches.concat subset + end + elsif children = node.children + stack.concat children.reverse + end + end + matches.empty? ? nil : matches + end + @source << " " << second.to_s + else + # The last selector is where we check that we parsed + # all the parts. + unless statement.empty? || statement.strip.empty? + raise ArgumentError, "Invalid selector: #{statement}" + end + end + end + + + # :call-seq: + # match(element, first?) => array or nil + # + # Matches an element against the selector. + # + # For a simple selector this method returns an array with the + # element if the element matches, nil otherwise. + # + # For a complex selector (sibling and descendant) this method + # returns an array with all matching elements, nil if no match is + # found. + # + # Use +first_only=true+ if you are only interested in the first element. + # + # For example: + # if selector.match(element) + # puts "Element is a login form" + # end + def match(element, first_only = false) + # Match element if no element name or element name same as element name + if matched = (!@tag_name || @tag_name == element.name) + # No match if one of the attribute matches failed + for attr in @attributes + if element.attributes[attr[0]] !~ attr[1] + matched = false + break + end + end + end + + # Pseudo class matches (nth-child, empty, etc). + if matched + for pseudo in @pseudo + unless pseudo.call(element) + matched = false + break + end + end + end + + # Negation. Same rules as above, but we fail if a match is made. + if matched && @negation + for negation in @negation + if negation[:tag_name] == element.name + matched = false + else + for attr in negation[:attributes] + if element.attributes[attr[0]] =~ attr[1] + matched = false + break + end + end + end + if matched + for pseudo in negation[:pseudo] + if pseudo.call(element) + matched = false + break + end + end + end + break unless matched + end + end + + # If element matched but depends on another element (child, + # sibling, etc), apply the dependent matches instead. + if matched && @depends + matches = @depends.call(element, first_only) + else + matches = matched ? [element] : nil + end + + # If this selector is part of the group, try all the alternative + # selectors (unless first_only). + if !first_only || !matches + @alternates.each do |alternate| + break if matches && first_only + if subset = alternate.match(element, first_only) + if matches + matches.concat subset + else + matches = subset + end + end + end + end + + matches + end + + + # :call-seq: + # select(root) => array + # + # Selects and returns an array with all matching elements, beginning + # with one node and traversing through all children depth-first. + # Returns an empty array if no match is found. + # + # The root node may be any element in the document, or the document + # itself. + # + # For example: + # selector = HTML::Selector.new "input[type=text]" + # matches = selector.select(element) + # matches.each do |match| + # puts "Found text field with name #{match.attributes['name']}" + # end + def select(root) + matches = [] + stack = [root] + while node = stack.pop + if node.tag? && subset = match(node, false) + subset.each do |match| + matches << match unless matches.any? { |item| item.equal?(match) } + end + elsif children = node.children + stack.concat children.reverse + end + end + matches + end + + + # Similar to #select but returns the first matching element. Returns +nil+ + # if no element matches the selector. + def select_first(root) + stack = [root] + while node = stack.pop + if node.tag? && subset = match(node, true) + return subset.first if !subset.empty? + elsif children = node.children + stack.concat children.reverse + end + end + nil + end + + + def to_s #:nodoc: + @source + end + + + # Return the next element after this one. Skips sibling text nodes. + # + # With the +name+ argument, returns the next element with that name, + # skipping other sibling elements. + def next_element(element, name = nil) + if siblings = element.parent.children + found = false + siblings.each do |node| + if node.equal?(element) + found = true + elsif found && node.tag? + return node if (name.nil? || node.name == name) + end + end + end + nil + end + + + protected + + + # Creates a simple selector given the statement and array of + # substitution values. + # + # Returns a hash with the values +tag_name+, +attributes+, + # +pseudo+ (classes) and +negation+. + # + # Called the first time with +can_negate+ true to allow + # negation. Called a second time with false since negation + # cannot be negated. + def simple_selector(statement, values, can_negate = true) + tag_name = nil + attributes = [] + pseudo = [] + negation = [] + + # Element name. (Note that in negation, this can come at + # any order, but for simplicity we allow if only first). + statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match| + match.strip! + tag_name = match.downcase unless match == "*" + @source << match + "" # Remove + end + + # Get identifier, class, attribute name, pseudo or negation. + while true + # Element identifier. + next if statement.sub!(/^#(\?|[\w\-]+)/) do |match| + id = $1 + if id == "?" + id = values.shift + end + @source << "##{id}" + id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp) + attributes << ["id", id] + "" # Remove + end + + # Class name. + next if statement.sub!(/^\.([\w\-]+)/) do |match| + class_name = $1 + @source << ".#{class_name}" + class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp) + attributes << ["class", class_name] + "" # Remove + end + + # Attribute value. + next if statement.sub!(/^\[\s*([[:alpha:]][\w\-]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match| + name, equality, value = $1, $2, $3 + if value == "?" + value = values.shift + else + # Handle single and double quotes. + value.strip! + if (value[0] == ?" || value[0] == ?') && value[0] == value[-1] + value = value[1..-2] + end + end + @source << "[#{name}#{equality}'#{value}']" + attributes << [name.downcase.strip, attribute_match(equality, value)] + "" # Remove + end + + # Root element only. + next if statement.sub!(/^:root/) do |match| + pseudo << lambda do |element| + element.parent.nil? || !element.parent.tag? + end + @source << ":root" + "" # Remove + end + + # Nth-child including last and of-type. + next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match| + reverse = $1 == "last-" + of_type = $2 == "of-type" + @source << ":nth-#{$1}#{$2}(" + case $3 + when "odd" + pseudo << nth_child(2, 1, of_type, reverse) + @source << "odd)" + when "even" + pseudo << nth_child(2, 2, of_type, reverse) + @source << "even)" + when /^(\d+|\?)$/ # b only + b = ($1 == "?" ? values.shift : $1).to_i + pseudo << nth_child(0, b, of_type, reverse) + @source << "#{b})" + when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/ + a = ($1 == "?" ? values.shift : + $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i + b = ($2 == "?" ? values.shift : $2).to_i + pseudo << nth_child(a, b, of_type, reverse) + @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})") + else + raise ArgumentError, "Invalid nth-child #{match}" + end + "" # Remove + end + # First/last child (of type). + next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match| + reverse = $1 == "last" + of_type = $2 == "of-type" + pseudo << nth_child(0, 1, of_type, reverse) + @source << ":#{$1}-#{$2}" + "" # Remove + end + # Only child (of type). + next if statement.sub!(/^:only-(child|of-type)/) do |match| + of_type = $1 == "of-type" + pseudo << only_child(of_type) + @source << ":only-#{$1}" + "" # Remove + end + + # Empty: no child elements or meaningful content (whitespaces + # are ignored). + next if statement.sub!(/^:empty/) do |match| + pseudo << lambda do |element| + empty = true + for child in element.children + if child.tag? || !child.content.strip.empty? + empty = false + break + end + end + empty + end + @source << ":empty" + "" # Remove + end + # Content: match the text content of the element, stripping + # leading and trailing spaces. + next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match| + content = $1 + if content == "?" + content = values.shift + elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1] + content = content[1..-2] + end + @source << ":content('#{content}')" + content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp) + pseudo << lambda do |element| + text = "" + for child in element.children + unless child.tag? + text << child.content + end + end + text.strip =~ content + end + "" # Remove + end + + # Negation. Create another simple selector to handle it. + if statement.sub!(/^:not\(\s*/, "") + raise ArgumentError, "Double negatives are not missing feature" unless can_negate + @source << ":not(" + negation << simple_selector(statement, values, false) + raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "") + @source << ")" + next + end + + # No match: moving on. + break + end + + # Return hash. The keys are mapped to instance variables. + {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation} + end + + + # Create a regular expression to match an attribute value based + # on the equality operator (=, ^=, |=, etc). + def attribute_match(equality, value) + regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s) + case equality + when "=" then + # Match the attribute value in full + Regexp.new("^#{regexp}$") + when "~=" then + # Match a space-separated word within the attribute value + Regexp.new("(^|\s)#{regexp}($|\s)") + when "^=" + # Match the beginning of the attribute value + Regexp.new("^#{regexp}") + when "$=" + # Match the end of the attribute value + Regexp.new("#{regexp}$") + when "*=" + # Match substring of the attribute value + regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp) + when "|=" then + # Match the first space-separated item of the attribute value + Regexp.new("^#{regexp}($|\s)") + else + raise InvalidSelectorError, "Invalid operation/value" unless value.empty? + # Match all attributes values (existence check) + // + end + end + + + # Returns a lambda that can match an element against the nth-child + # pseudo class, given the following arguments: + # * +a+ -- Value of a part. + # * +b+ -- Value of b part. + # * +of_type+ -- True to test only elements of this type (of-type). + # * +reverse+ -- True to count in reverse order (last-). + def nth_child(a, b, of_type, reverse) + # a = 0 means select at index b, if b = 0 nothing selected + return lambda { |element| false } if a == 0 && b == 0 + # a < 0 and b < 0 will never match against an index + return lambda { |element| false } if a < 0 && b < 0 + b = a + b + 1 if b < 0 # b < 0 just picks last element from each group + b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based + lambda do |element| + # Element must be inside parent element. + return false unless element.parent && element.parent.tag? + index = 0 + # Get siblings, reverse if counting from last. + siblings = element.parent.children + siblings = siblings.reverse if reverse + # Match element name if of-type, otherwise ignore name. + name = of_type ? element.name : nil + found = false + for child in siblings + # Skip text nodes/comments. + if child.tag? && (name == nil || child.name == name) + if a == 0 + # Shortcut when a == 0 no need to go past count + if index == b + found = child.equal?(element) + break + end + elsif a < 0 + # Only look for first b elements + break if index > b + if child.equal?(element) + found = (index % a) == 0 + break + end + else + # Otherwise, break if child found and count == an+b + if child.equal?(element) + found = (index % a) == b + break + end + end + index += 1 + end + end + found + end + end + + + # Creates a only child lambda. Pass +of-type+ to only look at + # elements of its type. + def only_child(of_type) + lambda do |element| + # Element must be inside parent element. + return false unless element.parent && element.parent.tag? + name = of_type ? element.name : nil + other = false + for child in element.parent.children + # Skip text nodes/comments. + if child.tag? && (name == nil || child.name == name) + unless child.equal?(element) + other = true + break + end + end + end + !other + end + end + + + # Called to create a dependent selector (sibling, descendant, etc). + # Passes the remainder of the statement that will be reduced to zero + # eventually, and array of substitution values. + # + # This method is called from four places, so it helps to put it here + # for reuse. The only logic deals with the need to detect comma + # separators (alternate) and apply them to the selector group of the + # top selector. + def next_selector(statement, values) + second = Selector.new(statement, values) + # If there are alternate selectors, we group them in the top selector. + if alternates = second.instance_variable_get(:@alternates) + second.instance_variable_set(:@alternates, []) + @alternates.concat alternates + end + second + end + + end + + + # See HTML::Selector.new + def self.selector(statement, *values) + Selector.new(statement, *values) + end + + + class Tag + + def select(selector, *values) + selector = HTML::Selector.new(selector, values) + selector.select(self) + end + + end + +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb new file mode 100644 index 000000000..b950e8462 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -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 diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb new file mode 100644 index 000000000..6d645c3e1 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb @@ -0,0 +1,11 @@ +module HTML #:nodoc: + module Version #:nodoc: + + MAJOR = 0 + MINOR = 5 + TINY = 3 + + STRING = [ MAJOR, MINOR, TINY ].join(".") + + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_controller/verification.rb b/vendor/rails-2.0.2/actionpack/lib/action_controller/verification.rb new file mode 100644 index 000000000..e5045fba7 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_controller/verification.rb @@ -0,0 +1,114 @@ +module ActionController #:nodoc: + module Verification #:nodoc: + def self.included(base) #:nodoc: + 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. If no specific action is configured, + # verification failures will by default result in a 400 Bad Request response. + # + # Usage: + # + # class GlobalController < ActionController::Base + # # Prevent the #update_settings action from being invoked unless + # # the 'admin_privileges' request parameter exists. The + # # settings action will be redirected to in current controller + # # if verification fails. + # 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. The route + # # named category_url will be redirected to if verification fails. + # + # verify :params => "post", :session => "post", "flash" => "note", + # :only => :update_post, + # :add_flash => { "alert" => "Failed to create your message" }, + # :redirect_to => :category_url + # + # Note that these prerequisites are not business rules. They do not examine + # the content of the session or the parameters. That level of validation should + # be encapsulated by your domain model or helper methods in the controller. + 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 <tt>session</tt> 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>:add_headers</tt> - a hash of name/value pairs that should be + # merged into the response's headers hash if the prerequisites cannot + # be satisfied. + # * <tt>:redirect_to</tt> - the redirection parameters to be used when + # redirecting if the prerequisites cannot be satisfied. You can + # redirect either to named route or to the action in some controller. + # * <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] + response.headers.update(options[:add_headers]) if options[:add_headers] + + unless performed? + case + when options[:render] + render(options[:render]) + when options[:redirect_to] + options[:redirect_to] = self.send!(options[:redirect_to]) if options[:redirect_to].is_a?(Symbol) + redirect_to(options[:redirect_to]) + else + head(:bad_request) + end + end + end + end + + private :verify_action + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_pack.rb b/vendor/rails-2.0.2/actionpack/lib/action_pack.rb new file mode 100644 index 000000000..006c83dbc --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_pack.rb @@ -0,0 +1,24 @@ +#-- +# Copyright (c) 2004-2007 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'action_pack/version' diff --git a/vendor/rails-2.0.2/actionpack/lib/action_pack/version.rb b/vendor/rails-2.0.2/actionpack/lib/action_pack/version.rb new file mode 100644 index 000000000..7aa6a5db9 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_pack/version.rb @@ -0,0 +1,9 @@ +module ActionPack #:nodoc: + module VERSION #:nodoc: + MAJOR = 2 + MINOR = 0 + TINY = 2 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view.rb b/vendor/rails-2.0.2/actionpack/lib/action_view.rb new file mode 100644 index 000000000..bfcfcab0b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view.rb @@ -0,0 +1,37 @@ +#-- +# Copyright (c) 2004-2007 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'action_view/template_handler' +require 'action_view/template_handlers/builder' +require 'action_view/template_handlers/erb' +require 'action_view/template_handlers/rjs' + +require 'action_view/base' +require 'action_view/partials' +require 'action_view/template_error' + +ActionView::Base.class_eval do + include ActionView::Partials +end + +ActionView::Base.load_helpers diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb new file mode 100644 index 000000000..0f966addb --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/base.rb @@ -0,0 +1,642 @@ +module ActionView #:nodoc: + class ActionViewError < StandardError #:nodoc: + end + + # Action View templates can be written in three ways. If the template file has a +.erb+ (or +.rhtml+) extension then it uses a mixture of ERb + # (included in Ruby) and HTML. If the template file has a +.builder+ (or +.rxml+) extension then Jim Weirich's Builder::XmlMarkup library is used. + # If the template file has a +.rjs+ extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. + # + # = ERb + # + # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # following loop for names: + # + # <b>Names of all the people</b> + # <% for person in @people %> + # Name: <%= person.name %><br/> + # <% end %> + # + # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this + # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong: + # + # Hi, Mr. <% puts "Frodo" %> + # + # If you absolutely must write from within a function, you can use the TextHelper#concat + # + # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>. + # + # == Using sub templates + # + # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The + # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts): + # + # <%= render "shared/header" %> + # Something really specific and terrific + # <%= render "shared/footer" %> + # + # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the + # result of the rendering. The output embedding writes it to the current template. + # + # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance + # variables defined using the regular embedding tags. Like this: + # + # <% @page_title = "A Wonderful Hello" %> + # <%= render "shared/header" %> + # + # Now the header can pick up on the @page_title variable and use it for outputting a title tag: + # + # <title><%= @page_title %></title> + # + # == Passing local variables to sub templates + # + # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: + # + # <%= render "shared/header", { :headline => "Welcome", :person => person } %> + # + # These can now be accessed in shared/header with: + # + # Headline: <%= headline %> + # First name: <%= person.first_name %> + # + # If you need to find out whether a certain local variable has been assigned a value in a particular render call, + # you need to use the following pattern: + # + # <% if local_assigns.has_key? :headline %> + # Headline: <%= headline %> + # <% end %> + # + # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction. + # + # == Template caching + # + # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will + # check the file's modification time and recompile it. + # + # == Builder + # + # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object + # named +xml+ is automatically made available to templates with a +.builder+ extension. + # + # Here are some basic examples: + # + # xml.em("emphasized") # => <em>emphasized</em> + # xml.em { xml.b("emph & bold") } # => <em><b>emph & bold</b></em> + # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> + # xml.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\> + # # NOTE: order of attributes is not specified. + # + # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: + # + # xml.div { + # xml.h1(@person.name) + # xml.p(@person.bio) + # } + # + # would produce something like: + # + # <div> + # <h1>David Heinemeier Hansson</h1> + # <p>A product of Danish Design during the Winter of '79...</p> + # </div> + # + # A full-length RSS example actually used on Basecamp: + # + # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + # xml.channel do + # xml.title(@feed_title) + # xml.link(@url) + # xml.description "Basecamp: Recent items" + # xml.language "en-us" + # xml.ttl "40" + # + # for item in @recent_items + # xml.item do + # xml.title(item_title(item)) + # xml.description(item_description(item)) if item_description(item) + # xml.pubDate(item_pubDate(item)) + # xml.guid(@person.firm.account.url + @recent_items.url(item)) + # xml.link(@person.firm.account.url + @recent_items.url(item)) + # + # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) + # end + # end + # end + # end + # + # More builder documentation can be found at http://builder.rubyforge.org. + # + # == JavaScriptGenerator + # + # JavaScriptGenerator templates end in +.rjs+. Unlike conventional templates which are used to + # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to + # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax + # and make updates to the page where the request originated from. + # + # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block. + # + # When an .rjs action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example: + # + # link_to_remote :url => {:action => 'delete'} + # + # The subsequently rendered +delete.rjs+ might look like: + # + # page.replace_html 'sidebar', :partial => 'sidebar' + # page.remove "person-#{@person.id}" + # page.visual_effect :highlight, 'user-list' + # + # This refreshes the sidebar, removes a person element and highlights the user list. + # + # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details. + class Base + include ERB::Util + + attr_reader :first_render + attr_accessor :base_path, :assigns, :template_extension + attr_accessor :controller, :view_paths + + attr_reader :logger, :response, :headers + attr_internal :cookies, :flash, :headers, :params, :request, :response, :session + + attr_writer :template_format + + # Specify trim mode for the ERB compiler. Defaults to '-'. + # See ERb documentation for suitable values. + @@erb_trim_mode = '-' + cattr_accessor :erb_trim_mode + + # Specify whether file modification times should be checked to see if a template needs recompilation + @@cache_template_loading = false + cattr_accessor :cache_template_loading + + # Specify whether file extension lookup should be cached, and whether template base path lookup should be cached. + # Should be +false+ for development environments. Defaults to +true+. + @@cache_template_extensions = true + cattr_accessor :cache_template_extensions + + # Specify whether local_assigns should be able to use string keys. + # Defaults to +true+. String keys are deprecated and will be removed + # shortly. + @@local_assigns_support_string_keys = true + cattr_accessor :local_assigns_support_string_keys + + # Specify whether RJS responses should be wrapped in a try/catch block + # that alert()s the caught exception (and then re-raises it). + @@debug_rjs = false + cattr_accessor :debug_rjs + + @@erb_variable = '_erbout' + cattr_accessor :erb_variable + + delegate :request_forgery_protection_token, :to => :controller + + @@template_handlers = HashWithIndifferentAccess.new + + module CompiledTemplates #:nodoc: + # holds compiled template code + end + include CompiledTemplates + + # Maps inline templates to their method names + @@method_names = {} + # Map method names to their compile time + @@compile_time = {} + # Map method names to the names passed in local assigns so far + @@template_args = {} + # Count the number of inline templates + @@inline_template_count = 0 + # Maps template paths without extension to their file extension returned by pick_template_extension. + # If for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions + # used by pick_template_extension determines whether ext1 or ext2 will be stored. + @@cached_template_extension = {} + # Maps template paths / extensions to + @@cached_base_paths = {} + + # Cache public asset paths + cattr_reader :computed_public_paths + @@computed_public_paths = {} + + @@template_handlers = {} + @@default_template_handlers = nil + + class ObjectWrapper < Struct.new(:value) #:nodoc: + end + + def self.load_helpers #:nodoc: + Dir.entries("#{File.dirname(__FILE__)}/helpers").sort.each do |file| + next unless file =~ /^([a-z][a-z_]*_helper).rb$/ + require "action_view/helpers/#{$1}" + helper_module_name = $1.camelize + if Helpers.const_defined?(helper_module_name) + include Helpers.const_get(helper_module_name) + end + end + end + + # Register a class that knows how to handle template files with the given + # extension. This can be used to implement new template types. + # The constructor for the class must take the ActiveView::Base instance + # as a parameter, and the class must implement a #render method that + # takes the contents of the template to render as well as the Hash of + # local assigns available to the template. The #render method ought to + # return the rendered template as a string. + def self.register_template_handler(extension, klass) + @@template_handlers[extension.to_sym] = klass + end + + def self.template_handler_extensions + @@template_handler_extensions ||= @@template_handlers.keys.map(&:to_s).sort + end + + def self.register_default_template_handler(extension, klass) + register_template_handler(extension, klass) + @@default_template_handlers = klass + end + + def self.handler_for_extension(extension) + (extension && @@template_handlers[extension.to_sym]) || @@default_template_handlers + end + + register_default_template_handler :erb, TemplateHandlers::ERB + register_template_handler :rjs, TemplateHandlers::RJS + register_template_handler :builder, TemplateHandlers::Builder + + # TODO: Depreciate old template extensions + register_template_handler :rhtml, TemplateHandlers::ERB + register_template_handler :rxml, TemplateHandlers::Builder + + def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc: + @view_paths = view_paths.respond_to?(:find) ? view_paths.dup : [*view_paths].compact + @assigns = assigns_for_first_render + @assigns_added = nil + @controller = controller + @logger = controller && controller.logger + end + + # Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true, + # it's relative to the view_paths array, otherwise it's absolute. The hash in <tt>local_assigns</tt> + # is made available as local variables. + def render_file(template_path, use_full_path = true, local_assigns = {}) #:nodoc: + if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/") + raise ActionViewError, <<-END_ERROR +Due to changes in ActionMailer, you need to provide the mailer_name along with the template name. + + render "user_mailer/signup" + render :file => "user_mailer/signup" + +If you are rendering a subtemplate, you must now use controller-like partial syntax: + + render :partial => 'signup' # no mailer_name necessary + END_ERROR + end + + @first_render ||= template_path + template_path_without_extension, template_extension = path_and_extension(template_path) + if use_full_path + if template_extension + template_file_name = full_template_path(template_path_without_extension, template_extension) + else + template_extension = pick_template_extension(template_path).to_s + unless template_extension + raise ActionViewError, "No template found for #{template_path} in #{view_paths.inspect}" + end + template_file_name = full_template_path(template_path, template_extension) + template_extension = template_extension.gsub(/^.+\./, '') # strip off any formats + end + else + template_file_name = template_path + end + + template_source = nil # Don't read the source until we know that it is required + + if template_file_name.blank? + raise ActionViewError, "Couldn't find template file for #{template_path} in #{view_paths.inspect}" + end + + begin + render_template(template_extension, template_source, template_file_name, local_assigns) + rescue Exception => e + if TemplateError === e + e.sub_template_of(template_file_name) + raise e + else + raise TemplateError.new(find_base_path_for("#{template_path_without_extension}.#{template_extension}") || view_paths.first, template_file_name, @assigns, template_source, e) + end + end + end + + # Renders the template present at <tt>template_path</tt> (relative to the view_paths array). + # The hash in <tt>local_assigns</tt> is made available as local variables. + def render(options = {}, old_local_assigns = {}, &block) #:nodoc: + if options.is_a?(String) + render_file(options, true, old_local_assigns) + elsif options == :update + update_page(&block) + elsif options.is_a?(Hash) + options = options.reverse_merge(:locals => {}, :use_full_path => true) + + if options[:layout] + path, partial_name = partial_pieces(options.delete(:layout)) + + if block_given? + @content_for_layout = capture(&block) + concat(render(options.merge(:partial => "#{path}/#{partial_name}")), block.binding) + else + @content_for_layout = render(options) + render(options.merge(:partial => "#{path}/#{partial_name}")) + end + elsif options[:file] + render_file(options[:file], options[:use_full_path], options[:locals]) + elsif options[:partial] && options[:collection] + render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals]) + elsif options[:partial] + render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]) + elsif options[:inline] + render_template(options[:type], options[:inline], nil, options[:locals]) + end + end + end + + # Renders the +template+ which is given as a string as either erb or builder depending on <tt>template_extension</tt>. + # The hash in <tt>local_assigns</tt> is made available as local variables. + def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc: + handler = self.class.handler_for_extension(template_extension) + + if template_handler_is_compilable?(handler) + compile_and_render_template(handler, template, file_path, local_assigns) + else + template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded. + delegate_render(handler, template, local_assigns) + end + end + + # Gets the full template path with base path for the given template_path and extension. + # + # full_template_path('users/show', 'html.erb') + # # => '~/rails/app/views/users/show.html.erb + # + def full_template_path(template_path, extension) + if @@cache_template_extensions + (@@cached_base_paths[template_path] ||= {})[extension.to_s] ||= find_full_template_path(template_path, extension) + else + find_full_template_path(template_path, extension) + end + end + + # Gets the extension for an existing template with the given template_path. + # Returns the format with the extension if that template exists. + # + # pick_template_extension('users/show') + # # => 'html.erb' + # + # pick_template_extension('users/legacy') + # # => "rhtml" + # + def pick_template_extension(template_path)#:nodoc: + if @@cache_template_extensions + (@@cached_template_extension[template_path] ||= {})[template_format] ||= find_template_extension_for(template_path) + else + find_template_extension_for(template_path) + end + end + + def file_exists?(template_path)#:nodoc: + template_file_name, template_file_extension = path_and_extension(template_path) + if template_file_extension + template_exists?(template_file_name, template_file_extension) + else + template_exists?(template_file_name, pick_template_extension(template_path)) + end + end + + # Returns true is the file may be rendered implicitly. + def file_public?(template_path)#:nodoc: + template_path.split('/').last[0,1] != '_' + end + + # symbolized version of the :format parameter of the request, or :html by default. + def template_format + return @template_format if @template_format + format = controller && controller.respond_to?(:request) && controller.request.parameters[:format] + @template_format = format.blank? ? :html : format.to_sym + end + + # Adds a view_path to the front of the view_paths array. + # This change affects the current request only. + # + # @template.prepend_view_path("views/default") + # @template.prepend_view_path(["views/default", "views/custom"]) + # + def prepend_view_path(path) + @view_paths.unshift(*path) + end + + # Adds a view_path to the end of the view_paths array. + # This change affects the current request only. + # + # @template.append_view_path("views/default") + # @template.append_view_path(["views/default", "views/custom"]) + # + def append_view_path(path) + @view_paths.push(*path) + end + + private + def find_full_template_path(template_path, extension) + file_name = "#{template_path}.#{extension}" + base_path = find_base_path_for(file_name) + base_path.blank? ? "" : "#{base_path}/#{file_name}" + end + + # Asserts the existence of a template. + def template_exists?(template_path, extension) + file_path = full_template_path(template_path, extension) + !file_path.blank? && @@method_names.has_key?(file_path) || File.exist?(file_path) + end + + # Splits the path and extension from the given template_path and returns as an array. + def path_and_extension(template_path) + template_path_without_extension = template_path.sub(/\.(\w+)$/, '') + [ template_path_without_extension, $1 ] + end + + # Returns the view path that contains the given relative template path. + def find_base_path_for(template_file_name) + view_paths.find { |p| File.file?(File.join(p, template_file_name)) } + end + + # Returns the view path that the full path resides in. + def extract_base_path_from(full_path) + view_paths.find { |p| full_path[0..p.size - 1] == p } + end + + # Determines the template's file extension, such as rhtml, rxml, or rjs. + def find_template_extension_for(template_path) + find_template_extension_from_handler(template_path, true) || + find_template_extension_from_handler(template_path) || + find_template_extension_from_first_render() + end + + def find_template_extension_from_handler(template_path, formatted = nil) + checked_template_path = formatted ? "#{template_path}.#{template_format}" : template_path + + self.class.template_handler_extensions.each do |extension| + if template_exists?(checked_template_path, extension) + return formatted ? "#{template_format}.#{extension}" : extension.to_s + end + end + nil + end + + # Determine the template extension from the <tt>@first_render</tt> filename + def find_template_extension_from_first_render + File.basename(@first_render.to_s)[/^[^.]+\.(.+)$/, 1] + end + + # This method reads a template file. + def read_template_file(template_path, extension) + File.read(template_path) + end + + # Evaluate the local assigns and pushes them to the view. + def evaluate_assigns + unless @assigns_added + assign_variables_from_controller + @assigns_added = true + end + end + + def delegate_render(handler, template, local_assigns) + handler.new(self).render(template, local_assigns) + end + + def delegate_compile(handler, template) + handler.new(self).compile(template) + end + + def template_handler_is_compilable?(handler) + handler.new(self).respond_to?(:compile) + end + + # Assigns instance variables from the controller to the view. + def assign_variables_from_controller + @assigns.each { |key, value| instance_variable_set("@#{key}", value) } + end + + + # Return true if the given template was compiled for a superset of the keys in local_assigns + def supports_local_assigns?(render_symbol, local_assigns) + local_assigns.empty? || + ((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) }) + end + + # Method to check whether template compilation is necessary. + # The template will be compiled if the inline template or file has not been compiled yet, + # if local_assigns has a new key, which isn't supported by the compiled code yet, + # or if the file has changed on disk and checking file mods hasn't been disabled. + def compile_template?(template, file_name, local_assigns) + method_key = file_name || template + render_symbol = @@method_names[method_key] + + compile_time = @@compile_time[render_symbol] + if compile_time && supports_local_assigns?(render_symbol, local_assigns) + if file_name && !@@cache_template_loading + template_changed_since?(file_name, compile_time) + end + else + true + end + end + + # Method to handle checking a whether a template has changed since last compile; isolated so that templates + # not stored on the file system can hook and extend appropriately. + def template_changed_since?(file_name, compile_time) + lstat = File.lstat(file_name) + compile_time < lstat.mtime || + (lstat.symlink? && compile_time < File.stat(file_name).mtime) + end + + # Method to create the source code for a given template. + def create_template_source(handler, template, render_symbol, locals) + body = delegate_compile(handler, template) + + @@template_args[render_symbol] ||= {} + locals_keys = @@template_args[render_symbol].keys | locals + @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h } + + locals_code = "" + locals_keys.each do |key| + locals_code << "#{key} = local_assigns[:#{key}]\n" + end + + "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" + end + + def assign_method_name(handler, template, file_name) + method_key = file_name || template + @@method_names[method_key] ||= compiled_method_name(handler, template, file_name) + end + + def compiled_method_name(handler, template, file_name) + ['_run', handler.to_s.demodulize.underscore, compiled_method_name_file_path_segment(file_name)].compact.join('_').to_sym + end + + def compiled_method_name_file_path_segment(file_name) + if file_name + s = File.expand_path(file_name) + s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT) + s.gsub!(/([^a-zA-Z0-9_])/) { $1.ord } + s + else + (@@inline_template_count += 1).to_s + end + end + + # Compile and evaluate the template's code + def compile_template(handler, template, file_name, local_assigns) + render_symbol = assign_method_name(handler, template, file_name) + render_source = create_template_source(handler, template, render_symbol, local_assigns.keys) + line_offset = @@template_args[render_symbol].size + handler.line_offset + + begin + file_name = 'compiled-template' if file_name.blank? + CompiledTemplates.module_eval(render_source, file_name, -line_offset) + rescue Exception => e # errors from template code + if logger + logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}" + logger.debug "Function body: #{render_source}" + logger.debug "Backtrace: #{e.backtrace.join("\n")}" + end + + raise TemplateError.new(extract_base_path_from(file_name) || view_paths.first, file_name || template, @assigns, template, e) + end + + @@compile_time[render_symbol] = Time.now + # logger.debug "Compiled template #{file_name || template}\n ==> #{render_symbol}" if logger + end + + # Render the provided template with the given local assigns. If the template has not been rendered with the provided + # local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method. + # + # Either, but not both, of template and file_path may be nil. If file_path is given, the template + # will only be read if it has to be compiled. + # + def compile_and_render_template(handler, template = nil, file_path = nil, local_assigns = {}) #:nodoc: + # convert string keys to symbols if requested + local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys + + # compile the given template, if necessary + if compile_template?(template, file_path, local_assigns) + template ||= read_template_file(file_path, nil) + compile_template(handler, template, file_path, local_assigns) + end + + # Get the method name for this template and run it + method_name = @@method_names[file_path || template] + evaluate_assigns + + send(method_name, local_assigns) do |*name| + instance_variable_get "@content_for_#{name.first || 'layout'}" + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb new file mode 100644 index 000000000..5a286432e --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/compiled_templates.rb @@ -0,0 +1,69 @@ +module ActionView + + # CompiledTemplates modules hold methods that have been compiled. + # Templates are compiled into these methods so that they do not need to be + # read and parsed for each request. + # + # Each template may be compiled into one or more methods. Each method accepts a given + # set of parameters which is used to implement local assigns passing. + # + # To use a compiled template module, create a new instance and include it into the class + # in which you want the template to be rendered. + class CompiledTemplates < Module + attr_reader :method_names + + def initialize + @method_names = Hash.new do |hash, key| + hash[key] = "__compiled_method_#{(hash.length + 1)}" + end + @mtimes = {} + end + + # Return the full key for the given identifier and argument names + def full_key(identifier, arg_names) + [identifier, arg_names] + end + + # Return the selector for this method or nil if it has not been compiled + def selector(identifier, arg_names) + key = full_key(identifier, arg_names) + method_names.key?(key) ? method_names[key] : nil + end + alias :compiled? :selector + + # Return the time at which the method for the given identifier and argument names was compiled. + def mtime(identifier, arg_names) + @mtimes[full_key(identifier, arg_names)] + end + + # Compile the provided source code for the given argument names and with the given initial line number. + # The identifier should be unique to this source. + # + # The file_name, if provided will appear in backtraces. If not provided, the file_name defaults + # to the identifier. + # + # This method will return the selector for the compiled version of this method. + def compile_source(identifier, arg_names, source, initial_line_number = 0, file_name = nil) + file_name ||= identifier + name = method_names[full_key(identifier, arg_names)] + arg_desc = arg_names.empty? ? '' : "(#{arg_names * ', '})" + fake_file_name = "#{file_name}#{arg_desc}" # Include the arguments for this version (for now) + + method_def = wrap_source(name, arg_names, source) + + begin + module_eval(method_def, fake_file_name, initial_line_number) + @mtimes[full_key(identifier, arg_names)] = Time.now + rescue Exception => e # errors from compiled source + e.blame_file! identifier + raise + end + name + end + + # Wrap the provided source in a def ... end block. + def wrap_source(name, arg_names, source) + "def #{name}(#{arg_names * ', '})\n#{source}\nend" + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb new file mode 100644 index 000000000..9b3292a9b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/active_record_helper.rb @@ -0,0 +1,255 @@ +require 'cgi' +require 'action_view/helpers/form_helper' + +module ActionView + class Base + @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" } + cattr_accessor :field_error_proc + end + + module Helpers + # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form + # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This + # is a great way of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form. + # In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html + module ActiveRecordHelper + # Returns a default input tag for the type of object returned by the method. For example, let's say you have a model + # that has an attribute +title+ of type VARCHAR column, and this instance holds "Hello World": + # input("post", "title") => + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> + def input(record_name, method, options = {}) + InstanceTag.new(record_name, method, self).to_tag(options) + end + + # Returns an entire form with all needed input tags for a specified Active Record object. For example, let's say you + # have a table model <tt>Post</tt> with attributes named <tt>title</tt> of type <tt>VARCHAR</tt> and <tt>body</tt> of type <tt>TEXT</tt>: + # form("post") + # That line would yield a form like the following: + # <form action='/post/create' method='post'> + # <p> + # <label for="post_title">Title</label><br /> + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> + # </p> + # <p> + # <label for="post_body">Body</label><br /> + # <textarea cols="40" id="post_body" name="post[body]" rows="20"> + # </textarea> + # </p> + # <input type='submit' value='Create' /> + # </form> + # + # It's possible to specialize the form builder by using a different action name and by supplying another + # block renderer. For example, let's say you have a model <tt>Entry</tt> with an attribute <tt>message</tt> of type <tt>VARCHAR</tt>: + # + # form("entry", :action => "sign", :input_block => + # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) => + # + # <form action='/post/sign' method='post'> + # Message: + # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br /> + # <input type='submit' value='Sign' /> + # </form> + # + # It's also possible to add additional content to the form by giving it a block, such as: + # + # form("entry", :action => "sign") do |form| + # form << content_tag("b", "Department") + # form << collection_select("department", "id", @departments, "id", "name") + # end + def form(record_name, options = {}) + record = instance_variable_get("@#{record_name}") + + options = options.symbolize_keys + options[:action] ||= record.new_record? ? "create" : "update" + action = url_for(:action => options[:action], :id => record) + + submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize + + contents = '' + contents << hidden_field(record_name, :id) unless record.new_record? + contents << all_input_tags(record, record_name, options) + yield contents if block_given? + contents << submit_tag(submit_value) + + content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil) + end + + # Returns a string containing the error message attached to the +method+ on the +object+ if one exists. + # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+ + # (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or + # the actual object. As an example, let's say you have a model + # +post+ that has an error message on the +title+ attribute: + # + # <%= error_message_on "post", "title" %> => + # <div class="formError">can't be empty</div> + # + # <%= error_message_on @post, "title" %> => + # <div class="formError">can't be empty</div> + # + # <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %> => + # <div class="inputError">Title simply can't be empty (or it won't work).</div> + def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") + if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) && + (errors = obj.errors.on(method)) + content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) + else + '' + end + end + + # Returns a string with a <tt>DIV</tt> containing all of the error messages for the objects located as instance variables by the names + # given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are + # provided. + # + # This <tt>DIV</tt> can be tailored by the following options: + # + # * <tt>header_tag</tt> - Used for the header of the error div (default: h2) + # * <tt>id</tt> - The id of the error div (default: errorExplanation) + # * <tt>class</tt> - The class of the error div (default: errorExplanation) + # * <tt>object</tt> - The object (or array of objects) for which to display errors, if you need to escape the instance variable convention + # * <tt>object_name</tt> - The object name to use in the header, or any text that you prefer. If <tt>object_name</tt> is not set, the name of the first object will be used. + # * <tt>header_message</tt> - The message in the header of the error div. Pass +nil+ or an empty string to avoid the header message altogether. (default: X errors prohibited this object from being saved) + # * <tt>message</tt> - The explanation message after the header message and before the error list. Pass +nil+ or an empty string to avoid the explanation message altogether. (default: There were problems with the following fields:) + # + # To specify the display for one object, you simply provide its name as a parameter. For example, for the +User+ model: + # + # error_messages_for 'user' + # + # To specify more than one object, you simply list them; optionally, you can add an extra +object_name+ parameter, which + # will be the name used in the header message. + # + # error_messages_for 'user_common', 'user', :object_name => 'user' + # + # If the objects cannot be located as instance variables, you can add an extra +object+ paremeter which gives the actual + # object (or array of objects to use) + # + # error_messages_for 'user', :object => @question.user + # + # NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what + # you need is significantly different from the default presentation, it makes plenty of sense to access the object.errors + # instance yourself and set it up. View the source of this method to see how easy it is. + def error_messages_for(*params) + options = params.extract_options!.symbolize_keys + if object = options.delete(:object) + objects = [object].flatten + else + objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact + end + count = objects.inject(0) {|sum, object| sum + object.errors.count } + unless count.zero? + html = {} + [:id, :class].each do |key| + if options.include?(key) + value = options[key] + html[key] = value unless value.blank? + else + html[key] = 'errorExplanation' + end + end + options[:object_name] ||= params.first + options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message) + options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message) + error_messages = objects.map {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } } + + contents = '' + contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank? + contents << content_tag(:p, options[:message]) unless options[:message].blank? + contents << content_tag(:ul, error_messages) + + content_tag(:div, contents, html) + else + '' + end + end + + private + def all_input_tags(record, record_name, options) + input_block = options[:input_block] || default_input_block + record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n") + end + + def default_input_block + Proc.new { |record, column| %(<p><label for="#{record}_#{column.name}">#{column.human_name}</label><br />#{input(record, column.name)}</p>) } + end + end + + class InstanceTag #:nodoc: + def to_tag(options = {}) + case column_type + when :string + field_type = @method_name.include?("password") ? "password" : "text" + to_input_field_tag(field_type, options) + when :text + to_text_area_tag(options) + when :integer, :float, :decimal + to_input_field_tag("text", options) + when :date + to_date_select_tag(options) + when :datetime, :timestamp + to_datetime_select_tag(options) + when :time + to_time_select_tag(options) + when :boolean + to_boolean_select_tag(options) + end + end + + alias_method :tag_without_error_wrapping, :tag + def tag(name, options) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name)) + else + tag_without_error_wrapping(name, options) + end + end + + alias_method :content_tag_without_error_wrapping, :content_tag + def content_tag(name, value, options) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name)) + else + content_tag_without_error_wrapping(name, value, options) + end + end + + alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag + def to_date_select_tag(options = {}) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) + else + to_date_select_tag_without_error_wrapping(options) + end + end + + alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag + def to_datetime_select_tag(options = {}) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) + else + to_datetime_select_tag_without_error_wrapping(options) + end + end + + alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag + def to_time_select_tag(options = {}) + if object.respond_to?("errors") && object.errors.respond_to?("on") + error_wrapping(to_time_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) + else + to_time_select_tag_without_error_wrapping(options) + end + end + + def error_wrapping(html_tag, has_error) + has_error ? Base.field_error_proc.call(html_tag, self) : html_tag + end + + def error_message + object.errors.on(@method_name) + end + + def column_type + object.send("column_for_attribute", @method_name).type + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb new file mode 100644 index 000000000..ba474a8ff --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -0,0 +1,548 @@ +require 'cgi' +require 'action_view/helpers/url_helper' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers #:nodoc: + # This module provides methods for generating HTML that links views to assets such + # as images, javascripts, stylesheets, and feeds. These methods do not verify + # the assets exist before linking to them. + # + # === Using asset hosts + # By default, Rails links to these assets on the current host in the public + # folder, but you can direct Rails to link to assets from a dedicated assets server by + # setting ActionController::Base.asset_host in your environment.rb. For example, + # let's say your asset host is assets.example.com. + # + # ActionController::Base.asset_host = "assets.example.com" + # image_tag("rails.png") + # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> + # stylesheet_include_tag("application") + # => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" /> + # + # This is useful since browsers typically open at most two connections to a single host, + # which means your assets often wait in single file for their turn to load. You can + # alleviate this by using a %d wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com") + # to automatically distribute asset requests among four hosts (e.g., assets0.example.com through assets3.example.com) + # so browsers will open eight connections rather than two. + # + # image_tag("rails.png") + # => <img src="http://assets0.example.com/images/rails.png" alt="Rails" /> + # stylesheet_include_tag("application") + # => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" /> + # + # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME + # the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from + # your ISP. + # + # Note: This is purely a browser performance optimization and is not meant + # for server load balancing. See http://www.die.net/musings/page_load_time/ + # for background. + # + # Alternatively, you can exert more control over the asset host by setting <tt>asset_host</tt> to a proc + # that takes a single source argument. This is useful if you are unable to setup 4 actual hosts or have + # fewer/more than 4 hosts. The example proc below generates http://assets1.example.com and + # http://assets2.example.com randomly. + # + # ActionController::Base.asset_host = Proc.new { |source| "http://assets#{rand(2) + 1}.example.com" } + # image_tag("rails.png") + # => <img src="http://assets2.example.com/images/rails.png" alt="Rails" /> + # stylesheet_include_tag("application") + # => <link href="http://assets1.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" /> + # + # The proc takes a single <tt>source</tt> parameter which is the path of the source asset. This can be used to + # generate a particular asset host depending on the asset path. + # + # ActionController::Base.asset_host = Proc.new { |source| + # if source.starts_with?('/images') + # "http://images.example.com" + # else + # "http://assets.example.com" + # end + # } + # image_tag("rails.png") + # => <img src="http://images.example.com/images/rails.png" alt="Rails" /> + # stylesheet_include_tag("application") + # => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" /> + # + # === Using asset timestamps + # + # By default, Rails will append all asset paths with that asset's timestamp. This allows you to set a cache-expiration date for the + # asset far into the future, but still be able to instantly invalidate it by simply updating the file (and hence updating the timestamp, + # which then updates the URL as the timestamp is part of that, which in turn busts the cache). + # + # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take + # advantage of this feature. Here's an example for Apache: + # + # # Asset Expiration + # ExpiresActive On + # <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$"> + # ExpiresDefault "access plus 1 year" + # </FilesMatch> + # + # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must + # have their clocks synchronized. If one of them drift out of sync, you'll see different timestamps at random and the cache won't + # work. Which means that the browser will request the same assets over and over again even thought they didn't change. You can use + # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being + # requested over and over). + module AssetTagHelper + ASSETS_DIR = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "public" + JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts" + STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets" + + # Returns a link tag that browsers and news readers can use to auto-detect + # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or + # <tt>:atom</tt>. Control the link options in url_for format using the + # +url_options+. You can modify the LINK tag itself in +tag_options+. + # + # ==== Options: + # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate" + # * <tt>:type</tt> - Override the auto-generated mime type + # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+ + # + # ==== Examples + # auto_discovery_link_tag # => + # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:atom) # => + # <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:rss, {:action => "feed"}) # => + # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> + # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # => + # <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" /> + # auto_discovery_link_tag(:rss, {:controller => "news", :action => "feed"}) # => + # <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> + # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "Example RSS"}) # => + # <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) + tag( + "link", + "rel" => tag_options[:rel] || "alternate", + "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s, + "title" => tag_options[:title] || type.to_s.upcase, + "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options + ) + end + + # Computes the path to a javascript asset in the public javascripts directory. + # If the +source+ filename has no extension, .js will be appended. + # Full paths from the document root will be passed through. + # Used internally by javascript_include_tag to build the script path. + # + # ==== Examples + # javascript_path "xmlhr" # => /javascripts/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js + def javascript_path(source) + compute_public_path(source, 'javascripts', 'js') + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + + JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES) + @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup + + # Returns an html script tag for each of the +sources+ provided. You + # can pass in the filename (.js extension is optional) of javascript files + # that exist in your public/javascripts directory for inclusion into the + # current page or you can pass the full path relative to your document + # root. To include the Prototype and Scriptaculous javascript libraries in + # your application, pass <tt>:defaults</tt> as the source. When using + # :defaults, if an <tt>application.js</tt> file exists in your public + # javascripts directory, it will be included as well. You can modify the + # html attributes of the script tag by passing a hash as the last argument. + # + # ==== Examples + # javascript_include_tag "xmlhr" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js"></script> + # + # javascript_include_tag "xmlhr.js" # => + # <script type="text/javascript" src="/javascripts/xmlhr.js"></script> + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # <script type="text/javascript" src="/javascripts/common.javascript"></script> + # <script type="text/javascript" src="/elsewhere/cools.js"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script> + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => + # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script> + # + # javascript_include_tag :defaults # => + # <script type="text/javascript" src="/javascripts/prototype.js"></script> + # <script type="text/javascript" src="/javascripts/effects.js"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js"></script> + # + # * = The application.js file is only referenced if it exists + # + # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason + # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method. + # + # You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source: + # + # javascript_include_tag :all # => + # <script type="text/javascript" src="/javascripts/prototype.js"></script> + # <script type="text/javascript" src="/javascripts/effects.js"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js"></script> + # <script type="text/javascript" src="/javascripts/shop.js"></script> + # <script type="text/javascript" src="/javascripts/checkout.js"></script> + # + # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to + # all subsequently included files. + # + # == Caching multiple javascripts into one + # + # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching + # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development + # environment). + # + # ==== Examples + # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js"></script> + # <script type="text/javascript" src="/javascripts/effects.js"></script> + # ... + # <script type="text/javascript" src="/javascripts/application.js"></script> + # <script type="text/javascript" src="/javascripts/shop.js"></script> + # <script type="text/javascript" src="/javascripts/checkout.js"></script> + # + # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is true => + # <script type="text/javascript" src="/javascripts/all.js"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false => + # <script type="text/javascript" src="/javascripts/prototype.js"></script> + # <script type="text/javascript" src="/javascripts/cart.js"></script> + # <script type="text/javascript" src="/javascripts/checkout.js"></script> + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false => + # <script type="text/javascript" src="/javascripts/shop.js"></script> + def javascript_include_tag(*sources) + options = sources.extract_options!.stringify_keys + cache = options.delete("cache") + + if ActionController::Base.perform_caching && cache + joined_javascript_name = (cache == true ? "all" : cache) + ".js" + joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name) + + write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources)) + javascript_src_tag(joined_javascript_name, options) + else + expand_javascript_sources(sources).collect { |source| javascript_src_tag(source, options) }.join("\n") + end + end + + # Register one or more additional JavaScript files to be included when + # <tt>javascript_include_tag :defaults</tt> is called. This method is + # typically intended to be called from plugin initialization to register additional + # .js files that the plugin installed in <tt>public/javascripts</tt>. + def self.register_javascript_include_default(*sources) + @@javascript_default_sources.concat(sources) + end + + def self.reset_javascript_include_default #:nodoc: + @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup + end + + # Computes the path to a stylesheet asset in the public stylesheets directory. + # If the +source+ filename has no extension, .css will be appended. + # Full paths from the document root will be passed through. + # Used internally by stylesheet_link_tag to build the stylesheet path. + # + # ==== Examples + # stylesheet_path "style" # => /stylesheets/style.css + # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "/dir/style.css" # => /dir/style.css + # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css + # stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css + def stylesheet_path(source) + compute_public_path(source, 'stylesheets', 'css') + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + + # Returns a stylesheet link tag for the sources specified as arguments. If + # you don't specify an extension, .css will be appended automatically. + # You can modify the link attributes by passing a hash as the last argument. + # + # ==== Examples + # stylesheet_link_tag "style" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style.css" # => + # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => + # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "all" # => + # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "style", :media => "print" # => + # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" /> + # + # You can also include all styles in the stylesheet directory using :all as the source: + # + # stylesheet_link_tag :all # => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # == Caching multiple stylesheets into one + # + # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # ==== Examples + # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is false => + # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is true => + # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is false => + # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" /> + # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" /> + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true => + # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" /> + def stylesheet_link_tag(*sources) + options = sources.extract_options!.stringify_keys + cache = options.delete("cache") + + if ActionController::Base.perform_caching && cache + joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" + joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name) + + write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources)) + stylesheet_tag(joined_stylesheet_name, options) + else + expand_stylesheet_sources(sources).collect { |source| stylesheet_tag(source, options) }.join("\n") + end + end + + # Computes the path to an image asset in the public images directory. + # Full paths from the document root will be passed through. + # Used internally by image_tag to build the image path. + # + # ==== Examples + # image_path("edit") # => /images/edit + # image_path("edit.png") # => /images/edit.png + # image_path("icons/edit.png") # => /images/icons/edit.png + # image_path("/icons/edit.png") # => /icons/edit.png + # image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png + def image_path(source) + compute_public_path(source, 'images') + end + alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + + # Returns an html image tag for the +source+. The +source+ can be a full + # path or a file that exists in your public images directory. + # + # ==== Options + # You can add HTML attributes using the +options+. The +options+ supports + # three additional keys for convenience and conformance: + # + # * <tt>:alt</tt> - If no alt text is given, the file name part of the + # +source+ is used (capitalized and without the extension) + # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes + # width="30" and height="45". <tt>:size</tt> will be ignored if the + # value is not in the correct format. + # * <tt>:mouseover</tt> - Set an alternate image to be used when the onmouseover + # event is fired, and sets the original image to be replaced onmouseout. + # This can be used to implement an easy image toggle that fires on onmouseover. + # + # ==== Examples + # image_tag("icon") # => + # <img src="/images/icon" alt="Icon" /> + # image_tag("icon.png") # => + # <img src="/images/icon.png" alt="Icon" /> + # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry") # => + # <img src="/images/icon.png" width="16" height="10" alt="Edit Entry" /> + # image_tag("/icons/icon.gif", :size => "16x16") # => + # <img src="/icons/icon.gif" width="16" height="16" alt="Icon" /> + # image_tag("/icons/icon.gif", :height => '32', :width => '32') # => + # <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> + # image_tag("/icons/icon.gif", :class => "menu_icon") # => + # <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # => + # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> + # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # => + # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> + def image_tag(source, options = {}) + options.symbolize_keys! + + options[:src] = path_to_image(source) + options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize + + if size = options.delete(:size) + options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} + end + + if mouseover = options.delete(:mouseover) + options[:onmouseover] = "this.src='#{image_path(mouseover)}'" + options[:onmouseout] = "this.src='#{image_path(options[:src])}'" + end + + tag("img", options) + end + + private + def file_exist?(path) + @@file_exist_cache ||= {} + if !(@@file_exist_cache[path] ||= File.exist?(path)) + @@file_exist_cache[path] = true + false + else + true + end + end + + # Add the .ext if not present. Return full URLs otherwise untouched. + # Prefix with /dir/ if lacking a leading /. Account for relative URL + # roots. Rewrite the asset path for cache-busting asset ids. Include + # asset host, if configured, with the correct request protocol. + def compute_public_path(source, dir, ext = nil, include_host = true) + has_request = @controller.respond_to?(:request) + + cache_key = + if has_request + [ @controller.request.protocol, + ActionController::Base.asset_host.to_s, + @controller.request.relative_url_root, + dir, source, ext, include_host ].join + else + [ ActionController::Base.asset_host.to_s, + dir, source, ext, include_host ].join + end + + ActionView::Base.computed_public_paths[cache_key] ||= + begin + source += ".#{ext}" if File.extname(source).blank? && ext + + if source =~ %r{^[-a-z]+://} + source + else + source = "/#{dir}/#{source}" unless source[0] == ?/ + if has_request + source = "#{@controller.request.relative_url_root}#{source}" + end + rewrite_asset_path!(source) + + if include_host + host = compute_asset_host(source) + + if has_request && !host.blank? && host !~ %r{^[-a-z]+://} + host = "#{@controller.request.protocol}#{host}" + end + + "#{host}#{source}" + else + source + end + end + end + end + + # Pick an asset host for this source. Returns nil if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains %d (the number is the source hash mod 4), + # or the value returned from invoking the proc if it's a proc. + def compute_asset_host(source) + if host = ActionController::Base.asset_host + if host.is_a?(Proc) + host.call(source) + else + host % (source.hash % 4) + end + end + end + + # Use the RAILS_ASSET_ID environment variable or the source's + # modification time as its cache-busting asset id. + def rails_asset_id(source) + if asset_id = ENV["RAILS_ASSET_ID"] + asset_id + else + path = File.join(ASSETS_DIR, source) + + if File.exist?(path) + File.mtime(path).to_i.to_s + else + '' + end + end + end + + # Break out the asset path rewrite so you wish to put the asset id + # someplace other than the query string. + def rewrite_asset_path!(source) + asset_id = rails_asset_id(source) + source << "?#{asset_id}" if !asset_id.blank? + end + + def javascript_src_tag(source, options) + content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options)) + end + + def stylesheet_tag(source, options) + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false) + end + + def compute_javascript_paths(sources) + expand_javascript_sources(sources).collect { |source| compute_public_path(source, 'javascripts', 'js', false) } + end + + def compute_stylesheet_paths(sources) + expand_stylesheet_sources(sources).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) } + end + + def expand_javascript_sources(sources) + case + when sources.include?(:all) + all_javascript_files = Dir[File.join(JAVASCRIPTS_DIR, '*.js')].collect { |file| File.basename(file).split(".", 0).first }.sort + sources = ((@@javascript_default_sources.dup & all_javascript_files) + all_javascript_files).uniq + + when sources.include?(:defaults) + sources = sources[0..(sources.index(:defaults))] + + @@javascript_default_sources.dup + + sources[(sources.index(:defaults) + 1)..sources.length] + + sources.delete(:defaults) + sources << "application" if file_exist?(File.join(JAVASCRIPTS_DIR, "application.js")) + end + + sources + end + + def expand_stylesheet_sources(sources) + if sources.first == :all + @@all_stylesheet_sources ||= Dir[File.join(STYLESHEETS_DIR, '*.css')].collect { |file| File.basename(file).split(".", 1).first }.sort + else + sources + end + end + + def join_asset_file_contents(paths) + paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n") + end + + def write_asset_file_contents(joined_asset_path, asset_paths) + unless file_exist?(joined_asset_path) + FileUtils.mkdir_p(File.dirname(joined_asset_path)) + File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) } + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb new file mode 100644 index 000000000..c3ceecbf5 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -0,0 +1,111 @@ +# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other +# template languages). +module ActionView + module Helpers #:nodoc: + module AtomFeedHelper + # Full usage example: + # + # config/routes.rb: + # ActionController::Routing::Routes.draw do |map| + # map.resources :posts + # map.root :controller => "posts" + # end + # + # app/controllers/posts_controller.rb: + # class PostsController < ApplicationController::Base + # # GET /posts.html + # # GET /posts.atom + # def index + # @posts = Post.find(:all) + # + # respond_to do |format| + # format.html + # format.atom + # end + # end + # end + # + # app/views/posts/index.atom.builder: + # atom_feed do |feed| + # feed.title("My great blog!") + # feed.updated((@posts.first.created_at)) + # + # for post in @posts + # feed.entry(post) do |entry| + # entry.title(post.title) + # entry.content(post.body, :type => 'html') + # + # entry.author do |author| + # author.name("DHH") + # end + # end + # end + # end + # + # The options are for atom_feed are: + # + # * <tt>:language</tt>: Defaults to "en-US". + # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. + # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. + # + # atom_feed yields a AtomFeedBuilder instance. + def atom_feed(options = {}, &block) + xml = options[:xml] || eval("xml", block.binding) + xml.instruct! + + xml.feed "xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do + xml.id("tag:#{request.host}:#{request.request_uri.split(".")[0].gsub("/", "")}") + xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port)) + + if options[:url] + xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url) + end + + yield AtomFeedBuilder.new(xml, self) + end + end + + + class AtomFeedBuilder + def initialize(xml, view) + @xml, @view = xml, view + end + + # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + def updated(date_or_time = nil) + @xml.updated((date_or_time || Time.now.utc).xmlschema) + end + + # Creates an entry tag for a specific record and prefills the id using class and id. + # + # Options: + # + # * <tt>:updated</tt>: Time of update. Defaults to the created_at attribute on the record if one such exists. + # * <tt>:published</tt>: Time first published. Defaults to the updated_at attribute on the record if one such exists. + # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record. + def entry(record, options = {}) + @xml.entry do + @xml.id("tag:#{@view.request.host_with_port}:#{record.class}#{record.id}") + + if options[:published] || (record.respond_to?(:created_at) && record.created_at) + @xml.published((options[:published] || record.created_at).xmlschema) + end + + if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at) + @xml.updated((options[:updated] || record.updated_at).xmlschema) + end + + @xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:url] || @view.polymorphic_url(record)) + + yield @xml + end + end + + private + def method_missing(method, *arguments) + @xml.__send__(method, *arguments) + end + end + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb new file mode 100644 index 000000000..fefa28f4d --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/benchmark_helper.rb @@ -0,0 +1,31 @@ +require 'benchmark' + +module ActionView + module Helpers + # This helper offers a method to measure the execution time of a block + # in a template. + module BenchmarkHelper + # Allows you to measure the execution time of a block + # in a template and records the result to the log. Wrap this block around + # expensive operations or possible bottlenecks to get a time reading + # for the operation. For example, let's say you thought your file + # processing method was taking too long; you could wrap it in a benchmark block. + # + # <% benchmark "Process data files" do %> + # <%= expensive_files_operation %> + # <% end %> + # + # That would add something like "Process data files (0.34523)" to the log, + # which you can then use to compare timings when optimizing your code. + # + # You may give an optional logger level as the second argument + # (:debug, :info, :warn, :error); the default value is :info. + def benchmark(message = "Benchmarking", level = :info) + if @logger + real = Benchmark.realtime { yield } + @logger.send level, "#{message} (#{'%.5f' % real})" + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb new file mode 100644 index 000000000..cf5420a35 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/cache_helper.rb @@ -0,0 +1,39 @@ +module ActionView + module Helpers + # This helper to exposes a method for caching of view fragments. + # See ActionController::Caching::Fragments for usage instructions. + module CacheHelper + # A method for caching fragments of a view rather than an entire + # action or page. This technique is useful caching pieces like + # menus, lists of news topics, static HTML fragments, and so on. + # This method takes a block that contains the content you wish + # to cache. See ActionController::Caching::Fragments for more + # information. + # + # ==== Examples + # If you wanted to cache a navigation menu, you could do the + # following. + # + # <% cache do %> + # <%= render :partial => "menu" %> + # <% end %> + # + # You can also cache static content... + # + # <% cache do %> + # <p>Hello users! Welcome to our website!</p> + # <% end %> + # + # ...and static content mixed with RHTML content. + # + # <% cache do %> + # Topics: + # <%= render :partial => "topics", :collection => @topic_list %> + # <i>Topics listed alphabetically</i> + # <% end %> + def cache(name = {}, &block) + @controller.cache_erb_fragment(block, name) + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb new file mode 100644 index 000000000..fc8cd66d7 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/capture_helper.rb @@ -0,0 +1,162 @@ +module ActionView + module Helpers + # CaptureHelper exposes methods to let you extract generated markup which + # can be used in other parts of a template or layout file. + # It provides a method to capture blocks into variables through capture and + # a way to capture a block of markup for use in a layout through content_for. + module CaptureHelper + # The capture method allows you to extract part of a template into a + # variable. You can then use this variable anywhere in your templates or layout. + # + # ==== Examples + # The capture method can be used in ERb templates... + # + # <% @greeting = capture do %> + # Welcome to my shiny new web page! The date and time is + # <%= Time.now %> + # <% end %> + # + # ...and Builder (RXML) templates. + # + # @timestamp = capture do + # "The current timestamp is #{Time.now}." + # end + # + # You can then use that variable anywhere else. For example: + # + # <html> + # <head><title><%= @greeting %></title></head> + # <body> + # <b><%= @greeting %></b> + # </body></html> + # + def capture(*args, &block) + # execute the block + begin + buffer = eval(ActionView::Base.erb_variable, block.binding) + rescue + buffer = nil + end + + if buffer.nil? + capture_block(*args, &block).to_s + else + capture_erb_with_buffer(buffer, *args, &block).to_s + end + end + + # Calling content_for stores a block of markup in an identifier for later use. + # You can make subsequent calls to the stored content in other templates or the layout + # by passing the identifier as an argument to <tt>yield</tt>. + # + # ==== Examples + # + # <% content_for :not_authorized do %> + # alert('You are not authorized to do that!') + # <% end %> + # + # You can then use <tt>yield :not_authorized</tt> anywhere in your templates. + # + # <%= yield :not_authorized if current_user.nil? %> + # + # You can also use this syntax alongside an existing call to <tt>yield</tt> in a layout. For example: + # + # <%# This is the layout %> + # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + # <head> + # <title>My Website</title> + # <%= yield :script %> + # </head> + # <body> + # <%= yield %> + # </body> + # </html> + # + # And now, we'll create a view that has a content_for call that + # creates the <tt>script</tt> identifier. + # + # <%# This is our view %> + # Please login! + # + # <% content_for :script do %> + # <script type="text/javascript">alert('You are not authorized to view this page!')</script> + # <% end %> + # + # Then, in another view, you could to do something like this: + # + # <%= link_to_remote 'Logout', :action => 'logout' %> + # + # <% content_for :script do %> + # <%= javascript_include_tag :defaults %> + # <% end %> + # + # That will place <script> tags for Prototype, Scriptaculous, and application.js (if it exists) + # on the page; this technique is useful if you'll only be using these scripts in a few views. + # + # Note that content_for concatenates the blocks it is given for a particular + # identifier in order. For example: + # + # <% content_for :navigation do %> + # <li><%= link_to 'Home', :action => 'index' %></li> + # <% end %> + # + # <%# Add some other content, or use a different template: %> + # + # <% content_for :navigation do %> + # <li><%= link_to 'Login', :action => 'login' %></li> + # <% end %> + # + # Then, in another template or layout, this code would render both links in order: + # + # <ul><%= yield :navigation %></ul> + # + # Lastly, simple content can be passed as a parameter: + # + # <% content_for :script, javascript_include_tag(:defaults) %> + # + # WARNING: content_for is ignored in caches. So you shouldn't use it + # for elements that will be fragment cached. + # + # The deprecated way of accessing a content_for block is to use an instance variable + # named <tt>@content_for_#{name_of_the_content_block}</tt>. So <tt><%= content_for :footer %></tt> + # would be available as <tt><%= @content_for_footer %></tt>. The preferred usage is now + # <tt><%= yield :footer %></tt>. + def content_for(name, content = nil, &block) + existing_content_for = instance_variable_get("@content_for_#{name}").to_s + new_content_for = existing_content_for + (block_given? ? capture(&block) : content) + instance_variable_set("@content_for_#{name}", new_content_for) + end + + private + def capture_block(*args, &block) + block.call(*args) + end + + def capture_erb(*args, &block) + buffer = eval(ActionView::Base.erb_variable, block.binding) + capture_erb_with_buffer(buffer, *args, &block) + end + + def capture_erb_with_buffer(buffer, *args, &block) + pos = buffer.length + block.call(*args) + + # extract the block + data = buffer[pos..-1] + + # replace it in the original with empty string + buffer[pos..-1] = '' + + data + end + + def erb_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)" + end + + def block_content_for(name, &block) + eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)" + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb new file mode 100755 index 000000000..c2091dfd4 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/date_helper.rb @@ -0,0 +1,689 @@ +require "date" + +module ActionView + module Helpers + # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods + # share a number of common options that are as follows: + # + # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give + # birthday[month] instead of date[month] if passed to the select_month method. + # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date. + # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month + # method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]". + module DateHelper + DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') + + # Reports the approximate distance in time between two Time or Date objects or integers as seconds. + # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs + # Distances are reported base on the following table: + # + # 0 <-> 29 secs # => less than a minute + # 30 secs <-> 1 min, 29 secs # => 1 minute + # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes + # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour + # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours + # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day + # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days + # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month + # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months + # 1 yr <-> 2 yrs minus 1 secs # => about 1 year + # 2 yrs <-> max time or date # => over [2..X] years + # + # With include_seconds = true and the difference < 1 minute 29 seconds + # 0-4 secs # => less than 5 seconds + # 5-9 secs # => less than 10 seconds + # 10-19 secs # => less than 20 seconds + # 20-39 secs # => half a minute + # 40-59 secs # => less than a minute + # 60-89 secs # => 1 minute + # + # ==== Examples + # from_time = Time.now + # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour + # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour + # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute + # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds + # distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years + # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days + # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute + # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute + # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute + # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year + # distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years + # + # to_time = Time.now + 6.years + 19.days + # distance_of_time_in_words(from_time, to_time, true) # => over 6 years + # distance_of_time_in_words(to_time, from_time, true) # => over 6 years + # distance_of_time_in_words(Time.now, Time.now) # => less than a minute + # + def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) + from_time = from_time.to_time if from_time.respond_to?(:to_time) + to_time = to_time.to_time if to_time.respond_to?(:to_time) + distance_in_minutes = (((to_time - from_time).abs)/60).round + distance_in_seconds = ((to_time - from_time).abs).round + + case distance_in_minutes + when 0..1 + return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds + case distance_in_seconds + when 0..4 then 'less than 5 seconds' + when 5..9 then 'less than 10 seconds' + when 10..19 then 'less than 20 seconds' + when 20..39 then 'half a minute' + when 40..59 then 'less than a minute' + else '1 minute' + end + + when 2..44 then "#{distance_in_minutes} minutes" + when 45..89 then 'about 1 hour' + when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" + when 1440..2879 then '1 day' + when 2880..43199 then "#{(distance_in_minutes / 1440).round} days" + when 43200..86399 then 'about 1 month' + when 86400..525599 then "#{(distance_in_minutes / 43200).round} months" + when 525600..1051199 then 'about 1 year' + else "over #{(distance_in_minutes / 525600).round} years" + end + end + + # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. + # + # ==== Examples + # time_ago_in_words(3.minutes.from_now) # => 3 minutes + # time_ago_in_words(Time.now - 15.hours) # => 15 hours + # time_ago_in_words(Time.now) # => less than a minute + # + # from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days + def time_ago_in_words(from_time, include_seconds = false) + distance_of_time_in_words(from_time, Time.now, include_seconds) + end + + alias_method :distance_of_time_in_words_to_now, :time_ago_in_words + + # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by + # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, + # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of + # discard options. The discard options are <tt>:discard_year</tt>, <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll + # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly + # set the order of the tags using the <tt>:order</tt> option with an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in + # the desired order. Symbols may be omitted and the respective select is not included. + # + # Pass the <tt>:default</tt> option to set the default date. Use a Time object or a Hash of :year, :month, :day, :hour, :minute, and :second. + # + # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. + # + # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. + # + # ==== Examples + # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute + # date_select("post", "written_on") + # + # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute, + # # with the year in the year drop down box starting at 1995. + # date_select("post", "written_on", :start_year => 1995) + # + # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute, + # # with the year in the year drop down box starting at 1995, numbers used for months instead of words, + # # and without a day select box. + # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, + # :discard_day => true, :include_blank => true) + # + # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute + # # with the fields ordered as day, month, year rather than month, day, year. + # date_select("post", "written_on", :order => [:day, :month, :year]) + # + # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute + # # lacking a year field. + # date_select("user", "birthday", :order => [:month, :day]) + # + # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute + # # which is initially set to the date 3 days from the current date + # date_select("post", "written_on", :default => 3.days.from_now) + # + # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute + # # that will have a default day of 20. + # date_select("credit_card", "bill_due", :default => { :day => 20 }) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + # + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month + # choices are valid. + def date_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) + end + + # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified + # time-based attribute (identified by +method+) on an object assigned to the template (identified by +object+). + # You can include the seconds with <tt>:include_seconds</tt>. + # + # ==== Examples + # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute + # time_select("post", "sunrise") + # + # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute + # time_select("order", "submitted") + # + # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute + # time_select("mail", "sent_at") + # + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in + # # the sunrise attribute. + # time_select("post", "start_time", :include_seconds => true) + # + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in + # # the submission_time attribute. + # time_select("entry", "submission_time", :include_seconds => true) + # + # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45. + # time_select 'game', 'game_time', {:minute_step => 15} + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + # + # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that all month + # choices are valid. + def time_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_time_select_tag(options) + end + + # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based + # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: + # + # ==== Examples + # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on attribute + # datetime_select("post", "written_on") + # + # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the + # # post variable in the written_on attribute. + # datetime_select("post", "written_on", :start_year => 1995) + # + # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will be stored in the + # # trip variable in the departing attribute. + # datetime_select("trip", "departing", :default => 3.days.from_now) + # + # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable as the written_on + # # attribute. + # datetime_select("post", "written_on", :discard_type => true) + # + # The selects are prepared for multi-parameter assignment to an Active Record object. + def datetime_select(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) + end + + # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. + # It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it + # will be appended onto the <tt>:order</tt> passed in. You can also add <tt>:date_separator</tt> and <tt>:time_separator</tt> + # keys to the +options+ to control visual display of the elements. + # + # ==== Examples + # my_date_time = Time.now + 4.days + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # select_datetime(my_date_time) + # + # # Generates a datetime select that defaults to today (no specified datetime) + # select_datetime() + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with the fields ordered year, month, day rather than month, day, year. + # select_datetime(my_date_time, :order => [:year, :month, :day]) + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # with a '/' between each date field. + # select_datetime(my_date_time, :date_separator => '/') + # + # # Generates a datetime select that discards the type of the field and defaults to the datetime in + # # my_date_time (four days after today) + # select_datetime(my_date_time, :discard_type => true) + # + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # prefixed with 'payday' rather than 'date' + # select_datetime(my_date_time, :prefix => 'payday') + # + def select_datetime(datetime = Time.now, options = {}) + separator = options[:datetime_separator] || '' + select_date(datetime, options) + separator + select_time(datetime, options) + end + + # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. + # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, it + # will be appended onto the <tt>:order</tt> passed in. + # + # ==== Examples + # my_date = Time.today + 6.days + # + # # Generates a date select that defaults to the date in my_date (six days after today) + # select_date(my_date) + # + # # Generates a date select that defaults to today (no specified date) + # select_date() + # + # # Generates a date select that defaults to the date in my_date (six days after today) + # # with the fields ordered year, month, day rather than month, day, year. + # select_date(my_date, :order => [:year, :month, :day]) + # + # # Generates a date select that discards the type of the field and defaults to the date in + # # my_date (six days after today) + # select_datetime(my_date_time, :discard_type => true) + # + # # Generates a date select that defaults to the datetime in my_date (six days after today) + # # prefixed with 'payday' rather than 'date' + # select_datetime(my_date_time, :prefix => 'payday') + # + def select_date(date = Date.today, options = {}) + options[:order] ||= [] + [:year, :month, :day].each { |o| options[:order].push(o) unless options[:order].include?(o) } + + select_date = '' + options[:order].each do |o| + select_date << self.send("select_#{o}", date, options) + end + select_date + end + + # Returns a set of html select-tags (one for hour and minute) + # You can set <tt>:time_separator</tt> key to format the output, and + # the <tt>:include_seconds</tt> option to include an input for seconds. + # + # ==== Examples + # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds + # + # # Generates a time select that defaults to the time in my_time + # select_time(my_time) + # + # # Generates a time select that defaults to the current time (no specified time) + # select_time() + # + # # Generates a time select that defaults to the time in my_time, + # # which has fields separated by ':' + # select_time(my_time, :time_separator => ':') + # + # # Generates a time select that defaults to the time in my_time, + # # that also includes an input for seconds + # select_time(my_time, :include_seconds => true) + # + # # Generates a time select that defaults to the time in my_time, that has fields + # # separated by ':' and includes an input for seconds + # select_time(my_time, :time_separator => ':', :include_seconds => true) + # + def select_time(datetime = Time.now, options = {}) + separator = options[:time_separator] || '' + select_hour(datetime, options) + separator + select_minute(datetime, options) + (options[:include_seconds] ? separator + select_second(datetime, options) : '') + end + + # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. + # The <tt>second</tt> can also be substituted for a second number. + # Override the field name using the <tt>:field_name</tt> option, 'second' by default. + # + # ==== Examples + # my_time = Time.now + 16.minutes + # + # # Generates a select field for seconds that defaults to the seconds for the time in my_time + # select_second(my_time) + # + # # Generates a select field for seconds that defaults to the number given + # select_second(33) + # + # # Generates a select field for seconds that defaults to the seconds for the time in my_time + # # that is named 'interval' rather than 'second' + # select_second(my_time, :field_name => 'interval') + # + def select_second(datetime, options = {}) + val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) : '' + if options[:use_hidden] + options[:include_seconds] ? hidden_html(options[:field_name] || 'second', val, options) : '' + else + second_options = [] + 0.upto(59) do |second| + second_options << ((val == second) ? + %(<option value="#{leading_zero_on_single_digits(second)}" selected="selected">#{leading_zero_on_single_digits(second)}</option>\n) : + %(<option value="#{leading_zero_on_single_digits(second)}">#{leading_zero_on_single_digits(second)}</option>\n) + ) + end + select_html(options[:field_name] || 'second', second_options, options) + end + end + + # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. + # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute selected + # The <tt>minute</tt> can also be substituted for a minute number. + # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. + # + # ==== Examples + # my_time = Time.now + 6.hours + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # select_minute(my_time) + # + # # Generates a select field for minutes that defaults to the number given + # select_minute(14) + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # # that is named 'stride' rather than 'second' + # select_minute(my_time, :field_name => 'stride') + # + def select_minute(datetime, options = {}) + val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.min) : '' + if options[:use_hidden] + hidden_html(options[:field_name] || 'minute', val, options) + else + minute_options = [] + 0.step(59, options[:minute_step] || 1) do |minute| + minute_options << ((val == minute) ? + %(<option value="#{leading_zero_on_single_digits(minute)}" selected="selected">#{leading_zero_on_single_digits(minute)}</option>\n) : + %(<option value="#{leading_zero_on_single_digits(minute)}">#{leading_zero_on_single_digits(minute)}</option>\n) + ) + end + select_html(options[:field_name] || 'minute', minute_options, options) + end + end + + # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. + # The <tt>hour</tt> can also be substituted for a hour number. + # Override the field name using the <tt>:field_name</tt> option, 'hour' by default. + # + # ==== Examples + # my_time = Time.now + 6.hours + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # select_minute(my_time) + # + # # Generates a select field for minutes that defaults to the number given + # select_minute(14) + # + # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # # that is named 'stride' rather than 'second' + # select_minute(my_time, :field_name => 'stride') + # + def select_hour(datetime, options = {}) + val = datetime ? (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) : '' + if options[:use_hidden] + hidden_html(options[:field_name] || 'hour', val, options) + else + hour_options = [] + 0.upto(23) do |hour| + hour_options << ((val == hour) ? + %(<option value="#{leading_zero_on_single_digits(hour)}" selected="selected">#{leading_zero_on_single_digits(hour)}</option>\n) : + %(<option value="#{leading_zero_on_single_digits(hour)}">#{leading_zero_on_single_digits(hour)}</option>\n) + ) + end + select_html(options[:field_name] || 'hour', hour_options, options) + end + end + + # Returns a select tag with options for each of the days 1 through 31 with the current day selected. + # The <tt>date</tt> can also be substituted for a hour number. + # Override the field name using the <tt>:field_name</tt> option, 'day' by default. + # + # ==== Examples + # my_date = Time.today + 2.days + # + # # Generates a select field for days that defaults to the day for the date in my_date + # select_day(my_time) + # + # # Generates a select field for days that defaults to the number given + # select_day(5) + # + # # Generates a select field for days that defaults to the day for the date in my_date + # # that is named 'due' rather than 'day' + # select_day(my_time, :field_name => 'due') + # + def select_day(date, options = {}) + val = date ? (date.kind_of?(Fixnum) ? date : date.day) : '' + if options[:use_hidden] + hidden_html(options[:field_name] || 'day', val, options) + else + day_options = [] + 1.upto(31) do |day| + day_options << ((val == day) ? + %(<option value="#{day}" selected="selected">#{day}</option>\n) : + %(<option value="#{day}">#{day}</option>\n) + ) + end + select_html(options[:field_name] || 'day', day_options, options) + end + end + + # Returns a select tag with options for each of the months January through December with the current month selected. + # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values + # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- + # set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names, + # set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer to show month names as abbreviations, + # set the <tt>:use_short_month</tt> key in +options+ to true. If you want to use your own month names, set the + # <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. Override the field name using the + # <tt>:field_name</tt> option, 'month' by default. + # + # ==== Examples + # # Generates a select field for months that defaults to the current month that + # # will use keys like "January", "March". + # select_month(Date.today) + # + # # Generates a select field for months that defaults to the current month that + # # is named "start" rather than "month" + # select_month(Date.today, :field_name => 'start') + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "1", "3". + # select_month(Date.today, :use_month_numbers => true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "1 - January", "3 - March". + # select_month(Date.today, :add_month_numbers => true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "Jan", "Mar". + # select_month(Date.today, :use_short_month => true) + # + # # Generates a select field for months that defaults to the current month that + # # will use keys like "Januar", "Marts." + # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) + # + def select_month(date, options = {}) + val = date ? (date.kind_of?(Fixnum) ? date : date.month) : '' + if options[:use_hidden] + hidden_html(options[:field_name] || 'month', val, options) + else + month_options = [] + month_names = options[:use_month_names] || (options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES) + month_names.unshift(nil) if month_names.size < 13 + 1.upto(12) do |month_number| + month_name = if options[:use_month_numbers] + month_number + elsif options[:add_month_numbers] + month_number.to_s + ' - ' + month_names[month_number] + else + month_names[month_number] + end + + month_options << ((val == month_number) ? + %(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) : + %(<option value="#{month_number}">#{month_name}</option>\n) + ) + end + select_html(options[:field_name] || 'month', month_options, options) + end + end + + # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius + # can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. Both ascending and descending year + # lists are supported by making <tt>:start_year</tt> less than or greater than <tt>:end_year</tt>. The <tt>date</tt> can also be + # substituted for a year given as a number. Override the field name using the <tt>:field_name</tt> option, 'year' by default. + # + # ==== Examples + # # Generates a select field for years that defaults to the current year that + # # has ascending year values + # select_year(Date.today, :start_year => 1992, :end_year => 2007) + # + # # Generates a select field for years that defaults to the current year that + # # is named 'birth' rather than 'year' + # select_year(Date.today, :field_name => 'birth') + # + # # Generates a select field for years that defaults to the current year that + # # has descending year values + # select_year(Date.today, :start_year => 2005, :end_year => 1900) + # + # # Generates a select field for years that defaults to the year 2006 that + # # has ascending year values + # select_year(2006, :start_year => 2000, :end_year => 2010) + # + def select_year(date, options = {}) + val = date ? (date.kind_of?(Fixnum) ? date : date.year) : '' + if options[:use_hidden] + hidden_html(options[:field_name] || 'year', val, options) + else + year_options = [] + y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year + + start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5) + step_val = start_year < end_year ? 1 : -1 + start_year.step(end_year, step_val) do |year| + year_options << ((val == year) ? + %(<option value="#{year}" selected="selected">#{year}</option>\n) : + %(<option value="#{year}">#{year}</option>\n) + ) + end + select_html(options[:field_name] || 'year', year_options, options) + end + end + + private + + def select_html(type, html_options, options) + name_and_id_from_options(options, type) + select_html = %(<select id="#{options[:id]}" name="#{options[:name]}") + select_html << %( disabled="disabled") if options[:disabled] + select_html << %(>\n) + select_html << %(<option value=""></option>\n) if options[:include_blank] + select_html << html_options.to_s + select_html << "</select>\n" + end + + def hidden_html(type, value, options) + name_and_id_from_options(options, type) + hidden_html = %(<input type="hidden" id="#{options[:id]}" name="#{options[:name]}" value="#{value}" />\n) + end + + def name_and_id_from_options(options, type) + options[:name] = (options[:prefix] || DEFAULT_PREFIX) + (options[:discard_type] ? '' : "[#{type}]") + options[:id] = options[:name].gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') + end + + def leading_zero_on_single_digits(number) + number > 9 ? number : "0#{number}" + end + end + + class InstanceTag #:nodoc: + include DateHelper + + def to_date_select_tag(options = {}) + date_or_time_select(options.merge(:discard_hour => true)) + end + + def to_time_select_tag(options = {}) + date_or_time_select options.merge(:discard_year => true, :discard_month => true) + end + + def to_datetime_select_tag(options = {}) + date_or_time_select options + end + + private + def date_or_time_select(options) + defaults = { :discard_type => true } + options = defaults.merge(options) + datetime = value(object) + datetime ||= default_time_from_options(options[:default]) unless options[:include_blank] + + position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 } + + order = (options[:order] ||= [:year, :month, :day]) + + # Discard explicit and implicit by not being included in the :order + discard = {} + discard[:year] = true if options[:discard_year] or !order.include?(:year) + discard[:month] = true if options[:discard_month] or !order.include?(:month) + discard[:day] = true if options[:discard_day] or discard[:month] or !order.include?(:day) + discard[:hour] = true if options[:discard_hour] + discard[:minute] = true if options[:discard_minute] or discard[:hour] + discard[:second] = true unless options[:include_seconds] && !discard[:minute] + + # If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are valid + # (otherwise it could be 31 and february wouldn't be a valid date) + if datetime && discard[:day] && !discard[:month] + datetime = datetime.change(:day => 1) + end + + # Maintain valid dates by including hidden fields for discarded elements + [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) } + + # Ensure proper ordering of :hour, :minute and :second + [:hour, :minute, :second].each { |o| order.delete(o); order.push(o) } + + date_or_time_select = '' + order.reverse.each do |param| + # Send hidden fields for discarded elements once output has started + # This ensures AR can reconstruct valid dates using ParseDate + next if discard[param] && date_or_time_select.empty? + + date_or_time_select.insert(0, self.send("select_#{param}", datetime, options_with_prefix(position[param], options.merge(:use_hidden => discard[param])))) + date_or_time_select.insert(0, + case param + when :hour then (discard[:year] && discard[:day] ? "" : " — ") + when :minute then " : " + when :second then options[:include_seconds] ? " : " : "" + else "" + end) + + end + + date_or_time_select + end + + def options_with_prefix(position, options) + prefix = "#{@object_name}" + if options[:index] + prefix << "[#{options[:index]}]" + elsif @auto_index + prefix << "[#{@auto_index}]" + end + options.merge(:prefix => "#{prefix}[#{@method_name}(#{position}i)]") + end + + def default_time_from_options(default) + case default + when nil + Time.now + when Date, Time + default + else + # Rename :minute and :second to :min and :sec + default[:min] ||= default[:minute] + default[:sec] ||= default[:second] + + [:year, :month, :day, :hour, :min, :sec].each do |key| + default[key] ||= Time.now.send(key) + end + + Time.mktime(default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec]) + end + end + end + + class FormBuilder + def date_select(method, options = {}) + @template.date_select(@object_name, method, options.merge(:object => @object)) + end + + def time_select(method, options = {}) + @template.time_select(@object_name, method, options.merge(:object => @object)) + end + + def datetime_select(method, options = {}) + @template.datetime_select(@object_name, method, options.merge(:object => @object)) + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb new file mode 100644 index 000000000..20de7e465 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/debug_helper.rb @@ -0,0 +1,31 @@ +module ActionView + module Helpers + # Provides a set of methods for making it easier to debug Rails objects. + module DebugHelper + # Returns a <pre>-tag that has +object+ dumped by YAML. This creates a very + # readable way to inspect an object. + # + # ==== Example + # my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} + # debug(my_hash) + # + # => <pre class='debug_dump'>--- + # first: 1 + # second: two + # third: + # - 1 + # - 2 + # - 3 + # </pre> + def debug(object) + begin + Marshal::dump(object) + "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>" + rescue Exception => e # errors from Marshal or YAML + # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback + "<code class='debug_dump'>#{h(object.inspect)}</code>" + end + end + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb new file mode 100644 index 000000000..2bfb89663 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_helper.rb @@ -0,0 +1,689 @@ +require 'cgi' +require 'action_view/helpers/date_helper' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + # Form helpers are designed to make working with models much easier compared to using just standard HTML + # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML + # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form + # is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller. + # + # There are two types of form helpers: those that specifically work with model attributes and those that don't. + # This helper deals with those that work with model attributes; to see an example of form helpers that don't work + # with model attributes, check the ActionView::Helpers::FormTagHelper documentation. + # + # The core method of this helper, form_for, gives you the ability to create a form for a model instance; + # for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it: + # + # # Note: a @person variable will have been created in the controller. + # # For example: @person = Person.new + # <% form_for :person, @person, :url => { :action => "create" } do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= submit_tag 'Create' %> + # <% end %> + # + # The HTML generated for this would be: + # + # <form action="/persons/create" method="post"> + # <input id="person_first_name" name="person[first_name]" size="30" type="text" /> + # <input id="person_last_name" name="person[last_name]" size="30" type="text" /> + # <input name="commit" type="submit" value="Create" /> + # </form> + # + # The <tt>params</tt> object created when this form is submitted would look like: + # + # {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}} + # + # The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller. + # If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects + # attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it). + # + # If the object name contains square brackets the id for the object will be inserted. For example: + # + # <%= text_field "person[]", "name" %> + # + # ...will generate the following ERb. + # + # <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" /> + # + # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial + # used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example: + # + # <%= text_field "person", "name", "index" => 1 %> + # + # ...becomes... + # + # <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" /> + # + # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + module FormHelper + # Creates a form and a scope around a specific model object that is used as a base for questioning about + # values for the fields. + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= f.text_area :biography %> + # Admin? : <%= f.check_box :admin %> + # <% end %> + # + # Worth noting is that the form_for tag is called in a ERb evaluation block, not an ERb output block. So that's <tt><% %></tt>, + # not <tt><%= %></tt>. Also worth noting is that form_for yields a <tt>form_builder</tt> object, in this example as <tt>f</tt>, which emulates + # the API for the stand-alone FormHelper methods, but without the object name. So instead of <tt>text_field :person, :name</tt>, + # you get away with <tt>f.text_field :name</tt>. + # + # Even further, the form_for method allows you to more easily escape the instance variable convention. So while the stand-alone + # approach would require <tt>text_field :person, :name, :object => person</tt> + # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with + # <tt>:person, person</tt> and all subsequent field calls save <tt>:person</tt> and <tt>:object => person</tt>. + # + # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods + # and methods from FormTagHelper. For example: + # + # <% form_for :person, @person, :url => { :action => "update" } do |f| %> + # First name: <%= f.text_field :first_name %> + # Last name : <%= f.text_field :last_name %> + # Biography : <%= text_area :person, :biography %> + # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base, + # like FormOptionHelper#collection_select and DateHelper#datetime_select. + # + # HTML attributes for the form tag can be given as :html => {...}. For example: + # + # <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %> + # ... + # <% end %> + # + # The above form will then have the <tt>id</tt> attribute with the value </tt>person_form</tt>, which you can then + # style with CSS or manipulate with JavaScript. + # + # === Relying on record identification + # + # In addition to manually configuring the form_for call, you can also rely on record identification, which will use + # the conventions and named routes of that approach. Examples: + # + # <% form_for(@post) do |f| %> + # ... + # <% end %> + # + # This will expand to be the same as: + # + # <% form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> + # ... + # <% end %> + # + # And for new records: + # + # <% form_for(Post.new) do |f| %> + # ... + # <% end %> + # + # This will expand to be the same as: + # + # <% form_for :post, @post, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %> + # ... + # <% end %> + # + # You can also overwrite the individual conventions, like this: + # + # <% form_for(@post, :url => super_post_path(@post)) do |f| %> + # ... + # <% end %> + # + # And for namespaced routes, like admin_post_url: + # + # <% form_for([:admin, @post]) do |f| %> + # ... + # <% end %> + # + # === Customized form builders + # + # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, + # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs. + # + # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> + # <%= f.text_field :first_name %> + # <%= f.text_field :last_name %> + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", @person.company.admin? %> + # <% end %> + # + # In many cases you will want to wrap the above in another helper, so you could do something like the following: + # + # def labelled_form_for(name, object, options, &proc) + # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc) + # end + # + # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag. + def form_for(record_or_name_or_array, *args, &proc) + raise ArgumentError, "Missing block" unless block_given? + + options = args.extract_options! + + case record_or_name_or_array + when String, Symbol + object_name = record_or_name_or_array + when Array + object = record_or_name_or_array.last + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!(record_or_name_or_array, options) + args.unshift object + else + object = record_or_name_or_array + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!([object], options) + args.unshift object + end + + concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) + fields_for(object_name, *(args << options), &proc) + concat('</form>', proc.binding) + end + + def apply_form_for_options!(object_or_array, options) #:nodoc: + object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array + + html_options = + if object.respond_to?(:new_record?) && object.new_record? + { :class => dom_class(object, :new), :id => dom_id(object), :method => :post } + else + { :class => dom_class(object, :edit), :id => dom_id(object, :edit), :method => :put } + end + + options[:html] ||= {} + options[:html].reverse_merge!(html_options) + options[:url] ||= polymorphic_path(object_or_array) + end + + # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes + # fields_for suitable for specifying additional model objects in the same form: + # + # ==== Examples + # <% form_for @person, :url => { :action => "update" } do |person_form| %> + # First name: <%= person_form.text_field :first_name %> + # Last name : <%= person_form.text_field :last_name %> + # + # <% fields_for @person.permission do |permission_fields| %> + # Admin? : <%= permission_fields.check_box :admin %> + # <% end %> + # <% end %> + # + # ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person: + # + # <% fields_for :person, @client do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # ...or if you don't have an object, just a name of the parameter + # + # <% fields_for :person do |permission_fields| %> + # Admin?: <%= permission_fields.check_box :admin %> + # <% end %> + # + # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base, + # like FormOptionHelper#collection_select and DateHelper#datetime_select. + def fields_for(record_or_name_or_array, *args, &block) + raise ArgumentError, "Missing block" unless block_given? + options = args.extract_options! + + case record_or_name_or_array + when String, Symbol + object_name = record_or_name_or_array + object = args.first + when Array + object = record_or_name_or_array.last + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!(record_or_name_or_array, options) + else + object = record_or_name_or_array + object_name = ActionController::RecordIdentifier.singular_class_name(object) + end + + builder = options[:builder] || ActionView::Base.default_form_builder + yield builder.new(object_name, object, self, options, block) + end + + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify + # it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged + # onto the HTML as an HTML element attribute as in the example shown. + # + # ==== Examples + # label(:post, :title) + # #=> <label for="post_title">Title</label> + # + # label(:post, :title, "A short title") + # #=> <label for="post_title">A short title</label> + # + # label(:post, :title, "A short title", :class => "title_label") + # #=> <label for="post_title" class="title_label">A short title</label> + # + def label(object_name, method, text = nil, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_label_tag(text, options) + end + + # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # text_field(:post, :title, :size => 20) + # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" /> + # + # text_field(:post, :title, :class => "create_input") + # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> + # + # text_field(:session, :user, :onchange => "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }") + # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange = "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }"/> + # + # text_field(:snippet, :code, :size => 20, :class => 'code_input') + # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> + # + def text_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) + end + + # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # password_field(:login, :pass, :size => 20) + # # => <input type="text" id="login_pass" name="login[pass]" size="20" value="#{@login.pass}" /> + # + # password_field(:account, :secret, :class => "form_input") + # # => <input type="text" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" /> + # + # password_field(:user, :password, :onchange => "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }") + # # => <input type="text" id="user_password" name="user[password]" value="#{@user.password}" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/> + # + # password_field(:account, :pin, :size => 20, :class => 'form_input') + # # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" /> + # + def password_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options) + end + + # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # hidden_field(:signup, :pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # + # hidden_field(:post, :tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # + # hidden_field(:user, :token) + # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + def hidden_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options) + end + + # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example + # shown. + # + # ==== Examples + # file_field(:user, :avatar) + # # => <input type="file" id="user_avatar" name="user[avatar]" /> + # + # file_field(:post, :attached, :accept => 'text/html') + # # => <input type="file" id="post_attached" name="post[attached]" /> + # + # file_field(:attachment, :file, :class => 'file_input') + # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> + # + def file_field(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options) + end + + # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) + # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. + # + # ==== Examples + # text_area(:post, :body, :cols => 20, :rows => 40) + # # => <textarea cols="20" rows="40" id="post_body" name="post[body]"> + # # #{@post.body} + # # </textarea> + # + # text_area(:comment, :text, :size => "20x30") + # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]"> + # # #{@comment.text} + # # </textarea> + # + # text_area(:application, :notes, :cols => 40, :rows => 15, :class => 'app_input') + # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input"> + # # #{@application.notes} + # # </textarea> + # + # text_area(:entry, :body, :size => "20x20", :disabled => 'disabled') + # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled"> + # # #{@entry.body} + # # </textarea> + def text_area(object_name, method, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options) + end + + # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that + # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a + # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ + # is set to 0 which is convenient for boolean values. Since HTTP standards say that unchecked checkboxes don't post anything, + # we add a hidden value with the same name as the checkbox as a work around. + # + # ==== Examples + # # Let's say that @post.validated? is 1: + # check_box("post", "validated") + # # => <input type="checkbox" id="post_validate" name="post[validated]" value="1" checked="checked" /> + # # <input name="post[validated]" type="hidden" value="0" /> + # + # # Let's say that @puppy.gooddog is "no": + # check_box("puppy", "gooddog", {}, "yes", "no") + # # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> + # # <input name="puppy[gooddog]" type="hidden" value="no" /> + # + # check_box("eula", "accepted", {}, "yes", "no", :class => 'eula_check') + # # => <input type="checkbox" id="eula_accepted" name="eula[accepted]" value="no" /> + # # <input name="eula[accepted]" type="hidden" value="no" /> + # + def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) + end + + # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the + # radio button will be checked. Additional options on the input tag can be passed as a + # hash with +options+. + # + # ==== Examples + # # Let's say that @post.category returns "rails": + # radio_button("post", "category", "rails") + # radio_button("post", "category", "java") + # # => <input type="radio" id="post_category" name="post[category]" value="rails" checked="checked" /> + # # <input type="radio" id="post_category" name="post[category]" value="java" /> + # + # radio_button("user", "receive_newsletter", "yes") + # radio_button("user", "receive_newsletter", "no") + # # => <input type="radio" id="user_receive_newsletter" name="user[receive_newsletter]" value="yes" /> + # # <input type="radio" id="user_receive_newsletter" name="user[receive_newsletter]" value="no" checked="checked" /> + def radio_button(object_name, method, tag_value, options = {}) + InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options) + end + end + + class InstanceTag #:nodoc: + include Helpers::TagHelper + + attr_reader :method_name, :object_name + + DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS) + DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) + DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS) + + def initialize(object_name, method_name, template_object, local_binding = nil, object = nil) + @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup + @template_object, @local_binding = template_object, local_binding + @object = object + if @object_name.sub!(/\[\]$/,"") + if object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + @auto_index = object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end + end + + def to_label_tag(text = nil, options = {}) + name_and_id = options.dup + add_default_name_and_id(name_and_id) + options["for"] = name_and_id["id"] + content = (text.blank? ? nil : text.to_s) || method_name.humanize + content_tag("label", content, options) + end + + def to_input_field_tag(field_type, options = {}) + options = options.stringify_keys + options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size") + options = DEFAULT_FIELD_OPTIONS.merge(options) + if field_type == "hidden" + options.delete("size") + end + options["type"] = field_type + options["value"] ||= value_before_type_cast(object) unless field_type == "file" + add_default_name_and_id(options) + tag("input", options) + end + + def to_radio_button_tag(tag_value, options = {}) + options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) + options["type"] = "radio" + options["value"] = tag_value + if options.has_key?("checked") + cv = options.delete "checked" + checked = cv == true || cv == "checked" + else + checked = self.class.radio_button_checked?(value(object), tag_value) + end + options["checked"] = "checked" if checked + pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase + options["id"] ||= defined?(@auto_index) ? + "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" : + "#{@object_name}_#{@method_name}_#{pretty_tag_value}" + add_default_name_and_id(options) + tag("input", options) + end + + def to_text_area_tag(options = {}) + options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) + add_default_name_and_id(options) + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) + end + + content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options) + end + + def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") + options = options.stringify_keys + options["type"] = "checkbox" + options["value"] = checked_value + if options.has_key?("checked") + cv = options.delete "checked" + checked = cv == true || cv == "checked" + else + checked = self.class.check_box_checked?(value(object), checked_value) + end + options["checked"] = "checked" if checked + add_default_name_and_id(options) + tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value) + end + + def to_date_tag() + defaults = DEFAULT_DATE_OPTIONS.dup + date = value(object) || Date.today + options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } + html_day_select(date, options.call(3)) + + html_month_select(date, options.call(2)) + + html_year_select(date, options.call(1)) + end + + def to_boolean_select_tag(options = {}) + options = options.stringify_keys + add_default_name_and_id(options) + value = value(object) + tag_text = "<select" + tag_text << tag_options(options) + tag_text << "><option value=\"false\"" + tag_text << " selected" if value == false + tag_text << ">False</option><option value=\"true\"" + tag_text << " selected" if value + tag_text << ">True</option></select>" + end + + def to_content_tag(tag_name, options = {}) + content_tag(tag_name, value(object), options) + end + + def object + @object || (@template_object.instance_variable_get("@#{@object_name}") rescue nil) + end + + def value(object) + self.class.value(object, @method_name) + end + + def value_before_type_cast(object) + self.class.value_before_type_cast(object, @method_name) + end + + class << self + def value(object, method_name) + object.send method_name unless object.nil? + end + + def value_before_type_cast(object, method_name) + unless object.nil? + object.respond_to?(method_name + "_before_type_cast") ? + object.send(method_name + "_before_type_cast") : + object.send(method_name) + end + end + + def check_box_checked?(value, checked_value) + case value + when TrueClass, FalseClass + value + when NilClass + false + when Integer + value != 0 + when String + value == checked_value + else + value.to_i != 0 + end + end + + def radio_button_checked?(value, checked_value) + value.to_s == checked_value.to_s + end + end + + private + def add_default_name_and_id(options) + if options.has_key?("index") + options["name"] ||= tag_name_with_index(options["index"]) + options["id"] ||= tag_id_with_index(options["index"]) + options.delete("index") + elsif defined?(@auto_index) + options["name"] ||= tag_name_with_index(@auto_index) + options["id"] ||= tag_id_with_index(@auto_index) + else + options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '') + options["id"] ||= tag_id + end + end + + def tag_name + "#{@object_name}[#{@method_name}]" + end + + def tag_name_with_index(index) + "#{@object_name}[#{index}][#{@method_name}]" + end + + def tag_id + "#{sanitized_object_name}_#{@method_name}" + end + + def tag_id_with_index(index) + "#{sanitized_object_name}_#{index}_#{@method_name}" + end + + def sanitized_object_name + @object_name.gsub(/[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") + end + end + + class FormBuilder #:nodoc: + # The methods which wrap a form helper call. + class_inheritable_accessor :field_helpers + self.field_helpers = (FormHelper.instance_methods - ['form_for']) + + attr_accessor :object_name, :object, :options + + def initialize(object_name, object, template, options, proc) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + end + + (field_helpers - %w(label check_box radio_button fields_for)).each do |selector| + src = <<-end_src + def #{selector}(method, options = {}) + @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) + end + end_src + class_eval src, __FILE__, __LINE__ + end + + def fields_for(record_or_name_or_array, *args, &block) + case record_or_name_or_array + when String, Symbol + name = "#{object_name}[#{record_or_name_or_array}]" + when Array + object = record_or_name_or_array.last + name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + args.unshift(object) + else + object = record_or_name_or_array + name = "#{object_name}[#{ActionController::RecordIdentifier.singular_class_name(object)}]" + args.unshift(object) + end + + @template.fields_for(name, *args, &block) + end + + def label(method, text = nil, options = {}) + @template.label(@object_name, method, text, options.merge(:object => @object)) + end + + def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") + @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value) + end + + def radio_button(method, tag_value, options = {}) + @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object)) + end + + def error_message_on(method, prepend_text = "", append_text = "", css_class = "formError") + @template.error_message_on(@object, method, prepend_text, append_text, css_class) + end + + def error_messages(options = {}) + @template.error_messages_for(@object_name, options.merge(:object => @object)) + end + + def submit(value = "Save changes", options = {}) + @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit")) + end + end + end + + class Base + cattr_accessor :default_form_builder + self.default_form_builder = ::ActionView::Helpers::FormBuilder + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb new file mode 100644 index 000000000..0f1d2b02b --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -0,0 +1,425 @@ +require 'cgi' +require 'erb' +require 'action_view/helpers/form_helper' + +module ActionView + module Helpers + # Provides a number of methods for turning different kinds of containers into a set of option tags. + # == Options + # The <tt>collection_select</tt>, <tt>country_select</tt>, <tt>select</tt>, + # and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, + # a hash. + # + # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. + # + # For example, + # + # select("post", "category", Post::CATEGORIES, {:include_blank => true}) + # + # could become: + # + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # </select> + # + # Another common case is a select tag for an <tt>belongs_to</tt>-associated object. + # + # Example with @post.person_id => 2: + # + # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'}) + # + # could become: + # + # <select name="post[person_id]"> + # <option value="">None</option> + # <option value="1">David</option> + # <option value="2" selected="selected">Sam</option> + # <option value="3">Tobias</option> + # </select> + # + # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. + # + # Example: + # + # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'}) + # + # could become: + # + # <select name="post[person_id]"> + # <option value="">Select Person</option> + # <option value="1">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> + module FormOptionsHelper + include ERB::Util + + # Create a select tag and a series of contained option tags for the provided object and method. + # The option currently held by the object will be selected, provided that the object is available. + # See options_for_select for the required format of the choices parameter. + # + # Example with @post.person_id => 1: + # select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true }) + # + # could become: + # + # <select name="post[person_id]"> + # <option value=""></option> + # <option value="1" selected="selected">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> + # + # This can be used to provide a default set of options in the standard way: before rendering the create form, a + # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved + # to the database. Instead, a second model object is created when the create request is received. + # This allows the user to submit a form page more than once with the expected results of creating multiple records. + # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. + # + # By default, post.person_id is the selected option. Specify :selected => value to use a different selection + # or :selected => nil to leave all options unselected. + def select(object, method, choices, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options) + end + + # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will + # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt> + # or <tt>:include_blank</tt> in the +options+ hash. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member + # of +collection+. The return values are used as the +value+ attribute and contents of each + # <tt><option></tt> tag, respectively. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # belongs_to :author + # end + # class Author < ActiveRecord::Base + # has_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated +Author+ for an instance of +Post+, <tt>@post</tt>): + # collection_select(:post, :author_id, Author.find(:all), :id, :name_with_initial, {:prompt => true}) + # + # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: + # <select name="post[author_id]"> + # <option value="">Please select</option> + # <option value="1" selected="selected">D. Heinemeier Hansson</option> + # <option value="2">D. Thomas</option> + # <option value="3">M. Clark</option> + # </select> + def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) + end + + # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. + def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) + end + + # Return select and option tags for the given object and method, using + # #time_zone_options_for_select to generate the list of option tags. + # + # In addition to the <tt>:include_blank</tt> option documented above, + # this method also supports a <tt>:model</tt> option, which defaults + # to TimeZone. This may be used by users to specify a different time + # zone model object. (See #time_zone_options_for_select for more + # information.) + def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) + InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) + end + + # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container + # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and + # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+ + # may also be an array of values to be selected when using a multiple select. + # + # Examples (call, result): + # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) + # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option> + # + # options_for_select([ "VISA", "MasterCard" ], "MasterCard") + # <option>VISA</option>\n<option selected="selected">MasterCard</option> + # + # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") + # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option> + # + # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) + # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option> + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_for_select(container, selected = nil) + container = container.to_a if Hash === container + + options_for_select = container.inject([]) do |options, element| + text, value = option_text_and_value(element) + selected_attribute = ' selected="selected"' if option_value_selected?(value, selected) + options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}>#{html_escape(text.to_s)}</option>) + end + + options_for_select.join("\n") + end + + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the + # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. + # If +selected+ is specified, the element returning a match on +value_method+ will get the selected option tag. + # + # Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate an input tag: + # options_from_collection_for_select(@project.people, "id", "name") + # <option value="#{person.id}">#{person.name}</option> + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def options_from_collection_for_select(collection, value_method, text_method, selected = nil) + options = collection.map do |element| + [element.send(text_method), element.send(value_method)] + end + options_for_select(options, selected) + end + + # Returns a string of <tt><option></tt> tags, like <tt>#options_from_collection_for_select</tt>, but + # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments. + # + # Parameters: + # +collection+:: An array of objects representing the <tt><optgroup></tt> tags + # +group_method+:: The name of a method which, when called on a member of +collection+, returns an + # array of child objects representing the <tt><option></tt> tags + # +group_label_method+:: The name of a method which, when called on a member of +collection+, returns a + # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag + # +option_key_method+:: The name of a method which, when called on a child object of a member of + # +collection+, returns a value to be used as the +value+ attribute for its + # <tt><option></tt> tag + # +option_value_method+:: The name of a method which, when called on a child object of a member of + # +collection+, returns a value to be used as the contents of its + # <tt><option></tt> tag + # +selected_key+:: A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Corresponds to the return value + # of one of the calls to +option_key_method+. If +nil+, no selection is made. + # + # Example object structure for use with this method: + # class Continent < ActiveRecord::Base + # has_many :countries + # # attribs: id, name + # end + # class Country < ActiveRecord::Base + # belongs_to :continent + # # attribs: id, name, continent_id + # end + # + # Sample usage: + # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3) + # + # Possible output: + # <optgroup label="Africa"> + # <option value="1">Egypt</option> + # <option value="4">Rwanda</option> + # ... + # </optgroup> + # <optgroup label="Asia"> + # <option value="3" selected="selected">China</option> + # <option value="12">India</option> + # <option value="5">Japan</option> + # ... + # </optgroup> + # + # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to + # wrap the output in an appropriate <tt><select></tt> tag. + def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) + collection.inject("") do |options_for_select, group| + group_label_string = eval("group.#{group_label_method}") + options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">" + options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + options_for_select += '</optgroup>' + end + end + + # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to + # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so + # that they will be listed above the rest of the (long) list. + # + # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. + def country_options_for_select(selected = nil, priority_countries = nil) + country_options = "" + + if priority_countries + country_options += options_for_select(priority_countries, selected) + country_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + end + + return country_options + options_for_select(COUNTRIES, selected) + end + + # Returns a string of option tags for pretty much any time zone in the + # world. Supply a TimeZone name as +selected+ to have it marked as the + # selected option tag. You can also supply an array of TimeZone objects + # as +priority_zones+, so that they will be listed above the rest of the + # (long) list. (You can use TimeZone.us_zones as a convenience for + # obtaining a list of the US time zones.) + # + # The +selected+ parameter must be either +nil+, or a string that names + # a TimeZone. + # + # By default, +model+ is the TimeZone constant (which can be obtained + # in ActiveRecord as a value object). The only requirement is that the + # +model+ parameter be an object that responds to #all, and returns + # an array of objects that represent time zones. + # + # NOTE: Only the option tags are returned, you have to wrap this call in + # a regular HTML select tag. + def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) + zone_options = "" + + zones = model.all + convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } + + if priority_zones + zone_options += options_for_select(convert_zones[priority_zones], selected) + zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + + zones = zones.reject { |z| priority_zones.include?( z ) } + end + + zone_options += options_for_select(convert_zones[zones], selected) + zone_options + end + + private + def option_text_and_value(option) + # Options are [text, value] pairs or strings used for both. + if !option.is_a?(String) and option.respond_to?(:first) and option.respond_to?(:last) + [option.first, option.last] + else + [option, option] + end + end + + def option_value_selected?(value, selected) + if selected.respond_to?(:include?) && !selected.is_a?(String) + selected.include? value + else + value == selected + end + end + + # All the countries included in the country_options output. + COUNTRIES = ["Afghanistan", "Aland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", + "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", + "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", + "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", + "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", + "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", + "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", + "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", + "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", + "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", + "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", + "Guinea-Bissau", "Guyana", "Haiti", "Heard and McDonald Islands", "Holy See (Vatican City State)", + "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", + "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", + "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", + "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", + "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic Of", + "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", + "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", + "Monaco", "Mongolia", "Montenegro", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", + "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", + "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", + "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", + "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", + "Rwanda", "Saint Barthelemy", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", + "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", + "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", + "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", + "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", + "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", + "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", + "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", + "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", + "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", + "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", + "Yemen", "Zambia", "Zimbabwe"] unless const_defined?("COUNTRIES") + end + + class InstanceTag #:nodoc: + include FormOptionsHelper + + def to_select_tag(choices, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + value = value(object) + selected_value = options.has_key?(:selected) ? options[:selected] : value + content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options) + end + + def to_collection_select_tag(collection, value_method, text_method, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + value = value(object) + content_tag( + "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options + ) + end + + def to_country_select_tag(priority_countries, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + value = value(object) + content_tag("select", + add_options( + country_options_for_select(value, priority_countries), + options, value + ), html_options + ) + end + + def to_time_zone_select_tag(priority_zones, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + value = value(object) + content_tag("select", + add_options( + time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone), + options, value + ), html_options + ) + end + + private + def add_options(option_tags, options, value = nil) + if options[:include_blank] + option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags + end + if value.blank? && options[:prompt] + ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags + else + option_tags + end + end + end + + class FormBuilder + def select(method, choices, options = {}, html_options = {}) + @template.select(@object_name, method, choices, options.merge(:object => @object), html_options) + end + + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options) + end + + def country_select(method, priority_countries = nil, options = {}, html_options = {}) + @template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options) + end + + def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) + @template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options) + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb new file mode 100644 index 000000000..077465086 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -0,0 +1,432 @@ +require 'cgi' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + # Provides a number of methods for creating form tags that doesn't rely on an ActiveRecord object assigned to the template like + # FormHelper does. Instead, you provide the names and values manually. + # + # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying + # <tt>:disabled => true</tt> will give <tt>disabled="disabled"</tt>. + module FormTagHelper + # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like + # ActionController::Base#url_for. The method for the form defaults to POST. + # + # ==== Options + # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data". + # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post". + # If "put", "delete", or another verb is used, a hidden input with name _method + # is added to simulate the verb over post. + # * A list of parameters to feed to the URL the form will be posted to. + # + # ==== Examples + # form_tag('/posts') + # # => <form action="/posts" method="post"> + # + # form_tag('/posts/1', :method => :put) + # # => <form action="/posts/1" method="put"> + # + # form_tag('/upload', :multipart => true) + # # => <form action="/upload" method="post" enctype="multipart/form-data"> + # + # <% form_tag '/posts' do -%> + # <div><%= submit_tag 'Save' %></div> + # <% end -%> + # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> + def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block) + html_options = html_options_for_form(url_for_options, options, *parameters_for_url) + if block_given? + form_tag_in_block(html_options, &block) + else + form_tag_html(html_options) + end + end + + # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple + # choice selection box. + # + # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or + # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box. + # + # ==== Options + # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # select_tag "people", "<option>David</option>" + # # => <select id="people" name="people"><option>David</option></select> + # + # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>" + # # => <select id="count" name="count"><option>1</option><option>2</option> + # # <option>3</option><option>4</option></select> + # + # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>", :multiple => true + # # => <select id="colors" multiple="multiple" name="colors"><option>Red</option> + # # <option>Green</option><option>Blue</option></select> + # + # select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>" + # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> + # # <option>Out</option></select> + # + # select_tag "access", "<option>Read</option><option>Write</option>", :multiple => true, :class => 'form_input' + # # => <select class="form_input" id="access" multiple="multiple" name="access"><option>Read</option> + # # <option>Write</option></select> + # + # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>", :disabled => true + # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> + # # <option>Paris</option><option>Rome</option></select> + def select_tag(name, option_tags = nil, options = {}) + content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a standard text field; use these text fields to input smaller chunks of text like a username + # or a search query. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:size</tt> - The number of visible characters that will fit in the input. + # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # text_field_tag 'name' + # # => <input id="name" name="name" type="text" /> + # + # text_field_tag 'query', 'Enter your search query here' + # # => <input id="query" name="query" type="text" value="Enter your search query here" /> + # + # text_field_tag 'request', nil, :class => 'special_input' + # # => <input class="special_input" id="request" name="request" type="text" /> + # + # text_field_tag 'address', '', :size => 75 + # # => <input id="address" name="address" size="75" type="text" value="" /> + # + # text_field_tag 'zip', nil, :maxlength => 5 + # # => <input id="zip" maxlength="5" name="zip" type="text" /> + # + # text_field_tag 'payment_amount', '$0.00', :disabled => true + # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" /> + # + # text_field_tag 'ip', '0.0.0.0', :maxlength => 15, :size => 20, :class => "ip-input" + # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" /> + def text_field_tag(name, value = nil, options = {}) + tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + end + + # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or + # data that should be hidden from the user. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # + # ==== Examples + # hidden_field_tag 'tags_list' + # # => <input id="tags_list" name="tags_list" type="hidden" /> + # + # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@' + # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" /> + # + # hidden_field_tag 'collected_input', '', :onchange => "alert('Input collected!')" + # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')" + # # type="hidden" value="" /> + def hidden_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) + end + + # Creates a file upload field. If you are using file uploads then you will also need + # to set the multipart option for the form tag: + # + # <%= form_tag { :action => "post" }, { :multipart => true } %> + # <label for="file">File to Upload</label> <%= file_field_tag "file" %> + # <%= submit_tag %> + # <%= end_form_tag %> + # + # The specified URL will then be passed a File object containing the selected file, or if the field + # was left blank, a StringIO object. + # + # ==== Options + # * Creates standard HTML attributes for the tag. + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # + # ==== Examples + # file_field_tag 'attachment' + # # => <input id="attachment" name="attachment" type="file" /> + # + # file_field_tag 'avatar', :class => 'profile-input' + # # => <input class="profile-input" id="avatar" name="avatar" type="file" /> + # + # file_field_tag 'picture', :disabled => true + # # => <input disabled="disabled" id="picture" name="picture" type="file" /> + # + # file_field_tag 'resume', :value => '~/resume.doc' + # # => <input id="resume" name="resume" type="file" value="~/resume.doc" /> + # + # file_field_tag 'user_pic', :accept => 'image/png,image/gif,image/jpeg' + # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" /> + # + # file_field_tag 'file', :accept => 'text/html', :class => 'upload', :value => 'index.html' + # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" /> + def file_field_tag(name, options = {}) + text_field_tag(name, nil, options.update("type" => "file")) + end + + # Creates a password field, a masked text field that will hide the users input behind a mask character. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:size</tt> - The number of visible characters that will fit in the input. + # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # password_field_tag 'pass' + # # => <input id="pass" name="pass" type="password" /> + # + # password_field_tag 'secret', 'Your secret here' + # # => <input id="secret" name="secret" type="password" value="Your secret here" /> + # + # password_field_tag 'masked', nil, :class => 'masked_input_field' + # # => <input class="masked_input_field" id="masked" name="masked" type="password" /> + # + # password_field_tag 'token', '', :size => 15 + # # => <input id="token" name="token" size="15" type="password" value="" /> + # + # password_field_tag 'key', nil, :maxlength => 16 + # # => <input id="key" maxlength="16" name="key" type="password" /> + # + # password_field_tag 'confirm_pass', nil, :disabled => true + # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" /> + # + # password_field_tag 'pin', '1234', :maxlength => 4, :size => 6, :class => "pin-input" + # # => <input class="pin-input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" /> + def password_field_tag(name = "password", value = nil, options = {}) + text_field_tag(name, value, options.update("type" => "password")) + end + + # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. + # + # ==== Options + # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10"). + # * <tt>:rows</tt> - Specify the number of rows in the textarea + # * <tt>:cols</tt> - Specify the number of columns in the textarea + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML attributes for the tag. + # + # ==== Examples + # text_area_tag 'post' + # # => <textarea id="post" name="post"></textarea> + # + # text_area_tag 'bio', @user.bio + # # => <textarea id="bio" name="bio">This is my biography.</textarea> + # + # text_area_tag 'body', nil, :rows => 10, :cols => 25 + # # => <textarea cols="25" id="body" name="body" rows="10"></textarea> + # + # text_area_tag 'body', nil, :size => "25x10" + # # => <textarea name="body" id="body" cols="25" rows="10"></textarea> + # + # text_area_tag 'description', "Description goes here.", :disabled => true + # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea> + # + # text_area_tag 'comment', nil, :class => 'comment_input' + # # => <textarea class="comment_input" id="comment" name="comment"></textarea> + def text_area_tag(name, content = nil, options = {}) + options.stringify_keys! + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) + end + + content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys) + end + + # Creates a check box form input tag. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # check_box_tag 'accept' + # # => <input id="accept" name="accept" type="checkbox" value="1" /> + # + # check_box_tag 'rock', 'rock music' + # # => <input id="rock" name="rock" type="checkbox" value="rock music" /> + # + # check_box_tag 'receive_email', 'yes', true + # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" /> + # + # check_box_tag 'tos', 'yes', false, :class => 'accept_tos' + # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" /> + # + # check_box_tag 'eula', 'accepted', false, :disabled => true + # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" /> + def check_box_tag(name, value = "1", checked = false, options = {}) + html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a radio button; use groups of radio buttons named the same to allow users to + # select from a group of options. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # radio_button_tag 'gender', 'male' + # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # + # radio_button_tag 'receive_updates', 'no', true + # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> + # + # radio_button_tag 'time_slot', "3:00 p.m.", false, :disabled => true + # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." /> + # + # radio_button_tag 'color', "green", true, :class => "color_input" + # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" /> + def radio_button_tag(name, value, checked = false, options = {}) + pretty_tag_value = value.to_s.gsub(/\s/, "_").gsub(/(?!-)\W/, "").downcase + pretty_name = name.to_s.gsub(/\[/, "_").gsub(/\]/, "") + html_options = { "type" => "radio", "name" => name, "id" => "#{pretty_name}_#{pretty_tag_value}", "value" => value }.update(options.stringify_keys) + html_options["checked"] = "checked" if checked + tag :input, html_options + end + + # Creates a submit button with the text <tt>value</tt> as the caption. + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a disabled version + # of the submit button when the form is submitted. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # submit_tag + # # => <input name="commit" type="submit" value="Save changes" /> + # + # submit_tag "Edit this article" + # # => <input name="commit" type="submit" value="Edit this article" /> + # + # submit_tag "Save edits", :disabled => true + # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" /> + # + # submit_tag "Complete sale", :disable_with => "Please wait..." + # # => <input name="commit" onclick="this.disabled=true;this.value='Please wait...';this.form.submit();" + # # type="submit" value="Complete sale" /> + # + # submit_tag nil, :class => "form_submit" + # # => <input class="form_submit" name="commit" type="submit" /> + # + # submit_tag "Edit", :disable_with => "Editing...", :class => 'edit-button' + # # => <input class="edit-button" disable_with="Editing..." name="commit" type="submit" value="Edit" /> + def submit_tag(value = "Save changes", options = {}) + options.stringify_keys! + + if disable_with = options.delete("disable_with") + options["onclick"] = [ + "this.setAttribute('originalValue', this.value)", + "this.disabled=true", + "this.value='#{disable_with}'", + "#{options["onclick"]}", + "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit())", + "if (result == false) { this.value = this.getAttribute('originalValue'); this.disabled = false }", + "return result", + ].join(";") + end + + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) + end + + # Displays an image which when clicked will submit the form. + # + # <tt>source</tt> is passed to AssetTagHelper#image_path + # + # ==== Options + # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. + # * Any other key creates standard HTML options for the tag. + # + # ==== Examples + # image_submit_tag("login.png") + # # => <input src="/images/login.png" type="image" /> + # + # image_submit_tag("purchase.png"), :disabled => true + # # => <input disabled="disabled" src="/images/purchase.png" type="image" /> + # + # image_submit_tag("search.png"), :class => 'search-button' + # # => <input class="search-button" src="/images/search.png" type="image" /> + # + # image_submit_tag("agree.png"), :disabled => true, :class => "agree-disagree-button" + # # => <input class="agree-disagree-button" disabled="disabled" src="/images/agree.png" type="image" /> + def image_submit_tag(source, options = {}) + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) + end + + # Creates a field set for grouping HTML form elements. + # + # <tt>legend</tt> will become the fieldset's title (optional as per W3C). + # + # === Examples + # <% field_set_tag do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset> + # + # <% field_set_tag 'Your details' do %> + # <p><%= text_field_tag 'name' %></p> + # <% end %> + # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset> + def field_set_tag(legend = nil, &block) + content = capture(&block) + concat(tag(:fieldset, {}, true), block.binding) + concat(content_tag(:legend, legend), block.binding) unless legend.blank? + concat(content, block.binding) + concat("</fieldset>", block.binding) + end + + private + def html_options_for_form(url_for_options, options, *parameters_for_url) + returning options.stringify_keys do |html_options| + html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") + html_options["action"] = url_for(url_for_options, *parameters_for_url) + end + end + + def extra_tags_for_form(html_options) + case method = html_options.delete("method").to_s + when /^get$/i # must be case-insentive, but can't use downcase as might be nil + html_options["method"] = "get" + '' + when /^post$/i, "", nil + html_options["method"] = "post" + protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0') : '' + else + html_options["method"] = "post" + content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0') + end + end + + def form_tag_html(html_options) + extra_tags = extra_tags_for_form(html_options) + tag(:form, html_options, true) + extra_tags + end + + def form_tag_in_block(html_options, &block) + content = capture(&block) + concat(form_tag_html(html_options), block.binding) + concat(content, block.binding) + concat("</form>", block.binding) + end + + def token_tag + unless protect_against_forgery? + '' + else + tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb new file mode 100644 index 000000000..1ea3cbd74 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -0,0 +1,217 @@ +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/prototype_helper' + +module ActionView + module Helpers + # Provides functionality for working with JavaScript in your views. + # + # == Ajax, controls and visual effects + # + # * For information on using Ajax, see + # ActionView::Helpers::PrototypeHelper. + # * For information on using controls and visual effects, see + # ActionView::Helpers::ScriptaculousHelper. + # + # == Including the JavaScript libraries into your pages + # + # Rails includes the Prototype JavaScript framework and the Scriptaculous + # JavaScript controls and visual effects library. If you wish to use + # these libraries and their helpers (ActionView::Helpers::PrototypeHelper + # and ActionView::Helpers::ScriptaculousHelper), you must do one of the + # following: + # + # * Use <tt><%= javascript_include_tag :defaults %></tt> in the HEAD + # section of your page (recommended): This function will return + # references to the JavaScript files created by the +rails+ command in + # your <tt>public/javascripts</tt> directory. Using it is recommended as + # the browser can then cache the libraries instead of fetching all the + # functions anew on every request. + # * Use <tt><%= javascript_include_tag 'prototype' %></tt>: As above, but + # will only include the Prototype core library, which means you are able + # to use all basic AJAX functionality. For the Scriptaculous-based + # JavaScript helpers, like visual effects, autocompletion, drag and drop + # and so on, you should use the method described above. + # * Use <tt><%= define_javascript_functions %></tt>: this will copy all the + # JavaScript support functions within a single script block. Not + # recommended. + # + # For documentation on +javascript_include_tag+ see + # ActionView::Helpers::AssetTagHelper. + module JavaScriptHelper + unless const_defined? :JAVASCRIPT_PATH + JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') + end + + include PrototypeHelper + + # Returns a link that will trigger a JavaScript +function+ using the + # onclick handler and return false after the fact. + # + # The +function+ argument can be omitted in favor of an +update_page+ + # block, which evaluates to a string when the template is rendered + # (instead of making an Ajax request first). + # + # Examples: + # link_to_function "Greeting", "alert('Hello world!')" + # Produces: + # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> + # + # link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()") + # Produces: + # <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#"> + # <img src="/images/delete.png?" alt="Delete"/> + # </a> + # + # link_to_function("Show me more", nil, :id => "more_link") do |page| + # page[:details].visual_effect :toggle_blind + # page[:more_link].replace_html "Show me less" + # end + # Produces: + # <a href="#" id="more_link" onclick="try { + # $("details").visualEffect("toggle_blind"); + # $("more_link").update("Show me less"); + # } + # catch (e) { + # alert('RJS error:\n\n' + e.toString()); + # alert('$(\"details\").visualEffect(\"toggle_blind\"); + # \n$(\"more_link\").update(\"Show me less\");'); + # throw e + # }; + # return false;">Show me more</a> + # + def link_to_function(name, *args, &block) + html_options = args.extract_options! + function = args[0] || '' + + html_options.symbolize_keys! + function = update_page(&block) if block_given? + content_tag( + "a", name, + html_options.merge({ + :href => html_options[:href] || "#", + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;" + }) + ) + end + + # Returns a button that'll trigger a JavaScript +function+ using the + # onclick handler. + # + # The +function+ argument can be omitted in favor of an +update_page+ + # block, which evaluates to a string when the template is rendered + # (instead of making an Ajax request first). + # + # Examples: + # button_to_function "Greeting", "alert('Hello world!')" + # button_to_function "Delete", "if (confirm('Really?')) do_delete()" + # button_to_function "Details" do |page| + # page[:details].visual_effect :toggle_slide + # end + # button_to_function "Details", :class => "details_button" do |page| + # page[:details].visual_effect :toggle_slide + # end + def button_to_function(name, *args, &block) + html_options = args.extract_options! + function = args[0] || '' + + html_options.symbolize_keys! + function = update_page(&block) if block_given? + tag(:input, html_options.merge({ + :type => "button", :value => name, + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + # Includes the Action Pack JavaScript libraries inside a single <script> + # tag. The function first includes prototype.js and then its core extensions, + # (determined by filenames starting with "prototype"). + # Afterwards, any additional scripts will be included in undefined order. + # + # Note: The recommended approach is to copy the contents of + # lib/action_view/helpers/javascripts/ into your application's + # public/javascripts/ directory, and use +javascript_include_tag+ to + # create remote <script> links. + def define_javascript_functions + javascript = "<script type=\"#{Mime::JS}\">" + + # load prototype.js and its extensions first + prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse + prototype_libs.each do |filename| + javascript << "\n" << IO.read(filename) + end + + # load other libraries + (Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename| + javascript << "\n" << IO.read(filename) + end + javascript << '</script>' + end + + # Escape carrier returns and single and double quotes for JavaScript segments. + def escape_javascript(javascript) + (javascript || '').gsub('\\','\0\0').gsub('</','<\/').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } + end + + # Returns a JavaScript tag with the +content+ inside. Example: + # javascript_tag "alert('All is good')" + # + # Returns: + # <script type="text/javascript"> + # //<![CDATA[ + # alert('All is good') + # //]]> + # </script> + # + # +html_options+ may be a hash of attributes for the <script> tag. Example: + # javascript_tag "alert('All is good')", :defer => 'defer' + # # => <script defer="defer" type="text/javascript">alert('All is good')</script> + # + # Instead of passing the content as an argument, you can also use a block + # in which case, you pass your +html_options+ as the first parameter. + # <% javascript_tag :defer => 'defer' do -%> + # alert('All is good') + # <% end -%> + def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) + if block_given? + html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + content = capture(&block) + else + content = content_or_options_with_block + end + + javascript_tag = content_tag("script", javascript_cdata_section(content), html_options.merge(:type => Mime::JS)) + + if block_given? && block_is_within_action_view?(block) + concat(javascript_tag, block.binding) + else + javascript_tag + end + end + + def javascript_cdata_section(content) #:nodoc: + "\n//#{cdata_section("\n#{content}\n//")}\n" + end + + protected + def options_for_javascript(options) + '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}' + end + + def array_or_string_for_javascript(option) + js_option = if option.kind_of?(Array) + "['#{option.join('\',\'')}']" + elsif !option.nil? + "'#{option}'" + end + js_option + end + + private + def block_is_within_action_view?(block) + eval("defined? _erbout", block.binding) + end + end + + JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js new file mode 100644 index 000000000..fbc4418b8 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/controls.js @@ -0,0 +1,963 @@ +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = { } +Autocompleter.Base = Class.create({ + baseInitialize: function(element, update, options) { + element = $(element) + this.element = element; + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + this.oldElementValue = this.element.value; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || { }; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (Prototype.Browser.IE) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + '<iframe id="' + this.update.id + '_iefix" '+ + 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + + 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = $(selectedElement).select('.' + this.options.select) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); + } else { + this.element.value = value; + } + this.oldElementValue = this.element.value; + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + this.tokenBounds = null; + if(this.getToken().length>=this.options.minChars) { + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + this.oldElementValue = this.element.value; + }, + + getToken: function() { + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; + } + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); + } +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; + +Ajax.Autocompleter = Class.create(Autocompleter.Base, { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(Autocompleter.Base, { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + + elem.substr(entry.length) + "</li>"); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" + + elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( + foundPos + entry.length) + "</li>"); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "<ul>" + ret.join('') + "</ul>"; + } + }, options || { }); + } +}); + +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007). + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create({ + initialize: function(element, url, options) { + this.url = url; + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; + } + if (this.options.externalControl) + this.options.externalControl = $(this.options.externalControl); + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || 'transparent'; + this.element.title = this.options.clickToEditText; + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; + } + }, + createEditField: function() { + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; + var size = this.options.size || this.options.cols || 0; + if (0 < size) fld.size = size; + } else { + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; + } + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) + this.loadExternalText(); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); + }, + getText: function() { + return this.element.innerHTML; + }, + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; + } + }, + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); + } + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); + }, + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, + showSaving: function() { + this._oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + }, + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); + } + }, + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); + }, + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadCollectionURL, options); + }, + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; + } + tempOption.update((text || '').stripScripts().stripTags()); + }, + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); + }, + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); + } +}); + +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; + +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); + } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' + } +}); + +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create({ + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}); diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js new file mode 100644 index 000000000..ccf4a1e45 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/dragdrop.js @@ -0,0 +1,972 @@ +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(Object.isUndefined(Effect)) + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || { }); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if(Object.isArray(containment)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var drop, affected = []; + + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) + drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + if (drop != this.last_active) Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create({ + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || { }); + + this.element = $(element); + + if(options.handle && Object.isString(options.handle)) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(!Object.isUndefined(Draggable._dragging[this.element]) && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this.element._originallyAbsolute) + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } + + if(this.options.ghosting) { + if (!this.element._originallyAbsolute) + Position.relativize(this.element); + delete this.element._originallyAbsolute; + Element.remove(this._clone); + this._clone = null; + } + + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && Object.isFunction(revert)) revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(Object.isFunction(this.options.snap)) { + p = this.options.snap(p[0],p[1],this); + } else { + if(Object.isArray(this.options.snap)) { + p = p.map( function(v, i) { + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return (v/this.options.snap).round()*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +}); + +Draggable._dragging = { }; + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create({ + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +}); + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: { }, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || { }); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || { }); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + } + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || { }); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || { }); + + var nodeMap = { }; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +} diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js new file mode 100644 index 000000000..65aed2395 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/effects.js @@ -0,0 +1,1120 @@ +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if (this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if (this.slice(0,1) == '#') { + if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if (this.length==7) color = this.toLowerCase(); + } + } + return (color.length==7 ? color : (arguments[0] || this)); +}; + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +}; + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +}; + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +}; + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +}; + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + ((pos % (1/pulses)) * pulses).round() == 0 ? + ((pos * pulses * 2) - (pos * pulses * 2).floor()) : + 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor()) + ); + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if (child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + new Element('span', {style: tagifyStyle}).update( + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if (((typeof element == 'object') || + Object.isFunction(element)) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || { }); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || { }); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(Enumerable, { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = Object.isString(effect.options.queue) ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if (this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i<len;i++) + this.effects[i] && this.effects[i].loop(timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if (!Object.isString(queueName)) return queueName; + + return this.instances.get(queueName) || + this.instances.set(queueName, new Effect.ScopedQueue()); + } +}; +Effect.Queue = Effect.Queues.get('global'); + +Effect.Base = Class.create({ + position: null, + start: function(options) { + function codeForEvent(options,eventName){ + return ( + (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + + (options[eventName] ? 'this.options.'+eventName+'(this);' : '') + ); + } + if (options && options.transition === false) options.transition = Effect.Transitions.linear; + this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { }); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn+(this.options.duration*1000); + this.fromToDelta = this.options.to-this.options.from; + this.totalTime = this.finishOn-this.startOn; + this.totalFrames = this.options.fps*this.options.duration; + + eval('this.render = function(pos){ '+ + 'if (this.state=="idle"){this.state="running";'+ + codeForEvent(this.options,'beforeSetup')+ + (this.setup ? 'this.setup();':'')+ + codeForEvent(this.options,'afterSetup')+ + '};if (this.state=="running"){'+ + 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+ + 'this.position=pos;'+ + codeForEvent(this.options,'beforeUpdate')+ + (this.update ? 'this.update(pos);':'')+ + codeForEvent(this.options,'afterUpdate')+ + '}}'); + + this.event('beforeStart'); + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if (timePos >= this.startOn) { + if (timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if (this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>'; + } +}); + +Effect.Parallel = Class.create(Effect.Base, { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if (effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { + initialize: function() { + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || { }); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if (this.options.mode == 'absolute') { + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); +}; + +Effect.Scale = Class.create(Effect.Base, { + initialize: function(element, percent) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or { } with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || { }); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = { }; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if (fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if (this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if (/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if (!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if (this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if (this.elementPositioning == 'absolute') { + if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if (this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if (!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(), + max = (window.height || document.body.scrollHeight) - document.viewport.getHeight(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1] > max ? max : elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()) } + ); +}; + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || { }); + return new Effect.Opacity(element,options); +}; + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || { }) + ); +}; + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || { }) + ); +}; + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || { })); +}; + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }) + } + }, arguments[1] || { })); +}; + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || { })); +}; + +Effect.Shake = function(element) { + element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +}; + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || { }) + ); +}; + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if (window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); + } + }, arguments[1] || { }) + ); +}; + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +}; + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ) + } + }); +}; + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || { }); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +}; + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || { }; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +}; + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || { })); +}; + +Effect.Morph = Class.create(Effect.Base, { + initialize: function(element) { + this.element = $(element); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + } + } + } + this.start(options); + }, + + setup: function(){ + function parseColor(color){ + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create({ + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || { }; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + track = $H(track); + var data = track.values().first(); + this.tracks.push($H({ + ids: track.keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.__parseStyleElement = document.createElement('div'); +String.prototype.parseStyle = function(){ + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>'; + style = String.__parseStyleElement.childNodes[0].style; + } + + Element.CSS_PROPERTIES.each(function(property){ + if (style[property]) styleRules.set(property, style[property]); + }); + + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; +}; + +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) { + hash.set(property, css[property]); + return hash; + }); + if (!styles.opacity) styles.set('opacity', element.getOpacity()); + return styles; + }; +}; + +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element) + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + } + } +); + +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); + +Element.addMethods(Effect.Methods); diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js new file mode 100644 index 000000000..546f9fe44 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/javascripts/prototype.js @@ -0,0 +1,4225 @@ +/* Prototype JavaScript framework, version 1.6.0.1 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0.1', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ + }, + + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value, value = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (Object.isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return object && object.nodeType == 1; + }, + + isArray: function(object) { + return object && object.constructor === Array; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip"); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + }, + unescapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + iterator = iterator.bind(context); + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + iterator = iterator.bind(context); + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator(value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == null || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + iterator = iterator.bind(context); + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + function $A(iterable) { + if (!iterable) return []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + } +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, t, range; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + t = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + t.insert(element, content); + continue; + } + + content = Object.toHTML(content); + + range = element.ownerDocument.createRange(); + t.initializeRange(element, range); + t.insert(element, range.createContextualFragment(content.stripScripts())); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $(element).getElementsBySelector("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + var originalAncestor = ancestor; + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + } + + while (element = element.parentNode) + if (element == originalAncestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + + +if (!document.createRange || Prototype.Browser.Opera) { + Element.Methods.insert = function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = { bottom: insertions }; + + var t = Element._insertionTranslations, content, position, pos, tagName; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + pos = t[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + pos.insert(element, content); + continue; + } + + content = Object.toHTML(content); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + if (t.tags[tagName]) { + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + if (position == 'top' || position == 'after') fragments.reverse(); + fragments.each(pos.insert.curry(element)); + } + else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); + + content.evalScripts.bind(content).defer(); + } + + return element; + }; +} + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position != 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.clone(Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if (document.createElement('div').outerHTML) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: { + adjacency: 'beforeBegin', + insert: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + initializeRange: function(element, range) { + range.setStartBefore(element); + } + }, + top: { + adjacency: 'afterBegin', + insert: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + initializeRange: function(element, range) { + range.selectNodeContents(element); + range.collapse(true); + } + }, + bottom: { + adjacency: 'beforeEnd', + insert: function(element, node) { + element.appendChild(node); + } + }, + after: { + adjacency: 'afterEnd', + insert: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + initializeRange: function(element, range) { + range.setStartAfter(element); + } + }, + tags: { + TABLE: ['<table>', '</table>', 1], + TBODY: ['<table><tbody>', '</tbody></table>', 2], + TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3], + TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4], + SELECT: ['<select>', '</select>', 1] + } +}; + +(function() { + this.bottom.initializeRange = this.top.initializeRange; + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return node && node.specified; + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName, property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }; + var B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] : + (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D]; + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + this.compileMatcher(); + }, + + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(this.expression)) + return false; + + return true; + }, + + compileMatcher: function() { + if (this.shouldUseXPath()) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return Selector.operators[matches[2]](nodeValue, matches[3]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + matchElements: function(elements, expression) { + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + var exprs = expressions.join(','); + expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + Selector.handlers.concat = function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }; +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, index) { + if (Object.isUndefined(index)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._eventID) return element._eventID; + arguments.callee.id = arguments.callee.id || 1; + return element._eventID = ++arguments.callee.id; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event) + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + if (document.createEvent) { + var event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + var event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize() +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer, fired = false; + + function fireContentLoadedEvent() { + if (fired) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + fired = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>"); + $("__onDOMContentLoaded").onreadystatechange = function() { + if (this.readyState == "complete") { + this.onreadystatechange = null; + fireContentLoadedEvent(); + } + }; + } +})(); +/*------------------------------- DEPRECATED -------------------------------*/ + +Hash.toQueryString = Object.toQueryString; + +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); + }, + + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, + + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, + + After: function(element, content) { + return Element.insert(element, {after:content}); + } +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +// This should be moved to script.aculo.us; notice the deprecated methods +// further below, that map to the newer Element methods. +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = Element.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = Element.cumulativeScrollOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = Element.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + // Deprecation layer -- use newer Element methods now (1.5.2). + + cumulativeOffset: Element.Methods.cumulativeOffset, + + positionedOffset: Element.Methods.positionedOffset, + + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, + + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); + }, + + realOffset: Element.Methods.cumulativeScrollOffset, + + offsetParent: Element.Methods.getOffsetParent, + + page: Element.Methods.viewportOffset, + + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); + } + return elements; + }; + + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); + +/*--------------------------------------------------------------------------*/ + +Element.addMethods();
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb new file mode 100644 index 000000000..eae230ad9 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/number_helper.rb @@ -0,0 +1,179 @@ +module ActionView + module Helpers #:nodoc: + # Provides methods for converting numbers into formatted strings. + # Methods are provided for phone numbers, currency, percentage, + # precision, positional notation, and file size. + module NumberHelper + # Formats a +number+ into a US phone number (e.g., (555) 123-9876). You can customize the format + # in the +options+ hash. + # + # ==== Options + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the end of the + # generated number. + # * <tt>:country_code</tt> - Sets the country code for the phone number. + # + # ==== Examples + # number_to_phone(1235551234) # => 123-555-1234 + # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 + # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 + # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 + # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 + # + # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") + # => +1.123.555.1234 x 1343 + def number_to_phone(number, options = {}) + number = number.to_s.strip unless number.nil? + options = options.stringify_keys + area_code = options["area_code"] || nil + delimiter = options["delimiter"] || "-" + extension = options["extension"].to_s.strip || nil + country_code = options["country_code"] || nil + + begin + str = "" + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << if area_code + number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3") + else + number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + end + str << " x #{extension}" unless extension.blank? + str + rescue + number + end + end + + # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format + # in the +options+ hash. + # + # ==== Options + # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). + # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). + # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). + # + # ==== Examples + # number_to_currency(1234567890.50) # => $1,234,567,890.50 + # number_to_currency(1234567890.506) # => $1,234,567,890.51 + # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 + # + # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") + # # => £1234567890,50 + def number_to_currency(number, options = {}) + options = options.stringify_keys + precision = options["precision"] || 2 + unit = options["unit"] || "$" + separator = precision > 0 ? options["separator"] || "." : "" + delimiter = options["delimiter"] || "," + + begin + parts = number_with_precision(number, precision).split('.') + unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s + rescue + number + end + end + + # Formats a +number+ as a percentage string (e.g., 65%). You can customize the + # format in the +options+ hash. + # + # ==== Options + # * <tt>:precision</tt> - Sets the level of precision (defaults to 3). + # * <tt>:separator</tt> - Sets the separator between the units (defaults to "."). + # + # ==== Examples + # number_to_percentage(100) # => 100.000% + # number_to_percentage(100, :precision => 0) # => 100% + # + # number_to_percentage(302.24398923423, :precision => 5) + # # => 302.24399% + def number_to_percentage(number, options = {}) + options = options.stringify_keys + precision = options["precision"] || 3 + separator = options["separator"] || "." + + begin + number = number_with_precision(number, precision) + parts = number.split('.') + if parts.at(1).nil? + parts[0] + "%" + else + parts[0] + separator + parts[1].to_s + "%" + end + rescue + number + end + end + + # Formats a +number+ with grouped thousands using +delimiter+ (e.g., 12,324). You + # can customize the format using optional <em>delimiter</em> and <em>separator</em> parameters. + # + # ==== Options + # * <tt>delimiter</tt> - Sets the thousands delimiter (defaults to ","). + # * <tt>separator</tt> - Sets the separator between the units (defaults to "."). + # + # ==== Examples + # number_with_delimiter(12345678) # => 12,345,678 + # number_with_delimiter(12345678.05) # => 12,345,678.05 + # number_with_delimiter(12345678, ".") # => 12.345.678 + # + # number_with_delimiter(98765432.98, " ", ",") + # # => 98 765 432,98 + def number_with_delimiter(number, delimiter=",", separator=".") + begin + parts = number.to_s.split('.') + parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") + parts.join separator + rescue + number + end + end + + # Formats a +number+ with the specified level of +precision+ (e.g., 112.32 has a precision of 2). The default + # level of precision is 3. + # + # ==== Examples + # number_with_precision(111.2345) # => 111.235 + # number_with_precision(111.2345, 2) # => 111.24 + # number_with_precision(13, 5) # => 13.00000 + # number_with_precision(389.32314, 0) # => 389 + def number_with_precision(number, precision=3) + "%01.#{precision}f" % number + rescue + number + end + + # Formats the bytes in +size+ into a more understandable representation + # (e.g., giving it 1500 yields 1.5 KB). This method is useful for + # reporting file sizes to users. This method returns nil if + # +size+ cannot be converted into a number. You can change the default + # precision of 1 using the precision parameter +precision+. + # + # ==== Examples + # number_to_human_size(123) # => 123 Bytes + # number_to_human_size(1234) # => 1.2 KB + # number_to_human_size(12345) # => 12.1 KB + # number_to_human_size(1234567) # => 1.2 MB + # number_to_human_size(1234567890) # => 1.1 GB + # number_to_human_size(1234567890123) # => 1.1 TB + # number_to_human_size(1234567, 2) # => 1.18 MB + # number_to_human_size(483989, 0) # => 4 MB + def number_to_human_size(size, precision=1) + size = Kernel.Float(size) + case + when size.to_i == 1; "1 Byte" + when size < 1.kilobyte; "%d Bytes" % size + when size < 1.megabyte; "%.#{precision}f KB" % (size / 1.0.kilobyte) + when size < 1.gigabyte; "%.#{precision}f MB" % (size / 1.0.megabyte) + when size < 1.terabyte; "%.#{precision}f GB" % (size / 1.0.gigabyte) + else "%.#{precision}f TB" % (size / 1.0.terabyte) + end.sub(/([0-9])\.?0+ /, '\1 ' ) + rescue + nil + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb new file mode 100644 index 000000000..0435f5c42 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -0,0 +1,1268 @@ +require 'set' + +module ActionView + module Helpers + # Prototype[http://www.prototypejs.org/] is a JavaScript library that provides + # DOM[http://en.wikipedia.org/wiki/Document_Object_Model] manipulation, + # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php] + # functionality, and more traditional object-oriented facilities for JavaScript. + # This module provides a set of helpers to make it more convenient to call + # functions from Prototype using Rails, including functionality to call remote + # Rails methods (that is, making a background request to a Rails action) using Ajax. + # This means that you can call actions in your controllers without + # reloading the page, but still update certain parts of it using + # injections into the DOM. A common use case is having a form that adds + # a new element to a list without reloading the page or updating a shopping + # cart total when a new item is added. + # + # == Usage + # To be able to use these helpers, you must first include the Prototype + # JavaScript framework in your pages. + # + # javascript_include_tag 'prototype' + # + # (See the documentation for + # ActionView::Helpers::JavaScriptHelper for more information on including + # this and other JavaScript files in your Rails templates.) + # + # Now you're ready to call a remote action either through a link... + # + # link_to_remote "Add to cart", + # :url => { :action => "add", :id => product.id }, + # :update => { :success => "cart", :failure => "error" } + # + # ...through a form... + # + # <% form_remote_tag :url => '/shipping' do -%> + # <div><%= submit_tag 'Recalculate Shipping' %></div> + # <% end -%> + # + # ...periodically... + # + # periodically_call_remote(:url => 'update', :frequency => '5', :update => 'ticker') + # + # ...or through an observer (i.e., a form or field that is observed and calls a remote + # action when changed). + # + # <%= observe_field(:searchbox, + # :url => { :action => :live_search }), + # :frequency => 0.5, + # :update => :hits, + # :with => 'query' + # %> + # + # As you can see, there are numerous ways to use Prototype's Ajax functions (and actually more than + # are listed here); check out the documentation for each method to find out more about its usage and options. + # + # === Common Options + # See link_to_remote for documentation of options common to all Ajax + # helpers; any of the options specified by link_to_remote can be used + # by the other helpers. + # + # == Designing your Rails actions for Ajax + # When building your action handlers (that is, the Rails actions that receive your background requests), it's + # important to remember a few things. First, whatever your action would normall return to the browser, it will + # return to the Ajax call. As such, you typically don't want to render with a layout. This call will cause + # the layout to be transmitted back to your page, and, if you have a full HTML/CSS, will likely mess a lot of things up. + # You can turn the layout off on particular actions by doing the following: + # + # class SiteController < ActionController::Base + # layout "standard", :except => [:ajax_method, :more_ajax, :another_ajax] + # end + # + # Optionally, you could do this in the method you wish to lack a layout: + # + # render :layout => false + # + # You can tell the type of request from within your action using the <tt>request.xhr?</tt> (XmlHttpRequest, the + # method that Ajax uses to make background requests) method. + # def name + # # Is this an XmlHttpRequest request? + # if (request.xhr?) + # render :text => @name.to_s + # else + # # No? Then render an action. + # render :action => 'view_attribute', :attr => @name + # end + # end + # + # The else clause can be left off and the current action will render with full layout and template. An extension + # to this solution was posted to Ryan Heneise's blog at ArtOfMission["http://www.artofmission.com/"]. + # + # layout proc{ |c| c.request.xhr? ? false : "application" } + # + # Dropping this in your ApplicationController turns the layout off for every request that is an "xhr" request. + # + # If you are just returning a little data or don't want to build a template for your output, you may opt to simply + # render text output, like this: + # + # render :text => 'Return this from my method!' + # + # Since whatever the method returns is injected into the DOM, this will simply inject some text (or HTML, if you + # tell it to). This is usually how small updates, such updating a cart total or a file count, are handled. + # + # == Updating multiple elements + # See JavaScriptGenerator for information on updating multiple elements + # on the page in an Ajax response. + module PrototypeHelper + unless const_defined? :CALLBACKS + CALLBACKS = Set.new([ :uninitialized, :loading, :loaded, + :interactive, :complete, :failure, :success ] + + (100..599).to_a) + AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, + :asynchronous, :method, :insertion, :position, + :form, :with, :update, :script ]).merge(CALLBACKS) + end + + # Returns a link to a remote action defined by <tt>options[:url]</tt> + # (using the url_for format) that's called in the background using + # XMLHttpRequest. The result of that request can then be inserted into a + # DOM object whose id can be specified with <tt>options[:update]</tt>. + # Usually, the result would be a partial prepared by the controller with + # render :partial. + # + # Examples: + # # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true}); + # # return false;">Delete this post</a> + # link_to_remote "Delete this post", :update => "posts", + # :url => { :action => "destroy", :id => post.id } + # + # # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true}); + # # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a> + # link_to_remote(image_tag("refresh"), :update => "emails", + # :url => { :action => "list_emails" }) + # + # You can override the generated HTML options by specifying a hash in + # <tt>options[:html]</tt>. + # + # link_to_remote "Delete this post", :update => "posts", + # :url => post_url(@post), :method => :delete, + # :html => { :class => "destructive" } + # + # You can also specify a hash for <tt>options[:update]</tt> to allow for + # easy redirection of output to an other DOM element if a server-side + # error occurs: + # + # Example: + # # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5', + # # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a> + # link_to_remote "Delete this post", + # :url => { :action => "destroy", :id => post.id }, + # :update => { :success => "posts", :failure => "error" } + # + # Optionally, you can use the <tt>options[:position]</tt> parameter to + # influence how the target DOM element is updated. It must be one of + # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>. + # + # The method used is by default POST. You can also specify GET or you + # can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt> + # + # Example: + # # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'}); + # # return false;">Destroy</a> + # link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete + # + # By default, these remote requests are processed asynchronous during + # which various JavaScript callbacks can be triggered (for progress + # indicators and the likes). All callbacks get access to the + # <tt>request</tt> object, which holds the underlying XMLHttpRequest. + # + # To access the server response, use <tt>request.responseText</tt>, to + # find out the HTTP status, use <tt>request.status</tt>. + # + # Example: + # # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true, + # # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a> + # word = 'hello' + # link_to_remote word, + # :url => { :action => "undo", :n => word_counter }, + # :complete => "undoRequestCompleted(request)" + # + # The callbacks that may be specified are (in order): + # + # <tt>:loading</tt>:: Called when the remote document is being + # loaded with data by the browser. + # <tt>:loaded</tt>:: Called when the browser has finished loading + # the remote document. + # <tt>:interactive</tt>:: Called when the user can interact with the + # remote document, even though it has not + # finished loading. + # <tt>:success</tt>:: Called when the XMLHttpRequest is completed, + # and the HTTP status code is in the 2XX range. + # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed, + # and the HTTP status code is not in the 2XX + # range. + # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete + # (fires after success/failure if they are + # present). + # + # You can further refine <tt>:success</tt> and <tt>:failure</tt> by + # adding additional callbacks for specific status codes. + # + # Example: + # # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true, + # # on404:function(request){alert('Not found...? Wrong URL...?')}, + # # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a> + # link_to_remote word, + # :url => { :action => "action" }, + # 404 => "alert('Not found...? Wrong URL...?')", + # :failure => "alert('HTTP Error ' + request.status + '!')" + # + # A status code callback overrides the success/failure handlers if + # present. + # + # If you for some reason or another need synchronous processing (that'll + # block the browser while the request is happening), you can specify + # <tt>options[:type] = :synchronous</tt>. + # + # You can customize further browser side call logic by passing in + # JavaScript code snippets via some optional parameters. In their order + # of use these are: + # + # <tt>:confirm</tt>:: Adds confirmation dialog. + # <tt>:condition</tt>:: Perform remote request conditionally + # by this expression. Use this to + # describe browser-side conditions when + # request should not be initiated. + # <tt>:before</tt>:: Called before request is initiated. + # <tt>:after</tt>:: Called immediately after request was + # initiated and before <tt>:loading</tt>. + # <tt>:submit</tt>:: Specifies the DOM element ID that's used + # as the parent of the form elements. By + # default this is the current form, but + # it could just as well be the ID of a + # table row or any other DOM element. + # <tt>:with</tt>:: A JavaScript expression specifying + # the parameters for the XMLHttpRequest. + # Any expressions should return a valid + # URL query string. + # + # Example: + # + # :with => "'name=' + $('name').value" + # + # You can generate a link that uses AJAX in the general case, while + # degrading gracefully to plain link behavior in the absence of + # JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL. + # Note the extra curly braces around the <tt>options</tt> hash separate + # it as the second parameter from <tt>html_options</tt>, the third. + # + # Example: + # link_to_remote "Delete this post", + # { :update => "posts", :url => { :action => "destroy", :id => post.id } }, + # :href => url_for(:action => "destroy", :id => post.id) + def link_to_remote(name, options = {}, html_options = nil) + link_to_function(name, remote_function(options), html_options || options.delete(:html)) + end + + # Periodically calls the specified url (<tt>options[:url]</tt>) every + # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to + # update a specified div (<tt>options[:update]</tt>) with the results + # of the remote call. The options for specifying the target with :url + # and defining callbacks is the same as link_to_remote. + # Examples: + # # Call get_averages and put its results in 'avg' every 10 seconds + # # Generates: + # # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages', + # # {asynchronous:true, evalScripts:true})}, 10) + # periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg') + # + # # Call invoice every 10 seconds with the id of the customer + # # If it succeeds, update the invoice DIV; if it fails, update the error DIV + # # Generates: + # # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'}, + # # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10) + # periodically_call_remote(:url => { :action => 'invoice', :id => customer.id }, + # :update => { :success => "invoice", :failure => "error" } + # + # # Call update every 20 seconds and update the new_block DIV + # # Generates: + # # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20) + # periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block') + # + def periodically_call_remote(options = {}) + frequency = options[:frequency] || 10 # every ten seconds by default + code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})" + javascript_tag(code) + end + + # Returns a form tag that will submit using XMLHttpRequest in the + # background instead of the regular reloading POST arrangement. Even + # though it's using JavaScript to serialize the form elements, the form + # submission will work just like a regular submission as viewed by the + # receiving side (all elements available in <tt>params</tt>). The options for + # specifying the target with :url and defining callbacks is the same as + # link_to_remote. + # + # A "fall-through" target for browsers that doesn't do JavaScript can be + # specified with the :action/:method options on :html. + # + # Example: + # # Generates: + # # <form action="/some/place" method="post" onsubmit="new Ajax.Request('', + # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;"> + # form_remote_tag :html => { :action => + # url_for(:controller => "some", :action => "place") } + # + # The Hash passed to the :html key is equivalent to the options (2nd) + # argument in the FormTagHelper.form_tag method. + # + # By default the fall-through action is the same as the one specified in + # the :url (and the default method is :post). + # + # form_remote_tag also takes a block, like form_tag: + # # Generates: + # # <form action="/" method="post" onsubmit="new Ajax.Request('/', + # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); + # # return false;"> <div><input name="commit" type="submit" value="Save" /></div> + # # </form> + # <% form_remote_tag :url => '/posts' do -%> + # <div><%= submit_tag 'Save' %></div> + # <% end -%> + def form_remote_tag(options = {}, &block) + options[:form] = true + + options[:html] ||= {} + options[:html][:onsubmit] = + (options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") + + "#{remote_function(options)}; return false;" + + form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block) + end + + # Creates a form that will submit using XMLHttpRequest in the background + # instead of the regular reloading POST arrangement and a scope around a + # specific resource that is used as a base for questioning about + # values for the fields. + # + # === Resource + # + # Example: + # <% remote_form_for(@post) do |f| %> + # ... + # <% end %> + # + # This will expand to be the same as: + # + # <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> + # ... + # <% end %> + # + # === Nested Resource + # + # Example: + # <% remote_form_for([@post, @comment]) do |f| %> + # ... + # <% end %> + # + # This will expand to be the same as: + # + # <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %> + # ... + # <% end %> + # + # If you don't need to attach a form to a resource, then check out form_remote_tag. + # + # See FormHelper#form_for for additional semantics. + def remote_form_for(record_or_name_or_array, *args, &proc) + options = args.extract_options! + + case record_or_name_or_array + when String, Symbol + object_name = record_or_name_or_array + when Array + object = record_or_name_or_array.last + object_name = ActionController::RecordIdentifier.singular_class_name(object) + apply_form_for_options!(record_or_name_or_array, options) + args.unshift object + else + object = record_or_name_or_array + object_name = ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array) + apply_form_for_options!(object, options) + args.unshift object + end + + concat(form_remote_tag(options), proc.binding) + fields_for(object_name, *(args << options), &proc) + concat('</form>', proc.binding) + end + alias_method :form_remote_for, :remote_form_for + + # Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+ + # that will submit form using XMLHttpRequest in the background instead of a regular POST request that + # reloads the page. + # + # # Create a button that submits to the create action + # # + # # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create', + # # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)}); + # # return false;" type="button" value="Create" /> + # <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %> + # + # # Submit to the remote action update and update the DIV succeed or fail based + # # on the success or failure of the request + # # + # # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'}, + # # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)}); + # # return false;" type="button" value="Update" /> + # <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' }, + # :update => { :success => "succeed", :failure => "fail" } + # + # <tt>options</tt> argument is the same as in form_remote_tag. + def submit_to_remote(name, value, options = {}) + options[:with] ||= 'Form.serialize(this.form)' + + options[:html] ||= {} + options[:html][:type] = 'button' + options[:html][:onclick] = "#{remote_function(options)}; return false;" + options[:html][:name] = name + options[:html][:value] = value + + tag("input", options[:html], false) + end + + # Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function + # that form_remote_tag can call in :complete to evaluate a multiple + # update return document using update_element_function calls. + def evaluate_remote_response + "eval(request.responseText)" + end + + # Returns the JavaScript needed for a remote function. + # Takes the same arguments as link_to_remote. + # + # Example: + # # Generates: <select id="options" onchange="new Ajax.Updater('options', + # # '/testing/update_options', {asynchronous:true, evalScripts:true})"> + # <select id="options" onchange="<%= remote_function(:update => "options", + # :url => { :action => :update_options }) %>"> + # <option value="0">Hello</option> + # <option value="1">World</option> + # </select> + def remote_function(options) + javascript_options = options_for_ajax(options) + + update = '' + if options[:update] && options[:update].is_a?(Hash) + update = [] + update << "success:'#{options[:update][:success]}'" if options[:update][:success] + update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] + update = '{' + update.join(',') + '}' + elsif options[:update] + update << "'#{options[:update]}'" + end + + function = update.empty? ? + "new Ajax.Request(" : + "new Ajax.Updater(#{update}, " + + url_options = options[:url] + url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash) + function << "'#{url_for(url_options)}'" + function << ", #{javascript_options})" + + function = "#{options[:before]}; #{function}" if options[:before] + function = "#{function}; #{options[:after]}" if options[:after] + function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] + function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] + + return function + end + + # Observes the field with the DOM ID specified by +field_id+ and calls a + # callback when its contents have changed. The default callback is an + # Ajax call. By default the value of the observed field is sent as a + # parameter with the Ajax call. + # + # Example: + # # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest', + # # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})}) + # <%= observe_field :suggest, :url => { :action => :find_suggestion }, + # :frequency => 0.25, + # :update => :suggest, + # :with => 'q' + # %> + # + # Required +options+ are either of: + # <tt>:url</tt>:: +url_for+-style options for the action to call + # when the field has changed. + # <tt>:function</tt>:: Instead of making a remote call to a URL, you + # can specify javascript code to be called instead. + # Note that the value of this option is used as the + # *body* of the javascript function, a function definition + # with parameters named element and value will be generated for you + # for example: + # observe_field("glass", :frequency => 1, :function => "alert('Element changed')") + # will generate: + # new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')}) + # The element parameter is the DOM element being observed, and the value is its value at the + # time the observer is triggered. + # + # Additional options are: + # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to + # this field will be detected. Not setting this + # option at all or to a value equal to or less than + # zero will use event based observation instead of + # time based observation. + # <tt>:update</tt>:: Specifies the DOM ID of the element whose + # innerHTML should be updated with the + # XMLHttpRequest response text. + # <tt>:with</tt>:: A JavaScript expression specifying the parameters + # for the XMLHttpRequest. The default is to send the + # key and value of the observed field. Any custom + # expressions should return a valid URL query string. + # The value of the field is stored in the JavaScript + # variable +value+. + # + # Examples + # + # :with => "'my_custom_key=' + value" + # :with => "'person[name]=' + prompt('New name')" + # :with => "Form.Element.serialize('other-field')" + # + # Finally + # :with => 'name' + # is shorthand for + # :with => "'name=' + value" + # This essentially just changes the key of the parameter. + # <tt>:on</tt>:: Specifies which event handler to observe. By default, + # it's set to "changed" for text fields and areas and + # "click" for radio buttons and checkboxes. With this, + # you can specify it instead to be "blur" or "focus" or + # any other event. + # + # Additionally, you may specify any of the options documented in the + # <em>Common options</em> section at the top of this document. + # + # Example: + # + # # Sends params: {:title => 'Title of the book'} when the book_title input + # # field is changed. + # observe_field 'book_title', + # :url => 'http://example.com/books/edit/1', + # :with => 'title' + # + # # Sends params: {:book_title => 'Title of the book'} when the focus leaves + # # the input field. + # observe_field 'book_title', + # :url => 'http://example.com/books/edit/1', + # :on => 'blur' + # + def observe_field(field_id, options = {}) + if options[:frequency] && options[:frequency] > 0 + build_observer('Form.Element.Observer', field_id, options) + else + build_observer('Form.Element.EventObserver', field_id, options) + end + end + + # Observes the form with the DOM ID specified by +form_id+ and calls a + # callback when its contents have changed. The default callback is an + # Ajax call. By default all fields of the observed field are sent as + # parameters with the Ajax call. + # + # The +options+ for +observe_form+ are the same as the options for + # +observe_field+. The JavaScript variable +value+ available to the + # <tt>:with</tt> option is set to the serialized form by default. + def observe_form(form_id, options = {}) + if options[:frequency] + build_observer('Form.Observer', form_id, options) + else + build_observer('Form.EventObserver', form_id, options) + end + end + + # All the methods were moved to GeneratorMethods so that + # #include_helpers_from_context has nothing to overwrite. + class JavaScriptGenerator #:nodoc: + def initialize(context, &block) #:nodoc: + @context, @lines = context, [] + include_helpers_from_context + @context.instance_exec(self, &block) + end + + private + def include_helpers_from_context + @context.extended_by.each do |mod| + extend mod unless mod.name =~ /^ActionView::Helpers/ + end + extend GeneratorMethods + end + + # JavaScriptGenerator generates blocks of JavaScript code that allow you + # to change the content and presentation of multiple DOM elements. Use + # this in your Ajax response bodies, either in a <script> tag or as plain + # JavaScript sent with a Content-type of "text/javascript". + # + # Create new instances with PrototypeHelper#update_page or with + # ActionController::Base#render, then call #insert_html, #replace_html, + # #remove, #show, #hide, #visual_effect, or any other of the built-in + # methods on the yielded generator in any order you like to modify the + # content and appearance of the current page. + # + # Example: + # + # # Generates: + # # new Insertion.Bottom("list", "<li>Some item</li>"); + # # new Effect.Highlight("list"); + # # ["status-indicator", "cancel-link"].each(Element.hide); + # update_page do |page| + # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>" + # page.visual_effect :highlight, 'list' + # page.hide 'status-indicator', 'cancel-link' + # end + # + # + # Helper methods can be used in conjunction with JavaScriptGenerator. + # When a helper method is called inside an update block on the +page+ + # object, that method will also have access to a +page+ object. + # + # Example: + # + # module ApplicationHelper + # def update_time + # page.replace_html 'time', Time.now.to_s(:db) + # page.visual_effect :highlight, 'time' + # end + # end + # + # # Controller action + # def poll + # render(:update) { |page| page.update_time } + # end + # + # You can also use PrototypeHelper#update_page_tag instead of + # PrototypeHelper#update_page to wrap the generated JavaScript in a + # <script> tag. + module GeneratorMethods + def to_s #:nodoc: + returning javascript = @lines * $/ do + if ActionView::Base.debug_rjs + source = javascript.dup + javascript.replace "try {\n#{source}\n} catch (e) " + javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }" + end + end + end + + # Returns a element reference by finding it through +id+ in the DOM. This element can then be + # used for further method calls. Examples: + # + # page['blank_slate'] # => $('blank_slate'); + # page['blank_slate'].show # => $('blank_slate').show(); + # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up(); + # + # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup + # the correct id: + # + # page[@post] # => $('post_45') + # page[Post.new] # => $('new_post') + def [](id) + case id + when String, Symbol, NilClass + JavaScriptElementProxy.new(self, id) + else + JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id)) + end + end + + # Returns an object whose <tt>#to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript + # expression as an argument to another JavaScriptGenerator method. + def literal(code) + ActiveSupport::JSON::Variable.new(code.to_s) + end + + # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be + # used for further method calls. Examples: + # + # page.select('p') # => $$('p'); + # page.select('p.welcome b').first # => $$('p.welcome b').first(); + # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide(); + # + # You can also use prototype enumerations with the collection. Observe: + # + # # Generates: $$('#items li').each(function(value) { value.hide(); }); + # page.select('#items li').each do |value| + # value.hide + # end + # + # Though you can call the block param anything you want, they are always rendered in the + # javascript as 'value, index.' Other enumerations, like collect() return the last statement: + # + # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); }); + # page.select('#items li').collect('hidden') do |item| + # item.hide + # end + # + def select(pattern) + JavaScriptElementCollectionProxy.new(self, pattern) + end + + # Inserts HTML at the specified +position+ relative to the DOM element + # identified by the given +id+. + # + # +position+ may be one of: + # + # <tt>:top</tt>:: HTML is inserted inside the element, before the + # element's existing content. + # <tt>:bottom</tt>:: HTML is inserted inside the element, after the + # element's existing content. + # <tt>:before</tt>:: HTML is inserted immediately preceding the element. + # <tt>:after</tt>:: HTML is inserted immediately following the element. + # + # +options_for_render+ may be either a string of HTML to insert, or a hash + # of options to be passed to ActionView::Base#render. For example: + # + # # Insert the rendered 'navigation' partial just before the DOM + # # element with ID 'content'. + # # Generates: new Insertion.Before("content", "-- Contents of 'navigation' partial --"); + # insert_html :before, 'content', :partial => 'navigation' + # + # # Add a list item to the bottom of the <ul> with ID 'list'. + # # Generates: new Insertion.Bottom("list", "<li>Last item</li>"); + # insert_html :bottom, 'list', '<li>Last item</li>' + # + def insert_html(position, id, *options_for_render) + insertion = position.to_s.camelize + call "new Insertion.#{insertion}", id, render(*options_for_render) + end + + # Replaces the inner HTML of the DOM element with the given +id+. + # + # +options_for_render+ may be either a string of HTML to insert, or a hash + # of options to be passed to ActionView::Base#render. For example: + # + # # Replace the HTML of the DOM element having ID 'person-45' with the + # # 'person' partial for the appropriate object. + # # Generates: Element.update("person-45", "-- Contents of 'person' partial --"); + # replace_html 'person-45', :partial => 'person', :object => @person + # + def replace_html(id, *options_for_render) + call 'Element.update', id, render(*options_for_render) + end + + # Replaces the "outer HTML" (i.e., the entire element, not just its + # contents) of the DOM element with the given +id+. + # + # +options_for_render+ may be either a string of HTML to insert, or a hash + # of options to be passed to ActionView::Base#render. For example: + # + # # Replace the DOM element having ID 'person-45' with the + # # 'person' partial for the appropriate object. + # replace 'person-45', :partial => 'person', :object => @person + # + # This allows the same partial that is used for the +insert_html+ to + # be also used for the input to +replace+ without resorting to + # the use of wrapper elements. + # + # Examples: + # + # <div id="people"> + # <%= render :partial => 'person', :collection => @people %> + # </div> + # + # # Insert a new person + # # + # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, ""); + # page.insert_html :bottom, :partial => 'person', :object => @person + # + # # Replace an existing person + # + # # Generates: Element.replace("person_45", "-- Contents of partial --"); + # page.replace 'person_45', :partial => 'person', :object => @person + # + def replace(id, *options_for_render) + call 'Element.replace', id, render(*options_for_render) + end + + # Removes the DOM elements with the given +ids+ from the page. + # + # Example: + # + # # Remove a few people + # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove); + # page.remove 'person_23', 'person_9', 'person_2' + # + def remove(*ids) + loop_on_multiple_args 'Element.remove', ids + end + + # Shows hidden DOM elements with the given +ids+. + # + # Example: + # + # # Show a few people + # # Generates: ["person_6", "person_13", "person_223"].each(Element.show); + # page.show 'person_6', 'person_13', 'person_223' + # + def show(*ids) + loop_on_multiple_args 'Element.show', ids + end + + # Hides the visible DOM elements with the given +ids+. + # + # Example: + # + # # Hide a few people + # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide); + # page.hide 'person_29', 'person_9', 'person_0' + # + def hide(*ids) + loop_on_multiple_args 'Element.hide', ids + end + + # Toggles the visibility of the DOM elements with the given +ids+. + # Example: + # + # # Show a few people + # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle); + # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements + # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements + # + def toggle(*ids) + loop_on_multiple_args 'Element.toggle', ids + end + + # Displays an alert dialog with the given +message+. + # + # Example: + # + # # Generates: alert('This message is from Rails!') + # page.alert('This message is from Rails!') + def alert(message) + call 'alert', message + end + + # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+. + # + # Examples: + # + # # Generates: window.location.href = "/mycontroller"; + # page.redirect_to(:action => 'index') + # + # # Generates: window.location.href = "/account/signup"; + # page.redirect_to(:controller => 'account', :action => 'signup') + def redirect_to(location) + assign 'window.location.href', @context.url_for(location) + end + + # Calls the JavaScript +function+, optionally with the given +arguments+. + # + # If a block is given, the block will be passed to a new JavaScriptGenerator; + # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt> + # and passed as the called function's final argument. + # + # Examples: + # + # # Generates: Element.replace(my_element, "My content to replace with.") + # page.call 'Element.replace', 'my_element', "My content to replace with." + # + # # Generates: alert('My message!') + # page.call 'alert', 'My message!' + # + def call(function, *arguments, &block) + record "#{function}(#{arguments_for_call(arguments, block)})" + end + + # Assigns the JavaScript +variable+ the given +value+. + # + # Examples: + # + # # Generates: my_string = "This is mine!"; + # page.assign 'my_string', 'This is mine!' + # + # # Generates: record_count = 33; + # page.assign 'record_count', 33 + # + # # Generates: tabulated_total = 47 + # page.assign 'tabulated_total', @total_from_cart + # + def assign(variable, value) + record "#{variable} = #{javascript_object_for(value)}" + end + + # Writes raw JavaScript to the page. + # + # Example: + # + # page << "alert('JavaScript with Prototype.');" + def <<(javascript) + @lines << javascript + end + + # Executes the content of the block after a delay of +seconds+. Example: + # + # # Generates: + # # setTimeout(function() { + # # ; + # # new Effect.Fade("notice",{}); + # # }, 20000); + # page.delay(20) do + # page.visual_effect :fade, 'notice' + # end + def delay(seconds = 1) + record "setTimeout(function() {\n\n" + yield + record "}, #{(seconds * 1000).to_i})" + end + + # Starts a script.aculo.us visual effect. See + # ActionView::Helpers::ScriptaculousHelper for more information. + def visual_effect(name, id = nil, options = {}) + record @context.send(:visual_effect, name, id, options) + end + + # Creates a script.aculo.us sortable element. Useful + # to recreate sortable elements after items get added + # or deleted. + # See ActionView::Helpers::ScriptaculousHelper for more information. + def sortable(id, options = {}) + record @context.send(:sortable_element_js, id, options) + end + + # Creates a script.aculo.us draggable element. + # See ActionView::Helpers::ScriptaculousHelper for more information. + def draggable(id, options = {}) + record @context.send(:draggable_element_js, id, options) + end + + # Creates a script.aculo.us drop receiving element. + # See ActionView::Helpers::ScriptaculousHelper for more information. + def drop_receiving(id, options = {}) + record @context.send(:drop_receiving_element_js, id, options) + end + + private + def loop_on_multiple_args(method, ids) + record(ids.size>1 ? + "#{javascript_object_for(ids)}.each(#{method})" : + "#{method}(#{ids.first.to_json})") + end + + def page + self + end + + def record(line) + returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do + self << line + end + end + + def render(*options_for_render) + old_format = @context && @context.template_format + @context.template_format = :html if @context + Hash === options_for_render.first ? + @context.render(*options_for_render) : + options_for_render.first.to_s + ensure + @context.template_format = old_format if @context + end + + def javascript_object_for(object) + object.respond_to?(:to_json) ? object.to_json : object.inspect + end + + def arguments_for_call(arguments, block = nil) + arguments << block_to_function(block) if block + arguments.map { |argument| javascript_object_for(argument) }.join ', ' + end + + def block_to_function(block) + generator = self.class.new(@context, &block) + literal("function() { #{generator.to_s} }") + end + + def method_missing(method, *arguments) + JavaScriptProxy.new(self, method.to_s.camelize) + end + end + end + + # Yields a JavaScriptGenerator and returns the generated JavaScript code. + # Use this to update multiple elements on a page in an Ajax response. + # See JavaScriptGenerator for more information. + # + # Example: + # + # update_page do |page| + # page.hide 'spinner' + # end + def update_page(&block) + JavaScriptGenerator.new(@template, &block).to_s + end + + # Works like update_page but wraps the generated JavaScript in a <script> + # tag. Use this to include generated JavaScript in an ERb template. + # See JavaScriptGenerator for more information. + # + # +html_options+ may be a hash of <script> attributes to be passed + # to ActionView::Helpers::JavaScriptHelper#javascript_tag. + def update_page_tag(html_options = {}, &block) + javascript_tag update_page(&block), html_options + end + + protected + def options_for_ajax(options) + js_options = build_callbacks(options) + + js_options['asynchronous'] = options[:type] != :synchronous + js_options['method'] = method_option_to_s(options[:method]) if options[:method] + js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position] + js_options['evalScripts'] = options[:script].nil? || options[:script] + + if options[:form] + js_options['parameters'] = 'Form.serialize(this)' + elsif options[:submit] + js_options['parameters'] = "Form.serialize('#{options[:submit]}')" + elsif options[:with] + js_options['parameters'] = options[:with] + end + + if protect_against_forgery? + if js_options['parameters'] + js_options['parameters'] << " + '&" + else + js_options['parameters'] = "'" + end + js_options['parameters'] << "#{request_forgery_protection_token}=' + encodeURIComponent('#{escape_javascript form_authenticity_token}')" + end + + options_for_javascript(js_options) + end + + def method_option_to_s(method) + (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'" + end + + def build_observer(klass, name, options = {}) + if options[:with] && (options[:with] !~ /[\{=(.]/) + options[:with] = "'#{options[:with]}=' + value" + else + options[:with] ||= 'value' unless options[:function] + end + + callback = options[:function] || remote_function(options) + javascript = "new #{klass}('#{name}', " + javascript << "#{options[:frequency]}, " if options[:frequency] + javascript << "function(element, value) {" + javascript << "#{callback}}" + javascript << ", '#{options[:on]}'" if options[:on] + javascript << ")" + javascript_tag(javascript) + end + + def build_callbacks(options) + callbacks = {} + options.each do |callback, code| + if CALLBACKS.include?(callback) + name = 'on' + callback.to_s.capitalize + callbacks[name] = "function(request){#{code}}" + end + end + callbacks + end + end + + # Converts chained method calls on DOM proxy elements into JavaScript chains + class JavaScriptProxy < BasicObject #:nodoc: + def initialize(generator, root = nil) + @generator = generator + @generator << root if root + end + + private + def method_missing(method, *arguments, &block) + if method.to_s =~ /(.*)=$/ + assign($1, arguments.first) + else + call("#{method.to_s.camelize(:lower)}", *arguments, &block) + end + end + + def call(function, *arguments, &block) + append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})") + self + end + + def assign(variable, value) + append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}") + end + + def function_chain + @function_chain ||= @generator.instance_variable_get(:@lines) + end + + def append_to_function_chain!(call) + function_chain[-1].chomp!(';') + function_chain[-1] += ".#{call};" + end + end + + class JavaScriptElementProxy < JavaScriptProxy #:nodoc: + def initialize(generator, id) + @id = id + super(generator, "$(#{id.to_json})") + end + + # Allows access of element attributes through +attribute+. Examples: + # + # page['foo']['style'] # => $('foo').style; + # page['foo']['style']['color'] # => $('blank_slate').style.color; + # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red'; + # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red'; + def [](attribute) + append_to_function_chain!(attribute) + self + end + + def []=(variable, value) + assign(variable, value) + end + + def replace_html(*options_for_render) + call 'update', @generator.send(:render, *options_for_render) + end + + def replace(*options_for_render) + call 'replace', @generator.send(:render, *options_for_render) + end + + def reload(options_for_replace = {}) + replace(options_for_replace.merge({ :partial => @id.to_s })) + end + + end + + class JavaScriptVariableProxy < JavaScriptProxy #:nodoc: + def initialize(generator, variable) + @variable = variable + @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks + super(generator) + end + + # The JSON Encoder calls this to check for the #to_json method + # Since it's a blank slate object, I suppose it responds to anything. + def respond_to?(method) + true + end + + def to_json(options = nil) + @variable + end + + private + def append_to_function_chain!(call) + @generator << @variable if @empty + @empty = false + super + end + end + + class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc: + ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by, :in_groups_of, :each_slice] unless defined? ENUMERABLE_METHODS_WITH_RETURN + ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS + attr_reader :generator + delegate :arguments_for_call, :to => :generator + + def initialize(generator, pattern) + super(generator, @pattern = pattern) + end + + def each_slice(variable, number, &block) + if block + enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block + else + add_variable_assignment!(variable) + append_enumerable_function!("eachSlice(#{number.to_json});") + end + end + + def grep(variable, pattern, &block) + enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block + end + + def in_groups_of(variable, number, fill_with = nil) + arguments = [number] + arguments << fill_with unless fill_with.nil? + add_variable_assignment!(variable) + append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});") + end + + def inject(variable, memo, &block) + enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block + end + + def pluck(variable, property) + add_variable_assignment!(variable) + append_enumerable_function!("pluck(#{property.to_json});") + end + + def zip(variable, *arguments, &block) + add_variable_assignment!(variable) + append_enumerable_function!("zip(#{arguments_for_call arguments}") + if block + function_chain[-1] += ", function(array) {" + yield ActiveSupport::JSON::Variable.new('array') + add_return_statement! + @generator << '});' + else + function_chain[-1] += ');' + end + end + + private + def method_missing(method, *arguments, &block) + if ENUMERABLE_METHODS.include?(method) + returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method) + variable = arguments.first if returnable + enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block) + else + super + end + end + + # Options + # * variable - name of the variable to set the result of the enumeration to + # * method_args - array of the javascript enumeration method args that occur before the function + # * yield_args - array of the javascript yield args + # * return - true if the enumeration should return the last statement + def enumerate(enumerable, options = {}, &block) + options[:method_args] ||= [] + options[:yield_args] ||= [] + yield_args = options[:yield_args] * ', ' + method_args = arguments_for_call options[:method_args] # foo, bar, function + method_args << ', ' unless method_args.blank? + add_variable_assignment!(options[:variable]) if options[:variable] + append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {") + # only yield as many params as were passed in the block + yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1]) + add_return_statement! if options[:return] + @generator << '});' + end + + def add_variable_assignment!(variable) + function_chain.push("var #{variable} = #{function_chain.pop}") + end + + def add_return_statement! + unless function_chain.last =~ /return/ + function_chain.push("return #{function_chain.pop.chomp(';')};") + end + end + + def append_enumerable_function!(call) + function_chain[-1].chomp!(';') + function_chain[-1] += ".#{call}" + end + end + + class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\ + def initialize(generator, pattern) + super(generator, "$$(#{pattern.to_json})") + end + end + end +end + +require 'action_view/helpers/javascript_helper' diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb new file mode 100644 index 000000000..6c235bff3 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_identification_helper.rb @@ -0,0 +1,20 @@ +module ActionView + module Helpers + module RecordIdentificationHelper + # See ActionController::RecordIdentifier.partial_path -- this is just a delegate to that for convenient access in the view. + def partial_path(*args, &block) + ActionController::RecordIdentifier.partial_path(*args, &block) + end + + # See ActionController::RecordIdentifier.dom_class -- this is just a delegate to that for convenient access in the view. + def dom_class(*args, &block) + ActionController::RecordIdentifier.dom_class(*args, &block) + end + + # See ActionController::RecordIdentifier.dom_id -- this is just a delegate to that for convenient access in the view. + def dom_id(*args, &block) + ActionController::RecordIdentifier.dom_id(*args, &block) + end + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb new file mode 100644 index 000000000..7b82c38a0 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -0,0 +1,59 @@ +module ActionView + module Helpers + module RecordTagHelper + # Produces a wrapper DIV element with id and class parameters that + # relate to the specified ActiveRecord object. Usage example: + # + # <% div_for(@person, :class => "foo") do %> + # <%=h @person.name %> + # <% end %> + # + # produces: + # + # <div id="person_123" class="person foo"> Joe Bloggs </div> + # + def div_for(record, *args, &block) + content_tag_for(:div, record, *args, &block) + end + + # content_tag_for creates an HTML element with id and class parameters + # that relate to the specified ActiveRecord object. For example: + # + # <% content_tag_for(:tr, @person) do %> + # <td><%=h @person.first_name %></td> + # <td><%=h @person.last_name %></td> + # <% end %> + # + # would produce hthe following HTML (assuming @person is an instance of + # a Person object, with an id value of 123): + # + # <tr id="person_123" class="person">....</tr> + # + # If you require the HTML id attribute to have a prefix, you can specify it: + # + # <% content_tag_for(:tr, @person, :foo) do %> ... + # + # produces: + # + # <tr id="foo_person_123" class="person">... + # + # content_tag_for also accepts a hash of options, which will be converted to + # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined + # with the default class name for your object. For example: + # + # <% content_tag_for(:li, @person, :class => "bar") %>... + # + # produces: + # + # <li id="person_123" class="person bar">... + # + def content_tag_for(tag_name, record, *args, &block) + prefix = args.first.is_a?(Hash) ? nil : args.shift + options = args.first.is_a?(Hash) ? args.shift : {} + concat content_tag(tag_name, capture(&block), + options.merge({ :class => "#{dom_class(record)} #{options[:class]}".strip, :id => dom_id(record, prefix) })), + block.binding + end + end + end +end
\ No newline at end of file diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb new file mode 100644 index 000000000..47fbe3a27 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -0,0 +1,223 @@ +require 'action_view/helpers/tag_helper' +require 'html/document' + +module ActionView + module Helpers #:nodoc: + # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. + # These helper methods extend ActionView making them callable within your template files. + module SanitizeHelper + def self.included(base) + base.extend(ClassMethods) + end + + # This #sanitize helper will html encode all tags and strip all attributes that aren't specifically allowed. + # It also strips href/src tags with invalid protocols, like javascript: especially. It does its best to counter any + # tricks that hackers may use, like throwing in unicode/ascii/hex values to get past the javascript: filters. Check out + # the extensive test suite. + # + # <%= sanitize @article.body %> + # + # You can add or remove tags/attributes if you want to customize it a bit. See ActionView::Base for full docs on the + # available options. You can add tags/attributes for single uses of #sanitize by passing either the :attributes or :tags options: + # + # Normal Use + # + # <%= sanitize @article.body %> + # + # Custom Use (only the mentioned tags and attributes are allowed, nothing else) + # + # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style) + # + # Add table tags to the default allowed tags + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' + # end + # + # Remove tags to the default allowed tags + # + # Rails::Initializer.run do |config| + # config.after_initialize do + # ActionView::Base.sanitized_allowed_tags.delete 'div' + # end + # end + # + # Change allowed default attributes + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style' + # end + # + def sanitize(html, options = {}) + self.class.white_list_sanitizer.sanitize(html, options) + end + + # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute + def sanitize_css(style) + self.class.white_list_sanitizer.sanitize_css(style) + end + + # Strips all HTML tags from the +html+, including comments. This uses the + # html-scanner tokenizer and so its HTML parsing ability is limited by + # that of html-scanner. + # + # ==== Examples + # + # strip_tags("Strip <i>these</i> tags!") + # # => Strip these tags! + # + # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...") + # # => Bold no more! See more here... + # + # strip_tags("<div id='top-bar'>Welcome to my website!</div>") + # # => Welcome to my website! + def strip_tags(html) + self.class.full_sanitizer.sanitize(html) + end + + # Strips all link tags from +text+ leaving just the link text. + # + # ==== Examples + # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') + # # => Ruby on Rails + # + # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.') + # # => Please e-mail me at me@email.com. + # + # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') + # # => Blog: Visit + def strip_links(html) + self.class.link_sanitizer.sanitize(html) + end + + module ClassMethods #:nodoc: + def self.extended(base) + class << base + attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer + + # we want these to be class methods on ActionView::Base, they'll get mattr_readers for these below. + helper_def = [:sanitized_protocol_separator, :sanitized_uri_attributes, :sanitized_bad_tags, :sanitized_allowed_tags, + :sanitized_allowed_attributes, :sanitized_allowed_css_properties, :sanitized_allowed_css_keywords, + :sanitized_shorthand_css_properties, :sanitized_allowed_protocols, :sanitized_protocol_separator=].collect! do |prop| + prop = prop.to_s + "def #{prop}(#{:value if prop =~ /=$/}) white_list_sanitizer.#{prop.sub /sanitized_/, ''} #{:value if prop =~ /=$/} end" + end.join("\n") + eval helper_def + end + end + + # Gets the HTML::FullSanitizer instance used by strip_tags. Replace with + # any object that responds to #sanitize + # + # Rails::Initializer.run do |config| + # config.action_view.full_sanitizer = MySpecialSanitizer.new + # end + # + def full_sanitizer + @full_sanitizer ||= HTML::FullSanitizer.new + end + + # Gets the HTML::LinkSanitizer instance used by strip_links. Replace with + # any object that responds to #sanitize + # + # Rails::Initializer.run do |config| + # config.action_view.link_sanitizer = MySpecialSanitizer.new + # end + # + def link_sanitizer + @link_sanitizer ||= HTML::LinkSanitizer.new + end + + # Gets the HTML::WhiteListSanitizer instance used by sanitize and sanitize_css. + # Replace with any object that responds to #sanitize + # + # Rails::Initializer.run do |config| + # config.action_view.white_list_sanitizer = MySpecialSanitizer.new + # end + # + def white_list_sanitizer + @white_list_sanitizer ||= HTML::WhiteListSanitizer.new + end + + # Adds valid HTML attributes that the #sanitize helper checks for URIs. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target' + # end + # + def sanitized_uri_attributes=(attributes) + HTML::WhiteListSanitizer.uri_attributes.merge(attributes) + end + + # Adds to the Set of 'bad' tags for the #sanitize helper. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_bad_tags = 'embed', 'object' + # end + # + def sanitized_bad_tags=(attributes) + HTML::WhiteListSanitizer.bad_tags.merge(attributes) + end + # Adds to the Set of allowed tags for the #sanitize helper. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' + # end + # + def sanitized_allowed_tags=(attributes) + HTML::WhiteListSanitizer.allowed_tags.merge(attributes) + end + + # Adds to the Set of allowed html attributes for the #sanitize helper. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc' + # end + # + def sanitized_allowed_attributes=(attributes) + HTML::WhiteListSanitizer.allowed_attributes.merge(attributes) + end + + # Adds to the Set of allowed css properties for the #sanitize and #sanitize_css heleprs. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_css_properties = 'expression' + # end + # + def sanitized_allowed_css_properties=(attributes) + HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes) + end + + # Adds to the Set of allowed css keywords for the #sanitize and #sanitize_css helpers. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_css_keywords = 'expression' + # end + # + def sanitized_allowed_css_keywords=(attributes) + HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes) + end + + # Adds to the Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_shorthand_css_properties = 'expression' + # end + # + def sanitized_shorthand_css_properties=(attributes) + HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes) + end + + # Adds to the Set of allowed protocols for the #sanitize helper. + # + # Rails::Initializer.run do |config| + # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed' + # end + # + def sanitized_allowed_protocols=(attributes) + HTML::WhiteListSanitizer.allowed_protocols.merge(attributes) + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb new file mode 100644 index 000000000..f7a9daf60 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/scriptaculous_helper.rb @@ -0,0 +1,199 @@ +require 'action_view/helpers/javascript_helper' + +module ActionView + module Helpers + # Provides a set of helpers for calling Scriptaculous JavaScript + # functions, including those which create Ajax controls and visual effects. + # + # To be able to use these helpers, you must include the Prototype + # JavaScript framework and the Scriptaculous JavaScript library in your + # pages. See the documentation for ActionView::Helpers::JavaScriptHelper + # for more information on including the necessary JavaScript. + # + # The Scriptaculous helpers' behavior can be tweaked with various options. + # See the documentation at http://script.aculo.us for more information on + # using these helpers in your application. + module ScriptaculousHelper + unless const_defined? :TOGGLE_EFFECTS + TOGGLE_EFFECTS = [:toggle_appear, :toggle_slide, :toggle_blind] + end + + # Returns a JavaScript snippet to be used on the Ajax callbacks for + # starting visual effects. + # + # Example: + # <%= link_to_remote "Reload", :update => "posts", + # :url => { :action => "reload" }, + # :complete => visual_effect(:highlight, "posts", :duration => 0.5) + # + # If no element_id is given, it assumes "element" which should be a local + # variable in the generated JavaScript execution context. This can be + # used for example with drop_receiving_element: + # + # <%= drop_receiving_element (...), :loading => visual_effect(:fade) %> + # + # This would fade the element that was dropped on the drop receiving + # element. + # + # For toggling visual effects, you can use :toggle_appear, :toggle_slide, and + # :toggle_blind which will alternate between appear/fade, slidedown/slideup, and + # blinddown/blindup respectively. + # + # You can change the behaviour with various options, see + # http://script.aculo.us for more documentation. + def visual_effect(name, element_id = false, js_options = {}) + element = element_id ? element_id.to_json : "element" + + js_options[:queue] = if js_options[:queue].is_a?(Hash) + '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}' + elsif js_options[:queue] + "'#{js_options[:queue]}'" + end if js_options[:queue] + + [:endcolor, :direction, :startcolor, :scaleMode, :restorecolor].each do |option| + js_options[option] = "'#{js_options[option]}'" if js_options[option] + end + + if TOGGLE_EFFECTS.include? name.to_sym + "Effect.toggle(#{element},'#{name.to_s.gsub(/^toggle_/,'')}',#{options_for_javascript(js_options)});" + else + "new Effect.#{name.to_s.camelize}(#{element},#{options_for_javascript(js_options)});" + end + end + + # Makes the element with the DOM ID specified by +element_id+ sortable + # by drag-and-drop and make an Ajax call whenever the sort order has + # changed. By default, the action called gets the serialized sortable + # element as parameters. + # + # Example: + # <%= sortable_element("my_list", :url => { :action => "order" }) %> + # + # In the example, the action gets a "my_list" array parameter + # containing the values of the ids of elements the sortable consists + # of, in the current order. + # + # Important: For this to work, the sortable elements must have id + # attributes in the form "string_identifier". For example, "item_1". Only + # the identifier part of the id attribute will be serialized. + # + # Additional +options+ are: + # + # <tt>:format</tt>:: A regular expression to determine what to send + # as the serialized id to the server (the default + # is <tt>/^[^_]*_(.*)$/</tt>). + # + # <tt>:constraint</tt>:: Whether to constrain the dragging to either <tt>:horizontal</tt> + # or <tt>:vertical</tt> (or false to make it unconstrained). + # + # <tt>:overlap</tt>:: Calculate the item overlap in the <tt>:horizontal</tt> or + # <tt>:vertical</tt> direction. + # + # <tt>:tag</tt>:: Which children of the container element to treat as + # sortable (default is <tt>li</tt>). + # + # <tt>:containment</tt>:: Takes an element or array of elements to treat as + # potential drop targets (defaults to the original + # target element). + # + # <tt>:only</tt>:: A CSS class name or arry of class names used to filter + # out child elements as candidates. + # + # <tt>:scroll</tt>:: Determines whether to scroll the list during drag + # operationsif the list runs past the visual border. + # + # <tt>:tree</tt>:: Determines whether to treat nested lists as part of the + # main sortable list. This means that you can create multi- + # layer lists, and not only sort items at the same level, + # but drag and sort items between levels. + # + # <tt>:hoverclass</tt>:: If set, the Droppable will have this additional CSS class + # when an accepted Draggable is hovered over it. + # + # <tt>:handle</tt>:: Sets whether the element should only be draggable by an + # embedded handle. The value may be a string referencing a + # CSS class value (as of script.aculo.us V1.5). The first + # child/grandchild/etc. element found within the element + # that has this CSS class value will be used as the handle. + # + # <tt>:ghosting</tt>:: Clones the element and drags the clone, leaving the original + # in place until the clone is dropped (defaut is <tt>false</tt>). + # + # <tt>:dropOnEmpty</tt>:: If set to true, the Sortable container will be made into + # a Droppable, that can receive a Draggable (as according to + # the containment rules) as a child element when there are no + # more elements inside (defaut is <tt>false</tt>). + # + # <tt>:onChange</tt>:: Called whenever the sort order changes while dragging. When + # dragging from one Sortable to another, the callback is + # called once on each Sortable. Gets the affected element as + # its parameter. + # + # <tt>:onUpdate</tt>:: Called when the drag ends and the Sortable's order is + # changed in any way. When dragging from one Sortable to + # another, the callback is called once on each Sortable. Gets + # the container as its parameter. + # + # See http://script.aculo.us for more documentation. + def sortable_element(element_id, options = {}) + javascript_tag(sortable_element_js(element_id, options).chop!) + end + + def sortable_element_js(element_id, options = {}) #:nodoc: + options[:with] ||= "Sortable.serialize(#{element_id.to_json})" + options[:onUpdate] ||= "function(){" + remote_function(options) + "}" + options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) } + + [:tag, :overlap, :constraint, :handle].each do |option| + options[option] = "'#{options[option]}'" if options[option] + end + + options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment] + options[:only] = array_or_string_for_javascript(options[:only]) if options[:only] + + %(Sortable.create(#{element_id.to_json}, #{options_for_javascript(options)});) + end + + # Makes the element with the DOM ID specified by +element_id+ draggable. + # + # Example: + # <%= draggable_element("my_image", :revert => true) + # + # You can change the behaviour with various options, see + # http://script.aculo.us for more documentation. + def draggable_element(element_id, options = {}) + javascript_tag(draggable_element_js(element_id, options).chop!) + end + + def draggable_element_js(element_id, options = {}) #:nodoc: + %(new Draggable(#{element_id.to_json}, #{options_for_javascript(options)});) + end + + # Makes the element with the DOM ID specified by +element_id+ receive + # dropped draggable elements (created by draggable_element). + # and make an AJAX call By default, the action called gets the DOM ID + # of the element as parameter. + # + # Example: + # <%= drop_receiving_element("my_cart", :url => + # { :controller => "cart", :action => "add" }) %> + # + # You can change the behaviour with various options, see + # http://script.aculo.us for more documentation. + def drop_receiving_element(element_id, options = {}) + javascript_tag(drop_receiving_element_js(element_id, options).chop!) + end + + def drop_receiving_element_js(element_id, options = {}) #:nodoc: + options[:with] ||= "'id=' + encodeURIComponent(element.id)" + options[:onDrop] ||= "function(element){" + remote_function(options) + "}" + options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) } + + options[:accept] = array_or_string_for_javascript(options[:accept]) if options[:accept] + options[:hoverclass] = "'#{options[:hoverclass]}'" if options[:hoverclass] + + %(Droppables.add(#{element_id.to_json}, #{options_for_javascript(options)});) + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb new file mode 100644 index 000000000..999cbfb52 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/tag_helper.rb @@ -0,0 +1,133 @@ +require 'cgi' +require 'erb' + +module ActionView + module Helpers #:nodoc: + # Provides methods to generate HTML tags programmatically when you can't use + # a Builder. By default, they output XHTML compliant tags. + module TagHelper + include ERB::Util + + BOOLEAN_ATTRIBUTES = Set.new(%w(disabled readonly multiple)) + + # Returns an empty HTML tag of type +name+ which by default is XHTML + # compliant. Set +open+ to true to create an open tag compatible + # with HTML 4.0 and below. Add HTML attributes by passing an attributes + # hash to +options+. Set +escape+ to false to disable attribute value + # escaping. + # + # ==== Options + # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use + # symbols or strings for the attribute names. + # + # ==== Examples + # tag("br") + # # => <br /> + # + # tag("br", nil, true) + # # => <br> + # + # tag("input", { :type => 'text', :disabled => true }) + # # => <input type="text" disabled="disabled" /> + # + # tag("img", { :src => "open & shut.png" }) + # # => <img src="open & shut.png" /> + # + # tag("img", { :src => "open & shut.png" }, false, false) + # # => <img src="open & shut.png" /> + def tag(name, options = nil, open = false, escape = true) + "<#{name}#{tag_options(options, escape) if options}" + (open ? ">" : " />") + end + + # Returns an HTML block tag of type +name+ surrounding the +content+. Add + # HTML attributes by passing an attributes hash to +options+. + # Instead of passing the content as an argument, you can also use a block + # in which case, you pass your +options+ as the second parameter. + # Set escape to false to disable attribute value escaping. + # + # ==== Options + # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and + # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use + # symbols or strings for the attribute names. + # + # ==== Examples + # content_tag(:p, "Hello world!") + # # => <p>Hello world!</p> + # content_tag(:div, content_tag(:p, "Hello world!"), :class => "strong") + # # => <div class="strong"><p>Hello world!</p></div> + # content_tag("select", options, :multiple => true) + # # => <select multiple="multiple">...options...</select> + # + # <% content_tag :div, :class => "strong" do -%> + # Hello world! + # <% end -%> + # # => <div class="strong"><p>Hello world!</p></div> + def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) + if block_given? + options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) + content = capture(&block) + content_tag = content_tag_string(name, content, options, escape) + block_is_within_action_view?(block) ? concat(content_tag, block.binding) : content_tag + else + content = content_or_options_with_block + content_tag_string(name, content, options, escape) + end + end + + # Returns a CDATA section with the given +content+. CDATA sections + # are used to escape blocks of text containing characters which would + # otherwise be recognized as markup. CDATA sections begin with the string + # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>. + # + # ==== Examples + # cdata_section("<hello world>") + # # => <![CDATA[<hello world>]]> + # + # cdata_section(File.read("hello_world.txt")) + # # => <![CDATA[<hello from a text file]]> + def cdata_section(content) + "<![CDATA[#{content}]]>" + end + + # Returns an escaped version of +html+ without affecting existing escaped entities. + # + # ==== Examples + # escape_once("1 > 2 & 3") + # # => "1 < 2 & 3" + # + # escape_once("<< Accept & Checkout") + # # => "<< Accept & Checkout" + def escape_once(html) + html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } + end + + private + def content_tag_string(name, content, options, escape = true) + tag_options = tag_options(options, escape) if options + "<#{name}#{tag_options}>#{content}</#{name}>" + end + + def tag_options(options, escape = true) + unless options.blank? + attrs = [] + if escape + options.each do |key, value| + next unless value + key = key.to_s + value = BOOLEAN_ATTRIBUTES.include?(key) ? key : escape_once(value) + attrs << %(#{key}="#{value}") + end + else + attrs = options.map { |key, value| %(#{key}="#{value}") } + end + " #{attrs.sort * ' '}" unless attrs.empty? + end + end + + def block_is_within_action_view?(block) + eval("defined? _erbout", block.binding) + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb new file mode 100644 index 000000000..e5747f28f --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/text_helper.rb @@ -0,0 +1,479 @@ +require 'action_view/helpers/tag_helper' +require 'html/document' + +module ActionView + module Helpers #:nodoc: + # The TextHelper module provides a set of methods for filtering, formatting + # and transforming strings, which can reduce the amount of inline Ruby code in + # your views. These helper methods extend ActionView making them callable + # within your template files. + module TextHelper + # The preferred method of outputting text in your views is to use the + # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods + # do not operate as expected in an eRuby code block. If you absolutely must + # output text within a non-output code block (i.e., <% %>), you can use the concat method. + # + # ==== Examples + # <% + # concat "hello", binding + # # is the equivalent of <%= "hello" %> + # + # if (logged_in == true): + # concat "Logged in!", binding + # else + # concat link_to('login', :action => login), binding + # end + # # will either display "Logged in!" or a login link + # %> + def concat(string, binding) + eval(ActionView::Base.erb_variable, binding) << string + end + + # If +text+ is longer than +length+, +text+ will be truncated to the length of + # +length+ (defaults to 30) and the last characters will be replaced with the +truncate_string+ + # (defaults to "..."). + # + # ==== Examples + # truncate("Once upon a time in a world far far away", 14) + # # => Once upon a... + # + # truncate("Once upon a time in a world far far away") + # # => Once upon a time in a world f... + # + # truncate("And they found that many people were sleeping better.", 25, "(clipped)") + # # => And they found that many (clipped) + # + # truncate("And they found that many people were sleeping better.", 15, "... (continued)") + # # => And they found... (continued) + def truncate(text, length = 30, truncate_string = "...") + if text.nil? then return end + l = length - truncate_string.chars.length + (text.chars.length > length ? text.chars[0...l] + truncate_string : text).to_s + end + + # Highlights one or more +phrases+ everywhere in +text+ by inserting it into + # a +highlighter+ string. The highlighter can be specialized by passing +highlighter+ + # as a single-quoted string with \1 where the phrase is to be inserted (defaults to + # '<strong class="highlight">\1</strong>') + # + # ==== Examples + # highlight('You searched for: rails', 'rails') + # # => You searched for: <strong class="highlight">rails</strong> + # + # highlight('You searched for: ruby, rails, dhh', 'actionpack') + # # => You searched for: ruby, rails, dhh + # + # highlight('You searched for: rails', ['for', 'rails'], '<em>\1</em>') + # # => You searched <em>for</em>: <em>rails</em> + # + # highlight('You searched for: rails', 'rails', "<a href='search?q=\1'>\1</a>") + # # => You searched for: <a href='search?q=rails>rails</a> + def highlight(text, phrases, highlighter = '<strong class="highlight">\1</strong>') + if text.blank? || phrases.blank? + text + else + match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') + text.gsub(/(#{match})/i, highlighter) + end + end + + # Extracts an excerpt from +text+ that matches the first instance of +phrase+. + # The +radius+ expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters + # defined in +radius+ (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, + # then the +excerpt_string+ will be prepended/appended accordingly. If the +phrase+ + # isn't found, nil is returned. + # + # ==== Examples + # excerpt('This is an example', 'an', 5) + # # => "...s is an examp..." + # + # excerpt('This is an example', 'is', 5) + # # => "This is an..." + # + # excerpt('This is an example', 'is') + # # => "This is an example" + # + # excerpt('This next thing is an example', 'ex', 2) + # # => "...next t..." + # + # excerpt('This is also an example', 'an', 8, '<chop> ') + # # => "<chop> is also an example" + def excerpt(text, phrase, radius = 100, excerpt_string = "...") + if text.nil? || phrase.nil? then return end + phrase = Regexp.escape(phrase) + + if found_pos = text.chars =~ /(#{phrase})/i + start_pos = [ found_pos - radius, 0 ].max + end_pos = [ found_pos + phrase.chars.length + radius, text.chars.length ].min + + prefix = start_pos > 0 ? excerpt_string : "" + postfix = end_pos < text.chars.length ? excerpt_string : "" + + prefix + text.chars[start_pos..end_pos].strip + postfix + else + nil + end + end + + # Attempts to pluralize the +singular+ word unless +count+ is 1. If +plural+ + # is supplied, it will use that when count is > 1, if the ActiveSupport Inflector + # is loaded, it will use the Inflector to determine the plural form, otherwise + # it will just add an 's' to the +singular+ word. + # + # ==== Examples + # pluralize(1, 'person') + # # => 1 person + # + # pluralize(2, 'person') + # # => 2 people + # + # pluralize(3, 'person', 'users') + # # => 3 users + # + # pluralize(0, 'person') + # # => 0 people + def pluralize(count, singular, plural = nil) + "#{count || 0} " + if count == 1 || count == '1' + singular + elsif plural + plural + elsif Object.const_defined?("Inflector") + Inflector.pluralize(singular) + else + singular + "s" + end + end + + # Wraps the +text+ into lines no longer than +line_width+ width. This method + # breaks on the first whitespace character that does not exceed +line_width+ + # (which is 80 by default). + # + # ==== Examples + # word_wrap('Once upon a time', 4) + # # => Once\nupon\na\ntime + # + # word_wrap('Once upon a time', 8) + # # => Once upon\na time + # + # word_wrap('Once upon a time') + # # => Once upon a time + # + # word_wrap('Once upon a time', 1) + # # => Once\nupon\na\ntime + def word_wrap(text, line_width = 80) + text.split("\n").collect do |line| + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line + end * "\n" + end + + begin + require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth) + + # Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags. + # + # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile]. + # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/] + # is available</i>. + # + # ==== Examples + # textilize("*This is Textile!* Rejoice!") + # # => "<p><strong>This is Textile!</strong> Rejoice!</p>" + # + # textilize("I _love_ ROR(Ruby on Rails)!") + # # => "<p>I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!</p>" + # + # textilize("h2. Textile makes markup -easy- simple!") + # # => "<h2>Textile makes markup <del>easy</del> simple!</h2>" + # + # textilize("Visit the Rails website "here":http://www.rubyonrails.org/.) + # # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>" + def textilize(text) + if text.blank? + "" + else + textilized = RedCloth.new(text, [ :hard_breaks ]) + textilized.hard_breaks = true if textilized.respond_to?("hard_breaks=") + textilized.to_html + end + end + + # Returns the text with all the Textile codes turned into HTML tags, + # but without the bounding <p> tag that RedCloth adds. + # + # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile]. + # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/] + # is available</i>. + # + # ==== Examples + # textilize_without_paragraph("*This is Textile!* Rejoice!") + # # => "<strong>This is Textile!</strong> Rejoice!" + # + # textilize_without_paragraph("I _love_ ROR(Ruby on Rails)!") + # # => "I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!" + # + # textilize_without_paragraph("h2. Textile makes markup -easy- simple!") + # # => "<h2>Textile makes markup <del>easy</del> simple!</h2>" + # + # textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.) + # # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>." + def textilize_without_paragraph(text) + textiled = textilize(text) + if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end + if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end + return textiled + end + rescue LoadError + # We can't really help what's not there + end + + begin + require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth) + + # Returns the text with all the Markdown codes turned into HTML tags. + # <i>This method is only available if BlueCloth[http://www.deveiate.org/projects/BlueCloth] + # is available</i>. + # + # ==== Examples + # markdown("We are using __Markdown__ now!") + # # => "<p>We are using <strong>Markdown</strong> now!</p>" + # + # markdown("We like to _write_ `code`, not just _read_ it!") + # # => "<p>We like to <em>write</em> <code>code</code>, not just <em>read</em> it!</p>" + # + # markdown("The [Markdown website](http://daringfireball.net/projects/markdown/) has more information.") + # # => "<p>The <a href="http://daringfireball.net/projects/markdown/">Markdown website</a> + # # has more information.</p>" + # + # markdown('') + # # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>' + def markdown(text) + text.blank? ? "" : BlueCloth.new(text).to_html + end + rescue LoadError + # We can't really help what's not there + end + + # Returns +text+ transformed into HTML using simple formatting rules. + # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a + # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is + # considered as a linebreak and a <tt><br /></tt> tag is appended. This + # method does not remove the newlines from the +text+. + # + # ==== Examples + # my_text = """Here is some basic text... + # ...with a line break.""" + # + # simple_format(my_text) + # # => "<p>Here is some basic text...<br />...with a line break.</p>" + # + # more_text = """We want to put a paragraph... + # + # ...right there.""" + # + # simple_format(more_text) + # # => "<p>We want to put a paragraph...</p><p>...right there.</p>" + def simple_format(text) + content_tag 'p', text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "</p>\n\n<p>"). # 2+ newline -> paragraph + gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br + end + + # Turns all URLs and e-mail addresses into clickable links. The +link+ parameter + # will limit what should be linked. You can add HTML attributes to the links using + # +href_options+. Options for +link+ are <tt>:all</tt> (default), + # <tt>:email_addresses</tt>, and <tt>:urls</tt>. If a block is given, each URL and + # e-mail address is yielded and the result is used as the link text. + # + # ==== Examples + # auto_link("Go to http://www.rubyonrails.org and say hello to david@loudthinking.com") + # # => "Go to <a href=\"http://www.rubyonrails.org\">http://www.rubyonrails.org</a> and + # # say hello to <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>" + # + # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :urls) + # # => "Visit <a href=\"http://www.loudthinking.com/\">http://www.loudthinking.com/</a> + # # or e-mail david@loudthinking.com" + # + # auto_link("Visit http://www.loudthinking.com/ or e-mail david@loudthinking.com", :email_addresses) + # # => "Visit http://www.loudthinking.com/ or e-mail <a href=\"mailto:david@loudthinking.com\">david@loudthinking.com</a>" + # + # post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com." + # auto_link(post_body, :all, :target => '_blank') do |text| + # truncate(text, 15) + # end + # # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>. + # Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>." + # + def auto_link(text, link = :all, href_options = {}, &block) + return '' if text.blank? + case link + when :all then auto_link_email_addresses(auto_link_urls(text, href_options, &block), &block) + when :email_addresses then auto_link_email_addresses(text, &block) + when :urls then auto_link_urls(text, href_options, &block) + end + end + + # Creates a Cycle object whose _to_s_ method cycles through elements of an + # array every time it is called. This can be used for example, to alternate + # classes for table rows. You can use named cycles to allow nesting in loops. + # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a + # named cycle. You can manually reset a cycle by calling reset_cycle and passing the + # name of the cycle. + # + # ==== Examples + # # Alternate CSS classes for even and odd numbers... + # @items = [1,2,3,4] + # <table> + # <% @items.each do |item| %> + # <tr class="<%= cycle("even", "odd") -%>"> + # <td>item</td> + # </tr> + # <% end %> + # </table> + # + # + # # Cycle CSS classes for rows, and text colors for values within each row + # @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'}, + # {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'}, + # {:first => 'June', :middle => 'Dae', :last => 'Jones'}] + # <% @items.each do |item| %> + # <tr class="<%= cycle("even", "odd", :name => "row_class") -%>"> + # <td> + # <% item.values.each do |value| %> + # <%# Create a named cycle "colors" %> + # <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>"> + # <%= value %> + # </span> + # <% end %> + # <% reset_cycle("colors") %> + # </td> + # </tr> + # <% end %> + def cycle(first_value, *values) + if (values.last.instance_of? Hash) + params = values.pop + name = params[:name] + else + name = "default" + end + values.unshift(first_value) + + cycle = get_cycle(name) + if (cycle.nil? || cycle.values != values) + cycle = set_cycle(name, Cycle.new(*values)) + end + return cycle.to_s + end + + # Resets a cycle so that it starts from the first element the next time + # it is called. Pass in +name+ to reset a named cycle. + # + # ==== Example + # # Alternate CSS classes for even and odd numbers... + # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]] + # <table> + # <% @items.each do |item| %> + # <tr class="<%= cycle("even", "odd") -%>"> + # <% item.each do |value| %> + # <span style="color:<%= cycle("#333", "#666", "#999", :name => "colors") -%>"> + # <%= value %> + # </span> + # <% end %> + # + # <% reset_cycle("colors") %> + # </tr> + # <% end %> + # </table> + def reset_cycle(name = "default") + cycle = get_cycle(name) + cycle.reset unless cycle.nil? + end + + class Cycle #:nodoc: + attr_reader :values + + def initialize(first_value, *values) + @values = values.unshift(first_value) + reset + end + + def reset + @index = 0 + end + + def to_s + value = @values[@index].to_s + @index = (@index + 1) % @values.size + return value + end + end + + private + # The cycle helpers need to store the cycles in a place that is + # guaranteed to be reset every time a page is rendered, so it + # uses an instance variable of ActionView::Base. + def get_cycle(name) + @_cycles = Hash.new unless defined?(@_cycles) + return @_cycles[name] + end + + def set_cycle(name, cycle_object) + @_cycles = Hash.new unless defined?(@_cycles) + @_cycles[name] = cycle_object + end + + AUTO_LINK_RE = %r{ + ( # leading text + <\w+.*?>| # leading HTML tag, or + [^=!:'"/]| # leading punctuation, or + ^ # beginning of line + ) + ( + (?:https?://)| # protocol spec, or + (?:www\.) # www.* + ) + ( + [-\w]+ # subdomain or domain + (?:\.[-\w]+)* # remaining subdomains or domain + (?::\d+)? # port + (?:/(?:(?:[~\w\+@%-]|(?:[,.;:][^\s$]))+)?)* # path + (?:\?[\w\+@%&=.;-]+)? # query string + (?:\#[\w\-]*)? # trailing anchor + ) + ([[:punct:]]|\s|<|$) # trailing text + }x unless const_defined?(:AUTO_LINK_RE) + + # Turns all urls into clickable links. If a block is given, each url + # is yielded and the result is used as the link text. + def auto_link_urls(text, href_options = {}) + extra_options = tag_options(href_options.stringify_keys) || "" + text.gsub(AUTO_LINK_RE) do + all, a, b, c, d = $&, $1, $2, $3, $4 + if a =~ /<a\s/i # don't replace URL's that are already linked + all + else + text = b + c + text = yield(text) if block_given? + %(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{text}</a>#{d}) + end + end + end + + # Turns all email addresses into clickable links. If a block is given, + # each email is yielded and the result is used as the link text. + def auto_link_email_addresses(text) + body = text.dup + text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do + text = $1 + + if body.match(/<a\b[^>]*>(.*)(#{Regexp.escape(text)})(.*)<\/a>/) + text + else + display_text = (block_given?) ? yield(text) : text + %{<a href="mailto:#{text}">#{display_text}</a>} + end + end + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb new file mode 100644 index 000000000..eb3391180 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/helpers/url_helper.rb @@ -0,0 +1,524 @@ +require 'action_view/helpers/javascript_helper' + +module ActionView + module Helpers #:nodoc: + # Provides a set of methods for making links and getting URLs that + # depend on the routing subsystem (see ActionController::Routing). + # This allows you to use the same format for links in views + # and controllers. + module UrlHelper + include JavaScriptHelper + + # Returns the URL for the set of +options+ provided. This takes the + # same options as url_for in ActionController (see the + # documentation for ActionController::Base#url_for). Note that by default + # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative /controller/action + # instead of the fully qualified URL like http://example.com/controller/action. + # + # When called from a view, url_for returns an HTML escaped url. If you + # need an unescaped url, pass :escape => false in the +options+. + # + # ==== Options + # * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path. + # * <tt>:only_path</tt> -- if true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified) + # * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this + # is currently not recommended since it breaks caching. + # * <tt>:host</tt> -- overrides the default (current) host if provided + # * <tt>:protocol</tt> -- overrides the default (current) protocol if provided + # * <tt>:user</tt> -- Inline HTTP authentication (only plucked out if :password is also present) + # * <tt>:password</tt> -- Inline HTTP authentication (only plucked out if :user is also present) + # * <tt>:escape</tt> -- Determines whether the returned URL will be HTML escaped or not (<tt>true</tt> by default) + # + # ==== Relying on named routes + # + # If you instead of a hash pass a record (like an Active Record or Active Resource) as the options parameter, + # you'll trigger the named route for that record. The lookup will happen on the name of the class. So passing + # a Workshop object will attempt to use the workshop_path route. If you have a nested route, such as + # admin_workshop_path you'll have to call that explicitly (it's impossible for url_for to guess that route). + # + # ==== Examples + # <%= url_for(:action => 'index') %> + # # => /blog/ + # + # <%= url_for(:action => 'find', :controller => 'books') %> + # # => /books/find + # + # <%= url_for(:action => 'login', :controller => 'members', :only_path => false, :protocol => 'https') %> + # # => https://www.railsapplication.com/members/login/ + # + # <%= url_for(:action => 'play', :anchor => 'player') %> + # # => /messages/play/#player + # + # <%= url_for(:action => 'checkout', :anchor => 'tax&ship') %> + # # => /testing/jump/#tax&ship + # + # <%= url_for(:action => 'checkout', :anchor => 'tax&ship', :escape => false) %> + # # => /testing/jump/#tax&ship + # + # <%= url_for(Workshop.new) %> + # # relies on Workshop answering a new_record? call (and in this case returning true) + # # => /workshops + # + # <%= url_for(@workshop) %> + # # calls @workshop.to_s + # # => /workshops/5 + def url_for(options = {}) + case options + when Hash + show_path = options[:host].nil? ? true : false + options = { :only_path => show_path }.update(options.symbolize_keys) + escape = options.key?(:escape) ? options.delete(:escape) : true + url = @controller.send(:url_for, options) + when String + escape = true + url = options + when NilClass + url = @controller.send(:url_for, nil) + else + escape = false + url = polymorphic_path(options) + end + + escape ? escape_once(url) : url + end + + # Creates a link tag of the given +name+ using a URL created by the set + # of +options+. See the valid options in the documentation for + # url_for. It's also possible to pass a string instead + # of an options hash to get a link tag that uses the value of the string as the + # href for the link, or use +:back+ to link to the referrer - a JavaScript back + # link will be used in place of a referrer if none exists. If nil is passed as + # a name, the link itself will become the name. + # + # ==== Options + # * <tt>:confirm => 'question?'</tt> -- This will add a JavaScript confirm + # prompt with the question specified. If the user accepts, the link is + # processed normally, otherwise no action is taken. + # * <tt>:popup => true || array of window options</tt> -- This will force the + # link to open in a popup window. By passing true, a default browser window + # will be opened with the URL. You can also specify an array of options + # that are passed-thru to JavaScripts window.open method. + # * <tt>:method => symbol of HTTP verb</tt> -- This modifier will dynamically + # create an HTML form and immediately submit the form for processing using + # the HTTP verb specified. Useful for having links perform a POST operation + # in dangerous actions like deleting a record (which search bots can follow + # while spidering your site). Supported verbs are :post, :delete and :put. + # Note that if the user has JavaScript disabled, the request will fall back + # to using GET. If you are relying on the POST behavior, you should check + # for it in your controller's action by using the request object's methods + # for post?, delete? or put?. + # * The +html_options+ will accept a hash of html attributes for the link tag. + # + # Note that if the user has JavaScript disabled, the request will fall back + # to using GET. If :href=>'#' is used and the user has JavaScript disabled + # clicking the link will have no effect. If you are relying on the POST + # behavior, your should check for it in your controller's action by using the + # request object's methods for post?, delete? or put?. + # + # You can mix and match the +html_options+ with the exception of + # :popup and :method which will raise an ActionView::ActionViewError + # exception. + # + # ==== Examples + # link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?" + # # => <a href="http://www.rubyonrails.org/" onclick="return confirm('Are you sure?');">Visit Other Site</a> + # + # link_to "Help", { :action => "help" }, :popup => true + # # => <a href="/testing/help/" onclick="window.open(this.href);return false;">Help</a> + # + # link_to "View Image", { :action => "view" }, :popup => ['new_window_name', 'height=300,width=600'] + # # => <a href="/testing/view/" onclick="window.open(this.href,'new_window_name','height=300,width=600');return false;">View Image</a> + # + # link_to "Delete Image", { :action => "delete", :id => @image.id }, :confirm => "Are you sure?", :method => :delete + # # => <a href="/testing/delete/9/" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form'); + # f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; + # var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); + # m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;">Delete Image</a> + def link_to(name, options = {}, html_options = nil) + url = case options + when String + options + when :back + @controller.request.env["HTTP_REFERER"] || 'javascript:history.back()' + else + self.url_for(options) + end + + if html_options + html_options = html_options.stringify_keys + href = html_options['href'] + convert_options_to_javascript!(html_options, url) + tag_options = tag_options(html_options) + else + tag_options = nil + end + + href_attr = "href=\"#{url}\"" unless href + "<a #{href_attr}#{tag_options}>#{name || url}</a>" + end + + # Generates a form containing a single button that submits to the URL created + # by the set of +options+. This is the safest method to ensure links that + # cause changes to your data are not triggered by search bots or accelerators. + # If the HTML button does not work with your layout, you can also consider + # using the link_to method with the <tt>:method</tt> modifier as described in + # the link_to documentation. + # + # The generated FORM element has a class name of <tt>button-to</tt> + # to allow styling of the form itself and its children. You can control + # the form submission and input element behavior using +html_options+. + # This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers + # described in the link_to documentation. If no <tt>:method</tt> modifier + # is given, it will default to performing a POST operation. You can also + # disable the button by passing <tt>:disabled => true</tt> in +html_options+. + # If you are using RESTful routes, you can pass the <tt>:method</tt> + # to change the HTTP verb used to submit the form. + # + # ==== Options + # The +options+ hash accepts the same options at url_for. + # + # There are a few special +html_options+: + # * <tt>:method</tt> -- specifies the anchor name to be appended to the path. + # * <tt>:disabled</tt> -- specifies the anchor name to be appended to the path. + # * <tt>:confirm</tt> -- This will add a JavaScript confirm + # prompt with the question specified. If the user accepts, the link is + # processed normally, otherwise no action is taken. + # + # ==== Examples + # <%= button_to "New", :action => "new" %> + # # => "<form method="post" action="/controller/new" class="button-to"> + # # <div><input value="New" type="submit" /></div> + # # </form>" + # + # button_to "Delete Image", { :action => "delete", :id => @image.id }, + # :confirm => "Are you sure?", :method => :delete + # # => "<form method="post" action="/images/delete/1" class="button-to"> + # # <div> + # # <input type="hidden" name="_method" value="delete" /> + # # <input onclick="return confirm('Are you sure?');" + # # value="Delete" type="submit" /> + # # </div> + # # </form>" + def button_to(name, options = {}, html_options = {}) + html_options = html_options.stringify_keys + convert_boolean_attributes!(html_options, %w( disabled )) + + method_tag = '' + if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s) + method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s) + end + + form_method = method.to_s == 'get' ? 'get' : 'post' + + request_token_tag = '' + if form_method == 'post' && protect_against_forgery? + request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) + end + + if confirm = html_options.delete("confirm") + html_options["onclick"] = "return #{confirm_javascript_function(confirm)};" + end + + url = options.is_a?(String) ? options : self.url_for(options) + name ||= url + + html_options.merge!("type" => "submit", "value" => name) + + "<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" + + method_tag + tag("input", html_options) + request_token_tag + "</div></form>" + end + + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ unless the current request URI is the same as the links, in + # which case only the name is returned (or the given block is yielded, if + # one exists). You can give link_to_unless_current a block which will + # specialize the default behavior (e.g., show a "Start Here" link rather + # than the link's text). + # + # ==== Examples + # Let's say you have a navigation menu... + # + # <ul id="navbar"> + # <li><%= link_to_unless_current("Home", { :action => "index" }) %></li> + # <li><%= link_to_unless_current("About Us", { :action => "about" }) %></li> + # </ul> + # + # If in the "about" action, it will render... + # + # <ul id="navbar"> + # <li><a href="/controller/index">Home</a></li> + # <li>About Us</li> + # </ul> + # + # ...but if in the "home" action, it will render: + # + # <ul id="navbar"> + # <li><a href="/controller/index">Home</a></li> + # <li><a href="/controller/about">About Us</a></li> + # </ul> + # + # The implicit block given to link_to_unless_current is evaluated if the current + # action is the action given. So, if we had a comments page and wanted to render a + # "Go Back" link instead of a link to the comments page, we could do something like this... + # + # <%= + # link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do + # link_to("Go back", { :controller => 'posts', :action => 'index' }) + # end + # %> + def link_to_unless_current(name, options = {}, html_options = {}, &block) + link_to_unless current_page?(options), name, options, html_options, &block + end + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ unless +condition+ is true, in which case only the name is + # returned. To specialize the default behavior (i.e., show a login link rather + # than just the plaintext link text), you can pass a block that + # accepts the name or the full argument list for link_to_unless. + # + # ==== Examples + # <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) %> + # # If the user is logged in... + # # => <a href="/controller/reply/">Reply</a> + # + # <%= + # link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name| + # link_to(name, { :controller => "accounts", :action => "signup" }) + # end + # %> + # # If the user is logged in... + # # => <a href="/controller/reply/">Reply</a> + # # If not... + # # => <a href="/accounts/signup">Reply</a> + def link_to_unless(condition, name, options = {}, html_options = {}, &block) + if condition + if block_given? + block.arity <= 1 ? yield(name) : yield(name, options, html_options) + else + name + end + else + link_to(name, options, html_options) + end + end + + # Creates a link tag of the given +name+ using a URL created by the set of + # +options+ if +condition+ is true, in which case only the name is + # returned. To specialize the default behavior, you can pass a block that + # accepts the name or the full argument list for link_to_unless (see the examples + # in link_to_unless). + # + # ==== Examples + # <%= link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) %> + # # If the user isn't logged in... + # # => <a href="/sessions/new/">Login</a> + # + # <%= + # link_to_if(@current_user.nil?, "Login", { :controller => "sessions", :action => "new" }) do + # link_to(@current_user.login, { :controller => "accounts", :action => "show", :id => @current_user }) + # end + # %> + # # If the user isn't logged in... + # # => <a href="/sessions/new/">Login</a> + # # If they are logged in... + # # => <a href="/accounts/show/3">my_username</a> + def link_to_if(condition, name, options = {}, html_options = {}, &block) + link_to_unless !condition, name, options, html_options, &block + end + + # Creates a mailto link tag to the specified +email_address+, which is + # also used as the name of the link unless +name+ is specified. Additional + # HTML attributes for the link can be passed in +html_options+. + # + # mail_to has several methods for hindering email harvesters and customizing + # the email itself by passing special keys to +html_options+. + # + # ==== Options + # * <tt>:encode</tt> - This key will accept the strings "javascript" or "hex". + # Passing "javascript" will dynamically create and encode the mailto: link then + # eval it into the DOM of the page. This method will not show the link on + # the page if the user has JavaScript disabled. Passing "hex" will hex + # encode the +email_address+ before outputting the mailto: link. + # * <tt>:replace_at</tt> - When the link +name+ isn't provided, the + # +email_address+ is used for the link label. You can use this option to + # obfuscate the +email_address+ by substituting the @ sign with the string + # given as the value. + # * <tt>:replace_dot</tt> - When the link +name+ isn't provided, the + # +email_address+ is used for the link label. You can use this option to + # obfuscate the +email_address+ by substituting the . in the email with the + # string given as the value. + # * <tt>:subject</tt> - Preset the subject line of the email. + # * <tt>:body</tt> - Preset the body of the email. + # * <tt>:cc</tt> - Carbon Copy addition recipients on the email. + # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. + # + # ==== Examples + # mail_to "me@domain.com" + # # => <a href="mailto:me@domain.com">me@domain.com</a> + # + # mail_to "me@domain.com", "My email", :encode => "javascript" + # # => <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script> + # + # mail_to "me@domain.com", "My email", :encode => "hex" + # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a> + # + # mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email" + # # => <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a> + # + # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com", + # :subject => "This is an example email" + # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a> + def mail_to(email_address, name = nil, html_options = {}) + html_options = html_options.stringify_keys + encode = html_options.delete("encode").to_s + cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body") + + string = '' + extras = '' + extras << "cc=#{CGI.escape(cc).gsub("+", "%20")}&" unless cc.nil? + extras << "bcc=#{CGI.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? + extras << "body=#{CGI.escape(body).gsub("+", "%20")}&" unless body.nil? + extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil? + extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? + + email_address = email_address.to_s + + email_address_obfuscated = email_address.dup + email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at") + email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") + + if encode == "javascript" + tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');" + for i in 0...tmp.length + string << sprintf("%%%x",tmp[i]) + end + "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>" + elsif encode == "hex" + email_address_encoded = '' + email_address_obfuscated.each_byte do |c| + email_address_encoded << sprintf("&#%d;", c) + end + + protocol = 'mailto:' + protocol.each_byte { |c| string << sprintf("&#%d;", c) } + + for i in 0...email_address.length + if email_address[i,1] =~ /\w/ + string << sprintf("%%%x",email_address[i]) + else + string << email_address[i,1] + end + end + content_tag "a", name || email_address_encoded, html_options.merge({ "href" => "#{string}#{extras}" }) + else + content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" }) + end + end + + # True if the current request URI was generated by the given +options+. + # + # ==== Examples + # Let's say we're in the <tt>/shop/checkout</tt> action. + # + # current_page?(:action => 'process') + # # => false + # + # current_page?(:controller => 'shop', :action => 'checkout') + # # => true + # + # current_page?(:action => 'checkout') + # # => true + # + # current_page?(:controller => 'library', :action => 'checkout') + # # => false + def current_page?(options) + url_string = CGI.escapeHTML(url_for(options)) + request = @controller.request + if url_string =~ /^\w+:\/\// + url_string == "#{request.protocol}#{request.host_with_port}#{request.request_uri}" + else + url_string == request.request_uri + end + end + + private + def convert_options_to_javascript!(html_options, url = '') + confirm, popup = html_options.delete("confirm"), html_options.delete("popup") + + method, href = html_options.delete("method"), html_options['href'] + + html_options["onclick"] = case + when popup && method + raise ActionView::ActionViewError, "You can't use :popup and :method in the same link" + when confirm && popup + "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;" + when confirm && method + "if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;" + when confirm + "return #{confirm_javascript_function(confirm)};" + when method + "#{method_javascript_function(method, url, href)}return false;" + when popup + popup_javascript_function(popup) + 'return false;' + else + html_options["onclick"] + end + end + + def confirm_javascript_function(confirm) + "confirm('#{escape_javascript(confirm)}')" + end + + def popup_javascript_function(popup) + popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);" + end + + def method_javascript_function(method, url = '', href = nil) + action = (href && url.size > 0) ? "'#{url}'" : 'this.href' + submit_function = + "var f = document.createElement('form'); f.style.display = 'none'; " + + "this.parentNode.appendChild(f); f.method = 'POST'; f.action = #{action};" + + unless method == :post + submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); " + submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);" + end + + if protect_against_forgery? + submit_function << "var s = document.createElement('input'); s.setAttribute('type', 'hidden'); " + submit_function << "s.setAttribute('name', '#{request_forgery_protection_token}'); s.setAttribute('value', '#{escape_javascript form_authenticity_token}'); f.appendChild(s);" + end + submit_function << "f.submit();" + end + + # Processes the _html_options_ hash, converting the boolean + # attributes from true/false form into the form required by + # HTML/XHTML. (An attribute is considered to be boolean if + # its name is listed in the given _bool_attrs_ array.) + # + # More specifically, for each boolean attribute in _html_options_ + # given as: + # + # "attr" => bool_value + # + # if the associated _bool_value_ evaluates to true, it is + # replaced with the attribute's name; otherwise the attribute is + # removed from the _html_options_ hash. (See the XHTML 1.0 spec, + # section 4.5 "Attribute Minimization" for more: + # http://www.w3.org/TR/xhtml1/#h-4.5) + # + # Returns the updated _html_options_ hash, which is also modified + # in place. + # + # Example: + # + # convert_boolean_attributes!( html_options, + # %w( checked disabled readonly ) ) + def convert_boolean_attributes!(html_options, bool_attrs) + bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } + html_options + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb new file mode 100644 index 000000000..0795b8ec5 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/partials.rb @@ -0,0 +1,200 @@ +module ActionView + # There's also a convenience method for rendering sub templates within the current controller that depends on a single object + # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being + # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own. + # + # In a template for Advertiser#account: + # + # <%= render :partial => "account" %> + # + # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable +account+ to + # the template for display. + # + # In another template for Advertiser#buy, we could have: + # + # <%= render :partial => "account", :locals => { :account => @buyer } %> + # + # <% for ad in @advertisements %> + # <%= render :partial => "ad", :locals => { :ad => ad } %> + # <% end %> + # + # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then render + # "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. + # + # == Rendering a collection of partials + # + # The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub + # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders + # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten + # with a single line: + # + # <%= render :partial => "ad", :collection => @advertisements %> + # + # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An iteration counter + # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the + # example above, the template would be fed +ad_counter+. + # + # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also just keep domain objects, + # like Active Records, in there. + # + # == Rendering shared partials + # + # Two controllers can share a set of partials and render them like this: + # + # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %> + # + # This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from. + # + # == Rendering partials with layouts + # + # Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally + # for the entire action, but they work in a similar fashion. Imagine a list with two types of users: + # + # <%# app/views/users/index.html.erb &> + # Here's the administrator: + # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %> + # + # Here's the editor: + # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %> + # + # <%# app/views/users/_user.html.erb &> + # Name: <%= user.name %> + # + # <%# app/views/users/_administrator.html.erb &> + # <div id="administrator"> + # Budget: $<%= user.budget %> + # <%= yield %> + # </div> + # + # <%# app/views/users/_editor.html.erb &> + # <div id="editor"> + # Deadline: $<%= user.deadline %> + # <%= yield %> + # </div> + # + # ...this will return: + # + # Here's the administrator: + # <div id="administrator"> + # Budget: $<%= user.budget %> + # Name: <%= user.name %> + # </div> + # + # Here's the editor: + # <div id="editor"> + # Deadline: $<%= user.deadline %> + # Name: <%= user.name %> + # </div> + # + # You can also apply a layout to a block within any template: + # + # <%# app/views/users/_chief.html.erb &> + # <% render(:layout => "administrator", :locals => { :user => chief }) do %> + # Title: <%= chief.title %> + # <% end %> + # + # ...this will return: + # + # <div id="administrator"> + # Budget: $<%= user.budget %> + # Title: <%= chief.name %> + # </div> + # + # As you can see, the :locals hash is shared between both the partial and its layout. + module Partials + private + def render_partial(partial_path, object_assigns = nil, local_assigns = nil) #:nodoc: + case partial_path + when String, Symbol, NilClass + path, partial_name = partial_pieces(partial_path) + object = extracting_object(partial_name, object_assigns) + local_assigns = local_assigns ? local_assigns.clone : {} + add_counter_to_local_assigns!(partial_name, local_assigns) + add_object_to_local_assigns!(partial_name, local_assigns, object) + + if logger && logger.debug? + ActionController::Base.benchmark("Rendered #{path}/_#{partial_name}", Logger::DEBUG, false) do + render("#{path}/_#{partial_name}", local_assigns) + end + else + render("#{path}/_#{partial_name}", local_assigns) + end + when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Associations::HasManyThroughAssociation + if partial_path.any? + path = ActionController::RecordIdentifier.partial_path(partial_path.first) + collection = partial_path + render_partial_collection(path, collection, nil, object_assigns.value) + else + "" + end + else + render_partial( + ActionController::RecordIdentifier.partial_path(partial_path), + object_assigns, local_assigns) + end + end + + def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil) #:nodoc: + collection_of_partials = Array.new + counter_name = partial_counter_name(partial_name) + local_assigns = local_assigns ? local_assigns.clone : {} + collection.each_with_index do |element, counter| + local_assigns[counter_name] = counter + collection_of_partials.push(render_partial(partial_name, element, local_assigns)) + end + + return " " if collection_of_partials.empty? + + if partial_spacer_template + spacer_path, spacer_name = partial_pieces(partial_spacer_template) + collection_of_partials.join(render("#{spacer_path}/_#{spacer_name}")) + else + collection_of_partials.join + end + end + + alias_method :render_collection_of_partials, :render_partial_collection + + def partial_pieces(partial_path) + if partial_path.include?('/') + return File.dirname(partial_path), File.basename(partial_path) + else + return controller.class.controller_path, partial_path + end + end + + def partial_counter_name(partial_name) + "#{partial_variable_name(partial_name)}_counter".intern + end + + def partial_variable_name(partial_name) + partial_name.split('/').last.split('.').first.intern + end + + def extracting_object(partial_name, object_assigns) + variable_name = partial_variable_name(partial_name) + if object_assigns.nil? + controller.instance_variable_get("@#{variable_name}") + else + object_assigns + end + end + + def add_counter_to_local_assigns!(partial_name, local_assigns) + counter_name = partial_counter_name(partial_name) + local_assigns[counter_name] = 1 unless local_assigns.has_key?(counter_name) + end + + def add_object_to_local_assigns!(partial_name, local_assigns, object) + variable_name = partial_variable_name(partial_name) + + local_assigns[:object] ||= + local_assigns[variable_name] ||= + if object.is_a?(ActionView::Base::ObjectWrapper) + object.value + else + object + end || controller.instance_variable_get("@#{variable_name}") + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb new file mode 100644 index 000000000..01b4b1555 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_error.rb @@ -0,0 +1,102 @@ +module ActionView + # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a + # bunch of intimate details and uses it to report a very precise exception message. + class TemplateError < ActionViewError #:nodoc: + SOURCE_CODE_RADIUS = 3 + + attr_reader :original_exception + + def initialize(base_path, file_path, assigns, source, original_exception) + @base_path, @assigns, @source, @original_exception = + base_path, assigns.dup, source, original_exception + @file_path = file_path + end + + def message + ActiveSupport::Deprecation.silence { original_exception.message } + end + + def clean_backtrace + original_exception.clean_backtrace + end + + def sub_template_message + if @sub_templates + "Trace of template inclusion: " + + @sub_templates.collect { |template| strip_base_path(template) }.join(", ") + else + "" + end + end + + def source_extract(indentation = 0) + return unless num = line_number + num = num.to_i + + source_code = IO.readlines(@file_path) + + start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max + end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min + + indent = ' ' * indentation + line_counter = start_on_line + + source_code[start_on_line..end_on_line].sum do |line| + line_counter += 1 + "#{indent}#{line_counter}: #{line}" + end + end + + def sub_template_of(template_path) + @sub_templates ||= [] + @sub_templates << template_path + end + + def line_number + @line_number ||= + if file_name + regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/ + + $1 if message =~ regexp or clean_backtrace.find { |line| line =~ regexp } + end + end + + def file_name + stripped = strip_base_path(@file_path) + stripped.slice!(0,1) if stripped[0] == ?/ + stripped + end + + def to_s + "\n\n#{self.class} (#{message}) #{source_location}:\n" + + "#{source_extract}\n #{clean_backtrace.join("\n ")}\n\n" + end + + def backtrace + [ + "#{source_location.capitalize}\n\n#{source_extract(4)}\n " + + clean_backtrace.join("\n ") + ] + end + + private + def strip_base_path(path) + stripped_path = File.expand_path(path).gsub(@base_path, "") + stripped_path.gsub!(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '') if defined?(RAILS_ROOT) + stripped_path + end + + def source_location + if line_number + "on line ##{line_number} of " + else + 'in ' + end + file_name + end + end +end + +if defined?(Exception::TraceSubstitutions) + Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, ''] + Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}/}, ''] if defined?(RAILS_ROOT) +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb new file mode 100644 index 000000000..b9f4330a2 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handler.rb @@ -0,0 +1,17 @@ +module ActionView + class TemplateHandler + def self.line_offset + 0 + end + + def initialize(view) + @view = view + end + + def render(template, local_assigns) + end + + def compile(template) + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb new file mode 100644 index 000000000..0f49d6ab3 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/builder.rb @@ -0,0 +1,19 @@ +require 'builder' + +module ActionView + module TemplateHandlers + class Builder < TemplateHandler + def self.line_offset + 2 + end + + def compile(template) + content_type_handler = (@view.send!(:controller).respond_to?(:response) ? "controller.response" : "controller") + "#{content_type_handler}.content_type ||= Mime::XML\n" + + "xml = Builder::XmlMarkup.new(:indent => 2)\n" + + template + + "\nxml.target!\n" + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb new file mode 100644 index 000000000..022fc362e --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/erb.rb @@ -0,0 +1,21 @@ +require 'erb' + +class ERB + module Util + HTML_ESCAPE = { '&' => '&', '"' => '"', '>' => '>', '<' => '<' } + + def html_escape(s) + s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] } + end + end +end + +module ActionView + module TemplateHandlers + class ERB < TemplateHandler + def compile(template) + ::ERB.new(template, nil, @view.erb_trim_mode).src + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb new file mode 100644 index 000000000..4ca9fc327 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/action_view/template_handlers/rjs.rb @@ -0,0 +1,14 @@ +module ActionView + module TemplateHandlers + class RJS < TemplateHandler + def self.line_offset + 2 + end + + def compile(template) + "controller.response.content_type ||= Mime::JS\n" + + "update_page do |page|\n#{template}\nend" + end + end + end +end diff --git a/vendor/rails-2.0.2/actionpack/lib/actionpack.rb b/vendor/rails-2.0.2/actionpack/lib/actionpack.rb new file mode 100644 index 000000000..2fe2832f8 --- /dev/null +++ b/vendor/rails-2.0.2/actionpack/lib/actionpack.rb @@ -0,0 +1 @@ +require 'action_pack' |