1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from abc import ABCMeta
18 from BaseHTTPServer import BaseHTTPRequestHandler
19 from Cookie import CookieError, BaseCookie, SimpleCookie
20 import cgi
21 from datetime import datetime
22 from hashlib import md5
23 import new
24 import mimetypes
25 import os
26 import re
27 from StringIO import StringIO
28 import sys
29 import urlparse
30
31 from genshi.builder import Fragment
32 from trac.core import Interface, TracBaseError, TracError
33 from trac.util import as_bool, as_int, get_last_traceback, lazy, unquote
34 from trac.util.datefmt import http_date, localtz
35 from trac.util.html import tag
36 from trac.util.text import empty, exception_to_unicode, to_unicode
37 from trac.util.translation import _, N_, tag_
38 from trac.web.href import Href
39 from trac.web.wsgi import _FileWrapper, is_client_disconnect_exception
43 """Extension point interface for components that can provide the name
44 of the remote user."""
45
47 """Return the name of the remote user, or `None` if the identity of the
48 user is unknown."""
49
52 """Decide which `trac.core.Component` handles which `Request`, and how.
53
54 The boolean property `is_valid_default_handler` determines whether the
55 `IRequestFilter` can be used as a `default_handler` and defaults to
56 `True`. To be suitable as a `default_handler`, an `IRequestFilter` must
57 return an HTML document and `data` dictionary for rendering the document,
58 and must not require that `match_request` be called prior to
59 `process_request`.
60
61 The boolean property `jquery_noconflict` determines whether jQuery's
62 `noConflict` mode will be activated by the handler, and defaults to
63 `False`.
64 """
65
67 """Return whether the handler wants to process the given request."""
68
70 """Process the request.
71
72 Return a `(template_name, data, content_type)` tuple,
73 where `data` is a dictionary of substitutions for the Genshi template.
74
75 "text/html" is assumed if `content_type` is `None`.
76
77 Note that if template processing should not occur, this method can
78 simply send the response itself and not return anything.
79
80 :Since 1.0: Clearsilver templates are no longer supported.
81
82 :Since 1.1.2: the rendering `method` (xml, xhtml or text) may be
83 returned as a fourth parameter in the tuple, but if not specified
84 it will be inferred from the `content_type` when rendering the
85 template.
86 """
87
90 """Returns `True` if the `handler` is a valid default handler, as
91 described in the `IRequestHandler` interface documentation.
92 """
93 return handler and getattr(handler, 'is_valid_default_handler', True)
94
97 """Enable components to interfere with the processing done by the
98 main handler, either before and/or after it enters in action.
99 """
100
102 """Called after initial handler selection, and can be used to change
103 the selected handler or redirect request.
104
105 Always returns the request handler, even if unchanged.
106 """
107
108 - def post_process_request(req, template, data, content_type, method=None):
109 """Do any post-processing the request might need; typically adding
110 values to the template `data` dictionary, or changing the Genshi
111 template or mime type.
112
113 `data` may be updated in place.
114
115 Always returns a tuple of (template, data, content_type), even if
116 unchanged.
117
118 Note that `template`, `data`, `content_type` will be `None` if:
119 - called when processing an error page
120 - the default request handler did not return any result
121
122 :Since 0.11: there's a `data` argument for supporting Genshi templates;
123 this introduced a difference in arity which made it possible to
124 distinguish between the IRequestFilter components still targeted
125 at ClearSilver templates and the newer ones targeted at Genshi
126 templates.
127
128 :Since 1.0: Clearsilver templates are no longer supported.
129
130 :Since 1.1.2: the rendering `method` will be passed if it is returned
131 by the request handler, otherwise `method` will be `None`. For
132 backward compatibility, the parameter is optional in the
133 implementation's signature.
134 """
135
138 """Transform the generated content by filtering the Genshi event stream
139 generated by the template, prior to its serialization.
140 """
141
143 """Return a filtered Genshi event stream, or the original unfiltered
144 stream if no match.
145
146 `req` is the current request object, `method` is the Genshi render
147 method (xml, xhtml or text), `filename` is the filename of the template
148 to be rendered, `stream` is the event stream and `data` is the data for
149 the current template.
150
151 See the Genshi_ documentation for more information.
152
153 .. _Genshi: http://genshi.edgewall.org/wiki/Documentation/filters.html
154 """
155
158 """Raised when a `NotImplementedError` is trapped.
159
160 This exception is for internal use and should not be raised by
161 plugins. Plugins should raise `NotImplementedError`.
162
163 :since: 1.0.11
164 """
165
166 title = N_("Not Implemented Error")
167
168
169 HTTP_STATUS = dict([(code, reason.title()) for code, (reason, description)
170 in BaseHTTPRequestHandler.responses.items()])
174
175 __metaclass__ = ABCMeta
176
189
190 @property
202
203 @property
218
219 @classmethod
229
230 _HTTPException_subclass_names = []
231 for code in [code for code in HTTP_STATUS if code >= 400]:
232 exc_name = HTTP_STATUS[code].replace(' ', '').replace('-', '')
233
234 if exc_name == 'InternalServerError':
235 exc_name = 'InternalError'
236 if exc_name.lower().startswith('http'):
237 exc_name = exc_name[4:]
238 exc_name = 'HTTP' + exc_name
239 setattr(sys.modules[__name__], exc_name,
240 HTTPException.subclass(exc_name, code))
241 _HTTPException_subclass_names.append(exc_name)
242 del code, exc_name
246 """Our own version of cgi.FieldStorage, with tweaks."""
247
249 try:
250 cgi.FieldStorage.read_multi(self, *args, **kwargs)
251 except ValueError:
252
253
254 self.read_single()
255
258 """Dictionary subclass that provides convenient access to request
259 parameters that may contain multiple values."""
260
261 - def as_int(self, name, default=None, min=None, max=None):
262 """Return the value as an integer. Return `default` if
263 if an exception is raised while converting the value to an
264 integer.
265
266 :param name: the name of the request parameter
267 :keyword default: the value to return if the parameter is not
268 specified or an exception occurs converting
269 the value to an integer.
270 :keyword min: lower bound to which the value is limited
271 :keyword max: upper bound to which the value is limited
272
273 :since: 1.2
274 """
275 if name not in self:
276 return default
277 return as_int(self.getfirst(name), default, min, max)
278
279 - def as_bool(self, name, default=None):
280 """Return the value as a boolean. Return `default` if
281 if an exception is raised while converting the value to a
282 boolean.
283
284 :param name: the name of the request parameter
285 :keyword default: the value to return if the parameter is not
286 specified or an exception occurs converting
287 the value to a boolean.
288
289 :since: 1.2
290 """
291 if name not in self:
292 return default
293 return as_bool(self.getfirst(name), default)
294
295 - def getbool(self, name, default=None):
296 """Return the value as a boolean. Raise an `HTTPBadRequest`
297 exception if an exception occurs while converting the value to
298 a boolean.
299
300 :param name: the name of the request parameter
301 :keyword default: the value to return if the parameter is not
302 specified.
303
304 :since: 1.2
305 """
306 if name not in self:
307 return default
308 value = self[name]
309 if isinstance(value, list):
310 raise HTTPBadRequest(tag_("Invalid value for request argument "
311 "%(name)s.", name=tag.em(name)))
312 value = as_bool(value, None)
313 if value is None:
314 raise HTTPBadRequest(tag_("Invalid value for request argument "
315 "%(name)s.", name=tag.em(name)))
316 return value
317
318 - def getint(self, name, default=None, min=None, max=None):
319 """Return the value as an integer. Raise an `HTTPBadRequest`
320 exception if an exception occurs while converting the value
321 to an integer.
322
323 :param name: the name of the request parameter
324 :keyword default: the value to return if the parameter is not
325 specified
326 :keyword min: lower bound to which the value is limited
327 :keyword max: upper bound to which the value is limited
328
329 :since: 1.2
330 """
331 if name not in self:
332 return default
333 value = as_int(self[name], None, min, max)
334 if value is None:
335 raise HTTPBadRequest(tag_("Invalid value for request argument "
336 "%(name)s.", name=tag.em(name)))
337 return value
338
339 - def getfirst(self, name, default=None):
340 """Return the first value for the specified parameter, or `default` if
341 the parameter was not provided.
342 """
343 if name not in self:
344 return default
345 val = self[name]
346 if isinstance(val, list):
347 val = val[0]
348 return val
349
351 """Return a list of values for the specified parameter, even if only
352 one value was provided.
353 """
354 if name not in self:
355 return []
356 val = self[name]
357 if not isinstance(val, list):
358 val = [val]
359 return val
360
362 """Raise an `HTTPBadRequest` exception if the parameter is
363 not in the request.
364
365 :param name: the name of the request parameter
366
367 :since: 1.2
368 """
369 if name not in self:
370 raise HTTPBadRequest(
371 tag_("Missing request argument. The %(name)s argument "
372 "must be included in the request.", name=tag.em(name)))
373
376 """Parse a query string into a list of `(name, value)` tuples.
377
378 :Since 1.1.2: a leading `?` is stripped from `query_string`."""
379 args = []
380 if not query_string:
381 return args
382 query_string = query_string.lstrip('?')
383 for arg in query_string.split('&'):
384 nv = arg.split('=', 1)
385 if len(nv) == 2:
386 (name, value) = nv
387 else:
388 (name, value) = (nv[0], empty)
389 name = unquote(name.replace('+', ' '))
390 if isinstance(name, str):
391 name = unicode(name, 'utf-8')
392 value = unquote(value.replace('+', ' '))
393 if isinstance(value, str):
394 value = unicode(value, 'utf-8')
395 args.append((name, value))
396 return args
397
411
414 """Marker exception that indicates whether request processing has completed
415 and a response was sent.
416 """
417 iterable = None
418
421
422
423 -class Cookie(SimpleCookie):
424 - def load(self, rawdata, ignore_parse_errors=False):
425 if ignore_parse_errors:
426 self.bad_cookies = []
427 self._BaseCookie__set = self._loose_set
428 SimpleCookie.load(self, rawdata)
429 if ignore_parse_errors:
430 self._BaseCookie__set = self._strict_set
431 for key in self.bad_cookies:
432 del self[key]
433
434 _strict_set = BaseCookie._BaseCookie__set
435
436 - def _loose_set(self, key, real_value, coded_value):
437
438
439 if key in self:
440 return
441 try:
442 self._strict_set(key, real_value, coded_value)
443 except CookieError:
444 self.bad_cookies.append(key)
445 dict.__setitem__(self, key, None)
446
449 """Represents a HTTP request/response pair.
450
451 This class provides a convenience API over WSGI.
452 """
453
454 _disallowed_control_codes_re = re.compile(r'[\x00-\x08\x0a-\x1f\x7f]')
455 _reserved_headers = set(['content-type', 'content-length', 'location',
456 'etag', 'pragma', 'cache-control', 'expires'])
457
458 _valid_header_re = re.compile(r"[-0-9A-Za-z!#$%&'*+.^_`|~]+\Z")
459
460 - def __init__(self, environ, start_response):
461 """Create the request wrapper.
462
463 :param environ: The WSGI environment dict
464 :param start_response: The WSGI callback for starting the response
465 :param callbacks: A dictionary of functions that are used to lazily
466 evaluate attribute lookups
467 """
468 self.environ = environ
469 self._start_response = start_response
470 self._write = None
471 self._status = '200 OK'
472 self._response = None
473 self._content_type = None
474
475 self._outheaders = []
476 self._outcharset = None
477 self.outcookie = Cookie()
478
479 self.callbacks = {
480 'arg_list': Request._parse_arg_list,
481 'args': lambda req: arg_list_to_args(req.arg_list),
482 'languages': Request._parse_languages,
483 'incookie': Request._parse_cookies,
484 '_inheaders': Request._parse_headers
485 }
486 self.redirect_listeners = []
487
488 self.base_url = self.environ.get('trac.base_url')
489 if not self.base_url:
490 self.base_url = self._reconstruct_url()
491 self.href = Href(self.base_path)
492 self.abs_href = Href(self.base_url)
493
495 """Performs lazy attribute lookup by delegating to the functions in the
496 callbacks dictionary."""
497 if name in self.callbacks:
498 value = self.callbacks[name](self)
499 setattr(self, name, value)
500 return value
501 raise AttributeError(name)
502
504 uri = self.environ.get('PATH_INFO', '')
505 qs = self.query_string
506 if qs:
507 uri += '?' + qs
508 return '<%s "%s %r">' % (self.__class__.__name__, self.method, uri)
509
510
511
512 @lazy
514 """Returns `True` if the request is an `XMLHttpRequest`.
515
516 :since: 1.1.6
517 """
518 return self.get_header('X-Requested-With') == 'XMLHttpRequest'
519
520 @property
522 """The HTTP method of the request"""
523 return self.environ['REQUEST_METHOD']
524
525 @property
527 """Path inside the application"""
528 path_info = self.environ.get('PATH_INFO', '')
529 try:
530 return unicode(path_info, 'utf-8')
531 except UnicodeDecodeError:
532 raise HTTPNotFound(_("Invalid URL encoding (was %(path_info)r)",
533 path_info=path_info))
534
535 @property
537 """Query part of the request"""
538 return self.environ.get('QUERY_STRING', '')
539
540 @property
542 """IP address of the remote user"""
543 return self.environ.get('REMOTE_ADDR')
544
545 @property
547 """ Name of the remote user.
548
549 Will be `None` if the user has not logged in using HTTP authentication.
550 """
551 user = self.environ.get('REMOTE_USER')
552 if user is not None:
553 return to_unicode(user)
554
555 @property
557 return self._write is not None
558
559 @property
561 """The scheme of the request URL"""
562 return self.environ['wsgi.url_scheme']
563
564 @property
566 """The root path of the application"""
567 return self.environ.get('SCRIPT_NAME', '')
568
569 @property
571 """Name of the server"""
572 return self.environ['SERVER_NAME']
573
574 @property
576 """Port number the server is bound to"""
577 return int(self.environ['SERVER_PORT'])
578
580 """Add a callable to be called prior to executing a redirect.
581
582 The callable is passed the arguments to the `redirect()` call.
583 """
584 self.redirect_listeners.append(listener)
585
587 """Return the value of the specified HTTP header, or `None` if there's
588 no such header in the request.
589 """
590 name = name.lower()
591 for key, value in self._inheaders:
592 if key == name:
593 return value
594 return None
595
597 """Set the status code of the response."""
598 self._status = '%s %s' % (code, HTTP_STATUS.get(code, 'Unknown'))
599
601 """Send the response header with the specified name and value.
602
603 `value` must either be an `unicode` string or can be converted to one
604 (e.g. numbers, ...)
605 """
606 lower_name = name.lower()
607 if lower_name == 'content-type':
608 self._content_type = value.split(';', 1)[0]
609 ctpos = value.find('charset=')
610 if ctpos >= 0:
611 self._outcharset = value[ctpos + 8:].strip()
612 elif lower_name == 'content-length':
613 self._content_length = int(value)
614 self._outheaders.append((name, unicode(value).encode('utf-8')))
615
617 """Must be called after all headers have been sent and before the
618 actual content is written.
619 """
620 if self.method == 'POST' and self._content_type == 'text/html':
621
622 self.send_header('X-XSS-Protection', 0)
623 self._send_configurable_headers()
624 self._send_cookie_headers()
625 self._write = self._start_response(self._status, self._outheaders)
626
628 """Check the request "If-None-Match" header against an entity tag.
629
630 The entity tag is generated from the specified last modified time
631 (`datetime`), optionally appending an `extra` string to
632 indicate variants of the requested resource.
633
634 That `extra` parameter can also be a list, in which case the MD5 sum
635 of the list content will be used.
636
637 If the generated tag matches the "If-None-Match" header of the request,
638 this method sends a "304 Not Modified" response to the client.
639 Otherwise, it adds the entity tag as an "ETag" header to the response
640 so that consecutive requests can be cached.
641 """
642 if isinstance(extra, list):
643 m = md5()
644 for elt in extra:
645 m.update(repr(elt))
646 extra = m.hexdigest()
647 etag = 'W/"%s/%s/%s"' % (self.authname, http_date(datetime), extra)
648 inm = self.get_header('If-None-Match')
649 if not inm or inm != etag:
650 self.send_header('ETag', etag)
651 else:
652 self.send_response(304)
653 self.send_header('Content-Length', 0)
654 self.end_headers()
655 raise RequestDone
656
657 _trident_re = re.compile(r' Trident/([0-9]+)')
658
659 - def redirect(self, url, permanent=False):
660 """Send a redirect to the client, forwarding to the specified URL.
661
662 The `url` may be relative or absolute, relative URLs will be translated
663 appropriately.
664 """
665 for listener in self.redirect_listeners:
666 listener(self, url, permanent)
667
668 if permanent:
669 status = 301
670 elif self.method == 'POST':
671 status = 303
672 else:
673 status = 302
674
675 self.send_response(status)
676 if not url.startswith(('http://', 'https://')):
677
678 scheme, host = urlparse.urlparse(self.base_url)[:2]
679 url = urlparse.urlunparse((scheme, host, url, None, None, None))
680
681
682 if status == 303 and '#' in url:
683 user_agent = self.environ.get('HTTP_USER_AGENT', '')
684 match_trident = self._trident_re.search(user_agent)
685 if ' MSIE ' in user_agent and \
686 (not match_trident or int(match_trident.group(1)) < 6):
687 url = url.replace('#', '#__msie303:')
688
689 self.send_header('Location', url)
690 self.send_header('Content-Type', 'text/plain')
691 self.send_header('Content-Length', 0)
692 self.send_header('Pragma', 'no-cache')
693 self.send_header('Cache-Control', 'no-cache')
694 self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
695 self.end_headers()
696 raise RequestDone
697
698 - def send(self, content, content_type='text/html', status=200):
710
711 - def send_error(self, exc_info, template='error.html',
712 content_type='text/html', status=500, env=None, data={}):
713 try:
714 if template.endswith('.html'):
715 if env:
716 from trac.web.chrome import Chrome, add_stylesheet
717 add_stylesheet(self, 'common/css/code.css')
718 try:
719 data = Chrome(env).render_template(self, template,
720 data, 'text/html')
721 except Exception:
722
723 data['trac_error_rendering'] = True
724 data = Chrome(env).render_template(self, template,
725 data, 'text/html')
726 else:
727 content_type = 'text/plain'
728 data = '%s\n\n%s: %s' % (data.get('title'),
729 data.get('type'),
730 data.get('message'))
731 except Exception:
732 data = get_last_traceback()
733 content_type = 'text/plain'
734
735 if isinstance(data, unicode):
736 data = data.encode('utf-8')
737
738 self.send_response(status)
739 self._outheaders = []
740 self.send_header('Cache-Control', 'must-revalidate')
741 self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
742 self.send_header('Content-Type', content_type + ';charset=utf-8')
743 self.send_header('Content-Length', len(data))
744 self._send_configurable_headers()
745 self._send_cookie_headers()
746
747 self._write = self._start_response(self._status, self._outheaders,
748 exc_info)
749
750 if self.method != 'HEAD':
751 self.write(data)
752 raise RequestDone
753
754 - def send_no_content(self):
755 self.send_response(204)
756 self.send_header('Content-Length', 0)
757 self.send_header('Content-Type', 'text/plain')
758 self.end_headers()
759 raise RequestDone
760
762 """Send a local file to the browser.
763
764 This method includes the "Last-Modified", "Content-Type" and
765 "Content-Length" headers in the response, corresponding to the file
766 attributes. It also checks the last modification time of the local file
767 against the "If-Modified-Since" provided by the user agent, and sends a
768 "304 Not Modified" response if it matches.
769 """
770 if not os.path.isfile(path):
771 raise HTTPNotFound(_("File %(path)s not found", path=path))
772
773 stat = os.stat(path)
774 mtime = datetime.fromtimestamp(stat.st_mtime, localtz)
775 last_modified = http_date(mtime)
776 if last_modified == self.get_header('If-Modified-Since'):
777 self.send_response(304)
778 self.send_header('Content-Length', 0)
779 self.end_headers()
780 raise RequestDone
781
782 if not mimetype:
783 mimetype = mimetypes.guess_type(path)[0] or \
784 'application/octet-stream'
785
786 self.send_response(200)
787 self.send_header('Content-Type', mimetype)
788 self.send_header('Content-Length', stat.st_size)
789 self.send_header('Last-Modified', last_modified)
790 use_xsendfile = getattr(self, 'use_xsendfile', False)
791 if use_xsendfile:
792 xsendfile_header = getattr(self, 'xsendfile_header', None)
793 if xsendfile_header:
794 self.send_header(xsendfile_header, os.path.abspath(path))
795 else:
796 use_xsendfile = False
797 self.end_headers()
798
799 if not use_xsendfile and self.method != 'HEAD':
800 fileobj = open(path, 'rb')
801 file_wrapper = self.environ.get('wsgi.file_wrapper', _FileWrapper)
802 self._response = file_wrapper(fileobj, 4096)
803 raise RequestDone
804
805 - def read(self, size=None):
806 """Read the specified number of bytes from the request body."""
807 fileobj = self.environ['wsgi.input']
808 if size is None:
809 size = self.get_header('Content-Length')
810 if size is None:
811 size = -1
812 else:
813 size = int(size)
814 data = fileobj.read(size)
815 return data
816
817 CHUNK_SIZE = 4096
818
820 """Write the given data to the response body.
821
822 *data* **must** be a `str` string or an iterable instance
823 which iterates `str` strings, encoded with the charset which
824 has been specified in the ``'Content-Type'`` header or UTF-8
825 otherwise.
826
827 Note that when the ``'Content-Length'`` header is specified,
828 its value either corresponds to the length of *data*, or, if
829 there are multiple calls to `write`, to the cumulative length
830 of the *data* arguments.
831 """
832 if not self._write:
833 self.end_headers()
834 try:
835 chunk_size = self.CHUNK_SIZE
836 bufsize = 0
837 buf = []
838 buf_append = buf.append
839 if isinstance(data, basestring):
840 data = [data]
841 for chunk in data:
842 if isinstance(chunk, unicode):
843 raise ValueError("Can't send unicode content")
844 if not chunk:
845 continue
846 bufsize += len(chunk)
847 buf_append(chunk)
848 if bufsize >= chunk_size:
849 self._write(''.join(buf))
850 bufsize = 0
851 buf[:] = ()
852 if bufsize > 0:
853 self._write(''.join(buf))
854 except IOError as e:
855 if is_client_disconnect_exception(e):
856 raise RequestDone
857 raise
858
859 @classmethod
869
870
871
873 """Parse the supplied request parameters into a list of
874 `(name, value)` tuples.
875 """
876 fp = self.environ['wsgi.input']
877
878
879
880 ctype = self.get_header('Content-Type')
881 if ctype:
882 ctype, options = cgi.parse_header(ctype)
883 if ctype not in ('application/x-www-form-urlencoded',
884 'multipart/form-data'):
885 fp = StringIO('')
886
887
888
889
890 if self.method == 'POST':
891 qs_on_post = self.environ.pop('QUERY_STRING', '')
892 try:
893 fs = _FieldStorage(fp, environ=self.environ,
894 keep_blank_values=True)
895 except IOError as e:
896 if is_client_disconnect_exception(e):
897 raise HTTPBadRequest(
898 _("Exception caught while reading request: %(msg)s",
899 msg=exception_to_unicode(e)))
900 raise
901 if self.method == 'POST':
902 self.environ['QUERY_STRING'] = qs_on_post
903
904 def raise_if_null_bytes(value):
905 if value and '\x00' in value:
906 raise HTTPBadRequest(_("Invalid request arguments."))
907
908 args = []
909 for value in fs.list or ():
910 name = value.name
911 raise_if_null_bytes(name)
912 try:
913 if name is not None:
914 name = unicode(name, 'utf-8')
915 if value.filename:
916 raise_if_null_bytes(value.filename)
917 else:
918 value = value.value
919 raise_if_null_bytes(value)
920 value = unicode(value, 'utf-8')
921 except UnicodeDecodeError as e:
922 raise HTTPBadRequest(
923 _("Invalid encoding in form data: %(msg)s",
924 msg=exception_to_unicode(e)))
925 args.append((name, value))
926 return args
927
929 cookies = Cookie()
930 header = self.get_header('Cookie')
931 if header:
932 cookies.load(header, ignore_parse_errors=True)
933 return cookies
934
936 headers = [(name[5:].replace('_', '-').lower(), value)
937 for name, value in self.environ.items()
938 if name.startswith('HTTP_')]
939 if 'CONTENT_LENGTH' in self.environ:
940 headers.append(('content-length', self.environ['CONTENT_LENGTH']))
941 if 'CONTENT_TYPE' in self.environ:
942 headers.append(('content-type', self.environ['CONTENT_TYPE']))
943 return headers
944
946 """The list of languages preferred by the remote user, taken from the
947 ``Accept-Language`` header.
948 """
949 header = self.get_header('Accept-Language') or 'en-us'
950 langs = []
951 for i, lang in enumerate(header.split(',')):
952 code, params = cgi.parse_header(lang)
953 q = 1
954 if 'q' in params:
955 try:
956 q = float(params['q'])
957 except ValueError:
958 q = 0
959 langs.append((-q, i, code))
960 langs.sort()
961 return [code for q, i, code in langs]
962
964 """Reconstruct the absolute base URL of the application."""
965 host = self.get_header('Host')
966 if not host:
967
968
969 default_port = {'http': 80, 'https': 443}
970 if self.server_port and self.server_port != \
971 default_port[self.scheme]:
972 host = '%s:%d' % (self.server_name, self.server_port)
973 else:
974 host = self.server_name
975 return urlparse.urlunparse((self.scheme, host, self.base_path, None,
976 None, None))
977
979 sent_headers = [name.lower() for name, val in self._outheaders]
980 for name, val in getattr(self, 'configurable_headers', []):
981 if name.lower() not in sent_headers:
982 self.send_header(name, val)
983
985 for name in self.outcookie.keys():
986 path = self.outcookie[name].get('path')
987 if path:
988 path = path.replace(' ', '%20') \
989 .replace(';', '%3B') \
990 .replace(',', '%3C')
991 self.outcookie[name]['path'] = path
992
993 cookies = to_unicode(self.outcookie.output(header='')).encode('utf-8')
994 for cookie in cookies.splitlines():
995 self._outheaders.append(('Set-Cookie', cookie.strip()))
996
997
998 __no_apidoc__ = _HTTPException_subclass_names
999