Edgewall Software

root/branches/0.9-stable/trac/web/api.py

Revision 3426, 9.3 kB (checked in by cboos, 2 years ago)

Applied patch from Peter Kropf which adds a missing import for TracError. Closes #3209.

  • Property svn:eol-style set to native
Line 
1# -*- coding: iso-8859-1 -*-
2#
3# Copyright (C) 2005 Edgewall Software
4# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
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 http://trac.edgewall.com/license.html.
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 http://projects.edgewall.com/trac/.
14#
15# Author: Christopher Lenz <cmlenz@gmx.de>
16
17from Cookie import SimpleCookie as Cookie
18import mimetypes
19import os
20import urlparse
21
22from trac.core import Interface
23from trac.util import http_date, TracError
24
25
26class RequestDone(Exception):
27    """Marker exception that indicates whether request processing has completed
28    and a response was sent.
29    """
30
31
32class Request(object):
33    """This class is used to abstract the interface between different frontends.
34
35    Trac modules must use this interface. It is not allowed to have
36    frontend (cgi, tracd, mod_python) specific code in the modules.
37    """
38
39    method = None
40    scheme = None
41    server_name = None
42    server_port = None
43    remote_addr = None
44    remote_user = None
45
46    args = None
47    hdf = None
48    authname = None
49    perm = None
50    session = None
51    _headers = None # additional headers to send
52
53    def __init__(self):
54        self.incookie = Cookie()
55        self.outcookie = Cookie()
56        self._headers = []
57
58    def get_header(self, name):
59        """Return the value of the specified HTTP header, or `None` if there's
60        no such header in the request.
61        """
62        raise NotImplementedError
63
64    def send_response(self, code):
65        """Set the status code of the response."""
66        raise NotImplementedError
67
68    def send_header(self, name, value):
69        """Send the response header with the specified name and value."""
70        raise NotImplementedError
71
72    def end_headers(self):
73        """Must be called after all headers have been sent and before the actual
74        content is written.
75        """
76        raise NotImplementedError
77
78    def _send_cookie_headers(self):
79        # Cookie values can not contain " ,;" characters, so escape them
80        for name in self.outcookie.keys():
81            path = self.outcookie[name].get('path')
82            if path:
83                path = path.replace(' ', '%20') \
84                           .replace(';', '%3B') \
85                           .replace(',', '%3C')
86            self.outcookie[name]['path'] = path
87
88        cookies = self.outcookie.output(header='')
89        for cookie in cookies.splitlines():
90            self.send_header('Set-Cookie', cookie.strip())
91
92    def check_modified(self, timesecs, extra=''):
93        """Check the request "If-None-Match" header against an entity tag
94        generated from the specified last modified time in seconds (`timesecs`),
95        optionally appending an `extra` string to indicate variants of the
96        requested resource.
97
98        If the generated tag matches the "If-None-Match" header of the request,
99        this method sends a "304 Not Modified" response to the client.
100        Otherwise, it adds the entity tag as as "ETag" header to the response so
101        that consequetive requests can be cached.
102        """
103        etag = 'W"%s/%d/%s"' % (self.authname, timesecs, extra)
104        inm = self.get_header('If-None-Match')
105        if (not inm or inm != etag):
106            self._headers.append(('ETag', etag))
107        else:
108            self.send_response(304)
109            self.end_headers()
110            raise RequestDone()
111
112    def redirect(self, url):
113        """Send a redirect to the client, forwarding to the specified URL. The
114        `url` may be relative or absolute, relative URLs will be translated
115        appropriately.
116        """
117        if self.session:
118            self.session.save() # has to be done before the redirect is sent
119        self.send_response(302)
120        if not url.startswith('http://') and not url.startswith('https://'):
121            # Make sure the URL is absolute
122            url = absolute_url(self, url)
123        self.send_header('Location', url)
124        self.send_header('Content-Type', 'text/plain')
125        self.send_header('Pragma', 'no-cache')
126        self.send_header('Cache-control', 'no-cache')
127        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
128        self._send_cookie_headers()
129        self.end_headers()
130
131        if self.method != 'HEAD':
132            self.write('Redirecting...')
133
134        raise RequestDone
135
136    def display(self, template, content_type='text/html', response=200):
137        """Render the response using the ClearSilver template given by the
138        `template` parameter, which can be either the name of the template file,
139        or an already parsed `neo_cs.CS` object.
140        """
141        assert self.hdf, 'HDF dataset not available'
142        if self.args.has_key('hdfdump'):
143            # FIXME: the administrator should probably be able to disable HDF
144            #        dumps
145            content_type = 'text/plain'
146            data = str(self.hdf)
147        else:
148            data = self.hdf.render(template)
149
150        self.send_response(response)
151        self.send_header('Cache-control', 'must-revalidate')
152        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
153        self.send_header('Content-Type', content_type + ';charset=utf-8')
154        self.send_header('Content-Length', len(data))
155        for name, value in self._headers:
156            self.send_header(name, value)
157        self._send_cookie_headers()
158        self.end_headers()
159
160        if self.method != 'HEAD':
161            self.write(data)
162
163        raise RequestDone
164
165    def send_file(self, path, mimetype=None):
166        """Send a local file to the browser.
167       
168        This method includes the "Last-Modified", "Content-Type" and
169        "Content-Length" headers in the response, corresponding to the file
170        attributes. It also checks the last modification time of the local file
171        against the "If-Modified-Since" provided by the user agent, and sends a
172        "304 Not Modified" response if it matches.
173        """
174        if not os.path.isfile(path):
175            raise TracError, "File %s not found" % path
176
177        stat = os.stat(path)
178        last_modified = http_date(stat.st_mtime)
179        if last_modified == self.get_header('If-Modified-Since'):
180            self.send_response(304)
181            self.end_headers()
182            raise RequestDone
183
184        self.send_response(200)
185        if not mimetype:
186            mimetype = mimetypes.guess_type(path)[0]
187        self.send_header('Content-Type', mimetype)
188        self.send_header('Content-Length', stat.st_size)
189        self.send_header('Last-Modified', last_modified)
190        for name, value in self._headers:
191            self.send_header(name, value)
192        self._send_cookie_headers()
193        self.end_headers()
194
195        if self.method != 'HEAD':
196            try:
197                fd = open(path, 'rb')
198                while True:
199                    data = fd.read(4096)
200                    if not data:
201                        break
202                    self.write(data)
203            finally:
204                fd.close()
205
206        raise RequestDone
207
208    def read(self, size):
209        """Read the specified number of bytes from the request body."""
210        raise NotImplementedError
211
212    def write(self, data):
213        """Write the given data to the response body."""
214        raise NotImplementedError
215
216
217class IAuthenticator(Interface):
218    """Extension point interface for components that can provide the name
219    of the remote user."""
220
221    def authenticate(req):
222        """Return the name of the remote user, or `None` if the identity of the
223        user is unknown."""
224
225
226class IRequestHandler(Interface):
227    """Extension point interface for request handlers."""
228
229    def match_request(req):
230        """Return whether the handler wants to process the given request."""
231
232    def process_request(req):
233        """Process the request. Should return a (template_name, content_type)
234        tuple, where `template` is the ClearSilver template to use (either
235        a `neo_cs.CS` object, or the file name of the template), and
236        `content_type` is the MIME type of the content. If `content_type` is
237        `None`, "text/html" is assumed.
238
239        Note that if template processing should not occur, this method can
240        simply send the response itself and not return anything.
241        """
242
243
244def absolute_url(req, path=None):
245    """Reconstruct the absolute URL of the given request.
246   
247    If the `path` parameter is specified, the path is appended to the URL.
248    Otherwise, only a URL with the components scheme, host and port is returned.
249    """
250    if hasattr(req, 'base_url'):
251        scheme, host, _, _, _, _ = urlparse.urlparse(req.base_url)
252    else:
253        scheme = req.scheme
254        host = req.get_header('Host')
255        if not host:
256            # Missing host header, so reconstruct the host from the
257            # server name and port
258            default_port = {'http': 80, 'https': 443}
259            if req.server_port and req.server_port != default_port[scheme]:
260                host = '%s:%d' % (req.server_name, req.server_port)
261            else:
262                host = req.server_name
263        if not path:
264            path = req.cgi_location
265    return urlparse.urlunparse((scheme, host, path, None, None, None))
Note: See TracBrowser for help on using the browser.