web/request.py
branchstable
changeset 8463 a964c40adbe3
parent 8316 d5b1b75805dd
child 8480 086cff6a306a
child 8496 e4d71fc0b701
--- a/web/request.py	Tue Jul 10 10:33:19 2012 +0200
+++ b/web/request.py	Tue Jul 10 15:07:52 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -25,8 +25,9 @@
 from hashlib import sha1 # pylint: disable=E0611
 from Cookie import SimpleCookie
 from calendar import timegm
-from datetime import date
+from datetime import date, datetime
 from urlparse import urlsplit
+import httplib
 from itertools import count
 from warnings import warn
 
@@ -43,8 +44,8 @@
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET
-from cubicweb.web.http_headers import Headers, Cookie
+from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
 
 _MARKER = object()
 
@@ -81,34 +82,53 @@
 
 
 class CubicWebRequestBase(DBAPIRequest):
-    """abstract HTTP request, should be extended according to the HTTP backend"""
-    json_request = False # to be set to True by json controllers
+    """abstract HTTP request, should be extended according to the HTTP backend
+    Immutable attributes that describe the received query and generic configuration
+    """
+    ajax_request = False # to be set to True by ajax controllers
 
-    def __init__(self, vreg, https, form=None):
+    def __init__(self, vreg, https=False, form=None, headers={}):
+        """
+        :vreg: Vregistry,
+        :https: boolean, s this a https request
+        :form: Forms value
+        """
         super(CubicWebRequestBase, self).__init__(vreg)
+        #: (Boolean) Is this an https request.
         self.https = https
+        #: User interface property (vary with https) (see uiprops_)
+        self.uiprops = None
+        #: url for serving datadir (vary with https) (see resources_)
+        self.datadir_url = None
         if https:
             self.uiprops = vreg.config.https_uiprops
             self.datadir_url = vreg.config.https_datadir_url
         else:
             self.uiprops = vreg.config.uiprops
             self.datadir_url = vreg.config.datadir_url
-        # raw html headers that can be added from any view
+        #: raw html headers that can be added from any view
         self.html_headers = HTMLHead(self)
-        # form parameters
+        #: received headers
+        self._headers_in = Headers()
+        for k, v in headers.iteritems():
+            self._headers_in.addRawHeader(k, v)
+        #: form parameters
         self.setup_params(form)
-        # dictionary that may be used to store request data that has to be
-        # shared among various components used to publish the request (views,
-        # controller, application...)
+        #: dictionary that may be used to store request data that has to be
+        #: shared among various components used to publish the request (views,
+        #: controller, application...)
         self.data = {}
-        # search state: 'normal' or 'linksearch' (eg searching for an object
-        # to create a relation with another)
+        #:  search state: 'normal' or 'linksearch' (eg searching for an object
+        #:  to create a relation with another)
         self.search_state = ('normal',)
-        # page id, set by htmlheader template
+        #: page id, set by htmlheader template
         self.pageid = None
         self._set_pageid()
         # prepare output header
+        #: Header used for the final response
         self.headers_out = Headers()
+        #: HTTP status use by the final response
+        self.status_out  = 200
 
     def _set_pageid(self):
         """initialize self.pageid
@@ -121,10 +141,41 @@
             self.html_headers.define_var('pageid', pid, override=False)
         self.pageid = pid
 
+    def _get_json_request(self):
+        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+             DeprecationWarning, stacklevel=2)
+        return self.ajax_request
+    def _set_json_request(self, value):
+        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+             DeprecationWarning, stacklevel=2)
+        self.ajax_request = value
+    json_request = property(_get_json_request, _set_json_request)
+
+    def base_url(self, secure=None):
+        """return the root url of the instance
+
+        secure = False -> base-url
+        secure = None  -> https-url if req.https
+        secure = True  -> https if it exist
+        """
+        if secure is None:
+            secure = self.https
+        base_url = None
+        if secure:
+            base_url = self.vreg.config.get('https-url')
+        if base_url is None:
+            base_url = super(CubicWebRequestBase, self).base_url()
+        return base_url
+
     @property
     def authmode(self):
+        """Authentification mode of the instance
+
+        (see `Configuring the Web server`_)"""
         return self.vreg.config['auth-mode']
 
+    # Various variable generator.
+
     @property
     def varmaker(self):
         """the rql varmaker is exposed both as a property and as the
@@ -259,7 +310,6 @@
             form = self.form
         return list_form_param(form, param, pop)
 
-
     def reset_headers(self):
         """used by AutomaticWebTest to clear html headers between tests on
         the same resultset
@@ -701,14 +751,33 @@
         return 'view'
 
     def validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
+        """raise a `StatusResponse` exception if a cached page along the way
         exists and is still usable.
 
         calls the client-dependant implementation of `_validate_cache`
         """
-        self._validate_cache()
-        if self.http_method() == 'HEAD':
-            raise StatusResponse(200, '')
+        modified = True
+        if self.get_header('Cache-Control') not in ('max-age=0', 'no-cache'):
+            # Here, we search for any invalid 'not modified' condition
+            # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
+            validators = get_validators(self._headers_in)
+            if validators: # if we have no
+                modified = any(func(val, self.headers_out) for func, val in validators)
+        # Forge expected response
+        if modified:
+            if 'Expires' not in self.headers_out:
+                # Expires header seems to be required by IE7 -- Are you sure ?
+                self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
+            if self.http_method() == 'HEAD':
+                raise StatusResponse(200, '')
+            # /!\ no raise, the function returns and we keep processing the request)
+        else:
+            # overwrite headers_out to forge a brand new not-modified response
+            self.headers_out = self._forge_cached_headers()
+            if self.http_method() in ('HEAD', 'GET'):
+                raise StatusResponse(httplib.NOT_MODIFIED)
+            else:
+                raise StatusResponse(httplib.PRECONDITION_FAILED)
 
     # abstract methods to override according to the web front-end #############
 
@@ -716,11 +785,19 @@
         """returns 'POST', 'GET', 'HEAD', etc."""
         raise NotImplementedError()
 
-    def _validate_cache(self):
-        """raise a `DirectResponse` exception if a cached page along the way
-        exists and is still usable
-        """
-        raise NotImplementedError()
+    def _forge_cached_headers(self):
+        # overwrite headers_out to forge a brand new not-modified response
+        headers = Headers()
+        for header in (
+            # Required from sec 10.3.5:
+            'date', 'etag', 'content-location', 'expires',
+            'cache-control', 'vary',
+            # Others:
+            'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
+            value = self._headers_in.getRawHeaders(header)
+            if value is not None:
+                headers.setRawHeaders(header, value)
+        return headers
 
     def relative_path(self, includeparams=True):
         """return the normalized path of the request (ie at least relative
@@ -732,12 +809,37 @@
         """
         raise NotImplementedError()
 
-    def get_header(self, header, default=None):
-        """return the value associated with the given input HTTP header,
-        raise KeyError if the header is not set
+    # http headers ############################################################
+
+    ### incoming headers
+
+    def get_header(self, header, default=None, raw=True):
+        """return the value associated with the given input header, raise
+        KeyError if the header is not set
         """
-        raise NotImplementedError()
+        if raw:
+            return self._headers_in.getRawHeaders(header, [default])[0]
+        return self._headers_in.getHeader(header, default)
+
+    def header_accept_language(self):
+        """returns an ordered list of preferred languages"""
+        acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
+        for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
+                              reverse=True):
+            lang = lang.split('-')[0]
+            yield lang
 
+    def header_if_modified_since(self):
+        """If the HTTP header If-modified-since is set, return the equivalent
+        date time value (GMT), else return None
+        """
+        mtime = self.get_header('If-modified-since', raw=False)
+        if mtime:
+            # :/ twisted is returned a localized time stamp
+            return datetime.fromtimestamp(mtime) + GMTOFFSET
+        return None
+
+    ### outcoming headers
     def set_header(self, header, value, raw=True):
         """set an output HTTP header"""
         if raw:
@@ -785,12 +887,6 @@
         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
         return (raw_value for (raw_value, parsed_value, score) in values)
 
-    def header_if_modified_since(self):
-        """If the HTTP header If-modified-since is set, return the equivalent
-        mx date time value (GMT), else return None
-        """
-        raise NotImplementedError()
-
     def demote_to_html(self):
         """helper method to dynamically set request content type to text/html
 
@@ -805,6 +901,8 @@
             self.set_content_type('text/html')
             self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
 
+    # xml doctype #############################################################
+
     def set_doctype(self, doctype, reset_xmldecl=True):
         """helper method to dynamically change page doctype