diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/urlpublishing.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/views/urlpublishing.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,289 @@ +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""Associate url's path to view identifier / rql queries. + +CubicWeb finds all registered URLPathEvaluators, orders them according +to their ``priority`` attribute and calls their ``evaluate_path()`` +method. The first that returns something and doesn't raise a +``PathDontMatch`` exception wins. + +Here is the default evaluator chain: + +1. :class:`cubicweb.web.views.urlpublishing.RawPathEvaluator` handles + unique url segments that match exactly one of the registered + controller's *__regid__*. Urls such as */view?*, */edit?*, */json?* + fall in that category; + +2. :class:`cubicweb.web.views.urlpublishing.EidPathEvaluator` handles + unique url segments that are eids (e.g. */1234*); + +3. :class:`cubicweb.web.views.urlpublishing.URLRewriteEvaluator` + selects all urlrewriter components, sorts them according to their + priority, call their ``rewrite()`` method, the first one that + doesn't raise a ``KeyError`` wins. This is where the + :mod:`cubicweb.web.views.urlrewrite` and + :class:`cubicweb.web.views.urlrewrite.SimpleReqRewriter` comes into + play; + +4. :class:`cubicweb.web.views.urlpublishing.RestPathEvaluator` handles + urls based on entity types and attributes : ((/])?/)? This is why ``cwuser/carlos`` works; + +5. :class:`cubicweb.web.views.urlpublishing.ActionPathEvaluator` + handles any of the previous paths with an additional trailing + "/" segment, being one of the registered actions' + __regid__. + + +.. note:: + + Actionpath executes a query whose results is lost + because of redirecting instead of direct traversal. +""" +__docformat__ = "restructuredtext en" + +from rql import TypeResolverException + +from cubicweb import RegistryException +from cubicweb.web import NotFound, Redirect, component, views + + +class PathDontMatch(Exception): + """exception used by url evaluators to notify they can't evaluate + a path + """ + +class URLPublisherComponent(component.Component): + """Associate url path to view identifier / rql queries, by + applying a chain of urlpathevaluator components. + + An evaluator is a URLPathEvaluator subclass with an .evaluate_path + method taking the request object and the path to publish as + argument. It will either return a publishing method identifier + and an rql query on success or raise a `PathDontMatch` exception + on failure. URL evaluators are called according to their + `priority` attribute, with 0 as the greatest priority and greater + values as lower priority. The first evaluator returning a result + or raising something else than `PathDontMatch` will stop the + handlers chain. + """ + __regid__ = 'urlpublisher' + vreg = None # XXX necessary until property for deprecation warning is on appobject + + def __init__(self, vreg, default_method='view'): + super(URLPublisherComponent, self).__init__() + self.vreg = vreg + self.default_method = default_method + evaluators = [] + for evaluatorcls in vreg['components']['urlpathevaluator']: + # instantiation needed + evaluator = evaluatorcls(self) + evaluators.append(evaluator) + self.evaluators = sorted(evaluators, key=lambda x: x.priority) + + def process(self, req, path): + """Given a URL (essentially characterized by a path on the + server, but additional information may be found in the request + object), return a publishing method identifier + (e.g. controller) and an optional result set. + + :type req: `cubicweb.web.request.CubicWebRequestBase` + :param req: the request object + + :type path: str + :param path: the path of the resource to publish. If empty, None or "/" + "view" is used as the default path. + + :rtype: tuple(str, `cubicweb.rset.ResultSet` or None) + :return: the publishing method identifier and an optional result set + + :raise NotFound: if no handler is able to decode the given path + """ + parts = [part for part in path.split('/') + if part != ''] or (self.default_method,) + if req.form.get('rql'): + if parts[0] in self.vreg['controllers']: + return parts[0], None + return 'view', None + for evaluator in self.evaluators: + try: + pmid, rset = evaluator.evaluate_path(req, parts[:]) + break + except PathDontMatch: + continue + else: + raise NotFound(path) + if pmid is None: + pmid = self.default_method + return pmid, rset + + +class URLPathEvaluator(component.Component): + __abstract__ = True + __regid__ = 'urlpathevaluator' + vreg = None # XXX necessary until property for deprecation warning is on appobject + + def __init__(self, urlpublisher): + self.urlpublisher = urlpublisher + self.vreg = urlpublisher.vreg + + +class RawPathEvaluator(URLPathEvaluator): + """handle path of the form:: + + ?parameters... + """ + priority = 0 + def evaluate_path(self, req, parts): + if len(parts) == 1 and parts[0] in self.vreg['controllers']: + return parts[0], None + raise PathDontMatch() + + +class EidPathEvaluator(URLPathEvaluator): + """handle path with the form:: + + + """ + priority = 1 + def evaluate_path(self, req, parts): + if len(parts) != 1: + raise PathDontMatch() + try: + rset = req.execute('Any X WHERE X eid %(x)s', {'x': int(parts[0])}) + except ValueError: + raise PathDontMatch() + if rset.rowcount == 0: + raise NotFound() + return None, rset + + +class RestPathEvaluator(URLPathEvaluator): + """handle path with the form:: + + [[/]/]* + """ + priority = 3 + + def evaluate_path(self, req, parts): + if not (0 < len(parts) < 4): + raise PathDontMatch() + try: + etype = self.vreg.case_insensitive_etypes[parts.pop(0).lower()] + except KeyError: + raise PathDontMatch() + cls = self.vreg['etypes'].etype_class(etype) + if parts: + if len(parts) == 2: + attrname = parts.pop(0).lower() + try: + cls.e_schema.subjrels[attrname] + except KeyError: + raise PathDontMatch() + else: + attrname = cls.cw_rest_attr_info()[0] + value = req.url_unquote(parts.pop(0)) + return self.handle_etype_attr(req, cls, attrname, value) + return self.handle_etype(req, cls) + + def set_vid_for_rset(self, req, cls, rset): # cls is there to ease overriding + if rset.rowcount == 0: + raise NotFound() + if 'vid' not in req.form: + # check_table=False tells vid_from_rset not to try to use a table view if fetch_rql + # include some non final relation + req.form['vid'] = views.vid_from_rset(req, rset, req.vreg.schema, + check_table=False) + + def handle_etype(self, req, cls): + rset = req.execute(cls.fetch_rql(req.user)) + self.set_vid_for_rset(req, cls, rset) + return None, rset + + def handle_etype_attr(self, req, cls, attrname, value): + st = cls.fetch_rqlst(req.user, ordermethod=None) + st.add_constant_restriction(st.get_variable('X'), attrname, + 'x', 'Substitute') + if attrname == 'eid': + try: + rset = req.execute(st.as_string(), {'x': int(value)}) + except (ValueError, TypeResolverException): + # conflicting eid/type + raise PathDontMatch() + else: + rset = req.execute(st.as_string(), {'x': value}) + self.set_vid_for_rset(req, cls, rset) + return None, rset + + +class URLRewriteEvaluator(URLPathEvaluator): + """tries to find a rewrite rule to apply + + URL rewrite rule definitions are stored in URLRewriter objects + """ + priority = 2 + + def evaluate_path(self, req, parts): + # uri <=> req._twreq.path or req._twreq.uri + uri = req.url_unquote('/' + '/'.join(parts)) + evaluators = sorted(self.vreg['urlrewriting'].all_objects(), + key=lambda x: x.priority, reverse=True) + for rewritercls in evaluators: + rewriter = rewritercls(req) + try: + # XXX we might want to chain url rewrites + return rewriter.rewrite(req, uri) + except KeyError: + continue + raise PathDontMatch() + + +class ActionPathEvaluator(URLPathEvaluator): + """handle path with the form:: + + / + """ + priority = 4 + + def evaluate_path(self, req, parts): + if len(parts) < 2: + raise PathDontMatch() + # remove last part and see if this is something like an actions + # if so, call + # XXX bad smell: refactor to simpler code + try: + actionsreg = self.vreg['actions'] + requested = parts.pop(-1) + actions = actionsreg[requested] + except RegistryException: + raise PathDontMatch() + for evaluator in self.urlpublisher.evaluators: + if evaluator is self or evaluator.priority == 0: + continue + try: + pmid, rset = evaluator.evaluate_path(req, parts[:]) + except PathDontMatch: + continue + else: + try: + action = actionsreg._select_best(actions, req, rset=rset) + if action is not None: + raise Redirect(action.url()) + except RegistryException: + pass # continue searching + raise PathDontMatch()