From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../assertions/action_pack_assertions.rb | 199 ++++++ .../assertions/active_record_assertions.rb | 65 ++ actionpack/lib/action_controller/base.rb | 689 +++++++++++++++++++++ actionpack/lib/action_controller/benchmarking.rb | 49 ++ .../lib/action_controller/cgi_ext/cgi_ext.rb | 43 ++ .../lib/action_controller/cgi_ext/cgi_methods.rb | 91 +++ actionpack/lib/action_controller/cgi_process.rb | 124 ++++ actionpack/lib/action_controller/dependencies.rb | 49 ++ actionpack/lib/action_controller/filters.rb | 279 +++++++++ actionpack/lib/action_controller/flash.rb | 65 ++ actionpack/lib/action_controller/helpers.rb | 100 +++ actionpack/lib/action_controller/layout.rb | 149 +++++ actionpack/lib/action_controller/request.rb | 99 +++ actionpack/lib/action_controller/rescue.rb | 94 +++ actionpack/lib/action_controller/response.rb | 15 + actionpack/lib/action_controller/scaffolding.rb | 183 ++++++ .../session/active_record_store.rb | 72 +++ .../lib/action_controller/session/drb_server.rb | 9 + .../lib/action_controller/session/drb_store.rb | 31 + .../support/class_attribute_accessors.rb | 57 ++ .../support/class_inheritable_attributes.rb | 37 ++ .../lib/action_controller/support/clean_logger.rb | 10 + .../support/cookie_performance_fix.rb | 121 ++++ .../lib/action_controller/support/inflector.rb | 78 +++ .../templates/rescues/_request_and_response.rhtml | 28 + .../templates/rescues/diagnostics.rhtml | 22 + .../templates/rescues/layout.rhtml | 29 + .../templates/rescues/missing_template.rhtml | 2 + .../templates/rescues/template_error.rhtml | 26 + .../templates/rescues/unknown_action.rhtml | 2 + .../templates/scaffolds/edit.rhtml | 6 + .../templates/scaffolds/layout.rhtml | 29 + .../templates/scaffolds/list.rhtml | 24 + .../templates/scaffolds/new.rhtml | 5 + .../templates/scaffolds/show.rhtml | 9 + actionpack/lib/action_controller/test_process.rb | 195 ++++++ actionpack/lib/action_controller/url_rewriter.rb | 170 +++++ 37 files changed, 3255 insertions(+) create mode 100644 actionpack/lib/action_controller/assertions/action_pack_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/active_record_assertions.rb create mode 100755 actionpack/lib/action_controller/base.rb create mode 100644 actionpack/lib/action_controller/benchmarking.rb create mode 100755 actionpack/lib/action_controller/cgi_ext/cgi_ext.rb create mode 100755 actionpack/lib/action_controller/cgi_ext/cgi_methods.rb create mode 100644 actionpack/lib/action_controller/cgi_process.rb create mode 100644 actionpack/lib/action_controller/dependencies.rb create mode 100644 actionpack/lib/action_controller/filters.rb create mode 100644 actionpack/lib/action_controller/flash.rb create mode 100644 actionpack/lib/action_controller/helpers.rb create mode 100644 actionpack/lib/action_controller/layout.rb create mode 100755 actionpack/lib/action_controller/request.rb create mode 100644 actionpack/lib/action_controller/rescue.rb create mode 100755 actionpack/lib/action_controller/response.rb create mode 100644 actionpack/lib/action_controller/scaffolding.rb create mode 100644 actionpack/lib/action_controller/session/active_record_store.rb create mode 100644 actionpack/lib/action_controller/session/drb_server.rb create mode 100644 actionpack/lib/action_controller/session/drb_store.rb create mode 100644 actionpack/lib/action_controller/support/class_attribute_accessors.rb create mode 100644 actionpack/lib/action_controller/support/class_inheritable_attributes.rb create mode 100644 actionpack/lib/action_controller/support/clean_logger.rb create mode 100644 actionpack/lib/action_controller/support/cookie_performance_fix.rb create mode 100644 actionpack/lib/action_controller/support/inflector.rb create mode 100644 actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml create mode 100644 actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml create mode 100644 actionpack/lib/action_controller/templates/rescues/layout.rhtml create mode 100644 actionpack/lib/action_controller/templates/rescues/missing_template.rhtml create mode 100644 actionpack/lib/action_controller/templates/rescues/template_error.rhtml create mode 100644 actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml create mode 100644 actionpack/lib/action_controller/templates/scaffolds/edit.rhtml create mode 100644 actionpack/lib/action_controller/templates/scaffolds/layout.rhtml create mode 100644 actionpack/lib/action_controller/templates/scaffolds/list.rhtml create mode 100644 actionpack/lib/action_controller/templates/scaffolds/new.rhtml create mode 100644 actionpack/lib/action_controller/templates/scaffolds/show.rhtml create mode 100644 actionpack/lib/action_controller/test_process.rb create mode 100644 actionpack/lib/action_controller/url_rewriter.rb (limited to 'actionpack/lib/action_controller') diff --git a/actionpack/lib/action_controller/assertions/action_pack_assertions.rb b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb new file mode 100644 index 0000000000..2cfbcbc938 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/action_pack_assertions.rb @@ -0,0 +1,199 @@ +require 'test/unit' +require 'test/unit/assertions' +require 'rexml/document' + +module Test #:nodoc: + module Unit #:nodoc: + # Adds a wealth of assertions to do functional testing of Action Controllers. + module Assertions + # -- basic assertions --------------------------------------------------- + + # ensure that the web request has been serviced correctly + def assert_success(message=nil) + response = acquire_assertion_target + if response.success? + # to count the assertion + assert_block("") { true } + else + if response.redirect? + msg = build_message(message, "Response unexpectedly redirect to ", response.redirect_url) + else + msg = build_message(message, "unsuccessful request (response code = )", + response.response_code) + end + assert_block(msg) { false } + end + end + + # ensure the request was rendered with the appropriate template file + def assert_rendered_file(expected=nil, message=nil) + response = acquire_assertion_target + 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 + + # -- session assertions ------------------------------------------------- + + # ensure that the session has an object with the specified name + def assert_session_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, " is not in the session ", key, response.session) + assert_block(msg) { response.has_session_object?(key) } + end + + # ensure that the session has no object with the specified name + def assert_session_has_no(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, " is in the session ", key, response.session) + assert_block(msg) { !response.has_session_object?(key) } + end + + def assert_session_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, " expected in session['?'] but was ", expected, key, response.session[key]) + assert_block(msg) { expected == response.session[key] } + end + + # -- flash assertions --------------------------------------------------- + + # ensure that the flash has an object with the specified name + def assert_flash_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, " is not in the flash ", key, response.flash) + assert_block(msg) { response.has_flash_object?(key) } + end + + # ensure that the flash has no object with the specified name + def assert_flash_has_no(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, " is in the flash ", key, response.flash) + assert_block(msg) { !response.has_flash_object?(key) } + end + + # ensure the flash exists + def assert_flash_exists(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash does not exist ", response.session['flash'] ) + assert_block(msg) { response.has_flash? } + end + + # ensure the flash does not exist + def assert_flash_not_exists(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash exists ", response.flash) + assert_block(msg) { !response.has_flash? } + end + + # ensure the flash is empty but existant + def assert_flash_empty(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash is not empty ", response.flash) + assert_block(msg) { !response.has_flash_with_contents? } + end + + # ensure the flash is not empty + def assert_flash_not_empty(message=nil) + response = acquire_assertion_target + msg = build_message(message, "the flash is empty") + assert_block(msg) { response.has_flash_with_contents? } + end + + def assert_flash_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, " expected in flash['?'] but was ", expected, key, response.flash[key]) + assert_block(msg) { expected == response.flash[key] } + end + + # -- redirection assertions --------------------------------------------- + + # ensure we have be redirected + def assert_redirect(message=nil) + response = acquire_assertion_target + msg = build_message(message, "response is not a redirection (response code is )", response.response_code) + assert_block(msg) { response.redirect? } + end + + def assert_redirected_to(options = {}, message=nil) + assert_redirect(message) + response = acquire_assertion_target + + msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is )", response.redirected_to) + assert_block(msg) do + if options.is_a?(Symbol) + response.redirected_to == options + else + options.keys.all? { |k| options[k] == response.redirected_to[k] } + end + end + end + + # ensure our redirection url is an exact match + def assert_redirect_url(url=nil, message=nil) + assert_redirect(message) + response = acquire_assertion_target + msg = build_message(message, " is not the redirected location ", url, response.redirect_url) + assert_block(msg) { response.redirect_url == url } + end + + # ensure our redirection url matches a pattern + def assert_redirect_url_match(pattern=nil, message=nil) + assert_redirect(message) + response = acquire_assertion_target + msg = build_message(message, " was not found in the location: ", pattern, response.redirect_url) + assert_block(msg) { response.redirect_url_match?(pattern) } + end + + # -- template assertions ------------------------------------------------ + + # ensure that a template object with the given name exists + def assert_template_has(key=nil, message=nil) + response = acquire_assertion_target + msg = build_message(message, " is not a template object", key ) + assert_block(msg) { response.has_template_object?(key) } + end + + # ensure that a template object with the given name does not exist + def assert_template_has_no(key=nil,message=nil) + response = acquire_assertion_target + msg = build_message(message, " is a template object ", key, response.template_objects[key]) + assert_block(msg) { !response.has_template_object?(key) } + end + + # ensures that the object assigned to the template on +key+ is equal to +expected+ object. + def assert_assigned_equal(expected = nil, key = nil, message = nil) + response = acquire_assertion_target + msg = build_message(message, " expected in assigns['?'] but was ", expected, key, response.template.assigns[key.to_s]) + assert_block(msg) { expected == response.template.assigns[key.to_s] } + end + + # Asserts that the template returns the +expected+ string or array based on the XPath +expression+. + # This will only work if the template rendered a valid XML document. + def assert_template_xpath_match(expression=nil, expected=nil, message=nil) + response = acquire_assertion_target + xml, matches = REXML::Document.new(response.body), [] + xml.elements.each(expression) { |e| matches << e.text } + matches = matches.first if matches.length < 2 + + msg = build_message(message, " found , not ", expression, matches, expected) + assert_block(msg) { matches == expected } + end + + # -- helper functions --------------------------------------------------- + + # get the TestResponse object that these assertions depend upon + def acquire_assertion_target + target = ActionController::TestResponse.assertion_target + assert_block( "Unable to acquire the TestResponse.assertion_target. Please set this before calling this assertion." ) { !target.nil? } + target + end + + end # Assertions + end # Unit +end # Test diff --git a/actionpack/lib/action_controller/assertions/active_record_assertions.rb b/actionpack/lib/action_controller/assertions/active_record_assertions.rb new file mode 100644 index 0000000000..9167eae53e --- /dev/null +++ b/actionpack/lib/action_controller/assertions/active_record_assertions.rb @@ -0,0 +1,65 @@ +require 'test/unit' +require 'test/unit/assertions' +# active_record is assumed to be loaded by this point + +module Test #:nodoc: + module Unit #:nodoc: + module Assertions + # Assert the template object with the given name is an Active Record descendant and is valid. + def assert_valid_record(key = nil, message = nil) + record = find_record_in_template(key) + msg = build_message(message, "Active Record is invalid )", record.errors.full_messages) + assert_block(msg) { record.valid? } + end + + # Assert the template object with the given name is an Active Record descendant and is invalid. + def assert_invalid_record(key = nil, message = nil) + record = find_record_in_template(key) + msg = build_message(message, "Active Record is valid)") + assert_block(msg) { !record.valid? } + end + + # Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid. + def assert_valid_column_on_record(key = nil, columns = "", message = nil) + record = find_record_in_template(key) + record.validate + + cols = glue_columns(columns) + cols.delete_if { |col| !record.errors.invalid?(col) } + msg = build_message(message, "Active Record has invalid columns )", cols.join(",") ) + assert_block(msg) { cols.empty? } + end + + # Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid. + def assert_invalid_column_on_record(key = nil, columns = "", message = nil) + record = find_record_in_template(key) + record.validate + + cols = glue_columns(columns) + cols.delete_if { |col| record.errors.invalid?(col) } + msg = build_message(message, "Active Record has valid columns )", cols.join(",") ) + assert_block(msg) { cols.empty? } + end + + private + def glue_columns(columns) + cols = [] + cols << columns if columns.class == String + cols += columns if columns.class == Array + cols + end + + def find_record_in_template(key = nil) + response = acquire_assertion_target + + assert_template_has(key) + record = response.template_objects[key] + + assert_not_nil(record) + assert_kind_of ActiveRecord::Base, record + + return record + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb new file mode 100755 index 0000000000..0cbcbbf688 --- /dev/null +++ b/actionpack/lib/action_controller/base.rb @@ -0,0 +1,689 @@ +require 'action_controller/request' +require 'action_controller/response' +require 'action_controller/url_rewriter' +require 'action_controller/support/class_attribute_accessors' +require 'action_controller/support/class_inheritable_attributes' +require 'action_controller/support/inflector' + +module ActionController #:nodoc: + class ActionControllerError < StandardError #:nodoc: + end + class SessionRestoreError < ActionControllerError #:nodoc: + end + class MissingTemplate < ActionControllerError #:nodoc: + end + class UnknownAction < ActionControllerError #:nodoc: + end + + # Action Controllers are made up of one or more actions that performs its purpose and then either renders a template or + # redirects 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 a mod_rewrite mapping. 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 + # + # GuestBookController.template_root = "templates/" + # GuestBookController.process_cgi + # + # All actions assume that you want to render a template matching the name of the action at the end of the performance + # unless you tell it otherwise. The index action complies with this assumption, so after populating the @entries instance + # variable, the GuestBookController will render "templates/guestbook/index.rhtml". + # + # Unlike index, the sign action isn't interested in rendering a template. So after performing its main purpose (creating a + # new entry in the guest book), it sheds the rendering assumption and 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. + # + # Also note that it's the final call to process_cgi that actually initiates the action performance. It will extract + # request and response objects from the CGI + # + # == 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 in @request and is primarily used to query for http headers. These queries are made by + # accessing the environment hash, like this: + # + # def hello_ip + # location = @request.env["REMOTE_ADDRESS"] + # render_text "Hello stranger from #{location}" + # end + # + # == Parameters + # + # All request parameters whether they come from a GET or POST request, or from the URL, are available through the @params hash. + # So an action that was performed through /weblog/list?category=All&limit=5 will include { "category" => "All", "limit" => 5 } + # in @params. + # + # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # + # + # + # + # A request stemming from a form holding these inputs will include { "post" # => { "name" => "david", "address" => "hyacintvej" } }. + # If the address input had been named "post[address][street]", the @params would have included + # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting. + # + # == Sessions + # + # Sessions allows you to store objects in memory 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 @session hash: + # + # @session["person"] = Person.authenticate(user_name, password) + # + # And retrieved again through the same hash: + # + # Hello #{@session["person"]} + # + # Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a + # 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use + # of the session. + # + # == 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, so it's normally nothing you'll need to be concerned about. + # + # == 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 "weblog/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 + # + # Redirecting is what actions that update the model do when they're done. The save_post method shouldn't be responsible for also + # showing the post once it's saved -- that's the job for show_post. So once save_post has completed its business, it'll + # redirect to show_post. All redirects are external, which means that when the user refreshes his browser, it's not going to save + # the post again, but rather just show it one more time. + # + # This sounds fairly simple, but the redirection is complicated by the quest for a phenomenon known as "pretty urls". Instead of accepting + # the dreadful beings that is "weblog_controller?action=show&post_id=5", Action Controller goes out of its way to represent the former as + # "/weblog/show/5". And this is even the simple case. As an example of a more advanced pretty url consider + # "/library/books/ISBN/0743536703/show", which can be mapped to books_controller?action=show&type=ISBN&id=0743536703. + # + # Redirects work by rewriting the URL of the current action. So if the show action was called by "/library/books/ISBN/0743536703/show", + # we can redirect to an edit action simply by doing redirect_to(:action => "edit"), which could throw the user to + # "/library/books/ISBN/0743536703/edit". Naturally, you'll need to setup the .htaccess (or other means of URL rewriting for the web server) + # to point to the proper controller and action in the first place, but once you have, it can be rewritten with ease. + # + # Let's consider a bunch of examples on how to go from "/library/books/ISBN/0743536703/edit" to somewhere else: + # + # redirect_to(:action => "show", :action_prefix => "XTC/123") => + # "http://www.singlefile.com/library/books/XTC/123/show" + # + # redirect_to(:path_params => {"type" => "EXBC"}) => + # "http://www.singlefile.com/library/books/EXBC/0743536703/show" + # + # redirect_to(:controller => "settings") => + # "http://www.singlefile.com/library/settings/" + # + # For more examples of redirecting options, have a look at the unit test in test/controller/url_test.rb. It's very readable and will give + # you an excellent understanding of the different options and what they do. + # + # == Environments + # + # Action Controller works out of the box with CGI, FastCGI, and mod_ruby. CGI and mod_ruby controllers are triggered just the same using: + # + # WeblogController.process_cgi + # + # FastCGI controllers are triggered using: + # + # FCGI.each_cgi{ |cgi| WeblogController.process_cgi(cgi) } + class Base + include ClassInheritableAttributes + + DEFAULT_RENDER_STATUS_CODE = "200 OK" + + DEFAULT_SEND_FILE_OPTIONS = { + :type => 'application/octet_stream', + :disposition => 'attachment', + :stream => true, + :buffer_size => 4096 + } + + + # 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 + + # 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 local_request? + # 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 + + # When turned on (which is default), all dependencies are included using "load". This mean that any change is instant in cached + # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to + # be effective. + @@reload_dependencies = true + cattr_accessor :reload_dependencies + + # Template root determines the base from which template references will be made. So a call to render("test/template") + # will be converted to "#{template_root}/test/template.rhtml". + cattr_accessor :template_root + + # 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 + + # Holds the request object that's primarily used to get environment variables through access like + # @request.env["REQUEST_URI"]. + attr_accessor :request + + # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like @params["post_id"] + # to get the post_id. No type casts are made, so all values are returned as strings. + attr_accessor :params + + # Holds the response object that's primarily used to set additional HTTP headers through access like + # @response.headers["Cache-Control"] = "no-cache". Can also be used to access the final body HTML after a template + # has been rendered through @response.body -- useful for after_filters that wants to manipulate the output, + # such as a OutputCompressionFilter. + attr_accessor :response + + # Holds a hash of objects in the session. Accessed like @session["person"] 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. + attr_accessor :session + + # Holds a hash of header names and values. Accessed like @headers["Cache-Control"] to get the value of the Cache-Control + # directive. Values should always be specified as strings. + attr_accessor :headers + + # Holds a hash of cookie names and values. Accessed like @cookies["user_name"] to get the value of the user_name cookie. + # This hash is read-only. You set new cookies using the cookie method. + attr_accessor :cookies + + # 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 + + 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 + Inflector.demodulize(name) + end + + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat". + def controller_name + Inflector.underscore(controller_class_name.sub(/Controller/, "")) + end + + # Loads the file_name if reload_dependencies is true or requires if it's false. + def require_or_load(file_name) + reload_dependencies ? load("#{file_name}.rb") : require(file_name) + end + end + + public + # Extracts the action_name from the request parameters and performs that action. + def process(request, response) #:nodoc: + initialize_template_class(response) + assign_shortcuts(request, response) + initialize_current_url + + log_processing unless logger.nil? + perform_action + close_session + + return @response + end + + # Returns an URL that has been rewritten according to the hash of +options+ (for doing a complete redirect, use redirect_to). The + # valid keys in options are specified below with an example going from "/library/books/ISBN/0743536703/show" (mapped to + # books_controller?action=show&type=ISBN&id=0743536703): + # + # .---> controller .--> action + # /library/books/ISBN/0743536703/show + # '------> '--------------> action_prefix + # controller_prefix + # + # * :controller_prefix - specifies the string before the controller name, which would be "/library" for the example. + # Called with "/shop" gives "/shop/books/ISBN/0743536703/show". + # * :controller - specifies a new controller and clears out everything after the controller name (including the action, + # the pre- and suffix, and all params), so called with "settings" gives "/library/settings/". + # * :action_prefix - specifies the string between the controller name and the action name, which would + # be "/ISBN/0743536703" for the example. Called with "/XTC/123/" gives "/library/books/XTC/123/show". + # * :action - specifies a new action, so called with "edit" gives "/library/books/ISBN/0743536703/edit" + # * :action_suffix - specifies the string after the action name, which would be empty for the example. + # Called with "/detailed" gives "/library/books/ISBN/0743536703/detailed". + # * :path_params - specifies a hash that contains keys mapping to the request parameter names. In the example, + # { "type" => "ISBN", "id" => "0743536703" } would be the path_params. It serves as another way of replacing part of + # the action_prefix or action_suffix. So passing { "type" => "XTC" } would give "/library/books/XTC/0743536703/show". + # * :id - shortcut where ":id => 5" can be used instead of specifying :path_params => { "id" => 5 }. + # Called with "123" gives "/library/books/ISBN/123/show". + # * :params - specifies a hash that represents the regular request parameters, such as { "cat" => 1, + # "origin" => "there"} that would give "?cat=1&origin=there". Called with { "temporary" => 1 } in the example would give + # "/library/books/ISBN/0743536703/show?temporary=1" + # * :anchor - specifies the anchor name to be appended to the path. Called with "x14" would give + # "/library/books/ISBN/0743536703/show#x14" + # * :only_path - if true, returns the absolute URL (omitting the protocol, host name, and port). + # + # Naturally, you can combine multiple options in a single redirect. Examples: + # + # redirect_to(:controller_prefix => "/shop", :controller => "settings") + # redirect_to(:action => "edit", :id => 3425) + # redirect_to(:action => "edit", :path_params => { "type" => "XTC" }, :params => { "temp" => 1}) + # redirect_to(:action => "publish", :action_prefix => "/published", :anchor => "x14") + # + # Instead of passing an options hash, you can also pass a method reference in the form of a symbol. Consider this example: + # + # class WeblogController < ActionController::Base + # def update + # # do some update + # redirect_to :dashboard_url + # end + # + # protected + # def dashboard_url + # url_for :controller => (@project.active? ? "project" : "account"), :action => "dashboard" + # end + # end + def url_for(options = {}, *parameters_for_method_reference) #:doc: + case options + when String then options + when Symbol then send(options, *parameters_for_method_reference) + when Hash then @url.rewrite(rewrite_options(options)) + end + end + + def module_name + @params["module"] + 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 + + # Returns the name of the action this controller is processing. + def action_name + @params["action"] || "index" + end + + protected + # Renders the template specified by template_name, which defaults to the name of the current controller and action. + # So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or + # "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is + # shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK", + # but can be changed, such as by calling render("weblog/error", "500 Error"). + def render(template_name = nil, status = nil) #:doc: + render_file(template_name || default_template_name, status, true) + end + + # Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling + # render_action "show_many" in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or + # "#{template_root}/weblog/show_many.rxml". + def render_action(action_name, status = nil) #:doc: + render default_template_name(action_name), status + end + + # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be + # used like render_file "/Users/david/Code/Ruby/template" to render "/Users/david/Code/Ruby/template.rhtml" or + # "/Users/david/Code/Ruby/template.rxml". + def render_file(template_path, status = nil, use_full_path = false) #:doc: + assert_existance_of_template_file(template_path) if use_full_path + logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil? + + add_variables_to_assigns + render_text(@template.render_file(template_path, use_full_path), status) + end + + # Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So + # you'd call render_template "Hello, <%= @user.name %>" to greet the current user. Or if you want to render as Builder + # template, you could do render_template "xml.h1 @user.name", nil, "rxml". + def render_template(template, status = nil, type = "rhtml") #:doc: + add_variables_to_assigns + render_text(@template.render_template(type, template), status) + end + + # Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's + # considerably faster than rendering through the template engine. + # Use block for response body if provided (useful for deferred rendering or streaming output). + def render_text(text = nil, status = nil, &block) #:doc: + add_variables_to_assigns + @response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE + @response.body = block_given? ? block : text + @performed_render = true + end + + # 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: + # * :filename - suggests a filename for the browser to use. + # Defaults to File.basename(path). + # * :type - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * :streaming - 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. + # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. + # Defaults to 4096. + # + # 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 browser: + # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # + # 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 = {}) + raise MissingFile unless File.file?(path) and File.readable?(path) + + options[:length] ||= File.size(path) + options[:filename] ||= File.basename(path) + send_file_headers! options + + if options[:stream] + render_text do + logger.info "Streaming file #{path}" unless logger.nil? + len = options[:buffer_size] || 4096 + File.open(path, 'rb') do |file| + begin + while true + $stdout.syswrite file.sysread(len) + end + rescue EOFError + end + end + end + else + logger.info "Sending file #{path}" unless logger.nil? + File.open(path, 'rb') { |file| render_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: + # * :filename - Suggests a filename for the browser to use. + # * :type - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # + # Generic data download: + # send_data buffer + # + # Download a dynamically-generated tarball: + # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # + # Display an image Active Record in the browser: + # send_data image.data, :type => image.content_type, :disposition => 'inline' + # + # See +send_file+ for more information on HTTP Content-* headers and caching. + def send_data(data, options = {}) + logger.info "Sending data #{options[:filename]}" unless logger.nil? + send_file_headers! options.merge(:length => data.size) + render_text data + end + + def rewrite_options(options) + 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) + # { :controller_prefix => @project.active? ? "projects/" : "accounts/" } + # 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 an URL that has been rewritten according to the hash of +options+ using a "302 Moved" HTTP header. + # See url_for for a description of the valid options. + def redirect_to(options = {}, *parameters_for_method_reference) #:doc: + if parameters_for_method_reference.empty? + @response.redirected_to = options + redirect_to_url(url_for(options)) + else + @response.redirected_to, @response.redirected_to_method_params = options, parameters_for_method_reference + redirect_to_url(url_for(options, *parameters_for_method_reference)) + end + end + + # Redirects the browser to the specified path within the current host (specified with a leading /). Used to sidestep + # the URL rewriting and go directly to a known path. Example: redirect_to_path "/images/screenshot.jpg". + def redirect_to_path(path) #:doc: + redirect_to_url(@request.protocol + @request.host_with_port + path) + end + + # Redirects the browser to the specified url. Used to redirect outside of the current application. Example: + # redirect_to_url "http://www.rubyonrails.org". + def redirect_to_url(url) #:doc: + logger.info("Redirected to #{url}") unless logger.nil? + @response.redirect(url) + @performed_redirect = true + end + + # Creates a new cookie that is sent along-side the next render or redirect command. API is the same as for CGI::Cookie. + # Examples: + # + # cookie("name", "value1", "value2", ...) + # cookie("name" => "name", "value" => "value") + # cookie('name' => 'name', + # 'value' => ['value1', 'value2', ...], + # 'path' => 'path', # optional + # 'domain' => 'domain', # optional + # 'expires' => Time.now, # optional + # 'secure' => true # optional + # ) + def cookie(*options) #:doc: + @response.headers["cookie"] << CGI::Cookie.new(*options) + end + + # Resets the session by clearsing 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 initialize_template_class(response) + begin + response.template = template_class.new(template_root, {}, self) + rescue + raise "You must assign a template class through ActionController.template_class= before processing a request" + end + + @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, controller_name, action_name) + end + + def log_processing + logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin})" + logger.info " Parameters: #{@params.inspect}" + end + + def perform_action + if action_methods.include?(action_name) + send(action_name) + render unless @performed_render || @performed_redirect + elsif template_exists? && template_public? + render + else + raise UnknownAction, "No action responded to #{action_name}", caller + end + end + + def action_methods + action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) } + action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) } + end + + def add_variables_to_assigns + add_instance_variables_to_assigns + add_class_variables_to_assigns if view_controller_internals + end + + def add_instance_variables_to_assigns + protected_variables_cache = 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( template_root logger template_class ignore_missing_templates ).each do |cvar| + @assigns[cvar] = self.send(cvar) + end + end + + def protected_instance_variables + if view_controller_internals + [ "@assigns", "@performed_redirect", "@performed_render" ] + else + [ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template" ] + end + end + + def request_origin + "#{@request.remote_ip} at #{Time.now.to_s}" + end + + def close_session + @session.close unless @session.nil? || Hash === @session + 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 assert_existance_of_template_file(template_name) + unless template_exists?(template_name) || ignore_missing_templates + full_template_path = @template.send(:full_template_path, template_name, 'rhtml') + template_type = (template_name =~ /layouts/i) ? 'layout' : 'template' + raise(MissingTemplate, "Missing #{template_type} #{full_template_path}") + end + end + + 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] || 'attachment' + disposition <<= %(; filename="#{options[:filename]}") if options[:filename] + + @headers.update( + 'Content-Length' => options[:length], + 'Content-Type' => options[:type], + 'Content-Disposition' => disposition, + 'Content-Transfer-Encoding' => 'binary' + ); + end + + def default_template_name(default_action_name = action_name) + module_name ? "#{module_name}/#{controller_name}/#{default_action_name}" : "#{controller_name}/#{default_action_name}" + end + end +end diff --git a/actionpack/lib/action_controller/benchmarking.rb b/actionpack/lib/action_controller/benchmarking.rb new file mode 100644 index 0000000000..e6ff65e150 --- /dev/null +++ b/actionpack/lib/action_controller/benchmarking.rb @@ -0,0 +1,49 @@ +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.append_features(base) + super + base.class_eval { + alias_method :perform_action_without_benchmark, :perform_action + alias_method :perform_action, :perform_action_with_benchmark + + alias_method :render_without_benchmark, :render + alias_method :render, :render_with_benchmark + } + end + + def render_with_benchmark(template_name = default_template_name, status = "200 OK") + if logger.nil? + render_without_benchmark(template_name, status) + else + @rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real + end + end + + def perform_action_with_benchmark + if logger.nil? + perform_action_without_benchmark + else + runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max + log_message = "Completed in #{sprintf("%4f", runtime)} (#{(1 / runtime).floor} reqs/sec)" + log_message << rendering_runtime(runtime) if @rendering_runtime + log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? + logger.info(log_message) + end + end + + private + def rendering_runtime(runtime) + " | Rendering: #{sprintf("%f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime / runtime) * 100)}%)" + end + + def active_record_runtime(runtime) + db_runtime = ActiveRecord::Base.connection.reset_runtime + db_percentage = (db_runtime / runtime) * 100 + " | DB: #{sprintf("%f", db_runtime)} (#{sprintf("%d", db_percentage)}%)" + end + end +end diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb new file mode 100755 index 0000000000..371ead695b --- /dev/null +++ b/actionpack/lib/action_controller/cgi_ext/cgi_ext.rb @@ -0,0 +1,43 @@ +require 'cgi' +require 'cgi/session' +require 'cgi/session/pstore' +require 'action_controller/cgi_ext/cgi_methods' + +# Wrapper around the CGIMethods that have been secluded to allow testing without +# an instatiated CGI object +class CGI #:nodoc: + class << self + alias :escapeHTML_fail_on_nil :escapeHTML + + def escapeHTML(string) + escapeHTML_fail_on_nil(string) unless string.nil? + end + end + + # Returns a parameter hash including values from both the request (POST/GET) + # and the query string with the latter taking precedence. + def parameters + request_parameters.update(query_parameters) + end + + def query_parameters + CGIMethods.parse_query_parameters(query_string) + end + + def request_parameters + CGIMethods.parse_request_parameters(params) + end + + def redirect(where) + header({ + "Status" => "302 Moved", + "location" => "#{where}" + }) + end + + def session(parameters = nil) + parameters = {} if parameters.nil? + parameters['database_manager'] = CGI::Session::PStore + CGI::Session.new(self, parameters) + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb new file mode 100755 index 0000000000..261490580c --- /dev/null +++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb @@ -0,0 +1,91 @@ +require 'cgi' + +# Static methods for parsing the query and request parameters that can be used in +# a CGI extension class or testing in isolation. +class CGIMethods #:nodoc: + public + # Returns a hash with the pairs from the query string. The implicit hash construction that is done in + # parse_request_params is not done here. + def CGIMethods.parse_query_parameters(query_string) + parsed_params = {} + + query_string.split(/[&;]/).each { |p| + k, v = p.split('=') + + k = CGI.unescape(k) unless k.nil? + v = CGI.unescape(v) unless v.nil? + + if k =~ /(.*)\[\]$/ + if parsed_params.has_key? $1 + parsed_params[$1] << v + else + parsed_params[$1] = [v] + end + else + parsed_params[k] = v.nil? ? nil : v + end + } + + return parsed_params + end + + # Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" / + # "Somewhere cool!" are translated into a full hash hierarchy, like + # { "customer" => { "address" => { "street" => "Somewhere cool!" } } } + def CGIMethods.parse_request_parameters(params) + parsed_params = {} + + for key, value in params + value = [value] if key =~ /.*\[\]$/ + CGIMethods.build_deep_hash( + CGIMethods.get_typed_value(value[0]), + parsed_params, + CGIMethods.get_levels(key) + ) + end + + return parsed_params + end + + private + def CGIMethods.get_typed_value(value) + if value.respond_to?(:content_type) && !value.content_type.empty? + # Uploaded file + value + elsif value.respond_to?(:read) + # Value as part of a multipart request + value.read + elsif value.class == Array + value + else + # Standard value (not a multipart request) + value.to_s + end + end + + def CGIMethods.get_levels(key_string) + return [] if key_string.nil? or key_string.empty? + + levels = [] + main, existance = /(\w+)(\[)?.?/.match(key_string).captures + levels << main + + unless existance.nil? + hash_part = key_string.sub(/\w+\[/, "") + hash_part.slice!(-1, 1) + levels += hash_part.split(/\]\[/) + end + + levels + end + + def CGIMethods.build_deep_hash(value, hash, levels) + if levels.length == 0 + value; + elsif hash.nil? + { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) } + else + hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) }) + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb new file mode 100644 index 0000000000..e69a0b2f8d --- /dev/null +++ b/actionpack/lib/action_controller/cgi_process.rb @@ -0,0 +1,124 @@ +require 'action_controller/cgi_ext/cgi_ext' +require 'action_controller/support/cookie_performance_fix' +require 'action_controller/session/drb_store' +require 'action_controller/session/active_record_store' + +module ActionController #:nodoc: + class Base + # Process a request extracted from an CGI object and return a response. Pass false as session_options to disable + # sessions (large performance increase if sessions are not needed). The session_options are the same as for CGI::Session: + # + # * :database_manager - 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. + # * :session_key - the parameter name used for the session id. Defaults to '_session_id'. + # * :session_id - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter + # of the request, or automatically generated for a new session. + # * :new_session - 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. + # * :session_expires - the time the current session expires, as a +Time+ object. If not set, the session will continue + # indefinitely. + # * :session_domain - the hostname domain for which this session is valid. If not set, defaults to the hostname of the + # server. + # * :session_secure - if +true+, this session will only work over HTTPS. + # * :session_path - the path for which this session applies. Defaults to the directory of the CGI script. + def self.process_cgi(cgi = CGI.new, session_options = {}) + new.process_cgi(cgi, session_options) + end + + def process_cgi(cgi, session_options = {}) #:nodoc: + process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out + end + end + + class CgiRequest < AbstractRequest #:nodoc: + attr_accessor :cgi + + DEFAULT_SESSION_OPTIONS = + { "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" } + + def initialize(cgi, session_options = {}) + @cgi = cgi + @session_options = session_options + super() + end + + def query_parameters + @cgi.query_string ? CGIMethods.parse_query_parameters(@cgi.query_string) : {} + end + + def request_parameters + CGIMethods.parse_request_parameters(@cgi.params) + end + + def env + @cgi.send(:env_table) + end + + def cookies + @cgi.cookies.freeze + end + + def host + env["HTTP_X_FORWARDED_HOST"] || @cgi.host.split(":").first + end + + def session + return @session unless @session.nil? + begin + @session = (@session_options == false ? {} : CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options))) + @session["__valid_session"] + return @session + rescue ArgumentError => e + @session.delete if @session + raise( + ActionController::SessionRestoreError, + "Session contained objects where the class definition wasn't available. " + + "Remember to require classes for all objects kept in the session. " + + "The session has been deleted." + ) + end + end + + def reset_session + @session.delete + @session = (@session_options == false ? {} : new_session) + end + + def method_missing(method_id, *arguments) + @cgi.send(method_id, *arguments) rescue super + end + + private + def new_session + CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options).merge("new_session" => true)) + end + end + + class CgiResponse < AbstractResponse #:nodoc: + def initialize(cgi) + @cgi = cgi + super() + end + + def out + convert_content_type!(@headers) + $stdout.binmode if $stdout.respond_to?(:binmode) + print @cgi.header(@headers) + if @body.respond_to?(:call) + @body.call(self) + else + print @body + end + end + + private + def convert_content_type!(headers) + if headers["Content-Type"] + headers["type"] = headers["Content-Type"] + headers.delete "Content-Type" + end + end + end +end diff --git a/actionpack/lib/action_controller/dependencies.rb b/actionpack/lib/action_controller/dependencies.rb new file mode 100644 index 0000000000..6f092500d1 --- /dev/null +++ b/actionpack/lib/action_controller/dependencies.rb @@ -0,0 +1,49 @@ +module ActionController #:nodoc: + module Dependencies #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods + def model(*models) + require_dependencies(:model, models) + depend_on(:model, models) + end + + def service(*services) + require_dependencies(:service, services) + depend_on(:service, services) + end + + def observer(*observers) + require_dependencies(:observer, observers) + depend_on(:observer, observers) + instantiate_observers(observers) + end + + def dependencies_on(layer) # :nodoc: + read_inheritable_attribute("#{layer}_dependencies") + end + + def depend_on(layer, dependencies) + write_inheritable_array("#{layer}_dependencies", dependencies) + end + + private + def instantiate_observers(observers) + observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance } + end + + def require_dependencies(layer, dependencies) + dependencies.flatten.each do |dependency| + begin + require_or_load(dependency.to_s) + rescue LoadError + raise LoadError, "Missing #{layer} #{dependency}.rb" + end + end + end + end + end +end diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb new file mode 100644 index 0000000000..bd5c545dfb --- /dev/null +++ b/actionpack/lib/action_controller/filters.rb @@ -0,0 +1,279 @@ +module ActionController #:nodoc: + module Filters #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + base.class_eval { include ActionController::Filters::InstanceMethods } + end + + # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do + # authentication, caching, or auditing before the intended action is performed. Or to do localization or output + # compression after the action has been performed. + # + # Filters have access to the request, response, and all the instance variables set by other filters in the chain + # or by the action (in the case of after filters). Additionally, it's possible for a pre-processing before_filter + # to halt the processing before the intended action is processed by returning false. This is especially useful for + # filters like authentication where you're not interested in allowing the action to be performed if the proper + # credentials are not in order. + # + # == Filter inheritance + # + # Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without + # affecting the superclass. For example: + # + # class BankController < ActionController::Base + # before_filter :audit + # + # private + # def audit + # # record the action and parameters in an audit log + # end + # end + # + # class VaultController < BankController + # before_filter :verify_credentials + # + # private + # def verify_credentials + # # make sure the user is allowed into the vault + # end + # end + # + # Now any actions performed on the BankController will have the audit method called before. On the VaultController, + # first the audit method is called, then the verify_credentials method. If the audit method returns false, then + # verify_credentials and the intended action is 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| return false if controller.params["stop_action"] } + # end + # + # As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables. + # This means that the block has access to both the request and response objects complete with convenience methods for params, + # session, template, and assigns. Note: The inline method doesn't strictly has to be a block. Any object that responds to call + # and returns 1 or -1 on arity will do (such as a Proc or an Method object). + # + # == Filter chain ordering + # + # Using before_filter and after_filter 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 prepend_before_filter and prepend_after_filter. Filters added by these methods will be put at the + # beginning of their respective chain and executed before the rest. For example: + # + # class ShoppingController + # before_filter :verify_open_shop + # + # class CheckoutController + # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock + # + # The filter chain for the CheckoutController is now :ensure_items_in_cart, :ensure_items_in_stock, + # :verify_open_shop. So if either of the ensure filters return false, we'll never get around to see if the shop + # is open or not. + # + # You may pass multiple filter arguments of each type as well as a filter block. + # If a block is given, it is treated as the last argument. + # + # == Around filters + # + # In addition to the individual before and after filters, it's also possible to specify that a single object should handle + # both the before and after call. That's especially usefuly when you need to keep state active between the before and after, + # such as the example of a benchmark filter below: + # + # class WeblogController < ActionController::Base + # around_filter BenchmarkingFilter.new + # + # # Before this action is performed, BenchmarkingFilter#before(controller) is executed + # def index + # end + # # After this action has been performed, BenchmarkingFilter#after(controller) is executed + # end + # + # class BenchmarkingFilter + # def initialize + # @runtime + # end + # + # def before + # start_timer + # end + # + # def after + # stop_timer + # report_result + # end + # end + module ClassMethods + # The passed filters will be appended to the array of filters that's run _before_ actions + # on this controller are performed. + def append_before_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("before", filters) + end + + # The passed filters will be prepended to the array of filters that's run _before_ actions + # on this controller are performed. + def prepend_before_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("before", filters) + end + + # Short-hand for append_before_filter since that's the most common of the two. + alias :before_filter :append_before_filter + + # The passed filters will be appended to the array of filters that's run _after_ actions + # on this controller are performed. + def append_after_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("after", filters) + end + + # The passed filters will be prepended to the array of filters that's run _after_ actions + # on this controller are performed. + def prepend_after_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("after", filters) + end + + # Short-hand for append_after_filter since that's the most common of the two. + alias :after_filter :append_after_filter + + # The passed filters will have their +before+ method appended to the array of filters that's run both before actions + # on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all + # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like: + # + # B#before + # A#before + # A#after + # B#after + def append_around_filter(filters) + for filter in [filters].flatten + ensure_filter_responds_to_before_and_after(filter) + append_before_filter { |c| filter.before(c) } + prepend_after_filter { |c| filter.after(c) } + end + end + + # The passed filters will have their +before+ method prepended to the array of filters that's run both before actions + # on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all + # respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like: + # + # A#before + # B#before + # B#after + # A#after + def prepend_around_filter(filters) + for filter in [filters].flatten + ensure_filter_responds_to_before_and_after(filter) + prepend_before_filter { |c| filter.before(c) } + append_after_filter { |c| filter.after(c) } + end + end + + # Short-hand for append_around_filter since that's the most common of the two. + alias :around_filter :append_around_filter + + # Returns all the before filters for this class and all its ancestors. + def before_filters #:nodoc: + read_inheritable_attribute("before_filters") + end + + # Returns all the after filters for this class and all its ancestors. + def after_filters #:nodoc: + read_inheritable_attribute("after_filters") + end + + private + def append_filter_to_chain(condition, filters) + write_inheritable_array("#{condition}_filters", filters) + end + + def prepend_filter_to_chain(condition, filters) + write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters")) + end + + def ensure_filter_responds_to_before_and_after(filter) + unless filter.respond_to?(:before) && filter.respond_to?(:after) + raise ActionControllerError, "Filter object must respond to both before and after" + end + end + end + + module InstanceMethods # :nodoc: + def self.append_features(base) + super + base.class_eval { + alias_method :perform_action_without_filters, :perform_action + alias_method :perform_action, :perform_action_with_filters + } + end + + def perform_action_with_filters + return if before_action == false + perform_action_without_filters + after_action + end + + # Calls all the defined before-filter filters, which are added by using "before_filter :method". + # If any of the filters return false, no more filters will be executed and the action is aborted. + def before_action #:doc: + call_filters(self.class.before_filters) + end + + # Calls all the defined after-filter filters, which are added by using "after_filter :method". + # If any of the filters return false, no more filters will be executed. + def after_action #:doc: + call_filters(self.class.after_filters) + end + + private + def call_filters(filters) + filters.each do |filter| + if Symbol === filter + if self.send(filter) == false then return false end + elsif filter_block?(filter) + if filter.call(self) == false then return false end + elsif filter_class?(filter) + if filter.filter(self) == false then return false end + else + raise( + ActionControllerError, + "Filters need to be either a symbol, proc/method, or class implementing a static filter method" + ) + end + end + end + + def filter_block?(filter) + filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1) + end + + def filter_class?(filter) + filter.respond_to?("filter") + end + end + end +end diff --git a/actionpack/lib/action_controller/flash.rb b/actionpack/lib/action_controller/flash.rb new file mode 100644 index 0000000000..220ed8c77a --- /dev/null +++ b/actionpack/lib/action_controller/flash.rb @@ -0,0 +1,65 @@ +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 flash["notice"] = "Succesfully created" 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"] = "Succesfully created post" + # redirect_to :action => "display", :params => { "id" => post.id } + # end + # + # def display + # # doesn't need to assign the flash notice to the template, that's done automatically + # end + # end + # + # display.rhtml + # <% if @flash["notice"] %>
<%= @flash["notice"] %>
<% 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. + module Flash + def self.append_features(base) #:nodoc: + super + base.before_filter(:fire_flash) + base.after_filter(:clear_flash) + end + + protected + # Access the contents of the flash. Use flash["notice"] to read a notice you put there or + # flash["notice"] = "hello" to put a new one. + def flash #:doc: + if @session["flash"].nil? + @session["flash"] = {} + @session["flashes"] ||= 0 + end + @session["flash"] + end + + # Can be called by any action that would like to keep the current content of the flash around for one more action. + def keep_flash #:doc: + @session["flashes"] = 0 + end + + private + # Records that the contents of @session["flash"] was flashed to the action + def fire_flash + if @session["flash"] + @session["flashes"] += 1 unless @session["flash"].empty? + @assigns["flash"] = @session["flash"] + else + @assigns["flash"] = {} + end + end + + def clear_flash + if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1) + @session["flash"] = {} + @session["flashes"] = 0 + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/helpers.rb b/actionpack/lib/action_controller/helpers.rb new file mode 100644 index 0000000000..9c88582288 --- /dev/null +++ b/actionpack/lib/action_controller/helpers.rb @@ -0,0 +1,100 @@ +module ActionController #:nodoc: + module Helpers #:nodoc: + def self.append_features(base) + super + base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end } + base.extend(ClassMethods) + end + + # The template helpers serves to relieve the templates from including the same inline code again and again. It's a + # set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and + # Active Records (ActiveRecordHelper) that's available to all templates by default. + # + # It's also really easy to make your own helpers and it's much encouraged to keep the template files free + # from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers + # (often the common helpers) as they're used by the specific application. + # + # module MyHelper + # def hello_world() "hello world" end + # end + # + # MyHelper can now be included in a controller, like this: + # + # class MyController < ActionController::Base + # helper :my_helper + # end + # + # ...and, same as above, used in any template rendered from MyController, like this: + # + # Let's hear what the helper has to say: <%= hello_world %> + 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) + template_class.class_eval "include #{helper_module}" + end + + # Declare a helper. If you use this method in your controller, you don't + # have to do the +self.append_features+ incantation in your helper class. + # helper :foo + # requires 'foo_helper' and includes FooHelper in the template class. + # helper FooHelper + # includes FooHelper in the template class. + # helper { def foo() "#{bar} is the very best" end } + # evaluates the block in the template class, adding method #foo. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # does all three. + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when Module + add_template_helper(arg) + when String, Symbol + file_name = Inflector.underscore(arg.to_s.downcase) + '_helper' + class_name = Inflector.camelize(file_name) + begin + require_or_load(file_name) + rescue LoadError + raise LoadError, "Missing helper file helpers/#{file_name}.rb" + end + raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name) + add_template_helper(Object.const_get(class_name)) + else + raise ArgumentError, 'helper expects String, Symbol, or Module argument' + end + end + + # Evaluate block in template class if given. + template_class.module_eval(&block) if block_given? + end + + # Declare a controller method as a helper. For example, + # helper_method :link_to + # def link_to(name, options) ... end + # makes the link_to controller method available in the view. + def helper_method(*methods) + template_class.controller_delegate(*methods) + end + + # Declare a controller attribute as a helper. For example, + # helper_attr :name + # attr_accessor :name + # makes the name and name= controller methods available in the view. + # The is a convenience wrapper for helper_method. + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + private + def inherited(child) + inherited_without_helper(child) + begin + child.helper(child.controller_name) + rescue LoadError + # No default helper available for this controller + end + end + end + end +end diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb new file mode 100644 index 0000000000..7ae25ddabd --- /dev/null +++ b/actionpack/lib/action_controller/layout.rb @@ -0,0 +1,149 @@ +module ActionController #:nodoc: + module Layout #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + base.class_eval do + alias_method :render_without_layout, :render + alias_method :render, :render_with_layout + 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 is only mentioned in one place, like this: + # + # + # <%= @content_for_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: + # + # + # hello world + # + # + # == 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: + # + #

<%= @page_title %>

+ # <%= @content_for_layout %> + # + # ...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: + # + #

Welcome

+ # Off-world colonies offers you a chance to start a new life + # + # == Inheritance for layouts + # + # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples: + # + # class BankController < ActionController::Base + # layout "layouts/bank_standard" + # + # class InformationController < BankController + # + # class VaultController < BankController + # layout :access_level_layout + # + # class EmployeeController < BankController + # layout nil + # + # The InformationController uses "layouts/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 path: + # + # class WeblogController < ActionController::Base + # layout "layouts/weblog_standard" + # + # == Avoiding the use of a layout + # + # If you have a layout that by default is applied to all the actions of a controller, you still have the option to rendering + # a given action without a layout. Just use the method render_without_layout, which works just like Base.render -- + # it just doesn't apply any layouts. + module ClassMethods + # If a layout is specified, all actions rendered through render and render_action will have their result assigned + # to @content_for_layout, which can then be used by the layout to insert their contents with + # <%= @content_for_layout %>. 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) + write_inheritable_attribute "layout", template_name + 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 layout "weblog/standard" will return + # weblog/standard, but layout "standard" will return layouts/standard. + def active_layout(passed_layout = nil) + layout = passed_layout || self.class.read_inheritable_attribute("layout") + active_layout = case layout + when Symbol then send(layout) + when Proc then layout.call(self) + when String then layout + end + active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout + end + + def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc: + if layout ||= active_layout + add_variables_to_assigns + logger.info("Rendering #{template_name} within #{layout}") unless logger.nil? + @content_for_layout = @template.render_file(template_name, true) + render_without_layout(layout, status) + else + render_without_layout(template_name, status) + end + end + end +end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb new file mode 100755 index 0000000000..1085066ea0 --- /dev/null +++ b/actionpack/lib/action_controller/request.rb @@ -0,0 +1,99 @@ +module ActionController + # These methods are available in both the production and test Request objects. + class AbstractRequest + # Returns both GET and POST parameters in a single hash. + def parameters + @parameters ||= request_parameters.update(query_parameters) + end + + def method + env['REQUEST_METHOD'].downcase.intern + end + + def get? + method == :get + end + + def post? + method == :post + end + + def put? + method == :put + end + + def delete? + method == :delete + end + + # Determine originating IP address. REMOTE_ADDR is the standard + # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or + # HTTP_X_FORWARDED_FOR are set by proxies so check for these before + # falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma- + # delimited list in the case of multiple chained proxies; the first is + # the originating IP. + def remote_ip + if env['HTTP_CLIENT_IP'] + env['HTTP_CLIENT_IP'] + elsif env['HTTP_X_FORWARDED_FOR'] + remote_ip = env['HTTP_X_FORWARDED_FOR'].split(',').reject { |ip| + ip =~ /^unknown$|^(10|172\.16|192\.168)\./i + }.first + + remote_ip ? remote_ip.strip : env['REMOTE_ADDR'] + else + env['REMOTE_ADDR'] + end + end + + def request_uri + env["REQUEST_URI"] + end + + def protocol + port == 443 ? "https://" : "http://" + end + + def path + request_uri ? request_uri.split("?").first : "" + end + + def port + env["SERVER_PORT"].to_i + end + + def host_with_port + if env['HTTP_HOST'] + env['HTTP_HOST'] + elsif (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443) + host + else + host + ":#{port}" + end + end + + #-- + # Must be implemented in the concrete request + #++ + def query_parameters + end + + def request_parameters + end + + def env + end + + def host + end + + def cookies + end + + def session + end + + def reset_session + end + end +end diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb new file mode 100644 index 0000000000..c0933b2666 --- /dev/null +++ b/actionpack/lib/action_controller/rescue.rb @@ -0,0 +1,94 @@ +module ActionController #:nodoc: + # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view + # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view + # is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too + # could the decision on whether something is a public or a developer request. + # + # You can tailor the rescuing behavior and appearance by overwriting the following two stub methods. + module Rescue + def self.append_features(base) #:nodoc: + super + base.class_eval do + alias_method :perform_action_without_rescue, :perform_action + alias_method :perform_action, :perform_action_with_rescue + end + end + + protected + # Exception handler called when the performance of an action raises an exception. + def rescue_action(exception) + log_error(exception) unless logger.nil? + + if consider_all_requests_local || local_request? + rescue_action_locally(exception) + else + rescue_action_in_public(exception) + end + end + + # Overwrite to implement custom logging of errors. By default logs as fatal. + def log_error(exception) #:doc: + if ActionView::TemplateError === exception + logger.fatal(exception.to_s) + else + logger.fatal( + "\n\n#{exception.class} (#{exception.message}):\n " + + clean_backtrace(exception).join("\n ") + + "\n\n" + ) + end + end + + # Overwrite to implement public exception handling (for requests answering false to local_request?). + def rescue_action_in_public(exception) #:doc: + render_text "

Application error (Rails)

" + end + + # Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than + # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging + # remotely. + def local_request? #:doc: + @request.remote_addr == "127.0.0.1" + end + + # Renders a detailed diagnostics screen on action exceptions. + def rescue_action_locally(exception) + @exception = exception + @rescues_path = File.dirname(__FILE__) + "/templates/rescues/" + add_variables_to_assigns + @contents = @template.render_file(template_path_for_local_rescue(exception), false) + + @headers["Content-Type"] = "text/html" + render_file(rescues_path("layout"), "500 Internal Error") + end + + private + def perform_action_with_rescue #:nodoc: + begin + perform_action_without_rescue + rescue Exception => exception + rescue_action(exception) + end + end + + def rescues_path(template_name) + File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml" + end + + def template_path_for_local_rescue(exception) + rescues_path( + case exception + when MissingTemplate then "missing_template" + when UnknownAction then "unknown_action" + when ActionView::TemplateError then "template_error" + else "diagnostics" + end + ) + end + + def clean_backtrace(exception) + base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../") + exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") } + end + end +end diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb new file mode 100755 index 0000000000..836dd5ffef --- /dev/null +++ b/actionpack/lib/action_controller/response.rb @@ -0,0 +1,15 @@ +module ActionController + class AbstractResponse #:nodoc: + DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } + attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params + + def initialize + @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] + end + + def redirect(to_url) + @headers["Status"] = "302 Moved" + @headers["location"] = to_url + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/scaffolding.rb b/actionpack/lib/action_controller/scaffolding.rb new file mode 100644 index 0000000000..49b35b37df --- /dev/null +++ b/actionpack/lib/action_controller/scaffolding.rb @@ -0,0 +1,183 @@ +module ActionController + module Scaffolding # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + # Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions + # for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come + # with both controller logic and default templates that through introspection already know which fields to display + # and which input types to use. Example: + # + # class WeblogController < ActionController::Base + # scaffold :entry + # end + # + # This tiny piece of code will add all of the following methods to the controller: + # + # class WeblogController < ActionController::Base + # def index + # list + # end + # + # def list + # @entries = Entry.find_all + # render_scaffold "list" + # end + # + # def show + # @entry = Entry.find(@params["id"]) + # render_scaffold + # end + # + # def destroy + # Entry.find(@params["id"]).destroy + # redirect_to :action => "list" + # end + # + # def new + # @entry = Entry.new + # render_scaffold + # end + # + # def create + # @entry = Entry.new(@params["entry"]) + # if @entry.save + # flash["notice"] = "Entry was succesfully created" + # redirect_to :action => "list" + # else + # render "entry/new" + # end + # end + # + # def edit + # @entry = Entry.find(@params["id"]) + # render_scaffold + # end + # + # def update + # @entry = Entry.find(@params["entry"]["id"]) + # @entry.attributes = @params["entry"] + # + # if @entry.save + # flash["notice"] = "Entry was succesfully updated" + # redirect_to :action => "show/" + @entry.id.to_s + # else + # render "entry/edit" + # end + # end + # end + # + # The render_scaffold method will first check to see if you've made your own template (like "weblog/show.rhtml" for + # the show action) and if not, then render the generic template for that action. This gives you the possibility of using the + # scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template + # and one action at a time while relying on the rest of the scaffolded templates and actions. + module ClassMethods + # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless + # one is specifically provide through options[:class_name]. So scaffold :post would use Post as the class + # and @post/@posts for the instance variables. + # + # It's possible to use more than one scaffold in a single controller by specifying options[:suffix] = true. This will + # make scaffold :post, :suffix => true use method names like list_post, show_post, and create_post + # instead of just list, show, and post. If suffix is used, then no index method is added. + def scaffold(model_id, options = {}) + validate_options([ :class_name, :suffix ], options.keys) + + require "#{model_id.id2name}" rescue logger.warn "Couldn't auto-require #{model_id.id2name}.rb" unless logger.nil? + + singular_name = model_id.id2name + class_name = options[:class_name] || Inflector.camelize(singular_name) + plural_name = Inflector.pluralize(singular_name) + suffix = options[:suffix] ? "_#{singular_name}" : "" + + unless options[:suffix] + module_eval <<-"end_eval", __FILE__, __LINE__ + def index + list + end + end_eval + end + + module_eval <<-"end_eval", __FILE__, __LINE__ + def list#{suffix} + @#{plural_name} = #{class_name}.find_all + render#{suffix}_scaffold "list#{suffix}" + end + + def show#{suffix} + @#{singular_name} = #{class_name}.find(@params["id"]) + render#{suffix}_scaffold + end + + def destroy#{suffix} + #{class_name}.find(@params["id"]).destroy + redirect_to :action => "list#{suffix}" + end + + def new#{suffix} + @#{singular_name} = #{class_name}.new + render#{suffix}_scaffold + end + + def create#{suffix} + @#{singular_name} = #{class_name}.new(@params["#{singular_name}"]) + if @#{singular_name}.save + flash["notice"] = "#{class_name} was succesfully created" + redirect_to :action => "list#{suffix}" + else + render "#{singular_name}/new#{suffix}" + end + end + + def edit#{suffix} + @#{singular_name} = #{class_name}.find(@params["id"]) + render#{suffix}_scaffold + end + + def update#{suffix} + @#{singular_name} = #{class_name}.find(@params["#{singular_name}"]["id"]) + @#{singular_name}.attributes = @params["#{singular_name}"] + + if @#{singular_name}.save + flash["notice"] = "#{class_name} was succesfully updated" + redirect_to :action => "show#{suffix}/" + @#{singular_name}.id.to_s + else + render "#{singular_name}/edit#{suffix}" + end + end + + private + def render#{suffix}_scaffold(action = caller_method_name(caller)) + if template_exists?("\#{controller_name}/\#{action}") + render_action(action) + else + @scaffold_class = #{class_name} + @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}" + @scaffold_suffix = "#{suffix}" + add_instance_variables_to_assigns + + @content_for_layout = @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false) + self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout")) + end + end + + def scaffold_path(template_name) + File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml" + end + + def caller_method_name(caller) + caller.first.scan(/`(.*)'/).first.first # ' ruby-mode + end + end_eval + end + + private + # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty? + end + end + end +end diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb new file mode 100644 index 0000000000..c144f62e35 --- /dev/null +++ b/actionpack/lib/action_controller/session/active_record_store.rb @@ -0,0 +1,72 @@ +begin + +require 'active_record' +require 'cgi' +require 'cgi/session' + +# Contributed by Tim Bates +class CGI + class Session + # ActiveRecord database based session storage class. + # + # Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database + # has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text). + # The session data is stored in the +data+ column in YAML format; the user is responsible for ensuring that + # only data that can be YAMLized is stored in the session. + class ActiveRecordStore + # The ActiveRecord class which corresponds to the database table. + class Session < ActiveRecord::Base + serialize :data + # Isn't this class definition beautiful? + end + + # Create a new ActiveRecordStore 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. + # + # +option+ is currently ignored as no options are recognized. + # + # This session's ActiveRecord database row will be created if it does not exist, or opened if it does. + def initialize(session, option=nil) + @session = Session.find_first(["sessid = '%s'", session.session_id]) + if @session + @data = @session.data + else + @session = Session.new("sessid" => session.session_id, "data" => {}) + end + end + + # Update and close the session's ActiveRecord object. + def close + return unless @session + update + @session = nil + end + + # Close and destroy the session's ActiveRecord object. + def delete + return unless @session + @session.destroy + @session = nil + end + + # Restore session state from the session's ActiveRecord object. + def restore + return unless @session + @data = @session.data + end + + # Save session state in the session's ActiveRecord object. + def update + return unless @session + @session.data = @data + @session.save + end + end #ActiveRecordStore + end #Session +end #CGI + +rescue LoadError + # Couldn't load Active Record, so don't make this store available +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb new file mode 100644 index 0000000000..6005b8b2b3 --- /dev/null +++ b/actionpack/lib/action_controller/session/drb_server.rb @@ -0,0 +1,9 @@ +#!/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' + +DRb.start_service('druby://127.0.0.1:9192', Hash.new) +DRb.thread.join \ No newline at end of file diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb new file mode 100644 index 0000000000..8ea23e8fff --- /dev/null +++ b/actionpack/lib/action_controller/session/drb_store.rb @@ -0,0 +1,31 @@ +require 'cgi' +require 'cgi/session' +require 'drb' + +class CGI #:nodoc:all + class Session + class DRbStore + @@session_data = DRbObject.new(nil, 'druby://localhost:9192') + + def initialize(session, option=nil) + @session_id = session.session_id + end + + def restore + @h = @@session_data[@session_id] || {} + end + + def update + @@session_data[@session_id] = @h + end + + def close + update + end + + def delete + @@session_data.delete(@session_id) + end + end + end +end diff --git a/actionpack/lib/action_controller/support/class_attribute_accessors.rb b/actionpack/lib/action_controller/support/class_attribute_accessors.rb new file mode 100644 index 0000000000..786dcf98cb --- /dev/null +++ b/actionpack/lib/action_controller/support/class_attribute_accessors.rb @@ -0,0 +1,57 @@ +# Extends the class object with class and instance accessors for class attributes, +# just like the native attr* accessors for instance attributes. +class Class # :nodoc: + def cattr_reader(*syms) + syms.each do |sym| + class_eval <<-EOS + if ! defined? @@#{sym.id2name} + @@#{sym.id2name} = nil + end + + def self.#{sym.id2name} + @@#{sym} + end + + def #{sym.id2name} + @@#{sym} + end + + def call_#{sym.id2name} + case @@#{sym.id2name} + when Symbol then send(@@#{sym}) + when Proc then @@#{sym}.call(self) + when String then @@#{sym} + else nil + end + end + EOS + end + end + + def cattr_writer(*syms) + syms.each do |sym| + class_eval <<-EOS + if ! defined? @@#{sym.id2name} + @@#{sym.id2name} = nil + end + + def self.#{sym.id2name}=(obj) + @@#{sym.id2name} = obj + end + + def self.set_#{sym.id2name}(obj) + @@#{sym.id2name} = obj + end + + def #{sym.id2name}=(obj) + @@#{sym} = obj + end + EOS + end + end + + def cattr_accessor(*syms) + cattr_reader(*syms) + cattr_writer(*syms) + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/support/class_inheritable_attributes.rb b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb new file mode 100644 index 0000000000..7f061fdf1b --- /dev/null +++ b/actionpack/lib/action_controller/support/class_inheritable_attributes.rb @@ -0,0 +1,37 @@ +# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of +# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements +# to, for example, an array without those additions being shared with either their parent, siblings, or +# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy. +module ClassInheritableAttributes # :nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + end + + module ClassMethods # :nodoc: + @@classes ||= {} + + def inheritable_attributes + @@classes[self] ||= {} + end + + def write_inheritable_attribute(key, value) + inheritable_attributes[key] = value + end + + def write_inheritable_array(key, elements) + write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil? + write_inheritable_attribute(key, read_inheritable_attribute(key) + elements) + end + + def read_inheritable_attribute(key) + inheritable_attributes[key] + end + + private + def inherited(child) + @@classes[child] = inheritable_attributes.dup + end + + end +end diff --git a/actionpack/lib/action_controller/support/clean_logger.rb b/actionpack/lib/action_controller/support/clean_logger.rb new file mode 100644 index 0000000000..1a36562892 --- /dev/null +++ b/actionpack/lib/action_controller/support/clean_logger.rb @@ -0,0 +1,10 @@ +require 'logger' + +class Logger #:nodoc: + private + remove_const "Format" + Format = "%s\n" + def format_message(severity, timestamp, msg, progname) + Format % [msg] + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/support/cookie_performance_fix.rb b/actionpack/lib/action_controller/support/cookie_performance_fix.rb new file mode 100644 index 0000000000..225cea1905 --- /dev/null +++ b/actionpack/lib/action_controller/support/cookie_performance_fix.rb @@ -0,0 +1,121 @@ +CGI.module_eval { remove_const "Cookie" } + +class CGI #:nodoc: + # This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below. + # It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on + # http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14 + class Cookie < DelegateClass(Array) + # Create a new CGI::Cookie object. + # + # The contents of the cookie can be specified as a +name+ and one + # or more +value+ arguments. Alternatively, the contents can + # be specified as a single hash argument. The possible keywords of + # this hash are as follows: + # + # name:: the name of the cookie. Required. + # value:: the cookie's value or list of values. + # path:: the path for which this cookie applies. Defaults to the + # base directory of the CGI script. + # domain:: the domain for which this cookie applies. + # expires:: the time at which this cookie expires, as a +Time+ object. + # secure:: whether this cookie is a secure cookie or not (default to + # false). Secure cookies are only transmitted to HTTPS + # servers. + # + # These keywords correspond to attributes of the cookie object. + def initialize(name = "", *value) + options = if name.kind_of?(String) + { "name" => name, "value" => value } + else + name + end + unless options.has_key?("name") + raise ArgumentError, "`name' required" + end + + @name = options["name"] + @value = Array(options["value"]) + # simple support for IE + if options["path"] + @path = options["path"] + else + %r|^(.*/)|.match(ENV["SCRIPT_NAME"]) + @path = ($1 or "") + end + @domain = options["domain"] + @expires = options["expires"] + @secure = options["secure"] == true ? true : false + + super(@value) + end + + def __setobj__(obj) + @_dc_obj = obj + end + + attr_accessor("name", "value", "path", "domain", "expires") + attr_reader("secure") + + # Set whether the Cookie is a secure cookie or not. + # + # +val+ must be a boolean. + def secure=(val) + @secure = val if val == true or val == false + @secure + end + + # Convert the Cookie to its string representation. + def to_s + buf = "" + buf += @name + '=' + + if @value.kind_of?(String) + buf += CGI::escape(@value) + else + buf += @value.collect{|v| CGI::escape(v) }.join("&") + end + + if @domain + buf += '; domain=' + @domain + end + + if @path + buf += '; path=' + @path + end + + if @expires + buf += '; expires=' + CGI::rfc1123_date(@expires) + end + + if @secure == true + buf += '; secure' + end + + buf + end + + # Parse a raw cookie string into a hash of cookie-name=>Cookie + # pairs. + # + # cookies = CGI::Cookie::parse("raw_cookie_string") + # # { "name1" => cookie1, "name2" => cookie2, ... } + # + def self.parse(raw_cookie) + cookies = Hash.new([]) + return cookies unless raw_cookie + + raw_cookie.split(/; /).each do |pairs| + name, values = pairs.split('=',2) + next unless name and values + name = CGI::unescape(name) + values ||= "" + values = values.split('&').collect{|v| CGI::unescape(v) } + unless cookies.has_key?(name) + cookies[name] = new({ "name" => name, "value" => values }) + end + end + + cookies + end + end # class Cookie +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/support/inflector.rb b/actionpack/lib/action_controller/support/inflector.rb new file mode 100644 index 0000000000..05ff4fede9 --- /dev/null +++ b/actionpack/lib/action_controller/support/inflector.rb @@ -0,0 +1,78 @@ +# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without, +# and class names to foreign keys. +module Inflector + extend self + + def pluralize(word) + result = word.dup + plural_rules.each do |(rule, replacement)| + break if result.gsub!(rule, replacement) + end + return result + end + + def singularize(word) + result = word.dup + singular_rules.each do |(rule, replacement)| + break if result.gsub!(rule, replacement) + end + return result + end + + def camelize(lower_case_and_underscored_word) + lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase} + end + + def underscore(camel_cased_word) + camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase + end + + def demodulize(class_name_in_module) + class_name_in_module.gsub(/^.*::/, '') + end + + def tableize(class_name) + pluralize(underscore(class_name)) + end + + def classify(table_name) + camelize(singularize(table_name)) + end + + def foreign_key(class_name, separate_class_name_and_id_with_underscore = true) + Inflector.underscore(Inflector.demodulize(class_name)) + + (separate_class_name_and_id_with_underscore ? "_id" : "id") + end + + private + def plural_rules #:doc: + [ + [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address + [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency + [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife + [/sis$/, 'ses'], # basis, diagnosis + [/([ti])um$/, '\1a'], # datum, medium + [/person$/, 'people'], # person, salesperson + [/man$/, 'men'], # man, woman, spokesman + [/child$/, 'children'], # child + [/s$/, 's'], # no change (compatibility) + [/$/, 's'] + ] + end + + def singular_rules #:doc: + [ + [/(x|ch|ss)es$/, '\1'], + [/([^aeiouy]|qu)ies$/, '\1y'], + [/([lr])ves$/, '\1f'], + [/([^f])ves$/, '\1fe'], + [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'], + [/([ti])a$/, '\1um'], + [/people$/, 'person'], + [/men$/, 'man'], + [/status$/, 'status'], + [/children$/, 'child'], + [/s$/, ''] + ] + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml new file mode 100644 index 0000000000..f1b4a2a1dd --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/_request_and_response.rhtml @@ -0,0 +1,28 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + request_parameters_without_action = @request.parameters.clone + request_parameters_without_action.delete("action") + request_parameters_without_action.delete("controller") + + request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n") + session_dump = @request.session.instance_variable_get("@data").inspect.gsub(/,/, ",\n") + response_dump = @response.inspect.gsub(/,/, ",\n") + + template_assigns = @response.template.instance_variable_get("@assigns") + %w( response exception template session request template_root template_class url ignore_missing_templates logger cookies headers params ).each { |t| template_assigns.delete(t) } + template_dump = template_assigns.inspect.gsub(/,/, ",\n") +%> + +

Request

+

Parameters: <%=h request_dump == "{}" ? "None" : request_dump %>

+ +

Show session dump

+ + + +

Response

+Headers: <%=h @response.headers.inspect.gsub(/,/, ",\n") %>
+ +

Show template parameters

+ diff --git a/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml new file mode 100644 index 0000000000..4eb1ed0439 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/diagnostics.rhtml @@ -0,0 +1,22 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + clean_backtrace = @exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/../config/environments/../../", "") } + app_trace = clean_backtrace.reject { |line| line[0..6] == "vendor/" || line.include?("dispatch.cgi") } + framework_trace = clean_backtrace - app_trace +%> + +

+ <%=h @exception.class.to_s %> in + <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %> +

+

<%=h @exception.message %>

+ +<% unless app_trace.empty? %>
<%=h app_trace.collect { |line| line.gsub("/../", "") }.join("\n") %>
<% end %> + +<% unless framework_trace.empty? %> + Show framework trace + +<% end %> + +<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %> diff --git a/actionpack/lib/action_controller/templates/rescues/layout.rhtml b/actionpack/lib/action_controller/templates/rescues/layout.rhtml new file mode 100644 index 0000000000..d38f3e67f9 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/layout.rhtml @@ -0,0 +1,29 @@ + + + Action Controller: Exception caught + + + + +<%= @contents %> + + + \ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml new file mode 100644 index 0000000000..dbfdf76947 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/missing_template.rhtml @@ -0,0 +1,2 @@ +

Template is missing

+

<%=h @exception.message %>

diff --git a/actionpack/lib/action_controller/templates/rescues/template_error.rhtml b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml new file mode 100644 index 0000000000..326fd0b057 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/template_error.rhtml @@ -0,0 +1,26 @@ +<% + base_dir = File.expand_path(File.dirname(__FILE__)) + + framework_trace = @exception.original_exception.backtrace.collect do |line| + line.gsub(base_dir, "").gsub("/../config/environments/../../", "") + end +%> + +

+ <%=h @exception.original_exception.class.to_s %> in + <%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %> +

+ +

+ Showing <%=h @exception.file_name %> where line #<%=h @exception.line_number %> raised + <%=h @exception.message %> +

+ +
<%=h @exception.source_extract %>
+ +

<%=h @exception.sub_template_message %>

+ +Show template trace + + +<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %> diff --git a/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml new file mode 100644 index 0000000000..683379da10 --- /dev/null +++ b/actionpack/lib/action_controller/templates/rescues/unknown_action.rhtml @@ -0,0 +1,2 @@ +

Unknown action

+

<%=h @exception.message %>

diff --git a/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml new file mode 100644 index 0000000000..1c7f4d9770 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/edit.rhtml @@ -0,0 +1,6 @@ +

Editing <%= @scaffold_singular_name %>

+ +<%= form(@scaffold_singular_name, :action => "../update" + @scaffold_suffix) %> + +<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> | +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml new file mode 100644 index 0000000000..511054abe8 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/layout.rhtml @@ -0,0 +1,29 @@ + + + Scaffolding + + + + +<%= @content_for_layout %> + + + \ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/scaffolds/list.rhtml b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml new file mode 100644 index 0000000000..33af7079b2 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/list.rhtml @@ -0,0 +1,24 @@ +

Listing <%= @scaffold_plural_name %>

+ + + + <% for column in @scaffold_class.content_columns %> + + <% end %> + + +<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %> + + <% for column in @scaffold_class.content_columns %> + + <% end %> + + + + +<% end %> +
<%= column.human_name %>
<%= entry.send(column.name) %><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry.id %><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry.id %><%= link_to "Destroy", :action => "destroy#{@scaffold_suffix}", :id => entry.id %>
+ +
+ +<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/templates/scaffolds/new.rhtml b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml new file mode 100644 index 0000000000..02f52e72f5 --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/new.rhtml @@ -0,0 +1,5 @@ +

New <%= @scaffold_singular_name %>

+ +<%= form(@scaffold_singular_name, :action => "create" + @scaffold_suffix) %> + +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> \ No newline at end of file diff --git a/actionpack/lib/action_controller/templates/scaffolds/show.rhtml b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml new file mode 100644 index 0000000000..10c46342fd --- /dev/null +++ b/actionpack/lib/action_controller/templates/scaffolds/show.rhtml @@ -0,0 +1,9 @@ +<% for column in @scaffold_class.content_columns %> +

+ <%= column.human_name %>: + <%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %> +

+<% end %> + +<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> | +<%= link_to "Back", :action => "list#{@scaffold_suffix}" %> diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb new file mode 100644 index 0000000000..969f2573c5 --- /dev/null +++ b/actionpack/lib/action_controller/test_process.rb @@ -0,0 +1,195 @@ +require File.dirname(__FILE__) + '/assertions/action_pack_assertions' +require File.dirname(__FILE__) + '/assertions/active_record_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 + end + + class TestRequest < AbstractRequest #:nodoc: + attr_writer :cookies + attr_accessor :query_parameters, :request_parameters, :session, :env + attr_accessor :host, :path, :request_uri, :remote_addr + + def initialize(query_parameters = nil, request_parameters = nil, session = nil) + @query_parameters = query_parameters || {} + @request_parameters = request_parameters || {} + @session = session || TestSession.new + + initialize_containers + initialize_default_values + + super() + end + + def reset_session + @session = {} + end + + def cookies + @cookies.freeze + end + + def action=(action_name) + @query_parameters.update({ "action" => action_name }) + @parameters = nil + end + + def request_uri=(uri) + @request_uri = uri + @path = uri.split("?").first + end + + private + def initialize_containers + @env, @cookies = {}, {} + end + + def initialize_default_values + @host = "test.host" + @request_uri = "/" + @remote_addr = "127.0.0.1" + @env["SERVER_PORT"] = 80 + end + end + + class TestResponse < AbstractResponse #:nodoc: + # the class attribute ties a TestResponse to the assertions + class << self + attr_accessor :assertion_target + end + + # initializer + def initialize + TestResponse.assertion_target=self# if TestResponse.assertion_target.nil? + super() + end + + # the response code of the request + def response_code + headers['Status'][0,3].to_i rescue 0 + 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 server_error? + (500..599).include?(response_code) + end + + # returns the redirection location or nil + def redirect_url + redirect? ? headers['location'] : nil + end + + # does the redirect location match this regexp pattern? + def redirect_url_match?( pattern ) + return false if redirect_url.nil? + p = Regexp.new(pattern) if pattern.class == String + p = pattern if pattern.class == Regexp + return false if p.nil? + p.match(redirect_url) != nil + end + + # returns the template path of the file which was used to + # render this response (or nil) + def rendered_file(with_controller=false) + unless template.first_render.nil? + unless with_controller + template.first_render + else + template.first_render.split('/').last || template.first_render + end + end + end + + # was this template rendered by a file? + def rendered_with_file? + !rendered_file.nil? + end + + # a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!) + def flash + session['flash'] || {} + end + + # do we have a flash? + def has_flash? + !session['flash'].nil? + 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 +end + + class TestSession #:nodoc: + def initialize(attributes = {}) + @attributes = attributes + end + + def [](key) + @attributes[key] + end + + def []=(key, value) + @attributes[key] = value + end + + def update() end + def close() end + def delete() @attributes = {} end + end +end + +class Test::Unit::TestCase #:nodoc: + private + # execute the request and set/volley the response + def process(action, parameters = nil, session = nil) + @request.action = action.to_s + @request.parameters.update(parameters) unless parameters.nil? + @request.session = ActionController::TestSession.new(session) unless session.nil? + @controller.process(@request, @response) + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb new file mode 100644 index 0000000000..78638da39e --- /dev/null +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -0,0 +1,170 @@ +module ActionController + # Rewrites urls for Base.redirect_to and Base.url_for in the controller. + class UrlRewriter #:nodoc: + VALID_OPTIONS = [:action, :action_prefix, :action_suffix, :module, :controller, :controller_prefix, :anchor, :params, :path_params, :id, :only_path, :overwrite_params ] + + def initialize(request, controller, action) + @request, @controller, @action = request, controller, action + @rewritten_path = @request.path ? @request.path.dup : "" + end + + def rewrite(options = {}) + validate_options(VALID_OPTIONS, options.keys) + + rewrite_url( + rewrite_path(@rewritten_path, options), + options + ) + end + + def to_s + to_str + end + + def to_str + "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@controller}, #{@action}, #{@request.parameters.inspect}" + end + + private + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty? + end + + def rewrite_url(path, options) + rewritten_url = "" + rewritten_url << @request.protocol unless options[:only_path] + rewritten_url << @request.host_with_port unless options[:only_path] + + rewritten_url << path + rewritten_url << build_query_string(new_parameters(options)) if options[:params] || options[:overwrite_params] + rewritten_url << "##{options[:anchor]}" if options[:anchor] + return rewritten_url + end + + def rewrite_path(path, options) + include_id_in_path_params(options) + + path = rewrite_action(path, options) if options[:action] || options[:action_prefix] + path = rewrite_path_params(path, options) if options[:path_params] + path = rewrite_controller(path, options) if options[:controller] || options[:controller_prefix] + return path + end + + def rewrite_path_params(path, options) + index_action = options[:action] == 'index' || options[:action].nil? && @action == 'index' + id_only = options[:path_params].size == 1 && options[:path_params]['id'] + + if index_action && id_only + path += '/' unless path[-1..-1] == '/' + path += "index/#{options[:path_params]['id']}" + path + else + options[:path_params].inject(path) do |path, pair| + if options[:action].nil? && @request.parameters[pair.first] + path.sub(/\b#{@request.parameters[pair.first]}\b/, pair.last.to_s) + else + path += "/#{pair.last}" + end + end + end + end + + def rewrite_action(path, options) + # This regex assumes that "index" actions won't be included in the URL + all, controller_prefix, action_prefix, action_suffix = + /^\/(.*)#{@controller}\/(.*)#{@action == "index" ? "" : @action}(.*)/.match(path).to_a + + if @action == "index" + if action_prefix == "index" + # we broke the parsing assumption that this would be excluded, so + # don't tell action_name about our little boo-boo + path = path.sub(action_prefix, action_name(options, nil)) + elsif action_prefix && !action_prefix.empty? + path = path.sub(action_prefix, action_name(options, action_prefix)) + else + path = path.sub(%r(#{@controller}/?), @controller + "/" + action_name(options)) # " ruby-mode + end + else + path = path.sub((action_prefix || "") + @action + (action_suffix || ""), action_name(options, action_prefix)) + end + + if options[:controller_prefix] && !options[:controller] + ensure_slash_suffix(options, :controller_prefix) + if controller_prefix + path = path.sub(controller_prefix, options[:controller_prefix]) + else + path = options[:controller_prefix] + path + end + end + + return path + end + + def rewrite_controller(path, options) + all, controller_prefix = /^\/(.*?)#{@controller}/.match(path).to_a + path = "/" + path << controller_name(options, controller_prefix) + path << action_name(options) if options[:action] + path << path_params_in_list(options) if options[:path_params] + return path + end + + def action_name(options, action_prefix = nil, action_suffix = nil) + ensure_slash_suffix(options, :action_prefix) + ensure_slash_prefix(options, :action_suffix) + + prefix = options[:action_prefix] || action_prefix || "" + suffix = options[:action] == "index" ? "" : (options[:action_suffix] || action_suffix || "") + name = (options[:action] == "index" ? "" : options[:action]) || "" + + return prefix + name + suffix + end + + def controller_name(options, controller_prefix) + options[:controller_prefix] = "#{options[:module]}/#{options[:controller_prefix]}" if options[:module] + ensure_slash_suffix(options, :controller_prefix) + controller_name = options[:controller_prefix] || controller_prefix || "" + controller_name << (options[:controller] + "/") if options[:controller] + return controller_name + end + + def path_params_in_list(options) + options[:path_params].inject("") { |path, pair| path += "/#{pair.last}" } + end + + def ensure_slash_suffix(options, key) + options[key] = options[key] + "/" if options[key] && !options[key].empty? && options[key][-1..-1] != "/" + end + + def ensure_slash_prefix(options, key) + options[key] = "/" + options[key] if options[key] && !options[key].empty? && options[key][0..1] != "/" + end + + def include_id_in_path_params(options) + options[:path_params] = (options[:path_params] || {}).merge({"id" => options[:id]}) if options[:id] + end + + def new_parameters(options) + parameters = options[:params] || existing_parameters + parameters.update(options[:overwrite_params]) if options[:overwrite_params] + parameters.reject { |key,value| value.nil? } + end + + def existing_parameters + @request.parameters.reject { |key, value| %w( id action controller).include?(key) } + end + + # Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll + # be added as a path element instead of a regular parameter pair. + def build_query_string(hash) + elements = [] + query_string = "" + + hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" } + unless elements.empty? then query_string << ("?" + elements.join("&")) end + + return query_string + end + end +end -- cgit v1.2.3