Package web2py :: Package gluon :: Module rewrite
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.rewrite

   1  #!/bin/env python 
   2  # -*- coding: utf-8 -*- 
   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 
  28  from settings import global_settings 
  29   
  30  logger = logging.getLogger('web2py.rewrite') 
  31   
  32  thread = threading.local()  # thread-local storage for routing parameters 
  33   
34 -def _router_default():
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 map_hyphen = True, 48 acfe_match = r'\w+$', # legal app/ctlr/fcn/ext 49 file_match = r'(\w+[-=./]?)+$', # legal file (path) name 50 args_match = r'([\w@ -]+[=.]?)*$', # legal arg in args 51 ) 52 return router
53
54 -def _params_default(app=None):
55 "return new copy of default parameters" 56 p = Storage() 57 p.name = app or "BASE" 58 p.default_application = app or "init" 59 p.default_controller = "default" 60 p.default_function = "index" 61 p.routes_app = [] 62 p.routes_in = [] 63 p.routes_out = [] 64 p.routes_onerror = [] 65 p.routes_apps_raw = [] 66 p.error_handler = None 67 p.error_message = '<html><body><h1>%s</h1></body></html>' 68 p.error_message_ticket = \ 69 '<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>' 70 p.routers = None 71 return p
72 73 params_apps = dict() 74 params = _params_default(app=None) # regex rewrite parameters 75 thread.routes = params # default to base regex rewrite parameters 76 routers = None 77 78 ROUTER_KEYS = set(('default_application', 'applications', 'default_controller', 'controllers', 79 'default_function', 'functions', 'default_language', 'languages', 80 'domain', 'domains', 'root_static', 'path_prefix', 81 'map_hyphen', 'map_static', 82 'acfe_match', 'file_match', 'args_match')) 83 84 ROUTER_BASE_KEYS = set(('applications', 'default_application', 'domains', 'path_prefix')) 85 86 # The external interface to rewrite consists of: 87 # 88 # load: load routing configuration file(s) 89 # url_in: parse and rewrite incoming URL 90 # url_out: assemble and rewrite outgoing URL 91 # 92 # thread.routes.default_application 93 # thread.routes.error_message 94 # thread.routes.error_message_ticket 95 # thread.routes.try_redirect_on_error 96 # thread.routes.error_handler 97 # 98 # filter_url: helper for doctest & unittest 99 # filter_err: helper for doctest & unittest 100 # regex_filter_out: doctest 101
102 -def url_in(request, environ):
103 "parse and rewrite incoming URL" 104 if routers: 105 return map_url_in(request, environ) 106 return regex_url_in(request, environ)
107
108 -def url_out(request, env, application, controller, function, args, other, scheme, host, port):
109 "assemble and rewrite outgoing URL" 110 if routers: 111 acf = map_url_out(request, application, controller, function, args) 112 url = '%s%s' % (acf, other) 113 else: 114 url = '/%s/%s/%s%s' % (application, controller, function, other) 115 url = regex_filter_out(url, env) 116 # 117 # fill in scheme and host if absolute URL is requested 118 # scheme can be a string, eg 'http', 'https', 'ws', 'wss' 119 # 120 if scheme or port is not None: 121 if host is None: # scheme or port implies host 122 host = True 123 if not scheme or scheme is True: 124 if request and request.env: 125 scheme = request.env.get('WSGI_URL_SCHEME', 'http').lower() 126 else: 127 scheme = 'http' # some reasonable default in case we need it 128 if host is not None: 129 if host is True: 130 host = request.env.http_host 131 if host: 132 if port is None: 133 port = '' 134 else: 135 port = ':%s' % port 136 url = '%s://%s%s%s' % (scheme, host, port, url) 137 return url
138
139 -def try_redirect_on_error(http_object, request, ticket=None):
140 "called from main.wsgibase to rewrite the http response" 141 status = int(str(http_object.status).split()[0]) 142 if status>399 and thread.routes.routes_onerror: 143 keys=set(('%s/%s' % (request.application, status), 144 '%s/*' % (request.application), 145 '*/%s' % (status), 146 '*/*')) 147 for (key,redir) in thread.routes.routes_onerror: 148 if key in keys: 149 if redir == '!': 150 break 151 elif '?' in redir: 152 url = '%s&code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \ 153 (redir,status,ticket,request.env.request_uri,request.url) 154 else: 155 url = '%s?code=%s&ticket=%s&requested_uri=%s&request_url=%s' % \ 156 (redir,status,ticket,request.env.request_uri,request.url) 157 return HTTP(303, 158 'You are being redirected <a href="%s">here</a>' % url, 159 Location=url) 160 return http_object
161 162
163 -def load(routes='routes.py', app=None, data=None, rdict=None):
164 """ 165 load: read (if file) and parse routes 166 store results in params 167 (called from main.py at web2py initialization time) 168 If data is present, it's used instead of the routes.py contents. 169 If rdict is present, it must be a dict to be used for routers (unit test) 170 """ 171 global params 172 global routers 173 if app is None: 174 # reinitialize 175 global params_apps 176 params_apps = dict() 177 params = _params_default(app=None) # regex rewrite parameters 178 thread.routes = params # default to base regex rewrite parameters 179 routers = None 180 181 if isinstance(rdict, dict): 182 symbols = dict(routers=rdict) 183 path = 'rdict' 184 else: 185 if data is not None: 186 path = 'routes' 187 else: 188 if app is None: 189 path = abspath(routes) 190 else: 191 path = abspath('applications', app, routes) 192 if not os.path.exists(path): 193 return 194 routesfp = open(path, 'r') 195 data = routesfp.read().replace('\r\n','\n') 196 routesfp.close() 197 198 symbols = {} 199 try: 200 exec (data + '\n') in symbols 201 except SyntaxError, e: 202 logger.error( 203 '%s has a syntax error and will not be loaded\n' % path 204 + traceback.format_exc()) 205 raise e 206 207 p = _params_default(app) 208 209 for sym in ('routes_app', 'routes_in', 'routes_out'): 210 if sym in symbols: 211 for (k, v) in symbols[sym]: 212 p[sym].append(compile_regex(k, v)) 213 for sym in ('routes_onerror', 'routes_apps_raw', 214 'error_handler','error_message', 'error_message_ticket', 215 'default_application','default_controller', 'default_function'): 216 if sym in symbols: 217 p[sym] = symbols[sym] 218 if 'routers' in symbols: 219 p.routers = Storage(symbols['routers']) 220 for key in p.routers: 221 if isinstance(p.routers[key], dict): 222 p.routers[key] = Storage(p.routers[key]) 223 224 if app is None: 225 params = p # install base rewrite parameters 226 thread.routes = params # install default as current routes 227 # 228 # create the BASE router if routers in use 229 # 230 routers = params.routers # establish routers if present 231 if isinstance(routers, dict): 232 routers = Storage(routers) 233 if routers is not None: 234 router = _router_default() 235 if routers.BASE: 236 router.update(routers.BASE) 237 routers.BASE = router 238 239 # scan each app in applications/ 240 # create a router, if routers are in use 241 # parse the app-specific routes.py if present 242 # 243 all_apps = [] 244 for appname in [app for app in os.listdir(abspath('applications')) if not app.startswith('.')]: 245 if os.path.isdir(abspath('applications', appname)) and \ 246 os.path.isdir(abspath('applications', appname, 'controllers')): 247 all_apps.append(appname) 248 if routers: 249 router = Storage(routers.BASE) # new copy 250 if appname in routers: 251 for key in routers[appname].keys(): 252 if key in ROUTER_BASE_KEYS: 253 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, appname) 254 router.update(routers[appname]) 255 routers[appname] = router 256 if os.path.exists(abspath('applications', appname, routes)): 257 load(routes, appname) 258 259 if routers: 260 load_routers(all_apps) 261 262 else: # app 263 params_apps[app] = p 264 if routers and p.routers: 265 if app in p.routers: 266 routers[app].update(p.routers[app]) 267 268 logger.debug('URL rewrite is on. configuration in %s' % path)
269 270 271 regex_at = re.compile(r'(?<!\\)\$[a-zA-Z]\w*') 272 regex_anything = re.compile(r'(?<!\\)\$anything') 273
274 -def compile_regex(k, v):
275 """ 276 Preprocess and compile the regular expressions in routes_app/in/out 277 278 The resulting regex will match a pattern of the form: 279 280 [remote address]:[protocol]://[host]:[method] [path] 281 282 We allow abbreviated regexes on input; here we try to complete them. 283 """ 284 k0 = k # original k for error reporting 285 # bracket regex in ^...$ if not already done 286 if not k[0] == '^': 287 k = '^%s' % k 288 if not k[-1] == '$': 289 k = '%s$' % k 290 # if there are no :-separated parts, prepend a catch-all for the IP address 291 if k.find(':') < 0: 292 # k = '^.*?:%s' % k[1:] 293 k = '^.*?:https?://[^:/]+:[a-z]+ %s' % k[1:] 294 # if there's no ://, provide a catch-all for the protocol, host & method 295 if k.find('://') < 0: 296 i = k.find(':/') 297 if i < 0: 298 raise SyntaxError, "routes pattern syntax error: path needs leading '/' [%s]" % k0 299 k = r'%s:https?://[^:/]+:[a-z]+ %s' % (k[:i], k[i+1:]) 300 # $anything -> ?P<anything>.* 301 for item in regex_anything.findall(k): 302 k = k.replace(item, '(?P<anything>.*)') 303 # $a (etc) -> ?P<a>\w+ 304 for item in regex_at.findall(k): 305 k = k.replace(item, r'(?P<%s>\w+)' % item[1:]) 306 # same for replacement pattern, but with \g 307 for item in regex_at.findall(v): 308 v = v.replace(item, r'\g<%s>' % item[1:]) 309 return (re.compile(k, re.DOTALL), v)
310
311 -def load_routers(all_apps):
312 "load-time post-processing of routers" 313 314 for app in routers.keys(): 315 # initialize apps with routers that aren't present, on behalf of unit tests 316 if app not in all_apps: 317 all_apps.append(app) 318 router = Storage(routers.BASE) # new copy 319 if app != 'BASE': 320 for key in routers[app].keys(): 321 if key in ROUTER_BASE_KEYS: 322 raise SyntaxError, "BASE-only key '%s' in router '%s'" % (key, app) 323 router.update(routers[app]) 324 routers[app] = router 325 router = routers[app] 326 for key in router.keys(): 327 if key not in ROUTER_KEYS: 328 raise SyntaxError, "unknown key '%s' in router '%s'" % (key, app) 329 if not router.controllers: 330 router.controllers = set() 331 elif not isinstance(router.controllers, str): 332 router.controllers = set(router.controllers) 333 if router.functions: 334 router.functions = set(router.functions) 335 else: 336 router.functions = set() 337 if router.languages: 338 router.languages = set(router.languages) 339 else: 340 router.languages = set() 341 if app != 'BASE': 342 for base_only in ROUTER_BASE_KEYS: 343 router.pop(base_only, None) 344 if 'domain' in router: 345 routers.BASE.domains[router.domain] = app 346 if isinstance(router.controllers, str) and router.controllers == 'DEFAULT': 347 router.controllers = set() 348 if os.path.isdir(abspath('applications', app)): 349 cpath = abspath('applications', app, 'controllers') 350 for cname in os.listdir(cpath): 351 if os.path.isfile(abspath(cpath, cname)) and cname.endswith('.py'): 352 router.controllers.add(cname[:-3]) 353 if router.controllers: 354 router.controllers.add('static') 355 router.controllers.add(router.default_controller) 356 if router.functions: 357 router.functions.add(router.default_function) 358 359 if isinstance(routers.BASE.applications, str) and routers.BASE.applications == 'ALL': 360 routers.BASE.applications = list(all_apps) 361 if routers.BASE.applications: 362 routers.BASE.applications = set(routers.BASE.applications) 363 else: 364 routers.BASE.applications = set() 365 366 for app in routers.keys(): 367 # set router name 368 router = routers[app] 369 router.name = app 370 # compile URL validation patterns 371 router._acfe_match = re.compile(router.acfe_match) 372 router._file_match = re.compile(router.file_match) 373 if router.args_match: 374 router._args_match = re.compile(router.args_match) 375 # convert path_prefix to a list of path elements 376 if router.path_prefix: 377 if isinstance(router.path_prefix, str): 378 router.path_prefix = router.path_prefix.strip('/').split('/') 379 380 # rewrite BASE.domains as tuples 381 # 382 # key: 'domain[:port]' -> (domain, port) 383 # value: 'application[/controller] -> (application, controller) 384 # (port and controller may be None) 385 # 386 domains = dict() 387 if routers.BASE.domains: 388 for (domain, app) in [(d.strip(':'), a.strip('/')) for (d, a) in routers.BASE.domains.items()]: 389 port = None 390 if ':' in domain: 391 (domain, port) = domain.split(':') 392 ctlr = None 393 if '/' in app: 394 (app, ctlr) = app.split('/') 395 if app not in all_apps and app not in routers: 396 raise SyntaxError, "unknown app '%s' in domains" % app 397 domains[(domain, port)] = (app, ctlr) 398 routers.BASE.domains = domains
399
400 -def regex_uri(e, regexes, tag, default=None):
401 "filter incoming URI against a list of regexes" 402 path = e['PATH_INFO'] 403 host = e.get('HTTP_HOST', 'localhost').lower() 404 i = host.find(':') 405 if i > 0: 406 host = host[:i] 407 key = '%s:%s://%s:%s %s' % \ 408 (e.get('REMOTE_ADDR','localhost'), 409 e.get('WSGI_URL_SCHEME', 'http').lower(), host, 410 e.get('REQUEST_METHOD', 'get').lower(), path) 411 for (regex, value) in regexes: 412 if regex.match(key): 413 rewritten = regex.sub(value, key) 414 logger.debug('%s: [%s] [%s] -> %s' % (tag, key, value, rewritten)) 415 return rewritten 416 logger.debug('%s: [%s] -> %s (not rewritten)' % (tag, key, default)) 417 return default
418
419 -def regex_select(env=None, app=None, request=None):
420 """ 421 select a set of regex rewrite params for the current request 422 """ 423 if app: 424 thread.routes = params_apps.get(app, params) 425 elif env and params.routes_app: 426 if routers: 427 map_url_in(request, env, app=True) 428 else: 429 app = regex_uri(env, params.routes_app, "routes_app") 430 thread.routes = params_apps.get(app, params) 431 else: 432 thread.routes = params # default to base rewrite parameters 433 logger.debug("select routing parameters: %s" % thread.routes.name) 434 return app # for doctest
435
436 -def regex_filter_in(e):
437 "regex rewrite incoming URL" 438 query = e.get('QUERY_STRING', None) 439 e['WEB2PY_ORIGINAL_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '') 440 if thread.routes.routes_in: 441 path = regex_uri(e, thread.routes.routes_in, "routes_in", e['PATH_INFO']) 442 items = path.split('?', 1) 443 e['PATH_INFO'] = items[0] 444 if len(items) > 1: 445 if query: 446 query = items[1] + '&' + query 447 else: 448 query = items[1] 449 e['QUERY_STRING'] = query 450 e['REQUEST_URI'] = e['PATH_INFO'] + (query and ('?' + query) or '') 451 return e
452 453 454 # pattern to replace spaces with underscore in URL 455 # also the html escaped variants '+' and '%20' are covered 456 regex_space = re.compile('(\+|\s|%20)+') 457 458 # pattern to find valid paths in url /application/controller/... 459 # this could be: 460 # for static pages: 461 # /<b:application>/static/<x:file> 462 # for dynamic pages: 463 # /<a:application>[/<c:controller>[/<f:function>[.<e:ext>][/<s:args>]]] 464 # application, controller, function and ext may only contain [a-zA-Z0-9_] 465 # file and args may also contain '-', '=', '.' and '/' 466 # apps in routes_apps_raw must parse raw_args into args 467 468 regex_static = re.compile(r''' 469 (^ # static pages 470 /(?P<b> \w+) # b=app 471 /static # /b/static 472 /(?P<x> (\w[\-\=\./]?)* ) # x=file 473 $) 474 ''', re.X) 475 476 regex_url = re.compile(r''' 477 (^( # (/a/c/f.e/s) 478 /(?P<a> [\w\s+]+ ) # /a=app 479 ( # (/c.f.e/s) 480 /(?P<c> [\w\s+]+ ) # /a/c=controller 481 ( # (/f.e/s) 482 /(?P<f> [\w\s+]+ ) # /a/c/f=function 483 ( # (.e) 484 \.(?P<e> [\w\s+]+ ) # /a/c/f.e=extension 485 )? 486 ( # (/s) 487 /(?P<r> # /a/c/f.e/r=raw_args 488 .* 489 ) 490 )? 491 )? 492 )? 493 )? 494 /?$) 495 ''', re.X) 496 497 regex_args = re.compile(r''' 498 (^ 499 (?P<s> 500 ( [\w@/-][=.]? )* # s=args 501 )? 502 /?$) # trailing slash 503 ''', re.X) 504
505 -def regex_url_in(request, environ):
506 "rewrite and parse incoming URL" 507 508 # ################################################## 509 # select application 510 # rewrite URL if routes_in is defined 511 # update request.env 512 # ################################################## 513 514 regex_select(env=environ, request=request) 515 516 if thread.routes.routes_in: 517 environ = regex_filter_in(environ) 518 519 for (key, value) in environ.items(): 520 request.env[key.lower().replace('.', '_')] = value 521 522 path = request.env.path_info.replace('\\', '/') 523 524 # ################################################## 525 # serve if a static file 526 # ################################################## 527 528 match = regex_static.match(regex_space.sub('_', path)) 529 if match and match.group('x'): 530 static_file = os.path.join(request.env.applications_parent, 531 'applications', match.group('b'), 532 'static', match.group('x')) 533 return (static_file, environ) 534 535 # ################################################## 536 # parse application, controller and function 537 # ################################################## 538 539 path = re.sub('%20', ' ', path) 540 match = regex_url.match(path) 541 if not match or match.group('c') == 'static': 542 raise HTTP(400, 543 thread.routes.error_message % 'invalid request', 544 web2py_error='invalid path') 545 546 request.application = \ 547 regex_space.sub('_', match.group('a') or thread.routes.default_application) 548 request.controller = \ 549 regex_space.sub('_', match.group('c') or thread.routes.default_controller) 550 request.function = \ 551 regex_space.sub('_', match.group('f') or thread.routes.default_function) 552 group_e = match.group('e') 553 request.raw_extension = group_e and regex_space.sub('_', group_e) or None 554 request.extension = request.raw_extension or 'html' 555 request.raw_args = match.group('r') 556 request.args = List([]) 557 if request.application in thread.routes.routes_apps_raw: 558 # application is responsible for parsing args 559 request.args = None 560 elif request.raw_args: 561 match = regex_args.match(request.raw_args.replace(' ', '_')) 562 if match: 563 group_s = match.group('s') 564 request.args = \ 565 List((group_s and group_s.split('/')) or []) 566 if request.args and request.args[-1] == '': 567 request.args.pop() # adjust for trailing empty arg 568 else: 569 raise HTTP(400, 570 thread.routes.error_message % 'invalid request', 571 web2py_error='invalid path (args)') 572 return (None, environ)
573 574
575 -def regex_filter_out(url, e=None):
576 "regex rewrite outgoing URL" 577 if not hasattr(thread, 'routes'): 578 regex_select() # ensure thread.routes is set (for application threads) 579 if routers: 580 return url # already filtered 581 if thread.routes.routes_out: 582 items = url.split('?', 1) 583 if e: 584 host = e.get('http_host', 'localhost').lower() 585 i = host.find(':') 586 if i > 0: 587 host = host[:i] 588 items[0] = '%s:%s://%s:%s %s' % \ 589 (e.get('remote_addr', ''), 590 e.get('wsgi_url_scheme', 'http').lower(), host, 591 e.get('request_method', 'get').lower(), items[0]) 592 else: 593 items[0] = ':http://localhost:get %s' % items[0] 594 for (regex, value) in thread.routes.routes_out: 595 if regex.match(items[0]): 596 rewritten = '?'.join([regex.sub(value, items[0])] + items[1:]) 597 logger.debug('routes_out: [%s] -> %s' % (url, rewritten)) 598 return rewritten 599 logger.debug('routes_out: [%s] not rewritten' % url) 600 return url
601 602
603 -def filter_url(url, method='get', remote='0.0.0.0', out=False, app=False, lang=None, domain=(None,None), env=False):
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 scheme = match.group('scheme').lower() 608 host = 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) # simulate server 615 e = { 616 'REMOTE_ADDR': remote, 617 'REQUEST_METHOD': method, 618 'WSGI_URL_SCHEME': scheme, 619 'HTTP_HOST': host, 620 'REQUEST_URI': uri, 621 'PATH_INFO': path_info, 622 'QUERY_STRING': query_string, 623 #for filter_out request.env use lowercase 624 'remote_addr': remote, 625 'request_method': method, 626 'wsgi_url_scheme': scheme, 627 'http_host': host 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 # determine application only 636 # 637 if app: 638 if routers: 639 return map_url_in(request, e, app=True) 640 return regex_select(e) 641 642 # rewrite outbound URL 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() # adjust trailing empty args 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, a, c, f, items) 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 # rewrite inbound URL 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 # redirection 701 return status # no action
702 703 # router support 704 #
705 -class MapUrlIn(object):
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 = True 725 726 path = self.env['PATH_INFO'] 727 self.query = self.env.get('QUERY_STRING', None) 728 path = path.lstrip('/') 729 self.env['PATH_INFO'] = '/' + path 730 self.env['WEB2PY_ORIGINAL_URI'] = self.env['PATH_INFO'] + (self.query and ('?' + self.query) or '') 731 732 # to handle empty args, strip exactly one trailing slash, if present 733 # .../arg1// represents one trailing empty arg 734 # 735 if path.endswith('/'): 736 path = path[:-1] 737 self.args = List(path and path.split('/') or []) 738 739 # see http://www.python.org/dev/peps/pep-3333/#url-reconstruction for URL composition 740 self.remote_addr = self.env.get('REMOTE_ADDR','localhost') 741 self.scheme = self.env.get('WSGI_URL_SCHEME', 'http').lower() 742 self.method = self.env.get('REQUEST_METHOD', 'get').lower() 743 self.host = self.env.get('HTTP_HOST') 744 self.port = None 745 if not self.host: 746 self.host = self.env.get('SERVER_NAME') 747 self.port = self.env.get('SERVER_PORT') 748 if not self.host: 749 self.host = 'localhost' 750 self.port = '80' 751 if ':' in self.host: 752 (self.host, self.port) = self.host.split(':') 753 if not self.port: 754 if self.scheme == 'https': 755 self.port = '443' 756 else: 757 self.port = '80'
758
759 - def map_prefix(self):
760 "strip path prefix, if present in its entirety" 761 prefix = routers.BASE.path_prefix 762 if prefix: 763 prefixlen = len(prefix) 764 if prefixlen > len(self.args): 765 return 766 for i in xrange(prefixlen): 767 if prefix[i] != self.args[i]: 768 return # prefix didn't match 769 self.args = List(self.args[prefixlen:]) # strip the prefix
770
771 - def map_app(self):
772 "determine application name" 773 base = routers.BASE # base router 774 self.domain_application = None 775 self.domain_controller = None 776 arg0 = self.harg0 777 if base.applications and arg0 in base.applications: 778 self.application = arg0 779 elif (self.host, self.port) in base.domains: 780 (self.application, self.domain_controller) = base.domains[(self.host, self.port)] 781 self.env['domain_application'] = self.application 782 self.env['domain_controller'] = self.domain_controller 783 elif (self.host, None) in base.domains: 784 (self.application, self.domain_controller) = base.domains[(self.host, None)] 785 self.env['domain_application'] = self.application 786 self.env['domain_controller'] = self.domain_controller 787 elif arg0 and not base.applications: 788 self.application = arg0 789 else: 790 self.application = base.default_application or '' 791 self.pop_arg_if(self.application == arg0) 792 793 if not base._acfe_match.match(self.application): 794 raise HTTP(400, thread.routes.error_message % 'invalid request', 795 web2py_error="invalid application: '%s'" % self.application) 796 797 if self.application not in routers and \ 798 (self.application != thread.routes.default_application or self.application == 'welcome'): 799 raise HTTP(400, thread.routes.error_message % 'invalid request', 800 web2py_error="unknown application: '%s'" % self.application) 801 802 # set the application router 803 # 804 logger.debug("select application=%s" % self.application) 805 self.request.application = self.application 806 if self.application not in routers: 807 self.router = routers.BASE # support gluon.main.wsgibase init->welcome 808 else: 809 self.router = routers[self.application] # application router 810 self.controllers = self.router.controllers 811 self.default_controller = self.domain_controller or self.router.default_controller 812 self.functions = self.router.functions 813 self.languages = self.router.languages 814 self.default_language = self.router.default_language 815 self.map_hyphen = self.router.map_hyphen 816 self._acfe_match = self.router._acfe_match 817 self._file_match = self.router._file_match 818 self._args_match = self.router._args_match
819
820 - def map_root_static(self):
821 ''' 822 handle root-static files (no hyphen mapping) 823 824 a root-static file is one whose incoming URL expects it to be at the root, 825 typically robots.txt & favicon.ico 826 ''' 827 if len(self.args) == 1 and self.arg0 in self.router.root_static: 828 self.controller = self.request.controller = 'static' 829 root_static_file = os.path.join(self.request.env.applications_parent, 830 'applications', self.application, 831 self.controller, self.arg0) 832 logger.debug("route: root static=%s" % root_static_file) 833 return root_static_file 834 return None
835
836 - def map_language(self):
837 "handle language (no hyphen mapping)" 838 arg0 = self.arg0 # no hyphen mapping 839 if arg0 and self.languages and arg0 in self.languages: 840 self.language = arg0 841 else: 842 self.language = self.default_language 843 if self.language: 844 logger.debug("route: language=%s" % self.language) 845 self.pop_arg_if(self.language == arg0) 846 arg0 = self.arg0
847
848 - def map_controller(self):
849 "identify controller" 850 # handle controller 851 # 852 arg0 = self.harg0 # map hyphens 853 if not arg0 or (self.controllers and arg0 not in self.controllers): 854 self.controller = self.default_controller or '' 855 else: 856 self.controller = arg0 857 self.pop_arg_if(arg0 == self.controller) 858 logger.debug("route: controller=%s" % self.controller) 859 if not self.router._acfe_match.match(self.controller): 860 raise HTTP(400, thread.routes.error_message % 'invalid request', 861 web2py_error='invalid controller')
862
863 - def map_static(self):
864 ''' 865 handle static files 866 file_match but no hyphen mapping 867 ''' 868 if self.controller != 'static': 869 return None 870 file = '/'.join(self.args) 871 if not self.router._file_match.match(file): 872 raise HTTP(400, thread.routes.error_message % 'invalid request', 873 web2py_error='invalid static file') 874 # 875 # support language-specific static subdirectories, 876 # eg /appname/en/static/filename => applications/appname/static/en/filename 877 # if language-specific file doesn't exist, try same file in static 878 # 879 if self.language: 880 static_file = os.path.join(self.request.env.applications_parent, 881 'applications', self.application, 882 'static', self.language, file) 883 if not self.language or not os.path.isfile(static_file): 884 static_file = os.path.join(self.request.env.applications_parent, 885 'applications', self.application, 886 'static', file) 887 logger.debug("route: static=%s" % static_file) 888 return static_file
889
890 - def map_function(self):
891 "handle function.extension" 892 arg0 = self.harg0 # map hyphens 893 if not arg0 or self.functions and arg0 not in self.functions and self.controller == self.default_controller: 894 self.function = self.router.default_function or "" 895 self.pop_arg_if(arg0 and self.function == arg0) 896 else: 897 func_ext = arg0.split('.') 898 if len(func_ext) > 1: 899 self.function = func_ext[0] 900 self.extension = func_ext[-1] 901 else: 902 self.function = arg0 903 self.pop_arg_if(True) 904 logger.debug("route: function.ext=%s.%s" % (self.function, self.extension)) 905 906 if not self.router._acfe_match.match(self.function): 907 raise HTTP(400, thread.routes.error_message % 'invalid request', 908 web2py_error='invalid function') 909 if self.extension and not self.router._acfe_match.match(self.extension): 910 raise HTTP(400, thread.routes.error_message % 'invalid request', 911 web2py_error='invalid extension')
912
913 - def validate_args(self):
914 ''' 915 check args against validation pattern 916 ''' 917 for arg in self.args: 918 if not self.router._args_match.match(arg): 919 raise HTTP(400, thread.routes.error_message % 'invalid request', 920 web2py_error='invalid arg <%s>' % arg)
921
922 - def update_request(self):
923 ''' 924 update request from self 925 build env.request_uri 926 make lower-case versions of http headers in env 927 ''' 928 self.request.application = self.application 929 self.request.controller = self.controller 930 self.request.function = self.function 931 self.request.extension = self.extension 932 self.request.args = self.args 933 if self.language: 934 self.request.uri_language = self.language 935 uri = '/%s/%s/%s' % (self.application, self.controller, self.function) 936 if self.map_hyphen: 937 uri = uri.replace('_', '-') 938 if self.extension != 'html': 939 uri += '.' + self.extension 940 if self.language: 941 uri = '/%s%s' % (self.language, uri) 942 uri += self.args and urllib.quote('/' + '/'.join([str(x) for x in self.args])) or '' 943 uri += (self.query and ('?' + self.query) or '') 944 self.env['REQUEST_URI'] = uri 945 for (key, value) in self.env.items(): 946 self.request.env[key.lower().replace('.', '_')] = value
947 948 @property
949 - def arg0(self):
950 "return first arg" 951 return self.args(0)
952 953 @property
954 - def harg0(self):
955 "return first arg with optional hyphen mapping" 956 if self.map_hyphen and self.args(0): 957 return self.args(0).replace('-', '_') 958 return self.args(0)
959
960 - def pop_arg_if(self, dopop):
961 "conditionally remove first arg and return new first arg" 962 if dopop: 963 self.args.pop(0)
964
965 -class MapUrlOut(object):
966 "logic for mapping outgoing URLs" 967
968 - def __init__(self, application, controller, function, args, request):
969 "initialize a map-out object" 970 self.default_application = routers.BASE.default_application 971 if application in routers: 972 self.router = routers[application] 973 else: 974 self.router = routers.BASE 975 self.application = application 976 self.controller = controller 977 self.function = function 978 self.args = args 979 self.request = request 980 981 self.applications = routers.BASE.applications 982 self.controllers = self.router.controllers 983 self.functions = self.router.functions 984 self.languages = self.router.languages 985 self.default_language = self.router.default_language 986 self.map_hyphen = self.router.map_hyphen 987 self.map_static = self.router.map_static 988 self.path_prefix = routers.BASE.path_prefix 989 990 self.domain_application = request and self.request.env.domain_application 991 self.domain_controller = request and self.request.env.domain_controller 992 self.default_function = self.router.default_function 993 994 lang = request and request.uri_language 995 if lang and self.languages and lang in self.languages: 996 self.language = lang 997 else: 998 self.language = None 999 1000 self.omit_application = False 1001 self.omit_language = False 1002 self.omit_controller = False 1003 self.omit_function = False
1004
1005 - def omit_lang(self):
1006 "omit language if possible" 1007 1008 if not self.language or self.language == self.default_language: 1009 self.omit_language = True
1010
1011 - def omit_acf(self):
1012 "omit what we can of a/c/f" 1013 1014 router = self.router 1015 1016 # Handle the easy no-args case of tail-defaults: /a/c /a / 1017 # 1018 if not self.args and self.function == router.default_function: 1019 self.omit_function = True 1020 if self.controller == router.default_controller: 1021 self.omit_controller = True 1022 if self.application == self.default_application: 1023 self.omit_application = True 1024 1025 # omit default application 1026 # (which might be the domain default application) 1027 # 1028 default_application = self.domain_application or self.default_application 1029 if self.application == default_application: 1030 self.omit_application = True 1031 1032 # omit controller if default controller 1033 # 1034 default_controller = ((self.application == self.domain_application) and self.domain_controller) or router.default_controller or '' 1035 if self.controller == default_controller: 1036 self.omit_controller = True 1037 1038 # omit function if default controller/function 1039 # 1040 if self.functions and self.function == self.default_function and self.omit_controller: 1041 self.omit_function = True 1042 1043 # prohibit ambiguous cases 1044 # 1045 # because we presume the lang string to be unambiguous, its presence protects application omission 1046 # 1047 if self.omit_language: 1048 if not self.applications or self.controller in self.applications: 1049 self.omit_application = False 1050 if self.omit_application: 1051 if not self.applications or self.function in self.applications: 1052 self.omit_controller = False 1053 if not self.controllers or self.function in self.controllers: 1054 self.omit_controller = False 1055 if self.args: 1056 if self.args[0] in self.functions or self.args[0] in self.controllers or self.args[0] in self.applications: 1057 self.omit_function = False 1058 if self.omit_controller: 1059 if self.function in self.controllers or self.function in self.applications: 1060 self.omit_controller = False 1061 if self.omit_application: 1062 if self.controller in self.applications: 1063 self.omit_application = False 1064 1065 # handle static as a special case 1066 # (easier for external static handling) 1067 # 1068 if self.controller == 'static' or self.controller.startswith('static/'): 1069 if not self.map_static: 1070 self.omit_application = False 1071 if self.language: 1072 self.omit_language = False 1073 self.omit_controller = False 1074 self.omit_function = False
1075
1076 - def build_acf(self):
1077 "build acf from components" 1078 acf = '' 1079 if self.map_hyphen: 1080 self.application = self.application.replace('_', '-') 1081 self.controller = self.controller.replace('_', '-') 1082 if self.controller != 'static' and not self.controller.startswith('static/'): 1083 self.function = self.function.replace('_', '-') 1084 if not self.omit_application: 1085 acf += '/' + self.application 1086 if not self.omit_language: 1087 acf += '/' + self.language 1088 if not self.omit_controller: 1089 acf += '/' + self.controller 1090 if not self.omit_function: 1091 acf += '/' + self.function 1092 if self.path_prefix: 1093 acf = '/' + '/'.join(self.path_prefix) + acf 1094 if self.args: 1095 return acf 1096 return acf or '/'
1097
1098 - def acf(self):
1099 "convert components to /app/lang/controller/function" 1100 1101 if not routers: 1102 return None # use regex filter 1103 self.omit_lang() # try to omit language 1104 self.omit_acf() # try to omit a/c/f 1105 return self.build_acf() # build and return the /a/lang/c/f string
1106 1107
1108 -def map_url_in(request, env, app=False):
1109 "route incoming URL" 1110 1111 # initialize router-url object 1112 # 1113 thread.routes = params # default to base routes 1114 map = MapUrlIn(request=request, env=env) 1115 map.map_prefix() # strip prefix if present 1116 map.map_app() # determine application 1117 1118 # configure thread.routes for error rewrite 1119 # 1120 if params.routes_app: 1121 thread.routes = params_apps.get(app, params) 1122 1123 if app: 1124 return map.application 1125 1126 root_static_file = map.map_root_static() # handle root-static files 1127 if root_static_file: 1128 return (root_static_file, map.env) 1129 map.map_language() 1130 map.map_controller() 1131 static_file = map.map_static() 1132 if static_file: 1133 return (static_file, map.env) 1134 map.map_function() 1135 map.validate_args() 1136 map.update_request() 1137 return (None, map.env)
1138
1139 -def map_url_out(request, application, controller, function, args):
1140 ''' 1141 supply /a/c/f (or /a/lang/c/f) portion of outgoing url 1142 1143 The basic rule is that we can only make transformations 1144 that map_url_in can reverse. 1145 1146 Suppose that the incoming arguments are a,c,f,args,lang 1147 and that the router defaults are da, dc, df, dl. 1148 1149 We can perform these transformations trivially if args=[] and lang=None or dl: 1150 1151 /da/dc/df => / 1152 /a/dc/df => /a 1153 /a/c/df => /a/c 1154 1155 We would also like to be able to strip the default application or application/controller 1156 from URLs with function/args present, thus: 1157 1158 /da/c/f/args => /c/f/args 1159 /da/dc/f/args => /f/args 1160 1161 We use [applications] and [controllers] to suppress ambiguous omissions. 1162 1163 We assume that language names do not collide with a/c/f names. 1164 ''' 1165 map = MapUrlOut(application, controller, function, args, request) 1166 return map.acf()
1167
1168 -def get_effective_router(appname):
1169 "return a private copy of the effective router for the specified application" 1170 if not routers or appname not in routers: 1171 return None 1172 return Storage(routers[appname]) # return a copy
1173