Skip to content

Commit

Permalink
Merge 55cbbeb into 3c024c4
Browse files Browse the repository at this point in the history
  • Loading branch information
gbastien committed Jun 9, 2022
2 parents 3c024c4 + 55cbbeb commit 8cd5d64
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 13 deletions.
8 changes: 6 additions & 2 deletions CHANGES.rst
Expand Up @@ -5,8 +5,12 @@ Changelog
2.18 (unreleased)
-----------------

- Nothing changed yet.

- Added `BaseColumn.escape = True` so content is escaped.
Manage escape manually for the `TitleColumn`, `VocabularyColumn` and the
`AbbrColumn`, set it to `False` for `CheckBoxColumn`, `ElementNumberColumn`
and `ActionsColumn` that are entirely generated, set it to `False` for
`PrettyLinkColumnNothing` as `imio.prettylink` manages it itself.
[gbastien]

2.17 (2022-05-13)
-----------------
Expand Down
1 change: 1 addition & 0 deletions buildout.d/dev.cfg
Expand Up @@ -31,6 +31,7 @@ extensions += mr.developer
sources = sources
always-checkout = force
auto-checkout =
imio.prettylink

[code-analysis]
recipe = plone.recipe.codeanalysis
Expand Down
14 changes: 14 additions & 0 deletions buildout.d/sources.cfg
@@ -1 +1,15 @@
[remotes]
collective = https://github.com/collective
collective_push = git@github.com:collective
plone = https://github.com/plone
plone_push = git@github.com:plone
ftw = https://github.com/4teamwork
ftw_push = git@github.com:4teamwork
imio = https://github.com/IMIO
imio_push = git@github.com:IMIO
zopefoundation = https://github.com/zopefoundation
zopefoundation_push = git@github.com:zopefoundation
zopesvn = svn://svn.zope.org/repos/main/

[sources]
imio.prettylink = git ${remotes:imio}/imio.prettylink.git pushurl=${remotes:imio_push}/imio.prettylink.git
3 changes: 3 additions & 0 deletions src/collective/eeafaceted/z3ctable/browser/views.py
Expand Up @@ -13,6 +13,7 @@
from zope.component import queryMultiAdapter
from zope.interface import implements

import html
import logging
import traceback

Expand Down Expand Up @@ -80,6 +81,8 @@ def renderCell(self, item, column, colspan=0):
colspanStr = colspan and ' colspan="%s"' % colspan or ''
start = datetime.now()
renderedCell = column.renderCell(item)
if column.escape:
renderedCell = html.escape(renderedCell)
if self.debug:
if not hasattr(column, 'cumulative_time'):
column.cumulative_time = timedelta(0)
Expand Down
46 changes: 35 additions & 11 deletions src/collective/eeafaceted/z3ctable/columns.py
Expand Up @@ -18,6 +18,7 @@
from zope.interface import implements
from zope.schema.interfaces import IVocabularyFactory

import html
import os
import pkg_resources
import urllib
Expand Down Expand Up @@ -57,6 +58,8 @@ class BaseColumn(column.GetAttrColumn):
header_help = None
# enable caching, needs to be implemented by Column
use_caching = True
# escape
escape = True

@property
def cssClasses(self):
Expand Down Expand Up @@ -294,6 +297,8 @@ class DateColumn(BaseColumn):
long_format = False
time_only = False
ignored_value = EMPTY_DATE
# not necessary to escape, everything is generated
escape = False

def renderCell(self, item):
value = self.getValue(item)
Expand Down Expand Up @@ -362,6 +367,8 @@ class VocabularyColumn(BaseColumn):
# named utility
vocabulary = None
ignored_value = EMPTY_STRING
# we manage escape here manually
escape = False

def renderCell(self, item):
value = self.getValue(item)
Expand All @@ -377,11 +384,12 @@ def renderCell(self, item):
# the vocabulary instance is cached
if not hasattr(self, '_cached_vocab_instance'):
if not self.vocabulary:
raise KeyError('A "vocabulary" must be defined for column "{0}" !'.format(self.attrName))
raise KeyError('A "vocabulary" must be defined for column "{0}" !'.format(
self.attrName))
factory = queryUtility(IVocabularyFactory, self.vocabulary)
if not factory:
raise KeyError('The vocabulary "{0}" used for column "{1}" was not found !'.format(self.vocabulary,
self.attrName))
raise KeyError('The vocabulary "{0}" used for column "{1}" was not found !'.format(
self.vocabulary, self.attrName))

self._cached_vocab_instance = factory(self.context)

Expand All @@ -391,7 +399,7 @@ def renderCell(self, item):
res = []
for v in value:
try:
res.append(safe_unicode(self._cached_vocab_instance.getTerm(v).title))
res.append(html.escape(safe_unicode(self._cached_vocab_instance.getTerm(v).title)))
except LookupError:
# in case an element is not in the vocabulary, add the value
res.append(safe_unicode(v))
Expand All @@ -407,6 +415,9 @@ class AbbrColumn(VocabularyColumn):

# named utility
full_vocabulary = None
separator = u', '
# we manage escape here manually
escape = False

def renderCell(self, item):
value = self.getValue(item)
Expand Down Expand Up @@ -446,13 +457,13 @@ def renderCell(self, item):
tag_title = self._cached_full_vocab_instance.getTerm(v).title
tag_title = tag_title.replace("'", "'")
res.append(u"<abbr title='{0}'>{1}</abbr>".format(
safe_unicode(tag_title),
safe_unicode(self._cached_acronym_vocab_instance.getTerm(v).title)))
html.escape(safe_unicode(tag_title)),
html.escape(safe_unicode(self._cached_acronym_vocab_instance.getTerm(v).title))))
except LookupError:
# in case an element is not in the vocabulary, add the value
res.append(safe_unicode(v))
res.append(html.escape(safe_unicode(v)))

res = ', '.join(res)
res = self.separator.join(res)
if self.use_caching:
self._store_cached_result(value, res)
return res
Expand All @@ -467,17 +478,19 @@ class ColorColumn(I18nColumn):
# Hide the head cell but fill it with spaces so it does
# not shrink to nothing if table is too large
header = u'&nbsp;&nbsp;&nbsp;'
# we manage escape here manually
escape = False

def renderCell(self, item):
"""Display a message."""
translated_msg = super(ColorColumn, self).renderCell(item)
return u'<div title="{0}">&nbsp;</div>'.format(translated_msg)
return u'<div title="{0}">&nbsp;</div>'.format(html.escape(translated_msg))

def getCSSClasses(self, item):
"""Generate a CSS class to apply on the TD depending on the value."""
return {'td': "{0}_{1}_{2}".format(self.cssClassPrefix,
str(self.attrName),
self.getValue(item))}
html.escape(self.getValue(item)))}


class CheckBoxColumn(BaseColumn):
Expand All @@ -489,6 +502,8 @@ class CheckBoxColumn(BaseColumn):
checked_by_default = True
attrName = 'UID'
weight = 100
# not necessary to escape, everything is generated
escape = False

def renderHeadCell(self):
""" """
Expand Down Expand Up @@ -542,6 +557,8 @@ def renderCell(self, item):

class ElementNumberColumn(BaseColumn):
header = u''
# not necessary to escape, everything is generated
escape = False

def renderCell(self, item):
""" """
Expand Down Expand Up @@ -583,19 +600,24 @@ class TitleColumn(BaseColumn):
header = _('header_Title')
sort_index = 'sortable_title'
weight = 0
# we manage escape here manually
escape = False

def renderCell(self, item):
value = self.getValue(item)
if not value:
value = u'-'
value = safe_unicode(value)
return u'<a href="{0}">{1}</a>'.format(item.getURL(), value)
return u'<a href="{0}">{1}</a>'.format(item.getURL(), html.escape(value))


class PrettyLinkColumn(TitleColumn):
"""A column that displays the IPrettyLink.getLink column.
This rely on imio.prettylink."""

# escape is managed by imio.prettylink
escape = False

params = {}

@property
Expand Down Expand Up @@ -763,6 +785,8 @@ class ActionsColumn(BrowserViewCallColumn):
'jQuery(document).ready(preventDefaultClickTransition);</script>'
view_name = 'actions_panel'
params = {'showHistory': True, 'showActions': True}
# not necessary to escape, everything is generated
escape = False


class IconsColumn(BaseColumn):
Expand Down
15 changes: 15 additions & 0 deletions src/collective/eeafaceted/z3ctable/tests/test_columns.py
Expand Up @@ -34,6 +34,8 @@
from zope.component import queryMultiAdapter
from zope.intid.interfaces import IIntIds

import html


class TestColumns(IntegrationTestCase):

Expand Down Expand Up @@ -653,6 +655,19 @@ def test_IconsColumn(self):
u'<img title="01" class="" src="http://nohost/plone/01" /> '
u'<img title="02" class="" src="http://nohost/plone/02" />')

def test_escape(self):
table = self.faceted_z3ctable_view
column = BaseColumn(self.portal, self.portal.REQUEST, table)
column.attrName = u'Title'
malicious = 'Malicious"><script>alert(document.domain)</script>'
self.portal.eea_folder.setTitle(malicious)
self.portal.eea_folder.reindexObject()
brains = self.portal.portal_catalog(UID=self.portal.eea_folder.UID())
batch = Batch(brains, size=5)
table.update(batch)
self.assertFalse(malicious in table.render())
self.assertTrue(html.escape(malicious) in table.render())


class BrainsWithoutBatchTable(Table):
""" """
Expand Down

0 comments on commit 8cd5d64

Please sign in to comment.