# = Base class for scripts. # # Typically a handler evaluates a script. The base class describing # a script is defined here. # A generalized fragment caching system is also defined. # # see handlers/page-handler::PageScript for more information. # # TODO: # - convert all methods to __ to avoid collisions. # # code: # George Moschovitis # # (c) 2003 Navel, all rights reserved. # $Id: script.rb 71 2004-10-18 10:50:22Z gmosx $ require "fileutils" require "n/utils/cache" require "n/app/fragment" module N # = ScriptExitException # # Raise this Exception in your Script to avoid processing. # Typicaly used in request.redirect scenarios! # class ScriptExitException < Exception; end end # module module N; module App # = Script # # Base class for scripts. Typically a handler evaluates a script. # The base class describing a script is defined here. # A generalized fragment caching system is also defined. # # see handlers/page-handler::PageScript for more information. #-- # no need to prepend __ to the attributes! (@ is prepended) #++ # class Script # gmosx: it would be a good idea to make this a singleton, but we # had some problems when redefining the class when monitor_scripts # is enabled. investigate this! # include Singleton # the full path to the actual script file attr_accessor :path # the sub scripts. attr_accessor :sub_scripts attr_accessor :key # is this script cacheable? NONE, SERVER, DOWNSTREAM, CLIENT attr_accessor :cacheability def initialize(path) @path = "#$root_dir/#{path}" # gmosx, INVESTIGATE: createtime == filemtime ? # the creation time for this script-class @create_time = Time.now # when the filename was last modified @file_mtime = File.mtime(@path).to_i # sub-scripts set: a set of files this script depends on. # We use a hash to implement a set. @sub_scripts = N::SafeArray.new # a cache for the script outputs (fragments). Keeps multiple revisions # of the script output according to user, access rights etc. # must be thread safe because it is shared accross threads. # # gmosx: DONT DEPRECATE THIS! if we switch to FastCGI, perhaps # this will be needed! # # DISK CACHING IS SLOW! # @fragment_cache = N::LRUCache.new(1000) __init() end # This method is called at compile time to perform # additional initialization. # def __init end # This method is called before rendering to perform # additional initialization. # def __init_render(request) # nop end # Tag for this request # def __tag(request) # gmosx: dont return nil! return "" end # Recursively calculate tag. # def __calc_tag(request) tag = __tag(request) if @sub_scripts for script in @sub_scripts tag << script.__calc_tag(request) end end return tag end # Calculate LastModified for HTTP1.1 caching # Return the last modified time for this script. # def __lm(request) return nil end # Calculate LastModified for HTTP1.1 caching # takes into account all subscripts! also takes into account # the script file modification # def __calc_lm(request) unless lm = __lm(request) lm = @file_mtime else lm = @file_mtime if @file_mtime > lm end if @sub_scripts for script in @sub_scripts if slm = script.__calc_lm(request) unless lm lm = slm else lm = slm if slm > lm end end end end return lm end # Calculates the ETag fof HTTP1.1 caching. This etag is typically # used in the downstream, so we can be VERY granular :-) ie # we can even encode a user id! # # Coded by top level scripts only! # Typically combines __tag and __last_modified # def __etag(request) return "#{__calc_lm(request)}#{__calc_tag(request)}#{request.tag}" # .hash.to_s end # Is this script cacheable for this request? # def __cache?(request) # By default if lm is overriden the page is cacheable return __lm(request) end #--------------------------------------------------------------------- # Caching logic: # # gmosx, DEPRECATED: the following caching methods are deprecated! # they are still used by rx handler though! # clear the fragment_cache # def __cache_clear @fragment_cache.clear end # get a fragment from the fragment cache in a thread safe manner. # # Output: # the fragment object encapsulating the output of this script. # def __cache_get(key) return @fragment_cache[key] end # add a new fragment in the fragment cache in a thread safe manner. # encapsulate the fragment text (== body) in a fragment object. # def __cache_put(key, fragment) @fragment_cache[key] = fragment end #-------------------------------------------------------------------------- # Rendering logic: # renders the script as html output to be send to the # borwser. # the output of this script is the script fragment. # this method is constructed dynamically by the handler. # # Design: # # the parameters to this method are visible to page scripts. # in order to avoid namespace pollution and dangerous bugs in the # scripts we prepend an underscore to differentiate them. n1 used # @variables (class attributes) but we believe they where thread-unsafe. # def __render(request) end # Dynamically include ("inject") a subpage (fragment) in this page. # Dynamic means at run-time. # def __inject(url, request) return "" end # Dynamically include ("inject") a subpage (fragment) in this page. # Dynamic means at run-time. def __include(url, request) return "" end def __create_time return @create_time end def __file_mtime return @file_mtime end def __path return @path end #-------------------------------------------------------------------------- # Actions: def admin?(request) return false; end # Encodes an action url to the same page. # Params is almost always != nil, no need to optimize the case. # def __action(request, params = nil) if request.parameters.empty? return "#{request.translated_uri}?#{params}" else return "#{request.translated_uri}?#{request.query_string};#{params}" end end alias_method :_a, :__action #-------------------------------------------------------------------------- # Hooks: # executed prior to script evaluation, even for cached fragments! # not used yet. # def __pre_evaluate(request) end # executed after script evaluation, even for cached fragments! # not used yet. def __post_evaluate(request) end # Pre-render hook. NOT executed for cached fragments # not used yet. # # Returns: # text that is prepended to the request, by default the EMPTY_STRING # def __pre_render(request) return EMPTY_STRING end # Post-render hook. NOT executed for cached fragments. # not used yet. # # Returns: # text that is appended to the request, by default the EMPTY_STRING # def __post_render(request) return EMPTY_STRING end # Calculate a hash modifier (flag) to differentiate caching according # to user roles or other dynamic conditions. The calculated flag # is appended to the fragmant hash. # # Design: # use this separate method to allow the top level page to # collect the flags for all sub-pages and build # a super-flag. # def cache_flag(request) return Fragment::ADMIN_FLAG if admin?(request) end # enable fragment caching for this script? # to avoid storing in the cache uncacheable fragments # def cache?(request) # gmosx: AAAAAAAAARGHHH!!! NASTY keep this false! # to save memory! return false end # is the output of the script (the fragment) valid for this request? # used for cache invalidation calculations. By default returns false. # # Input: # __fragment_key: the server automatically generates a fragment_key (typically # the request.real_path) and passes this to this methods for further # customization. The key passed is the base key, the method is responsible to # build the final key (taking user roles, etc into account). # request: used to customize the fragment_key for this request. # customized fragment_keys denote request classes. # # Output: # is the fragment valid for this request? # # Side-effects: # WARNING: the method potentially modifies __fragment_key! # def cache_valid?(fragment, request) return false end # authorize access to this script ? # def __authorize?(request) return true end # get the shader to use for this script # def __shader(request) return nil end # force a logged-in user for this page. # implemented like the n1-version to avoid checking everytime # for login, use the if __force method. # def __force_login(request) if request.session["USER"].anonymous? request.redirect("/id/login.sx?_go=#{request.uri}") return false end return true end # -------------------------------------------------------------------- # Macros # TODO: create more usefull macros for admin?/cache_valid? etc. # Sets cache?() to return true # def self.enable_cache! class_eval %{ def __cache?(request) return true end } end # Evals a standard admin?() method. # def self.admin_role(role) class_eval %{ def admin?(request) return request.user.role?("#{role}") end } end end # class end; end # module