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:
@@ -1,6 +1,7 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
→ Fallback auf QuickLook → HTML-Quelltext.
|
||||
|
||||
## Lösung (2 Patches)
|
||||
## Lösung (nur htmlviewer-App, core-clean)
|
||||
|
||||
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
|
||||
Die htmlviewer-App registriert einen `IEditor` mit der hartcodeten App-Editor-ID
|
||||
**`eurooffice`** und Mimetype `text/html` + Bridge, 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.
|
||||
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
|
||||
|
||||
@@ -32,25 +34,33 @@ Bei Nextcloud-Core selbst (lib/, config) wurde **nichts** geändert.
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- htmlviewer-App-Update überschreibt `lib/DirectEditing/`, `templates/`, `Application.php`-Registrierung → `install.sh` erneut
|
||||
- Nextcloud-Update überschreibt `apps/text/.../TextDirectEditor.php` → `install.sh` erneut
|
||||
- htmlviewer-App-Update überschreibt `lib/DirectEditing/`, `templates/`,
|
||||
`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
|
||||
|
||||
- `patches/htmlviewer/HtmlDirectEditor.php` — IEditor-Implementierung (DirectEditing-Registrierung, für Web-Client/Wiederverwendung)
|
||||
- `patches/htmlviewer/HtmlDirectEditorBridge.php` — statische Bridge, von TextDirectEditor aufgerufen
|
||||
- `patches/htmlviewer/HtmlDirectEditor.php` — IEditor-Implementierung, ID `eurooffice`
|
||||
- `patches/htmlviewer/HtmlDirectEditorBridge.php` — Template-Renderer
|
||||
- `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
|
||||
- `install.sh` — Installer (core-clean: restores any old text-app backup)
|
||||
|
||||
## 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.
|
||||
Auf nextcloud1 (Nextcloud 34.0.2, htmlviewer 33.0.0) getestet:
|
||||
Long-Press → "Öffnen mit Office" rendert HTML in der iOS-App in sandboxed WebView.
|
||||
+20
-31
@@ -1,9 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# htmlviewer-directediting installer
|
||||
# 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
|
||||
# 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
|
||||
|
||||
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"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "=== htmlviewer-directediting installer ==="
|
||||
echo "=== htmlviewer-directediting installer (core-clean) ==="
|
||||
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
|
||||
# 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
|
||||
echo "--- restoring original TextDirectEditor.php from backup"
|
||||
docker exec "$CONTAINER" bash -c "cp /tmp/TextDirectEditor.php.bak-htmlproject $TEXT"
|
||||
else
|
||||
echo "--- no text-app backup found; nothing to restore (good)"
|
||||
fi
|
||||
|
||||
# 2. Deploy htmlviewer files (from repo to container via /tmp)
|
||||
# 2. Deploy htmlviewer files
|
||||
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
|
||||
"$APP/templates/directEditing.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
|
||||
# 4. 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
|
||||
"$APP/templates/directEditing.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). ==="
|
||||
echo "=== Done. iOS: long press HTML file -> 'Öffnen mit Office' renders the page. ==="
|
||||
@@ -34,7 +34,10 @@ class HtmlDirectEditor implements IEditor {
|
||||
|
||||
#[\Override]
|
||||
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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user