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