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
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* Streams the raw HTML file for the DirectEditing sandbox iframe.
|
||||
* Authenticated via the user session (direct editing keeps a session).
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class DirectController extends Controller {
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private IRootFolder $rootFolder,
|
||||
private IUserSession $userSession,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
#[NoCSRFRequired]
|
||||
public function content(int $fileId) {
|
||||
$user = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* DirectEditing editor for text/html so the Nextcloud iOS/Android apps
|
||||
* open HTML files in an embedded WebView instead of falling back to
|
||||
* QuickLook source display.
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\DirectEditing;
|
||||
|
||||
use OCA\HtmlViewer\AppInfo\Application;
|
||||
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\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class HtmlDirectEditor implements IEditor {
|
||||
|
||||
public function __construct(
|
||||
private IL10N $l10n,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getId(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return $this->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* Bridge called from OCA\Text\DirectEditing\TextDirectEditor::open()
|
||||
* for text/html files. Reuses the htmlviewer DirectEditing template
|
||||
* so the mobile apps (hard-coded editor registry) render HTML in a
|
||||
* sandboxed iframe instead of showing source via QuickLook.
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\DirectEditing;
|
||||
|
||||
use OCA\HtmlViewer\AppInfo\Application;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\DirectEditing\IToken;
|
||||
use OCP\Server;
|
||||
|
||||
class HtmlDirectEditorBridge {
|
||||
|
||||
public static function openHtml(IToken $token): Response {
|
||||
try {
|
||||
$file = $token->getFile();
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'directEditing',
|
||||
[
|
||||
'fileId' => $file->getId(),
|
||||
'fileName' => $file->getName(),
|
||||
'fileContent' => $file->getContent(),
|
||||
],
|
||||
'base'
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\Listener;
|
||||
|
||||
use OCA\HtmlViewer\DirectEditing\HtmlDirectEditor;
|
||||
use OCP\DirectEditing\RegisterDirectEditorEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
|
||||
/** @template-implements IEventListener<Event|RegisterDirectEditorEvent> */
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?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/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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* DirectEditing template: renders the HTML file in a sandboxed iframe
|
||||
* that fills the whole viewport (works in the iOS/Android WebView).
|
||||
* The file content is embedded as srcdoc, so the iframe needs no
|
||||
* session cookie (mobile WebViews do not share the app session).
|
||||
*/
|
||||
/** @var array $_ */
|
||||
?>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-main-background, #ffffff);
|
||||
}
|
||||
#htmlviewer-frame {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<iframe
|
||||
id="htmlviewer-frame"
|
||||
title="<?php echo htmlspecialchars($_['fileName'] ?? 'HTML'); ?>"
|
||||
srcdoc="<?php echo htmlspecialchars($_['fileContent'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8', false); ?>"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-modals"
|
||||
></iframe>
|
||||
Executable
+77
@@ -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 <container-name> [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). ==="
|
||||
@@ -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())
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* Streams the raw HTML file for the DirectEditing sandbox iframe.
|
||||
* Authenticated via the user session (direct editing keeps a session).
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class DirectController extends Controller {
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private IRootFolder $rootFolder,
|
||||
private IUserSession $userSession,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
#[NoAdminRequired]
|
||||
#[NoCSRFRequired]
|
||||
public function content(int $fileId) {
|
||||
$user = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* DirectEditing editor for text/html so the Nextcloud iOS/Android apps
|
||||
* open HTML files in an embedded WebView instead of falling back to
|
||||
* QuickLook source display.
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\DirectEditing;
|
||||
|
||||
use OCA\HtmlViewer\AppInfo\Application;
|
||||
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\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class HtmlDirectEditor implements IEditor {
|
||||
|
||||
public function __construct(
|
||||
private IL10N $l10n,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getId(): string {
|
||||
return Application::APP_ID;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return $this->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* Bridge called from OCA\Text\DirectEditing\TextDirectEditor::open()
|
||||
* for text/html files. Reuses the htmlviewer DirectEditing template
|
||||
* so the mobile apps (hard-coded editor registry) render HTML in a
|
||||
* sandboxed iframe instead of showing source via QuickLook.
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\DirectEditing;
|
||||
|
||||
use OCA\HtmlViewer\AppInfo\Application;
|
||||
use OCP\AppFramework\Http\NotFoundResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\DirectEditing\IToken;
|
||||
use OCP\Server;
|
||||
|
||||
class HtmlDirectEditorBridge {
|
||||
|
||||
public static function openHtml(IToken $token): Response {
|
||||
try {
|
||||
$file = $token->getFile();
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'directEditing',
|
||||
[
|
||||
'fileId' => $file->getId(),
|
||||
'fileName' => $file->getName(),
|
||||
'fileContent' => $file->getContent(),
|
||||
],
|
||||
'base'
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\HtmlViewer\Listener;
|
||||
|
||||
use OCA\HtmlViewer\DirectEditing\HtmlDirectEditor;
|
||||
use OCP\DirectEditing\RegisterDirectEditorEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
|
||||
/** @template-implements IEventListener<Event|RegisterDirectEditorEvent> */
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 HtmlViewer / patch by Hermes Agent
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* DirectEditing template: renders the HTML file in a sandboxed iframe
|
||||
* that fills the whole viewport (works in the iOS/Android WebView).
|
||||
* The file content is embedded as srcdoc, so the iframe needs no
|
||||
* session cookie (mobile WebViews do not share the app session).
|
||||
*/
|
||||
/** @var array $_ */
|
||||
?>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-main-background, #ffffff);
|
||||
}
|
||||
#htmlviewer-frame {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<iframe
|
||||
id="htmlviewer-frame"
|
||||
title="<?php echo htmlspecialchars($_['fileName'] ?? 'HTML'); ?>"
|
||||
srcdoc="<?php echo htmlspecialchars($_['fileContent'] ?? '', ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8', false); ?>"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-modals"
|
||||
></iframe>
|
||||
@@ -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())
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
echo "start\n";
|
||||
require "/var/www/html/lib/base.php";
|
||||
echo "base ok\n";
|
||||
$m = \OC::server->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()));
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
$_SERVER['HTTP_USER_AGENT']='test';
|
||||
define('PHPUNIT_RUN', true);
|
||||
$_SERVER['REQUEST_URI']='/index.php';
|
||||
require "/var/www/html/lib/base.php";
|
||||
$m = \OC::$server->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;
|
||||
Reference in New Issue
Block a user