aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/mime
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/mime')
-rw-r--r--actionpack/lib/action_controller/mime/default_types.rb21
-rw-r--r--actionpack/lib/action_controller/mime/responds.rb190
-rw-r--r--actionpack/lib/action_controller/mime/type.rb214
3 files changed, 425 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/mime/default_types.rb b/actionpack/lib/action_controller/mime/default_types.rb
new file mode 100644
index 0000000000..2d7fba1173
--- /dev/null
+++ b/actionpack/lib/action_controller/mime/default_types.rb
@@ -0,0 +1,21 @@
+# Build list of Mime types for HTTP responses
+# http://www.iana.org/assignments/media-types/
+
+Mime::Type.register "*/*", :all
+Mime::Type.register "text/plain", :text, [], %w(txt)
+Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
+Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
+Mime::Type.register "text/css", :css
+Mime::Type.register "text/calendar", :ics
+Mime::Type.register "text/csv", :csv
+Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
+Mime::Type.register "application/rss+xml", :rss
+Mime::Type.register "application/atom+xml", :atom
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
+
+Mime::Type.register "multipart/form-data", :multipart_form
+Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
+
+# http://www.ietf.org/rfc/rfc4627.txt
+# http://www.json.org/JSONRequest.html
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) \ No newline at end of file
diff --git a/actionpack/lib/action_controller/mime/responds.rb b/actionpack/lib/action_controller/mime/responds.rb
new file mode 100644
index 0000000000..bac225ab2a
--- /dev/null
+++ b/actionpack/lib/action_controller/mime/responds.rb
@@ -0,0 +1,190 @@
+module ActionController #:nodoc:
+ module MimeResponds #:nodoc:
+ def self.included(base)
+ base.module_eval do
+ include ActionController::MimeResponds::InstanceMethods
+ end
+ end
+
+ module InstanceMethods
+ # Without web-service support, an action which collects the data for displaying a list of people
+ # might look something like this:
+ #
+ # def index
+ # @people = Person.find(:all)
+ # end
+ #
+ # Here's the same action, with web-service support baked in:
+ #
+ # def index
+ # @people = Person.find(:all)
+ #
+ # respond_to do |format|
+ # format.html
+ # format.xml { render :xml => @people.to_xml }
+ # end
+ # end
+ #
+ # What that says is, "if the client wants HTML in response to this action, just respond as we
+ # would have before, but if the client wants XML, return them the list of people in XML format."
+ # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
+ #
+ # Supposing you have an action that adds a new person, optionally creating their company
+ # (by name) if it does not already exist, without web-services, it might look like this:
+ #
+ # def create
+ # @company = Company.find_or_create_by_name(params[:company][:name])
+ # @person = @company.people.create(params[:person])
+ #
+ # redirect_to(person_list_url)
+ # end
+ #
+ # Here's the same action, with web-service support baked in:
+ #
+ # def create
+ # company = params[:person].delete(:company)
+ # @company = Company.find_or_create_by_name(company[:name])
+ # @person = @company.people.create(params[:person])
+ #
+ # respond_to do |format|
+ # format.html { redirect_to(person_list_url) }
+ # format.js
+ # format.xml { render :xml => @person.to_xml(:include => @company) }
+ # end
+ # end
+ #
+ # If the client wants HTML, we just redirect them back to the person list. If they want Javascript
+ # (format.js), then it is an RJS request and we render the RJS template associated with this action.
+ # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
+ # include the person's company in the rendered XML, so you get something like this:
+ #
+ # <person>
+ # <id>...</id>
+ # ...
+ # <company>
+ # <id>...</id>
+ # <name>...</name>
+ # ...
+ # </company>
+ # </person>
+ #
+ # Note, however, the extra bit at the top of that action:
+ #
+ # company = params[:person].delete(:company)
+ # @company = Company.find_or_create_by_name(company[:name])
+ #
+ # This is because the incoming XML document (if a web-service request is in process) can only contain a
+ # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
+ #
+ # person[name]=...&person[company][name]=...&...
+ #
+ # And, like this (xml-encoded):
+ #
+ # <person>
+ # <name>...</name>
+ # <company>
+ # <name>...</name>
+ # </company>
+ # </person>
+ #
+ # In other words, we make the request so that it operates on a single entity's person. Then, in the action,
+ # we extract the company data from the request, find or create the company, and then create the new person
+ # with the remaining data.
+ #
+ # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
+ # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
+ # and accept Rails' defaults, life will be much easier.
+ #
+ # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
+ # environment.rb as follows.
+ #
+ # Mime::Type.register "image/jpg", :jpg
+ def respond_to(*types, &block)
+ raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block
+ block ||= lambda { |responder| types.each { |type| responder.send(type) } }
+ responder = Responder.new(self)
+ block.call(responder)
+ responder.respond
+ end
+ end
+
+ class Responder #:nodoc:
+
+ def initialize(controller)
+ @controller = controller
+ @request = controller.request
+ @response = controller.response
+
+ @mime_type_priority = @request.formats
+
+ @order = []
+ @responses = {}
+ end
+
+ def custom(mime_type, &block)
+ mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
+
+ @order << mime_type
+
+ @responses[mime_type] ||= Proc.new do
+ @response.template.formats = [mime_type.to_sym]
+ @response.content_type = mime_type.to_s
+ block_given? ? block.call : @controller.send(:render, :action => @controller.action_name)
+ end
+ end
+
+ def any(*args, &block)
+ if args.any?
+ args.each { |type| send(type, &block) }
+ else
+ custom(@mime_type_priority.first, &block)
+ end
+ end
+
+ def self.generate_method_for_mime(mime)
+ sym = mime.is_a?(Symbol) ? mime : mime.to_sym
+ const = sym.to_s.upcase
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{sym}(&block) # def html(&block)
+ custom(Mime::#{const}, &block) # custom(Mime::HTML, &block)
+ end # end
+ RUBY
+ end
+
+ Mime::SET.each do |mime|
+ generate_method_for_mime(mime)
+ end
+
+ def method_missing(symbol, &block)
+ mime_constant = Mime.const_get(symbol.to_s.upcase)
+
+ if Mime::SET.include?(mime_constant)
+ self.class.generate_method_for_mime(mime_constant)
+ send(symbol, &block)
+ else
+ super
+ end
+ end
+
+ def respond
+ for priority in @mime_type_priority
+ if priority == Mime::ALL
+ @responses[@order.first].call
+ return
+ else
+ if @responses[priority]
+ @responses[priority].call
+ return # mime type match found, be happy and return
+ end
+ end
+ end
+
+ if @order.include?(Mime::ALL)
+ @responses[Mime::ALL].call
+ else
+ @controller.send :head, :not_acceptable
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/mime/type.rb b/actionpack/lib/action_controller/mime/type.rb
new file mode 100644
index 0000000000..23a39dff54
--- /dev/null
+++ b/actionpack/lib/action_controller/mime/type.rb
@@ -0,0 +1,214 @@
+require 'set'
+
+module Mime
+ SET = []
+ EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
+ LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
+
+ def self.[](type)
+ Type.lookup_by_extension(type.to_s)
+ end
+
+ # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
+ #
+ # class PostsController < ActionController::Base
+ # def show
+ # @post = Post.find(params[:id])
+ #
+ # respond_to do |format|
+ # format.html
+ # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
+ # format.xml { render :xml => @people.to_xml }
+ # end
+ # end
+ # end
+ class Type
+ @@html_types = Set.new [:html, :all]
+ cattr_reader :html_types
+
+ # These are the content types which browsers can generate without using ajax, flash, etc
+ # i.e. following a link, getting an image or posting a form. CSRF protection
+ # only needs to protect against these types.
+ @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text]
+ cattr_reader :browser_generated_types
+ attr_reader :symbol
+
+ @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml]
+ def self.unverifiable_types
+ ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller)
+ @@unverifiable_types
+ end
+
+ # A simple helper class used in parsing the accept header
+ class AcceptItem #:nodoc:
+ attr_accessor :order, :name, :q
+
+ def initialize(order, name, q=nil)
+ @order = order
+ @name = name.strip
+ q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list
+ @q = ((q || 1.0).to_f * 100).to_i
+ end
+
+ def to_s
+ @name
+ end
+
+ def <=>(item)
+ result = item.q <=> q
+ result = order <=> item.order if result == 0
+ result
+ end
+
+ def ==(item)
+ name == (item.respond_to?(:name) ? item.name : item)
+ end
+ end
+
+ class << self
+ def lookup(string)
+ LOOKUP[string]
+ end
+
+ def lookup_by_extension(extension)
+ EXTENSION_LOOKUP[extension]
+ end
+
+ # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
+ # rendering different HTML versions depending on the user agent, like an iPhone.
+ def register_alias(string, symbol, extension_synonyms = [])
+ register(string, symbol, [], extension_synonyms, true)
+ end
+
+ def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
+ Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) }
+
+ SET << Mime.const_get(symbol.to_s.upcase)
+
+ ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup
+ ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
+ end
+
+ def parse(accept_header)
+ if accept_header !~ /,/
+ [Mime::Type.lookup(accept_header)]
+ else
+ # keep track of creation order to keep the subsequent sort stable
+ list = []
+ accept_header.split(/,/).each_with_index do |header, index|
+ params, q = header.split(/;\s*q=/)
+ if params
+ params.strip!
+ list << AcceptItem.new(index, params, q) unless params.empty?
+ end
+ end
+ list.sort!
+
+ # Take care of the broken text/xml entry by renaming or deleting it
+ text_xml = list.index("text/xml")
+ app_xml = list.index(Mime::XML.to_s)
+
+ if text_xml && app_xml
+ # set the q value to the max of the two
+ list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
+
+ # make sure app_xml is ahead of text_xml in the list
+ if app_xml > text_xml
+ list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
+ app_xml, text_xml = text_xml, app_xml
+ end
+
+ # delete text_xml from the list
+ list.delete_at(text_xml)
+
+ elsif text_xml
+ list[text_xml].name = Mime::XML.to_s
+ end
+
+ # Look for more specific XML-based types and sort them ahead of app/xml
+
+ if app_xml
+ idx = app_xml
+ app_xml_type = list[app_xml]
+
+ while(idx < list.length)
+ type = list[idx]
+ break if type.q < app_xml_type.q
+ if type.name =~ /\+xml$/
+ list[app_xml], list[idx] = list[idx], list[app_xml]
+ app_xml = idx
+ end
+ idx += 1
+ end
+ end
+
+ list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
+ list
+ end
+ end
+ end
+
+ def initialize(string, symbol = nil, synonyms = [])
+ @symbol, @synonyms = symbol, synonyms
+ @string = string
+ end
+
+ def to_s
+ @string
+ end
+
+ def to_str
+ to_s
+ end
+
+ def to_sym
+ @symbol || @string.to_sym
+ end
+
+ def ===(list)
+ if list.is_a?(Array)
+ (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
+ else
+ super
+ end
+ end
+
+ def ==(mime_type)
+ return false if mime_type.blank?
+ (@synonyms + [ self ]).any? do |synonym|
+ require "ruby-debug"
+ debugger if mime_type.is_a?(Array)
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
+ end
+ end
+
+ def =~(mime_type)
+ return false if mime_type.blank?
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+ (@synonyms + [ self ]).any? do |synonym|
+ synonym.to_s =~ regexp
+ end
+ end
+
+ # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
+ # ActionController::RequestForgeryProtection.
+ def verify_request?
+ @@browser_generated_types.include?(to_sym)
+ end
+
+ def html?
+ @@html_types.include?(to_sym) || @string =~ /html/
+ end
+
+ private
+ def method_missing(method, *args)
+ if method.to_s =~ /(\w+)\?$/
+ $1.downcase.to_sym == to_sym
+ else
+ super
+ end
+ end
+ end
+end
+
+require 'action_controller/mime/default_types'