diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 7c10fcbb8a17046d8c11c9158371e2089cd0278f..b8624fd1bab81c235bdbc3d6575704297cdaf9a2 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -6,6 +6,7 @@ module ActionController autoload :Base autoload :Caching + autoload :HTTP autoload :Metal autoload :Middleware diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 6ce78f34b5d47a427bb3303736a110cb0b23c3d5..71425cd54282e9f3ab6efd2b9d88ee4f64876e2f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -171,7 +171,7 @@ module ActionController class Base < Metal abstract! - # Shortcut helper that returns all the ActionController modules except the ones passed in the argument: + # Shortcut helper that returns all the ActionController::Base modules except the ones passed in the argument: # # class MetalController # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| diff --git a/actionpack/lib/action_controller/http.rb b/actionpack/lib/action_controller/http.rb new file mode 100644 index 0000000000000000000000000000000000000000..252a652cd96d86754c77b34e98dfddb72e605838 --- /dev/null +++ b/actionpack/lib/action_controller/http.rb @@ -0,0 +1,134 @@ +require "action_controller/log_subscriber" + +module ActionController + # HTTP Controller is a lightweight version of ActionController::Base, + # created for applications that don't require all functionality that a complete + # \Rails controller provides, allowing you to create faster controllers. The + # main scenario where HTTP Controllers could be used is API only applications. + # + # An HTTP Controller is different from a normal controller in the sense that + # by default it doesn't include a number of features that are usually required + # by browser access only: layouts and templates rendering, cookies, sessions, + # flash, assets, and so on. This makes the entire controller stack thinner and + # faster, suitable for API applications. It doesn't mean you won't have such + # features if you need them: they're all available for you to include in + # your application, they're just not part of the default HTTP Controller stack. + # + # By default, only the ApplicationController in a \Rails application inherits + # from ActionController::HTTP. All other controllers in turn inherit + # from ApplicationController. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # render json: @posts + # end + # end + # + # Request, response and parameters objects all work the exact same way as + # ActionController::Base. + # + # == Renders + # + # The default HTTP Controller stack includes all renderers, which means you + # can use render :json and brothers freely in your controllers. Keep + # in mind that templates are not going to be rendered, so you need to ensure + # your controller is calling either render or redirect in + # all actions. + # + # def show + # @post = Post.find(params[:id]) + # render json: @post + # end + # + # == Redirects + # + # Redirects are used to move from one action to another. You can use the + # redirect method in your controllers in the same way as + # ActionController::Base. For example: + # + # def create + # redirect_to root_url and return if not_authorized? + # # do stuff here + # end + # + # == Adding new behavior + # + # In some scenarios you may want to add back some functionality provided by + # ActionController::Base that is not present by default in + # ActionController::HTTP, for instance MimeResponds. This + # module gives you the respond_to and respond_with methods. + # Adding it is quite simple, you just need to include the module in a specific + # controller or in ApplicationController in case you want it + # available to your entire app: + # + # class ApplicationController < ActionController::HTTP + # include ActionController::MimeResponds + # end + # + # class PostsController < ApplicationController + # respond_to :json, :xml + # + # def index + # @posts = Post.all + # respond_with @posts + # end + # end + # + # Quite straightforward. Make sure to check ActionController::Base + # available modules if you want to include any other functionality that is + # not provided by ActionController::HTTP out of the box. + class HTTP < Metal + abstract! + + # Shortcut helper that returns all the ActionController::HTTP modules except the ones passed in the argument: + # + # class MetalController + # ActionController::HTTP.without_modules(:ParamsWrapper, :Streaming).each do |left| + # include left + # end + # end + # + # This gives better control over what you want to exclude and makes it easier + # to create a bare controller class, instead of listing the modules required manually. + def self.without_modules(*modules) + modules = modules.map do |m| + m.is_a?(Symbol) ? ActionController.const_get(m) : m + end + + MODULES - modules + end + + MODULES = [ + HideActions, + UrlFor, + Redirecting, + Rendering, + Renderers::All, + ConditionalGet, + RackDelegation, + + ForceSSL, + DataStreaming, + + # Before callbacks should also be executed the earliest as possible, so + # also include them at the bottom. + AbstractController::Callbacks, + + # Append rescue at the bottom to wrap as much as possible. + Rescue, + + # Add instrumentations hooks at the bottom, to ensure they instrument + # all the methods properly. + Instrumentation + ] + + MODULES.each do |mod| + include mod + end + + ActiveSupport.run_load_hooks(:action_controller, self) + end +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index a1e40fc4e0319bedbddc94f53d29870cce390982..ac12cbb625566c099d5679064885ba2655e15614 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -44,7 +44,7 @@ def force_ssl(options = {}) redirect_options = {:protocol => 'https://', :status => :moved_permanently} redirect_options.merge!(:host => host) if host redirect_options.merge!(:params => request.query_parameters) - flash.keep + flash.keep if respond_to?(:flash) redirect_to redirect_options end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 3e170d7872c4bf28d1d766669528a591d1cba0c4..5e837ca6e193c3a29dd66b595ab80921a7464e9a 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -14,7 +14,7 @@ class Railtie < Rails::Railtie #:nodoc: end initializer "action_controller.initialize_framework_caches" do - ActiveSupport.on_load(:action_controller) { self.cache_store ||= Rails.cache } + ActiveSupport.on_load(:action_controller) { self.cache_store ||= Rails.cache if respond_to?(:cache_store) } end initializer "action_controller.assets_config", :group => :all do |app| @@ -37,8 +37,15 @@ class Railtie < Rails::Railtie #:nodoc: ActiveSupport.on_load(:action_controller) do include app.routes.mounted_helpers extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) - extend ::ActionController::Railties::Paths.with(app) - options.each { |k,v| send("#{k}=", v) } + extend ::ActionController::Railties::Paths.with(app) if respond_to?(:helpers_path) + options.each do |k,v| + k = "#{k}=" + if respond_to?(k) + send(k, v) + elsif !Base.respond_to?(k) + raise "Invalid option key: #{k}" + end + end end end diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb index bbe63149ad42f888c06602e3efe4eca9ebe95c3e..7e79b036edaf7b44778d859d3caac6f0886f1d2f 100644 --- a/actionpack/lib/action_controller/railties/paths.rb +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -11,7 +11,6 @@ def self.with(app) else paths = app.helpers_paths end - klass.helpers_path = paths if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 84732085f086dacf135c4d876a18122660669b6b..078229efd287742c25042ba7e6e2f84e8fdc235a 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -51,12 +51,13 @@ class Response # If a character set has been defined for this response (see charset=) then # the character set information will also be included in the content type # information. - attr_accessor :charset, :content_type + attr_accessor :charset + attr_reader :content_type CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze - + cattr_accessor(:default_charset) { "utf-8" } include Rack::Response::Helpers @@ -83,6 +84,10 @@ def status=(status) @status = Rack::Utils.status_code(status) end + def content_type=(content_type) + @content_type = content_type.to_s + end + # The response code of the request def response_code @status diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index b1a5356ddd82291d90e77a190637992d30b9e41f..a05a816b712ab5ebe369d830b4cff387639a6793 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -293,6 +293,10 @@ def self.test_routes(&block) end end + class HTTP + include SharedTestRoutes.url_helpers + end + class TestCase include ActionDispatch::TestProcess diff --git a/actionpack/test/controller/http/action_methods_test.rb b/actionpack/test/controller/http/action_methods_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..20bb53aca26c16bbdc3c9da317193057f5a74b13 --- /dev/null +++ b/actionpack/test/controller/http/action_methods_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' + +class ActionMethodsHTTPController < ActionController::HTTP + def one; end + def two; end + hide_action :two +end + +class ActionMethodsHTTPTest < ActiveSupport::TestCase + def setup + @controller = ActionMethodsHTTPController.new + end + + def test_action_methods + assert_equal Set.new(%w(one)), + @controller.class.action_methods, + "#{@controller.controller_path} should not be empty!" + end +end diff --git a/actionpack/test/controller/http/conditional_get_test.rb b/actionpack/test/controller/http/conditional_get_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..70d5ef296fcf035d34ce8f3140621ae98999794e --- /dev/null +++ b/actionpack/test/controller/http/conditional_get_test.rb @@ -0,0 +1,55 @@ +require 'abstract_unit' + +class ConditionalGetHTTPController < ActionController::HTTP + before_filter :handle_last_modified_and_etags, :only => :two + + def one + if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123]) + render :text => "Hi!" + end + end + + def two + render :text => "Hi!" + end + + private + + def handle_last_modified_and_etags + fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ]) + end +end + +class ConditionalGetHTTPTest < ActionController::TestCase + tests ConditionalGetHTTPController + + def setup + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_request_with_bang_gets_last_modified + get :two + assert_equal @last_modified, @response.headers['Last-Modified'] + assert_response :success + end + + def test_request_with_bang_obeys_last_modified + @request.if_modified_since = @last_modified + get :two + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :two + assert_response :success + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :one + assert_equal 304, @response.status.to_i + assert_blank @response.body + assert_equal @last_modified, @response.headers['Last-Modified'] + end +end diff --git a/actionpack/test/controller/http/data_streaming_test.rb b/actionpack/test/controller/http/data_streaming_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..67457b25b01d936435d9ea6650140ed59faa37a9 --- /dev/null +++ b/actionpack/test/controller/http/data_streaming_test.rb @@ -0,0 +1,27 @@ +require 'abstract_unit' + +module TestHTTPFileUtils + def file_name() File.basename(__FILE__) end + def file_path() File.expand_path(__FILE__) end + def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end +end + +class DataStreamingHTTPController < ActionController::HTTP + include TestHTTPFileUtils + + def one; end + def two + send_data(file_data, {}) + end +end + +class DataStreamingHTTPTest < ActionController::TestCase + include TestHTTPFileUtils + tests DataStreamingHTTPController + + def test_data + response = process('two') + assert_kind_of String, response.body + assert_equal file_data, response.body + end +end diff --git a/actionpack/test/controller/http/force_ssl_test.rb b/actionpack/test/controller/http/force_ssl_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..479ede6b78af4f33303d789ae9578ed2a4bd7c19 --- /dev/null +++ b/actionpack/test/controller/http/force_ssl_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class ForceSSLHTTPController < ActionController::HTTP + force_ssl + + def one; end + def two + head :ok + end +end + +class ForceSSLHTTPTest < ActionController::TestCase + tests ForceSSLHTTPController + + def test_banana_redirects_to_https + get :two + assert_response 301 + assert_equal "https://test.host/force_sslhttp/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/http/redirect_to_test.rb b/actionpack/test/controller/http/redirect_to_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..c410910baeb79476d98ebc6ce6f7db342a8ef98b --- /dev/null +++ b/actionpack/test/controller/http/redirect_to_test.rb @@ -0,0 +1,19 @@ +require 'abstract_unit' + +class RedirectToHTTPController < ActionController::HTTP + def one + redirect_to :action => "two" + end + + def two; end +end + +class RedirectToHTTPTest < ActionController::TestCase + tests RedirectToHTTPController + + def test_redirect_to + get :one + assert_response :redirect + assert_equal "http://test.host/redirect_to_http/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/http/renderers_test.rb b/actionpack/test/controller/http/renderers_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..a28f226a944f741fb240648c579c2ec0462a2083 --- /dev/null +++ b/actionpack/test/controller/http/renderers_test.rb @@ -0,0 +1,37 @@ +require 'abstract_unit' + +class Model + def to_json(options = {}) + { :a => 'b' }.to_json(options) + end + + def to_xml(options = {}) + { :a => 'b' }.to_xml(options) + end +end + +class RenderersHTTPController < ActionController::HTTP + def one + render :json => Model.new + end + + def two + render :xml => Model.new + end +end + +class RenderersHTTPTest < ActionController::TestCase + tests RenderersHTTPController + + def test_render_json + get :one + assert_response :success + assert_equal({ :a => 'b' }.to_json, @response.body) + end + + def test_render_xml + get :two + assert_response :success + assert_equal({ :a => 'b' }.to_xml, @response.body) + end +end diff --git a/actionpack/test/controller/http/url_for_test.rb b/actionpack/test/controller/http/url_for_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..fba24011a22988d09c3a1ebf6cf7afc61ed49619 --- /dev/null +++ b/actionpack/test/controller/http/url_for_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' + +class UrlForHTTPController < ActionController::HTTP + def one; end + def two; end +end + +class UrlForHTTPTest < ActionController::TestCase + tests UrlForHTTPController + + def setup + super + @request.host = 'www.example.com' + end + + def test_url_for + get :one + assert_equal "http://www.example.com/url_for_http/one", @controller.url_for + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt index 4356f142829a7123b1e1e5916710684ac337ad6a..19cbf0e4f113d151b5b7f14770bb9b27af982c9b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt @@ -5,7 +5,7 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] + wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end <%- unless options.skip_active_record? -%> diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index a08e5b2374b9fc41469991d852c1d9fdfc1be2cd..cb321f0dd43f4d0f0116226134d0a84b17ee091d 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -130,6 +130,33 @@ def from_bar_helper assert_equal "false", last_response.body end + test "action_controller http initializes successfully" do + app_file "app/controllers/application_controller.rb", <<-RUBY + class ApplicationController < ActionController::HTTP + end + RUBY + + app_file "app/controllers/omg_controller.rb", <<-RUBY + class OmgController < ApplicationController + def show + render :json => { :omg => 'omg' } + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match "/:controller(/:action)" + end + RUBY + + require 'rack/test' + extend Rack::Test::Methods + + get '/omg/show' + assert_equal '{"omg":"omg"}', last_response.body + end + # AD test "action_dispatch extensions are applied to ActionDispatch" do add_to_config "config.action_dispatch.tld_length = 2"