require 'singleton' require 'sync' require 'stringio' require 'facet/string/blank' require 'glue/settings' require 'nitro/template' require 'glue/builder/xml' require 'nitro/helper/xhtml' require 'nitro/helper/form' require 'nitro/helper/table' require 'nitro/helper/buffer' module Nitro #-- # Raise or Throw this exception to stop the current action. # Typically called to skip the template. This is considerered # a low level tactic. Prefer to use the exit method. #++ class ActionExit < Exception # :nodoc: all end #-- # Raise or Throw this exception to stop rendering altogether. # Typically called by redirects. #++ class RenderExit < Exception # :nodoc: all end #-- # The output buffer. The output of a contoller action is # accumulated in this buffer before sending this to the client # as a HTTP Response. # # TODO: Implement a FAST string (maybe in C) #++ class OutputBuffer < String # :nodoc: all end # The rendering mixin. This module is typically included in # published objects and/or controllers to provide rendering # functionality. #-- # TODO: handle template_root here instead of the # controller. #++ module Render include BufferHelper # If true, auto redirect to referer on empty buffer. setting :redirect_on_empty, :default => false, :doc => 'If true, auto redirect to referer on empty buffer' # The output buffer. The output of a script/action is # accumulated in this buffer. attr_accessor :out alias_method :body, :out # The context. attr_accessor :context alias_method :ctx, :context alias_method :ctx=, :context= # Aliases for context. attr_accessor :request, :response # An array holding the rendering errors for this # request. attr_accessor :rendering_errors # The name of the currently executing action. attr_accessor :action_name # The current controller class. attr_accessor :controller # Initialize the render. # # [+context+] # A parent render/controller acts as the context. def initialize(context) @request = @response = @context = context @out = context.out end # Renders the action denoted by path. The path # is resolved by the dispatcher to get the correct # controller. # # Both relative and absolute paths are supported. Relative # paths are converted to absolute by prepending the mount path # of the controller. def render(path) # Convert relative paths to absolute paths. path = "#{self.class.mount_path}/#{path}".squeeze('/') unless path =~ /^\// Logger.debug "Rendering '#{path}'." if $DBG @controller, action = @context.dispatcher.dispatch(path.to_s, @context) raise 'No controller for action' unless @controller # FIXME: @context.content_type = @controller.instance_variable_get('@content_type') || 'text/html' @context.level += 1 old_controller = Controller.replace_current(@controller) if self.class == @controller self.send(action) else @controller.new(@context).send(action) end Controller.replace_current(old_controller) @context.level -= 1 rescue ActionError => e1 log_error(e1, path, false) print '(error)' rescue RenderExit, ActionExit => e2 # Just stop rendering. For example called by redirects. rescue Exception, StandardError => e3 # More fault tolerant, only flags the erroneous box with # error not the full page. log_error(e3, path) print '(error)' end private # Helper method to exit the current action, typically used # to skip the template rendering. # # === Example # # def my_action # ... # exit unless user.admin? # end def exit raise ActionExit.new end # Flush the IO object (OutputBuffer) if we are in streaming # mode. def flush @out.flush if @out.is_a?(IO) end # :section: Redirection methods. # Send a redirect response. # # If the url starts with '/' it is considered absolute, else # the url is considered relative to the current controller and # the controller base is prepended. # # The parameters are passed to the R operator (encode_url) # to actually build the url. So the following forms (among # others) are allowed: # # redirect 'home/login' # redirect ForaController, :post, :title, 'The title' # redirect :welcome # redirect article # => article.to_href # # You can also pass optional hash parameters at the end, # for example: # # redirect :welcome, :status => 307 # # The default redirect status is 303. def redirect(*args) # If this is an ajax and/or rpc request skip the redirect. # Allows to write more reusable code. return if request.script? if args.last.is_a? Hash status = args.last.fetch(:status, 303) else status = 303 end url = encode_url(*args) # gmosx, THINK: this may be unnecessary! unless url =~ /^http/ url = "#{@context.host_url}/#{url.gsub(/^\//, '')}" end @context.status = status @context.out = "#{url}.\n" @context.response_headers['location'] = url raise RenderExit end alias_method :redirect_to, :redirect # Redirect to the referer. def redirect_referer(postfix = nil, status = 303) redirect "#{@context.referer}#{postfix}", :status => status end alias_method :redirect_to_referer, :redirect_referer alias_method :redirect_referrer, :redirect_referer alias_method :redirect_to_referrer, :redirect_referer # Redirect to home. def redirect_home(status = 303) redirect '/', :status => status end alias_method :redirect_to_home, :redirect_home # Log a rendering error. def log_error(error, path, full = true) (@rendering_errors ||= []) << [error, path] if full # gmosx: Hmm perhaps this should not be logged # to avoid DOS attacks. Logger.error "Error while handling '#{path}'." Logger.error pp_exception(error) else Logger.error error.to_s end end # Convenience method to lookup the session. def session @context.session end # Add some text to the output buffer. def render_text(text) @out << text end alias_method :print, :render_text # Render a template into the output buffer. =begin def render_template(*args) url = encode_url(*args) filename = "#{url}.xhtml" unless url =~ /\.xhtml$/ template = File.read(File.join(Template.root, filename)) Template.process_template(template, '@out', binding) end =end def render_template(path) render(path) exit end alias_method :template, :render_template # Access the programmatic renderer (builder). def build(&block) if block.arity == 1 yield XmlBuilder.new(@out) else XmlBuilder.new(@out).instance_eval(&block) end end # Return a programmatic renderer that targets the # output buffer. def builder XmlBuilder.new(@out) end # A Helper class to access rendering mixins. Useful to avoid # poluting the Render with utility methods. #-- # TODO: find a less confusing name. #++ class Emitter include Singleton include XhtmlHelper include FormHelper include TableHelper end # A helper to access the utilities emitter: # # #{emit :form, entity} # #{emit :options, :labels => [..], :values => [..], :selected => 1} # # Useful to avoid poluting the render with mixin methods. def emit(meth, *options) Emitter.instance.send(meth, *options) end end end