Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce :plain, :html, and :body render options. #14062

Merged
merged 10 commits into from Feb 18, 2014
29 changes: 29 additions & 0 deletions actionpack/CHANGELOG.md
@@ -1,3 +1,32 @@
* 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.

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.

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.

Expand Down
4 changes: 2 additions & 2 deletions actionpack/lib/abstract_controller/rendering.rb
Expand Up @@ -23,7 +23,7 @@ module Rendering
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

Expand Down Expand Up @@ -98,7 +98,7 @@ def _process_options(options)

# Process the rendered format.
# :api: private
def _process_format(format)
def _process_format(format, options = {})
end

# Normalize args and options.
Expand Down
4 changes: 2 additions & 2 deletions actionpack/lib/action_controller/metal/rack_delegation.rb
Expand Up @@ -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)
Expand Down
45 changes: 38 additions & 7 deletions actionpack/lib/action_controller/metal/rendering.rb
Expand Up @@ -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
Expand All @@ -27,14 +29,29 @@ def render_to_string(*)
end

def render_to_body(options = {})
super || options[:text].presence || ' '
super || _render_in_priorities(options) || ' '
end

private

def _process_format(format)
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

if options[:body]
self.headers.delete "Content-Type"
elsif options[:plain]
self.content_type = Mime::TEXT
else
self.content_type ||= format.to_s
end
end

# Normalize arguments by catching blocks and setting them on :update.
Expand All @@ -46,12 +63,14 @@ def _normalize_args(action=nil, options={}, &blk) #:nodoc:

# Normalize both text and status options.
def _normalize_options(options) #:nodoc:
if options.key?(:text) && options[:text].respond_to?(:to_text)
options[:text] = options[:text].to_text
_normalize_text(options)

if options[:html]
options[:html] = ERB::Util.html_escape(options[:html])
end

if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?)
options[:text] = " "
if options.delete(:nothing) || _any_render_format_is_nil?(options)
options[:body] = " "
end

if options[:status]
Expand All @@ -61,6 +80,18 @@ def _normalize_options(options) #:nodoc:
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)
Expand Down
13 changes: 12 additions & 1 deletion actionpack/lib/action_dispatch/http/response.rb
Expand Up @@ -63,6 +63,8 @@ class Response
# 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
Expand Down Expand Up @@ -303,8 +305,17 @@ def append_charset?
!@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)
Expand Down
175 changes: 175 additions & 0 deletions 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