module Merb # Module that is mixed in to all implemented controllers. module ControllerMixin # Enqueu a block to run in a background thread outside of the request # response dispatch # # ==== Parameters # takes a block to run later # # ==== Example # run_later do # SomeBackgroundTask.run # end # def run_later(&blk) Merb::Dispatcher.work_queue << blk end # Renders the block given as a parameter using chunked encoding. # # ==== Parameters # &blk:: # A block that, when called, will use send_chunks to send chunks of data # down to the server. The chunking will terminate once the block returns. # # ==== Examples # def stream # prefix = '

' # suffix = "

\r\n" # render_chunked do # IO.popen("cat /tmp/test.log") do |io| # done = false # until done # sleep 0.3 # line = io.gets.chomp # # if line == 'EOF' # done = true # else # send_chunk(prefix + line + suffix) # end # end # end # end # end def render_chunked(&blk) must_support_streaming! headers['Transfer-Encoding'] = 'chunked' Proc.new { |response| @response = response response.send_status_no_connection_close('') response.send_header blk.call response.write("0\r\n\r\n") } end # Writes a chunk from +render_chunked+ to the response that is sent back to # the client. This should only be called within a +render_chunked+ block. # # ==== Parameters # data:: a chunk of data to return. def send_chunk(data) @response.write('%x' % data.size + "\r\n") @response.write(data + "\r\n") end # ==== Parameters # &blk:: # A proc that should get called outside the mutex, and which will return # the value to render. # # ==== Returns # Proc:: # A block that Mongrel can call later, allowing Merb to release the # thread lock and render another request. def render_deferred(&blk) must_support_streaming! Proc.new {|response| result = blk.call response.send_status(result.length) response.send_header response.write(result) } end # Renders the passed in string, then calls the block outside the mutex and # after the string has been returned to the client. # # ==== Parameters # str:: A +String+ to return to the client. # &blk:: A block that should get called once the string has been returned. # # ==== Returns # Proc:: # A block that Mongrel can call after returning the string to the user. def render_then_call(str, &blk) must_support_streaming! Proc.new {|response| response.send_status(str.length) response.send_header response.write(str) blk.call } end # ==== Parameters # url:: # URL to redirect to. It can be either a relative or fully-qualified URL. # opts:: An options hash (see below) # # ==== Options (opts) # :message:: # Messages to pass in url query string as value for "_message" # :permanent:: # When true, return status 301 Moved Permanently # # ==== Returns # String:: Explanation of redirect. # # ==== Examples # redirect("/posts/34") # redirect("/posts/34", :message => { :notice => 'Post updated successfully!' }) # redirect("http://www.merbivore.com/") # redirect("http://www.merbivore.com/", :permanent => true) def redirect(url, opts = {}) default_redirect_options = { :message => nil, :permanent => false } opts = default_redirect_options.merge(opts) if opts[:message] notice = Merb::Request.escape([Marshal.dump(opts[:message])].pack("m")) url = url =~ /\?/ ? "#{url}&_message=#{notice}" : "#{url}?_message=#{notice}" end self.status = opts[:permanent] ? 301 : 302 Merb.logger.info("Redirecting to: #{url} (#{self.status})") headers['Location'] = url "You are being redirected." end def message @_message = defined?(@_message) ? @_message : request.message end # Sends a file over HTTP. When given a path to a file, it will set the # right headers so that the static file is served directly. # # ==== Parameters # file:: Path to file to send to the client. # opts:: Options for sending the file (see below). # # ==== Options (opts) # :disposition:: # The disposition of the file send. Defaults to "attachment". # :filename:: # The name to use for the file. Defaults to the filename of file. # :type:: The content type. # # ==== Returns # IO:: An I/O stream for the file. def send_file(file, opts={}) opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts)) disposition = opts[:disposition].dup || 'attachment' disposition << %(; filename="#{opts[:filename] ? opts[:filename] : File.basename(file)}") headers.update( 'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary' ) File.open(file, 'rb') end # Send binary data over HTTP 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. # # ==== Parameters # data:: Path to file to send to the client. # opts:: Options for sending the data (see below). # # ==== Options (opts) # :disposition:: # The disposition of the file send. Defaults to "attachment". # :filename:: # The name to use for the file. Defaults to the filename of file. # :type:: The content type. def send_data(data, opts={}) opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts)) disposition = opts[:disposition].dup || 'attachment' disposition << %(; filename="#{opts[:filename]}") if opts[:filename] headers.update( 'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary' ) data end # Streams a file over HTTP. # # ==== Parameters # opts:: Options for the file streaming (see below). # &stream:: # A block that, when called, will return an object that responds to # +get_lines+ for streaming. # # ==== Options # :disposition:: # The disposition of the file send. Defaults to "attachment". # :type:: The content type. # :content_length:: The length of the content to send. # :filename:: The name to use for the streamed file. # # ==== Examples # stream_file({ :filename => file_name, :type => content_type, # :content_length => content_length }) do |response| # AWS::S3::S3Object.stream(user.folder_name + "-" + user_file.unique_id, bucket_name) do |chunk| # response.write chunk # end # end def stream_file(opts={}, &stream) must_support_streaming! opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts)) disposition = opts[:disposition].dup || 'attachment' disposition << %(; filename="#{opts[:filename]}") headers.update( 'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary', # Rack specification requires header values to respond to :each 'CONTENT-LENGTH' => opts[:content_length].to_s ) Proc.new{|response| response.send_status(opts[:content_length]) response.send_header stream.call(response) } end # Uses the nginx specific +X-Accel-Redirect+ header to send a file directly # from nginx. For more information, see the nginx wiki: # http://wiki.codemongers.com/NginxXSendfile # # ==== Parameters # file:: Path to file to send to the client. def nginx_send_file(file) headers['X-Accel-Redirect'] = file return ' ' end # Sets a cookie to be included in the response. # # If you need to set a cookie, then use the +cookies+ hash. # # ==== Parameters # name<~to_s>:: A name for the cookie. # value<~to_s>:: A value for the cookie. # expires<~gmtime:~strftime, Hash>:: An expiration time for the cookie, or a hash of cookie options. # --- # @public def set_cookie(name, value, expires) options = expires.is_a?(Hash) ? expires : {:expires => expires} cookies.set_cookie(name, value, options) end # Marks a cookie as deleted and gives it an expires stamp in the past. This # method is used primarily internally in Merb. # # Use the +cookies+ hash to manipulate cookies instead. # # ==== Parameters # name<~to_s>:: A name for the cookie to delete. def delete_cookie(name) set_cookie(name, nil, Merb::Const::COOKIE_EXPIRED_TIME) end # Escapes the string representation of +obj+ and escapes it for use in XML. # # ==== Parameter # obj<~to_s>:: The object to escape for use in XML. # # ==== Returns # String:: The escaped object. def escape_xml(obj) Erubis::XmlHelper.escape_xml(obj.to_s) end alias h escape_xml alias html_escape escape_xml private # Checks whether streaming is supported by the current Rack adapter. # # ==== Raises # NotImplemented:: The Rack adapter doens't support streaming. def must_support_streaming! unless request.env['rack.streaming'] raise(Merb::ControllerExceptions::NotImplemented, "Current Rack adapter does not support streaming") end end end end