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 @@ <dtml-if title_or_id><dtml-var title_or_id><dtml-else>Zope</dtml-if> - - + - + - - - - - - - + + + + + + + - " 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"