require 'singleton' require 'sync' require 'stringio' require 'facet/string/blank' require 'glue/attribute' require 'glue/settings' require 'glue/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. class ActionExit < Exception; end # Raise or Throw this exception to stop rendering altogether. # Typically called by redirects. class RenderExit < Exception; 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 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 base url for this render. attr_accessor :base # The name of the current controller. #-- # gmosx: Needed for WEE. Will be deprecated. #++ attr_accessor :controller_name # Initialize the render. # # [+context+] # A parent render/controller acts as the context. def initialize(context, base = nil) @rendering_context = 0 @request = @response = @context = context @controller_name = @base = base @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 @base # path of the controller. def render(path) # Convert relative paths to absolute paths. path = "#@base/#{path}" unless path =~ /^\// Logger.debug "Rendering '#{path}'." if $DBG klass, action, base = @context.dispatcher.dispatch(path, @context) # FIXME: @context.content_type = klass.instance_variable_get('@content_type') || 'text/html' raise 'No controller for action' unless klass @context.level += 1 if self.class == klass self.send(action) else klass.new(@context, base).send(action) end @context.level -= 1 rescue NoActionError => 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. def exit raise ActionExit.new end # Flush the IO object 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. def redirect(url, status = 303) url = url.to_s unless url =~ /^http/ url = "#@base/#{url}" unless url =~ /^\// 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) 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) end alias_method :redirect_to_home, :redirect_home # :section: Seaside style call/answer methods. # Call redirects to the given url but push the original # url in a callstack, so that the target can return by # executing answer. # # === Example # # caller: # color, type = call('utils/select_color') # # target: # answer(color, type) #-- # FIXME: dont use yet, you have to encode the branch to # make this safe for use. #++ def call(url, status = 303) (session[:CALL_STACK] ||= {}) << request.uri redirect(url, status) end # Returns from a call by poping the callstack. #-- # FIXME: don't use yet. #++ def answer(index = 0, status = 303) if stack = session[:CALL_STACK] and not stack.empty? redirect(stack.pop, status) else raise 'Cannot answer, call stack is empty' end end # 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. def render_template(filename) filename = "#{filename}.xhtml" unless filename =~ /\.xhtml$/ template = File.read("#{template_root}/#{filename}") Glue::Template.process_template(template, '@out', binding) end # Access the programmatic renderer (builder). def build(&block) if block.arity == 1 yield Glue::XmlBuilder.new(@out) else Glue::XmlBuilder.new(@out).instance_eval(&block) end end # Return a programmatic renderer that targets the # output buffer. def builder Glue::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 # * George Moschovitis