From 88d618b58a7c2b8e1b94634e9d63c32d19e436ce Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 11 Sep 2026 12:19:44 +0000 Subject: [PATCH] htmlviewer-directediting: HTML rendering in Nextcloud iOS app - htmlviewer: IEditor + Bridge + srcdoc iframe template + content controller - text app: open() bridges text/html to htmlviewer template - idempotent install.sh for re-deployment after app updates - iPhone-tested 2026-09-11 --- DirectController.php | 58 ++++++ HtmlDirectEditor.php | 88 +++++++++ HtmlDirectEditorBridge.php | 44 +++++ LICENSE | 17 ++ README.md | 56 ++++++ RegisterDirectEditorListener.php | 32 ++++ TextDirectEditor-current.php | 171 ++++++++++++++++++ TextDirectEditor-orig.php | 164 +++++++++++++++++ directEditing.php | 37 ++++ install.sh | 77 ++++++++ patch_app.py | 26 +++ patches/htmlviewer/DirectController.php | 58 ++++++ patches/htmlviewer/HtmlDirectEditor.php | 88 +++++++++ patches/htmlviewer/HtmlDirectEditorBridge.php | 44 +++++ .../RegisterDirectEditorListener.php | 32 ++++ patches/htmlviewer/directEditing-template.php | 37 ++++ patches/htmlviewer/patch_app.py | 26 +++ patches/text/TextDirectEditor.php | 171 ++++++++++++++++++ testde.php | 10 + testde2.php | 9 + 20 files changed, 1245 insertions(+) create mode 100644 DirectController.php create mode 100644 HtmlDirectEditor.php create mode 100644 HtmlDirectEditorBridge.php create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RegisterDirectEditorListener.php create mode 100644 TextDirectEditor-current.php create mode 100644 TextDirectEditor-orig.php create mode 100644 directEditing.php create mode 100755 install.sh create mode 100644 patch_app.py create mode 100644 patches/htmlviewer/DirectController.php create mode 100644 patches/htmlviewer/HtmlDirectEditor.php create mode 100644 patches/htmlviewer/HtmlDirectEditorBridge.php create mode 100644 patches/htmlviewer/RegisterDirectEditorListener.php create mode 100644 patches/htmlviewer/directEditing-template.php create mode 100644 patches/htmlviewer/patch_app.py create mode 100644 patches/text/TextDirectEditor.php create mode 100644 testde.php create mode 100644 testde2.php diff --git a/DirectController.php b/DirectController.php new file mode 100644 index 0000000..79337ec --- /dev/null +++ b/DirectController.php @@ -0,0 +1,58 @@ +userSession->getUser(); + if ($user === null) { + return new NotFoundResponse(); + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $nodes = $userFolder->getById($fileId); + if (empty($nodes)) { + return new NotFoundResponse(); + } + $file = array_shift($nodes); + if (!$file instanceof File) { + return new NotFoundResponse(); + } + + $response = new DataDisplayResponse($file->getContent()); + $response->addHeader('Content-Type', 'text/html; charset=utf-8'); + return $response; + } +} diff --git a/HtmlDirectEditor.php b/HtmlDirectEditor.php new file mode 100644 index 0000000..bb178ea --- /dev/null +++ b/HtmlDirectEditor.php @@ -0,0 +1,88 @@ +l10n->t('HTML Viewer'); + } + + #[\Override] + public function getMimetypes(): array { + return [ + 'text/html', + ]; + } + + #[\Override] + public function getMimetypesOptional(): array { + return []; + } + + #[\Override] + public function getCreators(): array { + return []; + } + + #[\Override] + public function isSecure(): bool { + return false; + } + + #[\Override] + public function open(IToken $token): Response { + $token->useTokenScope(); + + try { + $file = $token->getFile(); + + return new TemplateResponse( + Application::APP_ID, + 'directEditing', + [ + 'fileId' => $file->getId(), + 'fileName' => $file->getName(), + 'fileContent' => $file->getContent(), + ], + 'base' + ); + } catch (InvalidPathException|NotFoundException) { + return new NotFoundResponse(); + } + } +} diff --git a/HtmlDirectEditorBridge.php b/HtmlDirectEditorBridge.php new file mode 100644 index 0000000..bd01563 --- /dev/null +++ b/HtmlDirectEditorBridge.php @@ -0,0 +1,44 @@ +getFile(); + + return new TemplateResponse( + Application::APP_ID, + 'directEditing', + [ + 'fileId' => $file->getId(), + 'fileName' => $file->getName(), + 'fileContent' => $file->getContent(), + ], + 'base' + ); + } catch (\Throwable) { + return new NotFoundResponse(); + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..01105fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +MIT License + +Copyright (c) 2026 Hermes Agent / Niklas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +Note: patches/htmlviewer is derived from the Nextcloud htmlviewer app (AGPL-3.0, +by Marius David Wieschollek) and patches/text/TextDirectEditor.php is derived +from the Nextcloud text app (AGPL-3.0). Those files remain AGPL-3.0. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..676e63b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# htmlviewer-directediting + +Rendert HTML-Dateien in der **Nextcloud iOS/Android App** statt Quelltext (QuickLook) anzuzeigen. + +## Warum das existiert + +Die Mobile-Apps wählen den Editor für eine Datei über die Server-Capabilities +(`/ocs/v2.php/apps/files/api/v1/directEditing`) und haben eine **hartcodierte +Editor-Registry** (`NCDirectEditorAdapter` in nextcloud/ios): nur `text`, +`eurooffice`, `onlyoffice`, `richdocuments`, `whiteboard` sind bekannt. Ein neuer +Server-Editor mit eigener ID (z.B. `htmlviewer`) wird von der App nicht aufgelöst +→ Fallback auf QuickLook → HTML-Quelltext. + +## Lösung (2 Patches) + +1. **htmlviewer** (custom_apps): registriert einen `IEditor` (`HtmlDirectEditor`) + mit Mimetype `text/html` + Bridge-Klasse, Template mit sandboxed iframe + (`srcdoc` — Inhalt direkt eingebettet, kein Cookie-Problem in mobilen + WebViews) und Content-Controller. +2. **text** (Core-App, gepatcht): + - `open()` leitet `text/html` an die htmlviewer-Bridge um + (`HtmlDirectEditorBridge::openHtml`) — die iOS-App wählt Editor `text` + (registry-bekannt), der Server liefert aber das HTML-Template. + - `text/html` bleibt in der Mimetypeliste der Text-App (wichtig!). + +Bei Nextcloud-Core selbst (lib/, config) wurde **nichts** geändert. + +## Installation + +```bash +./install.sh nextcloud1-app-1 +``` + +Idempotent, kann nach jedem htmlviewer-/Nextcloud-Update neu laufen. +Backup des Original-TextDirectEditor landet unter `/tmp/TextDirectEditor.php.bak-htmlproject` im Container. + +## Nach App-Updates neu nötig + +- htmlviewer-App-Update überschreibt `lib/DirectEditing/`, `templates/`, `Application.php`-Registrierung → `install.sh` erneut +- Nextcloud-Update überschreibt `apps/text/.../TextDirectEditor.php` → `install.sh` erneut + +## Dateien + +- `patches/htmlviewer/HtmlDirectEditor.php` — IEditor-Implementierung (DirectEditing-Registrierung, für Web-Client/Wiederverwendung) +- `patches/htmlviewer/HtmlDirectEditorBridge.php` — statische Bridge, von TextDirectEditor aufgerufen +- `patches/htmlviewer/DirectController.php` — `/apps/htmlviewer/direct/{fileId}` Content-Endpoint (NoAdminRequired, Session) +- `patches/htmlviewer/RegisterDirectEditorListener.php` — RegisterDirectEditorEvent-Listener +- `patches/htmlviewer/directEditing-template.php` — Template: Fullscreen-iframe mit srcdoc + sandbox +- `patches/htmlviewer/patch_app.py` — idempotente Registrierung in Application.php + routes.php +- `patches/text/TextDirectEditor.php` — gepatchte Text-App (open()-Bridge) +- `install.sh` — Installer + +## Status (2026-09-11) + +Auf nextcloud1 (Nextcloud 34.0.2, htmlviewer 33.0.0) deployed und iPhone-getestet: +HTML rendert in der iOS-App in sandboxed WebView. \ No newline at end of file diff --git a/RegisterDirectEditorListener.php b/RegisterDirectEditorListener.php new file mode 100644 index 0000000..fca33f4 --- /dev/null +++ b/RegisterDirectEditorListener.php @@ -0,0 +1,32 @@ + */ +final class RegisterDirectEditorListener implements IEventListener { + + public function __construct( + private HtmlDirectEditor $editor, + ) { + } + + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof RegisterDirectEditorEvent) { + return; + } + $event->register($this->editor); + } +} diff --git a/TextDirectEditor-current.php b/TextDirectEditor-current.php new file mode 100644 index 0000000..13f3438 --- /dev/null +++ b/TextDirectEditor-current.php @@ -0,0 +1,171 @@ +l10n = $l10n; + $this->initialStateProvider = $initialStateProvider; + $this->apiService = $apiService; + $this->appConfig = $appConfig; + } + + /** + * Return a unique identifier for the editor + * + * e.g. richdocuments + * + * @return string + */ + public function getId(): string { + return Application::APP_NAME; + } + + /** + * Return a readable name for the editor + * + * e.g. Collabora Online + * + * @return string + */ + public function getName(): string { + return $this->l10n->t('Nextcloud Text'); + } + + /** + * A list of mimetypes that should open the editor by default + * + * @return string[] + */ + public function getMimetypes(): array { + return [ + 'text/markdown', + 'text/plain', + 'application/cmd', + 'application/x-empty', + 'application/x-msdos-program', + 'application/javascript', + 'application/json', + 'application/x-perl', + 'application/x-php', + 'application/x-tex', + 'application/xml', + 'application/yaml', + 'text/css', + 'text/csv', + + 'text/org', + 'text/x-c', + 'text/x-c++src', + 'text/x-h', + 'text/x-java-source', + 'text/x-ldif', + 'text/x-nfo', + 'text/x-python', + 'text/x-shellscript', + ]; + } + + /** + * A list of mimetypes that can be opened in the editor optionally + * + * @return string[] + */ + public function getMimetypesOptional(): array { + return []; + } + + /** + * Return a list of file creation options to be presented to the user + * + * @return TextDocumentCreator[] + */ + public function getCreators(): array { + return [ + new TextDocumentCreator($this->l10n, $this->appConfig), + ]; + } + + /** + * Return if the view is able to securely view a file without downloading it to the browser + * + * @return bool + */ + public function isSecure(): bool { + return false; + } + + /** + * Return a template response for displaying the editor + * + * open can only be called once when the client requests the editor with a one-time-use token + * For handling editing and later requests, editors need to impelement their own token handling and take care of invalidation + * + * This behavior is similar to the current direct editing implementation in collabora where we generate a one-time token and switch over to the regular wopi token for the actual editing/saving process + * + * @param IToken $token + * @return Response + */ + public function open(IToken $token): Response { + $token->useTokenScope(); + try { + // HTML patch (Hermes Agent 2026-09): route text/html to the + // htmlviewer sandbox template, because the iOS/Android apps + // only know the hard-coded editor id "text" and would + // otherwise show HTML source in QuickLook. + if ($token->getFile()->getMimeType() === 'text/html') { + return \OCA\HtmlViewer\DirectEditing\HtmlDirectEditorBridge::openHtml($token); + } + $session = $this->apiService->create($token->getFile()->getId()); + $this->initialStateProvider->provideFile([ + 'fileId' => $token->getFile()->getId(), + 'mimetype' => $token->getFile()->getMimeType(), + 'session' => \json_encode($session->getData()) + ]); + $this->initialStateProvider->provideDirectEditToken($token->getToken()); + $this->initialStateProvider->provideState(); + Util::addScript('text', 'text-text'); + Util::addStyle('text', 'text-text'); + return new TemplateResponse('text', 'main', [], 'base'); + } catch (InvalidPathException $e) { + } catch (NotFoundException $e) { + } catch (NotPermittedException $e) { + } + return new NotFoundResponse(); + } +} diff --git a/TextDirectEditor-orig.php b/TextDirectEditor-orig.php new file mode 100644 index 0000000..3c62ee2 --- /dev/null +++ b/TextDirectEditor-orig.php @@ -0,0 +1,164 @@ +l10n = $l10n; + $this->initialStateProvider = $initialStateProvider; + $this->apiService = $apiService; + $this->appConfig = $appConfig; + } + + /** + * Return a unique identifier for the editor + * + * e.g. richdocuments + * + * @return string + */ + public function getId(): string { + return Application::APP_NAME; + } + + /** + * Return a readable name for the editor + * + * e.g. Collabora Online + * + * @return string + */ + public function getName(): string { + return $this->l10n->t('Nextcloud Text'); + } + + /** + * A list of mimetypes that should open the editor by default + * + * @return string[] + */ + public function getMimetypes(): array { + return [ + 'text/markdown', + 'text/plain', + 'application/cmd', + 'application/x-empty', + 'application/x-msdos-program', + 'application/javascript', + 'application/json', + 'application/x-perl', + 'application/x-php', + 'application/x-tex', + 'application/xml', + 'application/yaml', + 'text/css', + 'text/csv', + 'text/html', + 'text/org', + 'text/x-c', + 'text/x-c++src', + 'text/x-h', + 'text/x-java-source', + 'text/x-ldif', + 'text/x-nfo', + 'text/x-python', + 'text/x-shellscript', + ]; + } + + /** + * A list of mimetypes that can be opened in the editor optionally + * + * @return string[] + */ + public function getMimetypesOptional(): array { + return []; + } + + /** + * Return a list of file creation options to be presented to the user + * + * @return TextDocumentCreator[] + */ + public function getCreators(): array { + return [ + new TextDocumentCreator($this->l10n, $this->appConfig), + ]; + } + + /** + * Return if the view is able to securely view a file without downloading it to the browser + * + * @return bool + */ + public function isSecure(): bool { + return false; + } + + /** + * Return a template response for displaying the editor + * + * open can only be called once when the client requests the editor with a one-time-use token + * For handling editing and later requests, editors need to impelement their own token handling and take care of invalidation + * + * This behavior is similar to the current direct editing implementation in collabora where we generate a one-time token and switch over to the regular wopi token for the actual editing/saving process + * + * @param IToken $token + * @return Response + */ + public function open(IToken $token): Response { + $token->useTokenScope(); + try { + $session = $this->apiService->create($token->getFile()->getId()); + $this->initialStateProvider->provideFile([ + 'fileId' => $token->getFile()->getId(), + 'mimetype' => $token->getFile()->getMimeType(), + 'session' => \json_encode($session->getData()) + ]); + $this->initialStateProvider->provideDirectEditToken($token->getToken()); + $this->initialStateProvider->provideState(); + Util::addScript('text', 'text-text'); + Util::addStyle('text', 'text-text'); + return new TemplateResponse('text', 'main', [], 'base'); + } catch (InvalidPathException $e) { + } catch (NotFoundException $e) { + } catch (NotPermittedException $e) { + } + return new NotFoundResponse(); + } +} diff --git a/directEditing.php b/directEditing.php new file mode 100644 index 0000000..eb7b78b --- /dev/null +++ b/directEditing.php @@ -0,0 +1,37 @@ + + + + \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..d4845f0 --- /dev/null +++ b/install.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# htmlviewer-directediting installer +# Applies the HTML-in-iOS-App patches to a Nextcloud container. +# Usage: ./install.sh [custom_apps-path-in-container] +# default container: nextcloud1-app-1 +# Idempotent: safe to re-run after app updates. +set -euo pipefail + +CONTAINER="${1:-nextcloud1-app-1}" +APP="/var/www/html/custom_apps/htmlviewer" +TEXT="/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "=== htmlviewer-directediting installer ===" +echo "Container: $CONTAINER" + +# 0. Sanity +docker exec "$CONTAINER" test -d "$APP" || { echo "ERROR: htmlviewer app not found at $APP"; exit 1; } + +# 1. Backup text editor (first run only) +if ! docker exec "$CONTAINER" test -f /tmp/TextDirectEditor.php.bak-htmlproject; then + docker exec "$CONTAINER" cp "$TEXT" /tmp/TextDirectEditor.php.bak-htmlproject 2>/dev/null || \ + docker exec "$CONTAINER" bash -c "cp $TEXT /tmp/TextDirectEditor.php.bak-htmlproject" || true +fi + +# 2. Deploy htmlviewer files (from repo to container via /tmp) +TMPDIR_ON_HOST=$(mktemp -d) +cp patches/htmlviewer/*.php "$TMPDIR_ON_HOST/" +cp patches/text/TextDirectEditor.php "$TMPDIR_ON_HOST/TextDirectEditor-patched.php" + +docker cp "$TMPDIR_ON_HOST/HtmlDirectEditor.php" "$CONTAINER:$APP/lib/DirectEditing/HtmlDirectEditor.php" +docker cp "$TMPDIR_ON_HOST/HtmlDirectEditorBridge.php" "$CONTAINER:$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" +docker cp "$TMPDIR_ON_HOST/DirectController.php" "$CONTAINER:$APP/lib/Controller/DirectController.php" +docker cp "$TMPDIR_ON_HOST/RegisterDirectEditorListener.php" "$CONTAINER:$APP/lib/Listeners/RegisterDirectEditorListener.php" +docker cp "$TMPDIR_ON_HOST/directEditing-template.php" "$CONTAINER:$APP/templates/directEditing.php" +docker cp "$TMPDIR_ON_HOST/TextDirectEditor-patched.php" "$CONTAINER:/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php" +docker exec "$CONTAINER" chown www-data:www-data \ + "$APP/lib/DirectEditing/HtmlDirectEditor.php" \ + "$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \ + "$APP/lib/Controller/DirectController.php" \ + "$APP/lib/Listeners/RegisterDirectEditorListener.php" \ + "$APP/templates/directEditing.php" \ + /var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php +rm -rf "$TMPDIR_ON_HOST" + +# 3. Register in Application.php + routes.php (idempotent) +docker cp patches/htmlviewer/patch_app.py "$CONTAINER:/tmp/patch_app.py" +docker exec "$CONTAINER" python3 /tmp/patch_app.py + +# 4. Text-app mimic list: remove text/html so iOS picks... (NO — text stays owner) +# NOTE: text/html must STAY in the text editor's mimetypes list so the +# iOS app (which only knows editor id "text") opens HTML via the bridge. +# If it was removed by an older version of this installer, re-add it. +docker exec "$CONTAINER" python3 - <<'PYEOF' +p='/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php' +c=open(p).read() +if "'text/html'" not in c: + c=c.replace("\t\t\t'text/css',", "\t\t\t'text/css',\n\t\t\t'text/html',") + open(p,'w').write(c) + print('text/html re-added to TextDirectEditor mimetypes') +else: + print('text/html present in TextDirectEditor') +PYEOF + +# 5. Lint + restart +for f in "$APP/lib/DirectEditing/HtmlDirectEditor.php" \ + "$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \ + "$APP/lib/Controller/DirectController.php" \ + "$APP/lib/Listeners/RegisterDirectEditorListener.php" \ + "$APP/templates/directEditing.php" \ + /var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php; do + docker exec "$CONTAINER" php -l "$f" +done + +echo "=== Restarting container ===" +docker restart "$CONTAINER" +echo "=== Done. Test: open an .html file in the iOS app (kill app first). ===" \ No newline at end of file diff --git a/patch_app.py b/patch_app.py new file mode 100644 index 0000000..f47358b --- /dev/null +++ b/patch_app.py @@ -0,0 +1,26 @@ +import re +APP='/media/orange/RocketChat/nextcloud1/nextcloud_data/custom_apps/htmlviewer' + +p=APP+'/lib/AppInfo/Application.php' +c=open(p).read() +if 'RegisterDirectEditorListener' not in c: + c=c.replace( + 'use OCP\\Security\\CSP\\AddContentSecurityPolicyEvent;', + 'use OCP\\Security\\CSP\\AddContentSecurityPolicyEvent;\nuse OCA\\HtmlViewer\\Listener\\RegisterDirectEditorListener;\nuse OCP\\DirectEditing\\RegisterDirectEditorEvent;' + ) + c=c.replace( + " $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);", + " $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);\n $context->registerEventListener(RegisterDirectEditorEvent::class, RegisterDirectEditorListener::class);" + ) + open(p,'w').write(c) +print('Application.php patched:', 'RegisterDirectEditorEvent' in open(p).read()) + +p=APP+'/appinfo/routes.php' +c=open(p).read() +if 'Direct#content' not in c: + c=c.replace( + "['name' => 'settings#disableWarning', 'url' => '/settings/warning', 'verb' => 'GET'],", + "['name' => 'settings#disableWarning', 'url' => '/settings/warning', 'verb' => 'GET'],\n\t\t['name' => 'Direct#content', 'url' => '/direct/{fileId}', 'verb' => 'GET']," + ) + open(p,'w').write(c) +print('routes.php patched:', 'Direct#content' in open(p).read()) diff --git a/patches/htmlviewer/DirectController.php b/patches/htmlviewer/DirectController.php new file mode 100644 index 0000000..79337ec --- /dev/null +++ b/patches/htmlviewer/DirectController.php @@ -0,0 +1,58 @@ +userSession->getUser(); + if ($user === null) { + return new NotFoundResponse(); + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $nodes = $userFolder->getById($fileId); + if (empty($nodes)) { + return new NotFoundResponse(); + } + $file = array_shift($nodes); + if (!$file instanceof File) { + return new NotFoundResponse(); + } + + $response = new DataDisplayResponse($file->getContent()); + $response->addHeader('Content-Type', 'text/html; charset=utf-8'); + return $response; + } +} diff --git a/patches/htmlviewer/HtmlDirectEditor.php b/patches/htmlviewer/HtmlDirectEditor.php new file mode 100644 index 0000000..bb178ea --- /dev/null +++ b/patches/htmlviewer/HtmlDirectEditor.php @@ -0,0 +1,88 @@ +l10n->t('HTML Viewer'); + } + + #[\Override] + public function getMimetypes(): array { + return [ + 'text/html', + ]; + } + + #[\Override] + public function getMimetypesOptional(): array { + return []; + } + + #[\Override] + public function getCreators(): array { + return []; + } + + #[\Override] + public function isSecure(): bool { + return false; + } + + #[\Override] + public function open(IToken $token): Response { + $token->useTokenScope(); + + try { + $file = $token->getFile(); + + return new TemplateResponse( + Application::APP_ID, + 'directEditing', + [ + 'fileId' => $file->getId(), + 'fileName' => $file->getName(), + 'fileContent' => $file->getContent(), + ], + 'base' + ); + } catch (InvalidPathException|NotFoundException) { + return new NotFoundResponse(); + } + } +} diff --git a/patches/htmlviewer/HtmlDirectEditorBridge.php b/patches/htmlviewer/HtmlDirectEditorBridge.php new file mode 100644 index 0000000..bd01563 --- /dev/null +++ b/patches/htmlviewer/HtmlDirectEditorBridge.php @@ -0,0 +1,44 @@ +getFile(); + + return new TemplateResponse( + Application::APP_ID, + 'directEditing', + [ + 'fileId' => $file->getId(), + 'fileName' => $file->getName(), + 'fileContent' => $file->getContent(), + ], + 'base' + ); + } catch (\Throwable) { + return new NotFoundResponse(); + } + } +} \ No newline at end of file diff --git a/patches/htmlviewer/RegisterDirectEditorListener.php b/patches/htmlviewer/RegisterDirectEditorListener.php new file mode 100644 index 0000000..fca33f4 --- /dev/null +++ b/patches/htmlviewer/RegisterDirectEditorListener.php @@ -0,0 +1,32 @@ + */ +final class RegisterDirectEditorListener implements IEventListener { + + public function __construct( + private HtmlDirectEditor $editor, + ) { + } + + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof RegisterDirectEditorEvent) { + return; + } + $event->register($this->editor); + } +} diff --git a/patches/htmlviewer/directEditing-template.php b/patches/htmlviewer/directEditing-template.php new file mode 100644 index 0000000..eb7b78b --- /dev/null +++ b/patches/htmlviewer/directEditing-template.php @@ -0,0 +1,37 @@ + + + + \ No newline at end of file diff --git a/patches/htmlviewer/patch_app.py b/patches/htmlviewer/patch_app.py new file mode 100644 index 0000000..f47358b --- /dev/null +++ b/patches/htmlviewer/patch_app.py @@ -0,0 +1,26 @@ +import re +APP='/media/orange/RocketChat/nextcloud1/nextcloud_data/custom_apps/htmlviewer' + +p=APP+'/lib/AppInfo/Application.php' +c=open(p).read() +if 'RegisterDirectEditorListener' not in c: + c=c.replace( + 'use OCP\\Security\\CSP\\AddContentSecurityPolicyEvent;', + 'use OCP\\Security\\CSP\\AddContentSecurityPolicyEvent;\nuse OCA\\HtmlViewer\\Listener\\RegisterDirectEditorListener;\nuse OCP\\DirectEditing\\RegisterDirectEditorEvent;' + ) + c=c.replace( + " $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);", + " $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);\n $context->registerEventListener(RegisterDirectEditorEvent::class, RegisterDirectEditorListener::class);" + ) + open(p,'w').write(c) +print('Application.php patched:', 'RegisterDirectEditorEvent' in open(p).read()) + +p=APP+'/appinfo/routes.php' +c=open(p).read() +if 'Direct#content' not in c: + c=c.replace( + "['name' => 'settings#disableWarning', 'url' => '/settings/warning', 'verb' => 'GET'],", + "['name' => 'settings#disableWarning', 'url' => '/settings/warning', 'verb' => 'GET'],\n\t\t['name' => 'Direct#content', 'url' => '/direct/{fileId}', 'verb' => 'GET']," + ) + open(p,'w').write(c) +print('routes.php patched:', 'Direct#content' in open(p).read()) diff --git a/patches/text/TextDirectEditor.php b/patches/text/TextDirectEditor.php new file mode 100644 index 0000000..13f3438 --- /dev/null +++ b/patches/text/TextDirectEditor.php @@ -0,0 +1,171 @@ +l10n = $l10n; + $this->initialStateProvider = $initialStateProvider; + $this->apiService = $apiService; + $this->appConfig = $appConfig; + } + + /** + * Return a unique identifier for the editor + * + * e.g. richdocuments + * + * @return string + */ + public function getId(): string { + return Application::APP_NAME; + } + + /** + * Return a readable name for the editor + * + * e.g. Collabora Online + * + * @return string + */ + public function getName(): string { + return $this->l10n->t('Nextcloud Text'); + } + + /** + * A list of mimetypes that should open the editor by default + * + * @return string[] + */ + public function getMimetypes(): array { + return [ + 'text/markdown', + 'text/plain', + 'application/cmd', + 'application/x-empty', + 'application/x-msdos-program', + 'application/javascript', + 'application/json', + 'application/x-perl', + 'application/x-php', + 'application/x-tex', + 'application/xml', + 'application/yaml', + 'text/css', + 'text/csv', + + 'text/org', + 'text/x-c', + 'text/x-c++src', + 'text/x-h', + 'text/x-java-source', + 'text/x-ldif', + 'text/x-nfo', + 'text/x-python', + 'text/x-shellscript', + ]; + } + + /** + * A list of mimetypes that can be opened in the editor optionally + * + * @return string[] + */ + public function getMimetypesOptional(): array { + return []; + } + + /** + * Return a list of file creation options to be presented to the user + * + * @return TextDocumentCreator[] + */ + public function getCreators(): array { + return [ + new TextDocumentCreator($this->l10n, $this->appConfig), + ]; + } + + /** + * Return if the view is able to securely view a file without downloading it to the browser + * + * @return bool + */ + public function isSecure(): bool { + return false; + } + + /** + * Return a template response for displaying the editor + * + * open can only be called once when the client requests the editor with a one-time-use token + * For handling editing and later requests, editors need to impelement their own token handling and take care of invalidation + * + * This behavior is similar to the current direct editing implementation in collabora where we generate a one-time token and switch over to the regular wopi token for the actual editing/saving process + * + * @param IToken $token + * @return Response + */ + public function open(IToken $token): Response { + $token->useTokenScope(); + try { + // HTML patch (Hermes Agent 2026-09): route text/html to the + // htmlviewer sandbox template, because the iOS/Android apps + // only know the hard-coded editor id "text" and would + // otherwise show HTML source in QuickLook. + if ($token->getFile()->getMimeType() === 'text/html') { + return \OCA\HtmlViewer\DirectEditing\HtmlDirectEditorBridge::openHtml($token); + } + $session = $this->apiService->create($token->getFile()->getId()); + $this->initialStateProvider->provideFile([ + 'fileId' => $token->getFile()->getId(), + 'mimetype' => $token->getFile()->getMimeType(), + 'session' => \json_encode($session->getData()) + ]); + $this->initialStateProvider->provideDirectEditToken($token->getToken()); + $this->initialStateProvider->provideState(); + Util::addScript('text', 'text-text'); + Util::addStyle('text', 'text-text'); + return new TemplateResponse('text', 'main', [], 'base'); + } catch (InvalidPathException $e) { + } catch (NotFoundException $e) { + } catch (NotPermittedException $e) { + } + return new NotFoundResponse(); + } +} diff --git a/testde.php b/testde.php new file mode 100644 index 0000000..8ce3ded --- /dev/null +++ b/testde.php @@ -0,0 +1,10 @@ +get(\OCP\DirectEditing\IManager::class); +echo "manager ok\n"; +$ev = new \OCP\DirectEditing\RegisterDirectEditorEvent($m); +\OC::server->get(\OCP\EventDispatcher\IEventDispatcher::class)->dispatchTyped($ev); +echo "dispatched\n"; +var_dump(array_keys($m->getEditors())); \ No newline at end of file diff --git a/testde2.php b/testde2.php new file mode 100644 index 0000000..ac1ec86 --- /dev/null +++ b/testde2.php @@ -0,0 +1,9 @@ +get(\OCP\DirectEditing\IManager::class); +$ev = new \OCP\DirectEditing\RegisterDirectEditorEvent($m); +\OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class)->dispatchTyped($ev); +echo json_encode(array_keys($m->getEditors())), PHP_EOL; \ No newline at end of file