diff --git a/CHANGES.rst b/CHANGES.rst index d193557..0087d3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,10 @@ Changelog 5.6.4 (unreleased) ------------------ -- Nothing changed yet. +- Add experimental.noacquisition as dependency. + [cekk] +- Patch absolutize_path method to disable acquisition when checking aliases. + [cekk] 5.6.3 (2024-12-02) diff --git a/setup.py b/setup.py index 8ed820e..a652160 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ "Products.PortalTransforms>=3.2.0", "collective.volto.sitesettings", "z3c.jbot", + "experimental.noacquisition", ], extras_require={ "advancedquery": [ diff --git a/src/redturtle/volto/__init__.py b/src/redturtle/volto/__init__.py index 86397e0..a4cd5e1 100644 --- a/src/redturtle/volto/__init__.py +++ b/src/redturtle/volto/__init__.py @@ -1,77 +1,6 @@ # -*- coding: utf-8 -*- -"""Init and utils.""" -from plone.app.content.browser.vocabulary import PERMISSIONS -from plone.folder.nogopip import GopipIndex -from Products.ZCatalog.Catalog import Catalog -from redturtle.volto.catalogplan import Catalog_sorted_search_indexes +from redturtle.volto import patches # noqa from zope.i18nmessageid import MessageFactory -from ZTUtils.Lazy import LazyCat -from ZTUtils.Lazy import LazyMap - -import logging - - -logger = logging.getLogger(__name__) _ = MessageFactory("redturtle.volto") - -PERMISSIONS["plone.app.vocabularies.Keywords"] = "View" - -# CATALOG PATCHES - -logger.info( - "install monkey patch for Products.ZCatalog.Catalog.Catalog._sorted_search_indexes #### WORK IN PROGRESS ####" -) -Catalog._orig_sorted_search_indexes = Catalog._sorted_search_indexes -Catalog._sorted_search_indexes = Catalog_sorted_search_indexes - -MAX_SORTABLE = 5000 - - -def Catalog_sortResults( - self, - rs, - sort_index, - reverse=False, - limit=None, - merge=True, - actual_result_count=None, - b_start=0, - b_size=None, -): - if MAX_SORTABLE > 0: - if actual_result_count is None: - actual_result_count = len(rs) - if actual_result_count >= MAX_SORTABLE and isinstance(sort_index, GopipIndex): - logger.warning( - "too many results %s disable GopipIndex sorting", actual_result_count - ) - switched_reverse = bool( - b_size and b_start and b_start > actual_result_count / 2 - ) - if hasattr(rs, "keys"): - sequence, slen = self._limit_sequence( - rs.keys(), actual_result_count, b_start, b_size, switched_reverse - ) - return LazyMap( - self.__getitem__, - sequence, - len(sequence), - actual_result_count=actual_result_count, - ) - else: - logger.error( - "too many results %s disable GopipIndex sorting results %s has no key", - actual_result_count, - type(rs), - ) - return LazyCat([], 0, actual_result_count) - return self._orig_sortResults( - rs, sort_index, reverse, limit, merge, actual_result_count, b_start, b_size - ) - - -logger.info("install monkey patch for Products.ZCatalog.Catalog.Catalog.sortResults") -Catalog._orig_sortResults = Catalog.sortResults -Catalog.sortResults = Catalog_sortResults diff --git a/src/redturtle/volto/patches.py b/src/redturtle/volto/patches.py new file mode 100644 index 0000000..b0ef529 --- /dev/null +++ b/src/redturtle/volto/patches.py @@ -0,0 +1,169 @@ +# These are patches not managed by collective.monkeypatcher +from Products.CMFPlone.controlpanel.browser import redirects +from plone.app.redirector.interfaces import IRedirectionStorage +from Products.CMFCore.utils import getToolByName +from urllib.parse import urlparse +from zope.component import getUtility +from zope.component.hooks import getSite +from zope.i18nmessageid import MessageFactory +from plone.app.content.browser.vocabulary import PERMISSIONS +from plone.folder.nogopip import GopipIndex +from Products.ZCatalog.Catalog import Catalog +from redturtle.volto.catalogplan import Catalog_sorted_search_indexes +from ZTUtils.Lazy import LazyCat +from ZTUtils.Lazy import LazyMap +from experimental.noacquisition import config + + +import logging + +logger = logging.getLogger(__name__) + +_ = MessageFactory("plone") + + +def absolutize_path_patched(path, is_source=True): + """Create path including the path of the portal root. + + The path must be absolute, so starting with a slash. + Or it can be a full url. + + If is_source is true, this is an alternative url + that will point to a target (unknown here). + + If is_source is true, path is the path of a target. + An object must exist at this path, unless it is a full url. + + Return a 2-tuple: (absolute redirection path, + an error message if something goes wrong and otherwise ''). + """ + + portal = getSite() + err = None + is_external_url = False + if not path: + if is_source: + err = _("You have to enter an alternative url.") + else: + err = _("You have to enter a target.") + elif not path.startswith("/"): + if is_source: + err = _("Alternative url path must start with a slash.") + else: + # For targets, we accept external urls. + # Do basic check. + parsed = urlparse(path) + if parsed.scheme in ("https", "http") and parsed.netloc: + is_external_url = True + else: + err = _("Target path must start with a slash.") + elif "@@" in path: + if is_source: + err = _("Alternative url path must not be a view.") + else: + err = _("Target path must not be a view.") + else: + context_path = "/".join(portal.getPhysicalPath()) + path = f"{context_path}{path}" + if not err and not is_external_url: + catalog = getToolByName(portal, "portal_catalog") + if is_source: + # Check whether already exists in storage + storage = getUtility(IRedirectionStorage) + if storage.get(path): + err = _("The provided alternative url already exists!") + else: + # Check whether obj exists at source path. + # A redirect would be useless then. + + ### THIS IS THE PATCH ### + # unrestrictedTraverse returns the object with acquisition, so if + # you have a content with path /Plone/aaa and try to call unrestrictedTraverse + # with /Plone/aaa/aaa/aaa/aaa it will always return /Plone/aaa object + # and this is not correct because we could want to create an alias for /Plone/aaa + # that is /Plone/aaa/aaa + # In Plone7 this will not be a problem anymore because of this: + # https://github.com/plone/Products.CMFPlone/issues/3871 + item = portal.unrestrictedTraverse(path, None) + # if item is not None: this is the original check + if item is not None and "/".join(item.getPhysicalPath()) == path: + # if paths are different, it's an acquisition false positive, + # so go on and let create the alias + err = _("Cannot use a working path as alternative url.") + ### END OF THE PATCH ### + else: + # Check whether obj exists at target path + result = catalog.searchResults(path={"query": path}) + if len(result) == 0: + err = _("The provided target object does not exist.") + + return path, err + + +MAX_SORTABLE = 5000 + + +def Catalog_sortResults( + self, + rs, + sort_index, + reverse=False, + limit=None, + merge=True, + actual_result_count=None, + b_start=0, + b_size=None, +): + if MAX_SORTABLE > 0: + if actual_result_count is None: + actual_result_count = len(rs) + if actual_result_count >= MAX_SORTABLE and isinstance(sort_index, GopipIndex): + logger.warning( + "too many results %s disable GopipIndex sorting", actual_result_count + ) + switched_reverse = bool( + b_size and b_start and b_start > actual_result_count / 2 + ) + if hasattr(rs, "keys"): + sequence, slen = self._limit_sequence( + rs.keys(), actual_result_count, b_start, b_size, switched_reverse + ) + return LazyMap( + self.__getitem__, + sequence, + len(sequence), + actual_result_count=actual_result_count, + ) + else: + logger.error( + "too many results %s disable GopipIndex sorting results %s has no key", + actual_result_count, + type(rs), + ) + return LazyCat([], 0, actual_result_count) + return self._orig_sortResults( + rs, sort_index, reverse, limit, merge, actual_result_count, b_start, b_size + ) + + +# apply patches +logger.info( + "install monkey patch for Products.ZCatalog.Catalog.Catalog._sorted_search_indexes #### WORK IN PROGRESS ####" +) +Catalog._orig_sorted_search_indexes = Catalog._sorted_search_indexes +Catalog._sorted_search_indexes = Catalog_sorted_search_indexes + +logger.info("install monkey patch for Products.ZCatalog.Catalog.Catalog.sortResults") +Catalog._orig_sortResults = Catalog.sortResults +Catalog.sortResults = Catalog_sortResults + +logger.info("install monkey patch for plone.app.content.browser.vocabulary.PERMISSIONS") +PERMISSIONS["plone.app.vocabularies.Keywords"] = "View" + +logger.info( + "install monkey patch for from Products.CMFPlone.controlpanel.browser.redirects.absolutize_path" +) +redirects.absolutize_path = absolutize_path_patched + +logger.info("enable experimental.noacquisition") +# config.DRYRUN = False diff --git a/src/redturtle/volto/restapi/services/search/get.py b/src/redturtle/volto/restapi/services/search/get.py index 5ba222f..dc90864 100644 --- a/src/redturtle/volto/restapi/services/search/get.py +++ b/src/redturtle/volto/restapi/services/search/get.py @@ -4,12 +4,15 @@ from plone.restapi.search.handler import SearchHandler as OriginalHandler from plone.restapi.search.utils import unflatten_dotted_dict from plone.restapi.services import Service -from redturtle.volto import logger from redturtle.volto.config import MAX_LIMIT from redturtle.volto.interfaces import IRedTurtleVoltoSettings from zope.component import getMultiAdapter +import logging + +logger = logging.getLogger(__name__) + # search for 'ranking' in 'SearchableText' and rank very high # when the term is in 'Subject' and high when it is in 'Title'. # print the id and the normalized rank