plone.restapi: A RESTful hypermedia API for Plone.¶
Contents¶
Introduction¶
A hypermedia API provides an entry point to the API, which contains hyperlinks the clients can follow. Just like a human user of a regular website, who knows the initial URL of a website and then follows hyperlinks to navigate through the site. This has the advantage that the client only needs to understand how to detect and follow links. The URLs (apart from the inital entry point) and other details of the API can change without breaking the client.
The entry point to the Plone RESTful API is the portal root.
The client can ask for a REST API response by setting the 'Accept'
HTTP header to 'application/json'
:
http
GET /plone HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
This uses so-called ‘content negotiation’
Content Negotiation¶
Content negotiation is a mechanism defined in the HTTP specification that makes it possible to serve different versions of a document (or more generally, a resource representation) at the same URI, so that user agents can specify which version fit their capabilities the best.
The user agent (or the REST consumer) can ask for a specific representation by providing an Accept HTTP header that lists acceptable media types (e.g. JSON):
GET / Accept: application/jsonThe server is then able to supply the version of the resource that best fits the user agent’s needs. This is reflected in the Content-Type header:
HTTP 200 OK Content-Type: application/json { 'data': ... }
The server will then respond with the portal root in the JSON format:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/@navigation"
}
},
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"id": "plone",
"is_folderish": true,
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"description": "Congratulations! You have successfully installed Plone.",
"review_state": "private",
"title": "Welcome to Plone"
}
],
"items_total": 1,
"parent": {},
"tiles": {},
"tiles_layout": {},
"title": "Plone site"
}
@id
is a unique identifier for resources (IRIs).
The @id
property can be used to navigate through the web API by following the links.
@type
sets the data type of a node or typed value
items
is a list that contains all objects within that resource.
A client application can “follow” the links (by calling the @id property) to other resources. This allows to build a losely coupled client that does not break if some of the URLs change, only the entry point of the entire API (in our case the portal root) needs to be known in advance.
Another example, this time showing a request and response for a document. Click on the buttons below to show the different syntaxes for the request.
http
GET /plone/front-page HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/front-page/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/front-page/@workflow"
}
},
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"UID": "SomeUUID000000000000000000000001",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-01-21T01:14:48+00:00",
"creators": [
"test_user_1_"
],
"description": "Congratulations! You have successfully installed Plone.",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "front-page",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-01-21T01:24:11+00:00",
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": {
"content-type": "text/plain",
"data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>",
"encoding": "utf-8"
},
"title": "Welcome to Plone",
"version": "current",
"versioning_enabled": true
}
And so on, see
Plone Content¶
How to get all standard Plone content representations. The syntax is given in various tools, click on ‘curl’, ‘http-request’ or ‘python-requests’ to see examples.
Note
For folderish types, collections or search results, the results will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.
Plone Portal Root:¶
http
GET /plone HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/@navigation" } }, "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "id": "plone", "is_folderish": true, "items": [ { "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "description": "Congratulations! You have successfully installed Plone.", "review_state": "private", "title": "Welcome to Plone" } ], "items_total": 1, "parent": {}, "tiles": {}, "tiles_layout": {}, "title": "Plone site" }Plone Folder:¶
http
GET /plone/folder HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/folder -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/folder Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/folder', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/folder/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/folder/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/folder/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/folder/@workflow" } }, "@id": "http://localhost:55001/plone/folder", "@type": "Folder", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "contributors": [], "created": "2016-01-21T07:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is a folder with two documents", "effective": null, "exclude_from_nav": false, "expires": null, "id": "folder", "is_folderish": true, "items": [ { "@id": "http://localhost:55001/plone/folder/doc1", "@type": "Document", "description": "", "review_state": "private", "title": "A document within a folder" }, { "@id": "http://localhost:55001/plone/folder/doc2", "@type": "Document", "description": "", "review_state": "private", "title": "A document within a folder" } ], "items_total": 2, "language": "", "layout": "listing_view", "modified": "2016-01-21T07:24:11+00:00", "nextPreviousEnabled": false, "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "relatedItems": [], "review_state": "private", "rights": "", "subjects": [], "title": "My Folder", "version": "current" }Plone Document:¶
http
GET /plone/front-page HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/front-page -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/front-page', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/front-page/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/front-page/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/front-page/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/front-page/@workflow" } }, "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "UID": "SomeUUID000000000000000000000001", "allow_discussion": false, "changeNote": "", "contributors": [], "created": "2016-01-21T01:14:48+00:00", "creators": [ "test_user_1_" ], "description": "Congratulations! You have successfully installed Plone.", "effective": null, "exclude_from_nav": false, "expires": null, "id": "front-page", "is_folderish": false, "language": "", "layout": "document_view", "modified": "2016-01-21T01:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "relatedItems": [], "review_state": "private", "rights": "", "subjects": [], "table_of_contents": null, "text": { "content-type": "text/plain", "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", "encoding": "utf-8" }, "title": "Welcome to Plone", "version": "current", "versioning_enabled": true }News Item:¶
Note
Here we show
uuid1
as an example uid for all image scales because this documentation is autogenerated by the tests. When running in a real application, theseuuid1
values will be exchanged by proper uuid4 values.http
GET /plone/newsitem HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/newsitem -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/newsitem Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/newsitem', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/newsitem/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/newsitem/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/newsitem/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/newsitem/@workflow" } }, "@id": "http://localhost:55001/plone/newsitem", "@type": "News Item", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "changeNote": "", "contributors": [], "created": "2016-01-21T02:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is a news item", "effective": null, "exclude_from_nav": false, "expires": null, "id": "newsitem", "image": { "content-type": "image/png", "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "filename": "image.png", "height": 56, "scales": { "icon": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 8, "width": 32 }, "large": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 56, "width": 215 }, "listing": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 4, "width": 16 }, "mini": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 52, "width": 200 }, "preview": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 56, "width": 215 }, "thumb": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 33, "width": 128 }, "tile": { "download": "http://localhost:55001/plone/newsitem/@@images/uuid1.png", "height": 16, "width": 64 } }, "size": 1185, "width": 215 }, "image_caption": "This is an image caption.", "is_folderish": false, "language": "", "layout": "newsitem_view", "modified": "2016-01-21T02:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "relatedItems": [], "review_state": "private", "rights": "", "subjects": [], "text": { "content-type": "text/plain", "data": "<p>Lorem ipsum</p>", "encoding": "utf-8" }, "title": "My News Item", "version": "current", "versioning_enabled": true }Event:¶
http
GET /plone/event HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/event -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/event Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/event', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/event/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/event/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/event/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/event/@workflow" } }, "@id": "http://localhost:55001/plone/event", "@type": "Event", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "attendees": [], "changeNote": "", "contact_email": null, "contact_name": null, "contact_phone": null, "contributors": [], "created": "2016-01-21T03:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is an event", "effective": null, "end": "2013-01-01T12:00:00", "event_url": null, "exclude_from_nav": false, "expires": null, "id": "event", "is_folderish": false, "language": "", "layout": "event_view", "location": null, "modified": "2016-01-21T03:24:11+00:00", "open_end": false, "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "recurrence": null, "relatedItems": [], "review_state": "private", "rights": "", "start": "2013-01-01T10:00:00", "subjects": [], "sync_uid": null, "text": null, "title": "Event", "version": "current", "versioning_enabled": true, "whole_day": false }Image:¶
Note
Here we show
uuid1
as an example uid for all image scales because this documentation is autogenerated by the tests. When running in a real application, theseuuid1
values will be exchanged by proper uuid4 values.http
GET /plone/image HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/image -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/image Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/image', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/image/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/image/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/image/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/image/@workflow" } }, "@id": "http://localhost:55001/plone/image", "@type": "Image", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "contributors": [], "created": "2016-01-21T06:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is an image", "effective": null, "exclude_from_nav": false, "expires": null, "id": "image", "image": { "content-type": "image/png", "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "filename": "image.png", "height": 56, "scales": { "icon": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 8, "width": 32 }, "large": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 56, "width": 215 }, "listing": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 4, "width": 16 }, "mini": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 52, "width": 200 }, "preview": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 56, "width": 215 }, "thumb": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 33, "width": 128 }, "tile": { "download": "http://localhost:55001/plone/image/@@images/uuid1.png", "height": 16, "width": 64 } }, "size": 1185, "width": 215 }, "is_folderish": false, "language": "", "layout": "image_view", "modified": "2016-01-21T06:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "relatedItems": [], "review_state": null, "rights": "", "subjects": [], "title": "My Image", "version": "current" }File:¶
http
GET /plone/file HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/file -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/file Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/file', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/file/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/file/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/file/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/file/@workflow" } }, "@id": "http://localhost:55001/plone/file", "@type": "File", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "contributors": [], "created": "2016-01-21T05:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is a file", "effective": null, "exclude_from_nav": false, "expires": null, "file": { "content-type": "application/pdf", "download": "http://localhost:55001/plone/file/@@download/file", "filename": "file.pdf", "size": 74429 }, "id": "file", "is_folderish": false, "language": "", "layout": "file_view", "modified": "2016-01-21T05:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "relatedItems": [], "review_state": null, "rights": "", "subjects": [], "title": "My File", "version": "current" }Link:¶
http
GET /plone/link HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/link -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/link Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/link', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/link/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/link/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/link/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/link/@workflow" } }, "@id": "http://localhost:55001/plone/link", "@type": "Link", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "changeNote": "", "contributors": [], "created": "2016-01-21T04:14:48+00:00", "creators": [ "test_user_1_" ], "description": "This is a link", "effective": null, "exclude_from_nav": false, "expires": null, "id": "link", "is_folderish": false, "language": "", "layout": "link_redirect_view", "modified": "2016-01-21T04:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "remoteUrl": null, "review_state": "private", "rights": "", "subjects": [], "title": "My Link", "version": "current", "versioning_enabled": true }Collection:¶
http
GET /plone/collection HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0curl
curl -i http://nohost/plone/collection -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/collection Accept:application/json -a admin:secretpython-requests
requests.get('http://nohost/plone/collection', headers={ 'Accept': 'application/json', }, auth=('admin', 'secret'))HTTP/1.1 200 OK Content-Type: application/json { "@components": { "actions": { "@id": "http://localhost:55001/plone/collection/@actions" }, "breadcrumbs": { "@id": "http://localhost:55001/plone/collection/@breadcrumbs" }, "navigation": { "@id": "http://localhost:55001/plone/collection/@navigation" }, "workflow": { "@id": "http://localhost:55001/plone/collection/@workflow" } }, "@id": "http://localhost:55001/plone/collection", "@type": "Collection", "UID": "SomeUUID000000000000000000000002", "allow_discussion": false, "contributors": [], "created": "2016-01-21T08:14:48+00:00", "creators": [ "test_user_1_" ], "customViewFields": [ { "title": "Title", "token": "Title" }, { "title": "Creator", "token": "Creator" }, { "title": "Type", "token": "Type" }, { "title": "ModificationDate", "token": "ModificationDate" } ], "description": "This is a collection with two documents", "effective": null, "exclude_from_nav": false, "expires": null, "id": "collection", "is_folderish": false, "item_count": 30, "items": [ { "@id": "http://localhost:55001/plone/front-page", "@type": "Document", "description": "Congratulations! You have successfully installed Plone.", "review_state": "private", "title": "Welcome to Plone" }, { "@id": "http://localhost:55001/plone/doc1", "@type": "Document", "description": "", "review_state": "private", "title": "Document 1" }, { "@id": "http://localhost:55001/plone/doc2", "@type": "Document", "description": "", "review_state": "private", "title": "Document 2" } ], "items_total": 3, "language": "", "layout": "listing_view", "limit": 1000, "modified": "2016-01-21T08:24:11+00:00", "parent": { "@id": "http://localhost:55001/plone", "@type": "Plone Site", "description": "", "title": "Plone site" }, "query": [ { "i": "portal_type", "o": "plone.app.querystring.operation.string.is", "v": "Document" } ], "relatedItems": [], "review_state": "private", "rights": "", "sort_on": null, "sort_reversed": null, "subjects": [], "text": null, "title": "My Collection", "version": "current" }
Authentication¶
plone.restapi
uses Plone PAS for Authentication.
That means that any authentication method supported by an installed PAS Plugin should work, assuming it’s an authentication method that makes sense to use with an API.
For example, to authenticate using HTTP basic auth, you’d set an Authorization
header:
GET /Plone HTTP/1.1
Authorization: Basic Zm9vYmFyOmZvb2Jhcgo=
Accept: application/json
HTTP client libraries usually contain helper functions to produce a proper Authorization
header for you based on given credentials.
Using the requests
library, you’d set up a session with basic auth like this:
import requests
session = requests.Session()
session.auth = ('username', 'password')
session.headers.update({'Accept': 'application/json'})
response = session.get(url)
Or the same example using curl
:
curl -u username:password -H 'Accept:application/json' $URL
JSON Web Tokens (JWT)¶
plone.restapi
includes a Plone PAS plugin for authentication with JWT. The
plugin is installed automatically when installing the product.
Acquiring a token (@login)¶
A JWT token can be acquired by posting a user’s credentials to the @login
endpoint.
http
POST /plone/@login HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"login": "admin",
"password": "secret"
}
curl
curl -i -X POST http://nohost/plone/@login -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"login": "admin", "password": "secret"}'
httpie
echo '{
"login": "admin",
"password": "secret"
}' | http POST http://nohost/plone/@login Accept:application/json Content-Type:application/json
python-requests
requests.post('http://nohost/plone/@login', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'login': 'admin',
'password': 'secret',
})
The server responds with a JSON object containing the token.
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0"
}
Authenticating with a token¶
The token can now be used in subsequent requests by including it in the
Authorization
header with the Bearer
scheme:
http
GET /plone/ HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0
curl
curl -i http://nohost/plone/ -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
httpie
http http://nohost/plone/ Accept:application/json Authorization:'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
python-requests
requests.get('http://nohost/plone/', headers={
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0',
})
Renewing a token (@login-renew)¶
By default the token will expire after 12 hours and thus must be renewed before
expiration. To renew the token simply post to the @login-renew
endpoint.
http
POST /plone/@login-renew HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0
curl
curl -i -X POST http://nohost/plone/@login-renew -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
httpie
http POST http://nohost/plone/@login-renew Accept:application/json Authorization:'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
python-requests
requests.post('http://nohost/plone/@login-renew', headers={
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0',
})
The server returns a JSON object with a new token:
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0"
}
Invalidating a token (@logout)¶
The @logout
endpoint can be used to invalidate tokens. However by default
tokens are not persisted on the server and thus can not be invalidated. To enable
token invaldiation, activate the store_tokes
option in the PAS plugin. If you
need tokens that are valid indefinitely you should also disable the use of Plone’s
keyring in the PAS plugin (option use_keyring
).
The logout request must contain the existing token in the Authorization
header.
http
POST /plone/@logout HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0
curl
curl -i -X POST http://nohost/plone/@logout -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
httpie
http POST http://nohost/plone/@logout Accept:application/json Authorization:'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0'
python-requests
requests.post('http://nohost/plone/@logout', headers={
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmdWxsbmFtZSI6IiIsInN1YiI6ImFkbWluIn0.RVl8ZFJWIaA-8ujyulJvw0j3F3qFjIHDIJFK0GF6j_0',
})
If invalidation succeeds, the server responds with an empty 204 reponse:
HTTP/1.1 204 No Content
Permissions¶
In order for a user to use the REST API, the plone.restapi: Use REST API
permission is required.
By default, installing the plone.restapi:default
profile will assign this
permission to the Anonymous
role, so everybody is allowed to use the REST
API by default.
If you wish to control in more detail which roles are allowed to use the REST API, please assign this permission accordingly.
As well as the plone.restapi: Use REST API
permission some of the common
Plone permissions are also required, depending on the particular service.
For example, retrieving a resource using GET will require View
, adding an
object using POST will require Add portal content
, and so on.
In order to modify/override this behavior, if your custom service class
inherits from plone.restapi.services.Service
, just override the method
check_permission
and add your custom checks accordingly.
Explore the API using Postman¶
To discover the API interactively, using Postman is recommended.
Note
The Chrome-Extension version of Postman is deprecated and it is recommended to use the native app available instead.
Configuration¶
To easily follow links returned by request based on the API,
- go to the menu under the wrench icon on the top right
- choose Settings
- activate the option Retain headers on clicking on links by selecting ON:
This option makes sure, once a HTTP-Header is configured, it will be reused during following requests , if these are initiated by clicking on links resulting from the initial request. This way navigating the structure using the API becomes a snap.
The option Send anonymous usage data to Postman should be deactivated by setting to OFF.
Usage¶
Choose the suitable HTTP Verb to be used for your request. This can be selected using the Postman drop-down menu.
Enter the Object URL of the object that should be the target of a request into the URL field right to the HTTP Verb:
Now set the appropriate HTTP headers.
- The Authorization Header for the authentication related to the right user
- The Accept Header to initiate the right behaviour by the API related to this Request.
To set the Authorization Header, there is a reserved tab, that is responsible to generate the final Header based on the authentication method and username + password.
You have to select
- in the drop-down menu Basic Auth as the authentication method the term
- A valid existing user with appropriate permissions
After providing these parameters you can create the resulting Authorization Header and insert it into the prepared request by clicking on Preview Request.
Under the Headers tab you now need to insert in the Accept Header application/json` header as well:
The request is now ready and can be send by clicking on Send button.
The Response of the server is now displayed below the Request. You can easily follow the links on the @id
attributes by clicking on them. For every link Postman has prepared another request sharing the same headers that can be send again by licking on the Send button.
Conclusion
You can now explore the whole stucture of your application easily via the API using GET requests.
Content Manipulation¶
plone.restapi does not only expose content objects via a RESTful API. The API consumer can create, read, update, and delete a content object. Those operations can be mapped to the HTTP verbs POST (Create), GET (Read), PUT (Update) and DELETE (Delete).
Manipulating resources across the network by using HTTP as an application protocol is one of core principles of the REST architectural pattern. This allows us to interact with a specific resource in a standardized way:
Verb | URL | Action |
---|---|---|
POST | /folder | Creates a new document within the folder |
GET | /folder/{document-id} | Request the current state of the document |
PATCH | /folder/{document-id} | Update the document details |
DELETE | /folder/{document-id} | Remove the document |
Creating a Resource with POST¶
To create a new resource, we send a POST request to the resource container. If we want to create a new document within an existing folder, we send a POST request to that folder:
http
POST /plone/folder HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"@type": "Document",
"title": "My Document"
}
curl
curl -i -X POST http://nohost/plone/folder -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"@type": "Document", "title": "My Document"}' --user admin:secret
httpie
echo '{
"@type": "Document",
"title": "My Document"
}' | http POST http://nohost/plone/folder Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/folder', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'@type': 'Document',
'title': 'My Document',
}, auth=('admin', 'secret'))
By setting the ‘Accept’ header, we tell the server that we would like to receive the response in the ‘application/json’ representation format.
The ‘Content-Type’ header indicates that the body uses the ‘application/json’ format.
The request body contains the minimal necessary information needed to create a document (the type and the title). You could set other properties, like “description” here as well.
Successful Response (201 Created)¶
If a resource has been created, the server responds with the 201 Created status code. The ‘Location’ header contains the URL of the newly created resource and the resource representation in the payload:
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/folder/my-document
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-10-21T19:00:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-10-21T19:00:00+00:00",
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My Document",
"version": "current",
"versioning_enabled": true
}
Unsuccessful Response (400 Bad Request)¶
If the resource could not be created, for instance because the title was missing in the request, the server responds with 400 Bad Request:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
'message': 'Required title field is missing'
}
The response body can contain information about why the request failed.
Unsuccessful Response (500 Internal Server Error)¶
If the server can not properly process a request, it responds with 500 Internal Server Error:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
'message': 'Internal Server Error'
}
The response body can contain further information such as an error trace or a link to the documentation.
Possible POST Responses¶
Possible server reponses for a POST request are:
- 201 Created (Resource has been created successfully)
- 400 Bad Request (malformed request to the service)
- 500 Internal Server Error (server fault, can not recover internally)
POST Implementation¶
A pseudo-code example of the POST implementation on the server:
try:
order = createOrder()
if order == None:
# Bad Request
response.setStatus(400)
else:
# Created
response.setStatus(201)
except:
# Internal Server Error
response.setStatus(500)
TODO: Link to the real implementation… [
Reading a Resource with GET¶
After a successful POST, we can access the resource by sending a GET request to the resource URL:
http
GET /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/folder/my-document -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/folder/my-document Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/folder/my-document', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
You can also set the include_items GET parameter to false if you don’t want to include children.
Successful Response (200 OK)¶
If a resource has been retrieved successfully, the server responds with 200 OK:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-10-21T19:00:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-10-21T19:00:00+00:00",
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My Document",
"version": "current",
"versioning_enabled": true
}
Note
For folderish types, collections or search results, the results will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.
Unsuccessful response (404 Not Found)¶
If a resource could not be found, the server will respond with 404 Not Found:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
'error': 'NotFound'
}
GET Implementation¶
A pseudo-code example of the GET implementation on the server:
try:
order = getOrder()
if order == None:
# Not Found
response.setStatus(404)
else:
# OK
response.setStatus(200)
except:
# Internal Server Error
response.setStatus(500)
You can find implementation details in the plone.restapi.services.content.add.FolderPost class
GET Responses¶
Possible server reponses for a GET request are:
Updating a Resource with PATCH¶
To update an existing resource we send a PATCH request to the server. PATCH allows to provide just a subset of the resource (the values you actually want to change).
If you send the value null
for a field, the field’s content will be
deleted and the missing_value
defined for the field in the schema
will be set. Note that this is not possible if the field is required
,
and it only works for Dexterity types, not Archetypes:
http
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"title": "My New Document Title"
}
curl
curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"title": "My New Document Title"}' --user admin:secret
httpie
echo '{
"title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/folder/my-document', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'title': 'My New Document Title',
}, auth=('admin', 'secret'))
Successful Response (204 No Content)¶
A successful response to a PATCH request will be indicated by a 204 No Content response by default:
HTTP/1.1 204 No Content
Successful Response (200 OK)¶
You can get the object representation by adding a Prefer header with a value of return=representation to the PATCH request. In this case, the response will be a 200 OK:
http
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Prefer: return=representation
Content-Type: application/json
{
"title": "My New Document Title"
}
curl
curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'Prefer: return=representation' --data-raw '{"title": "My New Document Title"}' --user admin:secret
httpie
echo '{
"title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json Prefer:return=representation -a admin:secret
python-requests
requests.patch('http://nohost/plone/folder/my-document', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Prefer': 'return=representation',
}, json={
'title': 'My New Document Title',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-10-21T19:00:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-10-21T19:00:00+00:00",
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My New Document Title",
"version": "current",
"versioning_enabled": true
}
See for full specs the RFC 5789: PATCH Method for HTTP
Replacing a Resource with PUT¶
Note
PUT is not implemented yet.
To replace an existing resource we send a PUT request to the server:
TODO: Add example.
In accordance with the HTTP specification, a successful PUT will not create a new resource or produce a new URL.
PUT expects the entire resource representation to be supplied to the server, rather than just changes to the resource state. This is usually not a problem since the consumer application requested the resource representation before a PUT anyways.
When the PUT request is accepted and processed by the service, the consumer will receive a 204 No Content response (200 OK would be a valid alternative).
Successful Update (204 No Content)¶
When a resource has been updated successfully, the server sends a 204 No Content response:
TODO: Add example.
Unsuccessful Update (409 Conflict)¶
Sometimes requests fail due to incompatible changes. The response body includes additional information about the problem.
TODO: Add example.
PUT Implementation¶
A pseudo-code example of the PUT implementation on the server:
try:
order = getOrder()
if order:
try:
saveOrder()
except conflict:
response.setStatus(409)
# OK
response.setStatus(200)
else:
# Not Found
response.setStatus(404)
except:
# Internal Server Error
response.setStatus(500)
TODO: Link to the real implementation…
PUT Responses¶
Possible server reponses for a PUT request are:
POST vs. PUT¶
Difference between POST and PUT:
- Use POST to create a resource identified by a service-generated URI
- Use POST to append a resource to a collection identified by a service-generated URI
- Use PUT to overwrite a resource
This follows RFC 7231: HTTP 1.1: PUT Method.
Removing a Resource with DELETE¶
We can delete an existing resource by sending a DELETE request:
http
DELETE /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X DELETE http://nohost/plone/folder/my-document -H 'Accept: application/json' --user admin:secret
httpie
http DELETE http://nohost/plone/folder/my-document Accept:application/json -a admin:secret
python-requests
requests.delete('http://nohost/plone/folder/my-document', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
A successful response will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
DELETE Implementation¶
A pseudo-code example of the DELETE implementation on the server:
try:
order = getOrder()
if order:
if can_delete(order):
# No Content
response.setStatus(204)
else:
# Not Allowed
response.setStatus(405)
else:
# Not Found
response.setStatus(404)
except:
# Internal Server Error
response.setStatus(500)
TODO: Link to the real implementation…
DELETE Responses¶
Possible responses to a delete request are:
- 204 No Content
- 404 Not Found (if the resource does not exist)
- 405 Method Not Allowed (if deleting the resource is not allowed)
- 500 Internal Server Error
Reordering sub resources¶
The resources contained within a resource can be reordered using the ordering key using a PATCH request on the container.
Use the obj_id subkey to specify which resource to reorder. The subkey delta can be ‘top’, ‘bottom’, or a negative or positive integer for moving up or down.
Reordering resources within a subset of resources can be done using the subset_ids subkey.
A response 400 BadRequest with a message ‘Client/server ordering mismatch’ will be returned if the value is not in the same order as serverside.
A response 400 BadRequest with a message ‘Content ordering is not supported by this resource’ will be returned if the container does not support ordering.
http
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"ordering": {"obj_id": "item_3", "delta": "top", "subset_ids": ["item_1", "item_3", "item5"]}
}
curl
curl -i -X PATCH http://nohost/plone/folder/my-document -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"ordering": {"delta": "top", "obj_id": "item_3", "subset_ids": ["item_1", "item_3", "item5"]}}' --user admin:secret
httpie
echo '{
"ordering": {
"delta": "top",
"obj_id": "item_3",
"subset_ids": [
"item_1",
"item_3",
"item5"
]
}
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/folder/my-document', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'ordering': {
'delta': 'top',
'obj_id': 'item_3',
'subset_ids': ['item_1', 'item_3', 'item5'],
},
}, auth=('admin', 'secret'))
History¶
The @history endpoint exposes history and versioning information on previous versions of the content. Each change or workflow change on a content object or file is listed. It also allows to revert to a previous version of the file.
Listing the History of a Content Object¶
Listing versions and history of a resource:
http
GET /plone/front-page/@history HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@history -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page/@history Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@history', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"action": "Create",
"actor": {
"@id": "http://localhost:55001/plone/@users/test_user_1_",
"fullname": "",
"id": "test_user_1_",
"username": "test-user"
},
"comments": "",
"review_state": "private",
"state_title": "Private",
"time": "2016-10-21T19:00:00",
"transition_title": "Create",
"type": "workflow"
},
{
"@id": "http://localhost:55001/plone/front-page/@history/0",
"action": "Edited",
"actor": {
"@id": "http://localhost:55001/plone/@users/test-user",
"fullname": "test-user",
"id": "test-user",
"username": null
},
"comments": null,
"may_revert": true,
"time": "2016-10-21T19:00:00",
"transition_title": "Edited",
"type": "versioning",
"version": 0
}
]
This following fields are returned:
- action: the workflow transition id, ‘Edited’ for versioning, or ‘Create’ for initial state.
- actor: the user who performed the action. This contains a subobject with the details.
- comments: a changenote
- @id: link to the content endpoint of this specific version.
- may_revert: true if the user has permission to revert.
- time: when this action occured in ISO format.
- transition_title: the workflow transition’s title, ‘Edited’ for versioning, or ‘Create’ for initial state.
- type: ‘workflow’ for workflow changes, ‘versioning’ for editing, or null for content creation.
- version: identifier for this specific version of the resource.
Get a Historical Version¶
Older versions of a resource can be retrieved by appending version to the @history endpoint url.
http
GET /plone/folder/my-document/@history/0 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/folder/my-document/@history/0 -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/folder/my-document/@history/0 Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/folder/my-document/@history/0', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Revert to a Historical Version¶
Reverting to an older versions of a resource can be done by sending a PATCH request to the @history endpoint and appending the version you want to revert to.
http
PATCH /plone/front-page/@history HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"version": 0
}
curl
curl -i -X PATCH http://nohost/plone/front-page/@history -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"version": 0}' --user admin:secret
httpie
echo '{
"version": 0
}' | http PATCH http://nohost/plone/front-page/@history Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/front-page/@history', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'version': 0,
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "Welcome to Plone has been reverted to revision 0."
}
Batching¶
Representations of collection-like resources are batched / paginated if the size of the resulset exceeds the batching size:
{
"@id": "http://.../folder/search",
"batching": {
"@id": "http://.../folder/search?b_size=10&b_start=20",
"first": "http://.../plone/folder/search?b_size=10&b_start=0",
"last": "http://.../plone/folder/search?b_size=10&b_start=170",
"prev": "http://.../plone/folder/search?b_size=10&b_start=10",
"next": "http://.../plone/folder/search?b_size=10&b_start=30"
},
"items": [
"..."
],
"items_total": 175,
}
If the entire resulset fits into a single batch page (as determined by
b_size
), the top-level batching
links will be omitted.
Top-level attributes¶
Attribute | Description |
---|---|
@id |
Canonical base URL for the resource, without any batching parameters |
items |
Current batch of items / members of the collection-like resource |
items_total |
Total number of items |
batching |
Batching related navigation links (see below) |
Batching links¶
If, and only if, the resultset has been batched over several pages, the
response body will contain a top-level attribute batching
that contains
batching links. These links that can be used to navigate batches in a
Hypermedia fashion:
Attribute | Description |
---|---|
@id |
Link to the current batch page |
first |
Link to the first batch page |
prev |
Link to the previous batch page (if applicable) |
next |
Link to the next batch page (if applicable) |
last |
Link to the last batch page |
Parameters¶
Batching can be controlled with two query string parameters. In order to
address a specific batch page, the b_start
parameter can be used to
request a specific batch page, containing b_size
items starting from
b_start
.
Parameter | Description |
---|---|
b_size |
Batch size (default is 25) |
b_start |
First item of the batch |
Full example of a batched request and response:
http
GET /plone/folder/@search?b_size=5&sort_on=path HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/folder/@search?b_size=5&sort_on=path' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/folder/@search?b_size=5&sort_on=path' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/folder/@search?b_size=5&sort_on=path', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/folder/@search",
"batching": {
"@id": "http://localhost:55001/plone/folder/@search?b_size=5&sort_on=path",
"first": "http://localhost:55001/plone/folder/@search?b_start=0&b_size=5&sort_on=path",
"last": "http://localhost:55001/plone/folder/@search?b_start=5&b_size=5&sort_on=path",
"next": "http://localhost:55001/plone/folder/@search?b_start=5&b_size=5&sort_on=path"
},
"items": [
{
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder"
},
{
"@id": "http://localhost:55001/plone/folder/doc-1",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Document 1"
},
{
"@id": "http://localhost:55001/plone/folder/doc-2",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Document 2"
},
{
"@id": "http://localhost:55001/plone/folder/doc-3",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Document 3"
},
{
"@id": "http://localhost:55001/plone/folder/doc-4",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Document 4"
}
],
"items_total": 8
}
Comments¶
Plone offers users to post comments on any content object with plone.app.discussion.
Commenting can be enabled globally, for specific content types and for single content objects.
When commenting is enabled on your content object, you can retrieve a list of all existing comments, add new comments, reply to existing comments or delete a comment.
Listing Comments¶
You can list the existing comment on a content object by sending a GET request to the URL of the content object and appending ‘/@comments’:
http
GET /plone/front-page/@comments HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@comments -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page/@comments Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@comments', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a Status 200 and a batched list of all comments:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@comments",
"items": [
{
"@id": "http://localhost:55001/plone/front-page/@comments/1477076400000000",
"@parent": null,
"@type": "Discussion Item",
"author_name": null,
"author_username": null,
"comment_id": "1477076400000000",
"creation_date": "2016-10-21T19:00:00",
"in_reply_to": null,
"is_deletable": true,
"is_editable": true,
"modification_date": "2016-10-21T19:00:00",
"text": {
"data": "Comment 1",
"mime-type": "text/plain"
},
"user_notification": null
},
{
"@id": "http://localhost:55001/plone/front-page/@comments/1477076400000001",
"@parent": "http://localhost:55001/plone/front-page/@comments/1477076400000000",
"@type": "Discussion Item",
"author_name": null,
"author_username": null,
"comment_id": "1477076400000001",
"creation_date": "2016-10-21T19:00:00",
"in_reply_to": "1477076400000000",
"is_deletable": true,
"is_editable": true,
"modification_date": "2016-10-21T19:00:00",
"text": {
"data": "Comment 1.1",
"mime-type": "text/plain"
},
"user_notification": null
}
],
"items_total": 2
}
These following fields are returned:
- @id: Link to the current endpoint
- items: a list of comments for the current resource
- items_total: the total number of comments for the resource
- batching: batching information
The items attribute returns a list of comments, each comment provides the following fields:
- @id: hyperlink to the comment
- @parent: (optional) the parent comment
- author_name: the full name of the author of this comment
- author_username: the username of the author of this comment
- comment_id: the comment ID uniquely identifies the comment
- in_reply_to: the comment ID of the parent comment
- creation_date: when the comment was placed
- modification_date: when the comment was last updated
- text: contains a ‘mime-type’ and ‘text’ attribute with the text of the comment. Default mime-type is ‘text/plain’.
- user_notification: boolean value to indicate if the author of the comment requested notifications on replies
Adding a Comment¶
To add a new comment to a content object, send a POST request to the URL of the content object and append ‘/@comments’ to the URL. The body of the request needs to contain a JSON structure with a ‘text’ attribute that contains the comment text:
http
POST /plone/front-page/@comments/ HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"text": "My comment"
}
curl
curl -i -X POST http://nohost/plone/front-page/@comments/ -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"text": "My comment"}' --user admin:secret
httpie
echo '{
"text": "My comment"
}' | http POST http://nohost/plone/front-page/@comments/ Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@comments/', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'text': 'My comment',
}, auth=('admin', 'secret'))
If the creation of the comment has been successful, the server will respond with a 204 No Content status and the URL of the newly created comment in the location header:
HTTP/1.1 204 No Content
Location: http://localhost:55001/plone/front-page/@comments/123456
Replying to a Comment¶
To add a direct reply to an existing comment, send a POST request to the URL of the comment you want to reply to. The body of the request needs to contain a JSON structure with a ‘text’ attribute that contains the comment text:
http
POST /plone/front-page/@comments/123456 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"text": "My reply"
}
curl
curl -i -X POST http://nohost/plone/front-page/@comments/123456 -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"text": "My reply"}' --user admin:secret
httpie
echo '{
"text": "My reply"
}' | http POST http://nohost/plone/front-page/@comments/123456 Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@comments/123456', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'text': 'My reply',
}, auth=('admin', 'secret'))
If the creation of the comment has been successful, the server will respond with a 204 No Content status and the URL of the newly created comment in the location header:
HTTP/1.1 204 No Content
Location: http://localhost:55001/plone/front-page/@comments/123456
Updating a Comment¶
..note: The permission to update a comment is, by default, only granted to the creater (owner role) of the comment.
An existing comment can be updated by sending a PATCH request to the URL of the comment. The request body needs to contain a JSON structure with at least a ‘text’ attribute:
http
PATCH /plone/front-page/@comments/123456 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"text": "My NEW comment"
}
curl
curl -i -X PATCH http://nohost/plone/front-page/@comments/123456 -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"text": "My NEW comment"}' --user admin:secret
httpie
echo '{
"text": "My NEW comment"
}' | http PATCH http://nohost/plone/front-page/@comments/123456 Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/front-page/@comments/123456', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'text': 'My NEW comment',
}, auth=('admin', 'secret'))
The server will respond with a 204 No Content response and a location header with the comment URL when the comment has been updated successfully:
HTTP/1.1 204 No Content
Location: http://localhost:55001/plone/front-page/@comments/123456
Deleting a Comment¶
An existing comment can be deleted by sending a DELETE request to the URL of the comment.
..note: Deleting a comment will, by default, also delete all existing replies to that comment.
http
DELETE /plone/front-page/@comments/123456 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X DELETE http://nohost/plone/front-page/@comments/123456 -H 'Accept: application/json' --user admin:secret
httpie
http DELETE http://nohost/plone/front-page/@comments/123456 Accept:application/json -a admin:secret
python-requests
requests.delete('http://nohost/plone/front-page/@comments/123456', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
When the comment has been deleted successfully, the server will respond with a 204 No Content response:
HTTP/1.1 204 No Content
Copy / Move¶
Copying an object¶
To copy a content object send a POST request to the /@copy
endpoint at the
destinations url with the source object specified in the request body. The source
object can be specified either by url, path, UID or intid.
http
POST /plone/@copy HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"source": "http://localhost:55001/plone/front-page"
}
curl
curl -i -X POST http://nohost/plone/@copy -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"source": "http://localhost:55001/plone/front-page"}' --user admin:secret
httpie
echo '{
"source": "http://localhost:55001/plone/front-page"
}' | http POST http://nohost/plone/@copy Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@copy', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'source': 'http://localhost:55001/plone/front-page',
}, auth=('admin', 'secret'))
If the copy operation succeeds, the server will respond with status 200 (OK) and return the new and old url of the copied object.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:55001/plone/front-page",
"target": "http://localhost:55001/plone/copy_of_front-page"
}
]
Moving an object¶
To move a content object send a POST request to the /@move
endpoint at the
destinations url with the source object specified in the request body. The source
object can be specified either by url, path, UID or intid.
http
POST /plone/folder/@move HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"source": "http://localhost:55001/plone/front-page"
}
curl
curl -i -X POST http://nohost/plone/folder/@move -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"source": "http://localhost:55001/plone/front-page"}' --user admin:secret
httpie
echo '{
"source": "http://localhost:55001/plone/front-page"
}' | http POST http://nohost/plone/folder/@move Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/folder/@move', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'source': 'http://localhost:55001/plone/front-page',
}, auth=('admin', 'secret'))
If the move operation succeeds, the server will respond with status 200 (OK) and return the new and old url of the moved object.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:55001/plone/front-page",
"target": "http://localhost:55001/plone/folder/front-page"
}
]
Copying/moving multiple objects¶
Multiple objects can be moved/copied by giving a list of sources.
http
POST /plone/@copy HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"source": [
"http://localhost:55001/plone/front-page",
"http://localhost:55001/plone/newsitem"
]
}
curl
curl -i -X POST http://nohost/plone/@copy -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"source": ["http://localhost:55001/plone/front-page", "http://localhost:55001/plone/newsitem"]}' --user admin:secret
httpie
echo '{
"source": [
"http://localhost:55001/plone/front-page",
"http://localhost:55001/plone/newsitem"
]
}' | http POST http://nohost/plone/@copy Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@copy', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'source': ['http://localhost:55001/plone/front-page', 'http://localhost:55001/plone/newsitem'],
}, auth=('admin', 'secret'))
If the operation succeeds, the server will respond with status 200 (OK) and return the new and old urls for each copied/moved object.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"source": "http://localhost:55001/plone/front-page",
"target": "http://localhost:55001/plone/copy_of_front-page"
},
{
"source": "http://localhost:55001/plone/newsitem",
"target": "http://localhost:55001/plone/copy_of_newsitem"
}
]
Expansion¶
Expansion is a mechanism in plone.restapi to embed additional “components”, such as navigation, breadcrumbs, schema, or workflow within the main content response. This helps the API consumers to avoid unneccesary request.
Say you want to show a document in Plone together with the breadcrumbs and a workflow switcher. Instead of doing three individual requests, you can just expand the breadcrumbs and the workflow “components” within the document GET request.
The list of expandable components is listed in the “@components” attribute in the reponse of any content GET request:
GET /plone/front-page HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
{
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"@components": [
{"@id": "http://localhost:55001/plone/front-page/@actions"},
{"@id": "http://localhost:55001/plone/front-page/@breadcrumbs"},
{"@id": "http://localhost:55001/plone/front-page/@navigation"},
{"@id": "http://localhost:55001/plone/front-page/@schema"},
{"@id": "http://localhost:55001/plone/front-page/@workflow"},
...
},
"UID": "1f699ffa110e45afb1ba502f75f7ec33",
"title": "Welcome to Plone",
...
}
Request Unexpanded:
http
GET /plone/front-page HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Response Unexpanded:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/front-page/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/front-page/@workflow"
}
},
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"UID": "SomeUUID000000000000000000000001",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-01-21T01:14:48+00:00",
"creators": [
"test_user_1_"
],
"description": "Congratulations! You have successfully installed Plone.",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "front-page",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-01-21T01:24:11+00:00",
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": {
"content-type": "text/plain",
"data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>",
"encoding": "utf-8"
},
"title": "Welcome to Plone",
"version": "current",
"versioning_enabled": true
}
In order to expand and embed one or more components, use the “expand” GET parameter and provide either a single component or a comma-separated list of the components you want to embed. Say you want to expand the “breadcrumbs” component:
GET /plone/front-page?expand=breadcrumbs HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
{
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/front-page/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs",
"items": [
{
"title": "Welcome to Plone",
"url": "http://localhost:55001/plone/front-page"
}
]
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation"
},
"schema": {
"@id": "http://localhost:55001/plone/front-page/@schema"
},
"workflow": {
"@id": http://localhost:55001/plone/front-page/@workflow"
},
},
"UID": "1f699ffa110e45afb1ba502f75f7ec33",
"title": "Welcome to Plone"
}
Request Expanded:
http
GET /plone/front-page?expand=breadcrumbs HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/front-page?expand=breadcrumbs' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/front-page?expand=breadcrumbs' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page?expand=breadcrumbs', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Response Expanded:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/front-page/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/front-page/@workflow"
}
},
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"UID": "SomeUUID000000000000000000000001",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-01-21T01:14:48+00:00",
"creators": [
"test_user_1_"
],
"description": "Congratulations! You have successfully installed Plone.",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "front-page",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-01-21T01:24:11+00:00",
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": {
"content-type": "text/plain",
"data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>",
"encoding": "utf-8"
},
"title": "Welcome to Plone",
"version": "current",
"versioning_enabled": true
}
Here is an exaxmple of a request that expands all possible expansions:
http
GET /plone/front-page?expand=actions,breadcrumbs,navigation,schema,workflow HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/front-page?expand=actions,breadcrumbs,navigation,schema,workflow' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/front-page?expand=actions,breadcrumbs,navigation,schema,workflow' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page?expand=actions,breadcrumbs,navigation,schema,workflow', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
And the response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"document_actions": [],
"object": [
{
"icon": "",
"id": "view",
"title": "View"
},
{
"icon": "",
"id": "edit",
"title": "Edit"
},
{
"icon": "",
"id": "folderContents",
"title": "Contents"
},
{
"icon": "",
"id": "history",
"title": "History"
},
{
"icon": "",
"id": "local_roles",
"title": "Sharing"
}
],
"object_buttons": [
{
"icon": "",
"id": "cut",
"title": "Cut"
},
{
"icon": "",
"id": "copy",
"title": "Copy"
},
{
"icon": "",
"id": "delete",
"title": "Delete"
},
{
"icon": "",
"id": "rename",
"title": "Rename"
}
],
"portal_tabs": [
{
"icon": "",
"id": "index_html",
"title": "Home"
}
],
"site_actions": [
{
"icon": "",
"id": "sitemap",
"title": "Site Map"
},
{
"icon": "",
"id": "accessibility",
"title": "Accessibility"
},
{
"icon": "",
"id": "contact",
"title": "Contact"
}
],
"user": [
{
"icon": "",
"id": "preferences",
"title": "Preferences"
},
{
"icon": "",
"id": "dashboard",
"title": "Dashboard"
},
{
"icon": "",
"id": "plone_setup",
"title": "Site Setup"
},
{
"icon": "",
"id": "logout",
"title": "Log out"
}
]
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation",
"items": [
{
"@id": "http://localhost:55001/plone",
"description": "",
"title": "Home"
},
{
"@id": "http://localhost:55001/plone/front-page",
"description": "Congratulations! You have successfully installed Plone.",
"title": "Welcome to Plone"
}
]
},
"workflow": {
"@id": "http://localhost:55001/plone/front-page/@workflow",
"history": [
{
"action": null,
"actor": "test_user_1_",
"comments": "",
"review_state": "private",
"time": "2016-10-21T19:00:00+00:00",
"title": "Private"
}
],
"transitions": [
{
"@id": "http://localhost:55001/plone/front-page/@workflow/publish",
"title": "Publish"
},
{
"@id": "http://localhost:55001/plone/front-page/@workflow/submit",
"title": "Submit for publication"
}
]
}
},
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"UID": "SomeUUID000000000000000000000001",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-01-21T01:14:48+00:00",
"creators": [
"test_user_1_"
],
"description": "Congratulations! You have successfully installed Plone.",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "front-page",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-01-21T01:24:11+00:00",
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": {
"content-type": "text/plain",
"data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>",
"encoding": "utf-8"
},
"title": "Welcome to Plone",
"version": "current",
"versioning_enabled": true
}
Portal Actions¶
Plone has the concept of configurable actions (called “portal_actions”). Each actions defines an id, a title, the required permissions and a condition that will be checked to decide if the action will be available for a user. Actions are sorted by categories.
Actions can be used to build UI elements that adapt to the available actions. An example is the Plone toolbar where the “object_tabs” (view, edit, folder contents, sharing) and the “user_actions” (login, logout, preferences) are used to display the user only the actions that are allowed for the currently logged in user.
The available actions for the currently logged in user can be retrieved by calling the @actions endpoint on a specific context. This also works for not authenticated users.
Listing available actions¶
To list the available actions, send a GET request to the ‘@actions’ endpoint on a specific content object:
http
GET /plone/@actions HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@actions -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@actions Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@actions', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a 200 OK status code. The JSON response contains the available actions categories (object, object_buttons, user) on the top level. Each category contains a list of the available actions in that category:
HTTP/1.1 200 OK
Content-Type: application/json
{
"object": [
{
"icon": "",
"id": "view",
"title": "View"
},
{
"icon": "",
"id": "edit",
"title": "Edit"
},
{
"icon": "",
"id": "folderContents",
"title": "Contents"
},
{
"icon": "",
"id": "history",
"title": "History"
},
{
"icon": "",
"id": "local_roles",
"title": "Sharing"
}
],
"object_buttons": [
{
"icon": "",
"id": "cut",
"title": "Cut"
},
{
"icon": "",
"id": "copy",
"title": "Copy"
},
{
"icon": "",
"id": "delete",
"title": "Delete"
},
{
"icon": "",
"id": "rename",
"title": "Rename"
}
],
"user": [
{
"icon": "",
"id": "preferences",
"title": "Preferences"
},
{
"icon": "",
"id": "plone_setup",
"title": "Site Setup"
},
{
"icon": "",
"id": "logout",
"title": "Log out"
}
]
}
If you want to limit the categories that are retured, pass one or more parameters
categories:list
, i.e. @action?categories:list=object&categories:list=user
.
Workflow¶
Note
Currently the workflow support is limited to executing transitions on content.
In Plone, content almost always has a workflow attached.
We can get the current state and history of an object by issuing a GET
request using on any context:
http
GET /plone/front-page/@workflow HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@workflow -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page/@workflow Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@workflow', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@workflow",
"history": [
{
"action": null,
"actor": "test_user_1_",
"comments": "",
"review_state": "private",
"time": "2016-10-21T19:00:00+00:00",
"title": "Private"
}
],
"transitions": [
{
"@id": "http://localhost:55001/plone/front-page/@workflow/publish",
"title": "Publish"
},
{
"@id": "http://localhost:55001/plone/front-page/@workflow/submit",
"title": "Submit for publication"
}
]
}
Now, if we want to change the state of the front page to publish, we would proceed by issuing a POST
request to the given URL:
http
POST /plone/front-page/@workflow/publish HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X POST http://nohost/plone/front-page/@workflow/publish -H 'Accept: application/json' --user admin:secret
httpie
http POST http://nohost/plone/front-page/@workflow/publish Accept:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@workflow/publish', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"action": "publish",
"actor": "admin",
"comments": "",
"review_state": "published",
"time": "2016-10-21T19:05:00+00:00",
"title": "Published with accent \u00e9"
}
We can also also change the state recursively for all contained items, provide a comment and set effective and expiration dates:
http
POST /plone/folder/@workflow/publish HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"comment": "Publishing my folder...",
"effective": "2018-01-21T08:00:00",
"expires": "2019-01-21T08:00:00",
"include_children": true
}
curl
curl -i -X POST http://nohost/plone/folder/@workflow/publish -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"comment": "Publishing my folder...", "effective": "2018-01-21T08:00:00", "expires": "2019-01-21T08:00:00", "include_children": true}' --user admin:secret
httpie
echo '{
"comment": "Publishing my folder...",
"effective": "2018-01-21T08:00:00",
"expires": "2019-01-21T08:00:00",
"include_children": true
}' | http POST http://nohost/plone/folder/@workflow/publish Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/folder/@workflow/publish', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'comment': 'Publishing my folder...',
'effective': '2018-01-21T08:00:00',
'expires': '2019-01-21T08:00:00',
'include_children': True,
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"action": "publish",
"actor": "admin",
"comments": "Publishing my folder...",
"review_state": "published",
"time": "2016-10-21T19:05:00+00:00",
"title": "Published with accent \u00e9"
}
Locking¶
Locking is a mechanism to prevent users from accidentially overriding each others changes.
When a user edits a content object in Plone, the object is locked until the user hits the save or cancel button. If a second user tries to edit the object at the same time, she will see a message that this object is locked.
Locking an object¶
To lock an object send a POST request to the /@lock
endpoint that is available on any content object in Plone:
http
POST /plone/front-page/@lock HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X POST http://nohost/plone/front-page/@lock -H 'Accept: application/json' --user admin:secret
httpie
http POST http://nohost/plone/front-page/@lock Accept:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@lock', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
If the lock operation succeeds, the server will respond with status 200 OK and return various information about the lock including the lock token. The token is needed in later requests to update the locked object.
HTTP/1.1 200 OK
Content-Type: application/json
{
"creator": "admin",
"locked": true,
"name": "plone.locking.stealable",
"stealable": true,
"time": 1477076400.0,
"timeout": 600,
"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"
}
By default, locks are stealable. That means that another user can unlock the object.
If you want to create a non-stealable lock, pass "stealable": false
in the request body.
To create a lock with a non-default timeout, you can pass the the timeout value in seconds in the request body.
The following example creates a non-stealable lock with a timeout of 1h.
http
POST /plone/front-page/@lock HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"stealable": false,
"timeout": 3600
}
curl
curl -i -X POST http://nohost/plone/front-page/@lock -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"stealable": false, "timeout": 3600}' --user admin:secret
httpie
echo '{
"stealable": false,
"timeout": 3600
}' | http POST http://nohost/plone/front-page/@lock Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@lock', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'stealable': False,
'timeout': 3600,
}, auth=('admin', 'secret'))
The server responds with status 200 OK and returns the lock information.
HTTP/1.1 200 OK
Content-Type: application/json
{
"creator": "admin",
"locked": true,
"name": "plone.locking.stealable",
"stealable": true,
"time": 1477076400.0,
"timeout": 3600,
"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"
}
Unlocking an object¶
To unlock an object send a POST request to the /@unlock
endpoint.
http
POST /plone/front-page/@unlock HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X POST http://nohost/plone/front-page/@unlock -H 'Accept: application/json' --user admin:secret
httpie
http POST http://nohost/plone/front-page/@unlock Accept:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@unlock', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server responds with status 200 OK and returns the lock information.
HTTP/1.1 200 OK
Content-Type: application/json
{
"locked": false,
"stealable": true
}
Refreshing a lock¶
An existing lock can be refreshed by sending a POST request to the @refresh-lock
endpoint.
http
POST /plone/front-page/@refresh-lock HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X POST http://nohost/plone/front-page/@refresh-lock -H 'Accept: application/json' --user admin:secret
httpie
http POST http://nohost/plone/front-page/@refresh-lock Accept:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/front-page/@refresh-lock', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server responds with status 200 OK and returns the lock information containing the updated creation time.
HTTP/1.1 200 OK
Content-Type: application/json
{
"creator": "admin",
"locked": true,
"name": "plone.locking.stealable",
"stealable": true,
"time": 1477076400.0,
"timeout": 600,
"token": "0.684672730996-0.25195226375-00105A989226:1477076400.000"
}
Getting lock information¶
To find out if an object is locked or to get information about the current lock
you can send a GET request to the @lock
endpoint.
http
GET /plone/front-page/@lock HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@lock -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page/@lock Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@lock', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server responds with status 200 OK and returns the information about the lock.
HTTP/1.1 200 OK
Content-Type: application/json
{
"locked": false,
"stealable": true
}
Updating a locked object¶
To update a locked object with a PATCH request, you have to provide the lock
token with the Lock-Token
header.
http
PATCH /plone/front-page HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Lock-Token: 0.684672730996-0.25195226375-00105A989226:1477076400.000
Content-Type: application/json
{
"title": "New Title"
}
curl
curl -i -X PATCH http://nohost/plone/front-page -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'Lock-Token: 0.684672730996-0.25195226375-00105A989226:1477076400.000' --data-raw '{"title": "New Title"}' --user admin:secret
httpie
echo '{
"title": "New Title"
}' | http PATCH http://nohost/plone/front-page Accept:application/json Content-Type:application/json Lock-Token:0.684672730996-0.25195226375-00105A989226:1477076400.000 -a admin:secret
python-requests
requests.patch('http://nohost/plone/front-page', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Lock-Token': '0.684672730996-0.25195226375-00105A989226:1477076400.000',
}, json={
'title': 'New Title',
}, auth=('admin', 'secret'))
Sharing¶
Plone comes with a sophisticated user management system that allows to assign users and groups with global roles and permissions. Sometimes this in not enough though and you might want to give users the permission to access or edit a specific part of your website or a specific content object. This is where local roles (located in the Plone sharing tab) come in handy.
Retrieving Local Roles¶
In plone.restapi, the representation of any content object will include a hypermedia link to the local role / sharing information in the sharing
attribute:
GET /plone/folder HTTP/1.1
Accept: application/json
HTTP 200 OK
content-type: application/json
{
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
...
"sharing": {
"@id": "http://localhost:55001/plone/folder/@sharing",
"title": "Sharing",
}
}
The sharing information of a content object can also be directly accessed by appending /@sharing
to the GET request to the URL of a content object. E.g. to access the sharing information for a top-level folder, do:
http
GET /plone/folder/@sharing HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/folder/@sharing -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/folder/@sharing Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/folder/@sharing', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"available_roles": [
{
"id": "Contributor",
"title": "Can add"
},
{
"id": "Editor",
"title": "Can edit"
},
{
"id": "Reader",
"title": "Can view"
},
{
"id": "Reviewer",
"title": "Can review"
}
],
"entries": [
{
"disabled": false,
"id": "AuthenticatedUsers",
"login": null,
"roles": {
"Contributor": false,
"Editor": false,
"Reader": false,
"Reviewer": false
},
"title": "Logged-in users",
"type": "group"
}
],
"inherit": true
}
The available_roles
property contains the list of roles that can be
managed via the sharing page. It contains dictionaries with the role ID and
its translated title
(as it appears on the sharing page).
Searching for principals¶
Users and/or groups without a sharing entry can be found by appending the argument search
to the query string. ie ?search=admin
.
Global roles are marked with the string "global"
. Inherited roles are marked with the string "acquired"
.
http
GET /plone/folder/doc/@sharing?search=admin HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/folder/doc/@sharing?search=admin' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/folder/doc/@sharing?search=admin' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/folder/doc/@sharing?search=admin', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"available_roles": [
{
"id": "Contributor",
"title": "Can add"
},
{
"id": "Editor",
"title": "Can edit"
},
{
"id": "Reader",
"title": "Can view"
},
{
"id": "Reviewer",
"title": "Can review"
}
],
"entries": [
{
"id": "Administrators",
"login": null,
"roles": {
"Contributor": false,
"Editor": false,
"Reader": false,
"Reviewer": false
},
"title": "Administrators",
"type": "group"
},
{
"disabled": false,
"id": "AuthenticatedUsers",
"login": null,
"roles": {
"Contributor": false,
"Editor": false,
"Reader": false,
"Reviewer": false
},
"title": "Logged-in users",
"type": "group"
},
{
"id": "Site Administrators",
"login": null,
"roles": {
"Contributor": false,
"Editor": false,
"Reader": false,
"Reviewer": false
},
"title": "Site Administrators",
"type": "group"
},
{
"disabled": true,
"id": "admin",
"roles": {
"Contributor": "global",
"Editor": "acquired",
"Reader": false,
"Reviewer": false
},
"title": "admin",
"type": "user"
}
],
"inherit": true
}
Updating Local Roles¶
You can update the ‘sharing’ information by sending a POST request to the object URL and appending /@sharing
, e.g. /plone/folder/@sharing
. E.g. say you want to give the AuthenticatedUsers
group the Reader
local role for a folder:
http
POST /plone/folder/@sharing HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"entries": [
{
"id": "AuthenticatedUsers",
"roles": {
"Contributor": false,
"Editor": false,
"Reader": true,
"Reviewer": true
},
"type": "user"
}
],
"inherit": true
}
curl
curl -i -X POST http://nohost/plone/folder/@sharing -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"entries": [{"type": "user", "id": "AuthenticatedUsers", "roles": {"Contributor": false, "Reviewer": true, "Editor": false, "Reader": true}}], "inherit": true}' --user admin:secret
httpie
echo '{
"entries": [
{
"id": "AuthenticatedUsers",
"roles": {
"Contributor": false,
"Editor": false,
"Reader": true,
"Reviewer": true
},
"type": "user"
}
],
"inherit": true
}' | http POST http://nohost/plone/folder/@sharing Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/folder/@sharing', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'entries': [{
'type': 'user',
'id': 'AuthenticatedUsers',
'roles': {
'Contributor': False,
'Reviewer': True,
'Editor': False,
'Reader': True,
},
}],
'inherit': True,
}, auth=('admin', 'secret'))
HTTP/1.1 204 No Content
Registry¶
Registry records can be addressed through the @registry
endpoint on the
Plone site. In order to address a specific record, the fully qualified dotted
name of the registry record has to be passed as a path segment
(e.g. /plone/@registy/my.record).
Reading or writing registry records require the cmf.ManagePortal
permission.
Reading registry records¶
Reading a single record:
http
GET /plone/@registry/plone.app.querystring.field.path.title HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@registry/plone.app.querystring.field.path.title -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@registry/plone.app.querystring.field.path.title Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@registry/plone.app.querystring.field.path.title', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Example Response:
HTTP/1.1 200 OK
Content-Type: application/json
"Location"
Listing registry records¶
The registry records listing uses a batched method to access all registry records. See Batching for more details on how to work with batched results.
The output per record contains the following fields: name: The record’s fully qualified dotted name. value: The record’s value. This is the same as GETting @registry/name.
http
GET /plone/@registry HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@registry -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@registry Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@registry', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Example Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@registry",
"batching": {
"@id": "http://localhost:55001/plone/@registry",
"first": "http://localhost:55001/plone/@registry?b_start=0",
"last": "http://localhost:55001/plone/@registry?b_start=1675",
"next": "http://localhost:55001/plone/@registry?b_start=25"
},
"items": [
{
"name": "Products.CMFPlone.i18nl10n.override_dateformat.Enabled",
"schema": {
"properties": {
"description": "Override the translation machinery",
"title": "Enabled",
"type": "boolean"
}
},
"value": false
},
{
"name": "Products.CMFPlone.i18nl10n.override_dateformat.date_format_long",
"schema": {
"properties": {
"description": "Default value: %Y-%m-%d %H:%M (2038-01-19 03:14)",
"title": "old ZMI property: localLongTimeFormat",
"type": "string"
}
},
"value": "%Y-%m-%d %H:%M"
},
{
"name": "Products.CMFPlone.i18nl10n.override_dateformat.date_format_short",
"schema": {
"properties": {
"description": "Default value: %Y-%m-%d (2038-01-19)",
"title": "old ZMI property: localTimeFormat",
"type": "string"
}
},
"value": "%Y-%m-%d"
},
{
"name": "Products.CMFPlone.i18nl10n.override_dateformat.time_format",
"schema": {
"properties": {
"description": "Default value: %H:%M (03:14)",
"title": "old ZMI property: localTimeOnlyFormat",
"type": "string"
}
},
"value": "%H:%M"
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.allowed",
"schema": {
"properties": {
"default": true,
"description": "Allow syndication for collections and folders on site.",
"title": "Allowed",
"type": "boolean"
}
},
"value": true
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.allowed_feed_types",
"schema": {
"properties": {
"additionalItems": true,
"default": [
"RSS|RSS 1.0",
"rss.xml|RSS 2.0",
"atom.xml|Atom",
"itunes.xml|iTunes"
],
"description": "Separate view name and title by '|'",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Allowed Feed Types",
"type": "array",
"uniqueItems": true
}
},
"value": [
"RSS|RSS 1.0",
"rss.xml|RSS 2.0",
"atom.xml|Atom",
"itunes.xml|iTunes"
]
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.default_enabled",
"schema": {
"properties": {
"default": false,
"description": "If syndication should be enabled by default for all folders and collections.",
"title": "Enabled by default",
"type": "boolean"
}
},
"value": false
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.max_items",
"schema": {
"properties": {
"default": 15,
"description": "Maximum number of items that will be syndicated.",
"minimum": 1,
"title": "Maximum items",
"type": "integer"
}
},
"value": 15
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.render_body",
"schema": {
"properties": {
"default": false,
"description": "If body text available for item, render it, otherwise use description.",
"title": "Render Body",
"type": "boolean"
}
},
"value": false
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.search_rss_enabled",
"schema": {
"properties": {
"default": true,
"description": "Allows users to subscribe to feeds of search results",
"title": "Search RSS enabled",
"type": "boolean"
}
},
"value": true
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.show_author_info",
"schema": {
"properties": {
"default": true,
"description": "Should feeds include author information",
"title": "Show author info",
"type": "boolean"
}
},
"value": true
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.show_syndication_button",
"schema": {
"properties": {
"description": "Makes it possible to customize syndication settings for particular folders and collections ",
"title": "Show settings button",
"type": "boolean"
}
},
"value": null
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.show_syndication_link",
"schema": {
"properties": {
"description": "Enable RSS link document action on the syndication content item.",
"title": "Show feed link",
"type": "boolean"
}
},
"value": null
},
{
"name": "Products.CMFPlone.interfaces.syndication.ISiteSyndicationSettings.site_rss_items",
"schema": {
"properties": {
"additionalItems": true,
"default": [
"/news/aggregator"
],
"description": "Paths to folders and collections to link to at the portal root.",
"items": {
"description": "",
"title": "",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SyndicatableFeedItems"
}
},
"title": "Site RSS",
"type": "array",
"uniqueItems": true
}
},
"value": [
"/news/aggregator"
]
},
{
"name": "Products.ResourceRegistries.interfaces.settings.IResourceRegistriesSettings.resourceBundlesForThemes",
"schema": {
"properties": {
"description": "Maps skin names to lists of resource bundle names",
"key_type": {
"additional": {},
"schema": {
"description": "",
"title": "",
"type": "string"
}
},
"title": "Resource bundles for themes",
"type": "dict",
"value_type": {
"additional": {},
"schema": {
"description": "",
"title": "",
"type": "string"
}
}
}
},
"value": {
"(default)": [
"jquery",
"default"
]
}
},
{
"name": "plone.alignment_styles",
"schema": {
"properties": {
"additionalItems": true,
"default": [
"Left|alignleft|alignleft",
"Center|aligncenter|aligncenter",
"Right|alignright|alignright",
"Justify|alignjustify|alignjustify"
],
"description": "Name|format|icon",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Alignment styles",
"type": "array",
"uniqueItems": false
}
},
"value": [
"Left|alignleft|alignleft",
"Center|aligncenter|aligncenter",
"Right|alignright|alignright",
"Justify|alignjustify|alignjustify"
]
},
{
"name": "plone.allow_anon_views_about",
"schema": {
"properties": {
"default": false,
"description": "If not selected only logged-in users will be able to view information about who created an item and when it was modified.",
"title": "Allow anyone to view 'about' information",
"type": "boolean"
}
},
"value": false
},
{
"name": "plone.allow_external_login_sites",
"schema": {
"properties": {
"additionalItems": true,
"default": [],
"description": "",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Allow external login sites",
"type": "array",
"uniqueItems": true
}
},
"value": []
},
{
"name": "plone.allowed_sizes",
"schema": {
"properties": {
"additionalItems": true,
"default": [
"large 768:768",
"preview 400:400",
"mini 200:200",
"thumb 128:128",
"tile 64:64",
"icon 32:32",
"listing 16:16"
],
"description": "Specify all allowed maximum image dimensions, one per line. The required format is <name> <width>:<height>.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Allowed image sizes",
"type": "array",
"uniqueItems": false
}
},
"value": [
"large 768:768",
"preview 400:400",
"mini 200:200",
"thumb 128:128",
"tile 64:64",
"icon 32:32",
"listing 16:16"
]
},
{
"name": "plone.allowed_types",
"schema": {
"properties": {
"additionalItems": true,
"default": [
"text/html",
"text/x-web-textile"
],
"description": "Select which formats are available for users as alternative to the default format. Note that if new formats are installed, they will be enabled for text fields by default unless explicitly turned off here or by the relevant installer.",
"items": {
"description": "",
"title": "",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AllowableContentTypes"
}
},
"title": "Alternative formats",
"type": "array",
"uniqueItems": true
}
},
"value": [
"text/html",
"text/x-web-textile"
]
},
{
"name": "plone.always_show_selector",
"schema": {
"properties": {
"default": false,
"description": "",
"title": "Always show language selector",
"type": "boolean"
}
},
"value": false
},
{
"name": "plone.app.discussion.interfaces.IDiscussionSettings.anonymous_comments",
"schema": {
"properties": {
"default": false,
"description": "If selected, anonymous users are able to post comments without logging in. It is highly recommended to use a captcha solution to prevent spam if this setting is enabled.",
"title": "Enable anonymous comments",
"type": "boolean"
}
},
"value": false
},
{
"name": "plone.app.discussion.interfaces.IDiscussionSettings.anonymous_email_enabled",
"schema": {
"properties": {
"default": false,
"description": "If selected, anonymous user will have to give their email.",
"title": "Enable anonymous email field",
"type": "boolean"
}
},
"value": false
},
{
"name": "plone.app.discussion.interfaces.IDiscussionSettings.captcha",
"schema": {
"properties": {
"default": "disabled",
"description": "Use this setting to enable or disable Captcha validation for comments. Install plone.formwidget.captcha, plone.formwidget.recaptcha, collective.akismet, or collective.z3cform.norobots if there are no options available.",
"title": "Captcha",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.discussion.vocabularies.CaptchaVocabulary"
}
}
},
"value": "disabled"
},
{
"name": "plone.app.discussion.interfaces.IDiscussionSettings.delete_own_comment_enabled",
"schema": {
"properties": {
"default": false,
"description": "If selected, supports deleting of own comments for users with the \"Delete own comments\" permission.",
"title": "Enable deleting own comments",
"type": "boolean"
}
},
"value": false
}
],
"items_total": 1683
}
Updating registry records¶
Updating an existing record:
http
PATCH /plone/@registry/ HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"plone.app.querystring.field.path.title": "Value"
}
curl
curl -i -X PATCH http://nohost/plone/@registry/ -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"plone.app.querystring.field.path.title": "Value"}' --user admin:secret
httpie
echo '{
"plone.app.querystring.field.path.title": "Value"
}' | http PATCH http://nohost/plone/@registry/ Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@registry/', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'plone.app.querystring.field.path.title': 'Value',
}, auth=('admin', 'secret'))
Example Response:
HTTP/1.1 204 No Content
Types¶
Note
These docs are generated by code tests, therefore you will see some ‘test’ contenttypes appear here.
Available content types in a Plone site can be listed and queried by accessing the /@types
endpoint on any context (requires an authenticated user). The ‘addable’ key specifies if the content type can be added to the current context. The ‘layouts’ key specifies the defined views.
http
GET /plone/@types HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@types -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@types Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@types', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@types/Collection",
"addable": true,
"title": "Collection"
},
{
"@id": "http://localhost:55001/plone/@types/DXTestDocument",
"addable": true,
"title": "DX Test Document"
},
{
"@id": "http://localhost:55001/plone/@types/Event",
"addable": true,
"title": "Event"
},
{
"@id": "http://localhost:55001/plone/@types/File",
"addable": true,
"title": "File"
},
{
"@id": "http://localhost:55001/plone/@types/Folder",
"addable": true,
"title": "Folder"
},
{
"@id": "http://localhost:55001/plone/@types/Image",
"addable": true,
"title": "Image"
},
{
"@id": "http://localhost:55001/plone/@types/Link",
"addable": true,
"title": "Link"
},
{
"@id": "http://localhost:55001/plone/@types/News Item",
"addable": true,
"title": "News Item"
},
{
"@id": "http://localhost:55001/plone/@types/Document",
"addable": true,
"title": "Page"
}
]
To get the schema of a content type, access the /@types
endpoint with the name of the content type, e.g. ‘/plone/@types/Document’:
http
GET /plone/@types/Document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@types/Document -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@types/Document Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@types/Document', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json+schema
{
"fieldsets": [
{
"fields": [
"title",
"description",
"text",
"changeNote"
],
"id": "default",
"title": "Default"
},
{
"fields": [
"allow_discussion",
"exclude_from_nav",
"id",
"versioning_enabled",
"table_of_contents"
],
"id": "settings",
"title": "Settings"
},
{
"fields": [
"subjects",
"language",
"relatedItems"
],
"id": "categorization",
"title": "Categorization"
},
{
"fields": [
"effective",
"expires"
],
"id": "dates",
"title": "Dates"
},
{
"fields": [
"creators",
"contributors",
"rights"
],
"id": "ownership",
"title": "Ownership"
}
],
"layouts": [
"document_view"
],
"properties": {
"allow_discussion": {
"choices": [
[
"True",
"Yes"
],
[
"False",
"No"
]
],
"description": "Allow discussion for this content object.",
"enum": [
"True",
"False"
],
"enumNames": [
"Yes",
"No"
],
"title": "Allow discussion",
"type": "string"
},
"changeNote": {
"description": "Enter a comment that describes the changes you made.",
"title": "Change Note",
"type": "string"
},
"contributors": {
"additionalItems": true,
"description": "The names of people that have contributed to this item. Each contributor should be on a separate line.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Contributors",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
}
}
},
"creators": {
"additionalItems": true,
"description": "Persons responsible for creating the content of this item. Please enter a list of user names, one per line. The principal creator should come first.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Creators",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
}
}
},
"description": {
"description": "Used in item listings and search results.",
"minLength": 0,
"title": "Summary",
"type": "string",
"widget": "textarea"
},
"effective": {
"description": "If this date is in the future, the content will not show up in listings and searches until this date.",
"title": "Publishing Date",
"type": "string",
"widget": "datetime"
},
"exclude_from_nav": {
"default": false,
"description": "If selected, this item will not appear in the navigation tree",
"title": "Exclude from navigation",
"type": "boolean"
},
"expires": {
"description": "When this date is reached, the content will no longer be visible in listings and searches.",
"title": "Expiration Date",
"type": "string",
"widget": "datetime"
},
"id": {
"description": "This name will be displayed in the URL.",
"title": "Short name",
"type": "string"
},
"language": {
"default": "en",
"description": "",
"title": "Language",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
}
},
"relatedItems": {
"additionalItems": true,
"default": [],
"description": "",
"items": {
"description": "",
"title": "Related",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
}
},
"title": "Related Items",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"pattern_options": {
"recentlyUsed": true
},
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
}
}
},
"rights": {
"description": "Copyright statement or other rights information on this item.",
"minLength": 0,
"title": "Rights",
"type": "string",
"widget": "textarea"
},
"subjects": {
"additionalItems": true,
"description": "Tags are commonly used for ad-hoc organization of content.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Tags",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords"
}
}
},
"table_of_contents": {
"description": "If selected, this will show a table of contents at the top of the page.",
"title": "Table of contents",
"type": "boolean"
},
"text": {
"description": "",
"title": "Text",
"type": "string",
"widget": "richtext"
},
"title": {
"description": "",
"title": "Title",
"type": "string"
},
"versioning_enabled": {
"default": true,
"description": "Enable/disable versioning for this document.",
"title": "Versioning enabled",
"type": "boolean"
}
},
"required": [
"title"
],
"title": "Page",
"type": "object"
}
The content type schema uses the JSON Schema format. The tagged values for the widgets are also exposed in the the “properties” attribute of the schema. If a ‘vocabulary’ is defined, it will be the name of the vocabulary which should be used via the @vocabularies endpoint on the actual resource.
See Types Schema for a detailed documentation about the available field types.
Types Schema¶
A detailed list of all available Zope Schema field types and their corresponding representation as JSON Schema .
TextLine¶
Zope Schema:
zope.schema.TextLine(
title=u'My field',
description=u'My great field',
default=u'foobar'
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'default': u'foobar',
}
Text¶
Zope Schema:
zope.schema.Text(
title=u'My field',
description=u'My great field',
default=u'Lorem ipsum dolor sit amet',
min_length=10,
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'widget': 'textarea',
'default': u'Lorem ipsum dolor sit amet',
'minLength': 10,
}
Bool¶
Zope Schema:
zope.schema.Bool(
title=u'My field',
description=u'My great field',
default=False,
)
JSON Schema:
{
'type': 'boolean',
'title': u'My field',
'description': u'My great field',
'default': False,
}
Float¶
Zope Schema:
zope.schema.Float(
title=u'My field',
description=u'My great field',
min=0.0,
max=1.0,
default=0.5,
)
JSON Schema:
{
'minimum': 0.0,
'maximum': 1.0,
'type': 'number',
'title': u'My field',
'description': u'My great field',
'default': 0.5,
}
Decimal¶
Zope Schema:
zope.schema.Decimal(
title=u'My field',
description=u'My great field',
min=Decimal(0),
max=Decimal(1),
default=Decimal(0.5),
)
JSON Schema:
{
'minimum': 0.0,
'maximum': 1.0,
'type': 'number',
'title': u'My field',
'description': u'My great field',
'default': 0.5,
},
Int¶
Zope Schema:
zope.schema.Int(
title=u'My field',
description=u'My great field',
min=0,
max=100,
default=50,
)
JSON Schema:
{
'minimum': 0,
'maximum': 100,
'type': 'integer',
'title': u'My field',
'description': u'My great field',
'default': 50,
}
Choice¶
Zope Schema:
zope.schema.Choice(
title=u'My field',
description=u'My great field',
vocabulary=self.dummy_vocabulary,
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'enum': ['foo', 'bar'],
'enumNames': ['Foo', 'Bar'],
'choices': [('foo', 'Foo'), ('bar', 'Bar')],
}
List¶
Zope Schema:
zope.schema.List(
title=u'My field',
description=u'My great field',
min_length=1,
value_type=schema.TextLine(
title=u'Text',
description=u'Text field',
default=u'Default text'
),
default=['foobar'],
)
JSON Schema:
{
'type': 'array',
'title': u'My field',
'description': u'My great field',
'default': ['foobar'],
'minItems': 1,
'uniqueItems': False,
'additionalItems': True,
'items': {
'type': 'string',
'title': u'Text',
'description': u'Text field',
'default': u'Default text',
}
},
Tuple¶
Zope Schema:
field = zope.schema.Tuple(
title=u'My field',
value_type=schema.Int(),
default=(1, 2),
)
JSON Schema:
{
'type': 'array',
'title': u'My field',
'description': u'',
'uniqueItems': True,
'additionalItems': True,
'items': {
'title': u'',
'description': u'',
'type': 'integer',
},
'default': (1, 2),
}
Set¶
Zope Schema:
field = zope.schema.Set(
title=u'My field',
value_type=schema.TextLine(),
)
JSON Schema:
{
'type': 'array',
'title': u'My field',
'description': u'',
'uniqueItems': True,
'additionalItems': True,
'items': {
'title': u'',
'description': u'',
'type': 'string',
}
}
List of Choices¶
Zope Schema:
field = zope.schema.List(
title=u'My field',
value_type=schema.Choice(
vocabulary=self.dummy_vocabulary,
),
)
JSON Schema:
{
'type': 'array',
'title': u'My field',
'description': u'',
'uniqueItems': True,
'additionalItems': True,
'items': {
'title': u'',
'description': u'',
'type': 'string',
'enum': ['foo', 'bar'],
'enumNames': ['Foo', 'Bar'],
'choices': [('foo', 'Foo'), ('bar', 'Bar')],
}
}
Object¶
Zope Schema:
zope.schema.Object(
title=u'My field',
description=u'My great field',
schema=IDummySchema,
)
JSON Schema:
{
'type': 'object',
'title': u'My field',
'description': u'My great field',
'properties': {
'field1': {
'title': u'Foo',
'description': u'',
'type': 'boolean'
},
'field2': {
'title': u'Bar',
'description': u'',
'type': 'string'
},
}
},
RichText (plone.app.textfield)¶
Zope Schema:
from plone.app.textfield import RichText
field = RichText(
title=u'My field',
description=u'My great field',
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'widget': 'richtext',
}
Date¶
Zope Schema:
zope.schema.Date(
title=u'My field',
description=u'My great field',
default=date(2016, 1, 1),
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'default': date(2016, 1, 1),
'widget': u'date',
}
DateTime¶
Zope Schema:
zope.schema.Datetime(
title=u'My field',
description=u'My great field',
)
JSON Schema:
{
'type': 'string',
'title': u'My field',
'description': u'My great field',
'widget': u'datetime',
}
Users¶
Available users in a Plone site can be created, queried, updated and deleted by interacting with the /@users
endpoint on portal root (requires an authenticated user):
List Users¶
To retrieve a list of all current users in the portal, call the /@users
endpoint with a GET
request:
http
GET /plone/@users HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@users -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@users Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@users', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list of all users in the portal:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@users/admin",
"description": "This is an admin user",
"email": "admin@example.com",
"fullname": "Administrator",
"home_page": "http://www.example.com",
"id": "admin",
"location": "Berlin",
"portrait": null,
"roles": [
"Manager"
],
"username": "admin"
},
{
"@id": "http://localhost:55001/plone/@users/test_user_1_",
"description": "This is a test user",
"email": "test@example.com",
"fullname": "Test User",
"home_page": "http://www.example.com",
"id": "test_user_1_",
"location": "Bonn",
"portrait": null,
"roles": [
"Manager"
],
"username": "test-user"
}
]
This only works for Manager users, anonymous users or logged-in users without Manager rights are now allowed to list users. This is the example as an anonymous user:
http
GET /plone/@users HTTP/1.1
Accept: application/json
curl
curl -i http://nohost/plone/@users -H 'Accept: application/json'
httpie
http http://nohost/plone/@users Accept:application/json
python-requests
requests.get('http://nohost/plone/@users', headers={
'Accept': 'application/json',
})
The server will return a 401 Unauthorized status code
HTTP/1.1 401 Unauthorized
Content-Type: application/json
null
And this one as an user without the proper rights:
http
GET /plone/@users HTTP/1.1
Accept: application/json
Authorization: Basic bm9hbTpwYXNzd29yZA==
curl
curl -i http://nohost/plone/@users -H 'Accept: application/json' --user noam:password
httpie
http http://nohost/plone/@users Accept:application/json -a noam:password
python-requests
requests.get('http://nohost/plone/@users', headers={
'Accept': 'application/json',
}, auth=('noam', 'password'))
The server will return a 401 Unauthorized status code
HTTP/1.1 401 Unauthorized
Content-Type: application/json
null
The endpoint supports some basic filtering:
http
GET /plone/@users?query=noa HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@users?query=noa' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@users?query=noa' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@users?query=noa', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list the filtered users in the portal with username starts with the query.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@users/noam",
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"id": "noam",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Member"
],
"username": "noam"
}
]
The endpoint also takes a limit
parameter that defaults to a maximum of 25 users at a time for performance reasons.
Create User¶
To create a new user, send a POST
request to the global /@users
endpoint with a JSON representation of the user you want to create in the body:
http
POST /plone/@users HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"location": "Cambridge, MA",
"password": "colorlessgreenideas",
"roles": [
"Contributor"
],
"username": "noamchomsky"
}
curl
curl -i -X POST http://nohost/plone/@users -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"description": "Professor of Linguistics", "email": "noam.chomsky@example.com", "fullname": "Noam Avram Chomsky", "home_page": "web.mit.edu/chomsky", "location": "Cambridge, MA", "password": "colorlessgreenideas", "roles": ["Contributor"], "username": "noamchomsky"}' --user admin:secret
httpie
echo '{
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"location": "Cambridge, MA",
"password": "colorlessgreenideas",
"roles": [
"Contributor"
],
"username": "noamchomsky"
}' | http POST http://nohost/plone/@users Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@users', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'description': 'Professor of Linguistics',
'email': 'noam.chomsky@example.com',
'fullname': 'Noam Avram Chomsky',
'home_page': 'web.mit.edu/chomsky',
'location': 'Cambridge, MA',
'password': 'colorlessgreenideas',
'roles': ['Contributor'],
'username': 'noamchomsky',
}, auth=('admin', 'secret'))
Note
By default, “username”, and “password” are required fields. If email login is enabled, “email” and “password” are required fields. All other fields in the example are optional.
The field “username” is not allowed when email login is enabled.
If the user has been created successfully, the server will respond with a status 201 (Created). The Location
header contains the URL of the newly created user and the resource representation in the payload:
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/@users/noamchomsky
{
"@id": "http://localhost:55001/plone/@users/noamchomsky",
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"id": "noamchomsky",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Contributor"
],
"username": "noamchomsky"
}
If no roles has been specified, then a default Member
role is added as a sensible default.
Read User¶
To retrieve all details for a particular user, send a GET
request to the /@users
endpoint and append the user id to the URL:
http
GET /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@users/noam -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@users/noam Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a 200 OK status code and the JSON representation of the user in the body:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@users/noam",
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"id": "noam",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Member"
],
"username": "noam"
}
The key ‘roles’ lists the globally defined roles for the user.
Only users with Manager rights are allowed to get other users’ information:
http
GET /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic bm9hbS1mYWtlOnNlY3JldA==
curl
curl -i http://nohost/plone/@users/noam -H 'Accept: application/json' --user noam-fake:secret
httpie
http http://nohost/plone/@users/noam Accept:application/json -a noam-fake:secret
python-requests
requests.get('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
}, auth=('noam-fake', 'secret'))
If the user lacks this rights, the server will respond with a 401 Unauthorized status code:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
null
Also anonymous users are not allowed to get users’ information:
http
GET /plone/@users/noam HTTP/1.1
Accept: application/json
curl
curl -i http://nohost/plone/@users/noam -H 'Accept: application/json'
httpie
http http://nohost/plone/@users/noam Accept:application/json
python-requests
requests.get('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
})
If the user is an anonymous one, the server will respond with a 401 Unauthorized status code:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
null
But each user is allowed to get its own information.
http
GET /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic bm9hbTpzZWNyZXQ=
curl
curl -i http://nohost/plone/@users/noam -H 'Accept: application/json' --user noam:secret
httpie
http http://nohost/plone/@users/noam Accept:application/json -a noam:secret
python-requests
requests.get('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
}, auth=('noam', 'secret'))
In this case, the server will respond with a 200 OK status code and the JSON respresentation of the user in the body:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@users/noam",
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"id": "noam",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Member"
],
"username": "noam"
}
Update User¶
To update the settings of a user, send a PATCH
request with the user details you want to amend to the URL of that particular user, e.g. if you want to update the email address of the admin user to:
http
PATCH /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"email": "avram.chomsky@example.com",
"roles": {
"Contributor": false
}
}
curl
curl -i -X PATCH http://nohost/plone/@users/noam -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"email": "avram.chomsky@example.com", "roles": {"Contributor": false}}' --user admin:secret
httpie
echo '{
"email": "avram.chomsky@example.com",
"roles": {
"Contributor": false
}
}' | http PATCH http://nohost/plone/@users/noam Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'email': 'avram.chomsky@example.com',
'roles': {
'Contributor': False,
},
}, auth=('admin', 'secret'))
A successful response to a PATCH request will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
Note
The ‘roles’ object is a mapping of a role and a boolean indicating adding or removing.
Any user is able to update their own properties and password (if allowed) by using the same request.
The user portrait/avatar can also be updated using the same serialization as the file one:
http
PATCH /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"portrait": {
"content-type": "image/png",
"data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=",
"encoding": "base64",
"filename": "image.png"
}
}
curl
curl -i -X PATCH http://nohost/plone/@users/noam -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"portrait": {"content-type": "image/png", "data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", "encoding": "base64", "filename": "image.png"}}' --user admin:secret
httpie
echo '{
"portrait": {
"content-type": "image/png",
"data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=",
"encoding": "base64",
"filename": "image.png"
}
}' | http PATCH http://nohost/plone/@users/noam Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'portrait': {
'content-type': 'image/png',
'data': 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
'encoding': 'base64',
'filename': 'image.png',
},
}, auth=('admin', 'secret'))
A successful response to a PATCH request will be indicated by a 204 No Content response. Then asking for the user the portrait URL should be on the response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@users/noam",
"description": null,
"email": "noam.chomsky@example.com",
"fullname": null,
"home_page": null,
"id": "noam",
"location": null,
"portrait": "http://localhost:55001/plone/portal_memberdata/portraits/noam",
"roles": [
"Member"
],
"username": "noam"
}
Adding the portrait via the @user endpoint does not scale it since it’s assumed that the frontend will take care of it (resizing/cropping).
If you still want that Plone to take care of the scaling using the default Plone behavior for portraits, you have to add the scale
attribute to the request:
http
PATCH /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"portrait": {
"content-type": "image/png",
"data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=",
"encoding": "base64",
"filename": "image.png",
"scale": true
}
}
curl
curl -i -X PATCH http://nohost/plone/@users/noam -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"portrait": {"content-type": "image/png", "data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", "encoding": "base64", "filename": "image.png", "scale": true}}' --user admin:secret
httpie
echo '{
"portrait": {
"content-type": "image/png",
"data": "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=",
"encoding": "base64",
"filename": "image.png",
"scale": true
}
}' | http PATCH http://nohost/plone/@users/noam Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'portrait': {
'content-type': 'image/png',
'data': 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=',
'encoding': 'base64',
'filename': 'image.png',
'scale': True,
},
}, auth=('admin', 'secret'))
Delete User¶
To delete a user send a DELETE
request to the /@users
endpoint and append the user id of the user you want to delete, e.g. to delete the user with the id johndoe:
http
DELETE /plone/@users/noam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X DELETE http://nohost/plone/@users/noam -H 'Accept: application/json' --user admin:secret
httpie
http DELETE http://nohost/plone/@users/noam Accept:application/json -a admin:secret
python-requests
requests.delete('http://nohost/plone/@users/noam', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
A successful response will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
User registration¶
Plone allows you to enable the auto registration of users.
If it is enabled, then an anonymous user can register a new user using the user creation endpoint.
This new user will have the role Member
by default as the Plone registration process also does.
To create a new user send a POST request to the ‘@users’ endpoint:
http
POST /plone/@users HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"location": "Cambridge, MA",
"sendPasswordReset": true,
"username": "noamchomsky"
}
curl
curl -i -X POST http://nohost/plone/@users -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"description": "Professor of Linguistics", "email": "noam.chomsky@example.com", "fullname": "Noam Avram Chomsky", "home_page": "web.mit.edu/chomsky", "location": "Cambridge, MA", "sendPasswordReset": true, "username": "noamchomsky"}' --user admin:secret
httpie
echo '{
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"location": "Cambridge, MA",
"sendPasswordReset": true,
"username": "noamchomsky"
}' | http POST http://nohost/plone/@users Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@users', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'description': 'Professor of Linguistics',
'email': 'noam.chomsky@example.com',
'fullname': 'Noam Avram Chomsky',
'home_page': 'web.mit.edu/chomsky',
'location': 'Cambridge, MA',
'sendPasswordReset': True,
'username': 'noamchomsky',
}, auth=('admin', 'secret'))
If the user should receive an email to set her password, you should pass ‘sendPasswordReset”: true’ in the JSON body of the request. Keep in mind that Plone will send a URL that points to the URL of the Plone site, which might just be your API endpoint.
If the user has been created, the server will respond with a 201 Created response:
HTTP/1.1 201 Created
Location: http://localhost:55001/plone/@users/noamchomsky
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@users/noamchomsky",
"description": "Professor of Linguistics",
"email": "noam.chomsky@example.com",
"fullname": "Noam Avram Chomsky",
"home_page": "web.mit.edu/chomsky",
"id": "noamchomsky",
"location": "Cambridge, MA",
"portrait": null,
"roles": [
"Member"
],
"username": "noamchomsky"
}
Reset User Password¶
Plone allows to reset a password for a user by sending a POST request to the user resource and appending /reset-password to the URL:
POST /plone/@users/noam/reset-password HTTP/1.1
Host: localhost:8080
Accept: application/json
The server will respond with a 200 OK response and send an email to the user to reset her password.
The token that is part of the reset url in the email can be used to authorize setting a new password:
http
POST /plone/@users/noam/rest-password HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{"reset_token": "ef3d2aabacdc2345df63d6acf2edbef4", "new_password": "verysecret"}
curl
curl -i -X POST http://nohost/plone/@users/noam/rest-password -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"new_password": "verysecret", "reset_token": "ef3d2aabacdc2345df63d6acf2edbef4"}' --user admin:secret
httpie
echo '{
"new_password": "verysecret",
"reset_token": "ef3d2aabacdc2345df63d6acf2edbef4"
}' | http POST http://nohost/plone/@users/noam/rest-password Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@users/noam/rest-password', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'new_password': 'verysecret',
'reset_token': 'ef3d2aabacdc2345df63d6acf2edbef4',
}, auth=('admin', 'secret'))
Reset Own Password¶
Plone also allows a user to reset her own password directly without sending an email. The endpoint and the request is the same as above, but now the user can send the old password and the new password as payload:
POST /plone/@users/noam/reset-password HTTP/1.1
Host: localhost:8080
Accept: application/json
Content-Type: application/json
{
'old_password': 'secret',
'new_password': 'verysecret',
}
The server will respond with a 200 OK response without sending an email.
To set the password with the old password you need either the Set own password
or the plone.app.controlpanel.UsersAndGroups
permission.
If an API consumer tries to send a password in the payload that is not the same as the currently logged in user, the server will respond with a 400 Bad Request response.
Return Values¶
- 200 OK
- 400 Bad Request
- 403 (Unknown Token)
- 403 (Expired Token)
- 403 (Wrong user)
- 403 (Not allowed)
- 403 (Wrong password)
- 500 Internal Server Error (server fault, can not recover internally)
Groups¶
Available groups in a Plone site can be created, queried, updated and deleted by interacting with the /@groups
endpoint on portal root (requires an authenticated user):
List Groups¶
To retrieve a list of all current groups in the portal, call the /@groups
endpoint with a GET
request:
http
GET /plone/@groups HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@groups -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@groups Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@groups', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list of all groups in the portal:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@groups/Administrators",
"description": "",
"email": "",
"groupname": "Administrators",
"id": "Administrators",
"roles": [
"Manager",
"Authenticated"
],
"title": "Administrators"
},
{
"@id": "http://localhost:55001/plone/@groups/Reviewers",
"description": "",
"email": "",
"groupname": "Reviewers",
"id": "Reviewers",
"roles": [
"Reviewer",
"Authenticated"
],
"title": "Reviewers"
},
{
"@id": "http://localhost:55001/plone/@groups/Site Administrators",
"description": "",
"email": "",
"groupname": "Site Administrators",
"id": "Site Administrators",
"roles": [
"Site Administrator",
"Authenticated"
],
"title": "Site Administrators"
},
{
"@id": "http://localhost:55001/plone/@groups/ploneteam",
"description": "We are Plone",
"email": "ploneteam@plone.org",
"groupname": "ploneteam",
"id": "ploneteam",
"roles": [
"Authenticated"
],
"title": "Plone Team"
},
{
"@id": "http://localhost:55001/plone/@groups/AuthenticatedUsers",
"description": "Automatic Group Provider",
"email": "",
"groupname": "AuthenticatedUsers",
"id": "AuthenticatedUsers",
"roles": [],
"title": "Authenticated Users (Virtual Group)"
}
]
The endpoint supports some basic filtering:
http
GET /plone/@groups?query=plo HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@groups?query=plo' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@groups?query=plo' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@groups?query=plo', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list the filtered groups in the portal with groupname starts with the query.
The endpoint also takes a limit
parameter that defaults to a maximum of 25 groups at a time for performance reasons.
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@groups/ploneteam",
"description": "We are Plone",
"email": "ploneteam@plone.org",
"groupname": "ploneteam",
"id": "ploneteam",
"roles": [
"Authenticated"
],
"title": "Plone Team"
}
]
Create Group¶
To create a new group, send a POST
request to the global /@groups
endpoint with a JSON representation of the group you want to create in the body:
http
POST /plone/@groups HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"description": "The Plone Framework Team",
"email": "fwt@plone.org",
"groupname": "fwt",
"groups": [
"Administrators"
],
"roles": [
"Manager"
],
"title": "Framework Team",
"users": [
"admin",
"test_user_1_"
]
}
curl
curl -i -X POST http://nohost/plone/@groups -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"description": "The Plone Framework Team", "email": "fwt@plone.org", "groupname": "fwt", "groups": ["Administrators"], "roles": ["Manager"], "title": "Framework Team", "users": ["admin", "test_user_1_"]}' --user admin:secret
httpie
echo '{
"description": "The Plone Framework Team",
"email": "fwt@plone.org",
"groupname": "fwt",
"groups": [
"Administrators"
],
"roles": [
"Manager"
],
"title": "Framework Team",
"users": [
"admin",
"test_user_1_"
]
}' | http POST http://nohost/plone/@groups Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/@groups', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'description': 'The Plone Framework Team',
'email': 'fwt@plone.org',
'groupname': 'fwt',
'groups': ['Administrators'],
'roles': ['Manager'],
'title': 'Framework Team',
'users': ['admin', 'test_user_1_'],
}, auth=('admin', 'secret'))
Note
By default, “groupname” is a required field.
If the group has been created successfully, the server will respond with a status 201 (Created)
. The Location
header contains the URL of the newly created group and the resource representation in the payload:
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/@groups/fwt
{
"@id": "http://localhost:55001/plone/@groups/fwt",
"description": "The Plone Framework Team",
"email": "fwt@plone.org",
"groupname": "fwt",
"id": "fwt",
"roles": [
"Manager",
"Authenticated"
],
"title": "Framework Team",
"users": {
"@id": "http://localhost:55001/plone/@groups",
"items": [
"Administrators",
"admin",
"test_user_1_"
],
"items_total": 3
}
}
Read Group¶
To retrieve all details for a particular group, send a GET
request to the /@groups
endpoint and append the group id to the URL:
http
GET /plone/@groups/ploneteam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@groups/ploneteam -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@groups/ploneteam Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@groups/ploneteam', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a 200 OK
status code and the JSON representation of the group in the body:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@groups/ploneteam",
"description": "We are Plone",
"email": "ploneteam@plone.org",
"groupname": "ploneteam",
"id": "ploneteam",
"roles": [
"Authenticated"
],
"title": "Plone Team",
"users": {
"@id": "http://localhost:55001/plone/@groups/ploneteam",
"items": [],
"items_total": 0
}
}
Batching is supported for the ‘users’ object.
Update Group¶
To update the settings of a group, send a PATCH
request with the group details you want to amend to the URL of that particular group:
http
PATCH /plone/@groups/ploneteam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"email": "ploneteam2@plone.org",
"users": {
"test_user_1_": false
}
}
curl
curl -i -X PATCH http://nohost/plone/@groups/ploneteam -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"email": "ploneteam2@plone.org", "users": {"test_user_1_": false}}' --user admin:secret
httpie
echo '{
"email": "ploneteam2@plone.org",
"users": {
"test_user_1_": false
}
}' | http PATCH http://nohost/plone/@groups/ploneteam Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@groups/ploneteam', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'email': 'ploneteam2@plone.org',
'users': {
'test_user_1_': False,
},
}, auth=('admin', 'secret'))
Note
The ‘users’ object is a mapping of a user_id and a boolean indicating adding or removing from the group.
A successful response to a PATCH request will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
Delete Group¶
To delete a group send a DELETE
request to the /@groups
endpoint and append the group id of the group you want to delete:
http
DELETE /plone/@groups/ploneteam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X DELETE http://nohost/plone/@groups/ploneteam -H 'Accept: application/json' --user admin:secret
httpie
http DELETE http://nohost/plone/@groups/ploneteam Accept:application/json -a admin:secret
python-requests
requests.delete('http://nohost/plone/@groups/ploneteam', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
A successful response will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
Principals¶
This endpoint will search for all the available principals in the local PAS plugins given a query string. We call a principal to any user or group in the system (requires an authenticated user):
Search Principals¶
To retrieve a list of principals given a search string, call the /@principals
endpoint with a GET request and a search
query parameter:
http
GET /plone/@principals?search=ploneteam HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@principals?search=ploneteam' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@principals?search=ploneteam' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@principals?search=ploneteam', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list of the users and groups in the portal that match the query string:
HTTP/1.1 200 OK
Content-Type: application/json
{
"groups": [
{
"@id": "http://localhost:55001/plone/@groups/ploneteam",
"description": "We are Plone",
"email": "ploneteam@plone.org",
"groupname": "ploneteam",
"id": "ploneteam",
"roles": [
"Authenticated"
],
"title": "Plone Team"
}
],
"users": []
}
Roles¶
Available roles in a Plone site can be queried by interacting with the /@roles
endpoint on portal root (requires an authenticated user):
List Roles¶
To retrieve a list of all roles in the portal, call the /@roles
endpoint with a GET
request:
http
GET /plone/@roles HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@roles -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@roles Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@roles', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list of all roles in the portal:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@roles/Contributor",
"@type": "role",
"id": "Contributor",
"title": "Contributor"
},
{
"@id": "http://localhost:55001/plone/@roles/Editor",
"@type": "role",
"id": "Editor",
"title": "Editor"
},
{
"@id": "http://localhost:55001/plone/@roles/Member",
"@type": "role",
"id": "Member",
"title": "Member"
},
{
"@id": "http://localhost:55001/plone/@roles/Reader",
"@type": "role",
"id": "Reader",
"title": "Reader"
},
{
"@id": "http://localhost:55001/plone/@roles/Reviewer",
"@type": "role",
"id": "Reviewer",
"title": "Reviewer"
},
{
"@id": "http://localhost:55001/plone/@roles/Site Administrator",
"@type": "role",
"id": "Site Administrator",
"title": "Site Administrator"
},
{
"@id": "http://localhost:55001/plone/@roles/Manager",
"@type": "role",
"id": "Manager",
"title": "Manager"
}
]
The role title
is the translated role title as displayed in Plone’s
“Users and Groups” control panel.
Components¶
Warning
The @components endpoint is deprecated and has been removed in plone.restapi 1.0b1. Breadcrumbs and Navigation are now top-level endpoints.
How to get pages components (i.e. everything but the main content), like breadcrumbs, navigations, actions, etc.
Breadcrumbs¶
Getting the breadcrumbs for the current page:
http
GET /plone/front-page/@breadcrumbs HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@breadcrumbs -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/front-page/@breadcrumbs Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@breadcrumbs', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Example response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
}
Serialization¶
Throughout the REST API, content needs to be serialized and deserialized to and from JSON representations.
In general, the format used for serializing content when reading from the API is the same as is used to submit content to the API for writing.
Basic Types¶
Basic Python data types that have a corresponding type in JSON, like integers or strings, will simply be translated between the Python type and the respective JSON type.
Dates and Times¶
Since JSON doesn’t have native support for dates/times, the Python/Zope datetime types will be serialized to an ISO 8601 datestring.
Python | JSON |
---|---|
time(19, 45, 55) |
'19:45:55' |
date(2015, 11, 23) |
'2015-11-23' |
datetime(2015, 11, 23, 19, 45, 55) |
'2015-11-23T19:45:55' |
DateTime('2015/11/23 19:45:55') |
'2015-11-23T19:45:55' |
RichText fields¶
RichText fields will be serialized as follows:
A RichTextValue
like
RichTextValue(u'<p>Hallöchen</p>',
mimeType='text/html',
outputMimeType='text/html')
will be serialized to
{
"data": "<p>Hall\u00f6chen</p>",
"content-type": "text/html",
"encoding": "utf-8"
}
File / Image Fields¶
Download (serialization)¶
For download, a file field will be serialized to a mapping that contains the file’s most basic metadata, and a hyperlink that the client can follow to download the file:
{
"...": "",
"@type": "File",
"title": "My file",
"file": {
"content-type": "application/pdf",
"download": "http://localhost:55001/plone/file/@@download/file",
"filename": "file.pdf",
"size": 74429
}
}
That URL in the download
property points to the regular Plone download
view. The client can send a GET
request to that URL with an Accept
header containing the MIME type indicated in the content-type
property,
and will get a response containing the file.
Image fields are serialized in the same way, except that their serialization
contains their width
and height
, and an additional property
scales
that contains a mapping with the available image scales.
Image URLs are created using the UID-based URL that changes each time the image
is modified, so these URLs can be properly cached:
{
"icon": {
"download": "http://localhost:55001/plone/image/@@images/8eed3f80-5e1f-4115-85b8-650a10a6ca84.png",
"height": 32,
"width": 24
},
"large": {
"download": "http://localhost:55001/plone/image/@@images/0d1824d1-2672-4b62-9277-aeb220d3bf15.png",
"height": 768,
"width": 576
},
"..." : {}
}
Upload (deserialization)¶
For file or image fields, the client must provide the file’s data as a mapping containg the file data and some additional metadata:
data
- the base64 encoded contents of the fileencoding
- the encoding you used to encode the data, so usually base64content-type
- the MIME type of the filefilename
- the name of the file, including extension
{
"...": "",
"@type": "File",
"title": "My file",
"file": {
"data": "TG9yZW0gSXBzdW0uCg==",
"encoding": "base64",
"filename": "lorem.txt",
"content-type": "text/plain"}
}
Relations¶
Serialization¶
A RelationValue
will be serialized to a short summary representation of the referenced object:
{
"@id": "http://nohost/plone/doc1",
"@type": "DXTestDocument",
"title": "Document 1",
"description": "Description"
}
The RelationList
containing that reference will be represended as a list in JSON.
Deserialization¶
In order to set a relation when creating or updating content, you can use one of several ways to specify relations:
- UID
- path
- URL
- intid
Specify relations by UID:
{
"relatedItems": [
"158e5361282647e39bf0698fe238814b",
"5597250bda4b41eab6ed37cd25fb0979"
]
}
Specify relations by path:
{
"relatedItems": ["/page1", "/page2"]
}
Specify relations by URL:
{
"relatedItems": [
"http://localhost:8080/Plone/page1",
"http://localhost:8080/Plone/page2"
]
}
Specify relations by intid:
{
"relatedItems": [347127075, 347127076]
}
Search¶
Content in a Plone site can be searched for by invoking the /@search
endpoint on any context:
GET /plone/@search HTTP/1.1
Accept: application/json
A search is contextual by default, i.e. it is bound to a specific context (a collection in HTTP REST terms) and searches within that collection and any sub-collections.
Since a Plone site is also a collection, we therefore have a global search (by invoking the /@search
endpoint on the site root) and contextual searches (by invoking that endpoint on any other context) all using the same pattern.
In terms of the resulting catalog query this means that, by default, a search will be constrained by the path to the context it’s invoked on, unless you explicitly supply your own path
query.
Search results are represented similar to collections:
http
GET /plone/@search?sort_on=path HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@search?sort_on=path' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@search?sort_on=path' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@search?sort_on=path', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@search",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"@type": "Document",
"description": "Congratulations! You have successfully installed Plone.",
"review_state": "private",
"title": "Welcome to Plone"
}
],
"items_total": 1
}
The default representation for search results is a summary that contains only the most basic information.
In order to return specific metadata columns, see the documentation of the metadata_fields
parameter below.
Note
A search invoked on a container will by default include that container
itself as part of the search results. This is the same behavior as displayed by
ZCatalog, which is used internally.
If you add the query string
parameter depth=1
to your search, you will only get immediate
children of the container, and the container itself also won’t be part
of the results. See the Plone docs on
searching for content within a folder.
for more details.
Note
Search results results will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.
Warning
The @@search view or in Plone LiveSearch widget are coded in a way that the SearchableText parameter is expanded by including a * wildcard at the end. This is done in order to match also the partial results of the beginning of a search term(s). plone.restapi @search endpoint will not do that for you. You’ll have to add it if you want to keep this feature.
Query format¶
Queries and query-wide options (like sort_on
) are submitted as query string parameters to the /@search
request:
GET /plone/@search?SearchableText=lorem HTTP/1.1
This is nearly identical to the way that queries are passed to the Plone @@search
browser view, with only a few minor differences.
For general information on how to query the Plone catalog, please refer to the Plone Documentation on Querying.
Query options¶
In case you want to supply query options to a query against a particular index, you’ll need to flatten the corresponding query dictionary and use a dotted notation to indicate nesting.
For example, to specify the depth
query option for a path query, the original query as a Python dictionary would look like this:
query = {'path': {'query': '/folder',
'depth': 2}}
This dictionary will need to be flattened in dotted notation in order to pass it in a query string:
http
GET /plone/@search?path.query=%2Fplone%2Ffolder1&sort_on=path&path.depth=1 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&sort_on=path&path.depth=1' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&sort_on=path&path.depth=1' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&sort_on=path&path.depth=1', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@search?path.query=%2Fplone%2Ffolder1&path.depth=1",
"items": [
{
"@id": "http://localhost:55001/plone/folder1/folder2",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder 2"
}
],
"items_total": 1
}
Again, this is very similar to how Record Arguments are parsed by ZPublisher, except that you can omit the :record
suffix.
Restricting search to multiple paths¶
To restrict search to multiple paths, the original query as a Python dictionary would look like this (with an optional depth and sort_on):
query = {'path': {'query': ('/folder', '/folder2'),
'depth': 2},
'sort_on': 'path'}
This dictionary will need to be flattened in dotted notation in order to pass it in a query string. In order to specify multiple paths, simply repeat the query string parameter (the requests
module will do this automatically for you if you pass it a list of values for a query string parameter).
http
GET /plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&sort_on=path&path.depth=2 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&sort_on=path&path.depth=2' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&sort_on=path&path.depth=2' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&sort_on=path&path.depth=2', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@search?path.query=%2Fplone%2Ffolder1&path.query=%2Fplone%2Ffolder2&path.depth=2",
"items": [
{
"@id": "http://localhost:55001/plone/folder1",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder 1"
},
{
"@id": "http://localhost:55001/plone/folder1/doc1",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Lorem Ipsum"
},
{
"@id": "http://localhost:55001/plone/folder2",
"@type": "Folder",
"description": "",
"review_state": "private",
"title": "Folder 2"
},
{
"@id": "http://localhost:55001/plone/folder2/doc2",
"@type": "Document",
"description": "",
"review_state": "private",
"title": "Lorem Ipsum"
}
],
"items_total": 4
}
Data types in queries¶
Because HTTP query strings contain no information about data types, any query string parameter value ends up as a string in the Zope’s request. This means, that for values types that aren’t string, these data types need to be reconstructed on the server side in plone.restapi.
For most index types and their query values and query options, plone.restapi can handle this for you.
If you pass it path.query=foo&path.depth=1
, it has the necessary knowledge about the ExtendedPathIndex
’s options to turn the string 1
for the depth
argument back into an integer before passing the query on to the catalog.
However, certain index types (a FieldIndex
for example) may take arbitrary data types as query values.
In that case, plone.restapi
simply can’t know what data type to cast your query value to, and you’ll need to specify it using ZPublisher type hints:
GET /plone/@search?numeric_field=42:int HTTP/1.1
Accept: application/json
Please refer to the Documentation on Argument Conversion in ZPublisher for details.
Retrieving additional metadata¶
By default the results are represented as summaries that only contain the most basic information about the items, like their URL and title.
If you need to retrieve additional metadata columns, you can do so by specifying the additional column names in the metadata_fields
parameter:
http
GET /plone/@search?metadata_fields=modified&metadata_fields=created&SearchableText=lorem HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@search?metadata_fields=modified&metadata_fields=created&SearchableText=lorem' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@search?metadata_fields=modified&metadata_fields=created&SearchableText=lorem' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@search?metadata_fields=modified&metadata_fields=created&SearchableText=lorem', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@search?metadata_fields=modified&metadata_fields=created&SearchableText=lorem",
"items": [
{
"@id": "http://localhost:55001/plone/doc1",
"@type": "Document",
"created": "2016-10-21T19:00:00+00:00",
"description": "",
"modified": "2016-10-21T19:00:00+00:00",
"review_state": "private",
"title": "Lorem Ipsum"
}
],
"items_total": 1
}
The metadata from those columns then will be included in the results.
In order to specify multiple columns, simply repeat the query string parameter once for every column name (the requests
module will do this automatically for you if you pass it a list of values for a query string parameter).
In order to retrieve all metadata columns that the catalog knows about, use metadata_fields=_all
.
Retrieving full objects¶
If the data provided as metadata is not enough, you can retrieve search results as full serialized objects equivalent to what the resource GET request would produce.
You do so by specifying the fullobjects
parameter:
http
GET /plone/@search?fullobjects=1&SearchableText=lorem HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@search?fullobjects=1&SearchableText=lorem' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@search?fullobjects=1&SearchableText=lorem' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@search?fullobjects=1&SearchableText=lorem', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@search?fullobjects=1&SearchableText=lorem",
"items": [
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/doc1/@actions"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/doc1/@breadcrumbs"
},
"navigation": {
"@id": "http://localhost:55001/plone/doc1/@navigation"
},
"workflow": {
"@id": "http://localhost:55001/plone/doc1/@workflow"
}
},
"@id": "http://localhost:55001/plone/doc1",
"@type": "Document",
"UID": "SomeUUID000000000000000000000002",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "2016-10-21T19:00:00+00:00",
"creators": [
"test_user_1_"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "doc1",
"is_folderish": false,
"language": "",
"layout": "document_view",
"modified": "2016-10-21T19:00:00+00:00",
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site"
},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "Lorem Ipsum",
"version": "current",
"versioning_enabled": true
}
],
"items_total": 1
}
Warning
Be aware that this might induce performance issues when retrieving a lot of resources. Normally the search just serializes catalog brains, but with full objects we wake up all the returned objects.
TUS resumable upload¶
plone.restapi supports the TUS Open Protocol for resumable file uploads. There is a @tus-upload endpoint to upload a file and a @tus-replace endpoint to replace an existing file.
Creating an Upload URL¶
Note
POST requests to the @tus-upload endpoint are allowed on all IFolderish content types (e.g. Folder).
To create a new upload, send a POST request to the @tus-upload endpoint.
http
POST /plone/folder/@tus-upload HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
Upload-Length: 8
Upload-Metadata: filename dGVzdC50eHQ=,content-type dGV4dC9wbGFpbg==
curl
curl -i -X POST http://nohost/plone/folder/@tus-upload -H 'Accept: application/json' -H 'Tus-Resumable: 1.0.0' -H 'Upload-Length: 8' -H 'Upload-Metadata: filename dGVzdC50eHQ=,content-type dGV4dC9wbGFpbg==' --user admin:secret
httpie
http POST http://nohost/plone/folder/@tus-upload Accept:application/json Tus-Resumable:1.0.0 Upload-Length:8 Upload-Metadata:'filename dGVzdC50eHQ=,content-type dGV4dC9wbGFpbg==' -a admin:secret
python-requests
requests.post('http://nohost/plone/folder/@tus-upload', headers={
'Accept': 'application/json',
'Tus-Resumable': '1.0.0',
'Upload-Length': '8',
'Upload-Metadata': 'filename dGVzdC50eHQ=,content-type dGV4dC9wbGFpbg==',
}, auth=('admin', 'secret'))
The server will return a temporary upload URL in the location header of the response:
HTTP/1.1 201 Created
Tus-Resumable: 1.0.0
Location: http://localhost:55001/plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f
The file can then be uploaded in the next step to that temporary URL.
Uploading a File¶
Note
PATCH requests to the @tus-upload endpoint are allowed on all IContentish content types.
Once a temporary upload URL has been created, a client can send a PATCH request to upload a file. The file content should be send in the body of the request:
PATCH /plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
Upload-Offset: 3
Content-Type: application/offset+octet-stream
defgh
When just a single file is uploaded at once, the server will respond with a 204: No Content response after a successful upload. The HTTP location header contains he URL of the newly created content object:
HTTP/1.1 204 No Content
Upload-Offset: 8
Location: http://localhost:55001/plone/folder/document-2016-10-21
Tus-Resumable: 1.0.0
Partial Upload¶
TUS allows partial upload of files. A partial file is also uploaded by sending a PATCH request to the temporary URL:
PATCH /plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
Upload-Offset: 0
Content-Type: application/offset+octet-stream
abc
The server will also respond with a 204: No content response. Though, instead of providing the final file URL in the ‘location’ header, the server provides an updated ‘Upload-Offset’ value, to tell the client the new offset:
HTTP/1.1 204 No Content
Upload-Offset: 3
Tus-Resumable: 1.0.0
When the last partial file has been uploaded, the server will contain the final file URL in the ‘location’ header.
Replacing Existing Files¶
TUS can also be used to replace an existing file by sending a POST request to the @tus-replace endpoint instead.
POST /plone/myfile/@tus-replace HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
Upload-Length: 8
Upload-Metadata: filename dGVzdC50eHQ=,content-type dGV4dC9wbGFpbg==
The server will respond with a 201: Created status and return the URL of the temprorary created upload resource in the location header of the response:
HTTP/1.1 201 Created
Tus-Resumable: 1.0.0
Location: http://localhost:55001/plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f
The file can then be uploaded to that URL using the PATCH method in the same way as creating a new file:
PATCH /plone/myfile/@tus-upload/4e465958b24a46ec8657e6f3be720991 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
Upload-Offset: 0
Content-Type: application/offset+octet-stream
abcdefgh
The server will respond with a 204: No Content response and the final file URL in the HTTP location header:
HTTP/1.1 204 No Content
Upload-Offset: 8
Location: http://localhost:55001/plone/myfile
Tus-Resumable: 1.0.0
Asking for the Current File Offset¶
To ask the server for the current file offset, the client can send a HEAD request to the upload URL:
http
HEAD /plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Tus-Resumable: 1.0.0
curl
curl -i -X HEAD http://nohost/plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f -H 'Accept: application/json' -H 'Tus-Resumable: 1.0.0' --user admin:secret
httpie
http HEAD http://nohost/plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f Accept:application/json Tus-Resumable:1.0.0 -a admin:secret
python-requests
requests.head('http://nohost/plone/folder/@tus-upload/032803b64ad746b3ab46d9223ea3d90f', headers={
'Accept': 'application/json',
'Tus-Resumable': '1.0.0',
}, auth=('admin', 'secret'))
The server will respond with a 200: Ok status and the current file offset in the ‘Upload-Offset’ header:
HTTP/1.1 200 OK
Upload-Offset: 3
Upload-Length: 8
Tus-Resumable: 1.0.0
Configuration and Options¶
The current TUS configuration and a list of supported options can be retrieved sending an OPTIONS request to the @tus-upload endpoint:
http
OPTIONS /plone/folder/@tus-upload HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i -X OPTIONS http://nohost/plone/folder/@tus-upload -H 'Accept: application/json' --user admin:secret
httpie
http OPTIONS http://nohost/plone/folder/@tus-upload Accept:application/json -a admin:secret
python-requests
requests.options('http://nohost/plone/folder/@tus-upload', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a 204: No content status and HTTP headers containing information about the available extentions and the TUS version:
HTTP/1.1 204 No Content
Tus-Version: 1.0.0
Tus-Extension: creation,expiration
Tus-Resumable: 1.0.0
CORS Configuration¶
If you use CORS and want to make it work with TUS, you have to make sure the TUS specific HTTP headers are allowed by your CORS policy.
<plone:CORSPolicy
allow_origin="http://localhost"
allow_methods="DELETE,GET,OPTIONS,PATCH,POST,PUT"
allow_credentials="true"
allow_headers="Accept,Authorization,Origin,X-Requested-With,Content-Type,Upload-Length,Upload-Offset,Tus-Resumable,Upload-Metadata"
expose_headers="Upload-Offset,Location,Upload-Length,Tus-Version,Tus-Resumable,Tus-Max-Size,Tus-Extension,Upload-Metadata"
max_age="3600"
/>
See the plone.rest documentation for more information on how to configure CORS policies.
See http://tus.io/protocols/resumable-upload.html#headers for a list and description of the individual headers.
Temporary Upload Directory¶
During upload files are stored in a temporary directory that by default is located in the CLIENT_HOME directory.
If you are using a multi ZEO client setup without session stickiness you must configure this to a directory shared
by all ZEO clients by setting the TUS_TMP_FILE_DIR environment variable. E.g. TUS_TMP_FILE_DIR=/tmp/tus-uploads
Vocabularies¶
Vocabularies are utilities containing a list of values grouped by interest or different Plone features.
For example, plone.app.vocabularies.ReallyUserFriendlyTypes
will return all the content types registered in Plone.
The vocabularies return a list of objects with the items title
and token
.
Note
These docs are generated by code tests, therefore you will see some ‘test’ contenttypes appear here.
Get all vocabularies¶
To retrieve a list of all the available vocabularies, send a GET request to the @vocabularies endpoint:
http
GET /plone/@vocabularies HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@vocabularies -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@vocabularies Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@vocabularies', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The response will include a list with the URL (@id
) the dotted names (title
) of all the available vocabularies in Plone:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes",
"title": "plone.app.vocabularies.ReallyUserFriendlyTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AvailableContentLanguages",
"title": "plone.app.vocabularies.AvailableContentLanguages"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.schemaeditor.VocabulariesVocabulary",
"title": "plone.schemaeditor.VocabulariesVocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.users.group_ids",
"title": "plone.app.users.group_ids"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Permissions",
"title": "plone.app.vocabularies.Permissions"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.contentrules.events",
"title": "plone.contentrules.events"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.discussion.vocabularies.CaptchaVocabulary",
"title": "plone.app.discussion.vocabularies.CaptchaVocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Actions",
"title": "plone.app.vocabularies.Actions"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ImagesScales",
"title": "plone.app.vocabularies.ImagesScales"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.PortalActionCategories",
"title": "plone.app.vocabularies.PortalActionCategories"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.multilingual.vocabularies.AllAvailableLanguageVocabulary",
"title": "plone.app.multilingual.vocabularies.AllAvailableLanguageVocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.multilingual.RootCatalog",
"title": "plone.app.multilingual.RootCatalog"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.content.ValidAddableTypes",
"title": "plone.app.content.ValidAddableTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.CommonTimezones",
"title": "plone.app.vocabularies.CommonTimezones"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.restapi.testing.context_vocabulary",
"title": "plone.restapi.testing.context_vocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AvailableEditors",
"title": "plone.app.vocabularies.AvailableEditors"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Roles",
"title": "plone.app.vocabularies.Roles"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SupportedContentLanguages",
"title": "plone.app.vocabularies.SupportedContentLanguages"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords",
"title": "plone.app.vocabularies.Keywords"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Groups",
"title": "plone.app.vocabularies.Groups"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AvailableTimezones",
"title": "plone.app.vocabularies.AvailableTimezones"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog",
"title": "plone.app.vocabularies.Catalog"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.discussion.vocabularies.TextTransformVocabulary",
"title": "plone.app.discussion.vocabularies.TextTransformVocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Month",
"title": "plone.app.vocabularies.Month"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Weekdays",
"title": "plone.app.vocabularies.Weekdays"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.WorkflowTransitions",
"title": "plone.app.vocabularies.WorkflowTransitions"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.WeekdaysShort",
"title": "plone.app.vocabularies.WeekdaysShort"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.contenttypes.metadatafields",
"title": "plone.app.contenttypes.metadatafields"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/Interfaces",
"title": "Interfaces"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.WorkflowStates",
"title": "plone.app.vocabularies.WorkflowStates"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/Fields",
"title": "Fields"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.contenttypes.migration.atctypes",
"title": "plone.app.contenttypes.migration.atctypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.multilingual.vocabularies.AllContentLanguageVocabulary",
"title": "plone.app.multilingual.vocabularies.AllContentLanguageVocabulary"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SyndicatableFeedItems",
"title": "plone.app.vocabularies.SyndicatableFeedItems"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AllowedContentTypes",
"title": "plone.app.vocabularies.AllowedContentTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.users.user_registration_fields",
"title": "plone.app.users.user_registration_fields"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.WeekdaysAbbr",
"title": "plone.app.vocabularies.WeekdaysAbbr"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.UserFriendlyTypes",
"title": "plone.app.vocabularies.UserFriendlyTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.PortalTypes",
"title": "plone.app.vocabularies.PortalTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/Behaviors",
"title": "Behaviors"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SyndicationFeedTypes",
"title": "plone.app.vocabularies.SyndicationFeedTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Timezones",
"title": "plone.app.vocabularies.Timezones"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.formwidget.relations.cmfcontentsearch",
"title": "plone.formwidget.relations.cmfcontentsearch"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Skins",
"title": "plone.app.vocabularies.Skins"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AllowableContentTypes",
"title": "plone.app.vocabularies.AllowableContentTypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.contenttypes.migration.extendedtypes",
"title": "plone.app.contenttypes.migration.extendedtypes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.MonthAbbr",
"title": "plone.app.vocabularies.MonthAbbr"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Workflows",
"title": "plone.app.vocabularies.Workflows"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.contenttypes.migration.changed_base_classes",
"title": "plone.app.contenttypes.migration.changed_base_classes"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users",
"title": "plone.app.vocabularies.Users"
},
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.event.SynchronizationStrategies",
"title": "plone.app.event.SynchronizationStrategies"
}
]
Get a vocabulary¶
To get a particular vocabulary, use the @vocabularies
endpoint with the name of the vocabulary, e.g. /plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes
.
The endpoint can be used with the site root and content objects.
http
GET /plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
The server will respond with a list of terms. The title is purely for display purposes. The token is what should be sent to the server to retrieve the value of the term.
Note
Vocabulary terms will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes",
"items": [
{
"title": "Collection",
"token": "Collection"
},
{
"title": "Comment",
"token": "Discussion Item"
},
{
"title": "DX Test Document",
"token": "DXTestDocument"
},
{
"title": "Event",
"token": "Event"
},
{
"title": "File",
"token": "File"
},
{
"title": "Folder",
"token": "Folder"
},
{
"title": "Image",
"token": "Image"
},
{
"title": "Link",
"token": "Link"
},
{
"title": "News Item",
"token": "News Item"
},
{
"title": "Page",
"token": "Document"
},
{
"title": "Test Document",
"token": "ATTestDocument"
},
{
"title": "Test Folder",
"token": "ATTestFolder"
}
],
"items_total": 12
}
Filter Vocabularies¶
Vocabulary terms can be filtered using the title
or token
parameter.
Use the title
paramenter to filter vocabulary terms by title.
E.g. search for all terms that contain the string doc
in the title:
http
GET /plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?title=doc HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?title=doc' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?title=doc' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?title=doc', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?title=doc",
"items": [
{
"title": "DX Test Document",
"token": "DXTestDocument"
},
{
"title": "Test Document",
"token": "ATTestDocument"
}
],
"items_total": 2
}
Use the token
paramenter to filter vocabulary terms by token.
This is useful in case that you have the token and you need to retrieve the title
.
E.g. search the term doc
in the token:
http
GET /plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?token=Document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i 'http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?token=Document' -H 'Accept: application/json' --user admin:secret
httpie
http 'http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?token=Document' Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?token=Document', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?token=Document",
"items": [
{
"title": "Page",
"token": "Document"
}
],
"items_total": 1
}
Note
You must not filter by title and token at the same time. The API returns a 400 response code if you do so.
Control Panels¶
Control panels in Plone allow you to configure the global site setup of a Plone site. The @controlpanels endpoint in plone.restapi allows you to list all existing control panels in a Plone site and to retrieve or edit the settings of a specific control panel.
Most of the settings in the Plone control panels are based on plone.registry (since Plone 5.x). Therefore you can also use the @registry endpoint to retrieve or manipulate site settings. The @controlpanels endpoint just gives developers are more a convenience way of accessing the settings and makes it easier to render control panels on the front-end.
Note
This is currently only implemented for Plone 5.
Listing Control Panels¶
A list of all existing control panels in the portal can be retrieved by sending a GET request to the @controlpanels endpoint:
http
GET /plone/@controlpanels HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@controlpanels -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@controlpanels Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@controlpanels', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Response:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@controlpanels/date-and-time",
"group": "General",
"title": "Date and Time"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/language",
"group": "General",
"title": "Language"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/mail",
"group": "General",
"title": "Mail"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/navigation",
"group": "General",
"title": "Navigation"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/site",
"group": "General",
"title": "Site"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/search",
"group": "General",
"title": "Search"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/socialmedia",
"group": "General",
"title": "Social Media"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/editing",
"group": "Content",
"title": "Editing"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/imaging",
"group": "Content",
"title": "Image Handling"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/markup",
"group": "Content",
"title": "Markup"
},
{
"@id": "http://localhost:55001/plone/@controlpanels/security",
"group": "Security",
"title": "Security"
}
]
The following fields are returned:
- @id: hypermedia link to the control panel
- title: the title of the control panel
- group: the group where the control panel should show up (e.g. General, Content, Users, Security, Advanced, Add-on Configuration)
Retrieve a single Control Panel¶
To retrieve a single control panel, send a GET request to the URL of the control panel:
http
GET /plone/@controlpanels/editing HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@controlpanels/editing -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/@controlpanels/editing Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/@controlpanels/editing', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/@controlpanels/editing",
"data": {
"available_editors": [
"TinyMCE",
"None"
],
"default_editor": {
"title": "TinyMCE",
"token": "TinyMCE"
},
"enable_link_integrity_checks": true,
"ext_editor": false,
"lock_on_ttw_edit": true,
"subjects_of_navigation_root": false
},
"group": "Content",
"schema": {
"fieldsets": [
{
"fields": [
"available_editors",
"default_editor",
"ext_editor",
"enable_link_integrity_checks",
"lock_on_ttw_edit",
"subjects_of_navigation_root"
],
"id": "default",
"title": "Default"
}
],
"properties": {
"available_editors": {
"additionalItems": true,
"default": [
"TinyMCE",
"None"
],
"description": "Available editors in the portal.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Available editors",
"type": "array",
"uniqueItems": false
},
"default_editor": {
"default": "TinyMCE",
"description": "Select the default wysiwyg editor. Users will be able to choose their own or select to use the site default.",
"title": "Default editor",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AvailableEditors"
}
},
"enable_link_integrity_checks": {
"default": true,
"description": "Determines if the users should get warnings when they delete or move content that is linked from inside the site.",
"title": "Enable link integrity checks",
"type": "boolean"
},
"ext_editor": {
"default": false,
"description": "Determines if the external editor feature is enabled. This feature requires a special client-side application installed. The users also have to enable this in their preferences.",
"title": "Enable External Editor feature",
"type": "boolean"
},
"lock_on_ttw_edit": {
"default": true,
"description": "Disabling locking here will only affect users editing content through the Plone web UI. Content edited via WebDAV clients will still be subject to locking.",
"title": "Enable locking for through-the-web edits",
"type": "boolean"
},
"subjects_of_navigation_root": {
"default": false,
"description": "Limit tags aka keywords vocabulary used for Tags field and in searches to the terms used inside the subtree of the current navigation root. This can be used together with Plone's multilingual extension plone.app.multilingual to only offer keywords of the current selected language. Other addons may utilize this feature for its specific purposes.",
"title": "Limit tags/keywords to the current navigation root",
"type": "boolean"
}
},
"required": [
"available_editors",
"default_editor"
],
"type": "object"
},
"title": "Editing"
}
The following fields are returned:
- @id: hypermedia link to the control panel
- title: title of the control panel
- group: group name of the control panel
- schema: JSON Schema of the control panel
- data: current values of the control panel
Updating a Control Panel with PATCH¶
To update the settings on a control panel send a PATCH request to control panel resource:
http
PATCH /plone/@controlpanels/editing HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"default_editor": "CKeditor",
"ext_editor": true
}
curl
curl -i -X PATCH http://nohost/plone/@controlpanels/editing -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"default_editor": "CKeditor", "ext_editor": true}' --user admin:secret
httpie
echo '{
"default_editor": "CKeditor",
"ext_editor": true
}' | http PATCH http://nohost/plone/@controlpanels/editing Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.patch('http://nohost/plone/@controlpanels/editing', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'default_editor': 'CKeditor',
'ext_editor': True,
}, auth=('admin', 'secret'))
A successful response to a PATCH request will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
Tiles¶
Note
The tiles endpoint currently match only partially (the GET endpoints) the default Plone implementation. The serialization of tiles didn’t match the Mosaic (and plone.app.blocks) implementation and it’s done to not rely on those technologies. The serialization of the tile information on objects are subject to change in the future to extend or improve features.
A tile in Plone is an HTML snippet that can contain arbitrary content (e.g. text, images, videos).
Listing available tiles¶
Note
This endpoint currently does not return any data. The functionality needs to be implemented.
List all available tiles type by sending a GET request to the @tiles endpoint on the portal root:
GET /plone/@tiles HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
The server responds with a Status 200 and list all available tiles:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@tiles/title",
"title": "Title tile",
"description": "A field tile that will show the title of the content object",
},
{
"@id": "http://localhost:55001/plone/@tiles/description",
"title": "Description tile",
"description": "A field tile that will show the description of the content object",
},
]
Retrieve JSON schema of an individual tile¶
Note
This endpoint currently does not return any data. The functionality needs to be implemented.
Retrieve the JSON schema of a specific tile by calling the ‘@tiles’ endpoint with the id of the tile:
GET /plone/@tiles/title HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
The server responds with a JSON schema definition for that particular tile:
HTTP/1.1 200 OK
Content-Type: application/json+schema
{
"properties": {
"title": {
"description": "",
"title": "Title",
"type": "string"
},
...
},
"required": [
"title",
],
"title": "Title Tile",
"type": "object"
}
Retrieving tiles on a content object¶
Tiles data are stored in the objects via a Dexterity behavior plone.tiles. It has two attributes that stores existing tiles in the object (tiles) and the current layout (tiles_layout). As it’s a dexterity behavior, both attributes will be returned in a simple GET:
GET /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
The server responds with a Status 200 and list all stored tiles on that content object:
GET /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/my-document",
...
"tiles_layout": [
"#title-1",
"#description-1",
"#image-1"
],
"tiles": {
"#title-1": {
"@type": "title"
},
"#description-1": {
"@type": "Description"
},
"#image-1": {
"@type": "Image",
"image": "<some random url>"
}
}
}
Tiles objects will contain the tile metadata and the information to render it.
Adding tiles to an object¶
Storing tiles is done also via a default PATCH content operation:
PATCH /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"tiles_layout": [
"#title-1",
"#description-1",
"#image-1"
],
"tiles": {
"#title-1": {
"@type": "title"
},
"#description-1": {
"@type": "Description"
},
"#image-1": {
"@type": "Image",
"image": "<some random url>"
}
}
}
If the tile has been added, the server responds with a 204 status code.
Proposal on saving tiles layout¶
Note
This is not implemented (yet) in the tiles_layout field, but it’s a proposal on how could look like in the future. For now, we stick with the implementation shown in previous sections.
They might be serialized using this structure:
[
[
id: UUID,
columns: [
{
id: UUID, // column UUID
size: int // the size of the column
rows: [
{
id: UUID, // inner row UUID
cells: [
{
id: UUID, // cell UUID
component: string
content: {
// tile fields serialization (or tile id referal)
},
size: int
},
]
}
]
},
]
], // row 1
[], // row 2
]
It tries to match the usual way of CSS frameworks to map grid systems. So we have:
row (orderables up/down) -> column (resizables on width) -> row -> cell (actual tile content)
Rows are orderable vertically, columns resizables horizontally and cells can be moved around to an specific inner row.
Customizing the API¶
Content serialization¶
Dexterity fields¶
The API automatically converts all field values to JSON compatible data, whenever possible. However, you might use fields which store data that cannot be automatically converted, or you might want to customize the representation of certain fields.
For extending or changing the serializing of certain dexterity fields you need to register an IFieldSerializer
-adapter.
Example:
from plone.customfield.interfaces import ICustomField
from plone.dexterity.interfaces import IDexterityContent
from plone.restapi.interfaces import IFieldSerializer
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.serializer.dxfields import DefaultFieldSerializer
from zope.component import adapter
from zope.interface import Interface
from zope.interface import implementer
@adapter(ICustomField, IDexterityContent, Interface)
@implementer(IFieldSerializer)
class CustomFieldSerializer(DefaultFieldSerializer):
def __call__(self):
value = self.get_value()
if value is not None:
# Do custom serializing here, e.g.:
value = value.output()
return json_compatible(value)
Register the adapter in ZCML:
<configure xmlns="http://namespaces.zope.org/zope">
<adapter factory=".serializer.CustomFieldSerializer" />
</configure>
The json_compatible
function recursively converts the value to JSON compatible data, when possible.
When a value cannot be converted, a TypeError
is raised.
It is recommended to pass all values through json_compatible
in order to validate and convert them.
For customizing a specific field instance, a named IFieldSerializer
adapter can be registered.
The name may either be the full dottedname of the field
(plone.app.dexterity.behaviors.exclfromnav.IExcludeFromNavigation.exclude_from_nav
) or the shortname of the field (exclude_from_nav
).
Conventions¶
Nouns vs Verbs¶
Rule: Use nouns to represent resources.
Do:
/my-folder
/@registry
/@types
Don’t:
/createFolder
/deleteDocument
/updateEvent
Reason:
RESTful URI should refer to a resource that is a thing (noun) instead of referring to an action (verb) because nouns have properties as verbs do not. The REST architectural principle uses HTTP verbs to interact with resources.
Though, there is an exception to that rule, verbs can be used for specific actions or calculations, .e.g.:
/login
/logout
/move-to
/reset-password
Singluar vs Plural¶
Rule: Use plural resources.
Do:
/users
/users/21
Don’t:
/user
/user/21
Reason:
If you use singular for a collection like resource (e.g. “/user” to retrieve a list of all users) it feels wrong. Mixing singular and plural is confusing (e.g. user “/users” for retrieving users and “/user/21” to retrieve a single user).
Attribute names in URIs¶
Rule: Use hyphens to improve readability of URIs.
Do:
/users/noam/reset-password
Don’t:
/users/noam/resetPassword
/users/noam/ResetPassword
/users/noam/reset_password
Reason:
Upper vs. Lowercase¶
Rule: Use lowercase letters in URIs.
Do:
http://example.com/my-folder/my-document
Don’t:
http://example.com/My-Folder/My-Document
Reason: RFC 3986 defines URIs as case-sensitive except for the scheme and host components. e.g.
Those two URIs are equivalent:
http://example.org/my-folder/my-document
HTTP://EXAMPLE.ORG/my-folder/my-document
While this one is not equivalent to the two URIs above:
http://example.org/My-Folder/my-document
To avoid confusion we always use lowercase letters in URIs.
Versioning¶
Versioning APIs does make a lot of sense for public API services. Especially if an API provider needs to ship different versions of the API at the same time. Though, Plone already has a way to version packages and it currently does not make sense for us to expose that information via the API. We will always just ship one version of the API at a time and we are usually in full control over the backend and the frontend.
Translations¶
Note
This is only available on Plone 5.
Since Plone 5 the product plone.app.multilingual is included in the base Plone installation although it is not enabled by default.
Multilingualism in Plone not only allows the managers of the site to configure the site interface texts to be in one language or another (such as the configuration menus, error messages, information messages or other static text) but also to configure Plone to handle multilingual content. To achieve that it provides the user interface for managing content translations.
You can get additional information about the multilingual capabilities of Plone in the documentation.
In connection with that capabilities, plone.restapi provides a @translations endpoint to handle the translation information of the content objects.
Once we have installed plone.app.multilingual and enabled more than one language we can link two content-items of different languages to be the translation of each other issuing a POST query to the @translations endpoint including the id of the content which should be linked to. The id of the content must be a full URL of the content object:
http
POST /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"id": "http://localhost:55001/plone/es/test-document"
}
curl
curl -i -X POST http://nohost/plone/en/test-document/@translations -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"id": "http://localhost:55001/plone/es/test-document"}' --user admin:secret
httpie
echo '{
"id": "http://localhost:55001/plone/es/test-document"
}' | http POST http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.post('http://nohost/plone/en/test-document/@translations', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'id': 'http://localhost:55001/plone/es/test-document',
}, auth=('admin', 'secret'))
Note
“id” is a required field and needs to point to an existing content on the site.
The API will return a 201 Created response if the linking was successful.
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/en/test-document
{}
After linking the contents we can get the list of the translations of that
content item by issuing a GET
request on the @translations endpoint of
that content item.:
http
GET /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/en/test-document/@translations -H 'Accept: application/json' --user admin:secret
httpie
http http://nohost/plone/en/test-document/@translations Accept:application/json -a admin:secret
python-requests
requests.get('http://nohost/plone/en/test-document/@translations', headers={
'Accept': 'application/json',
}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/en/test-document/@translations",
"items": [
{
"@id": "http://localhost:55001/plone/es/test-document",
"language": "es"
}
]
}
To unlink the content, issue a DELETE
request on the @translations
endpoint of the content item and provide the language code you want to unlink.:
http
DELETE /plone/en/test-document/@translations HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"language": "es"
}
curl
curl -i -X DELETE http://nohost/plone/en/test-document/@translations -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"language": "es"}' --user admin:secret
httpie
echo '{
"language": "es"
}' | http DELETE http://nohost/plone/en/test-document/@translations Accept:application/json Content-Type:application/json -a admin:secret
python-requests
requests.delete('http://nohost/plone/en/test-document/@translations', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, json={
'language': 'es',
}, auth=('admin', 'secret'))
Note
“language” is a required field.
HTTP/1.1 204 No Content
Expansion¶
This endpoint can be used with the Expansion mechanism which allows to get additional information about a content item in one query, avoiding unnecesary requests.
If a simple GET
request is done on the content item, a new entry will be shown on the @components
entry with the URL of the @translations endpoint:
Email Send¶
Send Mail to Arbitrary Addresses¶
To send an email to an arbitrary e-mail address, send a POST request to the /@email-send
endpoint that is available on the site root:
POST http://localhost:8080/Plone/@email-send
Accept: application/json
Content-Type: application/json
{
'name': 'John Doe',
'from': 'john@doe.com',
'to': 'jane@doe.com',
'subject': 'Hello!',
'message': 'Just want to say hi.'
}
This endpoint is controlled via the Use mailhost services permission, the default one in Zope.
The ‘to’, ‘from’ and ‘message’ fields are required. The ‘subject’ and ‘name’ fields are optional.
The server will respond with status 204 No Content when the email has been sent successfully:
HTTP/1.1 204 No Content
i18n: internationalization of screen messages¶
Plone already provides user-interface translations using the plone.app.locales packages.
In plone.restapi we also use those translations where the end user needs to have those translated strings, this way the front-end work is easier, because you directly get from the server everything you need, instead of needing to query yet another endpoint to get the translations.
To do so, plone.restapi relies on Plone’s language-negotiation configuration and lets Plone to do the work of deciding the language in which the messages should be shown.
For the content of a multilingual site built using plone.app.multilingual this is an easy task: Plone is configured to show in the language of the content-object, so there is no need to ask anything to the REST API.
Nevertheless, when you want to query the Plone Site object of a multilingual site, or any other endpoint in a plain Plone site with multiple languages configured, you need to query the REST API which language do you want to have the messages on, otherwise you will get the messages on the default language configured in Plone.
To achieve that, the REST API requires to use the Accept-Language
HTTP header passing
as the value the code of the required language.
You will also need to configure Plone to use the browser request language negotiation. To do so, you need to go the Plone Control Panel, go to the Language Control Panel, open the Negotiation configuration tab and select “Use browser language request negotiation” option.
Using this option we can get the content-type titles translated:
http
GET /plone/@types HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@types -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret
httpie
http http://nohost/plone/@types Accept:application/json Accept-Language:es -a admin:secret
python-requests
requests.get('http://nohost/plone/@types', headers={
'Accept': 'application/json',
'Accept-Language': 'es',
}, auth=('admin', 'secret'))
And the response:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"@id": "http://localhost:55001/plone/@types/File",
"addable": true,
"title": "Archivo"
},
{
"@id": "http://localhost:55001/plone/@types/Folder",
"addable": true,
"title": "Carpeta"
},
{
"@id": "http://localhost:55001/plone/@types/Collection",
"addable": true,
"title": "Colecci\u00f3n"
},
{
"@id": "http://localhost:55001/plone/@types/DXTestDocument",
"addable": true,
"title": "DX Test Document"
},
{
"@id": "http://localhost:55001/plone/@types/Link",
"addable": true,
"title": "Enlace"
},
{
"@id": "http://localhost:55001/plone/@types/Event",
"addable": true,
"title": "Evento"
},
{
"@id": "http://localhost:55001/plone/@types/Image",
"addable": true,
"title": "Imagen"
},
{
"@id": "http://localhost:55001/plone/@types/News Item",
"addable": true,
"title": "Noticia"
},
{
"@id": "http://localhost:55001/plone/@types/Document",
"addable": true,
"title": "P\u00e1gina"
}
]
All the field titles and descriptions, will also be translated. For instance for the Folder content type:
http
GET /plone/@types/Folder HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/@types/Folder -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret
httpie
http http://nohost/plone/@types/Folder Accept:application/json Accept-Language:es -a admin:secret
python-requests
requests.get('http://nohost/plone/@types/Folder', headers={
'Accept': 'application/json',
'Accept-Language': 'es',
}, auth=('admin', 'secret'))
And the response:
HTTP/1.1 200 OK
Content-Type: application/json+schema
{
"fieldsets": [
{
"fields": [
"title",
"description"
],
"id": "default",
"title": "Default"
},
{
"fields": [
"subjects",
"language",
"relatedItems"
],
"id": "categorization",
"title": "Categorizaci\u00f3n"
},
{
"fields": [
"effective",
"expires"
],
"id": "dates",
"title": "Fechas"
},
{
"fields": [
"creators",
"contributors",
"rights"
],
"id": "ownership",
"title": "Propiedad"
},
{
"fields": [
"allow_discussion",
"exclude_from_nav",
"id",
"nextPreviousEnabled"
],
"id": "settings",
"title": "Configuraci\u00f3n"
}
],
"layouts": [
"album_view",
"event_listing",
"full_view",
"listing_view",
"summary_view",
"tabular_view"
],
"properties": {
"allow_discussion": {
"choices": [
[
"True",
"S\u00ed"
],
[
"False",
"No"
]
],
"description": "Permitir comentarios para este tipo de contenido",
"enum": [
"True",
"False"
],
"enumNames": [
"S\u00ed",
"No"
],
"title": "Permitir comentarios",
"type": "string"
},
"contributors": {
"additionalItems": true,
"description": "Los nombres de las personas que han contribuido a este elemento. Cada colaborador deber\u00eda estar en una l\u00ednea independiente.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Colaboradores",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
}
}
},
"creators": {
"additionalItems": true,
"description": "Personas responsables de la creaci\u00f3n del contenido de este elemento. Por favor, introduzca una lista de nombres de usuario, uno por l\u00ednea. El autor principal deber\u00eda ser el primero.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Creadores",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users"
}
}
},
"description": {
"description": "Usado en listados de elementos y resultados de b\u00fasquedas.",
"minLength": 0,
"title": "Descripci\u00f3n",
"type": "string",
"widget": "textarea"
},
"effective": {
"description": "La fecha en la que el documento ser\u00e1 publicado. Si no selecciona ninguna fecha, el documento ser\u00e1 publicado inmediatamente.",
"title": "Fecha de Publicaci\u00f3n",
"type": "string",
"widget": "datetime"
},
"exclude_from_nav": {
"default": false,
"description": "Si est\u00e1 marcado, este elemento no aparecer\u00e1 en el \u00e1rbol de navegaci\u00f3n",
"title": "Excluir de la navegaci\u00f3n",
"type": "boolean"
},
"expires": {
"description": "La fecha en la que expira el documento. Esto har\u00e1 autom\u00e1ticamente el documento invisible a otros a una fecha dada. Si no elije ninguna fecha, nunca expirar\u00e1.",
"title": "Fecha de Terminaci\u00f3n",
"type": "string",
"widget": "datetime"
},
"id": {
"description": "Este nombre se mostrar\u00e1 en la URL.",
"title": "Nombre corto",
"type": "string"
},
"language": {
"default": "en",
"description": "",
"title": "Idioma",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
}
},
"nextPreviousEnabled": {
"default": false,
"description": "Esto habilita el widget siguiente/pr\u00f3ximo en los elementos contenidos en esta carpeta.",
"title": "Habilitar la navegaci\u00f3n siguiente/anterior",
"type": "boolean"
},
"relatedItems": {
"additionalItems": true,
"default": [],
"description": "",
"items": {
"description": "",
"title": "Related",
"type": "string",
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
}
},
"title": "Contenido relacionado",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"pattern_options": {
"recentlyUsed": true
},
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Catalog"
}
}
},
"rights": {
"description": "Declaraci\u00f3n de copyright o informaci\u00f3n de otros derechos sobre este elemento.",
"minLength": 0,
"title": "Derechos de Autor",
"type": "string",
"widget": "textarea"
},
"subjects": {
"additionalItems": true,
"description": "Las etiquetas suelen utilizarse para la organizaci\u00f3n a medida del contenido.",
"items": {
"description": "",
"title": "",
"type": "string"
},
"title": "Etiquetas",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords"
}
}
},
"title": {
"description": "",
"title": "T\u00edtulo",
"type": "string"
}
},
"required": [
"title",
"nextPreviousEnabled"
],
"title": "Carpeta",
"type": "object"
}
In a given object, the workflow state and actions will be translated too:
http
GET /plone/front-page/@workflow HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@workflow -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret
httpie
http http://nohost/plone/front-page/@workflow Accept:application/json Accept-Language:es -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@workflow', headers={
'Accept': 'application/json',
'Accept-Language': 'es',
}, auth=('admin', 'secret'))
And the response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@workflow",
"history": [
{
"action": null,
"actor": "test_user_1_",
"comments": "",
"review_state": "private",
"time": "2016-10-21T19:00:00+00:00",
"title": "Privado"
}
],
"transitions": [
{
"@id": "http://localhost:55001/plone/front-page/@workflow/publish",
"title": "Publicar"
},
{
"@id": "http://localhost:55001/plone/front-page/@workflow/submit",
"title": "Enviar para publicaci\u00f3n"
}
]
}
The same happens in the @history endpoint, all the relevant messages, will be shown translated:
http
GET /plone/front-page/@history HTTP/1.1
Accept: application/json
Accept-Language: es
Authorization: Basic YWRtaW46c2VjcmV0
curl
curl -i http://nohost/plone/front-page/@history -H 'Accept: application/json' -H 'Accept-Language: es' --user admin:secret
httpie
http http://nohost/plone/front-page/@history Accept:application/json Accept-Language:es -a admin:secret
python-requests
requests.get('http://nohost/plone/front-page/@history', headers={
'Accept': 'application/json',
'Accept-Language': 'es',
}, auth=('admin', 'secret'))
And the response:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"action": "Crear",
"actor": {
"@id": "http://localhost:55001/plone/@users/test_user_1_",
"fullname": "",
"id": "test_user_1_",
"username": "test-user"
},
"comments": "",
"review_state": "private",
"state_title": "Privado",
"time": "2016-10-21T19:00:00",
"transition_title": "Crear",
"type": "workflow"
},
{
"@id": "http://localhost:55001/plone/front-page/@history/0",
"action": "Editado",
"actor": {
"@id": "http://localhost:55001/plone/@users/test-user",
"fullname": "test-user",
"id": "test-user",
"username": null
},
"comments": null,
"may_revert": true,
"time": "2016-10-21T19:00:00",
"transition_title": "Editado",
"type": "versioning",
"version": 0
}
]
Email Notification¶
Contact Site Owner aka Contact Form¶
Plone allows the user to contact the site owner via a form on the website. This makes sure the site owner does not have to expose their email addresses publicly and at the same time allow the users to reach out to the site owners.
To send an email notification to the site owner, send a POST request to the /@email-notification
endpoint that is available on the site root:
POST http://localhost:8080/Plone/@email-notification
Accept: application/json
Content-Type: application/json
{
'name': 'John Doe',
'from': 'john@doe.com',
'subject': 'Hello!',
'message': 'Just want to say hi.'
}
The ‘from’ and ‘message’ fields are required. The ‘subject’ and ‘name’ fields are optional.
The server will respond with status 204 No Content when the email has been sent successfully:
HTTP/1.1 204 No Content
Contact Portal Users¶
Note
This endpoint is NOT implemented yet.
To send an email notification to another user of the portal, send a POST request to the /@email-notification
endpoint on a particular user (e.g. the admin user):
POST http://localhost:8080/Plone/@users/admin/@email-notification
Accept: application/json
Content-Type: application/json
{
'name': 'John Doe',
'from': 'john@doe.com',
'subject': 'Hello!',
'message': 'Just want to say hi.'
}
Note
When using “email as login”, we strongly recommend to also enable the “Use UUID user ids” setting in the security control panel, to obfuscate the email in the user endpoint URL. Otherwise the ‘@users’ endpoint will expose the email addresses of all your users.
Upgrade Guide¶
This upgrade guide lists all breaking changes in plone.restapi and explains the necessary steps that are needed to upgrade to the lastest version.
Upgrading to plone.restapi 4.x¶
plone.restapi 4.0.0 introduces the following breaking changes:
- Fields with vocabularies now return the
token
andtitle
instead of the stored value. - Choice and list fields return a hyperlink to a vocabulary instead of
choices
,enum
, andenumNames
. - Serialize widget parameters into a
widgetOptions
object instead of adding them to the top level of the schema property. - The vocabularies endpoint does no longer returns an
@id
for terms, the results are batched, and terms are now listed asitems
instead ofterms
to match other batched responses.
Serialization and Deserialization of fields with vocabularies¶
The serialization of fields with vocabularies (e.g. Choice
) now return the
token and the title of the vocabulary term instead of the stored value.
This is allows displaying the term (title) without additionally querying the
vocabulary. However it’s necessary to adopt existing client implementations.
The date and time controlpanel previously returned a number for the
first_weekday
property:
{
"@id": "http://localhost:55001/plone/@controlpanels/date-and-time",
"data": {
...
"first_weekday": 0,
...
}
...
}
Now it returns an object with a token and a title:
{
"@id": "http://localhost:55001/plone/@controlpanels/date-and-time",
"data": {
...
"first_weekday": {
"title": "Monday",
"token": "0"
},
...
}
...
}
Deserialization accepts objects that contain a token, but also just the token or the value.
However it’s highly recommended to always use the token as vocabulary terms may contain values that are not JSON serializable.
Choice and List fields return link to vocabulary instead of the values¶
Choice and List fields using named vocabularies are now serialized
with a vocabulary
property giving the URL of the @vocabularies
endpoint for the vocabulary instead of including choices
,
enum
and enumNames
inline.
Old Response:
"choices": [
[
"de",
"Deutsch"
],
[
"en",
"English"
],
],
"enum": [
"de",
"en",
],
"enumNames": [
"Deutsch",
"English",
],
New response:
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.discussion.vocabularies.CaptchaVocabulary"
},
Serialize widget parameters into a widgetOptions
object¶
Serialize widget parameters into a widgetOptions
object instead of adding them to the top level of the schema property.
Old response:
"vocabulary": "plone.app.vocabularies.Users"
New response:
"widgetOptions": {
"pattern_options": {
"recentlyUsed": true
},
"vocabulary": { "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Users" }
}
Example: Vocabularies Subjects Field¶
The subjects
field is now serialized as an array
of string
items using the plone.app.vocabularies.Keywords
vocabulary.
Old response:
"subjects": {
"choices": [...],
"enum": [...],
"enumNames": [...],
}
"type": "string"
New response:
"additionalItems": true,
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.Keywords"
}
},
"items": {
"description": "",
"title": "",
"type": "string"
},
Example: Available Time Zones Field (vocabulary in items
)¶
Old response:
"available_timezones": {
"additionalItems": true,
"default": [],
"description": "The timezones, which should be available for the portal. Can be set for users and events",
"items": {
"choices": [
[
"Africa/Abidjan",
"Africa/Abidjan"
],
[
"Africa/Accra",
"Africa/Accra"
],
...
"enum": [
...
],
"enumNames": [
...
]
},
title: "Available timezones",
type: "array",
uniqueItems: true,
}
New response:
"available_timezones": {
"additionalItems": true,
"default": [],
"description": "The timezones, which should be available for the portal. Can be set for users and events",
"items": {
"description": "",
"title": "",
"type": "string",
"vocabulary": {
"@id": "http://localhost:8080/Plone/@vocabularies/plone.app.vocabularies.Timezones"
}
},
"title": "Available timezones",
"type": "array",
"uniqueItems": true
},
Example: Weekday Field (vocabulary in main property)¶
Old response:
"first_weekday": {
"choices": [
[
"0",
"Monday"
],
[
"1",
"Tuesday"
],
[
"2",
"Wednesday"
],
[
"3",
"Thursday"
],
[
"4",
"Friday"
],
[
"5",
"Saturday"
],
[
"6",
"Sunday"
]
],
"description": "First day in the week.",
"enum": [
"0",
"1",
"2",
"3",
"4",
"5",
"6"
],
"enumNames": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
],
"title": "First weekday",
"type": "string"
},
New response:
"first_weekday": {
"description": "First day in the week.",
"title": "First weekday",
"type": "string",
"vocabulary": {
"@id": "http://localhost:8080/Plone/@vocabularies/plone.app.vocabularies.Weekdays"
}
},
Vocabularies Endpoint¶
The vocabularies endpoint does no longer returns an @id
for terms.
The results are batched, and terms are now listed as items
instead of terms
to match other batched responses.
Batch size is 25 by default but can be overridden using the b_size
parameter.
Old response:
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes",
"terms": [
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes/Collection",
"title": "Collection",
"token": "Collection"
},
...
]
}
New response:
{
"@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes",
"items": [
{
"title": "Collection",
"token": "Collection"
},
...
],
"items_total": 12
}
Upgrading to plone.restapi 3.x¶
Image scales¶
Image download URLs and image scale URLs are created using the UID based url formats. This allows Plone to create different URLs when the image changes and thus ensuring caches are updated.
Old Response:
{
"icon": {
"download": "http://localhost:55001/plone/image/@@images/image/icon",
"height": 32,
"width": 24
},
"large": {
"download": "http://localhost:55001/plone/image/@@images/image/large",
"height": 768,
"width": 576
},
...
}
New Response:
{
"icon": {
"download": "http://localhost:55001/plone/image/@@images/8eed3f80-5e1f-4115-85b8-650a10a6ca84.png",
"height": 32,
"width": 24
},
"large": {
"download": "http://localhost:55001/plone/image/@@images/0d1824d1-2672-4b62-9277-aeb220d3bf15.png",
"height": 768,
"width": 576
},
...
}
@sharing endpoint¶
The available_roles
property in the response to a GET request to the
@sharing
endpoint has changed: Instead of a flat list of strings, it now
contains a list of dicts, with the role ID and their translated title:
Old Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"available_roles": [
"Contributor",
"Editor",
"Reviewer",
"Reader"
],
"entries": [
"..."
],
"inherit": true
}
New Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"available_roles": [
{
"id": "Contributor",
"title": "Can add"
},
{
"id": "Editor",
"title": "Can edit"
},
{
"id": "Reader",
"title": "Can view"
},
{
"id": "Reviewer",
"title": "Can review"
}
],
"entries": [
"..."
],
"inherit": true
}
Custom Content Deserializers¶
If you have implemented custom content deserializers, you have to handle the
new create
keyword in the __call__
method, which determines if deserialization
is performed during object creation or while updating an object.
Deserializers should only fire an IObjectModifiedEvent
event if an object
has been updated. They should not fire it, when a new object has been created.
See Dexterity content deserializer for an example.
Upgrading to plone.restapi 2.x¶
plone.restapi 2.0.0 converts all datetime, DateTime and time to UTC before serializing. The translations endpoint becomes “expandable”, which introduces the following breaking changes.
Translations¶
When using the @translations endpoint in plone.restapi 1.x, the endpoint returned a language key with the content object’s language and a translations key with all its translations.
Now, as the endpoint is expandable we want the endpoint to behave like the other expandable endpoints. As top level information we only include the name of the endpoint on the @id attribute and the actual translations of the content object in an attribute called items.
This means that now the JSON response to a GET request to the Translations endpoint does not include anymore the language of the actual content item and the translations in an attribute called items instead of translations.
Old response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/en/test-document",
"language": "en",
"translations": [
{
"@id": "http://localhost:55001/plone/es/test-document",
"language": "es"
}
]
}
New response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/en/test-document/@translations",
"items": [
{
"@id": "http://localhost:55001/plone/es/test-document",
"language": "es"
}
]
}
Upgrading to plone.restapi 1.0b1¶
In plone.restapi 1.0b1 the ‘url’ attribute on the Navigation and Breadcrumbs endpoint was renamed to ‘@id’ to be consistent with other links/URLs used in plone.restapi.
The JSON response to a GET request to the Breadcrumbs endpoint changed from using the ‘url’ attribute for ‘items’:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"title": "Welcome to Plone",
"url": "http://localhost:55001/plone/front-page"
}
]
}
to using the ‘@id’ for the URL of ‘items’:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
}
The JSON response to a GET request to the Navigation endpoint changed from using the ‘url’ attribute for ‘items’:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@navigation",
"items": [
{
"title": "Home",
"url": "http://localhost:55001/plone",
},
{
"title": "Welcome to Plone",
"url": "http://localhost:55001/plone/front-page"
}
]
}
to using the ‘@id’ for the URL of ‘items’:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/front-page/@navigation",
"items": [
{
"@id": "http://localhost:55001/plone",
"title": "Home"
},
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
}
The expansion mechanism is also affected by this change when Navigation or Breadcrumbs endpoints are expanded.
From using ‘url’ in the breadcrumb ‘items’:
{
"@components": {
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"title": "Welcome to Plone",
"url": "http://localhost:55001/plone/front-page"
}
]
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation",
"items": [
{
"title": "Home",
"url": "http://localhost:55001/plone",
},
{
"title": "Welcome to Plone",
"url": "http://localhost:55001/plone/front-page"
}
]
},
...
}
to using ‘@id’ in the breadcrumb ‘items’:
{
"@components": {
"breadcrumbs": {
"@id": "http://localhost:55001/plone/front-page/@breadcrumbs",
"items": [
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
},
"navigation": {
"@id": "http://localhost:55001/plone/front-page/@navigation",
"items": [
{
"@id": "http://localhost:55001/plone",
"title": "Home"
},
{
"@id": "http://localhost:55001/plone/front-page",
"title": "Welcome to Plone"
}
]
},
...
}
Changelog:
- Rename 'url' attribute on navigation / breadcrumb to '@id'. [timo]
Pull Request:
Upgrading to plone.restapi 1.0a25¶
plone.restapi 1.0a25 introduced three breaking changes:
- Remove @components navigation and breadcrumbs. Use top level @navigation and @breadcrumb endpoints instead. [timo]
- Remove “sharing” attributes from GET response. [timo,jaroel]
- Convert richtext using .output_relative_to. Direct conversion from RichText if no longer supported as we always need a context for the ITransformer. [jaroel]
Remove @components endpoint¶
plone.restapi 1.0a25 removed the @components endpoint which used to provide a Navigation and a Breadcrumbs endpoint.
Instead of using “@components/navigation”:
http://localhost:8080/Plone/@components/navigation
Use just “@navigation”:
http://localhost:8080/Plone/@navigation
Instead of using “@components/breadcrumbs”:
http://localhost:8080/Plone/@components/breadcrumbs
Use just “@breadcrumbs”:
http://localhost:8080/Plone/@breadcrumbs
Changelog:
- Remove @components navigation and breadcrumbs. Use top level @navigation and @breadcrumb endpoints instead. [timo]
Pull Request:
Remove “sharing” attributes¶
The “sharing” attribute was removed from all content GET responses:
"sharing": {
"@id": "http://localhost:55001/plone/collection/@sharing",
"title": "Sharing"
},
Use the Sharing endpoint that can be expanded instead.
Changelog:
- Remove "sharing" attributes from GET response. [timo,jaroel]
Pull Request:
Convert richtext using .output_relative_to¶
Using “.output_relative_to” in the
Changelog:
- Convert richtext using .output_relative_to. Direct conversion from RichText if no longer supported as we *always* need a context for the ITransformer. [jaroel]
Pull Request:
Upgrading to plone.restapi 1.0a17¶
plone.restapi 1.0a17 changed the serialization of the rich-text “text” field for content objects from using ‘raw’ (a unicode string with the original input markup):
"text": {
"content-type": "text/plain",
"data": "Lorem ipsum",
"encoding": "utf-8"
},
to using ‘output’ (a unicode object representing the transformed output):
"text": {
"content-type": "text/plain",
"data": "<p>Lorem ipsum</p>",
"encoding": "utf-8"
},
Changelog:
- Change RichText field value to use 'output' instead of 'raw' to fix inline paths. This fixes #302. [erral]
Pull Request:
Contributing to plone.restapi¶
Generating documentation examples¶
This documentation includes examples of requests and responses (http, curl, httpie and python-requests).
These examples are generated by the documentation tests in test_documentation.py
.
To generate a new example, add a new test case to test_documentation.py - for example test_documentation_search_fullobjects
, and run the test:
./bin/test -t test_documentation_search_fullobjects
This generates the request and the response files in tests/http-examples/
.
Include them in the documentation like this:
.. http:example:: curl httpie python-requests
:request: ../../src/plone/restapi/tests/http-examples/search_fullobjects.req
.. literalinclude:: ../../src/plone/restapi/tests/http-examples/search_fullobjects.resp
:language: http
Build the sphinx docs locally to test the rendering by running ./bin/sphinxbuilder
.
Make sure you add and commit the generated files in http-examples
.

Introduction¶
plone.restapi is a RESTful hypermedia API for Plone.
Documentation¶
Getting started¶
A live demo of Plone 5 with the latest plone.restapi release is available at:
http://plonedemo.kitconcept.com
Example GET request on the portal root:
$ curl -i https://plonedemo.kitconcept.com -H "Accept: application/json"
Example POST request to create a new document:
$ curl -i -X POST https://plonedemo.kitconcept.com -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"@type": "Document", "title": "My Document"}' --user admin:admin
Note
You will need some kind of API browser application to explore the API. We recommend using Postman.
Installation¶
Install plone.restapi by adding it to your buildout:
[buildout]
...
eggs =
plone.restapi
and then running bin/buildout
Contribute¶
- Issue Tracker: https://github.com/plone/plone.restapi/issues
- Source Code: https://github.com/plone/plone.restapi
- Documentation: https://plonerestapi.readthedocs.io/en/latest
Examples¶
plone.restapi is used in production since the first alpha release. It can be seen in action at the following sites:
- Zeelandia GmbH & Co. KG: https://www.zeelandia.de (by kitconcept GmbH)
- VHS-Ehrenamtsportal: https://vhs-ehrenamtsportal.de (by kitconcept GmbH)
- German Physical Society: https://www.dpg-physik.de (by kitconcept GmbH)
- Universitat Politècnica de Catalunya: https://www.upc.edu (by kitconcept GmbH)
Support¶
If you are having issues, please let us know via the issue tracker.
If you required professional support, here is a list of Plone solution providers that contributed significantly to plone.restapi in the past:
- kitconcept GmbH (Germany) https://kitconcept.com
- 4teamwork (Switzerland) https://www.4teamwork.ch
- CodeSyntax (Spain) https://www.codesyntax.com/en
License¶
The project is licensed under the GPLv2.
Appendix, Indices and tables¶
HTTP Status Codes¶
This is the list of status codes that are used in plone.restapi. Here is a full list of all HTTP status codes.
- 200 OK
- Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request, the response will contain an entity describing or containing the result of the action.
- 201 Created
- The request has been fulfilled and resulted in a new resource being created.
- 204 No Content
- The server successfully processed the request, but is not returning any content. Usually used as a response to a successful delete request.
- 2xx Success
- This class of status codes indicates the action requested by the client was received, understood, accepted and processed successfully.
- 400 Bad Request
- The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)
- Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the requested resource.
- 403 Forbidden
- The request was a valid request, but the server is refusing to respond to it. Unlike a 401 Unauthorized response, authenticating will make no difference.
- 404 Not Found
- The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible.
- 405 Method Not Allowed
- A request method is not supported for the requested resource; for example, a GET request on a form which requires data to be presented via POST, or a PUT request on a read-only resource.
- 409 Conflict
- Indicates that the request could not be processed because of conflict in the request, such as an edit conflict in the case of multiple updates.
- 4xx Client Error
- The 4xx class of status code is intended for cases in which the client seems to have errored.
- 500 Internal Server Error
- The server failed to fulfill an apparently valid request.
- 5xx Server Error
- The server failed to fulfill an apparently valid request.
Glossary¶
- Accept Header
- Part of the Request that is responsible to define the expected type of data to be accepted by the client in the Response.
- Authentication Method
- Access restriction provided by the connection chain to the server exposed to the client.
- Part of the Request that is responsible for the authentication related to the right user or service to ask for a Response.
- Basic Auth
- A simple Authentication Method referenced in the Authorization Header that needs to be provided by the server.
- HTTP-Header
- HTTP Header
- Header
- HTTP Header
- The part of the communication of the client with the server that provides the initialisation of the communication of a Request.
- HTTP-Request
- HTTP Request
- Request
- Requests
- HTTP Request
- The initial action performed by a web client to communicate with a server. The Request is usually followed by a Response by the server, either synchronous or asynchronous (which is more complex to handle on the user side)
- HTTP-Response
- HTTP Response
- Response
- HTTP Response
- Answer of or action by the server that is executed or send to the client after the Request is processed.
- HTTP-Verb
- HTTP Verb
- Verb
- HTTP Verb
- One of the basic actions that can be requested to be executed by the server (on an object) based on the Request.
- Object URL
- The target object of the Request
- REST
- REST stands for Representational State Transfer. It is a software architectural principle to create loosely coupled web APIs.
- workflow
- A concept in Plone (and other CMS’s) whereby a content object can be in a number of states (private, public, etcetera) and uses transitions to change between them (e.g. “publish”, “approve”, “reject”, “retract”). See the Plone docs on Workflow