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()