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