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, read_file 
  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 exclusive_domain = False, 48 map_hyphen = False, 49 acfe_match = r'\w+$', # legal app/ctlr/fcn/ext 50 file_match = r'(\w+[-=./]?)+$', # legal file (path) name 51 args_match = r'([\w@ -]+[=.]?)*$', # legal arg in args 52 ) 53 return router
54
55 -def _params_default(app=None):
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) # regex rewrite parameters 76 thread.routes = params # default to base regex rewrite parameters 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 # The external interface to rewrite consists of: 88 # 89 # load: load routing configuration file(s) 90 # url_in: parse and rewrite incoming URL 91 # url_out: assemble and rewrite outgoing URL 92 # 93 # thread.routes.default_application 94 # thread.routes.error_message 95 # thread.routes.error_message_ticket 96 # thread.routes.try_redirect_on_error 97 # thread.routes.error_handler 98 # 99 # filter_url: helper for doctest & unittest 100 # filter_err: helper for doctest & unittest 101 # regex_filter_out: doctest 102
103 -def url_in(request, environ):
104 "parse and rewrite incoming URL" 105 if routers: 106 return map_url_in(request, environ) 107 return regex_url_in(request, environ)
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 # fill in scheme and host if absolute URL is requested 119 # scheme can be a string, eg 'http', 'https', 'ws', 'wss' 120 # 121 if scheme or port is not None: 122 if host is None: # scheme or port implies host 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' # some reasonable default in case we need it 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
140 -def try_redirect_on_error(http_object, request, ticket=None):
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 # reinitialize 176 global params_apps 177 params_apps = dict() 178 params = _params_default(app=None) # regex rewrite parameters 179 thread.routes = params # default to base regex rewrite parameters 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 # install base rewrite parameters 225 thread.routes = params # install default as current routes 226 # 227 # create the BASE router if routers in use 228 # 229 routers = params.routers # establish routers if present 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 # scan each app in applications/ 239 # create a router, if routers are in use 240 # parse the app-specific routes.py if present 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) # new copy 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: # app 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
273 -def compile_regex(k, v):
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 # original k for error reporting 284 # bracket regex in ^...$ if not already done 285 if not k[0] == '^': 286 k = '^%s' % k 287 if not k[-1] == '$': 288 k = '%s$' % k 289 # if there are no :-separated parts, prepend a catch-all for the IP address 290 if k.find(':') < 0: 291 # k = '^.*?:%s' % k[1:] 292 k = '^.*?:https?://[^:/]+:[a-z]+ %s' % k[1:] 293 # if there's no ://, provide a catch-all for the protocol, host & method 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 # $anything -> ?P<anything>.* 300 for item in regex_anything.findall(k): 301 k = k.replace(item, '(?P<anything>.*)') 302 # $a (etc) -> ?P<a>\w+ 303 for item in regex_at.findall(k): 304 k = k.replace(item, r'(?P<%s>\w+)' % item[1:]) 305 # same for replacement pattern, but with \g 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
310 -def load_routers(all_apps):
311 "load-time post-processing of routers" 312 313 for app in routers.keys(): 314 # initialize apps with routers that aren't present, on behalf of unit tests 315 if app not in all_apps: 316 all_apps.append(app) 317 router = Storage(routers.BASE) # new copy 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 # set router name 367 router = routers[app] 368 router.name = app 369 # compile URL validation patterns 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 # convert path_prefix to a list of path elements 375 if router.path_prefix: 376 if isinstance(router.path_prefix, str): 377 router.path_prefix = router.path_prefix.strip('/').split('/') 378 379 # rewrite BASE.domains as tuples 380 # 381 # key: 'domain[:port]' -> (domain, port) 382 # value: 'application[/controller] -> (application, controller) 383 # (port and controller may be None) 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
418 -def regex_select(env=None, app=None, request=None):
419 """ 420 select a set of regex rewrite params for the current request 421 """ 422 if app: 423 thread.routes = params_apps.get(app, params) 424 elif env and params.routes_app: 425 if routers: 426 map_url_in(request, env, app=True) 427 else: 428 app = regex_uri(env, params.routes_app, "routes_app") 429 thread.routes = params_apps.get(app, params) 430 else: 431 thread.routes = params # default to base rewrite parameters 432 logger.debug("select routing parameters: %s" % thread.routes.name) 433 return app # for doctest
434
435 -def regex_filter_in(e):
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 # pattern to replace spaces with underscore in URL 454 # also the html escaped variants '+' and '%20' are covered 455 regex_space = re.compile('(\+|\s|%20)+') 456 457 # pattern to find valid paths in url /application/controller/... 458 # this could be: 459 # for static pages: 460 # /<b:application>/static/<x:file> 461 # for dynamic pages: 462 # /<a:application>[/<c:controller>[/<f:function>[.<e:ext>][/<s:args>]]] 463 # application, controller, function and ext may only contain [a-zA-Z0-9_] 464 # file and args may also contain '-', '=', '.' and '/' 465 # apps in routes_apps_raw must parse raw_args into args 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
504 -def regex_url_in(request, environ):
505 "rewrite and parse incoming URL" 506 507 # ################################################## 508 # select application 509 # rewrite URL if routes_in is defined 510 # update request.env 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 # serve if a static file 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 # parse application, controller and function 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 # application is responsible for parsing args 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() # adjust for trailing empty arg 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
574 -def regex_filter_out(url, e=None):
575 "regex rewrite outgoing URL" 576 if not hasattr(thread, 'routes'): 577 regex_select() # ensure thread.routes is set (for application threads) 578 if routers: 579 return url # already filtered 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) # simulate server 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 #for filter_out request.env use lowercase 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 # 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, 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 # 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 = 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 # to handle empty args, strip exactly one trailing slash, if present 734 # .../arg1// represents one trailing empty arg 735 # 736 if path.endswith('/'): 737 path = path[:-1] 738 self.args = List(path and path.split('/') or []) 739 740 # see http://www.python.org/dev/peps/pep-3333/#url-reconstruction for URL composition 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
760 - def map_prefix(self):
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 # prefix didn't match 770 self.args = List(self.args[prefixlen:]) # strip the prefix
771
772 - def map_app(self):
773 "determine application name" 774 base = routers.BASE # base router 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 # set the application router 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 # support gluon.main.wsgibase init->welcome 809 else: 810 self.router = routers[self.application] # application router 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
822 - def map_root_static(self):
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
838 - def map_language(self):
839 "handle language (no hyphen mapping)" 840 arg0 = self.arg0 # no hyphen mapping 841 if arg0 and self.languages and arg0 in self.languages: 842 self.language = arg0 843 else: 844 self.language = self.default_language 845 if self.language: 846 logger.debug("route: language=%s" % self.language) 847 self.pop_arg_if(self.language == arg0) 848 arg0 = self.arg0
849
850 - def map_controller(self):
851 "identify controller" 852 # handle controller 853 # 854 arg0 = self.harg0 # map hyphens 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
865 - def map_static(self):
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 # support language-specific static subdirectories, 878 # eg /appname/en/static/filename => applications/appname/static/en/filename 879 # if language-specific file doesn't exist, try same file in static 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
892 - def map_function(self):
893 "handle function.extension" 894 arg0 = self.harg0 # map hyphens 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
915 - def validate_args(self):
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
924 - def update_request(self):
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
951 - def arg0(self):
952 "return first arg" 953 return self.args(0)
954 955 @property
956 - def harg0(self):
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
962 - def pop_arg_if(self, dopop):
963 "conditionally remove first arg and return new first arg" 964 if dopop: 965 self.args.pop(0)
966
967 -class MapUrlOut(object):
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
1016 - def omit_lang(self):
1017 "omit language if possible" 1018 1019 if not self.language or self.language == self.default_language: 1020 self.omit_language = True
1021
1022 - def omit_acf(self):
1023 "omit what we can of a/c/f" 1024 1025 router = self.router 1026 1027 # Handle the easy no-args case of tail-defaults: /a/c /a / 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 # omit default application 1037 # (which might be the domain default application) 1038 # 1039 default_application = self.domain_application or self.default_application 1040 if self.application == default_application: 1041 self.omit_application = True 1042 1043 # omit controller if default controller 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 # omit function if default controller/function 1050 # 1051 if self.functions and self.function == self.default_function and self.omit_controller: 1052 self.omit_function = True 1053 1054 # prohibit ambiguous cases 1055 # 1056 # because we presume the lang string to be unambiguous, its presence protects application omission 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 # handle static as a special case 1077 # (easier for external static handling) 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
1087 - def build_acf(self):
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
1109 - def acf(self):
1110 "convert components to /app/lang/controller/function" 1111 1112 if not routers: 1113 return None # use regex filter 1114 self.omit_lang() # try to omit language 1115 self.omit_acf() # try to omit a/c/f 1116 return self.build_acf() # build and return the /a/lang/c/f string
1117 1118
1119 -def map_url_in(request, env, app=False):
1120 "route incoming URL" 1121 1122 # initialize router-url object 1123 # 1124 thread.routes = params # default to base routes 1125 map = MapUrlIn(request=request, env=env) 1126 map.map_prefix() # strip prefix if present 1127 map.map_app() # determine application 1128 1129 # configure thread.routes for error rewrite 1130 # 1131 if params.routes_app: 1132 thread.routes = params_apps.get(app, params) 1133 1134 if app: 1135 return map.application 1136 1137 root_static_file = map.map_root_static() # handle root-static files 1138 if root_static_file: 1139 return (root_static_file, map.env) 1140 map.map_language() 1141 map.map_controller() 1142 static_file = map.map_static() 1143 if static_file: 1144 return (static_file, map.env) 1145 map.map_function() 1146 map.validate_args() 1147 map.update_request() 1148 return (None, map.env)
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
1179 -def get_effective_router(appname):
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]) # return a copy
1184