# code: # * George Moschovitis # # (c) 2004 Navel, all rights reserved. # $Id: request.rb 167 2004-11-23 14:03:10Z gmosx $ require "cgi" require "ftools" require "glue/string" require "nitro/uri" require "nitro/http" require "nitro/server/cookie" require "nitro/server/requestpart" module N # = RequestUtils # # A collection of Request utility methods. Factored out from the # Request object in a separate module to allow for inclusion in generic # request objects (for example WEBrick). # module RequestUtils # Full URI # def full_uri if @query_string return "#{@translated_uri}?#{@query_string}" else return @translated_uri end end # Expand URI # Calculates a new uri based on the request.uri that includes # the additional parameters suplied. # def expand_uri(params) hash = @parameters.dup() hash.update(params) pairs = [] hash.each { |param, value| pairs << "#{param}=#{value}" } # gmosx: hash is ALWAYS non empty! return "#{@translated_uri}?#{pairs.join(';')}" end # Uses the passed oid_param to load a managed object (entity). # Enforces some form of security by checking the klass of the # requested entity. # def get_entity(oid_param, klass = nil) if oid = self[oid_param] obj = $og.load(oid, klass) if klass if obj.is_a?(klass) return obj else return nil end else return obj end else # $log.error "request.get_object('#{oid_param}') failed!" return nil end end # Uses the passed name_param to load a managed object (entity) # by name. # Enforces some form of security by checking the klass of the # requested entity. # def get_entity_by_name(name_param, klass) if name = self[name_param] obj = $og.load_by_name(name, klass) if klass if obj.is_a?(klass) return obj else return nil end else # $log.error "request.get_object_by_name('#{name_param}') failed!" return obj end else return nil end end end # = Request # # The context of an http protocol request. Encapsulates both # the request and the response context. # # === Future: # # - USE libapreq! # - Dont use env_table (arghhhh!) # - dont use a separate path and real path # - MEGA: unify request and request (like a socket, io # stream,etc). Evan subclass IO! # - extend request from hash? to make more compatible with irb? # # === Design: # # - uri: the original uri entered to the browser (INCLUDES qs) # gmosx: i included the qs and removed full_uri (always forgot # to use it anyway, was very error prone) # - translated_uri: as translated by the web server (no query string) # - path: the path to the actual script (or object) # # Example: # # http://www.site.com/faq/?id=1 # -> # uri: /faq/?id=1 # translated_uri: /faq/index.sx # path: base/site/root/faq/index.sx # query_string: id=1 # # - querystring should probably include the ? # - Encapsulate ModRuby/Apache requests # - Based on resin3.0 excellent code: # com/caucho/server/http/HttpRequest.java # - use as much of ruby's default cgi/http code as # possible (why reinvent the wheel?) # Will use params with symbols for args. use uppercase for system # 'args'. # class Request # include usefull query parsing code from the standard # lib. ARGHHH cgi.rb sucks! include CGI::QueryExtension include RequestUtils # request methods enumeration METHOD_GET = 0 METHOD_POST = 1 METHOD_HEAD = 2 # request method attr_accessor :method # content type attr_accessor :content_type # the uri for the request. For sub-requests keeps the # uri of the top level request. # gmosx: the writer is needed for injects. attr_accessor :uri # the uri as translated by the web server. For sub-requests keeps # the usri of the top level request. attr_accessor :translated_uri # the path to the actual object (script) # gets overriden by sub-requests, is not needed in scripts. attr_accessor :path # path info (extra parameters in the uri) # attr_accessor :path_info # the query string part of the uri attr_accessor :query_string # The query string is parsed to the parameters # hash. attr_accessor :parameters # alias for the parameters hash alias_method :query, :parameters # alias for the parameters hash alias_method :params, :parameters # the session this request is part-of. attr_accessor :session # The parts attached to this request. # A part is typically an uploaded file. By using # a separate hash instead of the parameters hash one # can easily enumerate parts. attr_accessor :parts # The level of the request (0 = toplevel). When the evaluating # the sub-scripts that the top level script includes, the # request level is incremented. attr_accessor :level # the remote address for this request attr_accessor :remote_addr # Is the request cacheable? # Set this attribute to 'true' to avoid caching the request # fragment. Used to avoid caching 'action' requests. attr_accessor :uncacheable # the locale hash for this request. attr_accessor :locale # the shader for this request. attr_accessor :shader # the script hash for this request attr_accessor :tag # keep the original handler process uri, usefull as a cache key. # Typically updated in subrequests only. Investigate if we could # use a hash here! attr_accessor :fragment_hash # Keep all errors to present them in-page when in admin mode. attr_accessor :error_log # The top level script for this request. attr_accessor :top_script # last modified cache attr_accessor :lm # request: # the incoming headers # gmosx: writer needed for inject (sub-req cloning) attr_accessor :in # the incoming cookies attr_accessor :in_cookies # response: # the outgoing headers attr_accessor :out # the outgoing cookies attr_accessor :out_cookies # the outgoing buffer attr_accessor :out_buffer # request content type, default: text/html attr_accessor :content_type # HTTP request status attr_accessor :status # HTTP request message attr_accessor :message def initialize # set to 0 (== top level). When including fragments # the level is incremented. @level = 0 # gmosx: it would be good to defere the hash creation, # but having the hash always created saves as a LOT of # checks in client code, so we create it here. # # FIXME: WE SHOULD NOT CREATE unneeded hash objects. # # @parts = {} # request: # provide some fair initialization values. set_status(200) @content_type = "text/html; charset=iso-8859-7" @out = {} @out_cookies = {} end #----------------------------------------------------------------------- # Headers # Return the referer to this resource. For the initial page in the # clickstream there is no referer, set "/" by default. def referer return @in["REFERER"] || "/" end alias_method :referrer, :referer #----------------------------------------------------------------------- # Cookies # this method is also usefull for probing (testing) the request class. # FIXME: optimize this (libapreq) def parse_cookies(cookie_string) @cookies = Cookie.parse(cookie_string) end alias_method :parse_cookie_string, :parse_cookies # === Input: # # the cookie name # # === Output: # - the cookie value, or an array of values for multivalued cookies. # - nil if the cookie doesnt exist. # # === Example: # nsid = request.get_cookie("nsid") # def get_cookie(cookie_name) return nil unless @in_cookies cookie = @in_cookies[cookie_name] return nil unless cookie return CGI.unescape(cookie.value) end # Removes a cookie from the client by seting the expire # time to the past (epoch). # def del_cookie(name) cookie = N::Cookie.new(name, "nil") cookie.path = "/" cookie.expires = Time.at(0) @out_cookies[name] = cookie end #------------------------------------------------------------------------------- # Query # Parse the query string and populate the parameters hash. # def parse_query_string return N::UriUtils.query_string_to_hash(@query_string) end # Return the value of a query parameter # def [](name) return @parameters[name] end # Same as [] but enforces a default value to! # Also tries to guess the parameters type from the default # value. It works like delete (ie returns nil for 'empty' # String parameters). # def get(key, default=nil) val = @parameters[key] if !val or (val.is_a?(String) and (not G::StringUtils.valid?(val))) @parameters[key] = default return default elsif default.is_a?(Integer) return val.to_i else return val end end # Set the value of a query parameter # # === FIXME: # # - handle multivalued parameters! # def []=(name, value) @parameters[name] = value end #----------------------------------------------------------------------- # Utilities # Returns true for the top-level request, but false for # any inject or forward # def is_top? return 0 == @level end # Is this an admin request? # FIXME: no longer valid, recode. # def admin? return @parameters.include?("*admin") end # Shorthand for request.session.user # def user return @session.user end # Shorthand for request.session.user.anonymous? # def anonymous? return @session.user.anonymous? end # Set errors as a transaction entity. # Returns the txid for the errors def set_errors(errors) new_tx_entity!(errors, "errid") unless errors.empty? end # Shorthand # # Output: # nil if no errors. # def errors return del_tx_entity!("errid") end # Returns the errors as an array. # def errors_to_a if errors = del_tx_entity!("errid") return errors.values end return nil end alias_method :errors_list, :errors_to_a # Check if a parameter is valid # def param?(param) return G::StringUtils.valid?(self[param]) end alias_method :action?, :param? # Check if a parameter exists! # Example: # url:www.mysite.com/page.sx?admin # request.include?(admin) => true # def include?(param) return @parameters.include?(param) end def update(*params) @parameters.update(*params) end # Use the delete name to make the request compatible with # hashes. # Tests is a parameter is passed to the request and removes it! # Used in action handlers. # def delete(param) oparam = param if param = @parameters.delete(param) # gmosx: remove from querystring too! NEEDED. # perhaps kinda slow but happens seldom and optimizes # another frequent case @query_string = @parameters.collect { |k, v| (v && k.is_a?(String)) ? "#{k}=#{v}" : k }.join(";") # If the parameter exist this is an action request, so # do NOT cache the fragment. @uncacheable = true end # gmosx: to avoid using param? if param.is_a?(String) and (not G::StringUtils.valid?(param)) return nil else return param end end # exclude those parameters for security EXCLUDED_PARAMETERS = %w{ oid pid name } # gmosx: hmm this is a really dangerous method, the EXCLUDED params # above dont seem enough :( # def update_entity(entity) @parameters.each { |param, val| begin # gmosx: DO NOT escape by default !!! # gmosx: We need to get non valid params if (not EXCLUDED_PARAMETERS.include?(param)) entity.send("__force_#{param}", val) end rescue NameError next end } return entity end # -------------------------------------------------------------------- # The tx sequence increases, the count of transactions / session # is usually bounded. # def new_tx_entity!(entity, txparam = "txid") unless seq = @session["TXSEQ"] seq = 0 end seq += 1 @session["TXSEQ"] = seq txid = "TX#{seq}" @session[txid] = entity @parameters[txparam] = txid return txid end # Set (update) an existing tx entity. Does NOT increase the # tx sequence. # def set_tx_entity!(entity, txparam = "txid") if txid = @parameters[txparam] @session[txid] = entity end end alias_method :update_tx_entity!, :set_tx_entity! # # def get_tx_entity(txparam = "txid") if txid = @parameters[txparam] return @session[txid] end return nil end # # def del_tx_entity!(txparam = "txid") if txid = @parameters[txparam] @session.delete(txid) end end # -------------------------------------------------------------------- # Debugging helper # Accumulate errors in a request log. This log can be presented # in the offending page when running in debug mode. # def log_error(str) @error_log = [] unless @error_log @error_log << str if @error_log.size < 200 # gmosx: dod attack! $log.error str end # ==================================================================== # Response # TODO: add status codes, messages # Set the request HTTP status and lookup the # corresponding request status message. def set_status(status = 200) @status = status @message = HTTP::STATUS_STRINGS[status] end # Set the HTTP NOT_MODIFIED status code. # Usefull for HTTP Caching. # def set_not_modified! @status = 304 @message = N::HTTP::STATUS_STRINGS[status] end # 302 is the redirect status! # # === Status 303: # # The request to the request can be found # under a different URI and SHOULD be retrieved using # a GET method on that resource. This method exists # primarily to allow the output of a POST-activated script # to redirect the user agent to a selected resource. The new # URI is not a substitute reference for the originally # requested resource. The 303 request MUST NOT be cached, # but the request to the second (redirected) request might # be cacheable. # # Note: Many pre-HTTP/1.1 user agents do not understand the # 303 status. When interoperability with such clients is a # concern, the 302 status code may be used instead, since # most user agents react to a 302 request as described # here for 303. # # === WARNING: # # Konqueror always performs a 307 redirect ARGH! # # === Redesign: # # Use one redirect method with an optional status # parameter, that reads messages from the status # constants. # # === Input: # # - url to redirect to # - if force_exit == true raises a ScriptExitException # - status (303 or 307) default = 303 # def redirect(url = nil, force_exit = false, status = 302) # FIXME: normalize the url # url = $srv_url + url if url =~ /^\//om # FIXME: check arguments # enforce a meaningfull default url ||= self["_go"] || referer() # the url should have a leading "/" # enforce it to be sure. FIXME: optimize this! url = "/#{url}".squeeze("/") unless url =~ /^http/ @out["Location"] = url # gmosx: NOT needed? see the exceprt from the spec. # @out['Cache-Control'] = "max-age=1" set_status(status) @out_buffer = "The URL has moved here" if force_exit # Stop rendering the script immediately! # This is the default behaviour! raise N::ScriptExitException end # for unit testing return url end # Internal redirect # # FIXME: implement me # def internal_redirect(url) end # Utility method to set the expires header. # def expires!(exp_time) @out["Expires"] = N::HttpUtils.time_to_string(exp_time) end def expires? return @out["Expires"] end end end # module