diff --git a/CHANGES.rst b/CHANGES.rst index 61c4a01f..71a8bb39 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,28 @@ Changelog 2.1.2 (unreleased) ^^^^^^^^^^^^^^^^^^ +- Adiciona Browser Page remote_url_utils. + Tratamento do valor de getRemoteUrl ou remoteUrl para evitar que o path do + site fique exposto nos links. + + [idgserpro] + +- Adiciona patch para Products.CMFPlone.browser.navtree.SitemapNavtreeStrategy.decoratorFactory + para substituição do path do site pela url em getRemoteUrl. + + [idgserpro] + +- Altera viewlet servicos para que trate o valor de getRemoteUrl através da + remote_url_utils. + + [idgserpro] + +- Customiza Browser Page link_redirect_view para que trate o valor de remote_url + através da remote_url_utils; e para que a formação url de links relativos (../, ./) + deixasse de utilizar como base a url do próprio objeto Link. + + [idgserpro] + - Adiciona collective.recaptcha. (fecha `#292 `_). [rodfersou] diff --git a/src/brasil/gov/portal/browser/content/configure.zcml b/src/brasil/gov/portal/browser/content/configure.zcml index 14437417..c4ea045d 100644 --- a/src/brasil/gov/portal/browser/content/configure.zcml +++ b/src/brasil/gov/portal/browser/content/configure.zcml @@ -70,4 +70,15 @@ layer="brasil.gov.portal.interfaces.IBrasilGov" /> + + + + + diff --git a/src/brasil/gov/portal/browser/content/link_redirect_view.py b/src/brasil/gov/portal/browser/content/link_redirect_view.py new file mode 100644 index 00000000..0a2222dd --- /dev/null +++ b/src/brasil/gov/portal/browser/content/link_redirect_view.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from plone.app.contenttypes.browser.link_redirect_view import LinkRedirectView as OriginalView +from plone.app.contenttypes.browser.link_redirect_view import NON_RESOLVABLE_URL_SCHEMES +from plone.app.contenttypes.utils import replace_link_variables_by_paths +from zope.component import getMultiAdapter + + +class LinkRedirectView(OriginalView): + """ + Substituicao do path pela url do site ao utilizar variaveis ${portal_url} + ou ${navigation_root_url} no campo remoteUrl do Link. + Acertada tambem a url de links relativos (../, ./), pois gerava a url + utilizando como base a url do proprio objeto Link. + Isso fazia com que a url, mesmo redirecionando corretamente, ficasse + diferente da real, e não seria atingida pelo purge. + Demanda PR para correção da issue 463: + https://github.com/plonegovbr/brasil.gov.portal/issues/463 + """ + + def absolute_target_url(self): + """Compute the absolute target URL.""" + url = self.context.remoteUrl + + if self._url_uses_scheme(NON_RESOLVABLE_URL_SCHEMES): + # For non http/https url schemes, there is no path to resolve. + return url + + remote_url_utils = getMultiAdapter( + (self.context, self.request), + name=u'remote_url_utils', + ) + path = '/'.join(self.context.getPhysicalPath()) + + if url.startswith('.'): + # ./ ../ ../../ + url = remote_url_utils.remote_url_transform( + path, + url, + ) + else: + url = replace_link_variables_by_paths(self.context, url) + url = remote_url_utils.remote_url_transform( + path, + url, + ) + if not url.startswith(('http://', 'https://')): + portal_state = self.context.restrictedTraverse( + '@@plone_portal_state', + ) + url = '{0}{1}'.format(portal_state.portal_url(), url) + return url diff --git a/src/brasil/gov/portal/browser/content/overrides.zcml b/src/brasil/gov/portal/browser/content/overrides.zcml new file mode 100644 index 00000000..877621ae --- /dev/null +++ b/src/brasil/gov/portal/browser/content/overrides.zcml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/src/brasil/gov/portal/browser/content/remote_url_utils.py b/src/brasil/gov/portal/browser/content/remote_url_utils.py new file mode 100644 index 00000000..60360ec8 --- /dev/null +++ b/src/brasil/gov/portal/browser/content/remote_url_utils.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from plone.app.contenttypes.browser.link_redirect_view import NON_RESOLVABLE_URL_SCHEMES +from Products.Five import BrowserView +from zope.component import getMultiAdapter +from zope.interface import implements +from zope.interface import Interface + + +class IRemoteUrlUtils(Interface): + + def _url_uses_scheme(self): + """""" + + def remote_url_transform(self): + """Transforma o path em url do site.""" + + +class RemoteUrlUtils(BrowserView): + """ + Substituicao do path pela url do site. + Utilizado para o tratamento do valor obtido do metadado getRemoteUrl + via portal_catalog e pela visao padrao do tipo Link (link_redirect_view). + Demanda PR para correção da issue 463: + https://github.com/plonegovbr/brasil.gov.portal/issues/463 + """ + implements(IRemoteUrlUtils) + + def __init__(self, context, request): + self.context = context + self.request = request + self.portal_state = getMultiAdapter( + (self.context, self.request), + name=u'plone_portal_state', + ) + self.portal_path = self.portal_state.navigation_root_path() + self.portal_url = self.portal_state.portal_url() + + def _url_uses_scheme(self, url=None): + for scheme in NON_RESOLVABLE_URL_SCHEMES: + if url.startswith(scheme): + return True + return False + + def remote_url_transform(self, path=None, url=None): + """Transforma o path em url do site.""" + if url: + # http:// https:// + if url.startswith('http://') or url.startswith('https://'): + return url + # file: ftp: mailto: webdav: ... + if self._url_uses_scheme(url): + return url + # ./ ../ + if path: + if url.startswith('.'): + path_items = path.split('/') + url_items = url.split('/') + # ./ + if url_items[0] == '.': + url = url.replace( + './', + '/'.join(path_items[:-1]) + '/', + ) + # ../ ../../../ + elif url_items[0] == '..': + count = url.count('../') + position = count + 1 + url = url.replace( + '../' * count, + '/'.join(path_items[:-position]) + '/', + ) + # /path/site + url = url.replace(self.portal_path, self.portal_url) + return url diff --git a/src/brasil/gov/portal/browser/viewlets/servicos.py b/src/brasil/gov/portal/browser/viewlets/servicos.py index 1940fb09..dda4475f 100644 --- a/src/brasil/gov/portal/browser/viewlets/servicos.py +++ b/src/brasil/gov/portal/browser/viewlets/servicos.py @@ -2,6 +2,7 @@ """ Modulo que implementa o viewlet de servicos do Portal""" from plone.app.layout.viewlets import ViewletBase from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from zope.component import getMultiAdapter class ServicosViewlet(ViewletBase): @@ -14,8 +15,18 @@ def update(self): """ Prepara/Atualiza os valores utilizados pelo Viewlet """ super(ServicosViewlet, self).update() - ps = self.context.restrictedTraverse('@@plone_portal_state') - tools = self.context.restrictedTraverse('@@plone_tools') + ps = getMultiAdapter( + (self.context, self.request), + name='plone_portal_state', + ) + tools = getMultiAdapter( + (self.context, self.request), + name='plone_tools', + ) + self.remote_url_utils = getMultiAdapter( + (self.context, self.request), + name='remote_url_utils', + ) portal = ps.portal() self._folder = 'servicos' in portal.objectIds() and portal['servicos'] self._ct = tools.catalog() @@ -24,10 +35,21 @@ def available(self): return self._folder and True or False def servicos(self): - ct = self._ct folder_path = '/'.join(self._folder.getPhysicalPath()) - portal_types = ['Link'] - results = ct.searchResults(portal_type=portal_types, - path=folder_path, - sort_on='getObjPositionInParent') - return results + query = { + 'portal_type': ['Link'], + 'path': folder_path, + 'sort_on': 'getObjPositionInParent', + } + return [ + { + 'getId': l.getId, + 'Title': l.Title, + 'Description': l.Description, + 'getRemoteUrl': self.remote_url_utils.remote_url_transform( + l.getPath(), + l.getRemoteUrl, + ), + } + for l in self._ct(query) + ] diff --git a/src/brasil/gov/portal/browser/viewlets/templates/servicos.pt b/src/brasil/gov/portal/browser/viewlets/templates/servicos.pt index dec651bb..2762396f 100644 --- a/src/brasil/gov/portal/browser/viewlets/templates/servicos.pt +++ b/src/brasil/gov/portal/browser/viewlets/templates/servicos.pt @@ -10,15 +10,8 @@ class string:portalservicos-item;"> - Tab Name - - + tal:attributes="href tab/getRemoteUrl; + title python:tab['Description'] and tab['Description'] or None;"> Tab Name diff --git a/src/brasil/gov/portal/patches.py b/src/brasil/gov/portal/patches.py index 9346e1f8..8b6e0cdb 100644 --- a/src/brasil/gov/portal/patches.py +++ b/src/brasil/gov/portal/patches.py @@ -1,7 +1,12 @@ # -*- coding: utf-8 -*- +from Acquisition import aq_inner from brasil.gov.portal.logger import logger from plone.app.contenttypes.content import Link +from plone.i18n.normalizer.interfaces import IIDNormalizer from plone.outputfilters.filters import resolveuid_and_caption as base +from Products.CMFPlone import utils +from zope.component import getMultiAdapter +from zope.component import queryUtility def outputfilters(): @@ -54,6 +59,78 @@ def image_tag(self): return self._old_image_tag() +def decoratorFactory(self, node): + """Substituicao do path pela url do site ao utilizar o metadado getRemoteUrl + obtido via portal_catalog. + Demanda PR para correção da issue 463: + https://github.com/plonegovbr/brasil.gov.portal/issues/463 + """ + context = aq_inner(self.context) + request = context.REQUEST + + newNode = node.copy() + item = node['item'] + + portalType = getattr(item, 'portal_type', None) + itemUrl = item.getURL() + if portalType is not None and portalType in self.viewActionTypes: + itemUrl += '/view' + + useRemoteUrl = False + getRemoteUrl = getattr(item, 'getRemoteUrl', None) + isCreator = self.memberId == getattr(item, 'Creator', None) + if getRemoteUrl and not isCreator: + useRemoteUrl = True + + isFolderish = getattr(item, 'is_folderish', None) + showChildren = False + if isFolderish and \ + (portalType is None or portalType not in self.parentTypesNQ): + showChildren = True + + ploneview = getMultiAdapter((context, request), name=u'plone') + + newNode['Title'] = utils.pretty_title_or_id(context, item) + newNode['id'] = item.getId + newNode['UID'] = item.UID + newNode['absolute_url'] = itemUrl + newNode['getURL'] = itemUrl + newNode['path'] = item.getPath() + newNode['item_icon'] = ploneview.getIcon(item) + newNode['Creator'] = getattr(item, 'Creator', None) + newNode['creation_date'] = getattr(item, 'CreationDate', None) + newNode['portal_type'] = portalType + newNode['review_state'] = getattr(item, 'review_state', None) + newNode['Description'] = getattr(item, 'Description', None) + newNode['show_children'] = showChildren + newNode['no_display'] = False # We sort this out with the nodeFilter + # BBB getRemoteUrl and link_remote are deprecated, remove in Plone 4 + # patch: Substitui o path pela url do site. + remote_url_utils = getMultiAdapter( + (context, request), + name=u'remote_url_utils', + ) + remote_url = item.getRemoteUrl and item.getRemoteUrl or None + newNode['getRemoteUrl'] = remote_url_utils.remote_url_transform( + newNode['path'], + remote_url, + ) + # patch + newNode['useRemoteUrl'] = useRemoteUrl + newNode['link_remote'] = newNode['getRemoteUrl'] \ + and newNode['Creator'] != self.memberId + + idnormalizer = queryUtility(IIDNormalizer) + newNode['normalized_portal_type'] = idnormalizer.normalize(portalType) + newNode['normalized_review_state'] = \ + idnormalizer.normalize(newNode['review_state']) + newNode['normalized_id'] = idnormalizer.normalize(newNode['id']) + + return newNode + + logger.info('Patched Products.CMFPlone.browser.navtree.SitemapNavtreeStrategy:decoratorFactory:191') + + def run(): outputfilters() link() diff --git a/src/brasil/gov/portal/patches.zcml b/src/brasil/gov/portal/patches.zcml index b0ee7444..c17596d8 100644 --- a/src/brasil/gov/portal/patches.zcml +++ b/src/brasil/gov/portal/patches.zcml @@ -18,4 +18,11 @@ preserveOriginal="True" /> + + diff --git a/src/brasil/gov/portal/tests/test_contenttypes.py b/src/brasil/gov/portal/tests/test_contenttypes.py index 59b783e3..e36262cc 100644 --- a/src/brasil/gov/portal/tests/test_contenttypes.py +++ b/src/brasil/gov/portal/tests/test_contenttypes.py @@ -110,3 +110,63 @@ def test_link_patched(self): ) plone.remoteUrl = 'http://plone.org/foundation' self.assertEqual(plone.getRemoteUrl(), plone.remoteUrl) + + def test_remote_url_utils(self): + portal_url = self.portal['portal_url']() + remote_url_utils = self.portal.restrictedTraverse('@@remote_url_utils') + # no url + path = '' + url = '' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual(final_url, url) + # no path + path = '' + url = 'http://plone.org/foundation' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual(final_url, url) + # http or https + path = '/plone/assuntos/editoria-a/link-externo' + url = 'http://plone.org/foundation' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual(final_url, url) + # use schema + path = '/plone/assuntos/editoria-a/link-mailto' + url = 'mailto:username@domainname' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual(final_url, url) + # ./ + path = '/plone/assuntos/editoria-a/link-relative' + url = './segundo-nivel' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual( + final_url, + '{0}/assuntos/editoria-a/segundo-nivel'.format(portal_url), + ) + # ../ + path = '/plone/assuntos/editoria-a/link-relative' + url = '../editoria-b' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual( + final_url, + '{0}/assuntos/editoria-b'.format(portal_url), + ) + # ../../ + path = '/plone/assuntos/editoria-a/link-relative' + url = '../../acesso-a-informacao/informacoes-classificadas' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual( + final_url, + '{0}/acesso-a-informacao/informacoes-classificadas'.format( + portal_url, + ), + ) + # /path/site + path = '/plone/assuntos/editoria-a/link-internal' + url = '/plone/acesso-a-informacao/informacoes-classificadas' + final_url = remote_url_utils.remote_url_transform(path, url) + self.assertEqual( + final_url, + '{0}/acesso-a-informacao/informacoes-classificadas'.format( + portal_url, + ), + ) diff --git a/src/brasil/gov/portal/tests/test_decorator_factory.py b/src/brasil/gov/portal/tests/test_decorator_factory.py new file mode 100644 index 00000000..d92df801 --- /dev/null +++ b/src/brasil/gov/portal/tests/test_decorator_factory.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +from brasil.gov.portal.testing import INTEGRATION_TESTING +from plone import api + +import unittest + + +class DecoratorFactoryTestCase(unittest.TestCase): + + layer = INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer['portal'] + self.request = self.layer['request'] + with api.env.adopt_roles(['Manager']): + self.assuntos = api.content.create( + type='Folder', + container=self.portal, + id='assuntos', + title=u'Assuntos', + ) + self.editoria_a = api.content.create( + type='Folder', + container=self.assuntos, + id='editoria-a', + title=u'Editoria A', + ) + self.editoria_b = api.content.create( + type='Folder', + container=self.assuntos, + id='editoria-b', + title=u'Editoria B', + ) + api.content.create( + type='Link', + container=self.editoria_a, + id='link-google', + title=u'Google', + remoteUrl=u'http://www.google.com', + ) + api.content.create( + type='Link', + container=self.editoria_a, + id='link-email', + title=u'Email', + remoteUrl=u'mailto:mailadress@domain.com', + ) + api.content.create( + type='Link', + container=self.editoria_a, + id='link-relativo-1', + title=u'Link Relativo 1', + remoteUrl=u'./link-google', + ) + api.content.create( + type='Link', + container=self.editoria_b, + id='link-relativo-2', + title=u'Link Relativo 2', + remoteUrl=u'../editoria-a', + ) + api.content.create( + type='Link', + container=self.editoria_b, + id='link-portal-url', + title=u'Link portal_url', + remoteUrl=u'${portal_url}/assuntos/editoria-a', + ) + api.content.create( + type='Link', + container=self.editoria_b, + id='link-navigation-root-url', + title=u'Link navigation_root_url', + remoteUrl=u'${navigation_root_url}/assuntos/editoria-a', + ) + + def test_decorator_factory(self): + portal_url = self.portal['portal_url']() + view = self.editoria_a.restrictedTraverse('@@navtree_builder_view') + items = view.navigationTree() + children = items['children'] + for child in children: + if child['id'] == 'assuntos': + for child_a in child['children']: + if child_a['id'] == 'editoria-a': + for child_edit_a in child_a['children']: + if child_edit_a['id'] == 'link-google': + self.assertEqual( + child_edit_a['getRemoteUrl'], + u'http://www.google.com', + ) + elif child_edit_a['id'] == 'link-email': + self.assertEqual( + child_edit_a['getRemoteUrl'], + u'mailto:mailadress@domain.com', + ) + elif child_edit_a['id'] == 'link-relativo-1': + self.assertEqual( + child_edit_a['getRemoteUrl'], + u'{0}/assuntos/editoria-a/link-google'.format(portal_url), + ) + view = self.editoria_b.restrictedTraverse('@@navtree_builder_view') + items = view.navigationTree() + children = items['children'] + for child in children: + if child['id'] == 'assuntos': + for child_a in child['children']: + if child_a['id'] == 'editoria-b': + for child_edit_b in child_a['children']: + if child_edit_b['id'] == 'link-relativo-2': + self.assertEqual( + child_edit_b['getRemoteUrl'], + u'{0}/assuntos/editoria-a'.format(portal_url), + ) + elif child_edit_b['id'] == 'link-portal-url': + self.assertEqual( + child_edit_b['getRemoteUrl'], + u'{0}/assuntos/editoria-a'.format(portal_url), + ) + elif child_edit_b['id'] == 'link-navigation-root-url': + self.assertEqual( + child_edit_b['getRemoteUrl'], + u'{0}/assuntos/editoria-a'.format(portal_url), + ) diff --git a/src/brasil/gov/portal/tests/test_viewlets.py b/src/brasil/gov/portal/tests/test_viewlets.py index 1e065962..becf5f1c 100644 --- a/src/brasil/gov/portal/tests/test_viewlets.py +++ b/src/brasil/gov/portal/tests/test_viewlets.py @@ -148,6 +148,27 @@ def setUp(self): title=u'Servico 2', remoteUrl=u'http://www.plone.org', ) + api.content.create( + type='Link', + container=self.servicos, + id='servico-3', + title=u'Servico 3', + remoteUrl=u'${portal_url}/assuntos/editoria-a', + ) + api.content.create( + type='Link', + container=self.servicos, + id='servico-4', + title=u'Servico 4', + remoteUrl=u'${navigation_root_url}/assuntos/editoria-b', + ) + api.content.create( + type='Link', + container=self.servicos, + id='servico-5', + title=u'Servico 5', + remoteUrl=u'../assuntos/editoria-c', + ) def viewlet(self): viewlet = ServicosViewlet(self.portal, self.request, None, None) @@ -167,15 +188,22 @@ def test_not_available(self): def test_servicos(self): viewlet = self.viewlet() servicos = viewlet.servicos() - self.assertEqual(len(servicos), 2) - self.assertEqual(servicos[0].Title, u'Servico 1') - self.assertEqual(servicos[1].Title, u'Servico 2') + self.assertEqual(len(servicos), 5) + self.assertEqual(servicos[0]['Title'], u'Servico 1') + self.assertEqual(servicos[1]['Title'], u'Servico 2') + self.assertEqual(servicos[2]['Title'], u'Servico 3') + self.assertEqual(servicos[3]['Title'], u'Servico 4') + self.assertEqual(servicos[4]['Title'], u'Servico 5') def test_render(self): + portal_url = self.portal['portal_url']() viewlet = self.viewlet() render = viewlet.render() self.assertIn(u'http://www.google.com', render) self.assertIn(u'http://www.plone.org', render) + self.assertIn(u'{0}/assuntos/editoria-a'.format(portal_url), render) + self.assertIn(u'{0}/assuntos/editoria-b'.format(portal_url), render) + self.assertIn(u'{0}/assuntos/editoria-c'.format(portal_url), render) class NITFBylineViewletTestCase(unittest.TestCase): diff --git a/src/brasil/gov/portal/tests/test_views.py b/src/brasil/gov/portal/tests/test_views.py index c3e71c0f..00cd65ee 100644 --- a/src/brasil/gov/portal/tests/test_views.py +++ b/src/brasil/gov/portal/tests/test_views.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from six.moves import range # noqa: I001 from brasil.gov.portal.browser.album.albuns import Pagination from brasil.gov.portal.config import LOCAL_TIME_FORMAT from brasil.gov.portal.config import TIME_FORMAT @@ -12,6 +11,7 @@ from plone.app.testing import TEST_USER_PASSWORD from plone.testing.z2 import Browser from plonetheme.sunburst.browser.interfaces import IThemeSpecific +from six.moves import range # noqa: I001 from zope.interface import alsoProvides import transaction @@ -449,3 +449,70 @@ def test_data_nao_pode_ser_1969_por_padrao_de_itens_criados(self): self.assertNotIn( '31/12/1969', contents_no_spaces) + + +class LinkRedirectViewTestCase(BaseViewTestCase): + + def setUp(self): + super(LinkRedirectViewTestCase, self).setUp() + with api.env.adopt_roles(['Manager']): + self.subfolder = api.content.create(self.portal, 'Folder', 'subfolder') + self.link_1 = api.content.create( + type='Link', + container=self.subfolder, + id='link-1', + title=u'Link 1', + remoteUrl=u'${portal_url}/assuntos/editoria-a', + ) + self.link_2 = api.content.create( + type='Link', + container=self.subfolder, + id='link-2', + title=u'Link 2', + remoteUrl=u'${navigation_root_url}/assuntos/editoria-b', + ) + self.link_3 = api.content.create( + type='Link', + container=self.subfolder, + id='link-3', + title=u'Link 3', + remoteUrl=u'../../assuntos/editoria-c', + ) + self.link_4 = api.content.create( + type='Link', + container=self.folder, + id='link-4', + title=u'Link 4', + remoteUrl=u'./subfolder', + ) + + def test_link_redirect_view(self): + portal_url = self.portal['portal_url']() + link_1_url = self.link_1.restrictedTraverse( + '@@link_redirect_view', + ).absolute_target_url() + self.assertEqual( + '{0}/assuntos/editoria-a'.format(portal_url), + link_1_url, + ) + link_2_url = self.link_2.restrictedTraverse( + '@@link_redirect_view', + ).absolute_target_url() + self.assertEqual( + '{0}/assuntos/editoria-b'.format(portal_url), + link_2_url, + ) + link_3_url = self.link_3.restrictedTraverse( + '@@link_redirect_view', + ).absolute_target_url() + self.assertEqual( + '{0}/assuntos/editoria-c'.format(portal_url), + link_3_url, + ) + link_4_url = self.link_4.restrictedTraverse( + '@@link_redirect_view', + ).absolute_target_url() + self.assertEqual( + '{0}/folder/subfolder'.format(portal_url), + link_4_url, + )