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