Ejemplo n.º 1
0
def routing_map(modules, nodb_only, converters=None):
    routing_map = werkzeug.routing.Map(strict_slashes=False,
                                       converters=converters)
    for module in modules:
        if module not in controllers_per_module:
            continue

        for _, cls in controllers_per_module[module]:
            subclasses = cls.__subclasses__()
            subclasses = [
                c for c in subclasses
                if c.__module__.startswith('openerp.addons.')
                and c.__module__.split(".")[2] in modules
            ]
            if subclasses:
                name = "%s (extended by %s)" % (cls.__name__, ', '.join(
                    sub.__name__ for sub in subclasses))
                cls = type(name, tuple(reversed(subclasses)), {})

            o = cls()
            members = inspect.getmembers(o)
            for mk, mv in members:
                if inspect.ismethod(mv) and hasattr(mv, 'routing'):
                    routing = dict(type='http',
                                   auth='user',
                                   methods=None,
                                   routes=None)
                    methods_done = list()
                    for claz in reversed(mv.im_class.mro()):
                        fn = getattr(claz, mv.func_name, None)
                        if fn and hasattr(
                                fn, 'routing') and fn not in methods_done:
                            methods_done.append(fn)
                            routing.update(fn.routing)
                    if not nodb_only or nodb_only == (routing['auth']
                                                      == "none"):
                        assert routing[
                            'routes'], "Method %r has not route defined" % mv
                        endpoint = EndPoint(mv, routing)
                        for url in routing['routes']:
                            if routing.get("combine", False):
                                # deprecated
                                url = o._cp_path.rstrip(
                                    '/') + '/' + url.lstrip('/')
                                if url.endswith("/") and len(url) > 1:
                                    url = url[:-1]

                            routing_map.add(
                                werkzeug.routing.Rule(
                                    url,
                                    endpoint=endpoint,
                                    methods=routing['methods']))
    return routing_map
Ejemplo n.º 2
0
def routing_map(modules, nodb_only, converters=None):
    routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)

    def get_subclasses(klass):
        def valid(c):
            return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
        subclasses = klass.__subclasses__()
        result = []
        for subclass in subclasses:
            if valid(subclass):
                result.extend(get_subclasses(subclass))
        if not result and valid(klass):
            result = [klass]
        return result

    uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()

    for module in modules:
        if module not in controllers_per_module:
            continue

        for _, cls in controllers_per_module[module]:
            subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
            if subclasses:
                name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
                cls = type(name, tuple(reversed(subclasses)), {})

            o = cls()
            members = inspect.getmembers(o, inspect.ismethod)
            for _, mv in members:
                if hasattr(mv, 'routing'):
                    routing = dict(type='http', auth='user', methods=None, routes=None)
                    methods_done = list()
                    # update routing attributes from subclasses(auth, methods...)
                    for claz in reversed(mv.im_class.mro()):
                        fn = getattr(claz, mv.func_name, None)
                        if fn and hasattr(fn, 'routing') and fn not in methods_done:
                            methods_done.append(fn)
                            routing.update(fn.routing)
                    if not nodb_only or routing['auth'] == "none":
                        assert routing['routes'], "Method %r has not route defined" % mv
                        endpoint = EndPoint(mv, routing)
                        for url in routing['routes']:
                            if routing.get("combine", False):
                                # deprecated v7 declaration
                                url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
                                if url.endswith("/") and len(url) > 1:
                                    url = url[: -1]

                            xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split()
                            kw = {k: routing[k] for k in xtra_keys if k in routing}
                            routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw))
    return routing_map
Ejemplo n.º 3
0
def routing_map(modules, nodb_only, converters=None):
    routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
    for module in modules:
        if module not in controllers_per_module:
            continue

        for _, cls in controllers_per_module[module]:
            subclasses = cls.__subclasses__()
            subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
            if subclasses:
                name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
                cls = type(name, tuple(reversed(subclasses)), {})

            o = cls()
            members = inspect.getmembers(o)
            for mk, mv in members:
                if inspect.ismethod(mv) and hasattr(mv, 'routing'):
                    routing = dict(type='http', auth='user', methods=None, routes=None)
                    methods_done = list()
                    routing_type = None
                    for claz in reversed(mv.im_class.mro()):
                        fn = getattr(claz, mv.func_name, None)
                        if fn and hasattr(fn, 'routing') and fn not in methods_done:
                            fn_type = fn.routing.get('type')
                            if not routing_type:
                                routing_type = fn_type
                            else:
                                if fn_type and routing_type != fn_type:
                                    _logger.warn("Subclass re-defines <function %s.%s> with different type than original."
                                                    " Will use original type: %r", fn.__module__, fn.__name__, routing_type)
                                fn.routing['type'] = routing_type
                            fn.original_func.routing_type = routing_type
                            methods_done.append(fn)
                            routing.update(fn.routing)
                    if not nodb_only or nodb_only == (routing['auth'] == "none"):
                        assert routing['routes'], "Method %r has not route defined" % mv
                        endpoint = EndPoint(mv, routing)
                        for url in routing['routes']:
                            if routing.get("combine", False):
                                # deprecated
                                url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
                                if url.endswith("/") and len(url) > 1:
                                    url = url[: -1]

                            routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
    return routing_map
Ejemplo n.º 4
0
def route(route=None, **kw):
    """
    Decorator marking the decorated method as being a handler for
    requests. The method must be part of a subclass of ``Controller``.

    memcached.route instead of http.route

    :param route: string or array. The route part that will determine which
                  http requests will match the decorated method. Can be a
                  single string or an array of strings. See werkzeug's routing
                  documentation for the format of route expression (
                  http://werkzeug.pocoo.org/docs/routing/ ).
    :param type: The type of request, can be ``'http'`` or ``'json'``.
    :param auth: The type of authentication method, can on of the following:

                 * ``user``: The user must be authenticated and the current request
                   will perform using the rights of the user.
                 * ``public``: The user may or may not be authenticated. If she isn't,
                   the current request will perform using the shared Public user.
                 * ``none``: The method is always active, even if there is no
                   database. Mainly used by the framework and authentication
                   modules. There request code will not have any facilities to access
                   the database nor have any configuration indicating the current
                   database nor the current user.
    :param methods: A sequence of http methods this route applies to. If not
                    specified, all methods are allowed.
    :param cors: The Access-Control-Allow-Origin cors directive value.

---- Cache specific params

    :param max_age: Number of seconds that the page is permitted in clients cache , default 10 minutes
    :param cache_age: Number of seconds that the cache will live in memcached, default one day. ETag will be checked every 10 minutes.
    :param private: True if must not be stored by a shared cache
    :param key:     function that returns a string that is used as a raw key. The key can use some formats {db} {context} {uid} {logged_in}
    :param binary:  do not render page
    :param no_store:  do not store in cache
    :param immutable:  Indicates that the response body will not change over time. The resource, if unexpired, is unchanged on the server and therefore the client should not send a conditional revalidation.  immutable is only honored on https:// transactions
    :param no_transform: No transformations or conversions should be made to the resource (for example do not transform png to jpeg)
    :param s_maxage:  Overrides max-age, but it only applies to shared caches / proxies and is ignored by a private cache
    :param content_type: set Content-Type
    :param no-store and no-cache "no-cache" indicates that the returned response can't be used to satisfy a subsequent request to the same URL without first checking with the server if the response has changed. As a result, if a proper validation token (ETag) is present, no-cache incurs a roundtrip to validate the cached response, but can eliminate the download if the resource has not changed. By contrast, "no-store" is much simpler. It simply disallows the browser and all intermediate caches from storing any version of the returned response—for example, one containing private personal or banking data. Every time the user requests this asset, a request is sent to the server and a full response is downloaded.
    :param immutable http://bitsup.blogspot.com/2016/05/cache-control-immutable.html
    :param  must-revalidate and proxy-revalidate
    :
    """
    # Default values for routing parameters
    routing = {
        'no_cache': True,
        'must_revalidate': True,
        'proxy_revalidate': True,
    }
    routing.update(kw)
    assert not 'type' in routing or routing['type'] in ("http", "json")
    def decorator(f):
        if route:
            if isinstance(route, list):
                routes = route
            else:
                routes = [route]
            routing['routes'] = routes
        @functools.wraps(f)
        def response_wrap(*args, **kw):
            #~ _logger.warn('\n\npath: %s\n' % request.httprequest.path)
            if routing.get('key'): # Function that returns a raw string for key making
                # Format {path}{session}{etc}
                key_raw = routing['key'](kw).format(path=request.httprequest.path,
                                                    session='%s' % {k:v for k,v in request.session.items() if len(k)<40},
                                                    device_type='%s' % request.session.get('device_type','md'),  # xs sm md lg
                                                    context='%s' % {k:v for k,v in request.env.context.items() if not k == 'uid'},
                                                    context_uid='%s' % {k:v for k,v in request.env.context.items()},
                                                    uid=request.env.context.get('uid'),
                                                    logged_in='1' if request.env.context.get('uid') > 0 else '0',
                                                    db=request.env.cr.dbname,
                                                    lang=request.env.context.get('lang'),
                                                    post='%s' % kw,
                                                    xmlid='%s' % kw.get('xmlid'),
                                                    version='%s' % kw.get('version'),
                                                    publisher='1' if request.env.ref('base.group_website_publisher') in request.env.user.groups_id else '0',
                                                    designer='1' if request.env.ref('base.group_website_designer') in request.env.user.groups_id else '0',
                                                    ).encode('latin-1')
                #~ raise Warning(request.env['res.users'].browse(request.uid).group_ids)
                key = str(MEMCACHED_HASH(key_raw))
            else:
                key_raw = ('%s,%s,%s' % (request.env.cr.dbname,request.httprequest.path,request.env.context)).encode('latin-1') # Default key
                key = str(MEMCACHED_HASH(key_raw))

############### Key is ready

            if 'cache_invalidate' in kw.keys():
                kw.pop('cache_invalidate',None)
                mc_delete(key)


            page_dict = None
            error = None
            try:
                page_dict = mc_load(key)
            except MemcacheClientError as e:
                error = "MemcacheClientError %s " % e
                _logger.warn(error)
            except MemcacheUnknownCommandError as e:
                error = "MemcacheUnknownCommandError %s " % e
                _logger.warn(error)
            except MemcacheIllegalInputError as e:
                error = "MemcacheIllegalInputError %s " % e
                _logger.warn(error)
            except MemcacheServerError as e:
                error = "MemcacheServerError %s " % e
                _logger.warn(error)
            except MemcacheUnknownError as e:
                error = clean_text(str(e))
                _logger.warn("MemcacheUnknownError %s key: %s path: %s" % (eror, key, request.httprequest.path))
                return werkzeug.wrappers.Response(status=500,headers=[
                        ('X-CacheKey',key),
                        ('X-CacheError','MemcacheUnknownError %s' %error),
                        ('X-CacheKeyRaw',key_raw),
                        ('Server','Odoo %s Memcached %s' % (common.exp_version().get('server_version'), MEMCACHED_VERSION)),
                        ])
            except MemcacheUnexpectedCloseError as e:
                error = "MemcacheUnexpectedCloseError %s " % e
                _logger.warn(error)
            except Exception as e:
                err = sys.exc_info()
                # ~ error = "Memcached Error %s key: %s path: %s %s" % (e,key,request.httprequest.path, ''.join(traceback.format_exception(err[0], err[1], err[2])))
                error = clean_text(''.join(traceback.format_exception(err[0], err[1], err[2])))
                _logger.warn("Memcached Error %s key: %s path: %s" % (error, key, request.httprequest.path))
                error = clean_text(str(e))
                return werkzeug.wrappers.Response(status=500,headers=[
                        ('X-CacheKey',key),
                        ('X-CacheError','Memcached Error %s' % error),
                        ('X-CacheKeyRaw',key_raw),
                        ('Server','Odoo %s Memcached %s' % (common.exp_version().get('server_version'), MEMCACHED_VERSION)),
                        ])

            if page_dict and not page_dict.get('db') == request.env.cr.dbname:
                _logger.warn('Database violation key=%s stored for=%s  env db=%s ' % (key,page_dict.get('db'),request.env.cr.dbname))
                page_dict = None

            # Blacklist
            if page_dict and any([p if p in request.httprequest.path else '' for p in kw.get('blacklist','').split(',')]):
                page_dict = None

            if 'cache_viewkey' in kw.keys():
                if page_dict:
                    res = mc_meta(key)
                    view_meta = '<h2>Metadata</h2><table>%s</table>' % ''.join(['<tr><td>%s</td><td>%s</td></tr>' % (k,v) for k,v in res['page_dict'].items() if not k == 'page'])
                    view_meta += 'Page Len : %.2f Kb<br>'  % res['size']
                    #~ view_meta += 'Chunks   : %s<br>' % ', '.join([len(c) for c in res['chunks']])
                    #~ view_meta += 'Chunks   : %s<br>' % res['chunks']
                    return http.Response('<h1>Key <a href="/mcpage/%s">%s</a></h1>%s' % (key,key,view_meta))
                else:
                    if error:
                        error = '<h1>Error</h1><h2>%s</h2>' % error
                    return http.Response('%s<h1>Key is missing %s</h1>' % (error if error else '',key))

            if routing.get('add_key') and not 'cache_key' in kw.keys():
                #~ raise Warning(args,kw,request.httprequest.args.copy())
                args = request.httprequest.args.copy()
                args['cache_key'] = key
                return werkzeug.utils.redirect('{}?{}'.format(request.httprequest.path, url_encode(args)), 302)

            max_age = routing.get('max_age',600)              # 10 minutes
            cache_age = routing.get('cache_age',24 * 60 * 60) # One day
            s_maxage =  routing.get('s_maxage',max_age)
            page = None

            if not page_dict:
                page_dict = {}
                controller_start = timer()
                response = f(*args, **kw) # calls original controller
                render_start = timer()

                if routing.get('content_type'):
                    response.headers['Content-Type'] = routing.get('content_type')
                    #~ if isinstance(response.headers,list) and isinstance(response.headers[0],tuple):
                        #~ _logger.error('response is list and tuple')
                    #~ header_dict = {h[0]: h[1] for h in response.headers}
                    #~ header_dict['Content-Type'] = routing.get('content_type')
                    #~ response.headers = [(h[0],h[1]) for h in header_dict.items()]

                if response.template:
                    #~ _logger.error('template %s values %s response %s' % (response.template,response.qcontext,response.response))
                    page = response.render()
                else:
                    page = ''.join(response.response)
                page_dict = {
                    'max-age':  max_age,
                    'cache-age':cache_age,
                    'private':  routing.get('private',False),
                    'key_raw':  key_raw,
                    'render_time': '%.3f sec' % (timer()-render_start),
                    'controller_time': '%.3f sec' % (render_start-controller_start),
                    'path':     request.httprequest.path,
                    'db':       request.env.cr.dbname,
                    'page':     base64.b64encode(page),
                    'date':     http_date(),
                    'module':   f.__module__,
                    'status_code': response.status_code,
                    'flush_type': routing['flush_type'](kw).lower().encode('ascii', 'replace').replace(' ', '-') if routing.get('flush_type', None) else "",
                    'headers': response.headers,
                    }
                if routing.get('no_cache'):
                    page_dict['ETag'] = '%s' % MEMCACHED_HASH(page)
                # ~ _logger.warn('\n\npath: %s\nstatus_code: %s\nETag: %s\n' % (page_dict.get('path'), page_dict.get('status_code'), page_dict.get('ETag')))
                mc_save(key, page_dict,cache_age)
                if routing.get('flush_type'):
                    add_flush_type(request.cr.dbname, routing['flush_type'](kw))
                #~ raise Warning(f.__module__,f.__name__,route())
            else:
                request_dict = {h[0]: h[1] for h in request.httprequest.headers}
                #~ _logger.warn('Page Exists If-None-Match %s Etag %s' %(request_dict.get('If-None-Match'), page_dict.get('ETag')))
                if request_dict.get('If-None-Match') and (request_dict.get('If-None-Match') == page_dict.get('ETag')):
                    header = [
                        ('X-CacheETag', page_dict.get('ETag')),
                        ('X-CacheKey',key),
                        ('X-Cache','newly rendered' if page else 'from cache'),
                        ('X-CacheKeyRaw',key_raw),
                        ('X-CacheController',page_dict.get('controller_time')),
                        ('X-CacheRender',page_dict.get('render_time')),
                        ('X-CacheCacheAge',cache_age),
                        ('Server','Odoo %s Memcached %s' % (common.exp_version().get('server_version'), MEMCACHED_VERSION)),
                        ]
                    header += [(k,v) for k,v in page_dict.get('headers',[(None,None)])]
                    _logger.warn('returns 304 headers %s' % header)
                    if page_dict.get('status_code') in [301, 302, 307, 308]:
                        return werkzeug.wrappers.Response(status=page_dict['status_code'],headers=header)
                    return werkzeug.wrappers.Response(status=304,headers=header)
            response = http.Response(base64.b64decode(page_dict.get('page'))) # always create a new response (drop response from controller)

            # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
            # https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching
            # https://jakearchibald.com/2016/caching-best-practices/
            #~ if page_dict.get('headers') and isinstance(page_dict['headers'],dict):
                #~ _logger.error('respnse headers dict')
                #~ for k,v in page_dict['headers'].items():
                    #~ response.headers.add(k,v)
                   #response.headers[k] = v
            #~ if page_dict.get('headers') and isinstance(page_dict['headers'],list):
                #~ _logger.error('respnse headers list')

                #~ response.headers = {h[0]: h[1] for h in response.headers}
            if page_dict.get('headers'):
                for k,v in page_dict['headers'].items():
                    #~ response.headers.add(k,v)
                    response.headers[k] = v
            response.headers['Cache-Control'] ='max-age=%s,s-maxage=%s, %s' % (max_age, s_maxage, ','.join([keyword for keyword in ['no-store', 'immutable', 'no-transform', 'no-cache', 'must-revalidate', 'proxy-revalidate'] if routing.get(keyword.replace('-', '_'))] + [routing.get('private', 'public')])) # private: must not be stored by a shared cache.
            if page_dict.get('ETag'):
                response.headers['ETag'] = page_dict.get('ETag')
            response.headers['X-CacheKey'] = key
            response.headers['X-Cache'] = 'newly rendered' if page else 'from cache'
            response.headers['X-CacheKeyRaw'] = key_raw
            response.headers['X-CacheController'] = page_dict.get('controller_time')
            response.headers['X-CacheRender'] = page_dict.get('render_time')
            response.headers['X-CacheCacheAge'] = cache_age
            response.headers['X-CacheBlacklist'] = kw.get('blacklist','')
            response.headers['Date'] = page_dict.get('date',http_date())
            response.headers['Server'] = 'Odoo %s Memcached %s' % (common.exp_version().get('server_version'), MEMCACHED_VERSION)
            response.status_code = page_dict.get('status_code', 200)
            return response

        response_wrap.routing = routing
        response_wrap.original_func = f
        return response_wrap
    return decorator