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