1"""URLParser
2
3URL parsing is done through objects which are subclasses of the `URLParser`
4class. `Application` delegates most of the URL parsing to these objects.
5
6Application has a single "root" URL parser, which is used to parse all URLs.
7This parser then can pass the request on to other parsers, usually taking off
8parts of the URL with each step.
9
10This root parser is generally `ContextParser`, which is instantiated and set
11up by `Application` (accessible through `Application.rootURLParser`).
12"""
13
14import os
15import re
16import sys
17
18from warnings import warn
19
20from HTTPExceptions import HTTPNotFound, HTTPMovedPermanently
21from MiscUtils.ParamFactory import ParamFactory
22from WebUtils.Funcs import urlDecode
23
24debug = False
25
26# Legal characters for use in a module name -- used when turning
27# an entire path into a module name.
28_moduleNameRE = re.compile('[^a-zA-Z_]')
29
30_globalApplication = None
31
32
33def application():
34    """Returns the global Application."""
35    return _globalApplication
36
37
38class URLParser(object):
39    """URLParser is the base class for all URL parsers.
40
41    Though its functionality is sparse, it may be expanded in the future.
42    Subclasses should implement a `parse` method, and may also want to
43    implement an `__init__` method with arguments that control how the
44    parser works (for instance, passing a starting path for the parser)
45
46    The `parse` method is where most of the work is done. It takes two
47    arguments -- the transaction and the portion of the URL that is still to
48    be parsed. The transaction may (and usually is) modified along the way.
49    The URL is passed through so that you can take pieces off the front,
50    and then pass the reduced URL to another parser. The method should return
51    a servlet (never None).
52
53    If you cannot find a servlet, or some other (somewhat) expected error
54    occurs, you should raise an exception. HTTPNotFound probably being the
55    most interesting.
56    """
57
58    def __init__(self):
59        pass
60
61    def findServletForTransaction(self, trans):
62        """Returns a servlet for the transaction.
63
64        This is the top-level entry point, below it `parse` is used.
65        """
66        return self.parse(trans, trans.request().urlPath())
67
68
69class ContextParser(URLParser):
70    """Find the context of a request.
71
72    ContextParser uses the ``Application.config`` context settings to find
73    the context of the request.  It then passes the request to a FileParser
74    rooted in the context path.
75
76    The context is the first element of the URL, or if no context matches
77    that then it is the ``default`` context (and the entire URL is passed
78    to the default context's FileParser).
79
80    There is generally only one ContextParser, which can be found as
81    ``application.rootURLParser()``.
82    """
83
84
85    ## Init ##
86
87    def __init__(self, app):
88        """Create ContextParser.
89
90        ContextParser is usually created by Application, which
91        passes all requests to it.
92
93        In __init__ we take the ``Contexts`` setting from
94        Application.config and parse it slightly.
95        """
96        URLParser.__init__(self)
97        # Need to set this here because we need for initialization, during
98        # which AppServer.globalAppServer.application() doesn't yet exist:
99        self._app = app
100        self._imp = app._imp
101        # self._context will be a dictionary of context names and context
102        # directories.  It is set by `addContext`.
103        self._contexts = {}
104        # add all contexts except the default, which we save until the end
105        contexts = app.setting('Contexts')
106        defaultContext = ''
107        for name, path in contexts.items():
108            path = os.path.normpath(path)  # for Windows
109            if name == 'default':
110                defaultContext = path
111            else:
112                name = '/'.join(filter(lambda x: x, name.split('/')))
113                self.addContext(name, path)
114        if not defaultContext:
115            # If no default context has been specified, and there is a unique
116            # context not built into Webware, use it as the default context.
117            for name in contexts:
118                if name.endswith('/Docs') or name in (
119                        'Admin', 'Docs', 'Examples',  'MKBrowser', 'Testing'):
120                    continue
121                if defaultContext:
122                    defaultContext = None
123                    break
124                else:
125                    defaultContext = name
126            if not defaultContext:
127                # otherwise, try using the following contexts if available
128                for defaultContext in ('Default', 'Examples', 'Docs'):
129                    if defaultContext in contexts:
130                        break
131                else:  # if not available, refuse the temptation to guess
132                    raise KeyError("No default context has been specified.")
133        if defaultContext in self._contexts:
134            self._defaultContext = defaultContext
135        else:
136            for name, path in self._contexts.items():
137                if defaultContext == path:
138                    self._defaultContext = name
139                    break
140            else:
141                self.addContext('default', defaultContext)
142                self._defaultContext = 'default'
143
144
145    ## Context handling ##
146
147    def resolveDefaultContext(self, dest):
148        """Find default context.
149
150        Figure out if the default context refers to an existing context,
151        the same directory as an existing context, or a unique directory.
152
153        Returns the name of the context that the default context refers to,
154        or 'default' if the default context is unique.
155        """
156        contexts = self._contexts
157        if dest in contexts:
158            # The default context refers to another context,
159            # not a unique context.  Return the name of that context.
160            return dest
161        destPath = self.absContextPath(dest)
162        for name, path in contexts.items():
163            if name != 'default' and self.absContextPath(path) == destPath:
164                # The default context has the same directory
165                # as another context, so it's still not unique
166                return name
167        # The default context has no other name
168        return 'default'
169
170    def addContext(self, name, path):
171        """Add a context to the system.
172
173        The context will be imported as a package, going by `name`,
174        from the given directory path. The directory doesn't have to match
175        the context name.
176        """
177        if name == 'default':
178            dest = self.resolveDefaultContext(path)
179            self._defaultContext = dest
180            if dest != 'default':
181                # in this case default refers to an existing context, so
182                # there's not much to do
183                print 'Default context aliases to: %s' % (dest,)
184                return
185
186        e = None
187        try:
188            importAsName = name
189            localDir, packageName = os.path.split(path)
190            if importAsName in sys.modules:
191                mod = sys.modules[importAsName]
192            else:
193                try:
194                    res = self._imp.find_module(packageName, [localDir])
195                    if not res:
196                        raise ImportError
197                except ImportError as e:
198                    if not str(e):
199                        e = 'Could not import package'
200                    # Maybe this happened because it had been forgotten
201                    # to add the __init__.py file. So we try to create one:
202                    if os.path.exists(path):
203                        f = os.path.join(path, '__init__.py')
204                        if (not os.path.exists(f)
205                                and not os.path.exists(f + 'c')
206                                and not os.path.exists(f + 'o')):
207                            print ("Creating __init__.py file"
208                                " for context '%s'" % name)
209                            try:
210                                open(f, 'w').write(
211                                    '# Auto-generated by WebKit' + os.linesep)
212                            except Exception:
213                                print ("Error: __init__.py file"
214                                    " could not be created.")
215                            else:
216                                res = self._imp.find_module(packageName,
217                                    [localDir])
218                                if res:
219                                    e = None
220                    if e:
221                        raise
222                mod = self._imp.load_module(name, *res)
223        except (ImportError, TypeError) as e:
224            # TypeError can be raised by imp.load_module()
225            # when the context path does not exist
226            pass
227        if e:
228            print 'Error loading context: %s: %s: dir=%s' % (name, e, path)
229            return
230
231        if hasattr(mod, 'contextInitialize'):
232            # @@ gat 2003-07-23: switched back to old method
233            # of passing application as first parameter
234            # to contextInitialize for backward compatibility
235            result = mod.contextInitialize(
236                application(),
237                os.path.normpath(os.path.join(os.getcwd(), path)))
238            # @@: funny hack...?
239            if result is not None and 'ContentLocation' in result:
240                path = result['ContentLocation']
241
242        print 'Loading context: %s at %s' % (name, path)
243        self._contexts[name] = path
244
245    def absContextPath(self, path):
246        """Get absolute context path.
247
248        Resolves relative paths, which are assumed to be relative to the
249        Application's serverSidePath (the working directory).
250        """
251        if os.path.isabs(path):
252            return path
253        else:
254            return self._app.serverSidePath(path)
255
256
257    ## Parsing ##
258
259    def parse(self, trans, requestPath):
260        """Parse request.
261
262        Get the context name, and dispatch to a FileParser rooted
263        in the context's path.
264
265        The context name and file path are stored in the request (accessible
266        through `Request.serverSidePath` and `Request.contextName`).
267        """
268        # This is a hack... should probably go in the Transaction class:
269        trans._fileParserInitSeen = {}
270        # If there is no path, redirect to the root path:
271        req = trans.request()
272        if not requestPath:
273            p = req.servletPath() + '/'
274            q = req.queryString()
275            if q:
276                p += "?" + q
277            raise HTTPMovedPermanently(location=p)
278        # Determine the context name:
279        if req._absolutepath:
280            contextName = self._defaultContext
281        else:
282            context = filter(None, requestPath.split('/'))
283            if requestPath.endswith('/'):
284                context.append('')
285            parts = []
286            while context:
287                contextName = '/'.join(context)
288                if contextName in self._contexts:
289                    break
290                parts.insert(0, context.pop())
291            if context:
292                if parts:
293                    parts.insert(0, '')
294                    requestPath = '/'.join(parts)
295                else:
296                    requestPath = ''
297            else:
298                contextName = self._defaultContext
299        context = self._contexts[contextName]
300        req._serverSideContextPath = context
301        req._contextName = contextName
302        fpp = FileParser(context)
303        return fpp.parse(trans, requestPath)
304
305
306class _FileParser(URLParser):
307    """Parse requests to the filesystem.
308
309    FileParser dispatches to servlets in the filesystem, as well as providing
310    hooks to override the FileParser.
311
312    FileParser objects are threadsafe. A factory function is used to cache
313    FileParser instances, so for any one path only a single FileParser instance
314    will exist.  The `_FileParser` class is the real class, and `FileParser` is
315    a factory that either returns an existent _FileParser object, or creates a
316    new one if none exists.
317
318    FileParser uses several settings from ``Application.config``, which are
319    persistent over the life of the application. These are set up in the
320    function `initApp`, as class variables. They cannot be set when the module
321    is loaded, because the Application is not yet set up, so `initApp` is
322    called in `Application.__init__`.
323    """
324
325
326    ## Init ##
327
328    def __init__(self, path):
329        """Create a FileParser.
330
331        Each parsed directory has a FileParser instance associated with it
332        (``self._path``).
333        """
334        URLParser.__init__(self)
335        self._path = path
336        self._initModule = None
337
338
339    ## Parsing ##
340
341    def parse(self, trans, requestPath):
342        """Return the servlet.
343
344        __init__ files will be used for various hooks
345        (see `parseInit` for more).
346
347        If the next part of the URL is a directory, it calls
348        ``FileParser(dirPath).parse(trans, restOfPath)`` where ``restOfPath``
349        is `requestPath` with the first section of the path removed (the part
350        of the path that this FileParser just handled).
351
352        This uses `fileNamesForBaseName` to find files in its directory.
353        That function has several functions to define what files are ignored,
354        hidden, etc.  See its documentation for more information.
355        """
356        if debug:
357            print "FP(%r) parses %r" % (self._path, requestPath)
358
359        req = trans.request()
360
361        if req._absolutepath:
362            name = req._fsPath
363            restPart = req._extraURLPath
364
365        else:
366            # First decode the URL, since we are dealing with filenames here:
367            requestPath = urlDecode(requestPath)
368
369            result = self.parseInit(trans, requestPath)
370            if result is not None:
371                return result
372
373            if not requestPath or requestPath == '/':
374                return self.parseIndex(trans, requestPath)
375
376            if not requestPath.startswith('/'):
377                raise HTTPNotFound("Invalid path info: %s" % requestPath)
378
379            parts = requestPath[1:].split('/', 1)
380            nextPart = parts[0]
381            restPart = '/' + parts[1] if len(parts) > 1 else ''
382
383            baseName = os.path.join(self._path, nextPart)
384            if restPart and not self._extraPathInfo:
385                names = [baseName]
386            else:
387                names = self.filenamesForBaseName(baseName)
388
389            if len(names) > 1:
390                warn("More than one file matches %s in %s: %s"
391                    % (requestPath, self._path, names))
392                raise HTTPNotFound("Page is ambiguous")
393            elif not names:
394                return self.parseIndex(trans, requestPath)
395
396            name = names[0]
397            if os.path.isdir(name):
398                # directories are dispatched to FileParsers
399                # rooted in that directory
400                fpp = FileParser(name)
401                return fpp.parse(trans, restPart)
402
403            req._extraURLPath = restPart
404
405        if not self._extraPathInfo and restPart:
406            raise HTTPNotFound("Invalid extra path info: %s" % restPart)
407
408        req._serverSidePath = name
409
410        return ServletFactoryManager.servletForFile(trans, name)
411
412    def filenamesForBaseName(self, baseName):
413        """Find all files for a given base name.
414
415        Given a path, like ``/a/b/c``, searches for files in ``/a/b``
416        that start with ``c``.  The final name may include an extension,
417        which is less ambiguous; though if you ask for file.html,
418        and file.html.py exists, that file will be returned.
419
420        The files are filtered according to the settings ``FilesToHide``,
421        ``FilesToServe``, ``ExtensionsToIgnore`` and ``ExtensionsToServe``.
422        See the shouldServeFile() method for details on these settings.
423
424        All files that start with the given base name are returned
425        as a list. When the base name itself is part of the list or
426        when extensions are prioritized and such an extension is found
427        in the list, then the list will be reduced to only that entry.
428
429        Some settings are used to control the prioritization of filenames.
430        All settings are in ``Application.config``:
431
432        UseCascadingExtensions:
433            If true, then extensions will be prioritized.  So if
434            extension ``.tmpl`` shows up in ExtensionCascadeOrder
435            before ``.html``, then even if filenames with both
436            extensions exist, only the .tmpl file will be returned.
437        ExtensionCascadeOrder:
438            A list of extensions, ordered by priority.
439        """
440        if '*' in baseName:
441            return []
442
443        fileStart = os.path.basename(baseName)
444        dirName = os.path.dirname(baseName)
445        filenames = []
446        for filename in os.listdir(dirName):
447            if filename.startswith('.'):
448                continue
449            elif filename == fileStart:
450                if self.shouldServeFile(filename):
451                    return [os.path.join(dirName, filename)]
452            elif (filename.startswith(fileStart)
453                    and os.path.splitext(filename)[0] == fileStart):
454                if self.shouldServeFile(filename):
455                    filenames.append(os.path.join(dirName, filename))
456
457        if self._useCascading and len(filenames) > 1:
458            for extension in self._cascadeOrder:
459                if baseName + extension in filenames:
460                    return [baseName + extension]
461
462        return filenames
463
464    def shouldServeFile(self, filename):
465        """Check if the file with the given filename should be served.
466
467        Some settings are used to control the filtering of filenames.
468        All settings are in ``Application.config``:
469
470        FilesToHide:
471            These files will be ignored, and even given a full
472            extension will not be used.  Takes a glob.
473        FilesToServe:
474            If set, *only* files matching these globs will be
475            served, all other files will be ignored.
476        ExtensionsToIgnore:
477            Files with these extensions will be ignored, but if a
478            complete filename (with extension) is given the file
479            *will* be served (unlike FilesToHide).  Extensions are
480            in the form ``".py"``
481        ExtensionsToServe:
482            If set, only files with these extensions will be
483            served.  Like FilesToServe, only doesn't use globs.
484        """
485        ext = os.path.splitext(filename)[1]
486        if ext in self._toIgnore:
487            return False
488        if self._toServe and ext not in self._toServe:
489            return False
490        for regex in self._filesToHideRegexes:
491            if regex.match(filename):
492                return False
493        if self._filesToServeRegexes:
494            for regex in self._filesToServeRegexes:
495                if regex.match(filename):
496                    break
497            else:
498                return False
499        return True
500
501    def parseIndex(self, trans, requestPath):
502        """Return index servlet.
503
504        Return the servlet for a directory index (i.e., ``Main`` or
505        ``index``).  When `parse` encounters a directory and there's nothing
506        left in the URL, or when there is something left and no file matches
507        it, then it will try `parseIndex` to see if there's an index file.
508
509        That means that if ``/a/b/c`` is requested, and in ``/a`` there's no
510        file or directory matching ``b``, then it'll look for an index file
511        (like ``Main.py``), and that servlet will be returned. In fact, if
512        no ``a`` was found, and the default context had an index (like
513        ``index.html``) then that would be called with ``/a/b/c`` as
514        `HTTPRequest.extraURLPath`.  If you don't want that to occur, you
515        should raise an HTTPNotFound in your no-extra-url-path-taking servlets.
516
517        The directory names are based off the ``Application.config`` setting
518        ``DirectoryFile``, which is a list of base names, by default
519        ``["Main", "index", "main", "Index"]``, which are searched in order.
520        A file with any extension is allowed, so the index can be an HTML file,
521        a PSP file, a Kid template, a Python servlet, etc.
522        """
523        req = trans.request()
524        # If requestPath is empty, then we're missing the trailing slash:
525        if not requestPath:
526            p = req.serverURL() + '/'
527            q = req.queryString()
528            if q:
529                p += "?" + q
530            raise HTTPMovedPermanently(location=p)
531        if requestPath == '/':
532            requestPath = ''
533        for directoryFile in self._directoryFile:
534            basename = os.path.join(self._path, directoryFile)
535            names = self.filenamesForBaseName(basename)
536            if len(names) > 1 and self._useCascading:
537                for ext in self._cascadeOrder:
538                    if basename + ext in names:
539                        names = [basename + ext]
540                        break
541            if len(names) > 1:
542                warn("More than one file matches the index file %s in %s: %s"
543                    % (directoryFile, self._path, names))
544                raise HTTPNotFound("Index page is ambiguous")
545            if names:
546                if requestPath and not self._extraPathInfo:
547                    raise HTTPNotFound
548                req._serverSidePath = names[0]
549                req._extraURLPath = requestPath
550                return ServletFactoryManager.servletForFile(trans, names[0])
551        raise HTTPNotFound("Index page not found")
552
553    def initModule(self):
554        """Get the __init__ module object for this FileParser's directory."""
555        path = self._path
556        # if this directory is a context, return the context package
557        for context, dir in self._app.contexts().items():
558            if dir == path:
559                # avoid reloading of the context package
560                return sys.modules.get(context)
561        name = 'WebKit_Cache_' + _moduleNameRE.sub('_', path)
562        try:
563            file, path, desc = self._imp.find_module('__init__', [path])
564            return self._imp.load_module(name, file, path, desc)
565        except (ImportError, TypeError):
566            pass
567
568    def parseInit(self, trans, requestPath):
569        """Parse the __init__ file.
570
571        Returns the resulting servlet, or None if no __init__ hooks were found.
572
573        Hooks are put in by defining special functions or objects in your
574        __init__, with specific names:
575
576        `urlTransactionHook`:
577            A function that takes one argument (the transaction).
578            The return value from the function is ignored.  You
579            can modify the transaction with this function, though.
580
581        `urlRedirect`:
582            A dictionary.  Keys in the dictionary are source
583            URLs, the value is the path to redirect to, or a
584            `URLParser` object to which the transaction should
585            be delegated.
586
587            For example, if the URL is ``/a/b/c``, and we've already
588            parsed ``/a`` and are looking for ``b/c``, and we fine
589            `urlRedirect`` in a.__init__, then we'll look for a key
590            ``b`` in the dictionary.  The value will be a directory
591            we should continue to (say, ``/destpath/``).  We'll
592            then look for ``c`` in ``destpath``.
593
594            If a key '' (empty string) is in the dictionary, then
595            if no more specific key is found all requests will
596            be redirected to that path.
597
598            Instead of a string giving a path to redirect to, you
599            can also give a URLParser object, so that some portions
600            of the path are delegated to different parsers.
601
602            If no matching key is found, and there is no '' key,
603            then parsing goes on as usual.
604
605        `SubParser`:
606            This should be a class object.  It will be instantiated,
607            and then `parse` will be called with it, delegating to
608            this instance.  When instantiated, it will be passed
609            *this* FileParser instance; the parser can use this to
610            return control back to the FileParser after doing whatever
611            it wants to do.
612
613            You may want to use a line like this to handle the names::
614
615                from ParserX import ParserX as SubParser
616
617        `urlParser`:
618            This should be an already instantiated URLParser-like
619            object.  `parse(trans, requestPath)` will be called
620            on this instance.
621
622        `urlParserHook`:
623            Like `urlParser`, except the method
624            `parseHook(trans, requestPath, fileParser)` will
625            be called, where fileParser is this FileParser instance.
626
627        `urlJoins`:
628            Either a single path, or a list of paths.  You can also
629            use URLParser objects, like with `urlRedirect`.
630
631            Each of these paths (or parsers) will be tried in
632            order.  If it raises HTTPNotFound, then the next path
633            will be tried, ending with the current path.
634
635            Paths are relative to the current directory.  If you
636            don't want the current directory to be a last resort,
637            you can include '.' in the joins.
638        """
639        if self._initModule is None:
640            self._initModule = self.initModule()
641        mod = self._initModule
642
643        seen = trans._fileParserInitSeen.setdefault(self._path, set())
644
645        if ('urlTransactionHook' not in seen
646                and hasattr(mod, 'urlTransactionHook')):
647            seen.add('urlTransactionHook')
648            mod.urlTransactionHook(trans)
649
650        if 'urlRedirect' not in seen and hasattr(mod, 'urlRedirect'):
651            # @@: do we need this shortcircuit?
652            seen.add('urlRedirect')
653            try:
654                nextPart, restPath = requestPath[1:].split('/', 1)
655                restPath = '/' + restPath
656            except ValueError:
657                nextPart = requestPath[1:]
658                restPath = ''
659            if nextPart in mod.urlRedirect:
660                redirTo = mod.urlRedirect[nextPart]
661                redirPath = restPath
662            elif '' in mod.urlRedirect:
663                redirTo = mod.urlRedirect['']
664                redirPath = restPath
665            else:
666                redirTo = None
667            if redirTo:
668                if isinstance(redirTo, basestring):
669                    fpp = FileParser(os.path.join(self._path, redirTo))
670                else:
671                    fpp = redirTo
672                return fpp.parse(trans, redirPath)
673
674        if 'SubParser' not in seen and hasattr(mod, 'SubParser'):
675            seen.add('SubParser')
676            pp = mod.SubParser(self)
677            return pp.parse(trans, requestPath)
678
679        if 'urlParser' not in seen and hasattr(mod, 'urlParser'):
680            seen.add('urlParser')
681            pp = mod.urlParser
682            return pp.parse(trans, requestPath)
683
684        if 'urlParserHook' not in seen and hasattr(mod, 'urlParserHook'):
685            seen.add('urlParserHook')
686            pp = mod.urlParserHook
687            return pp.parseHook(trans, requestPath, self)
688
689        if 'urlJoins' not in seen and hasattr(mod, 'urlJoins'):
690            seen.add('urlJoins')
691            joinPath = mod.urlJoins
692            if isinstance(joinPath, basestring):
693                joinPath = [joinPath]
694            for path in joinPath:
695                path = os.path.join(self._path, path)
696                if isinstance(path, basestring):
697                    parser = FileParser(os.path.join(self._path, path))
698                else:
699                    parser = path
700                try:
701                    return parser.parse(trans, requestPath)
702                except HTTPNotFound:
703                    pass
704
705        return None
706
707FileParser = ParamFactory(_FileParser)
708
709
710class URLParameterParser(URLParser):
711    """Strips named parameters out of the URL.
712
713    E.g. in ``/path/SID=123/etc`` the ``SID=123`` will be removed from the URL,
714    and a field will be set in the request (so long as no field by that name
715    already exists -- if a field does exist the variable is thrown away).
716    These are put in the place of GET or POST variables.
717
718    It should be put in an __init__, like::
719
720        from WebKit.URLParser import URLParameterParser
721        urlParserHook = URLParameterParser()
722
723    Or (slightly less efficient):
724
725        from WebKit.URLParser import URLParameterParser as SubParser
726    """
727
728
729    ## Init ##
730
731    def __init__(self, fileParser=None):
732        self._fileParser = fileParser
733
734
735    ## Parsing ##
736
737    def parse(self, trans, requestPath):
738        """Delegates to `parseHook`."""
739        return self.parseHook(trans, requestPath, self._fileParser)
740
741    @staticmethod
742    def parseHook(trans, requestPath, hook):
743        """Munges the path.
744
745        The `hook` is the FileParser object that originally called this --
746        we just want to strip stuff out of the URL and then give it back to
747        the FileParser instance, which can actually find the servlet.
748        """
749        parts = requestPath.split('/')
750        result = []
751        req = trans.request()
752        for part in parts:
753            if '=' in part:
754                name, value = part.split('=', 1)
755                if not req.hasField(name):
756                    req.setField(name, value)
757            else:
758                result.append(part)
759        return hook.parse(trans, '/'.join(result))
760
761
762class ServletFactoryManagerClass(object):
763    """Manage servlet factories.
764
765    This singleton (called `ServletFactoryManager`) collects and manages
766    all the servlet factories that are installed.
767
768    See `addServletFactory` for adding new factories, and `servletForFile`
769    for getting the factories back.
770    """
771
772
773    ## Init ##
774
775    def __init__(self):
776        self.reset()
777
778    def reset(self):
779        self._factories = []
780        self._factoryExtensions = {}
781
782
783    ## Manager ##
784
785    def addServletFactory(self, factory):
786        """Add a new servlet factory.
787
788        Servlet factories can add themselves with::
789
790            ServletFactoryManager.addServletFactory(factory)
791
792        The factory must have an `extensions` method, which should
793        return a list of extensions that the factory handles (like
794        ``['.ht']``).  The special extension ``.*`` will match any
795        file if no other factory is found.  See `ServletFactory`
796        for more information.
797        """
798
799        self._factories.append(factory)
800        for ext in factory.extensions():
801            assert ext not in self._factoryExtensions, (
802                "Extension %s for factory %s was already used by factory %s"
803                % (repr(ext), factory.__name__,
804                    self._factoryExtensions[ext].__name__))
805            self._factoryExtensions[ext] = factory
806
807    def factoryForFile(self, path):
808        """Get a factory for a filename."""
809        ext = os.path.splitext(path)[1]
810        if ext in self._factoryExtensions:
811            return self._factoryExtensions[ext]
812        if '.*' in self._factoryExtensions:
813            return self._factoryExtensions['.*']
814        raise HTTPNotFound
815
816    def servletForFile(self, trans, path):
817        """Get a servlet for a filename and transaction.
818
819        Uses `factoryForFile` to find the factory, which
820        creates the servlet.
821        """
822        factory = self.factoryForFile(path)
823        return factory.servletForTransaction(trans)
824
825ServletFactoryManager = ServletFactoryManagerClass()
826
827
828## Global Init ##
829
830def initApp(app):
831    """Initialize the application.
832
833    Installs the proper servlet factories, and gets some settings from
834    Application.config. Also saves the application in _globalApplication
835    for future calls to the application() function.
836
837    This needs to be called before any of the URLParser-derived classes
838    are instantiated.
839    """
840    global _globalApplication
841    _globalApplication = app
842    from UnknownFileTypeServlet import UnknownFileTypeServletFactory
843    from ServletFactory import PythonServletFactory
844
845    ServletFactoryManager.reset()
846    for factory in [UnknownFileTypeServletFactory, PythonServletFactory]:
847        ServletFactoryManager.addServletFactory(factory(app))
848
849    initParser(app)
850
851
852def initParser(app):
853    """Initialize the FileParser Class."""
854    cls = _FileParser
855    cls._app = app
856    cls._imp = app._imp
857    cls._contexts = app.contexts
858    cls._filesToHideRegexes = []
859    cls._filesToServeRegexes = []
860    from fnmatch import translate as fnTranslate
861    for pattern in app.setting('FilesToHide'):
862        cls._filesToHideRegexes.append(re.compile(fnTranslate(pattern)))
863    for pattern in app.setting('FilesToServe'):
864        cls._filesToServeRegexes.append(re.compile(fnTranslate(pattern)))
865    cls._toIgnore = app.setting('ExtensionsToIgnore')
866    cls._toServe = app.setting('ExtensionsToServe')
867    cls._useCascading = app.setting('UseCascadingExtensions')
868    cls._cascadeOrder = app.setting('ExtensionCascadeOrder')
869    cls._directoryFile = app.setting('DirectoryFile')
870    cls._extraPathInfo = app.setting('ExtraPathInfo')
871