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

3931.feature allow javascript to be inserted in head-section or at bottom of page. #361

Merged
merged 26 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b3cfef
Allow including js in head section
jladage Mar 26, 2024
be96cd2
Prevent KeyError getting ISiteSchema when not all records exist yet.
mauritsvanrees Mar 26, 2024
9a0c1b6
Improve news snippet.
mauritsvanrees Mar 26, 2024
cdffe7c
Let the AnalyticsHeadViewlet inherit from the AnalyticsViewlet so we …
mauritsvanrees Mar 26, 2024
105bef7
zpretty
mauritsvanrees Mar 28, 2024
276c66e
Use IHtmlHeadLinks viewlet provider to assure being the last script t…
jladage Mar 28, 2024
4bd740e
Fix tests
jladage Mar 28, 2024
f1fdfa9
Update changelog entry to refer to correct viewlet manager.
jladage Mar 28, 2024
677fcca
Fix tests
jladage Mar 28, 2024
70283a2
Fix tests
jladage Mar 28, 2024
63e1655
Fix tests, hopefully ci likes it now.
jladage Mar 28, 2024
a95f03d
Improve security by filtering all but script tags.
jladage Mar 30, 2024
89f8167
Black fixes
jladage Mar 30, 2024
15858c3
Change import to see it that fixes the dependencies problem
jladage Mar 30, 2024
222066a
Black fixes
jladage Mar 30, 2024
6fa0c7d
Add lxml to install_requires
jladage Mar 30, 2024
1bf6c5a
Add trailing comma to list, just to make black happy.
jladage Mar 30, 2024
7bbd28a
Use try/except in case someone puts in anything other than <script>-t…
jladage Mar 30, 2024
71178e4
Position script even before all other javascripts.
jladage Mar 30, 2024
5880d2e
Instead of returning only script tags, it now filters base and title …
jladage Apr 2, 2024
fc9b432
Black and spelling fixes
jladage Apr 2, 2024
c240ea1
Update Changlog entry to reflect new changes.
jladage Apr 4, 2024
82da269
Reduce number of xpath queries.
jladage Apr 4, 2024
a3f6c8a
Black code fixes
jladage Apr 4, 2024
c060d88
Do not filter webstats code with lxml.
mauritsvanrees Apr 23, 2024
a1fe14a
Merge remote-tracking branch 'origin/master' into 3931.feature
mauritsvanrees Apr 23, 2024
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
8 changes: 8 additions & 0 deletions news/3931.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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 <https://github.com/plone/Products.CMFPlone/issues/3931>`_:
some javascript needs to be loaded at the bottom of the page, and some in the head section.
Fool proofing by filering out <base>-tags, <title>-tags and rendering valid html for the
remaining tags.
[jladage]

7 changes: 7 additions & 0 deletions plone/app/layout/analytics/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
xmlns:browser="http://namespaces.zope.org/browser"
>

<browser:viewlet
name="plone.analytics.head"
manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
class=".view.AnalyticsHeadViewlet"
permission="zope2.View"
/>

<browser:viewlet
name="plone.analytics"
manager="plone.app.layout.viewlets.interfaces.IPortalFooter"
Expand Down
73 changes: 70 additions & 3 deletions plone/app/layout/analytics/tests/analytics.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 IPortalFooter
>>> from plone.app.layout.viewlets.interfaces import IHtmlHead, IPortalFooter
>>> from Products.Five.viewlet.manager import ViewletManager
>>> Footer = ViewletManager('left', IPortalFooter)

Expand All @@ -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
Expand All @@ -46,5 +46,72 @@ Now enter some non-ascii text

>>> site_settings.webstats_js = u"<script>window.title='C\xedsa\u0159'</script>"
>>> text = manager.render()
>>> site_settings.webstats_js in text
>>> "<script>window.title='Císař'</script>" in text
True

Now instantiate a viewlet manager for the header.

>>> Head = ViewletManager('left', IHtmlHead)
>>> manager = Head(portal, request, view)
>>> manager.update()
>>> for viewlet in manager.viewlets:
... if viewlet.__name__ == "plone.analytics.head":
... analytics_head = viewlet
... break

When no analytics (webstats_head_js) code is set up the viewlet will not be rendered:

>>> analytics_head.webstats_js == ""
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"<script>window.title='Hello'</script>"
>>> analytics_head.webstats_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

If we add other tags than script tags, we only render the script tags.

>>> site_settings.webstats_head_js = u"<script>window.title='Hello'</script><link rel='stylesheet' src='teststyle.css'/>"
>>> text = manager.render()
>>> "<link rel='stylesheet' src='teststyle.css'/>" in text
False
>>> site_settings.webstats_head_js in text
False
>>> "<script>window.title='Hello'</script>" in text
True

Now enter some tags we don't want to render, like a <base> and <title> 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</title><link rel='stylesheet' src='teststyle.css' /><script>window.title='C\xedsa\u0159'</script>"
>>> text = manager.render()
>>> '<link rel="stylesheet" src="teststyle.css">' in text
True
>>> "<base href='some-url'>" in text
False
>>> "<title>Gets removed</title>" in text
False
>>> "<script>window.title='Císař'</script>" 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
32 changes: 28 additions & 4 deletions plone/app/layout/analytics/view.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -7,9 +8,13 @@
from zope.viewlet.interfaces import IViewlet


UNWANTED_TAGS = ["base", "title"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, if this is really necessary. We're adding JavaScript and with that we can do anything, also unwanted stuff.
Maybe it should be up to the integrator to produce valid HTML?

If we keep it, I do like that e.g. <style> tags and the like are allowed. This gives an easy way of quickly adding a style messing with theming.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rational is that this is not security but more thinking about what a not so savy site manager, could fuck up :) Off course he/she should handle injecting javascript with care, like it was alway the case with that field in the control panel.

My code, because it injects in the head section (thanks to @yurj ) I got aware of the fact that we could prevent "user error" simply by filtering only script tag. A bit later I realized we can prevent double base or title tags at a low cost using lxml.

A pleasant second benefit is that with this code, even if the user makes a typo in the script, the viewlet will render valid HTML.
I'm not familar enough with the control panel code to write up a validator for this head field which would prevent adding base or title tags in the nicest possible way. Anyone would like to assist or give me some pointers?

Hope this helps to understand why it's there.



@implementer(IViewlet)
class AnalyticsViewlet(BrowserView):
render = ViewPageTemplateFile("view.pt")
record_name = "webstats_js"

def __init__(self, context, request, view, manager):
super().__init__(context, request)
Expand All @@ -21,11 +26,30 @@ 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 ""
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 ""

def update(self):
"""The viewlet manager _updateViewlets requires this method"""
pass


@implementer(IViewlet)
class AnalyticsHeadViewlet(AnalyticsViewlet):
render = ViewPageTemplateFile("view_head.pt")
record_name = "webstats_head_js"
9 changes: 9 additions & 0 deletions plone/app/layout/analytics/view_head.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div tal:define="
webstats_head_js view/webstats_js;
"
tal:condition="webstats_head_js"
tal:omit-tag=""
>
<!-- plone.analytics.head goes here -->
<span tal:replace="structure webstats_head_js"></span>
</div>
6 changes: 5 additions & 1 deletion plone/app/layout/links/viewlets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
5 changes: 3 additions & 2 deletions plone/app/layout/viewlets/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 19 additions & 12 deletions plone/app/layout/viewlets/document_byline.pt
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
<section id="section-byline"
tal:condition="view/show"
i18n:domain="plone">
tal:condition="view/show"
i18n:domain="plone"
>
<tal:creators tal:define="
creator_ids here/creators;
navigation_root_url context/@@plone_portal_state/navigation_root_url;
"
tal:condition="python:creator_ids and view.show_about()">
tal:condition="python:creator_ids and view.show_about()"
>
<span class="label-by-author">
<tal:i18n i18n:translate="">by</tal:i18n>
<tal:for repeat="user_id creator_ids">
<tal:user define="
url_path python: view.get_url_path(user_id);
fullname python:view.get_fullname(user_id);
">
url_path python: view.get_url_path(user_id);
fullname python:view.get_fullname(user_id);
">
<a class="badge rounded-pill bg-light text-dark fw-normal fs-6"
href="${navigation_root_url}/${url_path}"
tal:condition="url_path">${fullname}</a>
href="${navigation_root_url}/${url_path}"
tal:condition="url_path"
>${fullname}</a>
<span class="badge rounded-pill bg-light text-dark fw-normal fs-6"
tal:condition="not:url_path">${fullname}</span>
tal:condition="not:url_path"
>${fullname}</span>
</tal:user>
</tal:for>
&mdash;
Expand All @@ -30,14 +34,16 @@
show_modification_date python:view.show_modification_date();
">
<span class="documentPublished"
tal:condition="published">
tal:condition="published"
>
<span i18n:translate="box_published">published</span>
<span tal:content="python:context.toLocalizedTime(published)">Published</span>
<tal:sep condition="show_modification_date">,</tal:sep>
</span>

<span class="documentModified"
tal:condition="show_modification_date">
tal:condition="show_modification_date"
>
<span i18n:translate="box_last_modified">
last modified
</span>
Expand All @@ -50,7 +56,8 @@
<tal:expired tal:condition="view/isExpired">
&mdash;
<span class="state-expired"
i18n:translate="time_expired">expired</span>
i18n:translate="time_expired"
>expired</span>
</tal:expired>

</section>
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"Products.ZCatalog",
"setuptools",
"Zope",
"lxml",
],
extras_require=dict(
test=[
Expand Down
Loading