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
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
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
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