From 850eb3041c7fea0ec15c1f3b7b3944f5bc935414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 26 Aug 2019 16:00:00 +0200 Subject: [PATCH 1/7] Add direct editing backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/routes.php | 7 +- lib/AppInfo/Application.php | 6 +- lib/Capabilities.php | 71 +++++++++++ lib/Controller/DirectOCSController.php | 111 ++++++++++++++++++ lib/Controller/DirectViewController.php | 95 +++++++++++++++ lib/Controller/PublicSessionController.php | 28 ++++- lib/Db/Direct.php | 49 ++++++++ lib/Db/DirectMapper.php | 92 +++++++++++++++ lib/Db/Session.php | 2 + .../Version020000Date20190826115310.php | 82 +++++++++++++ lib/Service/ApiService.php | 42 ++++++- lib/Service/DocumentService.php | 15 ++- lib/Service/SessionService.php | 63 +++++++++- 13 files changed, 641 insertions(+), 22 deletions(-) create mode 100644 lib/Capabilities.php create mode 100644 lib/Controller/DirectOCSController.php create mode 100644 lib/Controller/DirectViewController.php create mode 100644 lib/Db/Direct.php create mode 100644 lib/Db/DirectMapper.php create mode 100644 lib/Migration/Version020000Date20190826115310.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 299d1181adf..1a534afdef7 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -38,5 +38,10 @@ ['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'], - ] + + ['name' => 'DirectView#show', 'url' => '/direct', 'verb' => 'GET'], + ], + 'ocs' => [ + ['name' => 'DirectOCS#create', 'url' => '/api/v1/document', 'verb' => 'POST'], + ], ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 93ba048065f..18582ec6519 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,14 +24,13 @@ namespace OCA\Text\AppInfo; +use OCA\Text\Capabilities; use OCP\AppFramework\App; class Application extends App { - const APP_NAME = 'text'; - /** * Application constructor. * @@ -39,6 +38,9 @@ class Application extends App { */ public function __construct(array $params = []) { parent::__construct(self::APP_NAME, $params); + + $this->getContainer()->registerCapability(Capabilities::class); + } } diff --git a/lib/Capabilities.php b/lib/Capabilities.php new file mode 100644 index 00000000000..10f8c72c5dc --- /dev/null +++ b/lib/Capabilities.php @@ -0,0 +1,71 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019 Julius Härtl + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Text; + +use OCP\Capabilities\ICapability; + +class Capabilities implements ICapability { + + private const MIMETYPES = [ + 'text/markdown' + ]; + + private const MIMETYPES_OPTIONAL = [ + 'text/plain' + ]; + + public function getCapabilities() { + return [ + 'text' => [ + 'mimetypes' => self::MIMETYPES, + 'mimetypesNoDefaultOpen' => self::MIMETYPES_OPTIONAL, + 'direct_editing' => true, + ], + ]; + } + +} diff --git a/lib/Controller/DirectOCSController.php b/lib/Controller/DirectOCSController.php new file mode 100644 index 00000000000..151d3ba3094 --- /dev/null +++ b/lib/Controller/DirectOCSController.php @@ -0,0 +1,111 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Text\Controller; + + + +use OCA\Text\Db\DirectMapper; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IRequest; +use OCP\IURLGenerator; + +class DirectOCSController extends OCSController { + + /** @var IRootFolder */ + private $rootFolder; + + /** @var string */ + private $userId; + + /** @var DirectMapper */ + private $directMapper; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * OCS controller + * + * @param string $appName + * @param IRequest $request + * @param IRootFolder $rootFolder + * @param string $userId + * @param DirectMapper $directMapper + * @param IURLGenerator $urlGenerator + */ + public function __construct(string $appName, + IRequest $request, + IRootFolder $rootFolder, + $userId, + DirectMapper $directMapper, + IURLGenerator $urlGenerator) { + parent::__construct($appName, $request); + + $this->rootFolder = $rootFolder; + $this->userId = $userId; + $this->directMapper = $directMapper; + $this->urlGenerator = $urlGenerator; + } + + /** + * @NoAdminRequired + * + * Init an editing session + * + * @param int $fileId + * @return DataResponse + * @throws OCSNotFoundException|OCSBadRequestException + */ + public function create($fileId) { + try { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $nodes = $userFolder->getById($fileId); + + if ($nodes === []) { + throw new OCSNotFoundException(); + } + + $node = $nodes[0]; + if ($node instanceof Folder) { + throw new OCSBadRequestException('Cannot view folder'); + } + + $direct = $this->directMapper->newToken($this->userId, $fileId); + + return new DataResponse([ + 'url' => $this->urlGenerator->linkToRouteAbsolute('text.DirectView.show', [ + 'token' => $direct->getToken() + ]) + ]); + } catch (NotFoundException $e) { + throw new OCSNotFoundException(); + } + } +} diff --git a/lib/Controller/DirectViewController.php b/lib/Controller/DirectViewController.php new file mode 100644 index 00000000000..1439d16b373 --- /dev/null +++ b/lib/Controller/DirectViewController.php @@ -0,0 +1,95 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019 Julius Härtl + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Text\Controller; + +use OCA\Text\AppInfo\Application; +use OCA\Text\Db\DirectMapper; +use OCA\Text\Service\DocumentService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IInitialStateService; +use OCP\IRequest; + +class DirectViewController extends Controller { + + public function __construct($appName, IRequest $request, DocumentService $documentService, DirectMapper $directMapper, IInitialStateService $initialStateService) { + parent::__construct($appName, $request); + $this->documentService = $documentService; + $this->directMapper = $directMapper; + $this->initialStateService = $initialStateService; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * + * @param string $token + * @return TemplateResponse + */ + public function show(string $token): Response { + try { + $direct = $this->directMapper->getByToken($token); + } catch (DoesNotExistException $e) { + //TODO show 404 + return new NotFoundResponse(); + } + + // Delete the token. They are for 1 time use only + //$this->directMapper->delete($direct); + + $this->initialStateService->provideInitialState(Application::APP_NAME, 'direct_token', $token); + $this->initialStateService->provideInitialState(Application::APP_NAME, 'direct_file_id', $direct->getFileId()); + $file = $this->documentService->getFileById($direct->getFileId(), $direct->getUserId()); + $this->initialStateService->provideInitialState(Application::APP_NAME, 'direct_mime', $file->getMimetype()); + return new TemplateResponse(Application::APP_NAME, 'main', [], 'empty'); + } + +} diff --git a/lib/Controller/PublicSessionController.php b/lib/Controller/PublicSessionController.php index 015f5c0a7b7..7be8d99b344 100644 --- a/lib/Controller/PublicSessionController.php +++ b/lib/Controller/PublicSessionController.php @@ -25,6 +25,8 @@ namespace OCA\Text\Controller; use OCA\Text\Service\ApiService; +use OCA\Text\Service\SessionService; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http\Response; use OCP\AppFramework\PublicShareController; use OCP\ISession; @@ -45,10 +47,14 @@ class PublicSessionController extends PublicShareController { /** @var ApiService */ private $apiService; - public function __construct(string $appName, IRequest $request, ISession $session, ShareManager $shareManager, ApiService $apiService) { + /** @var SessionService */ + private $sessionService; + + public function __construct(string $appName, IRequest $request, ISession $session, ShareManager $shareManager, ApiService $apiService, SessionService $sessionService) { parent::__construct($appName, $request, $session); $this->shareManager = $shareManager; $this->apiService = $apiService; + $this->sessionService = $sessionService; } protected function getPasswordHash(): string { @@ -56,16 +62,30 @@ protected function getPasswordHash(): string { } public function isValidToken(): bool { + try { $this->share = $this->shareManager->getShareByToken($this->getToken()); return true; - } catch (ShareNotFound $e) { - return false; + } catch (ShareNotFound $e) {} + + if ($this->sessionService->isDirectToken($this->getToken())) { + //$this->sessionService->clearDirectSession($this->getToken()); + return true; } + try { + $session = $this->sessionService->loadSession( + $this->request->getParam('documentId'), + $this->request->getParam('sessionId'), + $this->request->getParam('sessionToken') + ); + return $session->getDirect(); + } catch (DoesNotExistException $e) {} + + return false; } protected function isPasswordProtected(): bool { - return $this->share->getPassword() !== null; + return $this->share !== null && $this->share->getPassword() !== null; } /** diff --git a/lib/Db/Direct.php b/lib/Db/Direct.php new file mode 100644 index 00000000000..4649cd65044 --- /dev/null +++ b/lib/Db/Direct.php @@ -0,0 +1,49 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Text\Db; + +use OCP\AppFramework\Db\Entity; + +class Direct extends Entity { + + /** @var string */ + protected $token; + + /** @var string */ + protected $userId; + + /** @var int */ + protected $fileId; + + /** @var int */ + protected $timestamp; + + public function __construct() { + $this->addType('token', 'string'); + $this->addType('userId', 'string'); + $this->addType('fileId', 'int'); + $this->addType('timestamp', 'int'); + } + +} diff --git a/lib/Db/DirectMapper.php b/lib/Db/DirectMapper.php new file mode 100644 index 00000000000..89c7568ae02 --- /dev/null +++ b/lib/Db/DirectMapper.php @@ -0,0 +1,92 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Text\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IDBConnection; +use OCP\Security\ISecureRandom; + +/** + * @method insert(Direct $entity) : Direct + */ +class DirectMapper extends QBMapper { + + public const TOKEN_TTL = 600000; + + /** @var ISecureRandom */ + protected $random; + + /**@var ITimeFactory */ + protected $timeFactory; + + public function __construct(IDBConnection $db, + ISecureRandom $random, + ITimeFactory $timeFactory) { + parent::__construct($db, 'text_direct', Direct::class); + + $this->random = $random; + $this->timeFactory = $timeFactory; + } + + public function newToken(string $userId, int $fileId): Direct { + $direct = new Direct(); + $direct->setUserId($userId); + $direct->setFileId($fileId); + $direct->setToken($this->random->generate(64, ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER)); + $direct->setTimestamp($this->timeFactory->getTime()); + return $this->insert($direct); + } + + /** + * @param string $token + * @return Direct + * @throws DoesNotExistException + */ + public function getByToken(string $token): Direct { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))); + + $cursor = $qb->execute(); + $row = $cursor->fetch(); + $cursor->closeCursor(); + + //There can only be one as the token is unique + if ($row === false) { + throw new DoesNotExistException('Could not find token.'); + } + + $direct = Direct::fromRow($row); + + if (($direct->getTimestamp() + self::TOKEN_TTL) < $this->timeFactory->getTime()) { + $this->delete($direct); + throw new DoesNotExistException('Could not find token.'); + } + + return $direct; + } +} diff --git a/lib/Db/Session.php b/lib/Db/Session.php index dc3bc9b7d84..ff0b3c9eb2e 100644 --- a/lib/Db/Session.php +++ b/lib/Db/Session.php @@ -35,11 +35,13 @@ class Session extends Entity implements \JsonSerializable { protected $guestName; protected $lastContact; protected $documentId; + protected $direct; public function __construct() { $this->addType('id', 'integer'); $this->addType('documentId', 'integer'); $this->addType('lastContact', 'integer'); + $this->addType('direct', 'bool'); } diff --git a/lib/Migration/Version020000Date20190826115310.php b/lib/Migration/Version020000Date20190826115310.php new file mode 100644 index 00000000000..8dfe71ee8f8 --- /dev/null +++ b/lib/Migration/Version020000Date20190826115310.php @@ -0,0 +1,82 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +declare(strict_types=1); + +namespace OCA\Text\Migration; + +use Closure; +use Doctrine\DBAL\Types\Type; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version020000Date20190826115310 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('text_direct')) { + $table = $schema->createTable('text_direct'); + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'unsigned' => true, + ]); + $table->addColumn('user_id', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('token', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('file_id', Type::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('timestamp', Type::BIGINT, [ + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $table->setPrimaryKey(['id']); + $table->addIndex(['token'], 'rd_direct_token_idx'); + } + if ($schema->hasTable('text_sessions') && !$schema->getTable('text_sessions')->hasColumn('direct')) { + $table = $schema->getTable('text_sessions'); + $table->addColumn('direct', Type::BOOLEAN, [ + 'notnull' => true, + 'defaul' => false + ]); + } + return $schema; + } + +} diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index c6a35c610aa..8e6a0a61456 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -32,6 +32,7 @@ use OCA\Text\DocumentHasUnsavedChangesException; use OCA\Text\DocumentSaveConflictException; use OCA\Text\VersionMismatchException; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\NotFoundResponse; @@ -49,20 +50,42 @@ class ApiService { protected $documentService; protected $logger; + /** + * ApiService constructor. + * + * @param IRequest $request + * @param ICacheFactory $cacheFactory + * @param SessionService $sessionService + * @param DocumentService $documentService + * @param ILogger $logger + * @param IRequest $request + */ public function __construct(IRequest $request, ICacheFactory $cacheFactory, SessionService $sessionService, DocumentService $documentService, ILogger $logger) { $this->request = $request; $this->cache = $cacheFactory->createDistributed('textSession'); $this->sessionService = $sessionService; $this->documentService = $documentService; $this->logger = $logger; + + try { + $this->sessionService->loadSession( + $request->getParam('documentId'), + $request->getParam('sessionId'), + $request->getParam('sessionToken') + ); + } catch (DoesNotExistException $e) {} } public function create($fileId = null, $filePath = null, $token = null, $guestName = null, bool $forceRecreate = false): DataResponse { try { $readOnly = true; + $direct = false; /** @var File $file */ $file = null; - if ($token) { + if ($token && $direct = $this->sessionService->getDirect($token)) { + $file = $this->documentService->getFileById($direct->getFileId(), $direct->getUserId()); + $readOnly = !$file->isUpdateable(); + } elseif ($token) { $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); try { $this->documentService->checkSharePermissions($token, Constants::PERMISSION_UPDATE); @@ -93,7 +116,8 @@ public function create($fileId = null, $filePath = null, $token = null, $guestNa return new DataResponse($e->getMessage(), 500); } - $session = $this->sessionService->initSession($document->getId(), $guestName); + $session = $this->sessionService->initSession($document->getId(), $direct, !!$direct ? null : $guestName); + return new DataResponse([ 'document' => $document, 'session' => $session, @@ -132,12 +156,15 @@ public function close($documentId, $sessionId, $sessionToken): DataResponse { * @throws \OCP\AppFramework\Db\DoesNotExistException */ public function push($documentId, $sessionId, $sessionToken, $version, $steps, $token = null): DataResponse { - if ($token) { + $session = $this->sessionService->getSession($documentId, $sessionId, $sessionToken); + if ($this->sessionService->isDirectSession()) { + $file = $this->documentService->getFileById($documentId, $session->getUserId()); + } elseif ($token) { $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); } else { $file = $this->documentService->getFileById($documentId); } - if ($this->sessionService->isValidSession($documentId, $sessionId, $sessionToken) && !$this->documentService->isReadOnly($file, $token)) { + if ($this->sessionService->isValidSession($documentId, $sessionId, $sessionToken) && !$this->documentService->isReadOnly($file, $session->getDirect() ? null : $token)) { try { $steps = $this->documentService->addStep($documentId, $sessionId, $steps, $version); } catch (VersionMismatchException $e) { @@ -162,11 +189,14 @@ public function sync($documentId, $sessionId, $sessionToken, $version = 0, $auto 'document' => $this->documentService->get($documentId) ]; + $session = $this->sessionService->getSession(); try { - $result['document'] = $this->documentService->autosave($documentId, $version, $autosaveContent, $force, $manualSave, $token, $this->request->getParam('filePath')); + $result['document'] = $this->documentService->autosave($documentId, $this->sessionService->getSession(), $version, $autosaveContent, $force, $manualSave, $session->getDirect() ? null : $token, $this->request->getParam('filePath')); } catch (DocumentSaveConflictException $e) { try { - if ($token) { + if ($this->sessionService->isDirectSession()) { + $file = $this->documentService->getFileById($documentId, $session->getUserId()); + } elseif ($token) { /** @var File $file */ $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); } else { diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index bbfb2b199c7..6480db29dac 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -30,6 +30,7 @@ use OC\Files\Node\File; use OCA\Text\Db\Document; use OCA\Text\Db\DocumentMapper; +use OCA\Text\Db\Session; use OCA\Text\Db\Step; use OCA\Text\Db\StepMapper; use OCA\Text\DocumentHasUnsavedChangesException; @@ -226,12 +227,14 @@ public function getSteps($documentId, $lastVersion) { * @throws NotPermittedException * @throws ShareNotFound */ - public function autosave($documentId, $version, $autoaveDocument, $force = false, $manualSave = false, $token = null, $filePath = null): Document { + public function autosave($documentId, Session $session, $version, $autoaveDocument, $force = false, $manualSave = false, $token = null, $filePath = null): Document { /** @var Document $document */ $document = $this->documentMapper->find($documentId); /** @var File $file */ - if (!$token) { + if ($session->getDirect()) { + $file = $this->getFileById($documentId, $session->getUserId()); + } elseif (!$token) { $file = $this->getFileById($documentId); } else { $file = $this->getFileByShareToken($token, $filePath); @@ -307,12 +310,12 @@ public function resetDocument($documentId, $force = false): void { } } - public function getFileById($fileId): Node { - return $this->rootFolder->getUserFolder($this->userId)->getById($fileId)[0]; + public function getFileById($fileId, $userId = null): Node { + return $this->rootFolder->getUserFolder($userId ?? $this->userId)->getById($fileId)[0]; } - public function getFileByPath($path): Node { - return $this->rootFolder->getUserFolder($this->userId)->get($path); + public function getFileByPath($path, $userId = null): Node { + return $this->rootFolder->getUserFolder($userId ?? $this->userId)->get($path); } /** diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index b7be301ef34..c734ef9666a 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -25,6 +25,7 @@ use OC\Avatar\Avatar; +use OCA\Text\Db\DirectMapper; use OCA\Text\Db\Session; use OCA\Text\Db\SessionMapper; use OCP\AppFramework\Db\DoesNotExistException; @@ -41,20 +42,27 @@ class SessionService { private $secureRandom; private $timeFactory; private $userId; + /** @var Session Current session */ + private $session; - public function __construct(SessionMapper $sessionMapper, ISecureRandom $secureRandom, ITimeFactory $timeFactory, $userId) { + public function __construct(SessionMapper $sessionMapper, DirectMapper $directMapper, ISecureRandom $secureRandom, ITimeFactory $timeFactory, $userId) { $this->sessionMapper = $sessionMapper; + $this->directMapper = $directMapper; $this->secureRandom = $secureRandom; $this->timeFactory = $timeFactory; $this->userId = $userId; } - public function initSession($documentId, $guestName = null): Session { + public function initSession($documentId, $direct = false, $guestName = null): Session { + if ($direct) { + $this->userId = $direct->getUserId(); + } $session = new Session(); $session->setDocumentId($documentId); $userName = $this->userId ? $this->userId : $guestName; $session->setUserId($userName); $session->setToken($this->secureRandom->generate(64)); + $session->setDirect($direct); /** @var IAvatarManager $avatarGenerator */ $avatarGenerator = \OC::$server->query(IAvatarManager::class); $color = $avatarGenerator->getGuestAvatar($userName)->avatarBackgroundColor($userName); @@ -64,7 +72,8 @@ public function initSession($documentId, $guestName = null): Session { $session->setGuestName($guestName); } $session->setLastContact($this->timeFactory->getTime()); - return $this->sessionMapper->insert($session); + $this->session = $this->sessionMapper->insert($session); + return $this->session; } public function closeSession(int $documentId, int $sessionId, string $token): void { @@ -96,6 +105,54 @@ public function removeInactiveSessions($documentId = -1) { return $this->sessionMapper->deleteInactive($documentId); } + public function isDirectToken($token): bool { + try { + $this->directMapper->getByToken($token); + } catch (DoesNotExistException $e) { + return false; + } + return true; + } + + /** + * @return Session + */ + public function getSession(): Session { + return $this->session; + } + + /** + * @param $documentId + * @param $sessionId + * @param $token + * @return Session + * @throws DoesNotExistException + */ + public function loadSession($documentId, $sessionId, $token) { + $this->session = $this->sessionMapper->find($documentId, $sessionId, $token); + return $this->session; + } + + public function getDirect($token) { + try { + return $this->directMapper->getByToken($token); + } catch (DoesNotExistException $e) { + } + return null; + } + + public function isDirectSession() { + if ($this->session !== null) { + return $this->session->getDirect(); + } + return false; + } + + public function clearDirectSession($token) { + $direct = $this->directMapper->getByToken($token); + $this->directMapper->delete($direct); + } + public function isValidSession($documentId, $sessionId, $token) { try { $session = $this->sessionMapper->find($documentId, $sessionId, $token); From 893f42af11f9ca2837d06061111d8aa14f3c5bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 26 Aug 2019 16:01:59 +0200 Subject: [PATCH 2/7] Hand over direct token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- src/components/EditorWrapper.vue | 9 +++++++-- src/components/SessionList.vue | 2 +- src/main.js | 8 +++++--- src/services/PollingBackend.js | 8 ++++---- src/services/SyncService.js | 16 ++++++++++------ 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index 8b847c528fd..cf49fba56bb 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -105,6 +105,10 @@ export default { type: String, default: null }, + directToken: { + type: String, + default: null + }, mime: { type: String, default: null @@ -171,11 +175,11 @@ export default { }, backendUrl() { return (endpoint) => { - return endpointUrl(endpoint, !!this.shareToken) + return endpointUrl(endpoint, !!this.shareToken || !!this.directToken) } }, hasDocumentParameters() { - return this.fileId || this.shareToken + return this.fileId || !!this.shareToken || !!this.directToken }, isPublic() { return document.getElementById('isPublic') && document.getElementById('isPublic').value === '1' @@ -227,6 +231,7 @@ export default { const guestName = localStorage.getItem('nick') ? localStorage.getItem('nick') : getRandomGuestName() this.syncService = new SyncService({ shareToken: this.shareToken, + directToken: this.directToken, filePath: this.relativePath, guestName, forceRecreate: this.forceRecreate, diff --git a/src/components/SessionList.vue b/src/components/SessionList.vue index 8662c252b3e..9b4a32a120c 100644 --- a/src/components/SessionList.vue +++ b/src/components/SessionList.vue @@ -89,7 +89,7 @@ export default { }, activeSessions() { return Object.values(this.sessions).filter((session) => - session.lastContact > Date.now() / 1000 - COLLABORATOR_DISCONNECT_TIME && !session.isCurrent && session.userId !== null) + session.lastContact > Date.now() / 1000 - COLLABORATOR_DISCONNECT_TIME && !session.isCurrent) }, sessionStyle() { return (session) => { diff --git a/src/main.js b/src/main.js index 552d79e4185..9b4cc7aa561 100644 --- a/src/main.js +++ b/src/main.js @@ -15,12 +15,14 @@ if (document.getElementById('maineditor')) { const vm = new Vue({ render: h => h(Editor, { props: { - relativePath: '/welcome.md', - active: true + fileId: OCP.InitialState.loadState('text', 'direct_file_id'), + active: true, + directToken: OCP.InitialState.loadState('text', 'direct_token'), + mime: OCP.InitialState.loadState('text', 'direct_mime') } }) }) - vm.$mount(document.getElementById('preview')) + vm.$mount(document.getElementById('content')) }) } diff --git a/src/services/PollingBackend.js b/src/services/PollingBackend.js index 6a8f80c4515..464d3c1c1c9 100644 --- a/src/services/PollingBackend.js +++ b/src/services/PollingBackend.js @@ -70,7 +70,7 @@ class PollingBackend { } _isPublic() { - return !!this._authority.options.shareToken + return !!this._authority.options.shareToken || !!this._authority.options.directToken } forceSave() { @@ -110,7 +110,7 @@ class PollingBackend { autosaveContent, force: !!this._forcedSave, manualSave: !!this._manualSave, - token: this._authority.options.shareToken, + token: this._authority.options.shareToken || this._authority.session.token, filePath: this._authority.options.filePath }).then((response) => { this.fetchRetryCounter = 0 @@ -177,13 +177,13 @@ class PollingBackend { this.lock = true let sendable = (typeof _sendable === 'function') ? _sendable() : _sendable let steps = sendable.steps - axios.post(endpointUrl('session/push', !!this._authority.options.shareToken), { + axios.post(endpointUrl('session/push', !!this._isPublic()), { documentId: this._authority.document.id, sessionId: this._authority.session.id, sessionToken: this._authority.session.token, steps: steps.map(s => s.toJSON ? s.toJSON() : s) || [], version: sendable.version, - token: this._authority.options.shareToken, + token: this._authority.options.shareToken || this._authority.session.token, filePath: this._authority.options.filePath }).then((response) => { this.carefulRetryReset() diff --git a/src/services/SyncService.js b/src/services/SyncService.js index fdec914a191..e5869adabf4 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -112,12 +112,16 @@ class SyncService { this.backend.connect() } + _usePublicEndpoint() { + return !!this.options.shareToken || !!this.options.directToken + } + _openDocument({ fileId, filePath }) { - return axios.get(endpointUrl('session/create', !!this.options.shareToken), { + return axios.get(endpointUrl('session/create', this._usePublicEndpoint()), { params: { fileId: fileId, filePath, - token: this.options.shareToken, + token: this.options.shareToken ? this.options.shareToken : this.options.directToken, guestName: this.options.guestName, forceRecreate: this.options.forceRecreate } @@ -131,13 +135,13 @@ class SyncService { _fetchDocument() { return axios.get( - endpointUrl('session/fetch', !!this.options.shareToken), { + endpointUrl('session/fetch', this._usePublicEndpoint()), { transformResponse: [(data) => data], params: { documentId: this.document.id, sessionId: this.session.id, sessionToken: this.session.token, - token: this.options.shareToken + token: this.options.shareToken || 'foo' } } ) @@ -152,7 +156,7 @@ class SyncService { documentId: this.document.id, sessionId: this.session.id, sessionToken: this.session.token, - token: this.options.shareToken, + token: this.options.shareToken || this.session.token, guestName } ).then(({ data }) => { @@ -254,7 +258,7 @@ class SyncService { } this.backend.disconnect() return axios.get( - endpointUrl('session/close', !!this.options.shareToken), { + endpointUrl('session/close', this._usePublicEndpoint()), { params: { documentId: this.document.id, sessionId: this.session.id, From b5006eea05e36b3e9afe6abfccbd549595030170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 26 Aug 2019 16:15:32 +0200 Subject: [PATCH 3/7] Properly clean up direct tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/DirectMapper.php | 2 +- lib/Service/ApiService.php | 1 + src/components/EditorWrapper.vue | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Db/DirectMapper.php b/lib/Db/DirectMapper.php index 89c7568ae02..84302f07867 100644 --- a/lib/Db/DirectMapper.php +++ b/lib/Db/DirectMapper.php @@ -34,7 +34,7 @@ */ class DirectMapper extends QBMapper { - public const TOKEN_TTL = 600000; + public const TOKEN_TTL = 600; /** @var ISecureRandom */ protected $random; diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index 8e6a0a61456..db50ee3ca9a 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -85,6 +85,7 @@ public function create($fileId = null, $filePath = null, $token = null, $guestNa if ($token && $direct = $this->sessionService->getDirect($token)) { $file = $this->documentService->getFileById($direct->getFileId(), $direct->getUserId()); $readOnly = !$file->isUpdateable(); + $this->sessionService->clearDirectSession($token); } elseif ($token) { $file = $this->documentService->getFileByShareToken($token, $this->request->getParam('filePath')); try { diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index cf49fba56bb..49d1b24925c 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -21,7 +21,7 @@ -->