Edgewall Software

Changeset 4087

Show
Ignore:
Timestamp:
10/30/2006 04:07:14 PM (2 years ago)
Author:
cmlenz
Message:

Use a more transparent method to avoid building the authenticaton, session and chrome data for requests that don't need them: the Request object now supports specifying callback functions that are called when undefined request attributes are accessed. The request dispatcher provides callbacks for req.authname, req.session, req.hdf, etc, so that those are evaluated lazily.

Location:
trunk/trac/web
Files:
4 modified

Legend:

Unmodified
Added
Removed
  • trunk/trac/web/api.py

    r4010 r4087  
    124124    This class provides a convenience API over WSGI. 
    125125    """ 
    126     args = None 
    127     hdf = None 
    128     authname = None 
    129     perm = None 
    130     session = None 
    131126 
    132127    def __init__(self, environ, start_response): 
     
    135130        @param environ: The WSGI environment dict 
    136131        @param start_response: The WSGI callback for starting the response 
     132        @param callbacks: A dictionary of functions that are used to lazily 
     133            evaluate attribute lookups 
    137134        """ 
    138135        self.environ = environ 
     
    142139        self._response = None 
    143140 
    144         self._inheaders = [(name[5:].replace('_', '-').lower(), value) 
    145                            for name, value in environ.items() 
    146                            if name.startswith('HTTP_')] 
    147         if 'CONTENT_LENGTH' in environ: 
    148             self._inheaders.append(('content-length', 
    149                                     environ['CONTENT_LENGTH'])) 
    150         if 'CONTENT_TYPE' in environ: 
    151             self._inheaders.append(('content-type', environ['CONTENT_TYPE'])) 
    152141        self._outheaders = [] 
    153142        self._outcharset = None 
    154  
    155         self.incookie = Cookie() 
    156         cookie = self.get_header('Cookie') 
    157         if cookie: 
    158             self.incookie.load(cookie, ignore_parse_errors=True) 
    159143        self.outcookie = Cookie() 
     144 
     145        self.callbacks = { 
     146            'args': Request._parse_args, 
     147            'incookie': Request._parse_cookies, 
     148            '_inheaders': Request._parse_headers 
     149        } 
    160150 
    161151        self.base_url = self.environ.get('trac.base_url') 
     
    165155        self.abs_href = Href(self.base_url) 
    166156 
    167         self.args = self._parse_args() 
    168  
    169     def _parse_args(self): 
    170         """Parse the supplied request parameters into a dictionary.""" 
    171         args = _RequestArgs() 
    172  
    173         fp = self.environ['wsgi.input'] 
    174         ctype = self.get_header('Content-Type') 
    175         if ctype: 
    176             # Avoid letting cgi.FieldStorage consume the input stream when the 
    177             # request does not contain form data 
    178             ctype, options = cgi.parse_header(ctype) 
    179             if ctype not in ('application/x-www-form-urlencoded', 
    180                              'multipart/form-data'): 
    181                 fp = StringIO('') 
    182  
    183         fs = cgi.FieldStorage(fp, environ=self.environ, keep_blank_values=True) 
    184         if fs.list: 
    185             for name in fs.keys(): 
    186                 values = fs[name] 
    187                 if not isinstance(values, list): 
    188                     values = [values] 
    189                 for value in values: 
    190                     if not value.filename: 
    191                         value = unicode(value.value, 'utf-8') 
    192                     if name in args: 
    193                         if isinstance(args[name], list): 
    194                             args[name].append(value) 
    195                         else: 
    196                             args[name] = [args[name], value] 
    197                     else: 
    198                         args[name] = value 
    199  
    200         return args 
    201  
    202     def _reconstruct_url(self): 
    203         """Reconstruct the absolute base URL of the application.""" 
    204         host = self.get_header('Host') 
    205         if not host: 
    206             # Missing host header, so reconstruct the host from the 
    207             # server name and port 
    208             default_port = {'http': 80, 'https': 443} 
    209             if self.server_port and self.server_port != default_port[self.scheme]: 
    210                 host = '%s:%d' % (self.server_name, self.server_port) 
    211             else: 
    212                 host = self.server_name 
    213         return urlparse.urlunparse((self.scheme, host, self.base_path, None, 
    214                                     None, None)) 
     157    def __getattr__(self, name): 
     158        """Performs lazy attribute lookup by delegating to the functions in the 
     159        callbacks dictionary.""" 
     160        if name in self.callbacks: 
     161            value = self.callbacks[name](self) 
     162            setattr(self, name, value) 
     163            return value 
     164        return getattr(super(Request, self), name) 
     165 
     166    def __repr__(self): 
     167        return '<%s "%s %s">' % (self.__class__.__name__, self.method, 
     168                                 self.path_info) 
     169 
     170    # Public API 
    215171 
    216172    method = property(fget=lambda self: self.environ['REQUEST_METHOD'], 
     
    258214        self._outheaders.append((name, unicode(value).encode('utf-8'))) 
    259215 
    260     def _send_cookie_headers(self): 
    261         for name in self.outcookie.keys(): 
    262             path = self.outcookie[name].get('path') 
    263             if path: 
    264                 path = path.replace(' ', '%20') \ 
    265                            .replace(';', '%3B') \ 
    266                            .replace(',', '%3C') 
    267             self.outcookie[name]['path'] = path 
    268  
    269         cookies = self.outcookie.output(header='') 
    270         for cookie in cookies.splitlines(): 
    271             self._outheaders.append(('Set-Cookie', cookie.strip())) 
    272  
    273216    def end_headers(self): 
    274217        """Must be called after all headers have been sent and before the actual 
     
    463406        self._write(data) 
    464407 
     408    # Internal methods 
     409 
     410    def _parse_args(self): 
     411        """Parse the supplied request parameters into a dictionary.""" 
     412        args = _RequestArgs() 
     413 
     414        fp = self.environ['wsgi.input'] 
     415        ctype = self.get_header('Content-Type') 
     416        if ctype: 
     417            # Avoid letting cgi.FieldStorage consume the input stream when the 
     418            # request does not contain form data 
     419            ctype, options = cgi.parse_header(ctype) 
     420            if ctype not in ('application/x-www-form-urlencoded', 
     421                             'multipart/form-data'): 
     422                fp = StringIO('') 
     423 
     424        fs = cgi.FieldStorage(fp, environ=self.environ, keep_blank_values=True) 
     425        if fs.list: 
     426            for name in fs.keys(): 
     427                values = fs[name] 
     428                if not isinstance(values, list): 
     429                    values = [values] 
     430                for value in values: 
     431                    if not value.filename: 
     432                        value = unicode(value.value, 'utf-8') 
     433                    if name in args: 
     434                        if isinstance(args[name], list): 
     435                            args[name].append(value) 
     436                        else: 
     437                            args[name] = [args[name], value] 
     438                    else: 
     439                        args[name] = value 
     440 
     441        return args 
     442 
     443    def _parse_cookies(self): 
     444        cookies = Cookie() 
     445        header = self.get_header('Cookie') 
     446        if header: 
     447            cookies.load(header, ignore_parse_errors=True) 
     448        return cookies 
     449 
     450    def _parse_headers(self): 
     451        headers = [(name[5:].replace('_', '-').lower(), value) 
     452                   for name, value in self.environ.items() 
     453                   if name.startswith('HTTP_')] 
     454        if 'CONTENT_LENGTH' in self.environ: 
     455            headers.append(('content-length', self.environ['CONTENT_LENGTH'])) 
     456        if 'CONTENT_TYPE' in self.environ: 
     457            headers.append(('content-type', self.environ['CONTENT_TYPE'])) 
     458        return headers 
     459 
     460    def _reconstruct_url(self): 
     461        """Reconstruct the absolute base URL of the application.""" 
     462        host = self.get_header('Host') 
     463        if not host: 
     464            # Missing host header, so reconstruct the host from the 
     465            # server name and port 
     466            default_port = {'http': 80, 'https': 443} 
     467            if self.server_port and self.server_port != default_port[self.scheme]: 
     468                host = '%s:%d' % (self.server_name, self.server_port) 
     469            else: 
     470                host = self.server_name 
     471        return urlparse.urlunparse((self.scheme, host, self.base_path, None, 
     472                                    None, None)) 
     473 
     474    def _send_cookie_headers(self): 
     475        for name in self.outcookie.keys(): 
     476            path = self.outcookie[name].get('path') 
     477            if path: 
     478                path = path.replace(' ', '%20') \ 
     479                           .replace(';', '%3B') \ 
     480                           .replace(',', '%3C') 
     481            self.outcookie[name]['path'] = path 
     482 
     483        cookies = self.outcookie.output(header='') 
     484        for cookie in cookies.splitlines(): 
     485            self._outheaders.append(('Set-Cookie', cookie.strip())) 
     486 
    465487 
    466488class IAuthenticator(Interface): 
     
    476498    """Extension point interface for request handlers.""" 
    477499 
    478     # implementing classes should set this property to `True` if they 
    479     # don't need session and authentication related information 
    480     anonymous_request = False 
    481      
    482     # implementing classes should set this property to `False` if they 
    483     # don't need the HDF data and don't produce content using a template 
    484     use_template = True 
    485      
    486500    def match_request(req): 
    487501        """Return whether the handler wants to process the given request.""" 
  • trunk/trac/web/chrome.py

    r4063 r4087  
    2727from genshi.template import TemplateLoader, MarkupTemplate, TextTemplate 
    2828 
     29from trac import __version__ as VERSION 
    2930from trac import mimeview 
    3031from trac.config import * 
     
    4546    """ 
    4647    linkid = '%s:%s' % (rel, href) 
    47     linkset = req.environ.setdefault('trac.chrome.linkset', set()) 
     48    linkset = req.chrome.setdefault('linkset', set()) 
    4849    if linkid in linkset: 
    4950        return # Already added that link 
     
    5758        link['class'] = classname 
    5859 
    59     links = req.environ.setdefault('trac.chrome.links', {}) 
     60    links = req.chrome.setdefault('links', {}) 
    6061    links.setdefault(rel, []).append(link) 
    6162    linkset.add(linkid) 
     
    6566    in the generated HTML page. 
    6667    """ 
    67     if filename.startswith('common/') and 'trac.htdocs_location' in req.environ: 
    68         href = Href(req.environ['trac.htdocs_location']) 
     68    if filename.startswith('common/') and 'htdocs_location' in req.chrome: 
     69        href = Href(req.chrome['htdocs_location']) 
    6970        filename = filename[7:] 
    7071    else: 
     
    7475def add_script(req, filename, mimetype='text/javascript'): 
    7576    """Add a reference to an external javascript file to the template.""" 
    76     scriptset = req.environ.setdefault('trac.chrome.scriptset', set()) 
     77    scriptset = req.chrome.setdefault('trac.chrome.scriptset', set()) 
    7778    if filename in scriptset: 
    7879        return False # Already added that script 
    7980 
    80     if filename.startswith('common/') and 'trac.htdocs_location' in req.environ: 
    81         href = Href(req.environ['trac.htdocs_location']) 
     81    if filename.startswith('common/') and 'htdocs_location' in req.chrome: 
     82        href = Href(req.chrome['htdocs_location']) 
    8283        filename = filename[7:] 
    8384    else: 
     
    8586    script = {'href': href(filename), 'type': mimetype} 
    8687 
    87     req.environ.setdefault('trac.chrome.scripts', []).append(script) 
     88    req.chrome.setdefault('scripts', []).append(script) 
    8889    scriptset.add(filename) 
    8990 
     
    227228    # IRequestHandler methods 
    228229 
    229     anonymous_request = True 
    230     use_template = False 
    231  
    232230    def match_request(self, req): 
    233231        match = re.match(r'/chrome/(?P<prefix>[^/]+)/+(?P<filename>[/\w\-\.]+)', 
     
    286284 
    287285    def prepare_request(self, req, handler=None): 
    288         req.environ['trac.chrome.links'] = {} 
    289         req.environ['trac.chrome.scripts'] = [] 
     286        """Prepare the basic chrome data for the request. 
     287         
     288        @param req: the request object 
     289        @param handler: the `IRequestHandler` instance that is processing the 
     290            request 
     291        """ 
     292        self.log.debug('Prepare chrome data for request') 
     293 
     294        chrome = {'links': {}, 'scripts': []} 
     295 
     296        # This is ugly... we can't pass the real Request object to the 
     297        # add_xxx methods, because it doesn't yet have the chrome attribute 
     298        class FakeRequest(object): 
     299            def __init__(self, req): 
     300                self.base_path = req.base_path 
     301                self.chrome = chrome 
     302        fakereq = FakeRequest(req) 
     303 
    290304        htdocs_location = self.htdocs_location or req.href.chrome('common') 
    291         req.environ['trac.htdocs_location'] = htdocs_location.rstrip('/') + '/' 
     305        chrome['htdocs_location'] = htdocs_location.rstrip('/') + '/' 
    292306 
    293307        # HTML <head> links 
    294         add_link(req, 'start', req.href.wiki()) 
    295         add_link(req, 'search', req.href.search()) 
    296         add_link(req, 'help', req.href.wiki('TracGuide')) 
    297         add_stylesheet(req, 'common/css/trac.css') 
    298         add_script(req, 'common/js/jquery.js') 
    299         add_script(req, 'common/js/trac.js') 
    300         add_script(req, 'common/js/search.js') 
     308        add_link(fakereq, 'start', req.href.wiki()) 
     309        add_link(fakereq, 'search', req.href.search()) 
     310        add_link(fakereq, 'help', req.href.wiki('TracGuide')) 
     311        add_stylesheet(fakereq, 'common/css/trac.css') 
     312        add_script(fakereq, 'common/js/jquery.js') 
     313        add_script(fakereq, 'common/js/trac.js') 
     314        add_script(fakereq, 'common/js/search.js') 
    301315 
    302316        icon = self.env.project_icon 
     
    308322                    icon = req.href.chrome('common', icon) 
    309323            mimetype = mimeview.get_mimetype(icon) 
    310             add_link(req, 'icon', icon, mimetype=mimetype) 
    311             add_link(req, 'shortcut icon', icon, mimetype=mimetype) 
     324            add_link(fakereq, 'icon', icon, mimetype=mimetype) 
     325            add_link(fakereq, 'shortcut icon', icon, mimetype=mimetype) 
    312326 
    313327        # Logo image 
    314         req.environ['trac.chrome.logo'] = self.get_logo_data(req.href) 
     328        chrome['logo'] = self.get_logo_data(req.href) 
    315329 
    316330        # Navigation links 
     
    342356                    nav[category][-1]['active'] = True 
    343357 
    344         req.environ['trac.chrome.nav'] = nav 
     358        chrome['nav'] = nav 
     359 
     360        return chrome 
    345361 
    346362    def get_logo_data(self, href): 
     
    369385        """Add chrome-related data to the HDF (deprecated).""" 
    370386        req.hdf['HTTP.PathInfo'] = req.path_info 
    371         req.hdf['htdocs_location'] = req.environ.get('trac.htdocs_location') 
     387        req.hdf['htdocs_location'] = req.chrome['htdocs_location'] 
    372388 
    373389        req.hdf['chrome.href'] = req.href.chrome() 
    374         req.hdf['chrome.links'] = req.environ.get('trac.chrome.links', []) 
    375         req.hdf['chrome.logo'] = req.environ.get('trac.chrome.logo', {}) 
    376         req.hdf['chrome.scripts'] = req.environ.get('trac.chrome.scripts', []) 
    377  
    378         for category, items in req.environ.get('trac.chrome.nav', {}).items(): 
     390        req.hdf['chrome.links'] = req.chrome['links'] 
     391        req.hdf['chrome.scripts'] = req.chrome['scripts'] 
     392        req.hdf['chrome.logo'] = req.chrome['logo'] 
     393 
     394        for category, items in chrome['nav'].items(): 
    379395            for item in items: 
    380396                prefix = 'chrome.nav.%s.%s' % (category, item['name']) 
     
    382398 
    383399    def populate_data(self, req, data): 
    384         from trac import __version__ as VERSION 
    385  
    386400        data.update(self._default_context_data) 
    387401        data.setdefault('trac', {}).update({ 
     
    398412 
    399413        chrome = data.setdefault('chrome', {}) 
    400         chrome.update({ 
    401             'footer': Markup(self.env.project_footer), 
    402         }) 
    403414        if req: 
    404             chrome.update({ 
    405                 'htdocs_location': req.environ.get('trac.htdocs_location'), 
    406                 'logo': req.environ.get('trac.chrome.logo', {}), 
    407                 'links': req.environ.get('trac.chrome.links', []), 
    408                 'nav': req.environ.get('trac.chrome.nav', {}), 
    409                 'scripts': req.environ.get('trac.chrome.scripts', []), 
    410             }) 
     415            chrome.update(req.chrome) 
    411416        else: 
    412417            chrome.update({ 
     
    414419                'logo': self.get_logo_data(self.env.abs_href), 
    415420            }) 
    416  
    417         data['req'] = req 
    418         data['abs_href'] = req and req.abs_href or self.env.abs_href 
    419         data['href'] = req and req.href 
    420         data['perm'] = req and req.perm 
    421         data['authname'] = req and req.authname or '<trac>' 
    422  
    423         # Timezone-dependant functions 
     421        chrome.update({ 
     422            'footer': Markup(self.env.project_footer) 
     423        }) 
     424 
    424425        tzinfo = None 
    425426        if req: 
    426427            tzinfo = req.tz 
    427         data['format_datetime'] = partial(format_datetime, tzinfo=tzinfo) 
    428         data['format_date'] = partial(format_date, tzinfo=tzinfo) 
    429         data['format_time'] = partial(format_time, tzinfo=tzinfo) 
    430         data['fromtimestamp'] = partial(datetime.fromtimestamp, tz=tzinfo) 
     428 
     429        data.update({ 
     430            'req': req, 
     431            'abs_href': req and req.abs_href or self.env.abs_href, 
     432            'href': req and req.href, 
     433            'perm': req and req.perm, 
     434            'authname': req and req.authname or '<trac>', 
     435            'format_datetime': partial(format_datetime, tzinfo=tzinfo), 
     436            'format_date': partial(format_date, tzinfo=tzinfo), 
     437            'format_time': partial(format_time, tzinfo=tzinfo), 
     438            'fromtimestamp': partial(datetime.fromtimestamp, tz=tzinfo) 
     439        }) 
    431440 
    432441    def load_template(self, filename, method=None): 
  • trunk/trac/web/main.py

    r4086 r4087  
    3131from trac.env import open_environment 
    3232from trac.perm import PermissionCache, NoPermissionCache, PermissionError 
    33 from trac.util import reversed, get_lines_from_file, get_last_traceback 
     33from trac.util import get_lines_from_file, get_last_traceback 
     34from trac.util.compat import partial, reversed 
    3435from trac.util.datefmt import format_datetime, http_date, localtz, timezone 
    3536from trac.util.html import Markup 
     
    116117        if req.perm: 
    117118            for action in req.perm.permissions(): 
    118                 req.hdf['trac.acl.' + action] = True 
     119                hdf['trac.acl.' + action] = True 
    119120 
    120121        for arg in [k for k in req.args.keys() if k]: 
     
    144145 
    145146    default_timezone = Option('trac', 'default_timezone', '', 
    146                               doc="""The default timezone to use""") 
    147  
    148  
     147        """The default timezone to use""") 
    149148 
    150149    # Public API 
     
    165164        site chrome. 
    166165        """ 
     166        self.log.debug('Dispatching %r', req) 
     167        chrome = Chrome(self.env) 
     168 
     169        # Setup request callbacks for lazily-evaluated properties 
     170        req.callbacks.update({ 
     171            'authname': self.authenticate, 
     172            'chrome': chrome.prepare_request, 
     173            'hdf': self._get_hdf, 
     174            'perm': self._get_perm, 
     175            'session': self._get_session, 
     176            'tz': self._get_timezone 
     177        }) 
     178 
    167179        # Select the component that should handle the request 
    168180        chosen_handler = None 
    169         early_error = None 
    170         try: 
    171             if not req.path_info or req.path_info == '/': 
    172                 chosen_handler = self.default_handler 
    173             else: 
    174                 for handler in self.handlers: 
    175                     if handler.match_request(req): 
    176                         chosen_handler = handler 
    177                         break 
    178  
    179             chosen_handler = self._pre_process_request(req, chosen_handler) 
    180         except: 
    181             early_error = sys.exc_info() 
    182              
    183         if not chosen_handler and not early_error: 
    184             early_error = (HTTPNotFound('No handler matched request to %s', 
    185                                         req.path_info), 
    186                            None, None) 
    187  
    188         # Attach user information to the request 
    189         anonymous_request = getattr(chosen_handler, 'anonymous_request', 
    190                                     False) 
    191         if not anonymous_request: 
    192             try: 
    193                 req.authname = self.authenticate(req) 
    194                 req.perm = PermissionCache(self.env, req.authname) 
    195                 req.session = Session(self.env, req) 
    196             except: 
    197                 anonymous_request = True 
    198                 early_error = sys.exc_info() 
    199         if anonymous_request: 
    200             req.authname = 'anonymous' 
    201             req.perm = NoPermissionCache() 
    202  
    203         try: 
    204             req.tz = timezone(req.session.get('tz', self.default_timezone 
    205                                               or 'missing')) 
    206         except: 
    207             req.tz = localtz 
    208  
    209         # Prepare HDF for the clearsilver template 
    210         try: 
    211             use_template = getattr(chosen_handler, 'use_template', True) 
    212             req.hdf = None 
    213             if use_template: 
    214                 chrome = Chrome(self.env) 
    215                 req.hdf = HDFWrapper(loadpaths=chrome.get_all_templates_dirs()) 
    216                 populate_hdf(req.hdf, self.env, req) 
    217                 chrome.prepare_request(req, chosen_handler) 
    218         except: 
    219             req.hdf = None # revert to sending plaintext error 
    220             if not early_error: 
    221                 raise 
    222  
    223         if early_error: 
    224             try: 
    225                 self._post_process_request(req) 
    226             except Exception, e: 
    227                 self.log.exception(e) 
    228             raise early_error[0], early_error[1], early_error[2] 
     181        if not req.path_info or req.path_info == '/': 
     182            chosen_handler = self.default_handler 
     183        else: 
     184            for handler in self.handlers: 
     185                if handler.match_request(req): 
     186                    chosen_handler = handler 
     187                    break 
     188        chosen_handler = self._pre_process_request(req, chosen_handler) 
     189        if not chosen_handler: 
     190            raise HTTPNotFound('No handler matched request to %s', 
     191                               req.path_info) 
     192 
     193        req.callbacks['chrome'] = partial(chrome.prepare_request, 
     194                                          handler=chosen_handler) 
    229195 
    230196        # Process the request and render the template 
     
    233199                resp = chosen_handler.process_request(req) 
    234200                if resp: 
    235                     chrome = Chrome(self.env) 
    236201                    if len(resp) == 2: # Clearsilver 
    237202                        chrome.populate_hdf(req) 
     
    267232            raise HTTPInternalError(e.message) 
    268233 
     234    # Internal methods 
     235 
     236    def _get_hdf(self, req): 
     237        hdf = HDFWrapper(loadpaths=Chrome(self.env).get_all_templates_dirs()) 
     238        populate_hdf(hdf, self.env, req) 
     239        return hdf 
     240 
     241    def _get_perm(self, req): 
     242        return PermissionCache(self.env, req.authname) 
     243 
     244    def _get_session(self, req): 
     245        return Session(self.env, req) 
     246 
     247    def _get_timezone(self, req): 
     248        try: 
     249            return timezone(req.session.get('tz', self.default_timezone 
     250                                            or 'missing')) 
     251        except: 
     252            return localtz 
     253 
    269254    def _pre_process_request(self, req, chosen_handler): 
    270         for f in self.filters: 
    271             chosen_handler = f.pre_process_request(req, chosen_handler) 
     255        for filter_ in self.filters: 
     256            chosen_handler = filter_.pre_process_request(req, chosen_handler) 
    272257        return chosen_handler 
    273                  
     258 
    274259    def _post_process_request(self, req, template=None, content_type=None): 
    275260        for f in reversed(self.filters): 
  • trunk/trac/web/session.py

    r3616 r4087  
    6060 
    6161    def get_session(self, sid, authenticated=False): 
     62        self.env.log.debug('Retrieving session for ID %r', sid) 
     63 
    6264        db = self.env.get_db_cnx() 
    6365        cursor = db.cursor()