Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@aliases Endpoint #1398

Merged
merged 47 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
291e6b3
Initial alias push
Petchesi-Iulian May 4, 2022
b9448d1
Added @aliases object POST
Petchesi-Iulian May 4, 2022
271beb9
Remove extra lines
Petchesi-Iulian May 4, 2022
ce18904
Added @aliases DELETE.
Petchesi-Iulian May 4, 2022
27253c0
More errors.
Petchesi-Iulian May 4, 2022
c3e1b97
Merge branch 'master' of https://github.com/plone/plone.restapi into …
Petchesi-Iulian May 4, 2022
ff606b6
Added documentation tests.
Petchesi-Iulian May 4, 2022
785c0fb
Add http-examples.
Petchesi-Iulian May 4, 2022
79fad7a
Fix doc tests.
Petchesi-Iulian May 4, 2022
deec06f
update test examples.
Petchesi-Iulian May 4, 2022
25bb980
Update doc tests.
Petchesi-Iulian May 4, 2022
1274418
Update alias tests.
Petchesi-Iulian May 4, 2022
ae1b4f9
Remove portal root from url
Petchesi-Iulian May 4, 2022
90e97e2
Merge branch 'alias_controlpanel' of https://github.com/plone/plone.r…
Petchesi-Iulian May 4, 2022
70b7415
Modified aliases endpoint permissions.
Petchesi-Iulian May 5, 2022
253c365
Fix aliases test.
Petchesi-Iulian May 5, 2022
796c017
Fix tests
Petchesi-Iulian May 5, 2022
cd3acb2
Update http examples.
Petchesi-Iulian May 5, 2022
e7255bc
Ran black.
Petchesi-Iulian May 5, 2022
a282cd3
Added news feature.
Petchesi-Iulian May 5, 2022
bb60536
ZPretty.
Petchesi-Iulian May 5, 2022
4023828
@aliases Root endpoints (#1406)
Petchesi-Iulian May 6, 2022
013df0b
Added root docs tests.
Petchesi-Iulian May 6, 2022
ab3f00b
Added alias root docs
Petchesi-Iulian May 6, 2022
41db7d5
Added date option to aliases POST root endpoint.
Petchesi-Iulian May 6, 2022
afd7136
Update test docs.
Petchesi-Iulian May 6, 2022
4f53328
Ran black.
Petchesi-Iulian May 6, 2022
8b6a89d
Unify endpoint
Petchesi-Iulian May 6, 2022
23f2780
Update doc tests
Petchesi-Iulian May 6, 2022
97325e6
Update http examples
Petchesi-Iulian May 6, 2022
e6dd33a
Unify POST / DELETE response
avoinea May 6, 2022
0aac0a5
Update http examples.
Petchesi-Iulian May 6, 2022
ce303bb
@aliases - Update docs
avoinea May 6, 2022
8d385bc
Make aliases component expandable (#1416)
avoinea May 9, 2022
085d4a7
Update docs about aliases filtering
avoinea May 9, 2022
784f41f
Unify @aliases GET, POST, DELETE call params
avoinea May 9, 2022
a7876c3
Revert workingcopy_baseline_get.resp
avoinea May 9, 2022
59cb21d
Add total_items to result. (#1437)
avoinea May 24, 2022
6ec6884
Get total aliases count.
Petchesi-Iulian May 25, 2022
897375f
Black.
Petchesi-Iulian May 25, 2022
0b4e729
Set b_start to 0
Petchesi-Iulian May 25, 2022
89d3ed9
Merge branch 'master' into alias_controlpanel
avoinea Jun 15, 2022
11f78c0
Merge branch 'master' into alias_controlpanel
avoinea Jul 19, 2022
47fe714
Merge branch 'master' into alias_controlpanel
rohnsha0 Aug 16, 2022
7cf15f3
Merge branch 'master' into alias_controlpanel
avoinea Aug 30, 2022
845f9da
Merge branch 'master' into alias_controlpanel
avoinea Sep 1, 2022
485184d
Merge branch 'master' into alias_controlpanel
avoinea Sep 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions docs/source/aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
html_meta:
"description": "Aliases - a mechanism to redirect old URLs to new ones."
avoinea marked this conversation as resolved.
Show resolved Hide resolved
"property=og:description": "Aliases - a mechanism to redirect old URLs to new ones."
"property=og:title": "Aliases"
"keywords": "Plone, plone.app.redirector, redirector, REST, API, Aliases"
---

# Aliases

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 URL aliases for a Page

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 URL aliases of a Page

To list aliases, 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 URL aliases of a Page

To remove aliases, 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
```

## Adding URL aliases in bulk

You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root`. **datetime** parameter is optional:

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/aliases_root_add.req
```

Response:

```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_root_add.resp
:language: http
```


## Listing all available aliases

To list all aliases, send a `GET` request to the `/@aliases` endpoint on site `root`:

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/aliases_root_get.req
```

Response:

```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_root_get.resp
:language: http
```

## Filter aliases

To search for specific aliases, send a `GET` request to the `/@aliases` endpoint on site `root` with a `q` parameter:

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/aliases_root_filter.req
```

Response:

```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_root_filter.resp
:language: http
```


## Bulk removing aliases

To bulk remove aliases send a `DELETE` request to the `/@aliases` endpoint on site `root`:

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/aliases_root_delete.req
```

Response:

```{literalinclude} ../../src/plone/restapi/tests/http-examples/aliases_root_delete.resp
:language: http
```
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ expansion
actions
workflow
workingcopy
aliases
locking
sharing
registry
Expand Down
2 changes: 2 additions & 0 deletions news/1393.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added @aliases endpoint with GET/POST/DELETE
[iulianpetchesi]
Empty file.
119 changes: 119 additions & 0 deletions src/plone/restapi/services/aliases/add.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse
from zope.component import getUtility
from plone.app.redirector.interfaces import IRedirectionStorage
from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path
from zope.component import getMultiAdapter
from zExceptions import BadRequest
from DateTime import DateTime
from plone.restapi import _
import plone.protect.interfaces


@implementer(IPublishTraverse)
class AliasesPost(Service):
"""Creates new aliases"""

def reply(self):
data = json_body(self.request)
storage = getUtility(IRedirectionStorage)
aliases = data.get("items", [])

if isinstance(aliases, str):
aliases = [
aliases,
]

# Disable CSRF protection
if "IDisableCSRFProtection" in dir(plone.protect.interfaces):
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)

failed_aliases = []
for alias in aliases:
if isinstance(alias, dict):
alias = alias.get("path")

if alias.startswith("/"):
# Check navigation root
alias = self.edit_for_navigation_root(alias)
else:
failed_aliases.append(alias)
continue

alias, err = absolutize_path(alias, is_source=True)

if err:
failed_aliases.append(alias)
continue

storage.add(
alias,
"/".join(self.context.getPhysicalPath()),
manual=True,
)

if len(failed_aliases) > 0:
return {
"type": "error",
"failed": failed_aliases,
}

return self.reply_no_content()

def edit_for_navigation_root(self, alias):
# Check navigation root
pps = getMultiAdapter((self.context, self.request), name="plone_portal_state")
nav_url = pps.navigation_root_url()
portal_url = pps.portal_url()
if nav_url != portal_url:
# We are in a navigation root different from the portal root.
# Update the path accordingly, unless the user already did this.
extra = nav_url[len(portal_url) :]
if not alias.startswith(extra):
alias = f"{extra}{alias}"
# Finally, return the (possibly edited) redirection
return alias


@implementer(IPublishTraverse)
class AliasesRootPost(Service):
"""Creates new aliases via controlpanel"""

def reply(self):
data = json_body(self.request)
storage = getUtility(IRedirectionStorage)
aliases = data.get("items", [])

# Disable CSRF protection
if "IDisableCSRFProtection" in dir(plone.protect.interfaces):
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)

for alias in aliases:
redirection = alias.get("path")
target = alias.get("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)

date = alias.get("datetime", None)
if date:
date = DateTime(date)

storage.add(abs_redirection, abs_target, now=date, manual=True)

return self.reply_no_content()
61 changes: 61 additions & 0 deletions src/plone/restapi/services/aliases/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
>

<plone:service
method="GET"
accept="application/json,application/schema+json"
factory=".get.AliasesGet"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="zope2.View"
name="@aliases"
/>

<plone:service
method="GET"
accept="application/json,application/schema+json"
factory=".get.AliasesGet"
for="Products.CMFCore.interfaces.IContentish"
permission="zope2.View"
name="@aliases"
/>

<plone:service
method="POST"
factory=".add.AliasesPost"
for="*"
permission="cmf.ModifyPortalContent"
name="@aliases"
/>

<plone:service
method="POST"
factory=".add.AliasesRootPost"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ModifyPortalContent"
name="@aliases"
/>

<plone:service
method="DELETE"
factory=".delete.AliasesDelete"
for="*"
permission="cmf.ModifyPortalContent"
name="@aliases"
/>

<plone:service
method="DELETE"
factory=".delete.AliasesDelete"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ModifyPortalContent"
name="@aliases"
/>

<adapter
factory=".get.Aliases"
name="aliases"
/>

</configure>
51 changes: 51 additions & 0 deletions src/plone/restapi/services/aliases/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse
from zope.component import getUtility
from plone.app.redirector.interfaces import IRedirectionStorage
from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path
from plone.restapi.services.aliases.get import deroot_path
import plone.protect.interfaces


@implementer(IPublishTraverse)
class AliasesDelete(Service):
"""Deletes an alias from object"""

def reply(self):
data = json_body(self.request)
storage = getUtility(IRedirectionStorage)
aliases = data.get("items", [])

if isinstance(aliases, str):
aliases = [
aliases,
]

# Disable CSRF protection
if "IDisableCSRFProtection" in dir(plone.protect.interfaces):
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)

failed_aliases = []
for alias in aliases:
if isinstance(alias, dict):
alias = alias.get("path")

alias, _err = absolutize_path(alias, is_source=True)

try:
storage.remove(alias)
except KeyError:
alias = deroot_path(alias)
failed_aliases.append(alias)
continue

if len(failed_aliases) > 0:
return {
"type": "error",
"failed": failed_aliases,
}

return self.reply_no_content()
Loading