From 4b3cfef6116f8ded1816619c87977a9184932f5a Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Tue, 26 Mar 2024 11:39:03 +0000 Subject: [PATCH 01/25] Allow including js in head section --- news/3931.feature | 7 +++ plone/app/layout/analytics/configure.zcml | 7 +++ .../app/layout/analytics/tests/analytics.txt | 45 ++++++++++++++++++- plone/app/layout/analytics/view.py | 24 ++++++++++ plone/app/layout/analytics/view_head.pt | 8 ++++ 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 news/3931.feature create mode 100644 plone/app/layout/analytics/view_head.pt diff --git a/news/3931.feature b/news/3931.feature new file mode 100644 index 00000000..11d20f5a --- /dev/null +++ b/news/3931.feature @@ -0,0 +1,7 @@ +3931.feature javascripts in head section +---------------------------------------- + +- Add a field webstats_head_js to the Site controlpanel and render it's + contents in the head section using IScripts viewlet manager. + [jladage] + diff --git a/plone/app/layout/analytics/configure.zcml b/plone/app/layout/analytics/configure.zcml index dc599351..20ff958a 100644 --- a/plone/app/layout/analytics/configure.zcml +++ b/plone/app/layout/analytics/configure.zcml @@ -3,6 +3,13 @@ xmlns:browser="http://namespaces.zope.org/browser" > + + >> from zope.publisher.browser import BrowserView >>> view = BrowserView(portal, request) - >>> from plone.app.layout.viewlets.interfaces import IPortalFooter + >>> from plone.app.layout.viewlets.interfaces import IScripts, IPortalFooter >>> from Products.Five.viewlet.manager import ViewletManager >>> Footer = ViewletManager('left', IPortalFooter) @@ -48,3 +48,46 @@ Now enter some non-ascii text >>> text = manager.render() >>> site_settings.webstats_js in text True + +Now create a + >>> Header = ViewletManager('left', IScripts) + +Now we can instantiate the manager. + + >>> manager = Header(portal, request, view) + >>> manager.update() + >>> for viewlet in manager.viewlets: + ... if viewlet.__name__ == "plone.analytics.head": + ... analytics = viewlet + ... break + +When no analytics (webstats_head_js) code is set up the viewlet will not be rendered: + + >>> analytics.webstats_head_js == u"" + True + >>> text = manager.render() + >>> 'plone.analytics.head goes here' in text + False + +Set the analytics code through the controlpanel and verify it renders properly: + + >>> from plone.registry.interfaces import IRegistry + >>> from zope.component import getUtility + >>> from plone.base.interfaces import ISiteSchema + >>> registry = getUtility(IRegistry) + >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone") + >>> site_settings.webstats_head_js = u"" + >>> analytics.webstats_head_js == site_settings.webstats_head_js + True + >>> text = manager.render() + >>> 'plone.analytics.head goes here' in text + True + >>> site_settings.webstats_head_js in text + True + +Now enter some non-ascii text + + >>> site_settings.webstats_head_js = u"" + >>> text = manager.render() + >>> site_settings.webstats_head_js in text + True diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index c277714d..d2f8ad46 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -29,3 +29,27 @@ def webstats_js(self): def update(self): """The viewlet manager _updateViewlets requires this method""" pass + + +@implementer(IViewlet) +class AnalyticsHeadViewlet(BrowserView): + render = ViewPageTemplateFile("view_head.pt") + + def __init__(self, context, request, view, manager): + super().__init__(context, request) + self.__parent__ = view + self.view = view + self.manager = manager + + @property + def webstats_head_js(self): + registry = getUtility(IRegistry) + site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) + try: + return site_settings.webstats_head_js or "" + except AttributeError: + return "" + + def update(self): + """The viewlet manager _updateViewlets requires this method""" + pass diff --git a/plone/app/layout/analytics/view_head.pt b/plone/app/layout/analytics/view_head.pt new file mode 100644 index 00000000..7e71b1ec --- /dev/null +++ b/plone/app/layout/analytics/view_head.pt @@ -0,0 +1,8 @@ +
+ + + Here goes the webstats_head_js + +
From be96cd27aa58c34164611ffda93a5ac9d30d9ea2 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 26 Mar 2024 16:19:20 +0100 Subject: [PATCH 02/25] Prevent KeyError getting ISiteSchema when not all records exist yet. We need to run an upgrade step to define the new `webstats_head_js` field, but it would be nice if the UI looks reasonable before this. Various viewlets give: KeyError: 'Interface `plone.base.interfaces.controlpanel.ISiteSchema` defines a field `webstats_head_js`, for which there is no record.' --- plone/app/layout/links/viewlets.py | 6 +++++- plone/app/layout/viewlets/content.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plone/app/layout/links/viewlets.py b/plone/app/layout/links/viewlets.py index 5d12762d..4be24c2d 100644 --- a/plone/app/layout/links/viewlets.py +++ b/plone/app/layout/links/viewlets.py @@ -42,7 +42,11 @@ class FaviconViewlet(ViewletBase): def init_favicon(self) -> NoReturn: registry = getUtility(IRegistry) - settings: ISiteSchema = registry.forInterface(ISiteSchema, prefix="plone") + settings: ISiteSchema = registry.forInterface( + ISiteSchema, + prefix="plone", + check=False, + ) self.mimetype: str = getattr( settings, "site_favicon_mimetype", "image/vnd.microsoft.icon" ) diff --git a/plone/app/layout/viewlets/content.py b/plone/app/layout/viewlets/content.py index 785c8c04..ef4b3fbf 100644 --- a/plone/app/layout/viewlets/content.py +++ b/plone/app/layout/viewlets/content.py @@ -70,6 +70,7 @@ def show(self): settings = registry.forInterface( ISiteSchema, prefix="plone", + check=False, ) return not self.anonymous or settings.display_publication_date_in_byline @@ -145,7 +146,7 @@ def pub_date(self): """ # check if we are allowed to display publication date registry = getUtility(IRegistry) - settings = registry.forInterface(ISiteSchema, prefix="plone") + settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) if not settings.display_publication_date_in_byline: return None @@ -281,7 +282,7 @@ def pub_date(self): """ # check if we are allowed to display publication date registry = getUtility(IRegistry) - settings = registry.forInterface(ISiteSchema, prefix="plone") + settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) if not settings.display_publication_date_in_byline: return None From 9a0c1b6e8bfa49f945f6f470839a338ed3d21ea8 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 26 Mar 2024 16:23:50 +0100 Subject: [PATCH 03/25] Improve news snippet. --- news/3931.feature | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/news/3931.feature b/news/3931.feature index 11d20f5a..709da786 100644 --- a/news/3931.feature +++ b/news/3931.feature @@ -1,7 +1,6 @@ -3931.feature javascripts in head section ----------------------------------------- - -- Add a field webstats_head_js to the Site controlpanel and render it's - contents in the head section using IScripts viewlet manager. - [jladage] +Add a field ``webstats_head_js`` to the Site controlpanel and render its +contents in the head section using ``IScripts`` viewlet manager. +See `issue 3931 `_: +some javascript needs to be loaded at the bottom of the page, and some in the head section. +[jladage] From cdffe7cdb8025e6d29bc305fbc180e5729844a98 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Tue, 26 Mar 2024 17:01:57 +0100 Subject: [PATCH 04/25] Let the AnalyticsHeadViewlet inherit from the AnalyticsViewlet so we have shorter code. --- .../app/layout/analytics/tests/analytics.txt | 12 ++++---- plone/app/layout/analytics/view.py | 28 +++---------------- plone/app/layout/analytics/view_head.pt | 13 +++++---- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index fe5c35d7..4358a4fd 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -20,7 +20,7 @@ Now we can instantiate the manager. When no analytics (webstats_js) code is set up the viewlet will not be rendered: - >>> analytics.webstats_js == u"" + >>> analytics.webstats_js == "" True >>> text = manager.render() >>> 'id="plone-analytics"' in text @@ -49,11 +49,9 @@ Now enter some non-ascii text >>> site_settings.webstats_js in text True -Now create a - >>> Header = ViewletManager('left', IScripts) - -Now we can instantiate the manager. +Now instantiate a viewlet manager for the header. + >>> Header = ViewletManager('left', IScripts) >>> manager = Header(portal, request, view) >>> manager.update() >>> for viewlet in manager.viewlets: @@ -63,7 +61,7 @@ Now we can instantiate the manager. When no analytics (webstats_head_js) code is set up the viewlet will not be rendered: - >>> analytics.webstats_head_js == u"" + >>> analytics.webstats_js == "" True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text @@ -77,7 +75,7 @@ Set the analytics code through the controlpanel and verify it renders properly: >>> registry = getUtility(IRegistry) >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone") >>> site_settings.webstats_head_js = u"" - >>> analytics.webstats_head_js == site_settings.webstats_head_js + >>> analytics.webstats_js == site_settings.webstats_head_js True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index d2f8ad46..0f33c6bd 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -10,6 +10,7 @@ @implementer(IViewlet) class AnalyticsViewlet(BrowserView): render = ViewPageTemplateFile("view.pt") + record_name = "webstats_js" def __init__(self, context, request, view, manager): super().__init__(context, request) @@ -21,10 +22,7 @@ def __init__(self, context, request, view, manager): def webstats_js(self): registry = getUtility(IRegistry) site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) - try: - return site_settings.webstats_js or "" - except AttributeError: - return "" + return getattr(site_settings, self.record_name, "") def update(self): """The viewlet manager _updateViewlets requires this method""" @@ -32,24 +30,6 @@ def update(self): @implementer(IViewlet) -class AnalyticsHeadViewlet(BrowserView): +class AnalyticsHeadViewlet(AnalyticsViewlet): render = ViewPageTemplateFile("view_head.pt") - - def __init__(self, context, request, view, manager): - super().__init__(context, request) - self.__parent__ = view - self.view = view - self.manager = manager - - @property - def webstats_head_js(self): - registry = getUtility(IRegistry) - site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) - try: - return site_settings.webstats_head_js or "" - except AttributeError: - return "" - - def update(self): - """The viewlet manager _updateViewlets requires this method""" - pass + record_name = "webstats_head_js" diff --git a/plone/app/layout/analytics/view_head.pt b/plone/app/layout/analytics/view_head.pt index 7e71b1ec..c51a58d1 100644 --- a/plone/app/layout/analytics/view_head.pt +++ b/plone/app/layout/analytics/view_head.pt @@ -1,8 +1,9 @@ -
- - - Here goes the webstats_head_js - + tal:omit-tag="" +> + +
From 105bef7ff142d50a85c746cb6fbf0b96f1c5a777 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Thu, 28 Mar 2024 11:48:05 +0100 Subject: [PATCH 05/25] zpretty --- plone/app/layout/viewlets/document_byline.pt | 31 ++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/plone/app/layout/viewlets/document_byline.pt b/plone/app/layout/viewlets/document_byline.pt index 1a4fdb98..6539d981 100644 --- a/plone/app/layout/viewlets/document_byline.pt +++ b/plone/app/layout/viewlets/document_byline.pt @@ -1,23 +1,27 @@
+ tal:condition="view/show" + i18n:domain="plone" +> + tal:condition="python:creator_ids and view.show_about()" + > by + url_path python: view.get_url_path(user_id); + fullname python:view.get_fullname(user_id); + "> ${fullname} + href="${navigation_root_url}/${url_path}" + tal:condition="url_path" + >${fullname} ${fullname} + tal:condition="not:url_path" + >${fullname} — @@ -30,14 +34,16 @@ show_modification_date python:view.show_modification_date(); "> + tal:condition="published" + > published Published , + tal:condition="show_modification_date" + > last modified @@ -50,7 +56,8 @@ expired + i18n:translate="time_expired" + >expired
From 276c66ec0e80c7f9875dcdcb3dc72d4c445703c0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 11:14:46 +0000 Subject: [PATCH 06/25] Use IHtmlHeadLinks viewlet provider to assure being the last script tag in the head section. --- plone/app/layout/analytics/configure.zcml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plone/app/layout/analytics/configure.zcml b/plone/app/layout/analytics/configure.zcml index 20ff958a..4efb3be9 100644 --- a/plone/app/layout/analytics/configure.zcml +++ b/plone/app/layout/analytics/configure.zcml @@ -5,7 +5,7 @@ From 4bd740e0a890702692ec842150cf5268d82a134c Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 11:22:32 +0000 Subject: [PATCH 07/25] Fix tests --- plone/app/layout/analytics/tests/analytics.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index 4358a4fd..7fb79e10 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -5,7 +5,7 @@ We need a view on the content. >>> from zope.publisher.browser import BrowserView >>> view = BrowserView(portal, request) - >>> from plone.app.layout.viewlets.interfaces import IScripts, IPortalFooter + >>> from plone.app.layout.viewlets.interfaces import IHtmlHeadLinks, IPortalFooter >>> from Products.Five.viewlet.manager import ViewletManager >>> Footer = ViewletManager('left', IPortalFooter) @@ -51,7 +51,7 @@ Now enter some non-ascii text Now instantiate a viewlet manager for the header. - >>> Header = ViewletManager('left', IScripts) + >>> Header = ViewletManager('left', IHtmlHeadLinks) >>> manager = Header(portal, request, view) >>> manager.update() >>> for viewlet in manager.viewlets: From f1fdfa9cbaef2689cbac0785f59ef96186a872e1 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 13:15:24 +0000 Subject: [PATCH 08/25] Update changelog entry to refer to correct viewlet manager. --- news/3931.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/3931.feature b/news/3931.feature index 709da786..d0d8beb5 100644 --- a/news/3931.feature +++ b/news/3931.feature @@ -1,5 +1,5 @@ Add a field ``webstats_head_js`` to the Site controlpanel and render its -contents in the head section using ``IScripts`` viewlet manager. +contents in the head section using ``IHtmlHeadLinks`` viewlet manager. See `issue 3931 `_: some javascript needs to be loaded at the bottom of the page, and some in the head section. [jladage] From 677fccab1dd96758a60a468cfb8fb7d86d8db2e4 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 13:25:02 +0000 Subject: [PATCH 09/25] Fix tests --- plone/app/layout/analytics/tests/analytics.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index 7fb79e10..0738d580 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -75,8 +75,6 @@ Set the analytics code through the controlpanel and verify it renders properly: >>> registry = getUtility(IRegistry) >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone") >>> site_settings.webstats_head_js = u"" - >>> analytics.webstats_js == site_settings.webstats_head_js - True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text True From 70283a21732c25f390d4aa651313218d78a978fc Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 19:57:29 +0000 Subject: [PATCH 10/25] Fix tests --- plone/app/layout/analytics/tests/analytics.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index 0738d580..7fb79e10 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -75,6 +75,8 @@ Set the analytics code through the controlpanel and verify it renders properly: >>> registry = getUtility(IRegistry) >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone") >>> site_settings.webstats_head_js = u"" + >>> analytics.webstats_js == site_settings.webstats_head_js + True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text True From 63e1655977df317126a43cb51905afec10a2342f Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 28 Mar 2024 20:04:53 +0000 Subject: [PATCH 11/25] Fix tests, hopefully ci likes it now. --- plone/app/layout/analytics/tests/analytics.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index 7fb79e10..4ff7d2d5 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -56,12 +56,12 @@ Now instantiate a viewlet manager for the header. >>> manager.update() >>> for viewlet in manager.viewlets: ... if viewlet.__name__ == "plone.analytics.head": - ... analytics = viewlet + ... analytics_head = viewlet ... break When no analytics (webstats_head_js) code is set up the viewlet will not be rendered: - >>> analytics.webstats_js == "" + >>> analytics_head.webstats_js == "" True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text @@ -75,7 +75,7 @@ Set the analytics code through the controlpanel and verify it renders properly: >>> registry = getUtility(IRegistry) >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone") >>> site_settings.webstats_head_js = u"" - >>> analytics.webstats_js == site_settings.webstats_head_js + >>> analytics_head.webstats_js == site_settings.webstats_head_js True >>> text = manager.render() >>> 'plone.analytics.head goes here' in text From a95f03dadb171004a74c621be236a77a308da072 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:04:51 +0000 Subject: [PATCH 12/25] Improve security by filtering all but script tags. --- news/3931.feature | 1 + .../app/layout/analytics/tests/analytics.txt | 19 ++++++++++++++++--- plone/app/layout/analytics/view.py | 10 ++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/news/3931.feature b/news/3931.feature index d0d8beb5..a547c607 100644 --- a/news/3931.feature +++ b/news/3931.feature @@ -2,5 +2,6 @@ Add a field ``webstats_head_js`` to the Site controlpanel and render its contents in the head section using ``IHtmlHeadLinks`` viewlet manager. See `issue 3931 `_: some javascript needs to be loaded at the bottom of the page, and some in the head section. +Improved security by rendering only " >>> text = manager.render() - >>> site_settings.webstats_js in text + >>> "" in text True Now instantiate a viewlet manager for the header. @@ -83,9 +83,22 @@ Set the analytics code through the controlpanel and verify it renders properly: >>> site_settings.webstats_head_js in text True -Now enter some non-ascii text +If we add other tags than script tags, we only render the script tags. - >>> site_settings.webstats_head_js = u"" + >>> site_settings.webstats_head_js = u"" >>> text = manager.render() + >>> "" in text + False >>> site_settings.webstats_head_js in text + False + >>> "" in text True + +Now enter some non-ascii text + + >>> site_settings.webstats_head_js = u"" + >>> text = manager.render() + >>> "" in text + False + >>> "" in text + True \ No newline at end of file diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 0f33c6bd..2bba0f48 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -5,7 +5,7 @@ from zope.component import getUtility from zope.interface import implementer from zope.viewlet.interfaces import IViewlet - +import lxml.html @implementer(IViewlet) class AnalyticsViewlet(BrowserView): @@ -22,7 +22,13 @@ def __init__(self, context, request, view, manager): def webstats_js(self): registry = getUtility(IRegistry) site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) - return getattr(site_settings, self.record_name, "") + stats = getattr(site_settings, self.record_name, "") + if stats != "": + html = lxml.html.fromstring(stats) + if html.xpath("//script"): + script_tags = [lxml.html.tostring(tag, encoding='unicode') for tag in html.xpath("//script")] + return "\n".join(script_tags) + return "" def update(self): """The viewlet manager _updateViewlets requires this method""" From 89f8167b60b7956a604af089625eec68f96336a1 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:16:44 +0000 Subject: [PATCH 13/25] Black fixes --- plone/app/layout/analytics/view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 2bba0f48..e1d6ba8c 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -7,6 +7,7 @@ from zope.viewlet.interfaces import IViewlet import lxml.html + @implementer(IViewlet) class AnalyticsViewlet(BrowserView): render = ViewPageTemplateFile("view.pt") From 15858c3cdc94e0aa590935648320931d10d96bcf Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:24:13 +0000 Subject: [PATCH 14/25] Change import to see it that fixes the dependencies problem --- plone/app/layout/analytics/view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index e1d6ba8c..53cf2bfc 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -5,7 +5,7 @@ from zope.component import getUtility from zope.interface import implementer from zope.viewlet.interfaces import IViewlet -import lxml.html +from lxml import html as lxmlhtml @implementer(IViewlet) @@ -25,9 +25,9 @@ def webstats_js(self): site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) stats = getattr(site_settings, self.record_name, "") if stats != "": - html = lxml.html.fromstring(stats) + html = lxmlhtml.fromstring(stats) if html.xpath("//script"): - script_tags = [lxml.html.tostring(tag, encoding='unicode') for tag in html.xpath("//script")] + script_tags = [lxmlhtml.tostring(tag, encoding='unicode') for tag in html.xpath("//script")] return "\n".join(script_tags) return "" From 222066ac8d4f9a519b2eaaa6948acc6fb52adba1 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:33:40 +0000 Subject: [PATCH 15/25] Black fixes --- plone/app/layout/analytics/view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 53cf2bfc..de7de13a 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -1,3 +1,4 @@ +from lxml import html as lxmlhtml from plone.base.interfaces import ISiteSchema from plone.registry.interfaces import IRegistry from Products.Five.browser import BrowserView @@ -5,7 +6,6 @@ from zope.component import getUtility from zope.interface import implementer from zope.viewlet.interfaces import IViewlet -from lxml import html as lxmlhtml @implementer(IViewlet) @@ -27,7 +27,10 @@ def webstats_js(self): if stats != "": html = lxmlhtml.fromstring(stats) if html.xpath("//script"): - script_tags = [lxmlhtml.tostring(tag, encoding='unicode') for tag in html.xpath("//script")] + script_tags = [ + lxmlhtml.tostring(tag, encoding="unicode") + for tag in html.xpath("//script") + ] return "\n".join(script_tags) return "" From 6fa0c7d51a70d9a80ea802853054eb0f6b1b876b Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:36:01 +0000 Subject: [PATCH 16/25] Add lxml to install_requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 75db0a4b..01d157ed 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ "Products.ZCatalog", "setuptools", "Zope", + "lxml" ], extras_require=dict( test=[ From 1bf6c5a328891f0b9c7c3c3bed2dc50b1d2b329f Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 14:39:37 +0000 Subject: [PATCH 17/25] Add trailing comma to list, just to make black happy. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 01d157ed..d29892ba 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ "Products.ZCatalog", "setuptools", "Zope", - "lxml" + "lxml", ], extras_require=dict( test=[ From 7bbd28a41b60d656bceff0aff5741677b94fc024 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Sat, 30 Mar 2024 17:13:21 +0000 Subject: [PATCH 18/25] Use try/except in case someone puts in anything other than " in text - True \ No newline at end of file + True + +Final check, if we have bogus content in here, will things break? + + >>> site_settings.webstats_head_js = u"console.log('Missing script tag')" + >>> text = manager.render() + >>> "console.log('Missing script tag')" in text + False \ No newline at end of file diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 0871af31..12ff26e1 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -30,7 +30,7 @@ def webstats_js(self): html = lxmlhtml.fromstring(stats) except Exception: return "" - if html and html.xpath("//script"): + if html != "" and html.xpath("//script"): script_tags = [ lxmlhtml.tostring(tag, encoding="unicode") for tag in html.xpath("//script") From 5880d2ed9fc58dbcd1220ecc95af5d11985e13f4 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Tue, 2 Apr 2024 19:22:16 +0100 Subject: [PATCH 20/25] Instead of returning only script tags, it now filters base and title tags and renders up the remaining tags. --- plone/app/layout/analytics/tests/analytics.txt | 12 +++++++++--- plone/app/layout/analytics/view.py | 18 +++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index fb72c017..7a7c4dd0 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -94,11 +94,17 @@ If we add other tags than script tags, we only render the script tags. >>> "" in text True -Now enter some non-ascii text +Now enter some tags we don't want to render, like a and tag, +since they should occur only once and is set by Plone. <style>, <script> and +<link> tags are allowed. - >>> site_settings.webstats_head_js = u"<link rel='stylesheet' src='teststyle.css'></link><script>window.title='C\xedsa\u0159'</script>" + >>> site_settings.webstats_head_js = u"<base href='some-url' /><title>Get's removed" >>> text = manager.render() - >>> "" in text + >>> '' in text + True + >>> "" in text + False + >>> "Get's removed" in text False >>> "" in text True diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 12ff26e1..79b3ebf5 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -7,6 +7,8 @@ from zope.interface import implementer from zope.viewlet.interfaces import IViewlet +UNWANTED_TAGS = ["base", "title"] + @implementer(IViewlet) class AnalyticsViewlet(BrowserView): @@ -27,15 +29,17 @@ def webstats_js(self): html = "" if stats != "": try: - html = lxmlhtml.fromstring(stats) + html = lxmlhtml.fragment_fromstring(stats, create_parent='div') except Exception: return "" - if html != "" and html.xpath("//script"): - script_tags = [ - lxmlhtml.tostring(tag, encoding="unicode") - for tag in html.xpath("//script") - ] - return "\n".join(script_tags) + if html != "": + for tag in UNWANTED_TAGS: + bad_tags = html.xpath(f"//{tag}") + if bad_tags: + for bad_tag in bad_tags: + bad_tag.drop_tree() + + return '\n'.join(lxmlhtml.tostring(x, encoding="unicode") for x in html.iterchildren()) return "" def update(self): From fc9b432d6307c2c4b5f926417a362ed71568e702 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Tue, 2 Apr 2024 23:12:41 +0100 Subject: [PATCH 21/25] Black and spelling fixes --- plone/app/layout/analytics/tests/analytics.txt | 6 +++--- plone/app/layout/analytics/view.py | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt index 7a7c4dd0..08ddc088 100644 --- a/plone/app/layout/analytics/tests/analytics.txt +++ b/plone/app/layout/analytics/tests/analytics.txt @@ -98,13 +98,13 @@ Now enter some tags we don't want to render, like a and tag, since they should occur only once and is set by Plone. <style>, <script> and <link> tags are allowed. - >>> site_settings.webstats_head_js = u"<base href='some-url' /><title>Get's removed" + >>> site_settings.webstats_head_js = u"Gets removed" >>> text = manager.render() >>> '' in text True >>> "" in text False - >>> "Get's removed" in text + >>> "Gets removed" in text False >>> "" in text True @@ -114,4 +114,4 @@ Final check, if we have bogus content in here, will things break? >>> site_settings.webstats_head_js = u"console.log('Missing script tag')" >>> text = manager.render() >>> "console.log('Missing script tag')" in text - False \ No newline at end of file + False diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 79b3ebf5..8bc57025 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -7,6 +7,7 @@ from zope.interface import implementer from zope.viewlet.interfaces import IViewlet + UNWANTED_TAGS = ["base", "title"] @@ -29,7 +30,7 @@ def webstats_js(self): html = "" if stats != "": try: - html = lxmlhtml.fragment_fromstring(stats, create_parent='div') + html = lxmlhtml.fragment_fromstring(stats, create_parent="div") except Exception: return "" if html != "": @@ -37,9 +38,12 @@ def webstats_js(self): bad_tags = html.xpath(f"//{tag}") if bad_tags: for bad_tag in bad_tags: - bad_tag.drop_tree() + bad_tag.drop_tree() - return '\n'.join(lxmlhtml.tostring(x, encoding="unicode") for x in html.iterchildren()) + return "\n".join( + lxmlhtml.tostring(x, encoding="unicode") + for x in html.iterchildren() + ) return "" def update(self): From c240ea1b2585aee585a87d1bf24c5ce0866028a9 Mon Sep 17 00:00:00 2001 From: Jean-Paul Ladage Date: Thu, 4 Apr 2024 18:19:17 +0100 Subject: [PATCH 22/25] Update Changlog entry to reflect new changes. --- news/3931.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/news/3931.feature b/news/3931.feature index a547c607..078748e3 100644 --- a/news/3931.feature +++ b/news/3931.feature @@ -2,6 +2,7 @@ Add a field ``webstats_head_js`` to the Site controlpanel and render its contents in the head section using ``IHtmlHeadLinks`` viewlet manager. See `issue 3931 `_: some javascript needs to be loaded at the bottom of the page, and some in the head section. -Improved security by rendering only " - >>> text = manager.render() - >>> "" in text - False - >>> site_settings.webstats_head_js in text - False - >>> "" in text - True - -Now enter some tags we don't want to render, like a and tag, -since they should occur only once and is set by Plone. <style>, <script> and -<link> tags are allowed. - - >>> site_settings.webstats_head_js = u"<base href='some-url' /><title>Gets removed" - >>> text = manager.render() - >>> '' in text - True - >>> "" in text - False - >>> "Gets removed" in text - False - >>> "" in text - True - -Final check, if we have bogus content in here, will things break? - - >>> site_settings.webstats_head_js = u"console.log('Missing script tag')" - >>> text = manager.render() - >>> "console.log('Missing script tag')" in text - False diff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py index 34e741b8..f34ad967 100644 --- a/plone/app/layout/analytics/view.py +++ b/plone/app/layout/analytics/view.py @@ -1,4 +1,3 @@ -from lxml import html as lxmlhtml from plone.base.interfaces import ISiteSchema from plone.registry.interfaces import IRegistry from Products.Five.browser import BrowserView @@ -26,23 +25,7 @@ def __init__(self, context, request, view, manager): def webstats_js(self): registry = getUtility(IRegistry) site_settings = registry.forInterface(ISiteSchema, prefix="plone", check=False) - stats = getattr(site_settings, self.record_name, "") - html = "" - if stats != "": - try: - html = lxmlhtml.fragment_fromstring(stats, create_parent="div") - except Exception: - return "" - - query = "| //".join(UNWANTED_TAGS) - bad_tags = html.xpath(f"//{query}") - if bad_tags: - for bad_tag in bad_tags: - bad_tag.drop_tree() - return "\n".join( - lxmlhtml.tostring(x, encoding="unicode") for x in html.iterchildren() - ) - return "" + return getattr(site_settings, self.record_name, "") def update(self): """The viewlet manager _updateViewlets requires this method""" diff --git a/setup.py b/setup.py index d29892ba..75db0a4b 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,6 @@ "Products.ZCatalog", "setuptools", "Zope", - "lxml", ], extras_require=dict( test=[