Direct editing
Added in version 18.
The direct editing API unifies one-time token generation and editor discovery into a single server API, so that mobile apps and the desktop client can edit files without every editor app shipping its own token handling and endpoints.
The basic idea: a client asks the server for a one-time URL to create or open a file in a given editor. The server returns a URL that can be loaded in an unauthenticated webview. The editor app takes over from there, handling the actual editing and saving with its own session/token mechanism.
This page covers three layers:
Registering an editor — for app developers who want to make their editor available through direct editing (server-side PHP).
HTTP API — for client developers who consume the editor list and request one-time URLs (OCS HTTP API).
Webview integration — the messaging convention used between the editor web page and the native client once the webview is open.
Overview
A typical create-and-edit flow looks like this:
Client (mobile/desktop) Server Editor app
----------------------- ------ ----------
1. GET .../directEditing -> list of editors + creators
2. POST .../directEditing/create -> one-time URL (/directEditing/<token>)
3. open URL in webview -> route resolves token, calls
IEditor::open(IToken) -> editor page
4. webview <----------------------------------------------------> native client
(loading/loaded/close/... messages, see "Webview integration")
The one-time token is consumed the first time the webview URL is opened. From that point on, keeping the session alive (editing, saving, refreshing) is the editor app’s own responsibility — direct editing only bootstraps the session.
Registering an editor
To expose your app through direct editing, implement
OCP\DirectEditing\IEditor and register it when the
OCP\DirectEditing\RegisterDirectEditorEvent is dispatched.
The editor class
IEditor describes how your editor is presented and how a file is opened:
getId(): string— a unique identifier for the editor, e.g.text.getName(): string— a human-readable name, e.g.Nextcloud Text.getMimetypes(): string[]— mimetypes that should open in this editor by default.getMimetypesOptional(): string[]— mimetypes that can optionally be opened in this editor.getCreators(): array— a list ofACreateEmpty/ACreateFromTemplateinstances offered as “create new …” entries (see Document creators).isSecure(): bool— whether the editor can display a file securely without downloading it to the browser.open(IToken $token): Response— return the page that renders the editor. This is called once, when the client opens the one-time URL. See Opening a file.
Registering the editor
Register your editor in lib/AppInfo/Application.php by listening for
RegisterDirectEditorEvent and calling register() on it:
<?php
declare(strict_types=1);
namespace OCA\MyApp\AppInfo;
use OCA\MyApp\DirectEditing\MyEditor;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\DirectEditing\RegisterDirectEditorEvent;
class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
$context->registerEventListener(
RegisterDirectEditorEvent::class,
RegisterMyEditorListener::class,
);
}
public function boot(IBootContext $context): void {
}
}
The listener registers the editor instance on the manager carried by the event:
<?php
declare(strict_types=1);
namespace OCA\MyApp\Listener;
use OCA\MyApp\DirectEditing\MyEditor;
use OCP\DirectEditing\RegisterDirectEditorEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<RegisterDirectEditorEvent> */
class RegisterMyEditorListener implements IEventListener {
public function __construct(private MyEditor $editor) {
}
public function handle(Event $event): void {
if (!$event instanceof RegisterDirectEditorEvent) {
return;
}
$event->register($this->editor);
}
}
Note
text provides a reference implementation. See lib/DirectEditing/ in the
nextcloud/text repository.
Document creators
Creators produce the “Create a new …” entries shown by clients (for example
“New text document”). Each creator is returned from IEditor::getCreators().
Extend OCP\DirectEditing\ACreateEmpty for a plain creator:
getId(): string— unique id of the creator, e.g.textdocument.getName(): string— descriptive name, e.g.New text document.getExtension(): string— default file extension, e.g..md.getMimetype(): string— mimetype of the created file.create(File $file, ?string $creatorId = null, ?string $templateId = null): void— optionally prefill the freshly created file with content.
Extend OCP\DirectEditing\ACreateFromTemplate (which itself extends
ACreateEmpty) to additionally offer templates:
getTemplates(): ATemplate[]— the templates available for this creator.
Each template extends OCP\DirectEditing\ATemplate and exposes getId(),
getTitle() and a preview image link. Whether a creator offers templates is
advertised automatically (templates: true) when it is an instance of
ACreateFromTemplate.
Opening a file
When the client opens the one-time URL, the server resolves the token and calls
IEditor::open(IToken $token). Your implementation returns a Response that
renders the editor for the file behind the token. OCP\DirectEditing\IToken
gives you the context you need:
getFile(): File— the file to edit.getEditor(): string— the editor id the token was issued for.getUser(): string— the user the token belongs to.useTokenScope(): void— run subsequent file access within the token’s scope.extend(): void— extend the token validity.invalidate(): void— invalidate the token.hasBeenAccessed(): bool— whether the token has already been used.
The one-time token is only meant to bootstrap the editor. After open() has
rendered the page, the editor app must take over with its own token/session
handling for editing and saving (this mirrors how Collabora switches from a
one-time token to a regular WOPI token).
HTTP API
All endpoints are OCS endpoints under:
/ocs/v2.php/apps/files/api/v1/directEditing
As with every OCS request, send the OCS-APIRequest: true header and
authenticate the user. Add ?format=json to receive JSON. All examples below
omit boilerplate for brevity.
List editors and creators
GET /directEditing
Returns the available editors and creators. Clients use this for discovery before offering create/open actions. The response carries an ETag; clients should cache the result and re-request only when the ETag changes.
curl 'https://cloud.example.com/ocs/v2.php/apps/files/api/v1/directEditing?format=json' \
-u user:password \
-H 'OCS-APIRequest: true'
{
"ocs": {
"meta": { "status": "ok", "statuscode": 200, "message": "OK" },
"data": {
"editors": {
"text": {
"id": "text",
"name": "Nextcloud Text",
"mimetypes": ["text/markdown"],
"optionalMimetypes": ["text/plain"],
"secure": false
}
},
"creators": {
"textdocument": {
"id": "textdocument",
"editor": "text",
"name": "New text document",
"extension": ".md",
"templates": false,
"mimetype": "text/markdown"
}
}
}
}
}
List templates
GET /directEditing/templates/{editorId}/{creatorId}
Returns the templates for a creator that advertises templates: true.
curl 'https://cloud.example.com/ocs/v2.php/apps/files/api/v1/directEditing/templates/text/textdocumenttemplate?format=json' \
-u user:password \
-H 'OCS-APIRequest: true'
{
"ocs": {
"meta": { "status": "ok", "statuscode": 200, "message": "OK" },
"data": {
"templates": {
"1": { "id": "1", "title": "Weekly ToDo", "preview": "https://…" },
"2": { "id": "2", "title": "Meeting notes", "preview": "https://…" }
}
}
}
}
Create a file
POST /directEditing/create
Creates a new file and returns the one-time URL to edit it.
Parameters:
path(required) — target path of the new file.editorId(required) — editor to open the file in.creatorId(required) — creator to use for the new file.templateId(optional) — template to base the file on.
curl -X POST 'https://cloud.example.com/ocs/v2.php/apps/files/api/v1/directEditing/create?path=/foo.md&editorId=text&creatorId=textdocument&format=json' \
-u user:password \
-H 'OCS-APIRequest: true'
{
"ocs": {
"meta": { "status": "ok", "statuscode": 200, "message": "OK" },
"data": {
"url": "https://cloud.example.com/index.php/apps/files/directEditing/<token>"
}
}
}
Open a file
POST /directEditing/open
Returns a one-time URL to open an existing file.
Parameters:
path(required) — path of the file to open.editorId(optional) — editor to open the file in; if omitted, the default editor for the file’s mimetype is used.fileId(optional) — open by file id instead of path.
curl -X POST 'https://cloud.example.com/ocs/v2.php/apps/files/api/v1/directEditing/open?path=/foo.md&editorId=text&format=json' \
-u user:password \
-H 'OCS-APIRequest: true'
The response has the same shape as create — a data.url pointing at
/index.php/apps/files/directEditing/<token>.
Opening the editor
The returned URL is a one-time, unauthenticated webview URL:
GET /index.php/apps/files/directEditing/<token>
Loading it consumes the token and triggers IEditor::open() on the server,
which renders the editor page. Open this URL in the client’s webview.
Error responses follow the OCS convention. A disabled direct editing setup
returns HTTP 500 with a message; a failed create/open returns HTTP 403.
Webview integration
Once the editor page is loaded in the client’s webview, the page and the native
client exchange messages. This is a convention shared by the first-party editors
(text, richdocuments, whiteboard, eurooffice) and the Nextcloud
Android and iOS apps; it is not enforced by the server.
The interface object
The native client injects a named interface object, DirectEditingMobileInterface,
into the page, and the editor page sends messages through it. A page should
feature-detect the object before using it.
Two transports are used, picked by which one the host provides:
Android — injected interface (direct method call). The interface object exposes one method per message name. Arguments, if any, are passed as a single JSON string:
// no arguments
window.DirectEditingMobileInterface.close()
// with arguments
window.DirectEditingMobileInterface.hyperlink(JSON.stringify(values))
iOS — script message handler. All messages go through one postMessage
handler. A message with arguments is sent as an object with MessageName and
Values; a message without arguments is sent as the bare name string:
// no arguments
window.webkit.messageHandlers.DirectEditingMobileInterface.postMessage('close')
// with arguments
window.webkit.messageHandlers.DirectEditingMobileInterface.postMessage({
MessageName: 'hyperlink',
Values: values,
})
So a native client implements both shapes: a named method that receives a JSON
string (Android), and a single handler that switches on MessageName and reads
the Values object (iOS).
Messages: editor page → native client
The following messages are sent by the editor page.
Message |
Payload |
Meaning |
|---|---|---|
|
— |
Editor is starting to load. |
|
— |
Editor finished loading. |
|
— |
Deprecated since Nextcloud 34, use |
|
— |
The editor requests the webview to be closed. |
|
— |
Open the native share UI for the file. |
|
— |
The editor requests a reload (e.g. after session invalidation). |
|
— |
Trigger a native paste into the editor. |
|
— |
Ask the client to pick an image to insert (see |
|
object |
Download/export the file. Values: |
|
object |
The file was renamed. Values: |
|
object |
Open a hyperlink natively. Values: |
Note
The argument keys above are defined by the editor. downloadAs uses URL
(all caps) while hyperlink uses Url — mind the casing when parsing.
Calls: native client → editor page
The native client can call back into the page with evaluateJavascript. This is
currently used only by richdocuments, which exposes methods on
OCA.RichDocuments.documentsMain:
postAsset(filename, url)— insert an image the user selected natively. This is the reply to theinsertGraphicmessage: the client opens its picker, uploads/creates the asset, then callspostAssetwith the resulting file name and URL.postGrabFocus()— re-focus the editor after the native UI (picker, share sheet, app switch) is dismissed. Clients should feature-detect it, e.g.if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined').
Implementation notes
Editors should send only the messages they need and clients should ignore unknown ones. Because the page feature-detects the interface method (Android) and the client switches on the message name (iOS), unsupported messages are dropped silently rather than raising errors.
Coverage is not uniform across platforms today: for example,
loadingis a no-op or unhandled on some clients, andreloadis not handled on all of them. Do not rely on every message being acted on by every client.When adding a new message, document it here and implement it on both transports and both apps to keep behaviour consistent.
See also
OCP\DirectEditing\IEditorand the otherOCP\DirectEditing\*interfaces.The
RegisterDirectEditorEvententry in the list of events.Reference editor implementation:
lib/DirectEditing/in nextcloud/text.