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: |
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>' |