1
2
3
4 """
5 This file is part of the web2py Web Framework
6 Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 gluon.rewrite parses incoming URLs and formats outgoing URLs for gluon.html.URL.
10
11 In addition, it rewrites both incoming and outgoing URLs based on the (optional) user-supplied routes.py,
12 which also allows for rewriting of certain error messages.
13
14 routes.py supports two styles of URL rewriting, depending on whether 'routers' is defined.
15 Refer to router.example.py and routes.example.py for additional documentation.
16
17 """
18
19 import os
20 import re
21 import logging
22 import traceback
23 import threading
24 import urllib
25 from storage import Storage, List
26 from http import HTTP
27 from fileutils import abspath, read_file
28 from settings import global_settings
29
30 logger = logging.getLogger('web2py.rewrite')
31
32 thread = threading.local()
33
35 "return new copy of default base router"
36 router = Storage(
37 default_application = 'init',
38 applications = 'ALL',
39 default_controller = 'default',
40 controllers = 'DEFAULT',
41 default_function = 'index',
42 functions = None,
43 default_language = None,
44 languages = None,
45 root_static = ['favicon.ico', 'robots.txt'],
46 domains = None,
47 exclusive_domain = False,
48 map_hyphen = False,
49 acfe_match = r'\w+$',
50 file_match = r'(\w+[-=./]?)+$',
51 args_match = r'([\w@ -]+[=.]?)*$',
52 )
53 return router
54
56 "return new copy of default parameters"
57 p = Storage()
58 p.name = app or "BASE"
59 p.default_application = app or "init"
60 p.default_controller = "default"
61 p.default_function = "index"
62 p.routes_app = []
63 p.routes_in = []
64 p.routes_out = []
65 p.routes_onerror = []
66 p.routes_apps_raw = []
67 p.error_handler = None
68 p.error_message = '<html><body><h1>%s</h1></body></html>'
69 p.error_message_ticket = \
70 '<html><body><h1>Internal error</h1>Ticket issued: <a href="/admin/default/ticket/%(ticket)s" target="_blank">%(ticket)s</a></body><!-- this is junk text else IE does not display the page: '+('x'*512)+' //--></html>'
71 p.routers = None
72 return p
73
74 params_apps = dict()
75 params = _params_default(app=None)
76 thread.routes = params
77 routers = None
78
79 ROUTER_KEYS = set(('default_application', 'applications', 'default_controller', 'controllers',
80 'default_function', 'functions', 'default_language', 'languages',
81 'domain', 'domains', 'root_static', 'path_prefix',
82 'exclusive_domain', 'map_hyphen', 'map_static',
83 'acfe_match', 'file_match', 'args_match'))
84
85 ROUTER_BASE_KEYS = set(('applications', 'default_application', 'domains', 'path_prefix'))
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
108
109 -def url_out(request, env, application, controller, function, args, other, scheme, host, port):
110 "assemble and rewrite outgoing URL"
111 if routers:
112 acf = map_url_out(request, env, application, controller, function, args, other, scheme, host, port)
113 url = '%s%s' % (acf, other)
114 else:
115 url = '/%s/%s/%s%s' % (application, controller, function, other)
116 url = regex_filter_out(url, env)
117
118
119
120
121 if scheme or port is not None:
122 if host is None:
123 host = True
124 if not scheme or scheme is True:
125 if request and request.env:
126 scheme = request.env.get('WSGI_URL_SCHEME', 'http').lower()
127 else:
128 scheme = 'http'
129 if host is not None:
130 if host is True:
131 host = request.env.http_host
132 if host:
133 if port is None:
134 port = ''
135 else:
136 port = ':%s' % port
137 url = '%s://%s%s%s' % (scheme, host, port, url)
138 return url
139
141 "called from main.wsgibase to rewrite the http response"
142 status = int(str(http_object.status).split()[0])
143 if status>399 and thread.routes.routes_onerror:
144 keys=set(('%s/%s' % (request.application, status),
145 '%s/*' % (request.application),
146 '*/%s' % (status),
147 '*/*'))
148 for (key,redir) in thread.routes.routes_onerror:
149 if key in keys:
150 if redir == '!':
151 break
152 elif '?' in redir:
153 url = '%s&code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \
154 (redir,status,ticket,request.env.request_uri,request.url)
155 else:
156 url = '%s?code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \
157 (redir,status,ticket,request.env.request_uri,request.url)
158 return HTTP(303,
159 'You are being redirected <a href="%s">here</a>' % url,
160 Location=url)
161 return http_object
162
163
164 -def load(routes='routes.py', app=None, data=None, rdict=None):
165 """
166 load: read (if file) and parse routes
167 store results in params
168 (called from main.py at web2py initialization time)
169 If data is present, it's used instead of the routes.py contents.
170 If rdict is present, it must be a dict to be used for routers (unit test)
171 """
172 global params
173 global routers
174 if app is None:
175
176 global params_apps
177 params_apps = dict()
178 params = _params_default(app=None)
179 thread.routes = params
180 routers = None
181
182 if isinstance(rdict, dict):
183 symbols = dict(routers=rdict)
184 path = 'rdict'
185 else:
186 if data is not None:
187 path = 'routes'
188 else:
189 if app is None:
190 path = abspath(routes)
191 else:
192 path = abspath('applications', app, routes)
193 if not os.path.exists(path):
194 return
195 data = read_file(path).replace('\r\n','\n')
196
197 symbols = {}
198 try:
199 exec (data + '\n') in symbols
200 except SyntaxError, e:
201 logger.error(
202 '%s has a syntax error and will not be loaded\n' % path
203 + traceback.format_exc())
204 raise e
205
206 p = _params_default(app)
207
208 for sym in ('routes_app', 'routes_in', 'routes_out'):
209 if sym in symbols:
210 for (k, v) in symbols[sym]:
211 p[sym].append(compile_regex(k, v))
212 for sym in ('routes_onerror', 'routes_apps_raw',
213 'error_handler','error_message', 'error_message_ticket',
214 'default_application','default_controller', 'default_function'):
215 if sym in symbols:
216 p[sym] = symbols[sym]
217 if 'routers' in symbols:
218 p.routers = Storage(symbols['routers'])
219 for key in p.routers:
220 if isinstance(p.routers[key], dict):
221 p.routers[key] = Storage(p.routers[key])
222
223 if app is None:
224 params = p
225 thread.routes = params
226
227
228
229 routers = params.routers
230 if isinstance(routers, dict):
231 routers = Storage(routers)
232 if routers is not None:
233 router = _router_default()
234 if routers.BASE:
235 router.update(routers.BASE)
236 routers.BASE = router
237
238
239
240
241
242 all_apps = []
243 for appname in [app for app in os.listdir(abspath('applications')) if not app.startswith('.')]:
244 if os.path.isdir(abspath('applications', appname)) and \
245 os.path.isdir(abspath('applications', appname, 'controllers')):
246 all_apps.append(appname)
247 if routers:
248 router = Storage(routers.BASE)
249 if appname in routers:
250 for key in routers[appname].keys():
251 if key in ROUTER_BASE_KEYS:
252 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, appname)
253 router.update(routers[appname])
254 routers[appname] = router
255 if os.path.exists(abspath('applications', appname, routes)):
256 load(routes, appname)
257
258 if routers:
259 load_routers(all_apps)
260
261 else:
262 params_apps[app] = p
263 if routers and p.routers:
264 if app in p.routers:
265 routers[app].update(p.routers[app])
266
267 logger.debug('URL rewrite is on. configuration in %s' % path)
268
269
270 regex_at = re.compile(r'(?<!\\)\$[a-zA-Z]\w*')
271 regex_anything = re.compile(r'(?<!\\)\$anything')
272
274 """
275 Preprocess and compile the regular expressions in routes_app/in/out
276
277 The resulting regex will match a pattern of the form:
278
279 [remote address]:[protocol]://[host]:[method] [path]
280
281 We allow abbreviated regexes on input; here we try to complete them.
282 """
283 k0 = k
284
285 if not k[0] == '^':
286 k = '^%s' % k
287 if not k[-1] == '$':
288 k = '%s$' % k
289
290 if k.find(':') < 0:
291
292 k = '^.*?:https?://[^:/]+:[a-z]+ %s' % k[1:]
293
294 if k.find('://') < 0:
295 i = k.find(':/')
296 if i < 0:
297 raise SyntaxError, "routes pattern syntax error: path needs leading '/' [%s]" % k0
298 k = r'%s:https?://[^:/]+:[a-z]+ %s' % (k[:i], k[i+1:])
299
300 for item in regex_anything.findall(k):
301 k = k.replace(item, '(?P<anything>.*)')
302
303 for item in regex_at.findall(k):
304 k = k.replace(item, r'(?P<%s>\w+)' % item[1:])
305
306 for item in regex_at.findall(v):
307 v = v.replace(item, r'\g<%s>' % item[1:])
308 return (re.compile(k, re.DOTALL), v)
309
311 "load-time post-processing of routers"
312
313 for app in routers.keys():
314
315 if app not in all_apps:
316 all_apps.append(app)
317 router = Storage(routers.BASE)
318 if app != 'BASE':
319 for key in routers[app].keys():
320 if key in ROUTER_BASE_KEYS:
321 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, app)
322 router.update(routers[app])
323 routers[app] = router
324 router = routers[app]
325 for key in router.keys():
326 if key not in ROUTER_KEYS:
327 raise SyntaxError, "unknown key '%s' in router '%s'" % (key, app)
328 if not router.controllers:
329 router.controllers = set()
330 elif not isinstance(router.controllers, str):
331 router.controllers = set(router.controllers)
332 if router.functions:
333 router.functions = set(router.functions)
334 else:
335 router.functions = set()
336 if router.languages:
337 router.languages = set(router.languages)
338 else:
339 router.languages = set()
340 if app != 'BASE':
341 for base_only in ROUTER_BASE_KEYS:
342 router.pop(base_only, None)
343 if 'domain' in router:
344 routers.BASE.domains[router.domain] = app
345 if isinstance(router.controllers, str) and router.controllers == 'DEFAULT':
346 router.controllers = set()
347 if os.path.isdir(abspath('applications', app)):
348 cpath = abspath('applications', app, 'controllers')
349 for cname in os.listdir(cpath):
350 if os.path.isfile(abspath(cpath, cname)) and cname.endswith('.py'):
351 router.controllers.add(cname[:-3])
352 if router.controllers:
353 router.controllers.add('static')
354 router.controllers.add(router.default_controller)
355 if router.functions:
356 router.functions.add(router.default_function)
357
358 if isinstance(routers.BASE.applications, str) and routers.BASE.applications == 'ALL':
359 routers.BASE.applications = list(all_apps)
360 if routers.BASE.applications:
361 routers.BASE.applications = set(routers.BASE.applications)
362 else:
363 routers.BASE.applications = set()
364
365 for app in routers.keys():
366
367 router = routers[app]
368 router.name = app
369
370 router._acfe_match = re.compile(router.acfe_match)
371 router._file_match = re.compile(router.file_match)
372 if router.args_match:
373 router._args_match = re.compile(router.args_match)
374
375 if router.path_prefix:
376 if isinstance(router.path_prefix, str):
377 router.path_prefix = router.path_prefix.strip('/').split('/')
378
379
380
381
382
383
384
385 domains = dict()
386 if routers.BASE.domains:
387 for (domain, app) in [(d.strip(':'), a.strip('/')) for (d, a) in routers.BASE.domains.items()]:
388 port = None
389 if ':' in domain:
390 (domain, port) = domain.split(':')
391 ctlr = None
392 if '/' in app:
393 (app, ctlr) = app.split('/')
394 if app not in all_apps and app not in routers:
395 raise SyntaxError, "unknown app '%s' in domains" % app
396 domains[(domain, port)] = (app, ctlr)
397 routers.BASE.domains = domains
398
399 -def regex_uri(e, regexes, tag, default=None):
400 "filter incoming URI against a list of regexes"
401 path = e['PATH_INFO']
402 host = e.get('HTTP_HOST', 'localhost').lower()
403 i = host.find(':')
404 if i > 0:
405 host = host[:i]
406 key = '%s:%s://%s:%s %s' % \
407 (e.get('REMOTE_ADDR','localhost'),
408 e.get('WSGI_URL_SCHEME', 'http').lower(), host,
409 e.get('REQUEST_METHOD', 'get').lower(), path)
410 for (regex, value) in regexes:
411 if regex.match(key):
412 rewritten = regex.sub(value, key)
413 logger.debug('%s: [%s] [%s] -> %s' % (tag, key, value, rewritten))
414 return rewritten
415 logger.debug('%s: [%s] -> %s (not rewritten)' % (tag, key, default))
416 return default
417
434
436 "regex rewrite incoming URL"
437 query = e.get('QUERY_STRING', None)
438 e['WEB2PY_ORIGINAL_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
439 if thread.routes.routes_in:
440 path = regex_uri(e, thread.routes.routes_in, "routes_in", e['PATH_INFO'])
441 items = path.split('?', 1)
442 e['PATH_INFO'] = items[0]
443 if len(items) > 1:
444 if query:
445 query = items[1] + '&' + query
446 else:
447 query = items[1]
448 e['QUERY_STRING'] = query
449 e['REQUEST_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
450 return e
451
452
453
454
455 regex_space = re.compile('(\+|\s|%20)+')
456
457
458
459
460
461
462
463
464
465
466
467 regex_static = re.compile(r'''
468 (^ # static pages
469 /(?P<b> \w+) # b=app
470 /static # /b/static
471 /(?P<x> (\w[\-\=\./]?)* ) # x=file
472 $)
473 ''', re.X)
474
475 regex_url = re.compile(r'''
476 (^( # (/a/c/f.e/s)
477 /(?P<a> [\w\s+]+ ) # /a=app
478 ( # (/c.f.e/s)
479 /(?P<c> [\w\s+]+ ) # /a/c=controller
480 ( # (/f.e/s)
481 /(?P<f> [\w\s+]+ ) # /a/c/f=function
482 ( # (.e)
483 \.(?P<e> [\w\s+]+ ) # /a/c/f.e=extension
484 )?
485 ( # (/s)
486 /(?P<r> # /a/c/f.e/r=raw_args
487 .*
488 )
489 )?
490 )?
491 )?
492 )?
493 /?$)
494 ''', re.X)
495
496 regex_args = re.compile(r'''
497 (^
498 (?P<s>
499 ( [\w@/-][=.]? )* # s=args
500 )?
501 /?$) # trailing slash
502 ''', re.X)
503
505 "rewrite and parse incoming URL"
506
507
508
509
510
511
512
513 regex_select(env=environ, request=request)
514
515 if thread.routes.routes_in:
516 environ = regex_filter_in(environ)
517
518 for (key, value) in environ.items():
519 request.env[key.lower().replace('.', '_')] = value
520
521 path = request.env.path_info.replace('\\', '/')
522
523
524
525
526
527 match = regex_static.match(regex_space.sub('_', path))
528 if match and match.group('x'):
529 static_file = os.path.join(request.env.applications_parent,
530 'applications', match.group('b'),
531 'static', match.group('x'))
532 return (static_file, environ)
533
534
535
536
537
538 path = re.sub('%20', ' ', path)
539 match = regex_url.match(path)
540 if not match or match.group('c') == 'static':
541 raise HTTP(400,
542 thread.routes.error_message % 'invalid request',
543 web2py_error='invalid path')
544
545 request.application = \
546 regex_space.sub('_', match.group('a') or thread.routes.default_application)
547 request.controller = \
548 regex_space.sub('_', match.group('c') or thread.routes.default_controller)
549 request.function = \
550 regex_space.sub('_', match.group('f') or thread.routes.default_function)
551 group_e = match.group('e')
552 request.raw_extension = group_e and regex_space.sub('_', group_e) or None
553 request.extension = request.raw_extension or 'html'
554 request.raw_args = match.group('r')
555 request.args = List([])
556 if request.application in thread.routes.routes_apps_raw:
557
558 request.args = None
559 elif request.raw_args:
560 match = regex_args.match(request.raw_args.replace(' ', '_'))
561 if match:
562 group_s = match.group('s')
563 request.args = \
564 List((group_s and group_s.split('/')) or [])
565 if request.args and request.args[-1] == '':
566 request.args.pop()
567 else:
568 raise HTTP(400,
569 thread.routes.error_message % 'invalid request',
570 web2py_error='invalid path (args)')
571 return (None, environ)
572
573
575 "regex rewrite outgoing URL"
576 if not hasattr(thread, 'routes'):
577 regex_select()
578 if routers:
579 return url
580 if thread.routes.routes_out:
581 items = url.split('?', 1)
582 if e:
583 host = e.get('http_host', 'localhost').lower()
584 i = host.find(':')
585 if i > 0:
586 host = host[:i]
587 items[0] = '%s:%s://%s:%s %s' % \
588 (e.get('remote_addr', ''),
589 e.get('wsgi_url_scheme', 'http').lower(), host,
590 e.get('request_method', 'get').lower(), items[0])
591 else:
592 items[0] = ':http://localhost:get %s' % items[0]
593 for (regex, value) in thread.routes.routes_out:
594 if regex.match(items[0]):
595 rewritten = '?'.join([regex.sub(value, items[0])] + items[1:])
596 logger.debug('routes_out: [%s] -> %s' % (url, rewritten))
597 return rewritten
598 logger.debug('routes_out: [%s] not rewritten' % url)
599 return url
600
601
602 -def filter_url(url, method='get', remote='0.0.0.0', out=False, app=False, lang=None,
603 domain=(None,None), env=False, scheme=None, host=None, port=None):
604 "doctest/unittest interface to regex_filter_in() and regex_filter_out()"
605 regex_url = re.compile(r'^(?P<scheme>http|https|HTTP|HTTPS)\://(?P<host>[^/]*)(?P<uri>.*)')
606 match = regex_url.match(url)
607 urlscheme = match.group('scheme').lower()
608 urlhost = match.group('host').lower()
609 uri = match.group('uri')
610 k = uri.find('?')
611 if k < 0:
612 k = len(uri)
613 (path_info, query_string) = (uri[:k], uri[k+1:])
614 path_info = urllib.unquote(path_info)
615 e = {
616 'REMOTE_ADDR': remote,
617 'REQUEST_METHOD': method,
618 'WSGI_URL_SCHEME': urlscheme,
619 'HTTP_HOST': urlhost,
620 'REQUEST_URI': uri,
621 'PATH_INFO': path_info,
622 'QUERY_STRING': query_string,
623
624 'remote_addr': remote,
625 'request_method': method,
626 'wsgi_url_scheme': urlscheme,
627 'http_host': urlhost
628 }
629
630 request = Storage()
631 e["applications_parent"] = global_settings.applications_parent
632 request.env = Storage(e)
633 request.uri_language = lang
634
635
636
637 if app:
638 if routers:
639 return map_url_in(request, e, app=True)
640 return regex_select(e)
641
642
643
644 if out:
645 (request.env.domain_application, request.env.domain_controller) = domain
646 items = path_info.lstrip('/').split('/')
647 if items[-1] == '':
648 items.pop()
649 assert len(items) >= 3, "at least /a/c/f is required"
650 a = items.pop(0)
651 c = items.pop(0)
652 f = items.pop(0)
653 if not routers:
654 return regex_filter_out(uri, e)
655 acf = map_url_out(request, None, a, c, f, items, None, scheme, host, port)
656 if items:
657 url = '%s/%s' % (acf, '/'.join(items))
658 if items[-1] == '':
659 url += '/'
660 else:
661 url = acf
662 if query_string:
663 url += '?' + query_string
664 return url
665
666
667
668 (static, e) = url_in(request, e)
669 if static:
670 return static
671 result = "/%s/%s/%s" % (request.application, request.controller, request.function)
672 if request.extension and request.extension != 'html':
673 result += ".%s" % request.extension
674 if request.args:
675 result += " %s" % request.args
676 if e['QUERY_STRING']:
677 result += " ?%s" % e['QUERY_STRING']
678 if request.uri_language:
679 result += " (%s)" % request.uri_language
680 if env:
681 return request.env
682 return result
683
684
685 -def filter_err(status, application='app', ticket='tkt'):
686 "doctest/unittest interface to routes_onerror"
687 if status > 399 and thread.routes.routes_onerror:
688 keys = set(('%s/%s' % (application, status),
689 '%s/*' % (application),
690 '*/%s' % (status),
691 '*/*'))
692 for (key,redir) in thread.routes.routes_onerror:
693 if key in keys:
694 if redir == '!':
695 break
696 elif '?' in redir:
697 url = redir + '&' + 'code=%s&ticket=%s' % (status,ticket)
698 else:
699 url = redir + '?' + 'code=%s&ticket=%s' % (status,ticket)
700 return url
701 return status
702
703
704
706 "logic for mapping incoming URLs"
707
708 - def __init__(self, request=None, env=None):
709 "initialize a map-in object"
710 self.request = request
711 self.env = env
712
713 self.router = None
714 self.application = None
715 self.language = None
716 self.controller = None
717 self.function = None
718 self.extension = 'html'
719
720 self.controllers = set()
721 self.functions = set()
722 self.languages = set()
723 self.default_language = None
724 self.map_hyphen = False
725 self.exclusive_domain = False
726
727 path = self.env['PATH_INFO']
728 self.query = self.env.get('QUERY_STRING', None)
729 path = path.lstrip('/')
730 self.env['PATH_INFO'] = '/' + path
731 self.env['WEB2PY_ORIGINAL_URI'] = self.env['PATH_INFO'] + (self.query and ('?' + self.query) or '')
732
733
734
735
736 if path.endswith('/'):
737 path = path[:-1]
738 self.args = List(path and path.split('/') or [])
739
740
741 self.remote_addr = self.env.get('REMOTE_ADDR','localhost')
742 self.scheme = self.env.get('WSGI_URL_SCHEME', 'http').lower()
743 self.method = self.env.get('REQUEST_METHOD', 'get').lower()
744 self.host = self.env.get('HTTP_HOST')
745 self.port = None
746 if not self.host:
747 self.host = self.env.get('SERVER_NAME')
748 self.port = self.env.get('SERVER_PORT')
749 if not self.host:
750 self.host = 'localhost'
751 self.port = '80'
752 if ':' in self.host:
753 (self.host, self.port) = self.host.split(':')
754 if not self.port:
755 if self.scheme == 'https':
756 self.port = '443'
757 else:
758 self.port = '80'
759
761 "strip path prefix, if present in its entirety"
762 prefix = routers.BASE.path_prefix
763 if prefix:
764 prefixlen = len(prefix)
765 if prefixlen > len(self.args):
766 return
767 for i in xrange(prefixlen):
768 if prefix[i] != self.args[i]:
769 return
770 self.args = List(self.args[prefixlen:])
771
773 "determine application name"
774 base = routers.BASE
775 self.domain_application = None
776 self.domain_controller = None
777 arg0 = self.harg0
778 if base.applications and arg0 in base.applications:
779 self.application = arg0
780 elif (self.host, self.port) in base.domains:
781 (self.application, self.domain_controller) = base.domains[(self.host, self.port)]
782 self.env['domain_application'] = self.application
783 self.env['domain_controller'] = self.domain_controller
784 elif (self.host, None) in base.domains:
785 (self.application, self.domain_controller) = base.domains[(self.host, None)]
786 self.env['domain_application'] = self.application
787 self.env['domain_controller'] = self.domain_controller
788 elif arg0 and not base.applications:
789 self.application = arg0
790 else:
791 self.application = base.default_application or ''
792 self.pop_arg_if(self.application == arg0)
793
794 if not base._acfe_match.match(self.application):
795 raise HTTP(400, thread.routes.error_message % 'invalid request',
796 web2py_error="invalid application: '%s'" % self.application)
797
798 if self.application not in routers and \
799 (self.application != thread.routes.default_application or self.application == 'welcome'):
800 raise HTTP(400, thread.routes.error_message % 'invalid request',
801 web2py_error="unknown application: '%s'" % self.application)
802
803
804
805 logger.debug("select application=%s" % self.application)
806 self.request.application = self.application
807 if self.application not in routers:
808 self.router = routers.BASE
809 else:
810 self.router = routers[self.application]
811 self.controllers = self.router.controllers
812 self.default_controller = self.domain_controller or self.router.default_controller
813 self.functions = self.router.functions
814 self.languages = self.router.languages
815 self.default_language = self.router.default_language
816 self.map_hyphen = self.router.map_hyphen
817 self.exclusive_domain = self.router.exclusive_domain
818 self._acfe_match = self.router._acfe_match
819 self._file_match = self.router._file_match
820 self._args_match = self.router._args_match
821
823 '''
824 handle root-static files (no hyphen mapping)
825
826 a root-static file is one whose incoming URL expects it to be at the root,
827 typically robots.txt & favicon.ico
828 '''
829 if len(self.args) == 1 and self.arg0 in self.router.root_static:
830 self.controller = self.request.controller = 'static'
831 root_static_file = os.path.join(self.request.env.applications_parent,
832 'applications', self.application,
833 self.controller, self.arg0)
834 logger.debug("route: root static=%s" % root_static_file)
835 return root_static_file
836 return None
837
849
851 "identify controller"
852
853
854 arg0 = self.harg0
855 if not arg0 or (self.controllers and arg0 not in self.controllers):
856 self.controller = self.default_controller or ''
857 else:
858 self.controller = arg0
859 self.pop_arg_if(arg0 == self.controller)
860 logger.debug("route: controller=%s" % self.controller)
861 if not self.router._acfe_match.match(self.controller):
862 raise HTTP(400, thread.routes.error_message % 'invalid request',
863 web2py_error='invalid controller')
864
866 '''
867 handle static files
868 file_match but no hyphen mapping
869 '''
870 if self.controller != 'static':
871 return None
872 file = '/'.join(self.args)
873 if not self.router._file_match.match(file):
874 raise HTTP(400, thread.routes.error_message % 'invalid request',
875 web2py_error='invalid static file')
876
877
878
879
880
881 if self.language:
882 static_file = os.path.join(self.request.env.applications_parent,
883 'applications', self.application,
884 'static', self.language, file)
885 if not self.language or not os.path.isfile(static_file):
886 static_file = os.path.join(self.request.env.applications_parent,
887 'applications', self.application,
888 'static', file)
889 logger.debug("route: static=%s" % static_file)
890 return static_file
891
893 "handle function.extension"
894 arg0 = self.harg0
895 if not arg0 or self.functions and arg0 not in self.functions and self.controller == self.default_controller:
896 self.function = self.router.default_function or ""
897 self.pop_arg_if(arg0 and self.function == arg0)
898 else:
899 func_ext = arg0.split('.')
900 if len(func_ext) > 1:
901 self.function = func_ext[0]
902 self.extension = func_ext[-1]
903 else:
904 self.function = arg0
905 self.pop_arg_if(True)
906 logger.debug("route: function.ext=%s.%s" % (self.function, self.extension))
907
908 if not self.router._acfe_match.match(self.function):
909 raise HTTP(400, thread.routes.error_message % 'invalid request',
910 web2py_error='invalid function')
911 if self.extension and not self.router._acfe_match.match(self.extension):
912 raise HTTP(400, thread.routes.error_message % 'invalid request',
913 web2py_error='invalid extension')
914
916 '''
917 check args against validation pattern
918 '''
919 for arg in self.args:
920 if not self.router._args_match.match(arg):
921 raise HTTP(400, thread.routes.error_message % 'invalid request',
922 web2py_error='invalid arg <%s>' % arg)
923
925 '''
926 update request from self
927 build env.request_uri
928 make lower-case versions of http headers in env
929 '''
930 self.request.application = self.application
931 self.request.controller = self.controller
932 self.request.function = self.function
933 self.request.extension = self.extension
934 self.request.args = self.args
935 if self.language:
936 self.request.uri_language = self.language
937 uri = '/%s/%s/%s' % (self.application, self.controller, self.function)
938 if self.map_hyphen:
939 uri = uri.replace('_', '-')
940 if self.extension != 'html':
941 uri += '.' + self.extension
942 if self.language:
943 uri = '/%s%s' % (self.language, uri)
944 uri += self.args and urllib.quote('/' + '/'.join([str(x) for x in self.args])) or ''
945 uri += (self.query and ('?' + self.query) or '')
946 self.env['REQUEST_URI'] = uri
947 for (key, value) in self.env.items():
948 self.request.env[key.lower().replace('.', '_')] = value
949
950 @property
952 "return first arg"
953 return self.args(0)
954
955 @property
957 "return first arg with optional hyphen mapping"
958 if self.map_hyphen and self.args(0):
959 return self.args(0).replace('-', '_')
960 return self.args(0)
961
963 "conditionally remove first arg and return new first arg"
964 if dopop:
965 self.args.pop(0)
966
968 "logic for mapping outgoing URLs"
969
970 - def __init__(self, request, env, application, controller, function, args, other, scheme, host, port):
971 "initialize a map-out object"
972 self.default_application = routers.BASE.default_application
973 if application in routers:
974 self.router = routers[application]
975 else:
976 self.router = routers.BASE
977 self.request = request
978 self.env = env
979 self.application = application
980 self.controller = controller
981 self.function = function
982 self.args = args
983 self.other = other
984 self.scheme = scheme
985 self.host = host
986 self.port = port
987
988 self.applications = routers.BASE.applications
989 self.controllers = self.router.controllers
990 self.functions = self.router.functions
991 self.languages = self.router.languages
992 self.default_language = self.router.default_language
993 self.exclusive_domain = self.router.exclusive_domain
994 self.map_hyphen = self.router.map_hyphen
995 self.map_static = self.router.map_static
996 self.path_prefix = routers.BASE.path_prefix
997
998 self.domain_application = request and self.request.env.domain_application
999 self.domain_controller = request and self.request.env.domain_controller
1000 self.default_function = self.router.default_function
1001
1002 if (self.router.exclusive_domain and self.domain_application and self.domain_application != self.application and not self.host):
1003 raise SyntaxError, 'cross-domain conflict: must specify host'
1004
1005 lang = request and request.uri_language
1006 if lang and self.languages and lang in self.languages:
1007 self.language = lang
1008 else:
1009 self.language = None
1010
1011 self.omit_application = False
1012 self.omit_language = False
1013 self.omit_controller = False
1014 self.omit_function = False
1015
1017 "omit language if possible"
1018
1019 if not self.language or self.language == self.default_language:
1020 self.omit_language = True
1021
1023 "omit what we can of a/c/f"
1024
1025 router = self.router
1026
1027
1028
1029 if not self.args and self.function == router.default_function:
1030 self.omit_function = True
1031 if self.controller == router.default_controller:
1032 self.omit_controller = True
1033 if self.application == self.default_application:
1034 self.omit_application = True
1035
1036
1037
1038
1039 default_application = self.domain_application or self.default_application
1040 if self.application == default_application:
1041 self.omit_application = True
1042
1043
1044
1045 default_controller = ((self.application == self.domain_application) and self.domain_controller) or router.default_controller or ''
1046 if self.controller == default_controller:
1047 self.omit_controller = True
1048
1049
1050
1051 if self.functions and self.function == self.default_function and self.omit_controller:
1052 self.omit_function = True
1053
1054
1055
1056
1057
1058 if self.omit_language:
1059 if not self.applications or self.controller in self.applications:
1060 self.omit_application = False
1061 if self.omit_application:
1062 if not self.applications or self.function in self.applications:
1063 self.omit_controller = False
1064 if not self.controllers or self.function in self.controllers:
1065 self.omit_controller = False
1066 if self.args:
1067 if self.args[0] in self.functions or self.args[0] in self.controllers or self.args[0] in self.applications:
1068 self.omit_function = False
1069 if self.omit_controller:
1070 if self.function in self.controllers or self.function in self.applications:
1071 self.omit_controller = False
1072 if self.omit_application:
1073 if self.controller in self.applications:
1074 self.omit_application = False
1075
1076
1077
1078
1079 if self.controller == 'static' or self.controller.startswith('static/'):
1080 if not self.map_static:
1081 self.omit_application = False
1082 if self.language:
1083 self.omit_language = False
1084 self.omit_controller = False
1085 self.omit_function = False
1086
1088 "build acf from components"
1089 acf = ''
1090 if self.map_hyphen:
1091 self.application = self.application.replace('_', '-')
1092 self.controller = self.controller.replace('_', '-')
1093 if self.controller != 'static' and not self.controller.startswith('static/'):
1094 self.function = self.function.replace('_', '-')
1095 if not self.omit_application:
1096 acf += '/' + self.application
1097 if not self.omit_language:
1098 acf += '/' + self.language
1099 if not self.omit_controller:
1100 acf += '/' + self.controller
1101 if not self.omit_function:
1102 acf += '/' + self.function
1103 if self.path_prefix:
1104 acf = '/' + '/'.join(self.path_prefix) + acf
1105 if self.args:
1106 return acf
1107 return acf or '/'
1108
1110 "convert components to /app/lang/controller/function"
1111
1112 if not routers:
1113 return None
1114 self.omit_lang()
1115 self.omit_acf()
1116 return self.build_acf()
1117
1118
1149
1150 -def map_url_out(request, env, application, controller, function, args, other, scheme, host, port):
1151 '''
1152 supply /a/c/f (or /a/lang/c/f) portion of outgoing url
1153
1154 The basic rule is that we can only make transformations
1155 that map_url_in can reverse.
1156
1157 Suppose that the incoming arguments are a,c,f,args,lang
1158 and that the router defaults are da, dc, df, dl.
1159
1160 We can perform these transformations trivially if args=[] and lang=None or dl:
1161
1162 /da/dc/df => /
1163 /a/dc/df => /a
1164 /a/c/df => /a/c
1165
1166 We would also like to be able to strip the default application or application/controller
1167 from URLs with function/args present, thus:
1168
1169 /da/c/f/args => /c/f/args
1170 /da/dc/f/args => /f/args
1171
1172 We use [applications] and [controllers] and [functions] to suppress ambiguous omissions.
1173
1174 We assume that language names do not collide with a/c/f names.
1175 '''
1176 map = MapUrlOut(request, env, application, controller, function, args, other, scheme, host, port)
1177 return map.acf()
1178
1180 "return a private copy of the effective router for the specified application"
1181 if not routers or appname not in routers:
1182 return None
1183 return Storage(routers[appname])
1184