diff --git a/docs/source/aliases.md b/docs/source/aliases.md new file mode 100644 index 0000000000..bd9b10416b --- /dev/null +++ b/docs/source/aliases.md @@ -0,0 +1,70 @@ +--- +html_meta: + "description": "Aliases is a mechanism to redirect old URLs to new ones." + "property=og:description": "Aliases is a mechanism to redirect old URLs to new ones." + "property=og:title": "Aliases" + "keywords": "Plone, plone.app.redirector, redirector, REST, API, Aliases" +--- + +# Aliases + +Aliases is a mechanism to redirect old URLs to new ones. + +When an object is moved (renamed or cut/pasted into a different location), the redirection storage will remember the old path. It is smart enough to deal with transitive references (if we have a -> b and then add b -> c, it is replaced by a reference a -> c) and circular references (attempting to add a -> a does nothing). + +The API consumer can create, read, and delete aliases. + + +| Verb | URL | Action | +| -------- | ----------- | -------------------------------------- | +| `POST` | `/@aliases` | Add one or more aliases | +| `GET` | `/@aliases` | List all aliases | +| `DELETE` | `/@aliases` | Remove one or more aliases | + +## Adding new aliases on a Content Object + +By default, Plone automatically creates a new alias when an object is renamed or moved. Still, you can also create aliases manually. + +To create a new alias, send a POST request to the `/@aliases` endpoint: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../src/plone/restapi/tests/http-examples/aliases_add.req +``` + +Response: + +```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_add.resp +:language: http +``` + +## Listing aliases of a Content Object + +Listing aliases of a resource you can send a `GET` request to the `/@aliases` endpoint: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../src/plone/restapi/tests/http-examples/aliases_get.req +``` + +Response: + +```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_get.resp +:language: http +``` + + +## Removing aliases of a Content Object + +To remove aliases of an object, send a `DELETE` request to the `/@aliases` endpoint: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../src/plone/restapi/tests/http-examples/aliases_delete.req +``` + +Response: + +```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_delete.resp +:language: http +``` diff --git a/docs/source/index.md b/docs/source/index.md index a2ba0cd3bf..2bc47321de 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -33,6 +33,7 @@ expansion actions workflow workingcopy +aliases locking sharing registry diff --git a/src/plone/restapi/services/aliases/add.py b/src/plone/restapi/services/aliases/add.py index 6307f2dcc1..8069c41c21 100644 --- a/src/plone/restapi/services/aliases/add.py +++ b/src/plone/restapi/services/aliases/add.py @@ -5,9 +5,11 @@ from zope.publisher.interfaces import IPublishTraverse from zope.component import getUtility from plone.app.redirector.interfaces import IRedirectionStorage -import plone.protect.interfaces from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path from zope.component import getMultiAdapter +from zExceptions import BadRequest +from plone.restapi import _ +import plone.protect.interfaces @implementer(IPublishTraverse) @@ -73,3 +75,44 @@ def edit_for_navigation_root(self, alias): alias = f"{extra}{alias}" # Finally, return the (possibly edited) redirection return alias + + +@implementer(IPublishTraverse) +class AliasesRootPost(Service): + """Creates new aliases via controlpanel""" + + def __init__(self, context, request): + super().__init__(context, request) + + def reply(self): + data = json_body(self.request) + storage = getUtility(IRedirectionStorage) + aliases = data.get("aliases", []) + + # Disable CSRF protection + if "IDisableCSRFProtection" in dir(plone.protect.interfaces): + alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) + + for alias in aliases: + redirection = alias["path"] + target = alias["redirect-to"] + abs_redirection, err = absolutize_path(redirection, is_source=True) + abs_target, target_err = absolutize_path(target, is_source=False) + + if err and target_err: + err = f"{err} {target_err}" + elif target_err: + err = target_err + else: + if abs_redirection == abs_target: + err = _( + "Alternative urls that point to themselves will cause" + " an endless cycle of redirects." + ) + if err: + raise BadRequest(err) + + storage.add(abs_redirection, abs_target, manual=True) + + self.request.response.setStatus(201) + return {"message": "Successfully added the aliases %s" % aliases} diff --git a/src/plone/restapi/services/aliases/configure.zcml b/src/plone/restapi/services/aliases/configure.zcml index bdfd1d658b..62b8e5aba4 100644 --- a/src/plone/restapi/services/aliases/configure.zcml +++ b/src/plone/restapi/services/aliases/configure.zcml @@ -12,6 +12,15 @@ name="@aliases" /> + + + + + + diff --git a/src/plone/restapi/services/aliases/get.py b/src/plone/restapi/services/aliases/get.py index 483d95f912..503252a433 100644 --- a/src/plone/restapi/services/aliases/get.py +++ b/src/plone/restapi/services/aliases/get.py @@ -4,6 +4,8 @@ from zope.component import getUtility from plone.app.redirector.interfaces import IRedirectionStorage from zope.component.hooks import getSite +from Products.CMFPlone.controlpanel.browser.redirects import RedirectsControlPanel +from plone.restapi.serializer.converters import datetimelike_to_iso @implementer(IPublishTraverse) @@ -17,6 +19,24 @@ def reply(self): return {"aliases": aliases} +@implementer(IPublishTraverse) +class AliasesRootGet(Service): + def reply(self): + """ + redirect-to - target + path - path + redirect - full path with root + """ + batch = RedirectsControlPanel(self.context, self.request).redirects() + redirects = [entry for entry in batch] + + for redirect in redirects: + redirect["datetime"] = datetimelike_to_iso(redirect["datetime"]) + self.request.response.setStatus(201) + + return {"aliases": redirects} + + def deroot_path(path): """Remove the portal root from alias""" portal = getSite()