diff --git a/src/App/dtml/manage_page_header.dtml b/src/App/dtml/manage_page_header.dtml
index e8b8659e10..4845d45d44 100644
--- a/src/App/dtml/manage_page_header.dtml
+++ b/src/App/dtml/manage_page_header.dtml
@@ -10,27 +10,25 @@
Zope
-
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
" class="zmi zmi- zmi-">
diff --git a/src/zmi/styles/subscriber.py b/src/zmi/styles/subscriber.py
index 3320a3c6de..0f3acf7f36 100644
--- a/src/zmi/styles/subscriber.py
+++ b/src/zmi/styles/subscriber.py
@@ -1,14 +1,48 @@
+import itertools
+
import zope.component
import zope.interface
+from Acquisition import aq_parent
+from ZPublisher.HTTPRequest import HTTPRequest
+
+
+def prepend_authentication_path(request: HTTPRequest, path: str) -> str:
+ """Prepend the path of the user folder.
+
+ Because ++resource++zmi is protected, we generate URLs relative to the
+ user folder of the logged-in user, so that the user can access the
+ resources.
+ """
+ authenticated_user = request.get("AUTHENTICATED_USER")
+ if not authenticated_user:
+ return path
+
+ # prepend the authentication path, unless it is already part of the
+ # virtual host root.
+ authentication_path = []
+ ufpp = aq_parent(aq_parent(authenticated_user)).getPhysicalPath()
+ vrpp = request.get("VirtualRootPhysicalPath") or ()
+ for ufp, vrp in itertools.zip_longest(ufpp, vrpp):
+ if ufp == vrp:
+ continue
+ authentication_path.append(ufp)
+
+ parts = [
+ p for p in itertools.chain(authentication_path, path.split("/")) if p]
+
+ return request.physicalPathToURL(parts, relative=True)
@zope.component.adapter(zope.interface.Interface)
def css_paths(context):
"""Return paths to CSS files needed for the Zope 4 ZMI."""
return (
- '/++resource++zmi/bootstrap-4.6.0/bootstrap.min.css',
- '/++resource++zmi/fontawesome-free-5.15.2/css/all.css',
- '/++resource++zmi/zmi_base.css',
+ prepend_authentication_path(context.REQUEST, path)
+ for path in (
+ '/++resource++zmi/bootstrap-4.6.0/bootstrap.min.css',
+ '/++resource++zmi/fontawesome-free-5.15.2/css/all.css',
+ '/++resource++zmi/zmi_base.css',
+ )
)
@@ -16,8 +50,11 @@ def css_paths(context):
def js_paths(context):
"""Return paths to JS files needed for the Zope 4 ZMI."""
return (
- '/++resource++zmi/jquery-3.5.1.min.js',
- '/++resource++zmi/bootstrap-4.6.0/bootstrap.bundle.min.js',
- '/++resource++zmi/ace.ajax.org/ace.js',
- '/++resource++zmi/zmi_base.js',
+ prepend_authentication_path(context.REQUEST, path)
+ for path in (
+ '/++resource++zmi/jquery-3.5.1.min.js',
+ '/++resource++zmi/bootstrap-4.6.0/bootstrap.bundle.min.js',
+ '/++resource++zmi/ace.ajax.org/ace.js',
+ '/++resource++zmi/zmi_base.js',
+ )
)
diff --git a/src/zmi/styles/tests.py b/src/zmi/styles/tests.py
index 36c7b65bec..71ce7d9ce3 100644
--- a/src/zmi/styles/tests.py
+++ b/src/zmi/styles/tests.py
@@ -1,4 +1,7 @@
+"""Testing .subscriber.*"""
+
import Testing.ZopeTestCase
+from Products.SiteAccess.VirtualHostMonster import VirtualHostMonster
from Testing.ZopeTestCase.placeless import temporaryPlacelessSetUp
from Testing.ZopeTestCase.placeless import zcml
from zope.security.management import endInteraction
@@ -19,9 +22,16 @@ def setupZCML():
class SubscriberTests(Testing.ZopeTestCase.FunctionalTestCase):
- """Testing .subscriber.*"""
+ """Test subscribers URL generation with a user from a user folder
+ not at the root.
+ """
+ request_path = f"/{Testing.ZopeTestCase.folder_name}/manage_main"
+ resources_base_path = f"/{Testing.ZopeTestCase.folder_name}"
- base_path = f'/{Testing.ZopeTestCase.folder_name}'
+ def afterSetUp(self):
+ if "virtual_hosting" not in self.app:
+ vhm = VirtualHostMonster()
+ vhm.addToContainer(self.app)
def call_manage_main(self):
"""Call /folder/manage_main and return the HTML text."""
@@ -30,9 +40,7 @@ def _call_manage_main(self):
# temporaryPlacelessSetUp insists in creating an interaction
# which the WSGI publisher does not expect.
endInteraction()
- response = self.publish(
- f'{self.base_path}/manage_main',
- basic=basic_auth)
+ response = self.publish(self.request_path, basic=basic_auth)
return str(response)
return temporaryPlacelessSetUp(
_call_manage_main, required_zcml=setupZCML)(self)
@@ -41,12 +49,91 @@ def test_subscriber__css_paths__1(self):
"""The paths it returns are rendered in the ZMI."""
from .subscriber import css_paths
body = self.call_manage_main()
- for path in css_paths(None):
- self.assertIn(f'href="{self.base_path}{path}"', body)
+ for path in css_paths(self.folder):
+ self.assertIn(f'href="{self.resources_base_path}{path}"', body)
def test_subscriber__js_paths__1(self):
"""The paths it returns are rendered in the ZMI."""
from .subscriber import js_paths
body = self.call_manage_main()
- for path in js_paths(None):
- self.assertIn(f'src="{self.base_path}{path}"', body)
+ for path in js_paths(self.folder):
+ self.assertIn(f'src="{self.resources_base_path}{path}"', body)
+
+
+class SubscriberTestsUserFromRootUserFolderViewingRootFolder(SubscriberTests):
+ """Tests subscribers URL generation with a user from the root acl_users,
+ viewing the root of ZMI.
+ """
+
+ request_path = "/manage_main"
+ resources_base_path = ""
+
+ def _setupFolder(self):
+ self.folder = self.app
+
+ def _setupUserFolder(self):
+ # we use the user folder from self.app
+ pass
+
+
+class SubscriberTestsUserFromRootUserFolderViewingFolder(SubscriberTests):
+ """Tests subscribers URL generation with a user from the root acl_users,
+ viewing a folder not at the root of ZMI. In such case, the URLs are not
+ relative to that folder, the resources are served from the root.
+ """
+
+ request_path = f"/{Testing.ZopeTestCase.folder_name}/manage_main"
+ resources_base_path = ""
+
+ def _setupUser(self):
+ uf = self.app.acl_users
+ uf.userFolderAddUser(
+ Testing.ZopeTestCase.user_name,
+ Testing.ZopeTestCase.user_password,
+ ["Manager"],
+ [],
+ )
+
+ def setRoles(self, roles, name=...):
+ # we set roles in _setupUser
+ pass
+
+ def login(self):
+ pass
+
+
+class SubscriberUrlWithVirtualHostingTests(SubscriberTests):
+ """Tests subscribers URL generation using virtual host."""
+
+ request_path = (
+ "/VirtualHostBase/https/example.org:443/VirtualHostRoot/"
+ f"{Testing.ZopeTestCase.folder_name}/manage_main"
+ )
+ resources_base_path = f"/{Testing.ZopeTestCase.folder_name}"
+
+
+class SubscriberUrlWithVirtualHostingAndUserFolderInVirtualHostTests(
+ SubscriberTests):
+ """Tests subscribers URL generation using virtual host, when
+ the authentication path is part of the virtual host base.
+ """
+
+ request_path = (
+ "/VirtualHostBase/https/example.org:443/"
+ f"{Testing.ZopeTestCase.folder_name}/VirtualHostRoot/manage_main"
+ )
+ resources_base_path = ""
+
+
+class SubscriberUrlWithVirtualHostingAndVHTests(SubscriberTests):
+ """Tests subscribers URL generation using virtual host, when
+ the authentication path is part of the virtual host base and
+ using a "_vh_" path element.
+ """
+
+ request_path = (
+ "/VirtualHostBase/https/example.org:443"
+ f"/{Testing.ZopeTestCase.folder_name}/"
+ "VirtualHostRoot/_vh_zz/manage_main"
+ )
+ resources_base_path = "/zz"