From 103e18c87d50a53cd0a33b4e03f2c8a8eff19ece Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 31 Jan 2014 15:37:58 -0500 Subject: Introduce `render :body` for render raw content This is an option for sending a raw content back to browser. Note that this rendering option will unset the default content type and does not include "Content-Type" header back in the response. You should only use this option if you are expecting the "Content-Type" header to not be set. More information on "Content-Type" header can be found on RFC 2616, section 7.2.1. Please see #12374 for more detail. --- actionpack/CHANGELOG.md | 12 ++ actionpack/lib/abstract_controller/rendering.rb | 4 +- .../lib/action_controller/metal/rendering.rb | 17 +- actionpack/lib/action_dispatch/http/response.rb | 2 +- .../test/controller/new_base/render_body_test.rb | 175 +++++++++++++++++++++ actionpack/test/dispatch/response_test.rb | 8 + .../lib/action_view/helpers/rendering_helper.rb | 2 + actionview/lib/action_view/layouts.rb | 2 +- .../lib/action_view/renderer/template_renderer.rb | 6 +- actionview/lib/action_view/rendering.rb | 2 +- 10 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 actionpack/test/controller/new_base/render_body_test.rb diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 35b0a419d3..dcccc99d5b 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,15 @@ +* Introduce `render :body` as an option for sending a raw content back to + browser. Note that this rendering option will unset the default content type + and does not include "Content-Type" header back in the response. + + You should only use this option if you are expecting the "Content-Type" + header to not be set. More information on "Content-Type" header can be found + on RFC 2616, section 7.2.1. + + Please see #12374 for more detail. + + *Prem Sichanugrist* + * Set stream status to 500 (or 400 on BadRequest) when an error is thrown before commiting. diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index f24b03ad16..349bbf4ee7 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -23,7 +23,7 @@ module AbstractController def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) - _process_format(rendered_format) if rendered_format + _process_format(rendered_format, options) if rendered_format self.response_body end @@ -98,7 +98,7 @@ module AbstractController # Process the rendered format. # :api: private - def _process_format(format) + def _process_format(format, options = {}) end # Normalize args and options. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 5c48b4ab98..c9ae1ab388 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -27,14 +27,19 @@ module ActionController end def render_to_body(options = {}) - super || options[:text].presence || ' ' + super || options[:body].presence || options[:text].presence || ' ' end private - def _process_format(format) + def _process_format(format, options = {}) super self.content_type ||= format.to_s + + if options[:body].present? + self.content_type = "none" + self.headers.delete "Content-Type" + end end # Normalize arguments by catching blocks and setting them on :update. @@ -46,12 +51,16 @@ module ActionController # Normalize both text and status options. def _normalize_options(options) #:nodoc: + if options.key?(:body) && options[:body].respond_to?(:to_text) + options[:body] = options[:body].to_text + end + if options.key?(:text) && options[:text].respond_to?(:to_text) options[:text] = options[:text].to_text end - if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) - options[:text] = " " + if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) + options[:body] = " " end if options[:status] diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 2c6bcf7b7b..15a177aaff 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -288,7 +288,7 @@ module ActionDispatch # :nodoc: end def assign_default_content_type_and_charset!(headers) - return if headers[CONTENT_TYPE].present? + return if headers[CONTENT_TYPE].present? || @content_type == "none" @content_type ||= Mime::HTML @charset ||= self.class.default_charset unless @charset == false diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb new file mode 100644 index 0000000000..a7e4f87bd9 --- /dev/null +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -0,0 +1,175 @@ +require 'abstract_unit' + +module RenderBody + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render body: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render body: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render body: "hello david" + end + + def custom_code + render body: "hello world", status: 404 + end + + def with_custom_code_as_string + render body: "hello world", status: "404 Not Found" + end + + def with_nil + render body: nil + end + + def with_nil_and_status + render body: nil, status: 403 + end + + def with_false + render body: false + end + + def with_layout_true + render body: "hello world", layout: true + end + + def with_layout_false + render body: "hello world", layout: false + end + + def with_layout_nil + render body: "hello world", layout: nil + end + + def with_custom_layout + render body: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render body: "hello world", layout: "ivar" + end + end + + class RenderBodyTest < Rack::TestCase + test "rendering body from a minimal controller" do + get "/render_body/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering body from an action with default options renders the body with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_body/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body from an action with default options renders the body without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_body/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body, while also providing a custom status code" do + get "/render_body/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering body with nil returns an empty body padded for Safari" do + get "/render_body/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering body with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_body/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering body with false returns the string 'false'" do + get "/render_body/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering body with layout: true" do + get "/render_body/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering body with layout: 'greetings'" do + get "/render_body/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering body with layout: false" do + get "/render_body/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering body with layout: nil" do + get "/render_body/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering from minimal controller returns response with no content type" do + get "/render_body/minimal/index" + + assert_header_no_content_type + end + + test "rendering from normal controller returns response with no content type" do + get "/render_body/simple/index" + + assert_header_no_content_type + end + + def assert_header_no_content_type + assert_not response.headers.has_key?("Content-Type"), + %(Expect response not to have Content-Type header, got "#{response.headers["Content-Type"]}") + end + end +end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 959a3bc5cd..26b77dfaf7 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -235,6 +235,14 @@ class ResponseTest < ActiveSupport::TestCase assert_equal @response.body, body.each.to_a.join end end + + test "does not add default content-type if Content-Type is none" do + resp = ActionDispatch::Response.new.tap { |response| + response.content_type = 'none' + } + + assert_not resp.headers.has_key?('Content-Type') + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 458086de96..77d63b7f86 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -12,6 +12,8 @@ module ActionView # * :file - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * :inline - Renders an inline template similar to how it's done in the controller. # * :text - Renders the text passed in out. + # * :body - Renders the text passed in, and does not set content + # type in the response. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index ffa67649da..b67b749af5 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -420,7 +420,7 @@ module ActionView end def _include_layout?(options) - (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout) + (options.keys & [:body, :text, :inline, :partial]).empty? || options.key?(:layout) end end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 668831dff3..8f24f8dab0 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -21,7 +21,9 @@ module ActionView def determine_template(options) #:nodoc: keys = options.fetch(:locals, {}).keys - if options.key?(:text) + if options.key?(:body) + Template::Text.new(options[:body]) + elsif options.key?(:text) Template::Text.new(options[:text], formats.first) elsif options.key?(:file) with_fallbacks { find_template(options[:file], nil, false, keys, @details) } @@ -35,7 +37,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :text or :body option." end end diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 7c17220d14..017302d40f 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -100,7 +100,7 @@ module ActionView end # Assign the rendered format to lookup context. - def _process_format(format) #:nodoc: + def _process_format(format, options = {}) #:nodoc: super lookup_context.formats = [format.to_sym] lookup_context.rendered_format = lookup_context.formats.first -- cgit v1.2.3 From 9e9cc660773d331b04a829b4a727af70b3e23c16 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 7 Feb 2014 11:24:54 -0500 Subject: Update hash format for render_text_test --- .../test/controller/new_base/render_text_test.rb | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index 2a253799f3..abb81d7e71 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -14,7 +14,7 @@ module RenderText self.view_paths = [ActionView::FixtureResolver.new] def index - render :text => "hello david" + render text: "hello david" end end @@ -26,48 +26,48 @@ module RenderText )] def index - render :text => "hello david" + render text: "hello david" end def custom_code - render :text => "hello world", :status => 404 + render text: "hello world", status: 404 end def with_custom_code_as_string - render :text => "hello world", :status => "404 Not Found" + render text: "hello world", status: "404 Not Found" end def with_nil - render :text => nil + render text: nil end def with_nil_and_status - render :text => nil, :status => 403 + render text: nil, status: 403 end def with_false - render :text => false + render text: false end def with_layout_true - render :text => "hello world", :layout => true + render text: "hello world", layout: true end def with_layout_false - render :text => "hello world", :layout => false + render text: "hello world", layout: false end def with_layout_nil - render :text => "hello world", :layout => nil + render text: "hello world", layout: nil end def with_custom_layout - render :text => "hello world", :layout => "greetings" + render text: "hello world", layout: "greetings" end def with_ivar_in_layout @ivar = "hello world" - render :text => "hello world", :layout => "ivar" + render text: "hello world", layout: "ivar" end end @@ -80,7 +80,7 @@ module RenderText test "rendering text from an action with default options renders the text with the layout" do with_routing do |set| - set.draw { get ':controller', :action => 'index' } + set.draw { get ':controller', action: 'index' } get "/render_text/simple" assert_body "hello david" @@ -90,7 +90,7 @@ module RenderText test "rendering text from an action with default options renders the text without the layout" do with_routing do |set| - set.draw { get ':controller', :action => 'index' } + set.draw { get ':controller', action: 'index' } get "/render_text/with_layout" @@ -127,28 +127,28 @@ module RenderText assert_status 200 end - test "rendering text with :layout => true" do + test "rendering text with layout: true" do get "/render_text/with_layout/with_layout_true" assert_body "hello world, I'm here!" assert_status 200 end - test "rendering text with :layout => 'greetings'" do + test "rendering text with layout: 'greetings'" do get "/render_text/with_layout/with_custom_layout" assert_body "hello world, I wish thee well." assert_status 200 end - test "rendering text with :layout => false" do + test "rendering text with layout: false" do get "/render_text/with_layout/with_layout_false" assert_body "hello world" assert_status 200 end - test "rendering text with :layout => nil" do + test "rendering text with layout: nil" do get "/render_text/with_layout/with_layout_nil" assert_body "hello world" -- cgit v1.2.3 From 8cd9f6d205e5db5331dd5b01be35b537da17cdee Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 7 Feb 2014 13:45:57 -0500 Subject: Introduce `render :plain` for render plain text This is as an option to render content with a content type of `text/plain`. This is the preferred option if you are planning to render a plain text content. Please see #12374 for more detail. --- actionpack/CHANGELOG.md | 8 + .../lib/action_controller/metal/rendering.rb | 12 +- .../test/controller/new_base/render_plain_test.rb | 168 +++++++++++++++++++++ .../lib/action_view/helpers/rendering_helper.rb | 2 + actionview/lib/action_view/layouts.rb | 2 +- .../lib/action_view/renderer/template_renderer.rb | 4 +- 6 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 actionpack/test/controller/new_base/render_plain_test.rb diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index dcccc99d5b..3318a088a3 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,11 @@ +* Introduce `render :plain` as an option to render content with a content type + of `text/plain`. This is the preferred option if you are planning to render + a plain text content. + + Please see #12374 for more detail. + + *Prem Sichanugrist* + * Introduce `render :body` as an option for sending a raw content back to browser. Note that this rendering option will unset the default content type and does not include "Content-Type" header back in the response. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index c9ae1ab388..3133334369 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -27,7 +27,7 @@ module ActionController end def render_to_body(options = {}) - super || options[:body].presence || options[:text].presence || ' ' + super || options[:body].presence || options[:text].presence || options[:plain].presence || ' ' end private @@ -40,6 +40,10 @@ module ActionController self.content_type = "none" self.headers.delete "Content-Type" end + + if options[:plain].present? + self.content_type = Mime::TEXT + end end # Normalize arguments by catching blocks and setting them on :update. @@ -59,7 +63,11 @@ module ActionController options[:text] = options[:text].to_text end - if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) + if options.key?(:plain) && options[:plain].respond_to?(:to_text) + options[:plain] = options[:plain].to_text + end + + if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) || (options.key?(:plain) && options[:plain].nil?) options[:body] = " " end diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb new file mode 100644 index 0000000000..dba2e9f13e --- /dev/null +++ b/actionpack/test/controller/new_base/render_plain_test.rb @@ -0,0 +1,168 @@ +require 'abstract_unit' + +module RenderPlain + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render plain: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render plain: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.text.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.text.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.text.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render plain: "hello david" + end + + def custom_code + render plain: "hello world", status: 404 + end + + def with_custom_code_as_string + render plain: "hello world", status: "404 Not Found" + end + + def with_nil + render plain: nil + end + + def with_nil_and_status + render plain: nil, status: 403 + end + + def with_false + render plain: false + end + + def with_layout_true + render plain: "hello world", layout: true + end + + def with_layout_false + render plain: "hello world", layout: false + end + + def with_layout_nil + render plain: "hello world", layout: nil + end + + def with_custom_layout + render plain: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render plain: "hello world", layout: "ivar" + end + end + + class RenderPlainTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_plain/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering text from an action with default options renders the text with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_plain/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text from an action with default options renders the text without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_plain/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text, while also providing a custom status code" do + get "/render_plain/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering text with nil returns an empty body padded for Safari" do + get "/render_plain/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_plain/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering text with false returns the string 'false'" do + get "/render_plain/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering text with layout: true" do + get "/render_plain/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering text with layout: 'greetings'" do + get "/render_plain/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering text with layout: false" do + get "/render_plain/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering text with layout: nil" do + get "/render_plain/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering from minimal controller returns response with text/plain content type" do + get "/render_plain/minimal/index" + assert_content_type "text/plain" + end + + test "rendering from normal controller returns response with text/plain content type" do + get "/render_plain/simple/index" + assert_content_type "text/plain; charset=utf-8" + end + end +end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 77d63b7f86..4eae80cd93 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -12,6 +12,8 @@ module ActionView # * :file - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * :inline - Renders an inline template similar to how it's done in the controller. # * :text - Renders the text passed in out. + # * :plain - Renders the text passed in out. Setting the content + # type as text/plain. # * :body - Renders the text passed in, and does not set content # type in the response. # diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index b67b749af5..3f049e838a 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -420,7 +420,7 @@ module ActionView end def _include_layout?(options) - (options.keys & [:body, :text, :inline, :partial]).empty? || options.key?(:layout) + (options.keys & [:body, :text, :plain, :inline, :partial]).empty? || options.key?(:layout) end end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 8f24f8dab0..ba946b230a 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -25,6 +25,8 @@ module ActionView Template::Text.new(options[:body]) elsif options.key?(:text) Template::Text.new(options[:text], formats.first) + elsif options.key?(:plain) + Template::Text.new(options[:plain]) elsif options.key?(:file) with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) @@ -37,7 +39,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option." end end -- cgit v1.2.3 From 920f3ba2668e0622335f16f2f1318d9e6b5e6b28 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 09:57:47 -0500 Subject: Introduce `render :html` for render HTML string This is an option for to HTML content with a content type of `text/html`. This rendering option calls `ERB::Util.html_escape` internally to escape unsafe HTML string, so you will have to mark your string as html safe if you have any HTML tag in it. Please see #12374 for more detail. --- actionpack/CHANGELOG.md | 9 + .../lib/action_controller/metal/rendering.rb | 4 +- .../test/controller/new_base/render_html_test.rb | 190 +++++++++++++++++++++ .../lib/action_view/helpers/rendering_helper.rb | 3 + actionview/lib/action_view/layouts.rb | 2 +- .../lib/action_view/renderer/template_renderer.rb | 2 + actionview/lib/action_view/template.rb | 1 + actionview/lib/action_view/template/html.rb | 34 ++++ 8 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 actionpack/test/controller/new_base/render_html_test.rb create mode 100644 actionview/lib/action_view/template/html.rb diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 3318a088a3..b05aa21f95 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,12 @@ +* Introduce `render :html` as an option to render HTML content with a content + type of `text/html`. This rendering option calls `ERB::Util.html_escape` + internally to escape unsafe HTML string, so you will have to mark your + string as html safe if you have any HTML tag in it. + + Please see #12374 for more detail. + + *Prem Sichanugrist* + * Introduce `render :plain` as an option to render content with a content type of `text/plain`. This is the preferred option if you are planning to render a plain text content. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 3133334369..cff0405351 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -27,7 +27,7 @@ module ActionController end def render_to_body(options = {}) - super || options[:body].presence || options[:text].presence || options[:plain].presence || ' ' + super || options[:body].presence || options[:text].presence || options[:plain].presence || ERB::Util.h(options[:html]).presence || ' ' end private @@ -67,7 +67,7 @@ module ActionController options[:plain] = options[:plain].to_text end - if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) || (options.key?(:plain) && options[:plain].nil?) + if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) || (options.key?(:plain) && options[:plain].nil?) || (options.key?(:html) && options[:html].nil?) options[:body] = " " end diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb new file mode 100644 index 0000000000..bfe0271df7 --- /dev/null +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -0,0 +1,190 @@ +require 'abstract_unit' + +module RenderHtml + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render html: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render html: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.html.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render html: "hello david" + end + + def custom_code + render html: "hello world", status: 404 + end + + def with_custom_code_as_string + render html: "hello world", status: "404 Not Found" + end + + def with_nil + render html: nil + end + + def with_nil_and_status + render html: nil, status: 403 + end + + def with_false + render html: false + end + + def with_layout_true + render html: "hello world", layout: true + end + + def with_layout_false + render html: "hello world", layout: false + end + + def with_layout_nil + render html: "hello world", layout: nil + end + + def with_custom_layout + render html: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render html: "hello world", layout: "ivar" + end + + def with_unsafe_html_tag + render html: "

hello world

", layout: nil + end + + def with_safe_html_tag + render html: "

hello world

".html_safe, layout: nil + end + end + + class RenderHtmlTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_html/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering text from an action with default options renders the text with the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_html/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text from an action with default options renders the text without the layout" do + with_routing do |set| + set.draw { get ':controller', action: 'index' } + + get "/render_html/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text, while also providing a custom status code" do + get "/render_html/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering text with nil returns an empty body padded for Safari" do + get "/render_html/with_layout/with_nil" + + assert_body " " + assert_status 200 + end + + test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + get "/render_html/with_layout/with_nil_and_status" + + assert_body " " + assert_status 403 + end + + test "rendering text with false returns the string 'false'" do + get "/render_html/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering text with layout: true" do + get "/render_html/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering text with layout: 'greetings'" do + get "/render_html/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering text with layout: false" do + get "/render_html/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering text with layout: nil" do + get "/render_html/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering html should escape the string if it is not html safe" do + get "/render_html/with_layout/with_unsafe_html_tag" + + assert_body "<p>hello world</p>" + assert_status 200 + end + + test "rendering html should not escape the string if it is html safe" do + get "/render_html/with_layout/with_safe_html_tag" + + assert_body "

hello world

" + assert_status 200 + end + + test "rendering from minimal controller returns response with text/html content type" do + get "/render_html/minimal/index" + assert_content_type "text/html" + end + + test "rendering from normal controller returns response with text/html content type" do + get "/render_html/simple/index" + assert_content_type "text/html; charset=utf-8" + end + end +end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 4eae80cd93..15b88bcda6 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -14,6 +14,9 @@ module ActionView # * :text - Renders the text passed in out. # * :plain - Renders the text passed in out. Setting the content # type as text/plain. + # * :html - Renders the html safe string passed in out, otherwise + # performs html escape on the string first. Setting the content type as + # text/html. # * :body - Renders the text passed in, and does not set content # type in the response. # diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 3f049e838a..9ee05bd816 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -420,7 +420,7 @@ module ActionView end def _include_layout?(options) - (options.keys & [:body, :text, :plain, :inline, :partial]).empty? || options.key?(:layout) + (options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout) end end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index ba946b230a..be17097428 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -27,6 +27,8 @@ module ActionView Template::Text.new(options[:text], formats.first) elsif options.key?(:plain) Template::Text.new(options[:plain]) + elsif options.key?(:html) + Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 9b0619f1aa..961a969b6e 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -90,6 +90,7 @@ module ActionView eager_autoload do autoload :Error autoload :Handlers + autoload :HTML autoload :Text autoload :Types end diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb new file mode 100644 index 0000000000..282da1a8a2 --- /dev/null +++ b/actionview/lib/action_view/template/html.rb @@ -0,0 +1,34 @@ +module ActionView #:nodoc: + # = Action View HTML Template + class Template + class HTML #:nodoc: + attr_accessor :type + + def initialize(string, type = nil) + @string = string.to_s + @type = Types[type] || type if type + @type ||= Types[:html] + end + + def identifier + 'html template' + end + + def inspect + 'html template' + end + + def to_str + ERB::Util.h(@string) + end + + def render(*args) + to_str + end + + def formats + [@type.to_sym] + end + end + end +end -- cgit v1.2.3 From 243e6e4b2a53253d5ca734415564c419c5632f12 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 10:24:49 -0500 Subject: Fix a fragile test on `action_view/render` This test were assuming that the list of render options will always be the same. Fixing that so this doesn't break when we add/remove render option in the future. --- actionview/test/template/render_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index db5d99755c..ca508abfb8 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -22,7 +22,7 @@ module RenderTestCases def test_render_without_options e = assert_raises(ArgumentError) { @view.render() } - assert_match "You invoked render but did not give any of :partial, :template, :inline, :file or :text option.", e.message + assert_match(/You invoked render but did not give any of (.+) option./, e.message) end def test_render_file -- cgit v1.2.3 From 79c4983f893da985806d3d59ac23836bccb906f8 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 10:45:46 -0500 Subject: Cleanup `ActionController::Rendering` --- .../lib/action_controller/metal/rendering.rb | 38 +++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index cff0405351..ce37eaefd8 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,6 +2,8 @@ module ActionController module Rendering extend ActiveSupport::Concern + RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact @@ -27,11 +29,19 @@ module ActionController end def render_to_body(options = {}) - super || options[:body].presence || options[:text].presence || options[:plain].presence || ERB::Util.h(options[:html]).presence || ' ' + super || _render_in_priorities(options) || ' ' end private + def _render_in_priorities(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + return options[format] if options.key?(format) + end + + nil + end + def _process_format(format, options = {}) super self.content_type ||= format.to_s @@ -55,19 +65,13 @@ module ActionController # Normalize both text and status options. def _normalize_options(options) #:nodoc: - if options.key?(:body) && options[:body].respond_to?(:to_text) - options[:body] = options[:body].to_text - end + _normalize_text(options) - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text + if options[:html] + options[:html] = ERB::Util.html_escape(options[:html]) end - if options.key?(:plain) && options[:plain].respond_to?(:to_text) - options[:plain] = options[:plain].to_text - end - - if options.delete(:nothing) || (options.key?(:body) && options[:body].nil?) || (options.key?(:text) && options[:text].nil?) || (options.key?(:plain) && options[:plain].nil?) || (options.key?(:html) && options[:html].nil?) + if options.delete(:nothing) || _any_render_format_is_nil?(options) options[:body] = " " end @@ -78,6 +82,18 @@ module ActionController super end + def _normalize_text(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + if options.key?(format) && options[format].respond_to?(:to_text) + options[format] = options[format].to_text + end + end + end + + def _any_render_format_is_nil?(options) + RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } + end + # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) -- cgit v1.2.3 From 76be30fe40679b468cacf26d0b24b179c3d339ba Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 13:11:20 -0500 Subject: Update guides for new rendering options * Introduces `:plain`, `:html`, `:body` render option. * Update guide to use `render :plain` instead of `render :text`. --- guides/source/action_controller_overview.md | 2 +- guides/source/getting_started.md | 2 +- guides/source/layouts_and_rendering.md | 40 ++++++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 222d86afe9..5b5f53c9be 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -1088,7 +1088,7 @@ class ApplicationController < ActionController::Base private def record_not_found - render text: "404 Not Found", status: 404 + render plain: "404 Not Found", status: 404 end end ``` diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 53d2a9b55b..a16b9ac8da 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -608,7 +608,7 @@ look like, change the `create` action to this: ```ruby def create - render text: params[:article].inspect + render plain: params[:article].inspect end ``` diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 93e25d619e..66ed6f2e08 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -236,15 +236,34 @@ render inline: "xml.p {'Horrid coding practice!'}", type: :builder #### Rendering Text -You can send plain text - with no markup at all - back to the browser by using the `:text` option to `render`: +You can send plain text - with no markup at all - back to the browser by using +the `:plain` option to `render`: ```ruby -render text: "OK" +render plain: "OK" ``` -TIP: Rendering pure text is most useful when you're responding to Ajax or web service requests that are expecting something other than proper HTML. +TIP: Rendering pure text is most useful when you're responding to Ajax or web +service requests that are expecting something other than proper HTML. -NOTE: By default, if you use the `:text` option, the text is rendered without using the current layout. If you want Rails to put the text into the current layout, you need to add the `layout: true` option. +NOTE: By default, if you use the `:plain` option, the text is rendered without +using the current layout. If you want Rails to put the text into the current +layout, you need to add the `layout: true` option. + +#### Rendering HTML + +You can send a HTML string back to the browser by using the `:html` option to +`render`: + +```ruby +render html: "Not Found".html_safe +``` + +TIP: This is useful when you're rendering a small snippet of HTML code. +However, you might want to consider moving it to a template file if the markup +is complex. + +NOTE: This option will escape HTML entities if the string is not html safe. #### Rendering JSON @@ -276,6 +295,19 @@ render js: "alert('Hello Rails');" This will send the supplied string to the browser with a MIME type of `text/javascript`. +#### Rendering raw body + +You can send a raw content back to the browser, without setting any content +type, by using the `:body` option to `render`: + +```ruby +render body: "raw" +``` + +TIP: This option should be used only if you explicitly want the content type to +be unset. Using `:plain` or `:html` might be more appropriate in most of the +time. + #### Options for `render` Calls to the `render` method generally accept four options: -- cgit v1.2.3 From 9fe506e3944652c3681ca27d1c2a3a559f605359 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 13:15:47 -0500 Subject: Add missing CHANGELOG entry to Action View --- actionview/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index a0f298a6b1..cc21201903 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,8 @@ +* Added `:plain`, `:html` and `:body` option for `render` method. Please see + Action Pack's release note for more detail. + + *Prem Sichanugrist* + * Date select helpers accept a format string for the months selector via the new option `:month_format_string`. -- cgit v1.2.3 From 3047376870d4a7adc7ff15c3cb4852e073c8f1da Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Fri, 14 Feb 2014 15:50:08 -0500 Subject: Add `#no_content_type` attribute to `AD::Response` Setting this attribute to `true` will remove the content type header from the request. This is use in `render :body` feature. --- actionpack/lib/action_controller/metal/rack_delegation.rb | 4 ++-- actionpack/lib/action_controller/metal/rendering.rb | 10 ++++------ actionpack/lib/action_dispatch/http/response.rb | 15 +++++++++++++-- actionpack/test/dispatch/response_test.rb | 2 +- actionview/lib/action_view/rendering.rb | 5 +++++ 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index bdf6e88699..e1bee9e60c 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -5,8 +5,8 @@ module ActionController module RackDelegation extend ActiveSupport::Concern - delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + delegate :headers, :status=, :location=, :content_type=, :no_content_type=, + :status, :location, :content_type, :no_content_type, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index ce37eaefd8..3c4ef596c7 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -44,15 +44,13 @@ module ActionController def _process_format(format, options = {}) super - self.content_type ||= format.to_s - if options[:body].present? - self.content_type = "none" + if options[:body] self.headers.delete "Content-Type" - end - - if options[:plain].present? + elsif options[:plain] self.content_type = Mime::TEXT + else + self.content_type ||= format.to_s end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 15a177aaff..f14ca1ea44 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -63,6 +63,8 @@ module ActionDispatch # :nodoc: # content you're giving them, so we need to send that along. attr_accessor :charset + attr_accessor :no_content_type # :nodoc: + CONTENT_TYPE = "Content-Type".freeze SET_COOKIE = "Set-Cookie".freeze LOCATION = "Location".freeze @@ -288,7 +290,7 @@ module ActionDispatch # :nodoc: end def assign_default_content_type_and_charset!(headers) - return if headers[CONTENT_TYPE].present? || @content_type == "none" + return if headers[CONTENT_TYPE].present? @content_type ||= Mime::HTML @charset ||= self.class.default_charset unless @charset == false @@ -303,8 +305,17 @@ module ActionDispatch # :nodoc: !@sending_file && @charset != false end + def remove_content_type! + headers.delete CONTENT_TYPE + end + def rack_response(status, header) - assign_default_content_type_and_charset!(header) + if no_content_type + remove_content_type! + else + assign_default_content_type_and_charset!(header) + end + handle_conditional_get! header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join) diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 26b77dfaf7..1360ede3f8 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -238,7 +238,7 @@ class ResponseTest < ActiveSupport::TestCase test "does not add default content-type if Content-Type is none" do resp = ActionDispatch::Response.new.tap { |response| - response.content_type = 'none' + response.no_content_type = true } assert_not resp.headers.has_key?('Content-Type') diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 017302d40f..f96587c816 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -102,6 +102,11 @@ module ActionView # Assign the rendered format to lookup context. def _process_format(format, options = {}) #:nodoc: super + + if options[:body] + self.no_content_type = true + end + lookup_context.formats = [format.to_sym] lookup_context.rendered_format = lookup_context.formats.first end -- cgit v1.2.3 From ede0f8c62d825089e6676c2af7a035bd8ca94b01 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 18 Feb 2014 09:30:43 -0500 Subject: Update upgrading guide regarding `render :text` --- guides/source/upgrading_ruby_on_rails.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 76722c9ea9..a8b7c9d492 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -329,6 +329,25 @@ User.inactive # SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive' ``` +### Rendering content from string + +Rails 4.1 introduces `:plain`, `:html`, and `:body` options to `render`. Those +options are now the preferred way to render string-based content, as it allows +you to specify which content type you want the response sent as. + +* `render :plain` will set the content type to `text/plain` +* `render :html` will set the content type to `text/html` +* `render :body` will *not* set the content type header. + +From the security standpoint, if you don't expect to have any markup in your +response body, you should be using `render :plain` as most browsers will escape +unsafe content in the response for you. + +We will be deprecating the use of `render :text` in a future version. So please +start using the more precise `:plain:`, `:html`, and `:body` options instead. +Using `render :text` may pose a security risk, as the content is sent as +`text/html`. + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- -- cgit v1.2.3