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 """
142 called from main.wsgibase to rewrite the http response.
143 """
144 status = int(str(http_response.status).split()[0])
145 if status>=399 and thread.routes.routes_onerror:
146 keys=set(('%s/%s' % (request.application, status),
147 '%s/*' % (request.application),
148 '*/%s' % (status),
149 '*/*'))
150 for (key,uri) in thread.routes.routes_onerror:
151 if key in keys:
152 if uri == '!':
153
154 return http_response, environ
155 elif '?' in uri:
156 path_info, query_string = uri.split('?',1)
157 query_string += '&'
158 else:
159 path_info, query_string = uri, ''
160 query_string += \
161 'code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \
162 (status,ticket,request.env.request_uri,request.url)
163 if uri.startswith('http://') or uri.startswith('https://'):
164
165 url = path_info+'?'+query_string
166 message = 'You are being redirected <a href="%s">here</a>'
167 return HTTP(303, message % url, Location=url), environ
168 elif path_info!=environ['PATH_INFO']:
169
170 environ['PATH_INFO'] = path_info
171 environ['QUERY_STRING'] = query_string
172 return None, environ
173
174 return http_response, environ
175
177 "called from main.wsgibase to rewrite the http response"
178 status = int(str(http_object.status).split()[0])
179 if status>399 and thread.routes.routes_onerror:
180 keys=set(('%s/%s' % (request.application, status),
181 '%s/*' % (request.application),
182 '*/%s' % (status),
183 '*/*'))
184 for (key,redir) in thread.routes.routes_onerror:
185 if key in keys:
186 if redir == '!':
187 break
188 elif '?' in redir:
189 url = '%s&code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \
190 (redir,status,ticket,request.env.request_uri,request.url)
191 else:
192 url = '%s?code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \
193 (redir,status,ticket,request.env.request_uri,request.url)
194 return HTTP(303,
195 'You are being redirected <a href="%s">here</a>' % url,
196 Location=url)
197 return http_object
198
199
200 -def load(routes='routes.py', app=None, data=None, rdict=None):
201 """
202 load: read (if file) and parse routes
203 store results in params
204 (called from main.py at web2py initialization time)
205 If data is present, it's used instead of the routes.py contents.
206 If rdict is present, it must be a dict to be used for routers (unit test)
207 """
208 global params
209 global routers
210 if app is None:
211
212 global params_apps
213 params_apps = dict()
214 params = _params_default(app=None)
215 thread.routes = params
216 routers = None
217
218 if isinstance(rdict, dict):
219 symbols = dict(routers=rdict)
220 path = 'rdict'
221 else:
222 if data is not None:
223 path = 'routes'
224 else:
225 if app is None:
226 path = abspath(routes)
227 else:
228 path = abspath('applications', app, routes)
229 if not os.path.exists(path):
230 return
231 data = read_file(path).replace('\r\n','\n')
232
233 symbols = {}
234 try:
235 exec (data + '\n') in symbols
236 except SyntaxError, e:
237 logger.error(
238 '%s has a syntax error and will not be loaded\n' % path
239 + traceback.format_exc())
240 raise e
241
242 p = _params_default(app)
243
244 for sym in ('routes_app', 'routes_in', 'routes_out'):
245 if sym in symbols:
246 for (k, v) in symbols[sym]:
247 p[sym].append(compile_regex(k, v))
248 for sym in ('routes_onerror', 'routes_apps_raw',
249 'error_handler','error_message', 'error_message_ticket',
250 'default_application','default_controller', 'default_function'):
251 if sym in symbols:
252 p[sym] = symbols[sym]
253 if 'routers' in symbols:
254 p.routers = Storage(symbols['routers'])
255 for key in p.routers:
256 if isinstance(p.routers[key], dict):
257 p.routers[key] = Storage(p.routers[key])
258
259 if app is None:
260 params = p
261 thread.routes = params
262
263
264
265 routers = params.routers
266 if isinstance(routers, dict):
267 routers = Storage(routers)
268 if routers is not None:
269 router = _router_default()
270 if routers.BASE:
271 router.update(routers.BASE)
272 routers.BASE = router
273
274
275
276
277
278 all_apps = []
279 for appname in [app for app in os.listdir(abspath('applications')) if not app.startswith('.')]:
280 if os.path.isdir(abspath('applications', appname)) and \
281 os.path.isdir(abspath('applications', appname, 'controllers')):
282 all_apps.append(appname)
283 if routers:
284 router = Storage(routers.BASE)
285 if appname in routers:
286 for key in routers[appname].keys():
287 if key in ROUTER_BASE_KEYS:
288 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, appname)
289 router.update(routers[appname])
290 routers[appname] = router
291 if os.path.exists(abspath('applications', appname, routes)):
292 load(routes, appname)
293
294 if routers:
295 load_routers(all_apps)
296
297 else:
298 params_apps[app] = p
299 if routers and p.routers:
300 if app in p.routers:
301 routers[app].update(p.routers[app])
302
303 logger.debug('URL rewrite is on. configuration in %s' % path)
304
305
306 regex_at = re.compile(r'(?<!\\)\$[a-zA-Z]\w*')
307 regex_anything = re.compile(r'(?<!\\)\$anything')
308
310 """
311 Preprocess and compile the regular expressions in routes_app/in/out
312
313 The resulting regex will match a pattern of the form:
314
315 [remote address]:[protocol]://[host]:[method] [path]
316
317 We allow abbreviated regexes on input; here we try to complete them.
318 """
319 k0 = k
320
321 if not k[0] == '^':
322 k = '^%s' % k
323 if not k[-1] == '$':
324 k = '%s$' % k
325
326 if k.find(':') < 0:
327
328 k = '^.*?:https?://[^:/]+:[a-z]+ %s' % k[1:]
329
330 if k.find('://') < 0:
331 i = k.find(':/')
332 if i < 0:
333 raise SyntaxError, "routes pattern syntax error: path needs leading '/' [%s]" % k0
334 k = r'%s:https?://[^:/]+:[a-z]+ %s' % (k[:i], k[i+1:])
335
336 for item in regex_anything.findall(k):
337 k = k.replace(item, '(?P<anything>.*)')
338
339 for item in regex_at.findall(k):
340 k = k.replace(item, r'(?P<%s>\w+)' % item[1:])
341
342 for item in regex_at.findall(v):
343 v = v.replace(item, r'\g<%s>' % item[1:])
344 return (re.compile(k, re.DOTALL), v)
345
347 "load-time post-processing of routers"
348
349 for app in routers.keys():
350
351 if app not in all_apps:
352 all_apps.append(app)
353 router = Storage(routers.BASE)
354 if app != 'BASE':
355 for key in routers[app].keys():
356 if key in ROUTER_BASE_KEYS:
357 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, app)
358 router.update(routers[app])
359 routers[app] = router
360 router = routers[app]
361 for key in router.keys():
362 if key not in ROUTER_KEYS:
363 raise SyntaxError, "unknown key '%s' in router '%s'" % (key, app)
364 if not router.controllers:
365 router.controllers = set()
366 elif not isinstance(router.controllers, str):
367 router.controllers = set(router.controllers)
368 if router.functions:
369 router.functions = set(router.functions)
370 else:
371 router.functions = set()
372 if router.languages:
373 router.languages = set(router.languages)
374 else:
375 router.languages = set()
376 if app != 'BASE':
377 for base_only in ROUTER_BASE_KEYS:
378 router.pop(base_only, None)
379 if 'domain' in router:
380 routers.BASE.domains[router.domain] = app
381 if isinstance(router.controllers, str) and router.controllers == 'DEFAULT':
382 router.controllers = set()
383 if os.path.isdir(abspath('applications', app)):
384 cpath = abspath('applications', app, 'controllers')
385 for cname in os.listdir(cpath):
386 if os.path.isfile(abspath(cpath, cname)) and cname.endswith('.py'):
387 router.controllers.add(cname[:-3])
388 if router.controllers:
389 router.controllers.add('static')
390 router.controllers.add(router.default_controller)
391 if router.functions:
392 router.functions.add(router.default_function)
393
394 if isinstance(routers.BASE.applications, str) and routers.BASE.applications == 'ALL':
395 routers.BASE.applications = list(all_apps)
396 if routers.BASE.applications:
397 routers.BASE.applications = set(routers.BASE.applications)
398 else:
399 routers.BASE.applications = set()
400
401 for app in routers.keys():
402
403 router = routers[app]
404 router.name = app
405
406 router._acfe_match = re.compile(router.acfe_match)
407 router._file_match = re.compile(router.file_match)
408 if router.args_match:
409 router._args_match = re.compile(router.args_match)
410
411 if router.path_prefix:
412 if isinstance(router.path_prefix, str):
413 router.path_prefix = router.path_prefix.strip('/').split('/')
414
415
416
417
418
419
420
421 domains = dict()
422 if routers.BASE.domains:
423 for (domain, app) in [(d.strip(':'), a.strip('/')) for (d, a) in routers.BASE.domains.items()]:
424 port = None
425 if ':' in domain:
426 (domain, port) = domain.split(':')
427 ctlr = None
428 if '/' in app:
429 (app, ctlr) = app.split('/')
430 if app not in all_apps and app not in routers:
431 raise SyntaxError, "unknown app '%s' in domains" % app
432 domains[(domain, port)] = (app, ctlr)
433 routers.BASE.domains = domains
434
435 -def regex_uri(e, regexes, tag, default=None):
436 "filter incoming URI against a list of regexes"
437 path = e['PATH_INFO']
438 host = e.get('HTTP_HOST', 'localhost').lower()
439 i = host.find(':')
440 if i > 0:
441 host = host[:i]
442 key = '%s:%s://%s:%s %s' % \
443 (e.get('REMOTE_ADDR','localhost'),
444 e.get('WSGI_URL_SCHEME', 'http').lower(), host,
445 e.get('REQUEST_METHOD', 'get').lower(), path)
446 for (regex, value) in regexes:
447 if regex.match(key):
448 rewritten = regex.sub(value, key)
449 logger.debug('%s: [%s] [%s] -> %s' % (tag, key, value, rewritten))
450 return rewritten
451 logger.debug('%s: [%s] -> %s (not rewritten)' % (tag, key, default))
452 return default
453
470
472 "regex rewrite incoming URL"
473 query = e.get('QUERY_STRING', None)
474 e['WEB2PY_ORIGINAL_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
475 if thread.routes.routes_in:
476 path = regex_uri(e, thread.routes.routes_in, "routes_in", e['PATH_INFO'])
477 items = path.split('?', 1)
478 e['PATH_INFO'] = items[0]
479 if len(items) > 1:
480 if query:
481 query = items[1] + '&' + query
482 else:
483 query = items[1]
484 e['QUERY_STRING'] = query
485 e['REQUEST_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '')
486 return e
487
488
489
490
491 regex_space = re.compile('(\+|\s|%20)+')
492
493
494
495
496
497
498
499
500
501
502
503 regex_static = re.compile(r'''
504 (^ # static pages
505 /(?P<b> \w+) # b=app
506 /static # /b/static
507 /(?P<x> (\w[\-\=\./]?)* ) # x=file
508 $)
509 ''', re.X)
510
511 regex_url = re.compile(r'''
512 (^( # (/a/c/f.e/s)
513 /(?P<a> [\w\s+]+ ) # /a=app
514 ( # (/c.f.e/s)
515 /(?P<c> [\w\s+]+ ) # /a/c=controller
516 ( # (/f.e/s)
517 /(?P<f> [\w\s+]+ ) # /a/c/f=function
518 ( # (.e)
519 \.(?P<e> [\w\s+]+ ) # /a/c/f.e=extension
520 )?
521 ( # (/s)
522 /(?P<r> # /a/c/f.e/r=raw_args
523 .*
524 )
525 )?
526 )?
527 )?
528 )?
529 /?$)
530 ''', re.X)
531
532 regex_args = re.compile(r'''
533 (^
534 (?P<s>
535 ( [\w@/-][=.]? )* # s=args
536 )?
537 /?$) # trailing slash
538 ''', re.X)
539
541 "rewrite and parse incoming URL"
542
543
544
545
546
547
548
549 regex_select(env=environ, request=request)
550
551 if thread.routes.routes_in:
552 environ = regex_filter_in(environ)
553
554 for (key, value) in environ.items():
555 request.env[key.lower().replace('.', '_')] = value
556
557 path = request.env.path_info.replace('\\', '/')
558
559
560
561
562
563 match = regex_static.match(regex_space.sub('_', path))
564 if match and match.group('x'):
565 static_file = os.path.join(request.env.applications_parent,
566 'applications', match.group('b'),
567 'static', match.group('x'))
568 return (static_file, environ)
569
570
571
572
573
574 path = re.sub('%20', ' ', path)
575 match = regex_url.match(path)
576 if not match or match.group('c') == 'static':
577 raise HTTP(400,
578 thread.routes.error_message % 'invalid request',
579 web2py_error='invalid path')
580
581 request.application = \
582 regex_space.sub('_', match.group('a') or thread.routes.default_application)
583 request.controller = \
584 regex_space.sub('_', match.group('c') or thread.routes.default_controller)
585 request.function = \
586 regex_space.sub('_', match.group('f') or thread.routes.default_function)
587 group_e = match.group('e')
588 request.raw_extension = group_e and regex_space.sub('_', group_e) or None
589 request.extension = request.raw_extension or 'html'
590 request.raw_args = match.group('r')
591 request.args = List([])
592 if request.application in thread.routes.routes_apps_raw:
593
594 request.args = None
595 elif request.raw_args:
596 match = regex_args.match(request.raw_args.replace(' ', '_'))
597 if match:
598 group_s = match.group('s')
599 request.args = \
600 List((group_s and group_s.split('/')) or [])
601 if request.args and request.args[-1] == '':
602 request.args.pop()
603 else:
604 raise HTTP(400,
605 thread.routes.error_message % 'invalid request',
606 web2py_error='invalid path (args)')
607 return (None, environ)
608
609
611 "regex rewrite outgoing URL"
612 if not hasattr(thread, 'routes'):
613 regex_select()
614 if routers:
615 return url
616 if thread.routes.routes_out:
617 items = url.split('?', 1)
618 if e:
619 host = e.get('http_host', 'localhost').lower()
620 i = host.find(':')
621 if i > 0:
622 host = host[:i]
623 items[0] = '%s:%s://%s:%s %s' % \
624 (e.get('remote_addr', ''),
625 e.get('wsgi_url_scheme', 'http').lower(), host,
626 e.get('request_method', 'get').lower(), items[0])
627 else:
628 items[0] = ':http://localhost:get %s' % items[0]
629 for (regex, value) in thread.routes.routes_out:
630 if regex.match(items[0]):
631 rewritten = '?'.join([regex.sub(value, items[0])] + items[1:])
632 logger.debug('routes_out: [%s] -> %s' % (url, rewritten))
633 return rewritten
634 logger.debug('routes_out: [%s] not rewritten' % url)
635 return url
636
637
638 -def filter_url(url, method='get', remote='0.0.0.0', out=False, app=False, lang=None,
639 domain=(None,None), env=False, scheme=None, host=None, port=None):
640 "doctest/unittest interface to regex_filter_in() and regex_filter_out()"
641 regex_url = re.compile(r'^(?P<scheme>http|https|HTTP|HTTPS)\://(?P<host>[^/]*)(?P<uri>.*)')
642 match = regex_url.match(url)
643 urlscheme = match.group('scheme').lower()
644 urlhost = match.group('host').lower()
645 uri = match.group('uri')
646 k = uri.find('?')
647 if k < 0:
648 k = len(uri)
649 (path_info, query_string) = (uri[:k], uri[k+1:])
650 path_info = urllib.unquote(path_info)
651 e = {
652 'REMOTE_ADDR': remote,
653 'REQUEST_METHOD': method,
654 'WSGI_URL_SCHEME': urlscheme,
655 'HTTP_HOST': urlhost,
656 'REQUEST_URI': uri,
657 'PATH_INFO': path_info,
658 'QUERY_STRING': query_string,
659
660 'remote_addr': remote,
661 'request_method': method,
662 'wsgi_url_scheme': urlscheme,
663 'http_host': urlhost
664 }
665
666 request = Storage()
667 e["applications_parent"] = global_settings.applications_parent
668 request.env = Storage(e)
669 request.uri_language = lang
670
671
672
673 if app:
674 if routers:
675 return map_url_in(request, e, app=True)
676 return regex_select(e)
677
678
679
680 if out:
681 (request.env.domain_application, request.env.domain_controller) = domain
682 items = path_info.lstrip('/').split('/')
683 if items[-1] == '':
684 items.pop()
685 assert len(items) >= 3, "at least /a/c/f is required"
686 a = items.pop(0)
687 c = items.pop(0)
688 f = items.pop(0)
689 if not routers:
690 return regex_filter_out(uri, e)
691 acf = map_url_out(request, None, a, c, f, items, None, scheme, host, port)
692 if items:
693 url = '%s/%s' % (acf, '/'.join(items))
694 if items[-1] == '':
695 url += '/'
696 else:
697 url = acf
698 if query_string:
699 url += '?' + query_string
700 return url
701
702
703
704 (static, e) = url_in(request, e)
705 if static:
706 return static
707 result = "/%s/%s/%s" % (request.application, request.controller, request.function)
708 if request.extension and request.extension != 'html':
709 result += ".%s" % request.extension
710 if request.args:
711 result += " %s" % request.args
712 if e['QUERY_STRING']:
713 result += " ?%s" % e['QUERY_STRING']
714 if request.uri_language:
715 result += " (%s)" % request.uri_language
716 if env:
717 return request.env
718 return result
719
720
721 -def filter_err(status, application='app', ticket='tkt'):
722 "doctest/unittest interface to routes_onerror"
723 if status > 399 and thread.routes.routes_onerror:
724 keys = set(('%s/%s' % (application, status),
725 '%s/*' % (application),
726 '*/%s' % (status),
727 '*/*'))
728 for (key,redir) in thread.routes.routes_onerror:
729 if key in keys:
730 if redir == '!':
731 break
732 elif '?' in redir:
733 url = redir + '&' + 'code=%s&ticket=%s' % (status,ticket)
734 else:
735 url = redir + '?' + 'code=%s&ticket=%s' % (status,ticket)
736 return url
737 return status
738
739
740
742 "logic for mapping incoming URLs"
743
744 - def __init__(self, request=None, env=None):
745 "initialize a map-in object"
746 self.request = request
747 self.env = env
748
749 self.router = None
750 self.application = None
751 self.language = None
752 self.controller = None
753 self.function = None
754 self.extension = 'html'
755
756 self.controllers = set()
757 self.functions = set()
758 self.languages = set()
759 self.default_language = None
760 self.map_hyphen = False
761 self.exclusive_domain = False
762
763 path = self.env['PATH_INFO']
764 self.query = self.env.get('QUERY_STRING', None)
765 path = path.lstrip('/')
766 self.env['PATH_INFO'] = '/' + path
767 self.env['WEB2PY_ORIGINAL_URI'] = self.env['PATH_INFO'] + (self.query and ('?' + self.query) or '')
768
769
770
771
772 if path.endswith('/'):
773 path = path[:-1]
774 self.args = List(path and path.split('/') or [])
775
776
777 self.remote_addr = self.env.get('REMOTE_ADDR','localhost')
778 self.scheme = self.env.get('WSGI_URL_SCHEME', 'http').lower()
779 self.method = self.env.get('REQUEST_METHOD', 'get').lower()
780 self.host = self.env.get('HTTP_HOST')
781 self.port = None
782 if not self.host:
783 self.host = self.env.get('SERVER_NAME')
784 self.port = self.env.get('SERVER_PORT')
785 if not self.host:
786 self.host = 'localhost'
787 self.port = '80'
788 if ':' in self.host:
789 (self.host, self.port) = self.host.split(':')
790 if not self.port:
791 if self.scheme == 'https':
792 self.port = '443'
793 else:
794 self.port = '80'
795
797 "strip path prefix, if present in its entirety"
798 prefix = routers.BASE.path_prefix
799 if prefix:
800 prefixlen = len(prefix)
801 if prefixlen > len(self.args):
802 return
803 for i in xrange(prefixlen):
804 if prefix[i] != self.args[i]:
805 return
806 self.args = List(self.args[prefixlen:])
807
809 "determine application name"
810 base = routers.BASE
811 self.domain_application = None
812 self.domain_controller = None
813 arg0 = self.harg0
814 if base.applications and arg0 in base.applications:
815 self.application = arg0
816 elif (self.host, self.port) in base.domains:
817 (self.application, self.domain_controller) = base.domains[(self.host, self.port)]
818 self.env['domain_application'] = self.application
819 self.env['domain_controller'] = self.domain_controller
820 elif (self.host, None) in base.domains:
821 (self.application, self.domain_controller) = base.domains[(self.host, None)]
822 self.env['domain_application'] = self.application
823 self.env['domain_controller'] = self.domain_controller
824 elif arg0 and not base.applications:
825 self.application = arg0
826 else:
827 self.application = base.default_application or ''
828 self.pop_arg_if(self.application == arg0)
829
830 if not base._acfe_match.match(self.application):
831 raise HTTP(400, thread.routes.error_message % 'invalid request',
832 web2py_error="invalid application: '%s'" % self.application)
833
834 if self.application not in routers and \
835 (self.application != thread.routes.default_application or self.application == 'welcome'):
836 raise HTTP(400, thread.routes.error_message % 'invalid request',
837 web2py_error="unknown application: '%s'" % self.application)
838
839
840
841 logger.debug("select application=%s" % self.application)
842 self.request.application = self.application
843 if self.application not in routers:
844 self.router = routers.BASE
845 else:
846 self.router = routers[self.application]
847 self.controllers = self.router.controllers
848 self.default_controller = self.domain_controller or self.router.default_controller
849 self.functions = self.router.functions
850 self.languages = self.router.languages
851 self.default_language = self.router.default_language
852 self.map_hyphen = self.router.map_hyphen
853 self.exclusive_domain = self.router.exclusive_domain
854 self._acfe_match = self.router._acfe_match
855 self._file_match = self.router._file_match
856 self._args_match = self.router._args_match
857
859 '''
860 handle root-static files (no hyphen mapping)
861
862 a root-static file is one whose incoming URL expects it to be at the root,
863 typically robots.txt & favicon.ico
864 '''
865 if len(self.args) == 1 and self.arg0 in self.router.root_static:
866 self.controller = self.request.controller = 'static'
867 root_static_file = os.path.join(self.request.env.applications_parent,
868 'applications', self.application,
869 self.controller, self.arg0)
870 logger.debug("route: root static=%s" % root_static_file)
871 return root_static_file
872 return None
873
885
887 "identify controller"
888
889
890 arg0 = self.harg0
891 if not arg0 or (self.controllers and arg0 not in self.controllers):
892 self.controller = self.default_controller or ''
893 else:
894 self.controller = arg0
895 self.pop_arg_if(arg0 == self.controller)
896 logger.debug("route: controller=%s" % self.controller)
897 if not self.router._acfe_match.match(self.controller):
898 raise HTTP(400, thread.routes.error_message % 'invalid request',
899 web2py_error='invalid controller')
900
902 '''
903 handle static files
904 file_match but no hyphen mapping
905 '''
906 if self.controller != 'static':
907 return None
908 file = '/'.join(self.args)
909 if not self.router._file_match.match(file):
910 raise HTTP(400, thread.routes.error_message % 'invalid request',
911 web2py_error='invalid static file')
912
913
914
915
916
917 if self.language:
918 static_file = os.path.join(self.request.env.applications_parent,
919 'applications', self.application,
920 'static', self.language, file)
921 if not self.language or not os.path.isfile(static_file):
922 static_file = os.path.join(self.request.env.applications_parent,
923 'applications', self.application,
924 'static', file)
925 logger.debug("route: static=%s" % static_file)
926 return static_file
927
929 "handle function.extension"
930 arg0 = self.harg0
931 if not arg0 or self.functions and arg0 not in self.functions and self.controller == self.default_controller:
932 self.function = self.router.default_function or ""
933 self.pop_arg_if(arg0 and self.function == arg0)
934 else:
935 func_ext = arg0.split('.')
936 if len(func_ext) > 1:
937 self.function = func_ext[0]
938 self.extension = func_ext[-1]
939 else:
940 self.function = arg0
941 self.pop_arg_if(True)
942 logger.debug("route: function.ext=%s.%s" % (self.function, self.extension))
943
944 if not self.router._acfe_match.match(self.function):
945 raise HTTP(400, thread.routes.error_message % 'invalid request',
946 web2py_error='invalid function')
947 if self.extension and not self.router._acfe_match.match(self.extension):
948 raise HTTP(400, thread.routes.error_message % 'invalid request',
949 web2py_error='invalid extension')
950
952 '''
953 check args against validation pattern
954 '''
955 for arg in self.args:
956 if not self.router._args_match.match(arg):
957 raise HTTP(400, thread.routes.error_message % 'invalid request',
958 web2py_error='invalid arg <%s>' % arg)
959
961 '''
962 update request from self
963 build env.request_uri
964 make lower-case versions of http headers in env
965 '''
966 self.request.application = self.application
967 self.request.controller = self.controller
968 self.request.function = self.function
969 self.request.extension = self.extension
970 self.request.args = self.args
971 if self.language:
972 self.request.uri_language = self.language
973 uri = '/%s/%s/%s' % (self.application, self.controller, self.function)
974 if self.map_hyphen:
975 uri = uri.replace('_', '-')
976 if self.extension != 'html':
977 uri += '.' + self.extension
978 if self.language:
979 uri = '/%s%s' % (self.language, uri)
980 uri += self.args and urllib.quote('/' + '/'.join([str(x) for x in self.args])) or ''
981 uri += (self.query and ('?' + self.query) or '')
982 self.env['REQUEST_URI'] = uri
983 for (key, value) in self.env.items():
984 self.request.env[key.lower().replace('.', '_')] = value
985
986 @property
988 "return first arg"
989 return self.args(0)
990
991 @property
993 "return first arg with optional hyphen mapping"
994 if self.map_hyphen and self.args(0):
995 return self.args(0).replace('-', '_')
996 return self.args(0)
997
999 "conditionally remove first arg and return new first arg"
1000 if dopop:
1001 self.args.pop(0)
1002
1004 "logic for mapping outgoing URLs"
1005
1006 - def __init__(self, request, env, application, controller, function, args, other, scheme, host, port):
1007 "initialize a map-out object"
1008 self.default_application = routers.BASE.default_application
1009 if application in routers:
1010 self.router = routers[application]
1011 else:
1012 self.router = routers.BASE
1013 self.request = request
1014 self.env = env
1015 self.application = application
1016 self.controller = controller
1017 self.function = function
1018 self.args = args
1019 self.other = other
1020 self.scheme = scheme
1021 self.host = host
1022 self.port = port
1023
1024 self.applications = routers.BASE.applications
1025 self.controllers = self.router.controllers
1026 self.functions = self.router.functions
1027 self.languages = self.router.languages
1028 self.default_language = self.router.default_language
1029 self.exclusive_domain = self.router.exclusive_domain
1030 self.map_hyphen = self.router.map_hyphen
1031 self.map_static = self.router.map_static
1032 self.path_prefix = routers.BASE.path_prefix
1033
1034 self.domain_application = request and self.request.env.domain_application
1035 self.domain_controller = request and self.request.env.domain_controller
1036 self.default_function = self.router.default_function
1037
1038 if (self.router.exclusive_domain and self.domain_application and self.domain_application != self.application and not self.host):
1039 raise SyntaxError, 'cross-domain conflict: must specify host'
1040
1041 lang = request and request.uri_language
1042 if lang and self.languages and lang in self.languages:
1043 self.language = lang
1044 else:
1045 self.language = None
1046
1047 self.omit_application = False
1048 self.omit_language = False
1049 self.omit_controller = False
1050 self.omit_function = False
1051
1053 "omit language if possible"
1054
1055 if not self.language or self.language == self.default_language:
1056 self.omit_language = True
1057
1059 "omit what we can of a/c/f"
1060
1061 router = self.router
1062
1063
1064
1065 if not self.args and self.function == router.default_function:
1066 self.omit_function = True
1067 if self.controller == router.default_controller:
1068 self.omit_controller = True
1069 if self.application == self.default_application:
1070 self.omit_application = True
1071
1072
1073
1074
1075 default_application = self.domain_application or self.default_application
1076 if self.application == default_application:
1077 self.omit_application = True
1078
1079
1080
1081 default_controller = ((self.application == self.domain_application) and self.domain_controller) or router.default_controller or ''
1082 if self.controller == default_controller:
1083 self.omit_controller = True
1084
1085
1086
1087 if self.functions and self.function == self.default_function and self.omit_controller:
1088 self.omit_function = True
1089
1090
1091
1092
1093
1094 if self.omit_language:
1095 if not self.applications or self.controller in self.applications:
1096 self.omit_application = False
1097 if self.omit_application:
1098 if not self.applications or self.function in self.applications:
1099 self.omit_controller = False
1100 if not self.controllers or self.function in self.controllers:
1101 self.omit_controller = False
1102 if self.args:
1103 if self.args[0] in self.functions or self.args[0] in self.controllers or self.args[0] in self.applications:
1104 self.omit_function = False
1105 if self.omit_controller:
1106 if self.function in self.controllers or self.function in self.applications:
1107 self.omit_controller = False
1108 if self.omit_application:
1109 if self.controller in self.applications:
1110 self.omit_application = False
1111
1112
1113
1114
1115 if self.controller == 'static' or self.controller.startswith('static/'):
1116 if not self.map_static:
1117 self.omit_application = False
1118 if self.language:
1119 self.omit_language = False
1120 self.omit_controller = False
1121 self.omit_function = False
1122
1124 "build acf from components"
1125 acf = ''
1126 if self.map_hyphen:
1127 self.application = self.application.replace('_', '-')
1128 self.controller = self.controller.replace('_', '-')
1129 if self.controller != 'static' and not self.controller.startswith('static/'):
1130 self.function = self.function.replace('_', '-')
1131 if not self.omit_application:
1132 acf += '/' + self.application
1133 if not self.omit_language:
1134 acf += '/' + self.language
1135 if not self.omit_controller:
1136 acf += '/' + self.controller
1137 if not self.omit_function:
1138 acf += '/' + self.function
1139 if self.path_prefix:
1140 acf = '/' + '/'.join(self.path_prefix) + acf
1141 if self.args:
1142 return acf
1143 return acf or '/'
1144
1146 "convert components to /app/lang/controller/function"
1147
1148 if not routers:
1149 return None
1150 self.omit_lang()
1151 self.omit_acf()
1152 return self.build_acf()
1153
1154
1185
1186 -def map_url_out(request, env, application, controller, function, args, other, scheme, host, port):
1187 '''
1188 supply /a/c/f (or /a/lang/c/f) portion of outgoing url
1189
1190 The basic rule is that we can only make transformations
1191 that map_url_in can reverse.
1192
1193 Suppose that the incoming arguments are a,c,f,args,lang
1194 and that the router defaults are da, dc, df, dl.
1195
1196 We can perform these transformations trivially if args=[] and lang=None or dl:
1197
1198 /da/dc/df => /
1199 /a/dc/df => /a
1200 /a/c/df => /a/c
1201
1202 We would also like to be able to strip the default application or application/controller
1203 from URLs with function/args present, thus:
1204
1205 /da/c/f/args => /c/f/args
1206 /da/dc/f/args => /f/args
1207
1208 We use [applications] and [controllers] and [functions] to suppress ambiguous omissions.
1209
1210 We assume that language names do not collide with a/c/f names.
1211 '''
1212 map = MapUrlOut(request, env, application, controller, function, args, other, scheme, host, port)
1213 return map.acf()
1214
1216 "return a private copy of the effective router for the specified application"
1217 if not routers or appname not in routers:
1218 return None
1219 return Storage(routers[appname])
1220