diff -r 8af7c6d86efb -r a964c40adbe3 web/request.py --- 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