diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-10-16 11:33:39 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-12-02 14:28:30 +0300 |
commit | baa11f406390cca2a890c16bf1cf0b404b2fc700 (patch) | |
tree | 4e3d7eb0b39fb23650f89c56e93432041527e272 | |
parent | 2dee8b69ce453e7bc0ebcec28cf375f7742d75d8 (diff) |
Start with direct editing
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r-- | appinfo/app.php | 2 | ||||
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 12 | ||||
-rw-r--r-- | lib/Controller/DirectSessionController.php | 141 | ||||
-rw-r--r-- | lib/Controller/SessionController.php | 4 | ||||
-rw-r--r-- | lib/Db/Session.php | 5 | ||||
-rw-r--r-- | lib/DirectEditing/TextDirectEditor.php | 151 | ||||
-rw-r--r-- | lib/DirectEditing/TextDocumentCreator.php | 59 | ||||
-rw-r--r-- | lib/DirectEditing/TextDocumentTemplateCreator.php | 78 | ||||
-rw-r--r-- | lib/Service/ApiService.php | 22 | ||||
-rw-r--r-- | lib/Service/DocumentService.php | 63 | ||||
-rw-r--r-- | lib/Service/SessionService.php | 40 | ||||
-rw-r--r-- | src/components/EditorWrapper.vue | 33 | ||||
-rw-r--r-- | src/components/FilesEditor.vue | 6 | ||||
-rw-r--r-- | src/main.js | 21 | ||||
-rw-r--r-- | src/services/SyncService.js | 53 | ||||
-rw-r--r-- | src/views/DirectEditing.vue | 115 | ||||
-rw-r--r-- | templates/main.php | 1 |
18 files changed, 718 insertions, 89 deletions
diff --git a/appinfo/app.php b/appinfo/app.php index 85ed0d5af..4913f8aa4 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -38,3 +38,5 @@ $eventDispatcher->addListener('OCA\Files_Sharing::loadAdditionalScripts', functi \OCP\Util::addScript('text', 'public'); \OCP\Util::addStyle('text', 'icons'); }); + +$app = \OC::$server->query(Application::class); diff --git a/appinfo/routes.php b/appinfo/routes.php index 12c8330a1..903fdb95b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -37,6 +37,7 @@ return [ ['name' => 'PublicSession#fetch', 'url' => '/public/session/fetch', 'verb' => 'GET'], ['name' => 'PublicSession#sync', 'url' => '/public/session/sync', 'verb' => 'POST'], ['name' => 'PublicSession#push', 'url' => '/public/session/push', 'verb' => 'POST'], + ['name' => 'PublicSession#close', 'url' => '/public/session/close', 'verb' => 'GET'], ], 'ocs' => [ diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 93ba04806..954a32e1a 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,7 +24,10 @@ declare(strict_types=1); namespace OCA\Text\AppInfo; +use OCA\Text\DirectEditing\TextDirectEditor; use OCP\AppFramework\App; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; class Application extends App { @@ -36,9 +39,18 @@ class Application extends App { * Application constructor. * * @param array $params + * @throws \OCP\AppFramework\QueryException */ public function __construct(array $params = []) { parent::__construct(self::APP_NAME, $params); + + $container = $this->getContainer(); + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $this->getContainer()->getServer()->query(IEventDispatcher::class); + $eventDispatcher->addListener(RegisterDirectEditorEvent::class, function (RegisterDirectEditorEvent $event) use ($container) { + $editor = $container->query(TextDirectEditor::class); + $event->register($editor); + }); } } diff --git a/lib/Controller/DirectSessionController.php b/lib/Controller/DirectSessionController.php new file mode 100644 index 000000000..aef43873e --- /dev/null +++ b/lib/Controller/DirectSessionController.php @@ -0,0 +1,141 @@ +<?php +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Text\Controller; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OCA\Text\Service\ApiService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\PublicShareController; +use OCP\DirectEditing\IManager; +use OCP\ISession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; +use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; +use OCP\Share\IShare; + +class DirectSessionController extends Controller { + + /** @var ShareManager */ + private $shareManager; + + /** @var IShare */ + private $share; + + /** @var ApiService */ + private $apiService; + /** @var IManager */ + private $directManager; + + public function __construct(string $appName, IRequest $request, ShareManager $shareManager, ApiService $apiService, IManager $directManager) { + parent::__construct($appName, $request); + $this->shareManager = $shareManager; + $this->apiService = $apiService; + $this->directManager = $directManager; + } + + /** + * @PublicPage + */ + public function create(string $token, string $file = null, $guestName = null, bool $forceRecreate = false): DataResponse { + try { + $tokenObject = $this->directManager->getToken($token); + $tokenObject->extend(); + $tokenObject->useTokenScope(); + $node = $tokenObject->getFile(); + $node->touch(); + return new DataResponse([ + 'mtime' => $node->getMTime() + ]); + } catch (InvalidTokenException $e) { + return new DataResponse('error'); + } + //return $this->apiService->create(null, $file, $token, $guestName, $forceRecreate); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function fetch(int $documentId, string $sessionId, string $sessionToken): Response { + return $this->apiService->fetch($documentId, $sessionId, $sessionToken); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function close(int $documentId, int $sessionId, string $sessionToken): DataResponse { + return $this->apiService->close($documentId, $sessionId, $sessionToken); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function push(int $documentId, int $sessionId, string $sessionToken, int $version, array $steps, string $token): DataResponse { + return $this->apiService->push($documentId, $sessionId, $sessionToken, $version, $steps, $token); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function sync(string $token, int $documentId, int $sessionId, string $sessionToken, int $version = 0, string $autosaveContent = null, bool $force = false, bool $manualSave = false): DataResponse { + return $this->apiService->sync($documentId, $sessionId, $sessionToken, $version, $autosaveContent, $force, $manualSave, $token); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function updateSession(int $documentId, int $sessionId, string $sessionToken, string $guestName) { + return $this->apiService->updateSession($documentId, $sessionId, $sessionToken, $guestName); + } + +} diff --git a/lib/Controller/SessionController.php b/lib/Controller/SessionController.php index 28660cb77..ff95a6cd9 100644 --- a/lib/Controller/SessionController.php +++ b/lib/Controller/SessionController.php @@ -51,6 +51,7 @@ class SessionController extends Controller { /** * @NoAdminRequired + * @PublicPage */ public function fetch(int $documentId, int $sessionId, string $sessionToken): Response { return $this->apiService->fetch($documentId, $sessionId, $sessionToken); @@ -58,6 +59,7 @@ class SessionController extends Controller { /** * @NoAdminRequired + * @PublicPage */ public function close(int $documentId, int $sessionId, string $sessionToken): DataResponse { return $this->apiService->close($documentId, $sessionId, $sessionToken); @@ -65,6 +67,7 @@ class SessionController extends Controller { /** * @NoAdminRequired + * @PublicPage */ public function push(int $documentId, int $sessionId, string $sessionToken, int $version, array $steps): DataResponse { return $this->apiService->push($documentId, $sessionId, $sessionToken, $version, $steps); @@ -72,6 +75,7 @@ class SessionController extends Controller { /** * @NoAdminRequired + * @PublicPage */ public function sync(int $documentId, int $sessionId, string $sessionToken, int $version = 0, string $autosaveContent = null, bool $force = false, bool $manualSave = false): DataResponse { return $this->apiService->sync($documentId, $sessionId, $sessionToken, $version, $autosaveContent, $force, $manualSave); diff --git a/lib/Db/Session.php b/lib/Db/Session.php index dc3bc9b7d..9f5e17161 100644 --- a/lib/Db/Session.php +++ b/lib/Db/Session.php @@ -26,6 +26,11 @@ namespace OCA\Text\Db; use OCP\AppFramework\Db\Entity; +/** + * @method setLastContact(int $getTime) + * @method getDocumentId() + * @method getUserId() + */ class Session extends Entity implements \JsonSerializable { public $id; diff --git a/lib/DirectEditing/TextDirectEditor.php b/lib/DirectEditing/TextDirectEditor.php new file mode 100644 index 000000000..cb6e06879 --- /dev/null +++ b/lib/DirectEditing/TextDirectEditor.php @@ -0,0 +1,151 @@ +<?php +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Text\DirectEditing; + +use OCA\Files\Controller\ApiController; +use OCA\Text\AppInfo\Application; +use OCA\Text\Service\ApiService; +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\IInitialStateService; +use OCP\IL10N; + +class TextDirectEditor implements IEditor { + + /** @var IL10N */ + private $l10n; + + /** @var IInitialStateService */ + private $initialStateService; + + /** @var ApiService */ + private $apiService; + + public function __construct(IL10N $l10n, IInitialStateService $initialStateService, ApiService $apiService) { + $this->l10n = $l10n; + $this->initialStateService = $initialStateService; + $this->apiService = $apiService; + } + + /** + * 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 array + */ + public function getMimetypes(): array { + return [ + 'text/markdown' + ]; + } + + /** + * A list of mimetypes that can be opened in the editor optionally + * + * @return array + */ + public function getMimetypesOptional(): array { + return [ + 'text/plain' + ]; + } + + /** + * Return a list of file creation options to be presented to the user + * + * @return array of ACreateFromTemplate|ACreateEmpty + */ + public function getCreators(): array { + return [ + new TextDocumentCreator($this->l10n), + new TextDocumentTemplateCreator($this->l10n) + ]; + } + + /** + * 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->initialStateService->provideInitialState('text', 'file', [ + 'fileId' => $token->getFile()->getId(), + 'content' => $token->getFile()->getContent(), + 'session' => \json_encode($session->getData()) + ]); + $this->initialStateService->provideInitialState('text', 'directEditingToken', $token->getToken()); + return new TemplateResponse('text', 'main', [], 'base'); + } catch (InvalidPathException $e) { + } catch (NotFoundException $e) { + } catch (NotPermittedException $e) { + } + return new NotFoundResponse(); + } + +} diff --git a/lib/DirectEditing/TextDocumentCreator.php b/lib/DirectEditing/TextDocumentCreator.php new file mode 100644 index 000000000..6b78cefb4 --- /dev/null +++ b/lib/DirectEditing/TextDocumentCreator.php @@ -0,0 +1,59 @@ +<?php +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Text\DirectEditing; + + +use OCP\DirectEditing\ACreateEmpty; +use OCP\Files\File; +use OCP\IL10N; + +class TextDocumentCreator extends ACreateEmpty { + + /** + * @var IL10N + */ + private $l10n; + + public function __construct(IL10N $l10n) { + $this->l10n = $l10n; + } + + public function getId(): string { + return 'textdocument'; + } + + public function getName(): string { + return $this->l10n->t('New text document'); + } + + public function getExtension(): string { + return '.md'; + } + + public function create(File $file, string $creatorId = null, string $templateId = null): void { + parent::create($file, $creatorId, $templateId); // TODO: Change the autogenerated stub + + $file->putContent('## Empty document with Nextcloud Text'); + } +} diff --git a/lib/DirectEditing/TextDocumentTemplateCreator.php b/lib/DirectEditing/TextDocumentTemplateCreator.php new file mode 100644 index 000000000..d7732573c --- /dev/null +++ b/lib/DirectEditing/TextDocumentTemplateCreator.php @@ -0,0 +1,78 @@ +<?php +/** + * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Text\DirectEditing; + + +use OCP\DirectEditing\ACreateFromTemplate; +use OCP\Files\File; +use OCP\IL10N; + +class TextDocumentTemplateCreator extends ACreateFromTemplate { + + const TEMPLATES = [ + '1' => [ + 'id' => '1', + 'extension' => 'md', + 'name' => 'Weekly ToDo', + 'preview' => 'https://cloud.bitgrid.net/apps/richdocuments/template/preview/832537' + ], + '2' => [ + 'id' => '2', + 'extension' => 'md', + 'name' => 'Meeting notes', + 'preview' => 'https://cloud.bitgrid.net/apps/richdocuments/template/preview/832537' + ] + ]; + + /** + * @var IL10N + */ + private $l10n; + + public function __construct(IL10N $l10n) { + $this->l10n = $l10n; + } + + public function getId(): string { + return 'textdocumenttemplate'; + } + + public function getName(): string { + return $this->l10n->t('New text document from template'); + } + + public function getExtension(): string { + return '.md'; + } + + public function getTemplates(): array { + return self::TEMPLATES; + } + + public function create(File $file, string $creatorId = null, string $templateId = null): void { + $template = self::TEMPLATES[$templateId]; + $file->putContent('## ' . $template['name'] . '\n\n' . 'Created from a template with Nextcloud text'); + } + +} diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index c6a35c610..a43498ceb 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -61,7 +61,6 @@ class ApiService { try { $readOnly = true; /** @var File $file */ - $file = null; if ($token) { $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); try { @@ -72,9 +71,6 @@ class ApiService { } else if ($fileId) { $file = $this->documentService->getFileById($fileId); $readOnly = !$file->isUpdateable(); - } else if ($filePath) { - $file = $this->documentService->getFileByPath($filePath); - $readOnly = !$file->isUpdateable(); } else { return new DataResponse('No valid file argument provided', 500); } @@ -132,11 +128,8 @@ class ApiService { * @throws \OCP\AppFramework\Db\DoesNotExistException */ public function push($documentId, $sessionId, $sessionToken, $version, $steps, $token = null): DataResponse { - if ($token) { - $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); - } else { - $file = $this->documentService->getFileById($documentId); - } + $session = $this->sessionService->getSession($documentId, $sessionId, $sessionToken); + $file = $this->documentService->getFileForSession($session, $token); if ($this->sessionService->isValidSession($documentId, $sessionId, $sessionToken) && !$this->documentService->isReadOnly($file, $token)) { try { $steps = $this->documentService->addStep($documentId, $sessionId, $steps, $version); @@ -162,16 +155,13 @@ class ApiService { 'document' => $this->documentService->get($documentId) ]; + $session = $this->sessionService->getSession($documentId, $sessionId, $sessionToken); + $file = $this->documentService->getFileForSession($session, $token); + try { - $result['document'] = $this->documentService->autosave($documentId, $version, $autosaveContent, $force, $manualSave, $token, $this->request->getParam('filePath')); + $result['document'] = $this->documentService->autosave($file, $documentId, $version, $autosaveContent, $force, $manualSave, $token, $this->request->getParam('filePath'), $userId); } catch (DocumentSaveConflictException $e) { try { - if ($token) { - /** @var File $file */ - $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); - } else { - $file = $this->documentService->getFileById($documentId); - } $result['outsideChange'] = $file->getContent(); } catch (LockedException $e) { // Ignore locked exception since it might happen due to an autosave action happening at the same time diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 605a404fc..be682251d 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -26,6 +26,8 @@ declare(strict_types=1); namespace OCA\Text\Service; use \InvalidArgumentException; +use OCA\Text\Db\Session; +use OCP\DirectEditing\IManager; use function json_encode; use OC\Files\Node\File; use OCA\Text\Db\Document; @@ -108,6 +110,18 @@ class DocumentService { } catch (NotFoundException $e) { $this->appData->newFolder('documents'); } + + // FIXME + $token = \OC::$server->getRequest()->getParam('token'); + if ($this->userId === null && $token !== null) { + $this->directManager = \OC::$server->query(IManager::class); + try { + $tokenObject = $this->directManager->getToken($token); + $tokenObject->extend(); + $tokenObject->useTokenScope(); + $this->userId = $tokenObject->getUser(); + } catch (\Exception $e) {} + } } /** @@ -216,7 +230,7 @@ class DocumentService { * @param $autoaveDocument * @param bool $force * @param bool $manualSave - * @param null $token + * @param null $shareToken * @return Document * @throws DocumentSaveConflictException * @throws DoesNotExistException @@ -226,22 +240,15 @@ class DocumentService { * @throws NotPermittedException * @throws ShareNotFound */ - public function autosave($documentId, $version, $autoaveDocument, $force = false, $manualSave = false, $token = null, $filePath = null): Document { + public function autosave($file, $documentId, $version, $autoaveDocument, $force = false, $manualSave = false, $shareToken = null, $filePath = null, $userId = null): Document { /** @var Document $document */ $document = $this->documentMapper->find($documentId); - /** @var File $file */ - if (!$token) { - $file = $this->getFileById($documentId); - } else { - $file = $this->getFileByShareToken($token, $filePath); - } - if ($file === null) { throw new NotFoundException(); } - if ($this->isReadOnly($file, $token)) { + if ($this->isReadOnly($file, $shareToken)) { return $document; } @@ -308,19 +315,42 @@ class DocumentService { } /** + * @param Session $session + * @param $shareToken + * @return \OCP\Files\File|Folder|Node * @throws NotFoundException */ - public function getFileById($fileId): Node { - $files = $this->rootFolder->getUserFolder($this->userId)->getById($fileId); - if (count($files) === 0) { + public function getFileForSession(Session $session, $shareToken) { + if ($session->getUserId() !== null) { + return $this->getFileById($session->getDocumentId(), $session->getUserId()); + } + try { + $share = $this->shareManager->getShareByToken($shareToken); + + } catch (ShareNotFound $e) { throw new NotFoundException(); } - return $files[0]; + $node = $share->getNode(); + if ($node instanceof \OCP\Files\File) { + return $node; + } + if ($node instanceof Folder) { + return $node->getById($session->getDocumentId())[0]; + } + throw new \InvalidArgumentException('No proper share data'); } - public function getFileByPath($path): Node { - return $this->rootFolder->getUserFolder($this->userId)->get($path); + /** + * @throws NotFoundException + */ + public function getFileById($fileId, $userId = null): Node { + $files = $this->rootFolder->getUserFolder($this->userId ?? $userId)->getById($fileId); + if (count($files) === 0) { + throw new NotFoundException(); + } + + return $files[0]; } /** @@ -332,7 +362,6 @@ class DocumentService { public function getFileByShareToken($shareToken, $path = null) { try { $share = $this->shareManager->getShareByToken($shareToken); - } catch (ShareNotFound $e) { throw new NotFoundException(); } diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index b7be301ef..457fea497 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -29,6 +29,7 @@ use OCA\Text\Db\Session; use OCA\Text\Db\SessionMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DirectEditing\IManager; use OCP\IAvatar; use OCP\IAvatarManager; use OCP\Security\ISecureRandom; @@ -40,13 +41,29 @@ class SessionService { private $sessionMapper; private $secureRandom; private $timeFactory; + private $avatarManager; private $userId; - public function __construct(SessionMapper $sessionMapper, ISecureRandom $secureRandom, ITimeFactory $timeFactory, $userId) { + /** @var Session cache current session in the request */ + private $session = null; + + public function __construct(SessionMapper $sessionMapper, ISecureRandom $secureRandom, ITimeFactory $timeFactory, IAvatarManager $avatarManager, $userId) { $this->sessionMapper = $sessionMapper; $this->secureRandom = $secureRandom; $this->timeFactory = $timeFactory; $this->userId = $userId; + // FIXME + $token = \OC::$server->getRequest()->getParam('token'); + if ($this->userId === null && $token !== null) { + $this->directManager = \OC::$server->query(IManager::class); + try { + $tokenObject = $this->directManager->getToken($token); + $tokenObject->extend(); + $tokenObject->useTokenScope(); + $this->userId = $tokenObject->getUser(); + } catch (\Exception $e) {} + } + $this->avatarManager = $avatarManager; } public function initSession($documentId, $guestName = null): Session { @@ -55,9 +72,7 @@ class SessionService { $userName = $this->userId ? $this->userId : $guestName; $session->setUserId($userName); $session->setToken($this->secureRandom->generate(64)); - /** @var IAvatarManager $avatarGenerator */ - $avatarGenerator = \OC::$server->query(IAvatarManager::class); - $color = $avatarGenerator->getGuestAvatar($userName)->avatarBackgroundColor($userName); + $color = $this->avatarManager->getGuestAvatar($userName)->avatarBackgroundColor($userName); $color = sprintf("#%02x%02x%02x", $color->r, $color->g, $color->b); $session->setColor($color); if ($this->userId === null) { @@ -96,9 +111,24 @@ class SessionService { return $this->sessionMapper->deleteInactive($documentId); } + public function getSession($documentId, $sessionId, $token) { + if ($this->session !== null) { + return $this->session; + } + try { + return $this->sessionMapper->find($documentId, $sessionId, $token); + } catch (DoesNotExistException $e) { + $this->session = false; + return false; + } + } + public function isValidSession($documentId, $sessionId, $token) { + if ($this->userId) { + return true; + } try { - $session = $this->sessionMapper->find($documentId, $sessionId, $token); + $session = $this->getSession($documentId, $sessionId, $token); } catch (DoesNotExistException $e) { return false; } diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index 5c8cf17e3..7b4a7c674 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -45,6 +45,7 @@ <GuestNameDialog v-if="isPublic && currentSession.guestName" :sync-service="syncService" /> </SessionList> </div> + <slot name="header" /> </MenuBar> <div> <MenuBubble v-if="!readOnly && isRichEditor" :editor="tiptap" /> @@ -98,6 +99,10 @@ export default { isMobile, ], props: { + initialSession: { + type: Object, + default: null, + }, relativePath: { type: String, default: null, @@ -192,7 +197,7 @@ export default { } }, hasDocumentParameters() { - return this.fileId || this.shareToken + return this.fileId || this.shareToken || this.initialSession }, isPublic() { return document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' @@ -373,12 +378,20 @@ export default { this.dirty = state.dirty } }) - this.syncService.open({ - fileId: this.fileId, - filePath: this.relativePath, - }).catch((e) => { - this.hasConnectionIssue = true - }) + if (this.initialSession === null) { + this.syncService.open({ + fileId: this.fileId, + filePath: this.relativePath, + }).catch((e) => { + this.hasConnectionIssue = true + }) + } else { + this.syncService.open({ + initialSession: this.initialSession, + }).catch((e) => { + this.hasConnectionIssue = true + }) + } this.forceRecreate = false }, @@ -451,8 +464,7 @@ export default { display: block; width: 100%; max-width: 100%; - height: calc(100% - 50px); - top: 50px; + height: 100%; left: 0; margin: 0 auto; position: relative; @@ -516,8 +528,7 @@ export default { } #editor-session-list { - padding: 9px; - padding-right: 16px; + padding: 4px 16px 4px 4px; display: flex; input, div { diff --git a/src/components/FilesEditor.vue b/src/components/FilesEditor.vue index 28c3df2b7..708df3f3c 100644 --- a/src/components/FilesEditor.vue +++ b/src/components/FilesEditor.vue @@ -64,3 +64,9 @@ export default { }, } </script> +<style> +#editor-container { + height: calc(100% - 50px); + top: 50px; +} +</style> diff --git a/src/main.js b/src/main.js index a9a241f1b..1aed86f45 100644 --- a/src/main.js +++ b/src/main.js @@ -1,29 +1,18 @@ -import FilesEditor from './components/FilesEditor' - __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line __webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line -if (document.getElementById('maineditor')) { +if (document.getElementById('app-content')) { Promise.all([ import(/* webpackChunkName: "editor" */'vue'), - import(/* webpackChunkName: "editor" */'./components/EditorWrapper'), + import(/* webpackChunkName: "editor" */'./views/DirectEditing'), ]).then((imports) => { const Vue = imports[0].default Vue.prototype.t = window.t Vue.prototype.OCA = window.OCA - const Editor = imports[1].default + const DirectEditing = imports[1].default const vm = new Vue({ - render: h => h(Editor, { - props: { - relativePath: '/welcome.md', - active: true, - }, - }), + render: h => h(DirectEditing), }) - vm.$mount(document.getElementById('preview')) + vm.$mount(document.getElementById('app-content')) }) } - -OCA.Text = { - Editor: FilesEditor, -} diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 7af158ab0..0702b2c84 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -84,28 +84,38 @@ class SyncService { return this } - open({ fileId, filePath }) { - return this._openDocument({ fileId, filePath }).then(() => { - this.emit('opened', { + async open({ fileId, filePath, initialSession }) { + let connectionData = null + if (typeof initialSession === 'undefined') { + try { + const response = await this._openDocument({ fileId, filePath }) + connectionData = response.data + } catch (error) { + if (!error.response || error.code === 'ECONNABORTED') { + this.emit('error', ERROR_TYPE.CONNECTION_FAILED, {}) + } else { + this.emit('error', ERROR_TYPE.LOAD_ERROR, error.response.status) + } + throw error + } + } else { + connectionData = initialSession + } + + this.document = connectionData.document + this.document.readOnly = connectionData.readOnly + this.session = connectionData.session + + this.emit('opened', { + document: this.document, + session: this.session, + }) + return this._fetchDocument().then(({ data }) => { + this.emit('loaded', { document: this.document, session: this.session, + documentSource: '' + data, }) - return this._fetchDocument().then(({ data }) => { - - this.emit('loaded', { - document: this.document, - session: this.session, - documentSource: '' + data, - }) - }) - }).catch((error) => { - if (!error.response || error.code === 'ECONNABORTED') { - this.emit('error', ERROR_TYPE.CONNECTION_FAILED, {}) - } else { - this.emit('error', ERROR_TYPE.LOAD_ERROR, error.response.status) - } - - return Promise.reject(error) }) } @@ -122,11 +132,6 @@ class SyncService { guestName: this.options.guestName, forceRecreate: this.options.forceRecreate, }, - }).then((response) => { - this.document = response.data.document - this.document.readOnly = response.data.readOnly - this.session = response.data.session - return response.data }) } diff --git a/src/views/DirectEditing.vue b/src/views/DirectEditing.vue new file mode 100644 index 000000000..114996977 --- /dev/null +++ b/src/views/DirectEditing.vue @@ -0,0 +1,115 @@ +<!-- + - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div id="editor-wrapper"> + <EditorWrapper :initial-session="initialSession" :active="true" mime="text/markdown"> + <template #header> + <button class="icon-share" @click="share" /> + <button class="icon-close" @click="close" /> + </template> + </EditorWrapper> + <!--<hr> + <h3>Debug output</h3> + <pre>{{ initialSession }}</pre> + <pre>Last request time: {{ log.mtime }}</pre> + <pre>{{ messages }}</pre>--> + </div> +</template> + +<script> +import Vue from 'vue' +import EditorWrapper from '../components/EditorWrapper' + +const log = Vue.observable({ + messages: [], + mtime: 0, +}) + +window.OCP.DirectEditing = { + close() { + log.messages.push('OCP.DirectEditing.close got called') + }, +} + +window.addEventListener('message', function(message) { + log.messages.push(message.data) +}) +export default { + name: 'DirectEditing', + components: { EditorWrapper }, + data() { + return { + initial: OCP.InitialState.loadState('text', 'file'), + messages: log.messages, + log: log, + } + }, + computed: { + initialSession() { + return JSON.parse(this.initial.session) || null + }, + }, + methods: { + close() { + window.postMessage('close') + }, + share() { + window.postMessage('share') + }, + }, +} +</script> + +<style scoped lang="scss"> + pre { + width: 100%; + max-width: 700px; + margin: auto; + background-color: var(--color-background-dark); + } + + button { + width: 44px; + height: 44px; + margin: 0; + background-size: 16px; + border: 0; + background-color: transparent; + opacity: .5; + color: var(--color-main-text); + background-position: center center; + vertical-align: top; + &:hover, &:focus, &:active { + background-color: var(--color-background-dark); + } + &.is-active, + &:hover, + &:focus { + opacity: 1; + } + + &.icon-undo, &.icon-redo { + opacity: .4; + } + } +</style> diff --git a/templates/main.php b/templates/main.php index 6d0a82953..384bcfdd8 100644 --- a/templates/main.php +++ b/templates/main.php @@ -5,3 +5,4 @@ style('text', 'icons'); <div id="app-content"> <div id="maineditor"></div> </div> + |