Skip to content

Commit

Permalink
Merge pull request #237 from plone/maurits-issue-236
Browse files Browse the repository at this point in the history
Traverse to theme resources from the navigation root again.
  • Loading branch information
mauritsvanrees authored Apr 22, 2024
2 parents 994b26f + 58ac445 commit 6580fab
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 4 deletions.
4 changes: 4 additions & 0 deletions news/236.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Traverse to theme resources from the navigation root again.
Only when this gives an Unauthorized, try it on the portal as a fall back.
This fixes other use cases of traversing to absolute urls in a theme.
[maurits]
6 changes: 6 additions & 0 deletions src/plone/app/theming/tests/browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from Products.Five import BrowserView


class Title(BrowserView):
def __call__(self):
return self.context.Title()
14 changes: 14 additions & 0 deletions src/plone/app/theming/tests/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,18 @@
permission="zope.Public"
/>

<browser:page
name="test-title"
for="*"
class=".browser.Title"
permission="zope2.View"
/>

<browser:page
name="test-public-title"
for="*"
class=".browser.Title"
permission="zope.Public"
/>

</configure>
147 changes: 147 additions & 0 deletions src/plone/app/theming/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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.app.theming.testing import THEMING_FUNCTIONAL_TESTING
from plone.app.theming.testing import THEMING_INTEGRATION_TESTING
from plone.app.theming.utils import applyTheme
from plone.app.theming.utils import extractThemeInfo
from plone.app.theming.utils import getTheme
from plone.app.theming.utils import InternalResolver
from plone.base.interfaces import INavigationRoot
from plone.testing.zope import Browser
from Products.CMFCore.utils import getToolByName
from zExceptions import Unauthorized
from zope.interface import alsoProvides

import os.path
import tempfile
Expand Down Expand Up @@ -33,6 +40,26 @@
PACKAGE_THEME = os.path.join(HERE, PACKAGE_THEME_FILENAME)


class InternalResolverAsString(InternalResolver):
"""InternalResolver with some simplicifications.
InternalResolver has this main method:
def resolve(self, system_url, public_id, context):
At the end it calls:
return self.resolve_string(result, context)
This turns a string into some internal lxml document, and I don't know how
to turn that back into a string for easier testing. So override that
method to simply return the original string.
"""

def resolve_string(self, result, context):
return result


class TestIntegration(unittest.TestCase):
layer = THEMING_INTEGRATION_TESTING

Expand Down Expand Up @@ -329,6 +356,126 @@ def test_createThemeFromTemplate_ja_unicode_title(self):
self.fail(msg="Unicode Encode Error")


class TestInternalResolverNavigationRoot(unittest.TestCase):
"""Test how the InternalResolver handles navigation roots."""

layer = THEMING_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.layer["request"]

def resolve(self, system_url):
"""Resolve the system_url.
The standard resolve method ignores the public_id and the context,
so I don't want to pass it in all tests.
"""
resolver = InternalResolverAsString()
return resolver.resolve(system_url, public_id=None, context=None)

def setup_public(self):
# Create a public navigation root containing a public page.
setRoles(self.portal, TEST_USER_ID, ("Manager",))
self.portal.invokeFactory("Folder", "public", title="Public Folder")
folder = self.portal.public
alsoProvides(folder, INavigationRoot)
folder.invokeFactory("Document", "page", title="Public page in public folder")
wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(folder, action="publish")
wftool.doActionFor(folder.page, action="publish")

# If we want a page in the site root:
# self.portal.invokeFactory("Document", "page", title="Public page")
# wftool.doActionFor(self.portal.page, action="publish")
setRoles(self.portal, TEST_USER_ID, ("Member",))
return folder

def setup_private(self):
# Create a private navigation root containing a public page.
setRoles(self.portal, TEST_USER_ID, ("Manager",))
self.portal.invokeFactory("Folder", "private", title="Private Folder")
folder = self.portal.private
alsoProvides(folder, INavigationRoot)
folder.invokeFactory("Document", "page", title="Public page in private folder")
wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(folder.page, action="publish")
setRoles(self.portal, TEST_USER_ID, ("Member",))
return folder

def test_internal_resolver_site_root(self):
self.request.traverse("/plone")
# absolute
self.assertEqual("Plone site", self.resolve("/@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("/++theme++plone.app.theming.tests/resource.css"),
)
# relative
self.assertEqual("Plone site", self.resolve("@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("++theme++plone.app.theming.tests/resource.css"),
)

def test_internal_resolver_navigation_root_public(self):
self.setup_public()
self.request.traverse("/plone/public")
# absolute
self.assertEqual("Public Folder", self.resolve("/@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("/++theme++plone.app.theming.tests/resource.css"),
)
# relative
self.assertEqual("Public Folder", self.resolve("@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("++theme++plone.app.theming.tests/resource.css"),
)

def test_internal_resolver_navigation_root_public_page(self):
self.setup_public()
self.request.traverse("/plone/public/page")
# absolute
self.assertEqual("Public Folder", self.resolve("/@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("/++theme++plone.app.theming.tests/resource.css"),
)
# relative
self.assertEqual("Public page in public folder", self.resolve("@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("++theme++plone.app.theming.tests/resource.css"),
)

def test_internal_resolver_navigation_root_private(self):
self.setup_private()
# A traverse to "/plone/private" fails, because we are anonymous and
# cannot access this private navigation root:
with self.assertRaises(Unauthorized):
self.request.traverse("/plone/private")
self.request.traverse("/plone/private/page")
# An absolute browser view would fail, because we are not authorized
# to access this view on the private navigation root. But we fall back
# to accessing it on the site root.
self.assertEqual("Plone site", self.resolve("/@@test-title"))
# A publicly available version of the same browser view works fine though:
self.assertEqual("Private Folder", self.resolve("/@@test-public-title"))
# absolute resource
self.assertIn(
"A CSS file",
self.resolve("/++theme++plone.app.theming.tests/resource.css"),
)
# relative
self.assertEqual("Public page in private folder", self.resolve("@@test-title"))
self.assertIn(
"A CSS file",
self.resolve("++theme++plone.app.theming.tests/resource.css"),
)


class TestUnit(unittest.TestCase):
def _open_zipfile(self, filename):
"""Helper that opens a zip file in our test directory"""
Expand Down
18 changes: 14 additions & 4 deletions src/plone/app/theming/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from Acquisition import aq_base
from configparser import ConfigParser
from diazo.compiler import compile_theme
from diazo.compiler import quote_param
Expand Down Expand Up @@ -161,18 +162,27 @@ def resolve(self, system_url, public_id, context):

context = findContext(request)
portalState = queryMultiAdapter((context, request), name="plone_portal_state")
portal = portalState.portal()
root = portalState.navigation_root()

if not system_url.startswith("/"): # only for relative urls
root = portalState.navigation_root()
is_absolute_url = system_url.startswith("/")
if not is_absolute_url:
root_path = root.getPhysicalPath()
context_path = context.getPhysicalPath()[len(root_path) :]
if len(context_path) == 0:
system_url = "/" + system_url
else:
system_url = "/{:s}/{:s}".format("/".join(context_path), system_url)

response = subrequest(system_url, root=portal)
response = subrequest(system_url, root=root)
if is_absolute_url and response.status == 401:
# If we tried on the navigation root we can retry on the portal:
# the navigation root may be private. This is especially needed
# when requesting theme resources: otherwise accessing a public
# page within a private navigation root would show unstyled.
# See https://github.com/plone/plone.app.theming/issues/142
portal = portalState.portal()
if aq_base(portal) is not aq_base(root):
response = subrequest(system_url, root=portal)
if response.status != 200:
LOGGER.error(f"Couldn't resolve {system_url:s}")
return None
Expand Down

0 comments on commit 6580fab

Please sign in to comment.