Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental.noacquisition #126

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"Products.PortalTransforms>=3.2.0",
"collective.volto.sitesettings",
"z3c.jbot",
"experimental.noacquisition",
],
extras_require={
"advancedquery": [
Expand Down
73 changes: 1 addition & 72 deletions src/redturtle/volto/__init__.py
Original file line number Diff line number Diff line change
@@ -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
169 changes: 169 additions & 0 deletions src/redturtle/volto/patches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# These are patches not managed by collective.monkeypatcher
from experimental.noacquisition import config
from plone.app.content.browser.vocabulary import PERMISSIONS
from plone.app.redirector.interfaces import IRedirectionStorage
from plone.folder.nogopip import GopipIndex
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.controlpanel.browser import redirects
from Products.ZCatalog.Catalog import Catalog
from redturtle.volto.catalogplan import Catalog_sorted_search_indexes
from urllib.parse import urlparse
from zope.component import getUtility
from zope.component.hooks import getSite
from zope.i18nmessageid import MessageFactory
from ZTUtils.Lazy import LazyCat
from ZTUtils.Lazy import LazyMap

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 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
5 changes: 4 additions & 1 deletion src/redturtle/volto/restapi/services/search/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
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'.
Expand Down
3 changes: 3 additions & 0 deletions src/redturtle/volto/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import collective.volto.gdprcookie
import collective.volto.sitesettings
import experimental.noacquisition
import kitconcept.seo
import plone.app.caching
import plone.restapi
Expand All @@ -31,6 +32,7 @@ def setUpZope(self, app, configurationContext):
self.loadZCML(package=plone.volto)
self.loadZCML(package=plone.app.caching)
self.loadZCML(package=kitconcept.seo)
self.loadZCML(package=experimental.noacquisition)

def setUpPloneSite(self, portal):
applyProfile(portal, "plone.app.caching:default")
Expand Down Expand Up @@ -75,6 +77,7 @@ def setUpZope(self, app, configurationContext):
self.loadZCML(package=redturtle.volto)
self.loadZCML(package=plone.app.caching)
self.loadZCML(package=kitconcept.seo)
self.loadZCML(package=experimental.noacquisition)

def setUpPloneSite(self, portal):
applyProfile(portal, "plone.app.caching:default")
Expand Down
10 changes: 10 additions & 0 deletions src/redturtle/volto/tests/test_blocks_linkintegrity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from importlib import import_module
from plone import api
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
Expand All @@ -10,6 +11,11 @@
import unittest


HAS_PLONE_6 = getattr(
import_module("Products.CMFPlone.factory"), "PLONE60MARKER", False
)


class TestBlocksLinkIntegrity(unittest.TestCase):
layer = REDTURTLE_VOLTO_FUNCTIONAL_TESTING

Expand Down Expand Up @@ -704,6 +710,10 @@ def test_count_down_countdown_text_link_integrity(self):
self.assertEqual(reference["sources"][0]["uid"], self.document.UID())
self.assertEqual(reference["target"]["uid"], self.ref.UID())

@unittest.skipIf(
not HAS_PLONE_6,
"This test is only intended to run for Plone 6 and DX site root enabled",
)
def test_linkintegrity_works_also_on_site_root(self):
self.assertEqual(self.get_references(), [])
self.portal.blocks = {
Expand Down
39 changes: 39 additions & 0 deletions src/redturtle/volto/tests/test_noacquisition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from plone import api
from plone.app.testing import setRoles
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
from plone.app.testing import TEST_USER_ID
from plone.restapi.testing import RelativeSession
from redturtle.volto.testing import REDTURTLE_VOLTO_API_FUNCTIONAL_TESTING
from transaction import commit

import unittest


class TestNoAcquisition(unittest.TestCase):
layer = REDTURTLE_VOLTO_API_FUNCTIONAL_TESTING

def setUp(self):
self.app = self.layer["app"]
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.portal_url = self.portal.absolute_url()
setRoles(self.portal, TEST_USER_ID, ["Manager"])

self.api_session = RelativeSession(self.portal_url)
self.api_session.headers.update({"Accept": "application/json"})
self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)

self.document = api.content.create(
container=self.portal, type="Document", title="aaa"
)

self.child = api.content.create(
container=self.document, type="Document", title="bbb"
)

commit()

def test_with_noacquisition_enabled_get_not_found(self):
response = self.api_session.get(f"{self.document.absolute_url()}/aaa/aaa/bbb")
self.assertEqual(response.status_code, 404)
Loading
Loading