web/request.py
branchstable
changeset 8463 a964c40adbe3
parent 8316 d5b1b75805dd
child 8480 086cff6a306a
child 8496 e4d71fc0b701
equal deleted inserted replaced
8461:8af7c6d86efb 8463:a964c40adbe3
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    23 import random
    23 import random
    24 import base64
    24 import base64
    25 from hashlib import sha1 # pylint: disable=E0611
    25 from hashlib import sha1 # pylint: disable=E0611
    26 from Cookie import SimpleCookie
    26 from Cookie import SimpleCookie
    27 from calendar import timegm
    27 from calendar import timegm
    28 from datetime import date
    28 from datetime import date, datetime
    29 from urlparse import urlsplit
    29 from urlparse import urlsplit
       
    30 import httplib
    30 from itertools import count
    31 from itertools import count
    31 from warnings import warn
    32 from warnings import warn
    32 
    33 
    33 from rql.utils import rqlvar_maker
    34 from rql.utils import rqlvar_maker
    34 
    35 
    41 from cubicweb.uilib import remove_html_tags, js
    42 from cubicweb.uilib import remove_html_tags, js
    42 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
    43 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
    43 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
    44 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
    44 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
    45 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
    45                           RequestError, StatusResponse)
    46                           RequestError, StatusResponse)
    46 from cubicweb.web.httpcache import GMTOFFSET
    47 from cubicweb.web.httpcache import GMTOFFSET, get_validators
    47 from cubicweb.web.http_headers import Headers, Cookie
    48 from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
    48 
    49 
    49 _MARKER = object()
    50 _MARKER = object()
    50 
    51 
    51 def build_cb_uid(seed):
    52 def build_cb_uid(seed):
    52     sha = sha1('%s%s%s' % (time.time(), seed, random.random()))
    53     sha = sha1('%s%s%s' % (time.time(), seed, random.random()))
    79     return [v for v in value if v != INTERNAL_FIELD_VALUE]
    80     return [v for v in value if v != INTERNAL_FIELD_VALUE]
    80 
    81 
    81 
    82 
    82 
    83 
    83 class CubicWebRequestBase(DBAPIRequest):
    84 class CubicWebRequestBase(DBAPIRequest):
    84     """abstract HTTP request, should be extended according to the HTTP backend"""
    85     """abstract HTTP request, should be extended according to the HTTP backend
    85     json_request = False # to be set to True by json controllers
    86     Immutable attributes that describe the received query and generic configuration
    86 
    87     """
    87     def __init__(self, vreg, https, form=None):
    88     ajax_request = False # to be set to True by ajax controllers
       
    89 
       
    90     def __init__(self, vreg, https=False, form=None, headers={}):
       
    91         """
       
    92         :vreg: Vregistry,
       
    93         :https: boolean, s this a https request
       
    94         :form: Forms value
       
    95         """
    88         super(CubicWebRequestBase, self).__init__(vreg)
    96         super(CubicWebRequestBase, self).__init__(vreg)
       
    97         #: (Boolean) Is this an https request.
    89         self.https = https
    98         self.https = https
       
    99         #: User interface property (vary with https) (see uiprops_)
       
   100         self.uiprops = None
       
   101         #: url for serving datadir (vary with https) (see resources_)
       
   102         self.datadir_url = None
    90         if https:
   103         if https:
    91             self.uiprops = vreg.config.https_uiprops
   104             self.uiprops = vreg.config.https_uiprops
    92             self.datadir_url = vreg.config.https_datadir_url
   105             self.datadir_url = vreg.config.https_datadir_url
    93         else:
   106         else:
    94             self.uiprops = vreg.config.uiprops
   107             self.uiprops = vreg.config.uiprops
    95             self.datadir_url = vreg.config.datadir_url
   108             self.datadir_url = vreg.config.datadir_url
    96         # raw html headers that can be added from any view
   109         #: raw html headers that can be added from any view
    97         self.html_headers = HTMLHead(self)
   110         self.html_headers = HTMLHead(self)
    98         # form parameters
   111         #: received headers
       
   112         self._headers_in = Headers()
       
   113         for k, v in headers.iteritems():
       
   114             self._headers_in.addRawHeader(k, v)
       
   115         #: form parameters
    99         self.setup_params(form)
   116         self.setup_params(form)
   100         # dictionary that may be used to store request data that has to be
   117         #: dictionary that may be used to store request data that has to be
   101         # shared among various components used to publish the request (views,
   118         #: shared among various components used to publish the request (views,
   102         # controller, application...)
   119         #: controller, application...)
   103         self.data = {}
   120         self.data = {}
   104         # search state: 'normal' or 'linksearch' (eg searching for an object
   121         #:  search state: 'normal' or 'linksearch' (eg searching for an object
   105         # to create a relation with another)
   122         #:  to create a relation with another)
   106         self.search_state = ('normal',)
   123         self.search_state = ('normal',)
   107         # page id, set by htmlheader template
   124         #: page id, set by htmlheader template
   108         self.pageid = None
   125         self.pageid = None
   109         self._set_pageid()
   126         self._set_pageid()
   110         # prepare output header
   127         # prepare output header
       
   128         #: Header used for the final response
   111         self.headers_out = Headers()
   129         self.headers_out = Headers()
       
   130         #: HTTP status use by the final response
       
   131         self.status_out  = 200
   112 
   132 
   113     def _set_pageid(self):
   133     def _set_pageid(self):
   114         """initialize self.pageid
   134         """initialize self.pageid
   115         if req.form provides a specific pageid, use it, otherwise build a
   135         if req.form provides a specific pageid, use it, otherwise build a
   116         new one.
   136         new one.
   119         if pid is None:
   139         if pid is None:
   120             pid = make_uid(id(self))
   140             pid = make_uid(id(self))
   121             self.html_headers.define_var('pageid', pid, override=False)
   141             self.html_headers.define_var('pageid', pid, override=False)
   122         self.pageid = pid
   142         self.pageid = pid
   123 
   143 
       
   144     def _get_json_request(self):
       
   145         warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
       
   146              DeprecationWarning, stacklevel=2)
       
   147         return self.ajax_request
       
   148     def _set_json_request(self, value):
       
   149         warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
       
   150              DeprecationWarning, stacklevel=2)
       
   151         self.ajax_request = value
       
   152     json_request = property(_get_json_request, _set_json_request)
       
   153 
       
   154     def base_url(self, secure=None):
       
   155         """return the root url of the instance
       
   156 
       
   157         secure = False -> base-url
       
   158         secure = None  -> https-url if req.https
       
   159         secure = True  -> https if it exist
       
   160         """
       
   161         if secure is None:
       
   162             secure = self.https
       
   163         base_url = None
       
   164         if secure:
       
   165             base_url = self.vreg.config.get('https-url')
       
   166         if base_url is None:
       
   167             base_url = super(CubicWebRequestBase, self).base_url()
       
   168         return base_url
       
   169 
   124     @property
   170     @property
   125     def authmode(self):
   171     def authmode(self):
       
   172         """Authentification mode of the instance
       
   173 
       
   174         (see `Configuring the Web server`_)"""
   126         return self.vreg.config['auth-mode']
   175         return self.vreg.config['auth-mode']
       
   176 
       
   177     # Various variable generator.
   127 
   178 
   128     @property
   179     @property
   129     def varmaker(self):
   180     def varmaker(self):
   130         """the rql varmaker is exposed both as a property and as the
   181         """the rql varmaker is exposed both as a property and as the
   131         set_varmaker function since we've two use cases:
   182         set_varmaker function since we've two use cases:
   257         """
   308         """
   258         if form is None:
   309         if form is None:
   259             form = self.form
   310             form = self.form
   260         return list_form_param(form, param, pop)
   311         return list_form_param(form, param, pop)
   261 
   312 
   262 
       
   263     def reset_headers(self):
   313     def reset_headers(self):
   264         """used by AutomaticWebTest to clear html headers between tests on
   314         """used by AutomaticWebTest to clear html headers between tests on
   265         the same resultset
   315         the same resultset
   266         """
   316         """
   267         self.html_headers = HTMLHead(self)
   317         self.html_headers = HTMLHead(self)
   699         if controller in registered_controllers:
   749         if controller in registered_controllers:
   700             return controller
   750             return controller
   701         return 'view'
   751         return 'view'
   702 
   752 
   703     def validate_cache(self):
   753     def validate_cache(self):
   704         """raise a `DirectResponse` exception if a cached page along the way
   754         """raise a `StatusResponse` exception if a cached page along the way
   705         exists and is still usable.
   755         exists and is still usable.
   706 
   756 
   707         calls the client-dependant implementation of `_validate_cache`
   757         calls the client-dependant implementation of `_validate_cache`
   708         """
   758         """
   709         self._validate_cache()
   759         modified = True
   710         if self.http_method() == 'HEAD':
   760         if self.get_header('Cache-Control') not in ('max-age=0', 'no-cache'):
   711             raise StatusResponse(200, '')
   761             # Here, we search for any invalid 'not modified' condition
       
   762             # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
       
   763             validators = get_validators(self._headers_in)
       
   764             if validators: # if we have no
       
   765                 modified = any(func(val, self.headers_out) for func, val in validators)
       
   766         # Forge expected response
       
   767         if modified:
       
   768             if 'Expires' not in self.headers_out:
       
   769                 # Expires header seems to be required by IE7 -- Are you sure ?
       
   770                 self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
       
   771             if self.http_method() == 'HEAD':
       
   772                 raise StatusResponse(200, '')
       
   773             # /!\ no raise, the function returns and we keep processing the request)
       
   774         else:
       
   775             # overwrite headers_out to forge a brand new not-modified response
       
   776             self.headers_out = self._forge_cached_headers()
       
   777             if self.http_method() in ('HEAD', 'GET'):
       
   778                 raise StatusResponse(httplib.NOT_MODIFIED)
       
   779             else:
       
   780                 raise StatusResponse(httplib.PRECONDITION_FAILED)
   712 
   781 
   713     # abstract methods to override according to the web front-end #############
   782     # abstract methods to override according to the web front-end #############
   714 
   783 
   715     def http_method(self):
   784     def http_method(self):
   716         """returns 'POST', 'GET', 'HEAD', etc."""
   785         """returns 'POST', 'GET', 'HEAD', etc."""
   717         raise NotImplementedError()
   786         raise NotImplementedError()
   718 
   787 
   719     def _validate_cache(self):
   788     def _forge_cached_headers(self):
   720         """raise a `DirectResponse` exception if a cached page along the way
   789         # overwrite headers_out to forge a brand new not-modified response
   721         exists and is still usable
   790         headers = Headers()
   722         """
   791         for header in (
   723         raise NotImplementedError()
   792             # Required from sec 10.3.5:
       
   793             'date', 'etag', 'content-location', 'expires',
       
   794             'cache-control', 'vary',
       
   795             # Others:
       
   796             'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
       
   797             value = self._headers_in.getRawHeaders(header)
       
   798             if value is not None:
       
   799                 headers.setRawHeaders(header, value)
       
   800         return headers
   724 
   801 
   725     def relative_path(self, includeparams=True):
   802     def relative_path(self, includeparams=True):
   726         """return the normalized path of the request (ie at least relative
   803         """return the normalized path of the request (ie at least relative
   727         to the instance's root, but some other normalization may be needed
   804         to the instance's root, but some other normalization may be needed
   728         so that the returned path may be used to compare to generated urls
   805         so that the returned path may be used to compare to generated urls
   730         :param includeparams:
   807         :param includeparams:
   731            boolean indicating if GET form parameters should be kept in the path
   808            boolean indicating if GET form parameters should be kept in the path
   732         """
   809         """
   733         raise NotImplementedError()
   810         raise NotImplementedError()
   734 
   811 
   735     def get_header(self, header, default=None):
   812     # http headers ############################################################
   736         """return the value associated with the given input HTTP header,
   813 
   737         raise KeyError if the header is not set
   814     ### incoming headers
   738         """
   815 
   739         raise NotImplementedError()
   816     def get_header(self, header, default=None, raw=True):
   740 
   817         """return the value associated with the given input header, raise
       
   818         KeyError if the header is not set
       
   819         """
       
   820         if raw:
       
   821             return self._headers_in.getRawHeaders(header, [default])[0]
       
   822         return self._headers_in.getHeader(header, default)
       
   823 
       
   824     def header_accept_language(self):
       
   825         """returns an ordered list of preferred languages"""
       
   826         acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
       
   827         for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
       
   828                               reverse=True):
       
   829             lang = lang.split('-')[0]
       
   830             yield lang
       
   831 
       
   832     def header_if_modified_since(self):
       
   833         """If the HTTP header If-modified-since is set, return the equivalent
       
   834         date time value (GMT), else return None
       
   835         """
       
   836         mtime = self.get_header('If-modified-since', raw=False)
       
   837         if mtime:
       
   838             # :/ twisted is returned a localized time stamp
       
   839             return datetime.fromtimestamp(mtime) + GMTOFFSET
       
   840         return None
       
   841 
       
   842     ### outcoming headers
   741     def set_header(self, header, value, raw=True):
   843     def set_header(self, header, value, raw=True):
   742         """set an output HTTP header"""
   844         """set an output HTTP header"""
   743         if raw:
   845         if raw:
   744             # adding encoded header is important, else page content
   846             # adding encoded header is important, else page content
   745             # will be reconverted back to unicode and apart unefficiency, this
   847             # will be reconverted back to unicode and apart unefficiency, this
   783             value_parser = value_sort_key = None
   885             value_parser = value_sort_key = None
   784         accepteds = self.get_header(header, '')
   886         accepteds = self.get_header(header, '')
   785         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
   887         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
   786         return (raw_value for (raw_value, parsed_value, score) in values)
   888         return (raw_value for (raw_value, parsed_value, score) in values)
   787 
   889 
   788     def header_if_modified_since(self):
       
   789         """If the HTTP header If-modified-since is set, return the equivalent
       
   790         mx date time value (GMT), else return None
       
   791         """
       
   792         raise NotImplementedError()
       
   793 
       
   794     def demote_to_html(self):
   890     def demote_to_html(self):
   795         """helper method to dynamically set request content type to text/html
   891         """helper method to dynamically set request content type to text/html
   796 
   892 
   797         The global doctype and xmldec must also be changed otherwise the browser
   893         The global doctype and xmldec must also be changed otherwise the browser
   798         will display '<[' at the beginning of the page
   894         will display '<[' at the beginning of the page
   802                 raise Exception("Can't demote to html from an ajax context. You "
   898                 raise Exception("Can't demote to html from an ajax context. You "
   803                                 "should change force-html-content-type to yes "
   899                                 "should change force-html-content-type to yes "
   804                                 "in the instance configuration file.")
   900                                 "in the instance configuration file.")
   805             self.set_content_type('text/html')
   901             self.set_content_type('text/html')
   806             self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
   902             self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
       
   903 
       
   904     # xml doctype #############################################################
   807 
   905 
   808     def set_doctype(self, doctype, reset_xmldecl=True):
   906     def set_doctype(self, doctype, reset_xmldecl=True):
   809         """helper method to dynamically change page doctype
   907         """helper method to dynamically change page doctype
   810 
   908 
   811         :param doctype: the new doctype, e.g. '<!DOCTYPE html>'
   909         :param doctype: the new doctype, e.g. '<!DOCTYPE html>'