Products.CMFPlone/plip-1486-redirection: Add redirection control panel

Stephan Klinger <jenkins-z4DKO/[email protected]>
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: Products.CMFPlone
Branch: refs/heads/plip-1486-redirection
Date: 2017-07-10T09:31:40+03:00
Author: Stephan Klinger (staeff) <[email protected]>
Commit: https://github.com/plone/Products.CMFPlone/commit/5d84e82287ede7d71397a7b86e16fa3ce7e42a61

Add redirection control panel

Files changed:
A Products/CMFPlone/controlpanel/browser/redirects-controlpanel.pt
A Products/CMFPlone/controlpanel/browser/redirects-manage.pt
A Products/CMFPlone/controlpanel/browser/redirects.py
M Products/CMFPlone/controlpanel/browser/configure.zcml

diff --git a/Products/CMFPlone/controlpanel/browser/configure.zcml b/Products/CMFPlone/controlpanel/browser/configure.zcml
index 5115a526a..1c1c008e6 100644
--- a/Products/CMFPlone/controlpanel/browser/configure.zcml
+++ b/Products/CMFPlone/controlpanel/browser/configure.zcml
@@ -291,4 +291,13 @@
       permission="cmf.ManagePortal"
       />
 
+  <browser:page
+    name="redirection-controlpanel"
+    for="*"
+    class=".redirects.RedirectsControlPanel"
+    permission="cmf.ManagePortal"
+    />
+
+    <adapter factory=".redirects.RedirectsControlPanelAdapter" />
+
 </configure>
diff --git a/Products/CMFPlone/controlpanel/browser/redirects-controlpanel.pt b/Products/CMFPlone/controlpanel/browser/redirects-controlpanel.pt
new file mode 100644
index 000000000..e0da95330
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/redirects-controlpanel.pt
@@ -0,0 +1,149 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xml:lang="en-US" lang="en-US"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/prefs_main_template/macros/master"
+      i18n:domain="RedirectionTool">
+
+<metal:block metal:fill-slot="top_slot"
+             tal:define="dummy python:request.set('disable_border', 1)" />
+
+  <body>
+    <div metal:fill-slot="prefs_configlet_main">
+      <!--<dl tal:condition="view/errors"-->
+          <!--class="portalMessage error">-->
+        <!--<dt>Error</dt>-->
+        <!--<dd>-->
+          <!--<p i18n:translate="error_bulk_upload">-->
+            <!--No aliases were added. Please correct these errors in your CSV file and try again:-->
+          <!--</p>-->
+          <!--<table style="text-align: left; vertical-align: top" border="0" cellpadding="2" cellspacing="0">-->
+            <!--<tr tal:repeat="error view/errors"-->
+                <!--style="vertical-align: top">-->
+              <!--<td nowrap="nowrap"><tal:block i18n:translate="label_bulk_upload_line">Line</tal:block> <span tal:replace="error/line_number" /><tal:block i18n:translate="label_bulk_upload_line_suffix">:</tal:block></td>-->
+              <!--<td>-->
+                <!--<code tal:content="error/line" />-->
+              <!--</td>-->
+              <!--<td tal:content="error/message" />-->
+            <!--</tr>-->
+          <!--</table>-->
+        <!--</dd>-->
+      <!--</dl>-->
+
+      <h1 i18n:translate="heading_add_aliases">Manage aliases</h1>
+
+      <p i18n:translate="description_manage_aliases">
+        Using this form, you can select for which types you want to manage
+        aliases and manage all existing aliases.
+      </p>
+
+
+      <!--<form tal:attributes="action view/view_url" method="post">-->
+        <!--<tal:block tal:repeat="widget python: view.form.widgets.values()">-->
+
+            <!--<div class="field"-->
+                 <!--tal:define="description widget/field/description;-->
+                             <!--error widget/error"-->
+                 <!--tal:attributes="class python:'field'+(error and ' error' or '')">-->
+
+                <!--<label i18n:translate=""-->
+                       <!--tal:attributes="for widget/name"-->
+                       <!--tal:content="widget/label">-->
+                    <!--label-->
+                <!--</label>-->
+
+                <!--<span class="fieldRequired"-->
+                      <!--title="Required"-->
+                      <!--i18n:attributes="title title_required;"-->
+                      <!--i18n:translate="label_required"-->
+                      <!--tal:condition="widget/required">-->
+                    <!--(Required)-->
+                <!--</span>-->
+
+                <!--<div class="formHelp"-->
+                     <!--i18n:translate=""-->
+                     <!--tal:content="description"-->
+                     <!--tal:condition="description">-->
+                    <!--field description-->
+                <!--</div>-->
+
+                <!--<div tal:condition="error"-->
+                     <!--tal:content="structure error">-->
+                    <!--The Error-->
+                <!--</div>-->
+
+                <!--<div class="widget" tal:content="structure widget">-->
+                    <!--<input type="text" />-->
+                <!--</div>-->
+            <!--</div>-->
+
+        <!--</tal:block>-->
+
+        <!--<div class="formControls"-->
+             <!--i18n:domain="plone">-->
+          <!--<input class="context"-->
+                 <!--type="submit"-->
+                 <!--value="Save"-->
+                 <!--name="form.button.Save"-->
+                 <!--i18n:attributes="value label_save;"-->
+                 <!--/>-->
+        <!--</div>-->
+      <!--</form>-->
+
+      <!--<form tal:attributes="action view/view_url" method="post">-->
+        <!--<fieldset>-->
+          <!--<legend i18n:translate="legend_all_existing_aliases">All existing aliases for this site</legend>-->
+
+          <!--<tal:redirects repeat="redirect view/redirects">-->
+            <!--<div>-->
+              <!--<input type="checkbox"-->
+                     <!--class="noborder"-->
+                     <!--name="redirects:tuple"-->
+                     <!--tal:attributes="value redirect/redirect" />-->
+
+              <!--<span tal:replace="redirect/path" /> &rarr; <span tal:replace="redirect/redirect-to" />-->
+            <!--</div>-->
+          <!--</tal:redirects>-->
+
+          <!--<div class="formControls">-->
+            <!--<input class="context"-->
+                   <!--type="submit"-->
+                   <!--value="Remove"-->
+                   <!--name="form.button.Remove"-->
+                   <!--i18n:attributes="value"-->
+                   <!--/>-->
+          <!--</div>-->
+
+        <!--</fieldset>-->
+      <!--</form>-->
+
+      <!--<form tal:attributes="action view/view_url" method="post" enctype="multipart/form-data">-->
+        <!--<fieldset>-->
+          <!--<legend i18n:translate="legend_bulk_upload">Bulk-upload aliases</legend>-->
+
+          <!--<p i18n:translate="description_bulk_upload">-->
+            <!--Add many aliases at once by uploading a CSV file. The first column should be the path to redirect from; the second, the path to redirect to. Both paths must be Plone-site-relative, starting with a slash (/).-->
+          <!--</p>-->
+          <!--<p>-->
+            <!--<tal:block i18n:translate="example_caption_bulk_upload">Example:</tal:block><br />-->
+            <!--<code i18n:translate="example_bulk_upload">/old-home-page.asp,/front-page<br />-->
+            <!--/people/JoeT,/Users/joe-thurston</code>-->
+          <!--</p>-->
+
+          <!--<input type="file" name="file" />-->
+
+          <!--<div class="formControls">-->
+            <!--<input class="context"-->
+                   <!--type="submit"-->
+                   <!--value="Upload"-->
+                   <!--name="form.button.Upload"-->
+                   <!--i18n:attributes="value"-->
+                   <!--/>-->
+          <!--</div>-->
+
+        <!--</fieldset>-->
+      <!--</form>-->
+    </div>
+  </body>
+</html>
diff --git a/Products/CMFPlone/controlpanel/browser/redirects-manage.pt b/Products/CMFPlone/controlpanel/browser/redirects-manage.pt
new file mode 100644
index 000000000..51d8f9ebf
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/redirects-manage.pt
@@ -0,0 +1,100 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xml:lang="en-US" lang="en-US"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="here/main_template/macros/master"
+      i18n:domain="RedirectionTool">
+
+  <body>
+    <div metal:fill-slot="main"
+         tal:define="context_state context/@@plone_context_state">
+      <h1 class="documentFirstHeading" i18n:translate="heading_add_aliases">Manage aliases</h1>
+
+      <dl class="portalMessage info" tal:condition="context_state/is_default_page">
+          <dt i18n:translate="">
+              Info
+          </dt>
+          <dd i18n:translate="help_sharing_page_default_page">
+              You are managing the aliases of a default view in a container.
+              To manage them for the entire container,
+              <a i18n:name="go_here"
+                 i18n:translate="help_aliases_go_here"
+                 tal:define="folder context_state/folder"
+                 tal:attributes="href string:${folder/absolute_url}/@@manage-aliases">go here</a>.
+          </dd>
+      </dl>
+
+      <p i18n:translate="description_adding_aliases">
+      Using this form, you can manage aliases for an item. This
+      is an easy way to make an item available under two different URLs.
+      </p>
+
+      <form tal:attributes="action view/view_url" method="post">
+        <fieldset>
+          <legend i18n:translate="legend_existing_aliases">Existing aliases for this item</legend>
+
+          <div class="field"
+               tal:define="error options/errors/redirection|nothing;
+                           redirection request/title | nothing;"
+               tal:attributes="class python:error and 'field error' or 'field'">
+              <label for="title" i18n:translate="label_alias">Alias</label>
+
+              <span class="fieldRequired" title="Required"
+                    i18n:attributes="title"
+                    i18n:translate="label_required">(Required)</span>
+
+              <div tal:content="error">Validation error output</div>
+
+              <div class="formHelp" i18n:translate="help_alias">
+                  Enter the alternative Short Name or an absolute path where an alias should exist.
+              </div>
+
+              <input type="text"
+                     name="redirection"
+                     value="#"
+                     size="40"
+                     tal:attributes="value request/redirection | nothing;"
+                     />
+          </div>
+          <div class="formControls">
+            <input class="context"
+                   type="submit"
+                   value="Add"
+                   name="form.button.Add"
+                   i18n:attributes="value"
+                   />
+          </div>
+
+        </fieldset>
+      </form>
+
+      <form tal:attributes="action view/view_url" method="post">
+        <fieldset>
+          <legend i18n:translate="legend_existing_aliases">Existing aliases for this item</legend>
+
+          <tal:redirects repeat="redirect view/redirects">
+            <div>
+              <input type="checkbox"
+                     class="noborder"
+                     name="redirects:tuple"
+                     tal:attributes="value redirect/redirect" />
+
+              <span tal:replace="redirect/path" />
+            </div>
+          </tal:redirects>
+
+          <div class="formControls">
+            <input class="context"
+                   type="submit"
+                   value="Remove"
+                   name="form.button.Remove"
+                   i18n:attributes="value"
+                   />
+          </div>
+
+        </fieldset>
+      </form>
+    </div>
+  </body>
+</html>
diff --git a/Products/CMFPlone/controlpanel/browser/redirects.py b/Products/CMFPlone/controlpanel/browser/redirects.py
new file mode 100644
index 000000000..ecfdf7893
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/redirects.py
@@ -0,0 +1,261 @@
+import csv
+from cStringIO import StringIO
+
+from zope.interface import implements, Interface
+from zope.component import adapts, getUtility
+from zope.schema import Choice, Tuple
+
+from AccessControl import getSecurityManager
+#from Products.RedirectionTool.permissions import ModifyAliases
+
+from Products.Five.browser import BrowserView
+from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
+
+from Products.statusmessages.interfaces import IStatusMessage
+from Products.CMFCore.interfaces import ISiteRoot
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import IPloneSiteRoot
+from plone.app.redirector.interfaces import IRedirectionStorage
+
+from z3c.form import form, field
+
+from plone.memoize.instance import memoize
+
+from zope.i18nmessageid import MessageFactory
+_ = MessageFactory('plone')
+
+
+def absolutize_path(path, context=None, is_alias=True):
+    """Check whether `path` is a well-formed path from the portal root, and
+       make it Zope-root-relative. If `is_alias` (as opposed to "is_target"),
+       also make sure the user has the requisite ModifyAliases permissions to
+       make an alias from that path. Return a 2-tuple: (absolute redirection path,
+       an error message iff something goes wrong and otherwise '').
+
+    Assume relative paths are relative to `context`; reject relative paths if
+    `context` is None.
+
+    """
+    portal = getUtility(ISiteRoot)
+    err = None
+    if path is None or path == '':
+        err = (is_alias and _(u"You have to enter an alias.")
+               or _(u"You have to enter a target."))
+    elif '://' in path:
+        err = (is_alias and _(u"An alias is a path from the portal root and doesn't include http:// or alike.")  # noqa
+               or _(u"Target path must be relative to the portal root and not include http:// or the like."))  # noqa
+    else:
+        if path.startswith('/'):
+            context_path = "/".join(portal.getPhysicalPath())
+            path = "%s%s" % (context_path, path)
+        else:
+            if context is None:
+                err = (is_alias and _(u"Alias path must start with a slash.")
+                       or _(u"Target path must start with a slash."))
+            else:
+                context_path = "/".join(context.getPhysicalPath()[:-1])
+                path = "%s/%s" % (context_path, path)
+        if not err and is_alias:  # noqa XXX should we require Modify Alias permission on the target as well?
+            source = path.split('/')
+            while len(source):
+                obj = portal.unrestrictedTraverse(source, None)
+                if obj is None:
+                    source = source[:-1]
+                else:
+                    if not getSecurityManager().checkPermission(ModifyAliases, obj):
+                        obj = None
+                    break
+            if obj is None:
+                err = _(u"You don't have the permission to set an alias from the location you provided.")  # noqa
+            else:
+                pass
+                # XXX check if there is an existing alias
+                # XXX check whether there is an object
+    return path, err
+
+
+class RedirectsView(BrowserView):
+    template = ViewPageTemplateFile('redirects-manage.pt')
+
+    def redirects(self):
+        storage = getUtility(IRedirectionStorage)
+        portal = getUtility(ISiteRoot)
+        context_path = "/".join(self.context.getPhysicalPath())
+        portal_path = "/".join(portal.getPhysicalPath())
+        redirects = storage.redirects(context_path)
+        for redirect in redirects:
+            path = redirect[len(portal_path):]
+            yield {
+                'redirect': redirect,
+                'path': path,
+            }
+
+    def __call__(self):
+        storage = getUtility(IRedirectionStorage)
+        request = self.request
+        form = request.form
+        status = IStatusMessage(self.request)
+        errors = {}
+
+        if 'form.button.Add' in form:
+            redirection, err = absolutize_path(form.get('redirection'), is_alias=True)
+            if err:
+                errors['redirection'] = err
+                status.addStatusMessage(err, type='error')
+            else:
+                # XXX check if there is an existing alias
+                # XXX check whether there is an object
+                del form['redirection']
+                storage.add(redirection, "/".join(self.context.getPhysicalPath()))
+                status.addStatusMessage(_(u"Alias added."), type='info')
+        elif 'form.button.Remove' in form:
+            redirects = form.get('redirects', ())
+            for redirect in redirects:
+                storage.remove(redirect)
+            if len(redirects) > 1:
+                status.addStatusMessage(_(u"Aliases removed."), type='info')
+            else:
+                status.addStatusMessage(_(u"Alias removed."), type='info')
+
+        return self.template(errors=errors)
+
+    @memoize
+    def view_url(self):
+        return self.context.absolute_url() + '/@@manage-aliases'
+
+
+class IAliasesSchema(Interface):
+
+    managed_types = Tuple(title=_(u"Managed types"),
+                          description=_(u"Select the types for which the "
+                                        "aliases can be managed"),
+                          required=True,
+                          missing_value=tuple(),
+                          value_type=Choice(
+                              vocabulary="plone.app.vocabularies.ReallyUserFriendlyTypes"))
+
+
+class RedirectsControlPanelAdapter(object):
+
+    adapts(IPloneSiteRoot)
+    implements(IAliasesSchema)
+
+    def __init__(self, context):
+        self.context = context
+        self.rt = getToolByName(context, 'portal_redirection')
+
+    def get_managed_types(self):
+        return self.rt.getRedirectionAllowedForTypes()
+
+    def set_managed_types(self, value):
+        if type(value) in (str, unicode):
+            value = [value]
+        self.rt.setRedirectionAllowedForTypes(value)
+
+    managed_types = property(get_managed_types, set_managed_types)
+
+
+class RedirectsControlPanelForm(form.EditForm):
+    fields = field.Fields(IAliasesSchema)
+
+
+class RedirectsControlPanel(BrowserView):
+    template = ViewPageTemplateFile('redirects-controlpanel.pt')
+
+    def __init__(self, context, request):
+        super(RedirectsControlPanel, self).__init__(context, request)
+        self.errors = []
+        # list of tuples: (line_number, absolute_redirection_path, err_msg, target)
+
+    def redirects(self):
+        storage = getUtility(IRedirectionStorage)
+        portal = getUtility(ISiteRoot)
+        portal_path = "/".join(portal.getPhysicalPath())
+        portal_path_len = len(portal_path)
+        for redirect in storage:
+            if redirect.startswith(portal_path):
+                path = redirect[portal_path_len:]
+            else:
+                path = redirect
+            redirectto = storage.get(redirect)
+            if redirectto.startswith(portal_path):
+                redirectto = redirectto[portal_path_len:]
+            yield {
+                'redirect': redirect,
+                'path': path,
+                'redirect-to': redirectto,
+            }
+
+    def __call__(self):
+        storage = getUtility(IRedirectionStorage)
+        portal = getUtility(ISiteRoot)
+        request = self.request
+        form = request.form
+        status = IStatusMessage(self.request)
+
+        if 'form.button.Remove' in form:
+            redirects = form.get('redirects', ())
+            for redirect in redirects:
+                storage.remove(redirect)
+            if len(redirects) == 0:
+                status.addStatusMessage(_(u"No aliases selected for removal."), type='info')
+            elif len(redirects) > 1:
+                status.addStatusMessage(_(u"Aliases removed."), type='info')
+            else:
+                status.addStatusMessage(_(u"Alias removed."), type='info')
+        elif 'form.button.Save' in form:
+            dst = IAliasesSchema(self.context)
+            dst.managed_types = self.request.form['form.widgets.managed_types']
+        elif 'form.button.Upload' in form:
+            self.upload(form['file'], portal, storage, status)
+
+        self.form = RedirectsControlPanelForm(self.context, self.request)
+        self.form.update()
+        return self.template()
+
+    def upload(self, file, portal, storage, status):
+        """Add the redirections from the CSV file `file`. If anything goes wrong, do nothing."""
+        # Turn all kinds of newlines into LF ones. The csv module doesn't do
+        # its own newline sniffing and requires either \n or \r.
+        file = StringIO('\n'.join(file.read().splitlines()))
+
+        # Use first two lines as a representative sample for guessing format,
+        # in case one is a bunch of headers.
+        dialect = csv.Sniffer().sniff(file.readline() + file.readline())
+        file.seek(0)
+
+        successes = []  # list of tuples: (abs_redirection, target)
+        had_errors = False
+        for i, fields in enumerate(csv.reader(file, dialect)):
+            if len(fields) == 2:
+                redirection, target = fields
+                abs_redirection, err = absolutize_path(redirection, is_alias=True)
+                abs_target, target_err = absolutize_path(target, is_alias=False)
+                if err and target_err:
+                    err = "%s %s" % (err, target_err)  # sloppy w.r.t. i18n
+                elif target_err:
+                    err = target_err
+                else:
+                    if abs_redirection == abs_target:
+                        # TODO: detect indirect recursion
+                        err = _(u"Aliases that point to themselves will cause"
+                                u"an endless cycle of redirects.")
+            else:
+                err = _(u"Each line must have 2 columns.")
+
+            if not err:
+                if not had_errors:  # else don't bother
+                    successes.append((abs_redirection, abs_target))
+            else:
+                had_errors = True
+                self.errors.append(dict(line_number=i+1, line=dialect.delimiter.join(fields),
+                                        message=err))
+
+        if not had_errors:
+            for abs_redirection, abs_target in successes:
+                storage.add(abs_redirection, abs_target)
+            status.addStatusMessage(_(u"%i aliases added.") % len(successes), type='info')
+
+    @memoize
+    def view_url(self):
+        return self.context.absolute_url() + '/@@aliases-controlpanel'



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.