Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/source/endpoints/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ portrait
principals
querystring
querystringsearch
recycle-bin
registry
relations
roles
Expand Down
147 changes: 147 additions & 0 deletions docs/source/endpoints/recycle-bin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Recycle Bin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above this page title, insert required html meta stuff. See other files in this directory for examples.

Suggested change
# Recycle Bin
# Recycle bin


The Recycle Bin REST API provides endpoints to interact with the Plone Recycle Bin functionality.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The Recycle Bin REST API provides endpoints to interact with the Plone Recycle Bin functionality.
Plone's recycle bin functionality is managed through the `@recyclebin` endpoint.


## List recycle bin contents

To list all items in the recycle bin, send a GET request to the `@recyclebin` endpoint:

```http-example
GET /@recyclebin HTTP/1.1
Accept: application/json
```
Comment on lines +9 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and all http example requests and responses should follow the pattern of other documentation in this directory. This allows tests to run both on the documentation and on the code. Here's example MyST markup that pulls in files as literal includes. You'll also need to create the included files.

## Reading add-ons records
Reading a single record:
```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../../src/plone/restapi/tests/http-examples/addons_get.req
```
Example response:
```{literalinclude} ../../../src/plone/restapi/tests/http-examples/addons_get.resp
:language: http
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Steve is right--look at some of the other services for examples. We're supposed to have tests like the ones in test_documentation.py which actually call the endpoint, then record the request and response so they can be included in the docs. That way we will notice if something changes the response in the future.


Response:

```json
{
"@id": "http://localhost:8080/Plone/@recyclebin",
"items": [
{
"@id": "http://localhost:8080/Plone/@recyclebin/6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"id": "document-1",
"title": "My Document",
"type": "Document",
"path": "/Plone/folder/document-1",
"parent_path": "/Plone/folder",
"deletion_date": "2025-04-27T10:30:45.123456",
"size": 1024,
"recycle_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"actions": {
"restore": "http://localhost:8080/Plone/@recyclebin-restore",
"purge": "http://localhost:8080/Plone/@recyclebin-purge"
}
}
],
"items_total": 1
}
```

## Restore an item from the recycle bin

To restore an item from the recycle bin, send a POST request to the `@recyclebin-restore` endpoint:

```http-example
POST /@recyclebin-restore HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
}
```

You can optionally specify a target path to restore to:

```json
{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
"target_path": "/Plone/another-folder"
}
```

Response:

```json
{
"status": "success",
"message": "Item document-1 restored successfully",
"restored_item": {
"@id": "http://localhost:8080/Plone/document-1",
"id": "document-1",
"title": "My Document",
"type": "Document"
}
}
```

## Purge an item from the recycle bin

To permanently delete an item from the recycle bin, send a POST request to the `@recyclebin-purge` endpoint:

```http-example
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
}
```

Response:

```json
{
"status": "success",
"message": "Item document-1 purged successfully"
}
```

## Purge all items from the recycle bin

To purge all items from the recycle bin:

```http-example
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"purge_all": true
}
```

Response:

```json
{
"status": "success",
"purged_count": 5,
"message": "Purged 5 items from recycle bin"
}
```

## Purge expired items from the recycle bin

To purge only expired items (based on the retention period):

```http-example
POST /@recyclebin-purge HTTP/1.1
Accept: application/json
Content-Type: application/json

{
"purge_expired": true
}
```

Response:

```json
{
"status": "success",
"purged_count": 2,
"message": "Purged 2 expired items from recycle bin"
}
```
1 change: 1 addition & 0 deletions news/1919.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add endpoint for managing recycle bin. @rohnsha0
1 change: 1 addition & 0 deletions src/plone/restapi/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<include package=".querysources" />
<include package=".querystring" />
<include package=".querystringsearch" />
<include package=".recyclebin" />
<include package=".registry" />
<include package=".relations" />
<include package=".roles" />
Expand Down
1 change: 1 addition & 0 deletions src/plone/restapi/services/recyclebin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Empty init file to make the directory a Python package
41 changes: 41 additions & 0 deletions src/plone/restapi/services/recyclebin/configure.zcml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:zcml="http://namespaces.zope.org/zcml"
>

<include package="plone.restapi" />

<plone:service
method="GET"
factory=".get.RecycleBinGet"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin"
/>

<plone:service
method="GET"
factory=".get.RecycleBinGet"
for="Products.CMFCore.interfaces.IFolderish"
permission="cmf.ManagePortal"
name="@recyclebin"
/>

<plone:service
method="POST"
factory=".restore.RecycleBinRestore"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin-restore"
/>

<plone:service
method="POST"
factory=".purge.RecycleBinPurge"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="cmf.ManagePortal"
name="@recyclebin-purge"
/>

</configure>
50 changes: 50 additions & 0 deletions src/plone/restapi/services/recyclebin/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from plone.base.interfaces.recyclebin import IRecycleBin
from plone.restapi.services import Service
from zope.component import getUtility


class RecycleBinGet(Service):
"""GET /@recyclebin - List items in the recycle bin"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also want a GET for /@recyclebin/<item_id> to get one specific item (for the detail page).


def reply(self):
recycle_bin = getUtility(IRecycleBin)

# Check if recycle bin is enabled
if not recycle_bin.is_enabled():
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": "Recycle bin is disabled",
}
}

# Get all items from recycle bin
items = recycle_bin.get_items()

# Format items for response
results = []
for item in items:
results.append(
{
"@id": f"{self.context.absolute_url()}/@recyclebin/{item['recycle_id']}",
"id": item["id"],
"title": item["title"],
"type": item["type"],
"path": item["path"],
"parent_path": item["parent_path"],
"deletion_date": item["deletion_date"].isoformat(),
"size": item["size"],
"recycle_id": item["recycle_id"],
"actions": {
"restore": f"{self.context.absolute_url()}/@recyclebin-restore",
"purge": f"{self.context.absolute_url()}/@recyclebin-purge",
},
}
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should paginate the results using the same batching utility that other services use. This service will also need to take params for the filters needed to support the UI.


return {
"@id": f"{self.context.absolute_url()}/@recyclebin",
"items": results,
"items_total": len(results),
}
93 changes: 93 additions & 0 deletions src/plone/restapi/services/recyclebin/purge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from plone.base.interfaces.recyclebin import IRecycleBin
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zope.component import getUtility
from zope.interface import alsoProvides

import plone.protect.interfaces


class RecycleBinPurge(Service):
"""POST /@recyclebin-purge - Permanently delete an item from the recycle bin"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a DELETE request to /@recyclebin/<item_id>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also support a DELETE on /@recyclebin for emptying the entire recycle bin.


def reply(self):
# Disable CSRF protection for this request
alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)

data = json_body(self.request)
item_id = data.get("item_id", None)
purge_all = data.get("purge_all", False)
purge_expired = data.get("purge_expired", False)

recycle_bin = getUtility(IRecycleBin)

# Check if recycle bin is enabled
if not recycle_bin.is_enabled():
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": "Recycle bin is disabled",
}
}

# Handle purging all items
if purge_all:
purged_count = 0
for item in recycle_bin.get_items():
if recycle_bin.purge_item(item["recycle_id"]):
purged_count += 1

return {
"status": "success",
"purged_count": purged_count,
"message": f"Purged {purged_count} items from recycle bin",
}

# Handle purging expired items
if purge_expired:
purged_count = recycle_bin.purge_expired_items()

return {
"status": "success",
"purged_count": purged_count,
"message": f"Purged {purged_count} expired items from recycle bin",
}

# Handle purging a specific item
if not item_id:
self.request.response.setStatus(400)
return {
"error": {
"type": "BadRequest",
"message": "Missing required parameter: item_id, purge_all, or purge_expired",
}
}

# Get the item to purge
item_data = recycle_bin.get_item(item_id)
if not item_data:
self.request.response.setStatus(404)
return {
"error": {
"type": "NotFound",
"message": f"Item with ID {item_id} not found in recycle bin",
}
}

# Purge the item
success = recycle_bin.purge_item(item_id)

if not success:
self.request.response.setStatus(500)
return {
"error": {
"type": "InternalServerError",
"message": "Failed to purge item",
}
}

return {
"status": "success",
"message": f"Item {item_data['id']} purged successfully",
}
Loading
Loading