Core-clean variant: editor id eurooffice, no text-app patch

- HtmlDirectEditor getId() now returns 'eurooffice' (hard-coded in the
  iOS/Android editor registry) -> 'Öffnen mit Office' long-press renders HTML
- install.sh restores the original TextDirectEditor if a previous
  install patched it; no core app patch anymore
- README rewritten: behavior, variant rationale, files
This commit is contained in:
Hermes
2026-09-11 12:28:13 +00:00
parent 84d455d19c
commit 818c793ce6
4 changed files with 55 additions and 224 deletions
+31 -21
View File
@@ -1,6 +1,7 @@
# htmlviewer-directediting # htmlviewer-directediting
Rendert HTML-Dateien in der **Nextcloud iOS/Android App** statt Quelltext (QuickLook) anzuzeigen. Rendert HTML-Dateien in der **Nextcloud iOS/Android App** in einem sandboxed
WebView statt als Quelltext (QuickLook).
## Warum das existiert ## Warum das existiert
@@ -11,19 +12,20 @@ Editor-Registry** (`NCDirectEditorAdapter` in nextcloud/ios): nur `text`,
Server-Editor mit eigener ID (z.B. `htmlviewer`) wird von der App nicht aufgelöst Server-Editor mit eigener ID (z.B. `htmlviewer`) wird von der App nicht aufgelöst
→ Fallback auf QuickLook → HTML-Quelltext. → Fallback auf QuickLook → HTML-Quelltext.
## Lösung (2 Patches) ## Lösung (nur htmlviewer-App, core-clean)
1. **htmlviewer** (custom_apps): registriert einen `IEditor` (`HtmlDirectEditor`) Die htmlviewer-App registriert einen `IEditor` mit der hartcodeten App-Editor-ID
mit Mimetype `text/html` + Bridge-Klasse, Template mit sandboxed iframe **`eurooffice`** und Mimetype `text/html` + Bridge, Template mit sandboxed
(`srcdoc` — Inhalt direkt eingebettet, kein Cookie-Problem in mobilen iframe (`srcdoc` — Inhalt direkt eingebettet, kein Cookie-Problem in mobilen
WebViews) und Content-Controller. 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. Die Nextcloud-Text-App und der Core werden **nicht** verändert.
### Verhalten
- **Single-Tap** auf HTML: Quelltext-Ansicht (iOS-Standard, Text-App gewinnt
die Editor-Wahl — das ist ohne Text-App-Patch nicht änderbar, siehe unten)
- **Long-Press → "Öffnen mit Office"**: rendert das HTML in der WebView
## Installation ## Installation
@@ -32,25 +34,33 @@ Bei Nextcloud-Core selbst (lib/, config) wurde **nichts** geändert.
``` ```
Idempotent, kann nach jedem htmlviewer-/Nextcloud-Update neu laufen. Idempotent, kann nach jedem htmlviewer-/Nextcloud-Update neu laufen.
Backup des Original-TextDirectEditor landet unter `/tmp/TextDirectEditor.php.bak-htmlproject` im Container. Stellt ein altes Text-App-Patch-Backup automatisch wieder her, falls vorhanden.
## Nach App-Updates neu nötig ## Nach App-Updates neu nötig
- htmlviewer-App-Update überschreibt `lib/DirectEditing/`, `templates/`, `Application.php`-Registrierung → `install.sh` erneut - htmlviewer-App-Update überschreibt `lib/DirectEditing/`, `templates/`,
- Nextcloud-Update überschreibt `apps/text/.../TextDirectEditor.php``install.sh` erneut `Application.php`-Registrierung`install.sh` erneut
## Varianten
**Auto-Open bei Single-Tap** wäre nur möglich, wenn die Text-App gepatcht wird
(`open()`-Bridge auf die htmlviewer-Template + `text/html` in deren Mimetypen),
weil die iOS-App im automatischen Loop immer zuerst Editor `text` wählt und die
App-Registry nur die 5 hartcodeten IDs kennt. Das machen wir bewusst NICHT —
Core-App-Patches sind update-anfällig und unübersichtlich. Die Long-Press-
Variante funktioniert ausschließlich mit der installierten htmlviewer-App.
## Dateien ## Dateien
- `patches/htmlviewer/HtmlDirectEditor.php` — IEditor-Implementierung (DirectEditing-Registrierung, für Web-Client/Wiederverwendung) - `patches/htmlviewer/HtmlDirectEditor.php` — IEditor-Implementierung, ID `eurooffice`
- `patches/htmlviewer/HtmlDirectEditorBridge.php`statische Bridge, von TextDirectEditor aufgerufen - `patches/htmlviewer/HtmlDirectEditorBridge.php`Template-Renderer
- `patches/htmlviewer/DirectController.php``/apps/htmlviewer/direct/{fileId}` Content-Endpoint (NoAdminRequired, Session) - `patches/htmlviewer/DirectController.php``/apps/htmlviewer/direct/{fileId}` Content-Endpoint (NoAdminRequired, Session)
- `patches/htmlviewer/RegisterDirectEditorListener.php` — RegisterDirectEditorEvent-Listener - `patches/htmlviewer/RegisterDirectEditorListener.php` — RegisterDirectEditorEvent-Listener
- `patches/htmlviewer/directEditing-template.php` — Template: Fullscreen-iframe mit srcdoc + sandbox - `patches/htmlviewer/directEditing-template.php` — Template: Fullscreen-iframe mit srcdoc + sandbox
- `patches/htmlviewer/patch_app.py` — idempotente Registrierung in Application.php + routes.php - `patches/htmlviewer/patch_app.py` — idempotente Registrierung in Application.php + routes.php
- `patches/text/TextDirectEditor.php` — gepatchte Text-App (open()-Bridge) - `install.sh` — Installer (core-clean: restores any old text-app backup)
- `install.sh` — Installer
## Status (2026-09-11) ## Status (2026-09-11)
Auf nextcloud1 (Nextcloud 34.0.2, htmlviewer 33.0.0) deployed und iPhone-getestet: Auf nextcloud1 (Nextcloud 34.0.2, htmlviewer 33.0.0) getestet:
HTML rendert in der iOS-App in sandboxed WebView. Long-Press → "Öffnen mit Office" rendert HTML in der iOS-App in sandboxed WebView.
+20 -31
View File
@@ -1,9 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# htmlviewer-directediting installer # htmlviewer-directediting installer
# Applies the HTML-in-iOS-App patches to a Nextcloud container. # Applies the HTML-in-iOS-App patches to a Nextcloud container.
# Usage: ./install.sh <container-name> [custom_apps-path-in-container] # Usage: ./install.sh <container-name>
# default container: nextcloud1-app-1 # default container: nextcloud1-app-1
# Idempotent: safe to re-run after app updates. # Idempotent: safe to re-run after app updates.
#
# This version is CORE-CLEAN: the Nextcloud text app is NOT patched.
# The htmlviewer app registers its editor under the hard-coded app
# editor id "eurooffice" ("Öffnen mit Office" via long press). Plain
# tap on an HTML file still opens the source view (iOS default) —
# single-tap auto-open would require patching the text app, see
# README.md "Varianten".
set -euo pipefail set -euo pipefail
CONTAINER="${1:-nextcloud1-app-1}" CONTAINER="${1:-nextcloud1-app-1}"
@@ -11,67 +18,49 @@ APP="/var/www/html/custom_apps/htmlviewer"
TEXT="/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php" TEXT="/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== htmlviewer-directediting installer ===" echo "=== htmlviewer-directediting installer (core-clean) ==="
echo "Container: $CONTAINER" echo "Container: $CONTAINER"
# 0. Sanity # 0. Sanity
docker exec "$CONTAINER" test -d "$APP" || { echo "ERROR: htmlviewer app not found at $APP"; exit 1; } docker exec "$CONTAINER" test -d "$APP" || { echo "ERROR: htmlviewer app not found at $APP"; exit 1; }
# 1. Backup text editor (first run only) # 1. RESTORE original text app if it was patched by an earlier install
if ! docker exec "$CONTAINER" test -f /tmp/TextDirectEditor.php.bak-htmlproject; then 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 || \ echo "--- restoring original TextDirectEditor.php from backup"
docker exec "$CONTAINER" bash -c "cp $TEXT /tmp/TextDirectEditor.php.bak-htmlproject" || true docker exec "$CONTAINER" bash -c "cp /tmp/TextDirectEditor.php.bak-htmlproject $TEXT"
else
echo "--- no text-app backup found; nothing to restore (good)"
fi fi
# 2. Deploy htmlviewer files (from repo to container via /tmp) # 2. Deploy htmlviewer files
TMPDIR_ON_HOST=$(mktemp -d) TMPDIR_ON_HOST=$(mktemp -d)
cp patches/htmlviewer/*.php "$TMPDIR_ON_HOST/" 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/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/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/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/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/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 \ docker exec "$CONTAINER" chown www-data:www-data \
"$APP/lib/DirectEditing/HtmlDirectEditor.php" \ "$APP/lib/DirectEditing/HtmlDirectEditor.php" \
"$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \ "$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \
"$APP/lib/Controller/DirectController.php" \ "$APP/lib/Controller/DirectController.php" \
"$APP/lib/Listeners/RegisterDirectEditorListener.php" \ "$APP/lib/Listeners/RegisterDirectEditorListener.php" \
"$APP/templates/directEditing.php" \ "$APP/templates/directEditing.php"
/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php
rm -rf "$TMPDIR_ON_HOST" rm -rf "$TMPDIR_ON_HOST"
# 3. Register in Application.php + routes.php (idempotent) # 3. Register in Application.php + routes.php (idempotent)
docker cp patches/htmlviewer/patch_app.py "$CONTAINER:/tmp/patch_app.py" docker cp patches/htmlviewer/patch_app.py "$CONTAINER:/tmp/patch_app.py"
docker exec "$CONTAINER" python3 /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) # 4. Lint + restart
# 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" \ for f in "$APP/lib/DirectEditing/HtmlDirectEditor.php" \
"$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \ "$APP/lib/DirectEditing/HtmlDirectEditorBridge.php" \
"$APP/lib/Controller/DirectController.php" \ "$APP/lib/Controller/DirectController.php" \
"$APP/lib/Listeners/RegisterDirectEditorListener.php" \ "$APP/lib/Listeners/RegisterDirectEditorListener.php" \
"$APP/templates/directEditing.php" \ "$APP/templates/directEditing.php"; do
/var/www/html/apps/text/lib/DirectEditing/TextDirectEditor.php; do
docker exec "$CONTAINER" php -l "$f" docker exec "$CONTAINER" php -l "$f"
done done
echo "=== Restarting container ===" echo "=== Restarting container ==="
docker restart "$CONTAINER" docker restart "$CONTAINER"
echo "=== Done. Test: open an .html file in the iOS app (kill app first). ===" echo "=== Done. iOS: long press HTML file -> 'Öffnen mit Office' renders the page. ==="
+4 -1
View File
@@ -34,7 +34,10 @@ class HtmlDirectEditor implements IEditor {
#[\Override] #[\Override]
public function getId(): string { public function getId(): string {
return Application::APP_ID; // "eurooffice" is one of the hard-coded editor IDs the Nextcloud
// iOS/Android apps resolve in their editor registry. The menu
// "Öffnen mit Office" (long press) then opens this editor.
return 'eurooffice';
} }
#[\Override] #[\Override]
-171
View File
@@ -1,171 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Text\DirectEditing;
use OCA\Text\AppInfo\Application;
use OCA\Text\Service\ApiService;
use OCA\Text\Service\InitialStateProvider;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\DirectEditing\IEditor;
use OCP\DirectEditing\IToken;
use OCP\Files\InvalidPathException;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IAppConfig;
use OCP\IL10N;
use OCP\Util;
class TextDirectEditor implements IEditor {
/** @var IL10N */
private $l10n;
/** @var InitialStateProvider */
private $initialStateProvider;
/** @var ApiService */
private $apiService;
/**
* @var IAppConfig
*/
private $appConfig;
public function __construct(IL10N $l10n, InitialStateProvider $initialStateProvider, ApiService $apiService, IAppConfig $appConfig) {
$this->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();
}
}