Package trac :: Package web :: Module api

Source Code for Module trac.web.api

   1  # -*- coding: utf-8 -*- 
   2  # 
   3  # Copyright (C) 2005-2020 Edgewall Software 
   4  # Copyright (C) 2005-2006 Christopher Lenz <[email protected]> 
   5  # All rights reserved. 
   6  # 
   7  # This software is licensed as described in the file COPYING, which 
   8  # you should have received as part of this distribution. The terms 
   9  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
  10  # 
  11  # This software consists of voluntary contributions made by many 
  12  # individuals. For the exact contribution history, see the revision 
  13  # history and logs, available at https://trac.edgewall.org/log/. 
  14  # 
  15  # Author: Christopher Lenz <[email protected]> 
  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 
40 41 42 -class IAuthenticator(Interface):
43 """Extension point interface for components that can provide the name 44 of the remote user.""" 45
46 - def authenticate(req):
47 """Return the name of the remote user, or `None` if the identity of the 48 user is unknown."""
49
50 51 -class IRequestHandler(Interface):
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
66 - def match_request(req):
67 """Return whether the handler wants to process the given request."""
68
69 - def process_request(req):
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
88 89 -def is_valid_default_handler(handler):
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
95 96 -class IRequestFilter(Interface):
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
101 - def pre_process_request(req, handler):
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
136 137 -class ITemplateStreamFilter(Interface):
138 """Transform the generated content by filtering the Genshi event stream 139 generated by the template, prior to its serialization. 140 """ 141
142 - def filter_stream(req, method, filename, stream, data):
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
156 157 -class TracNotImplementedError(TracError, NotImplementedError):
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()])
171 172 173 -class HTTPException(TracBaseError):
174 175 __metaclass__ = ABCMeta 176
177 - def __init__(self, detail, *args):
178 """Factory for HTTPException classes.""" 179 if isinstance(detail, TracBaseError): 180 self.detail = detail.message 181 self.reason = detail.title 182 else: 183 self.detail = detail 184 if args: 185 self.detail = self.detail % args 186 super(HTTPException, self).__init__('%s %s (%s)' % (self.code, 187 self.reason, 188 self.detail))
189 190 @property
191 - def message(self):
192 # The message is based on the e.detail, which can be an Exception 193 # object, but not a TracError one: when creating HTTPException, 194 # a TracError.message is directly assigned to e.detail 195 if isinstance(self.detail, Exception): # not a TracBaseError 196 message = exception_to_unicode(self.detail) 197 elif isinstance(self.detail, Fragment): # TracBaseError markup 198 message = self.detail 199 else: 200 message = to_unicode(self.detail) 201 return message
202 203 @property
204 - def title(self):
205 try: 206 # We first try to get localized error messages here, but we 207 # should ignore secondary errors if the main error was also 208 # due to i18n issues 209 title = _("Error") 210 if self.reason: 211 if title.lower() in self.reason.lower(): 212 title = self.reason 213 else: 214 title = _("Error: %(message)s", message=self.reason) 215 except Exception: 216 title = "Error" 217 return title
218 219 @classmethod
220 - def subclass(cls, name, code):
221 """Create a new Exception class representing a HTTP status code.""" 222 reason = HTTP_STATUS.get(code, 'Unknown') 223 new_class = new.classobj(name, (HTTPException,), { 224 '__doc__': 'Exception for HTTP %d %s' % (code, reason) 225 }) 226 new_class.code = code 227 new_class.reason = reason 228 return new_class
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 # 2.5 compatibility hack: 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
243 244 245 -class _FieldStorage(cgi.FieldStorage):
246 """Our own version of cgi.FieldStorage, with tweaks.""" 247
248 - def read_multi(self, *args, **kwargs):
249 try: 250 cgi.FieldStorage.read_multi(self, *args, **kwargs) 251 except ValueError: 252 # Most likely "Invalid boundary in multipart form", 253 # possibly an upload of a .mht file? See #9880. 254 self.read_single()
255
256 257 -class _RequestArgs(dict):
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
350 - def getlist(self, name):
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
361 - def require(self, name):
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
374 375 -def parse_arg_list(query_string):
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
398 399 -def arg_list_to_args(arg_list):
400 """Convert a list of `(name, value)` tuples into into a `_RequestArgs`.""" 401 args = _RequestArgs() 402 for name, value in arg_list: 403 if name in args: 404 if isinstance(args[name], list): 405 args[name].append(value) 406 else: 407 args[name] = [args[name], value] 408 else: 409 args[name] = value 410 return args
411
412 413 -class RequestDone(TracBaseError):
414 """Marker exception that indicates whether request processing has completed 415 and a response was sent. 416 """ 417 iterable = None 418
419 - def __init__(self, iterable=None):
420 self.iterable = iterable
421 446
447 448 -class Request(object):
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 # RFC7230 3.2 Header Fields 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
494 - def __getattr__(self, name):
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
503 - def __repr__(self):
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 # Public API 511 512 @lazy
513 - def is_xhr(self):
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
521 - def method(self):
522 """The HTTP method of the request""" 523 return self.environ['REQUEST_METHOD']
524 525 @property
526 - def path_info(self):
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
536 - def query_string(self):
537 """Query part of the request""" 538 return self.environ.get('QUERY_STRING', '')
539 540 @property
541 - def remote_addr(self):
542 """IP address of the remote user""" 543 return self.environ.get('REMOTE_ADDR')
544 545 @property
546 - def remote_user(self):
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
556 - def response_started(self):
557 return self._write is not None
558 559 @property
560 - def scheme(self):
561 """The scheme of the request URL""" 562 return self.environ['wsgi.url_scheme']
563 564 @property
565 - def base_path(self):
566 """The root path of the application""" 567 return self.environ.get('SCRIPT_NAME', '')
568 569 @property
570 - def server_name(self):
571 """Name of the server""" 572 return self.environ['SERVER_NAME']
573 574 @property
575 - def server_port(self):
576 """Port number the server is bound to""" 577 return int(self.environ['SERVER_PORT'])
578
579 - def add_redirect_listener(self, listener):
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
586 - def get_header(self, name):
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
596 - def send_response(self, code=200):
597 """Set the status code of the response.""" 598 self._status = '%s %s' % (code, HTTP_STATUS.get(code, 'Unknown'))
599
600 - def send_header(self, name, value):
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
616 - def end_headers(self):
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 # Disable XSS protection (#12926) 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
627 - def check_modified(self, datetime, extra=''):
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 # 'Moved Permanently' 670 elif self.method == 'POST': 671 status = 303 # 'See Other' -- safe to use in response to a POST 672 else: 673 status = 302 # 'Found' -- normal temporary redirect 674 675 self.send_response(status) 676 if not url.startswith(('http://', 'https://')): 677 # Make sure the URL is absolute 678 scheme, host = urlparse.urlparse(self.base_url)[:2] 679 url = urlparse.urlunparse((scheme, host, url, None, None, None)) 680 681 # Workaround #10382, IE6-IE9 bug when post and redirect with hash 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):
699 self.send_response(status) 700 self.send_header('Cache-Control', 'must-revalidate') 701 self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT') 702 self.send_header('Content-Type', content_type + ';charset=utf-8') 703 if isinstance(content, basestring): 704 self.send_header('Content-Length', len(content)) 705 self.end_headers() 706 707 if self.method != 'HEAD': 708 self.write(content) 709 raise RequestDone
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 # second chance rendering, in "safe" mode 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: # failed to render 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
761 - def send_file(self, path, mimetype=None):
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
819 - def write(self, data):
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
860 - def is_valid_header(cls, name, value=None):
861 """Check whether the field name, and optionally the value, make 862 a valid HTTP header. 863 """ 864 valid_name = name and name.lower() not in cls._reserved_headers and \ 865 bool(cls._valid_header_re.match(name)) 866 valid_value = not cls._disallowed_control_codes_re.search(value) \ 867 if value else True 868 return valid_name & valid_value
869 870 # Internal methods 871
872 - def _parse_arg_list(self):
873 """Parse the supplied request parameters into a list of 874 `(name, value)` tuples. 875 """ 876 fp = self.environ['wsgi.input'] 877 878 # Avoid letting cgi.FieldStorage consume the input stream when the 879 # request does not contain form data 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 # Python 2.6 introduced a backwards incompatible change for 888 # FieldStorage where QUERY_STRING is no longer ignored for POST 889 # requests. We'll keep the pre 2.6 behaviour for now... 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
928 - def _parse_cookies(self):
929 cookies = Cookie() 930 header = self.get_header('Cookie') 931 if header: 932 cookies.load(header, ignore_parse_errors=True) 933 return cookies
934
935 - def _parse_headers(self):
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
945 - def _parse_languages(self):
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
963 - def _reconstruct_url(self):
964 """Reconstruct the absolute base URL of the application.""" 965 host = self.get_header('Host') 966 if not host: 967 # Missing host header, so reconstruct the host from the 968 # server name and port 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
978 - def _send_configurable_headers(self):
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 996 997 998 __no_apidoc__ = _HTTPException_subclass_names 999