diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-05-14 10:34:30 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-05-14 10:34:30 +0300 |
commit | fe6272f411f502b5734f74270f62fed17a0a8156 (patch) | |
tree | a518a1d6b1ffb0def71ea30d72502909dc858cf2 | |
parent | fd1e342338fbf6007ccbcd4fc2a34cdf8c8b2140 (diff) |
Add public endpoints
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r-- | appinfo/app.php | 2 | ||||
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | appinfo/routes.php | 12 | ||||
-rw-r--r-- | lib/Controller/PublicSessionController.php | 74 | ||||
-rw-r--r-- | lib/Controller/SessionController.php | 103 | ||||
-rw-r--r-- | lib/Service/ApiService.php | 121 | ||||
-rw-r--r-- | lib/Service/DocumentService.php | 146 | ||||
-rw-r--r-- | lib/Service/SessionService.php | 2 |
8 files changed, 326 insertions, 136 deletions
diff --git a/appinfo/app.php b/appinfo/app.php index 8dd192622..2487dd64a 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -13,5 +13,5 @@ if (\OC::$server->getUserSession()->isLoggedIn()) { } $eventDispatcher->addListener('OCA\Files_Sharing::loadAdditionalScripts', function () { - // load public stuff + \OCP\Util::addScript('text', 'public'); }); diff --git a/appinfo/info.xml b/appinfo/info.xml index c603c65f5..6bf6bd037 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,7 +15,7 @@ <repository type="git">https://github.com/nextcloud/text.git</repository> <screenshot>https://raw.githubusercontent.com/nextcloud/text/master/img/screenshot.png</screenshot> <dependencies> - <nextcloud min-version="17" max-version="17"/> + <nextcloud min-version="16" max-version="17"/> </dependencies> <navigations> <navigation> diff --git a/appinfo/routes.php b/appinfo/routes.php index d0bf8c9fd..6dc7cfefb 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -7,15 +7,17 @@ return [ 'routes' => [ ['name' => 'Navigation#navigate', 'url' => '/', 'verb' => 'GET'], - // Setup a new session ['name' => 'Session#create', 'url' => '/session/create', 'verb' => 'GET'], - // Load initial document ['name' => 'Session#fetch', 'url' => '/session/fetch', 'verb' => 'GET'], - // Load steps ['name' => 'Session#sync', 'url' => '/session/sync', 'verb' => 'POST'], - // Push own steps ['name' => 'Session#push', 'url' => '/session/push', 'verb' => 'POST'], - // Close session ['name' => 'Session#close', 'url' => '/session/close', 'verb' => 'GET'], + + ['name' => 'PublicSession#create', 'url' => '/public/session/create', 'verb' => 'GET'], + ['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'], + ] ]; diff --git a/lib/Controller/PublicSessionController.php b/lib/Controller/PublicSessionController.php index 446b02fde..ee5b446fc 100644 --- a/lib/Controller/PublicSessionController.php +++ b/lib/Controller/PublicSessionController.php @@ -26,26 +26,80 @@ declare(strict_types=1); namespace OCA\Text\Controller; -use OCP\AppFramework\Controller; +use OCA\Text\Service\ApiService; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\PublicShareController; +use OCP\ISession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\FileDisplayResponse; -use OCP\Files\IRootFolder; -use OCP\ICacheFactory; use OCP\IRequest; -use OCP\ITempManager; -use OCP\Security\ISecureRandom; +use OCP\Share\IShare; -class PublicSessionController extends SessionController { +class PublicSessionController extends PublicShareController { + + /** @var ShareManager */ + private $shareManager; + + /** @var IShare */ + private $share; + + /** @var ApiService */ + private $apiService; + + public function __construct(string $appName, IRequest $request, ISession $session, ShareManager $shareManager, ApiService $apiService) { + parent::__construct($appName, $request, $session); + $this->shareManager = $shareManager; + $this->apiService = $apiService; + } + + protected function getPasswordHash(): string { + return $this->share->getPassword(); + } + + public function isValidToken(): bool { + try { + $this->share = $this->shareManager->getShareByToken($this->getToken()); + return true; + } catch (ShareNotFound $e) { + return false; + } + } + + protected function isPasswordProtected(): bool { + return $this->share->getPassword() !== null; + } /** - * TODO: maybe set guestUserId in middleware + * @NoAdminRequired + * @PublicPage */ + public function create(string $token, string $file = null): DataResponse { + return $this->apiService->create(null, $file, $token); + } + + /** + * @NoAdminRequired + * @PublicPage + */ + public function fetch(int $documentId, string $sessionId, string $sessionToken): Response { + return $this->apiService->fetch($documentId, $sessionId, $sessionToken); + } + + /** + * @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); + } /** + * @NoAdminRequired * @PublicPage */ - public function push($transaction): DataResponse { - parent::push($transaction); + 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); } } diff --git a/lib/Controller/SessionController.php b/lib/Controller/SessionController.php index 58a2dd7d4..aff55f0bc 100644 --- a/lib/Controller/SessionController.php +++ b/lib/Controller/SessionController.php @@ -4,124 +4,57 @@ declare(strict_types=1); namespace OCA\Text\Controller; -use OC\Files\Node\File; -use OCA\Text\Service\DocumentService; -use OCA\Text\Service\SessionService; -use OCA\Text\VersionMismatchException; +use OCA\Text\Service\ApiService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\FileDisplayResponse; -use OCP\AppFramework\Http\NotFoundResponse; -use OCP\Files\IRootFolder; -use OCP\Files\NotFoundException; -use OCP\ICacheFactory; +use OCP\AppFramework\Http\Response; use OCP\IRequest; -use OCP\ITempManager; -use OCP\Security\ISecureRandom; -use OCA\Text\DocumentSaveConflictException; class SessionController extends Controller { - private $cache; - private $sessionService; - private $documentService; + /** + * @var ApiService + */ + private $apiService; - public function __construct(string $appName, IRequest $request, ICacheFactory $cacheFactory, SessionService $sessionService, DocumentService $documentService) { + public function __construct(string $appName, IRequest $request, ApiService $apiService) { parent::__construct($appName, $request); - - $this->cache = $cacheFactory->createDistributed('textSession'); - $this->sessionService = $sessionService; - $this->documentService = $documentService; + $this->apiService = $apiService; } /** - * Initialize the session as a client so it can use the other methods - * - * @NoCSRFRequired * @NoAdminRequired */ - public function create($file) { - $document = $this->documentService->createDocumentByPath($file); - $session = $this->sessionService->initSession($document->getId()); - return new DataResponse([ - 'document' => $document, - 'session' => $session - ]); + public function create(int $fileId = null, string $file = null): DataResponse { + return $this->apiService->create($fileId, $file); } /** - * - * - * @NoCSRFRequired * @NoAdminRequired */ - public function fetch($documentId, $sessionId, $token) { - if ($this->sessionService->isValidSession($documentId, $sessionId, $token)) { - $this->sessionService->removeInactiveSessions($documentId); - $file = $this->documentService->getBaseFile($documentId); - return new FileDisplayResponse($file); - } - return new NotFoundResponse(); + public function fetch(int $documentId, int $sessionId, string $sessionToken): Response { + return $this->apiService->fetch($documentId, $sessionId, $sessionToken); } /** - * Close existing session when quiting the client gracefully - * This reduces some cleanup work if used by the client - * - * @NoCSRFRequired * @NoAdminRequired */ - public function close($documentId, $sessionId, $token): DataResponse { - // TODO: To implement - return new DataResponse([]); + public function close(int $documentId, int $sessionId, string $sessionToken): DataResponse { + return $this->apiService->close($documentId, $sessionId, $sessionToken); } /** - * Client tries to commit a set of transactions to the document - * - * @NoCSRFRequired * @NoAdminRequired */ - public function push($documentId, $sessionId, $token, $version, $steps): DataResponse { - if ($this->sessionService->isValidSession($documentId, $sessionId, $token)) { - try { - $steps = $this->documentService->addStep($documentId, $sessionId, $steps, $version); - } catch (VersionMismatchException $e) { - return new DataResponse($e->getMessage(), $e->getStatus()); - } - return new DataResponse($steps); - } - return new DataResponse([], 500); + public function push(int $documentId, int $sessionId, string $sessionToken, int $version, array $steps): DataResponse { + return $this->apiService->push($documentId, $sessionId, $sessionToken, $version, $steps); } /** - * Eventsource based handler - * - * @NoCSRFRequired * @NoAdminRequired - * @PublicPage */ - public function sync($documentId, $sessionId, $token, $version = 0, $autosaveContent = null, bool $force = false, bool $manualSave = false): DataResponse { - if (!$this->sessionService->isValidSession($documentId, $sessionId, $token)) { - return new DataResponse([], 500); - } - if ($version === $this->cache->get('document-version-'.$documentId)) { - return new DataResponse(['steps' => []]); - } - try { - $document = $this->documentService->autosave($documentId, $version, $autosaveContent, $force, $manualSave); - } catch (DocumentSaveConflictException $e) { - /** @var File $file */ - $file = $this->documentService->getFile($documentId); - return new DataResponse([ - 'outsideChange' => $file->getContent() - ], 409); - } - return new DataResponse([ - 'steps' => $this->documentService->getSteps($documentId, $version), - 'sessions' => $this->sessionService->getActiveSessions($documentId), - 'document' => $document - ]); + 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/Service/ApiService.php b/lib/Service/ApiService.php new file mode 100644 index 000000000..07ea15e8f --- /dev/null +++ b/lib/Service/ApiService.php @@ -0,0 +1,121 @@ +<?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\Service; + + +use OCA\Text\DocumentSaveConflictException; +use OCA\Text\VersionMismatchException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\Files\NotFoundException; +use OCP\ICacheFactory; +use OCP\IRequest; + +class ApiService { + + protected $cache; + protected $sessionService; + protected $documentService; + + public function __construct(string $appName, IRequest $request, ICacheFactory $cacheFactory, SessionService $sessionService, DocumentService $documentService) { + $this->cache = $cacheFactory->createDistributed('textSession'); + $this->sessionService = $sessionService; + $this->documentService = $documentService; + } + + public function create($fileId = null, $file = null, $token = null): DataResponse { + try { + if ($token) { + $document = $this->documentService->createDocumentByShareToken($token, $file); + } else if ($fileId) { + $document = $this->documentService->createDocumentByFileId($fileId); + } else if ($file) { + $document = $this->documentService->createDocumentByPath($file); + } else { + return new DataResponse('No valid file argument provided', 500); + } + } catch (\Exception $e) { + return new DataResponse($e->getMessage(), 500); + } + $session = $this->sessionService->initSession($document->getId()); + return new DataResponse([ + 'document' => $document, + 'session' => $session + ]); + } + + public function fetch($documentId, $sessionId, $sessionToken) { + if ($this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + $this->sessionService->removeInactiveSessions($documentId); + try { + $file = $this->documentService->getBaseFile($documentId); + } catch (NotFoundException $e) { + return new NotFoundResponse(); + } + return new FileDisplayResponse($file); + } + return new NotFoundResponse(); + } + + public function close($documentId, $sessionId, $sessionToken): DataResponse { + // TODO: To implement + return new DataResponse([]); + } + + public function push($documentId, $sessionId, $sessionToken, $version, $steps): DataResponse { + if ($this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + try { + $steps = $this->documentService->addStep($documentId, $sessionId, $steps, $version); + } catch (VersionMismatchException $e) { + return new DataResponse($e->getMessage(), $e->getStatus()); + } + return new DataResponse($steps); + } + return new DataResponse([], 500); + } + + public function sync($documentId, $sessionId, $sessionToken, $version = 0, $autosaveContent = null, bool $force = false, bool $manualSave = false, $token = null): DataResponse { + if (!$this->sessionService->isValidSession($documentId, $sessionId, $sessionToken)) { + return new DataResponse([], 500); + } + if ($version === $this->cache->get('document-version-'.$documentId)) { + return new DataResponse(['steps' => []]); + } + try { + $document = $this->documentService->autosave($documentId, $version, $autosaveContent, $force, $manualSave, $token); + } catch (DocumentSaveConflictException $e) { + /** @var \OC\Files\Node\File $file */ + $file = $this->documentService->getFileByShareToken($token); + return new DataResponse([ + 'outsideChange' => $file->getContent() + ], 409); + } + return new DataResponse([ + 'steps' => $this->documentService->getSteps($documentId, $version), + 'sessions' => $this->sessionService->getActiveSessions($documentId), + 'document' => $document + ]); + } +} diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 2ccc588ae..af9570610 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -23,42 +23,73 @@ namespace OCA\Text\Service; -use http\Exception\InvalidArgumentException; +use \InvalidArgumentException; use function json_encode; use OC\Files\Node\File; use OCA\Text\Db\Document; use OCA\Text\Db\DocumentMapper; -use OCA\Text\Db\SessionMapper; use OCA\Text\Db\Step; use OCA\Text\Db\StepMapper; use OCA\Text\DocumentSaveConflictException; use OCA\Text\VersionMismatchException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\Entity; +use OCP\Constants; +use OCP\Files\Folder; +use OCP\Files\GenericFileException; use OCP\Files\IAppData; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\ICache; use OCP\ICacheFactory; use OCP\ILogger; -use OCP\Lock\ILockingProvider; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; class DocumentService { /** * Delay to wait for between autosave versions */ - const AUTOSAVE_MINIMUM_DELAY = 60; + public const AUTOSAVE_MINIMUM_DELAY = 60; - private $sessionMapper; + /** + * @var string|null + */ private $userId; + /** + * @var DocumentMapper + */ private $documentMapper; + /** + * @var ILogger + */ private $logger; + /** + * @var ShareManager + */ + private $shareManager; + /** + * @var StepMapper + */ + private $stepMapper; + /** + * @var IRootFolder + */ + private $rootFolder; + /** + * @var ICache + */ + private $cache; + /** + * @var IAppData + */ + private $appData; - public function __construct(SessionMapper $sessionMapper, DocumentMapper $documentMapper, StepMapper $stepMapper, IAppData $appData, $userId, IRootFolder $rootFolder, ICacheFactory $cacheFactory, ILogger $logger) { - $this->sessionMapper = $sessionMapper; + public function __construct(DocumentMapper $documentMapper, StepMapper $stepMapper, IAppData $appData, $userId, IRootFolder $rootFolder, ICacheFactory $cacheFactory, ILogger $logger, ShareManager $shareManager) { $this->documentMapper = $documentMapper; $this->stepMapper = $stepMapper; $this->userId = $userId; @@ -66,7 +97,7 @@ class DocumentService { $this->rootFolder = $rootFolder; $this->cache = $cacheFactory->createDistributed('text'); $this->logger = $logger; - + $this->shareManager = $shareManager; try { $this->appData->getFolder('documents'); } catch (NotFoundException $e) { @@ -94,30 +125,26 @@ class DocumentService { * @throws InvalidPathException * @throws NotPermittedException */ - public function getDocumentById($fileId) { - return $this->createDocument($this->getFile($fileId)); + public function createDocumentByFileId($fileId) { + $file = $this->getFileById($fileId); + return $this->createDocument($file); } - public function getFile($fileId) { - /** @var File $file */ - return $this->rootFolder->getUserFolder($this->userId)->getById($fileId)[0]; + /** + * @param $shareToken + * @param null $filePath + * @return Entity + * @throws InvalidPathException + * @throws NotFoundException + * @throws NotPermittedException + */ + public function createDocumentByShareToken($shareToken, $filePath = null) { + $file = $this->getFileByShareToken($shareToken, $filePath); + return $this->createDocument($file); } - public function getFileForShare($token) { - - $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); - $originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath()); - // Single file share - if ($share->getNode() instanceof \OCP\Files\File) { - // Single file download - return $share->getNode(); - } - - return null; - } - /** * @param File $file * @return Entity @@ -125,7 +152,7 @@ class DocumentService { * @throws InvalidPathException * @throws NotPermittedException */ - protected function createDocument(File $file) { + protected function createDocument(File $file): Document { try { $document = $this->documentMapper->find($file->getFileInfo()->getId()); @@ -174,7 +201,7 @@ class DocumentService { * @return ISimpleFile * @throws NotFoundException */ - public function getBaseFile($document) { + public function getBaseFile($document): ISimpleFile { return $this->appData->getFolder('documents')->getFile($document); } @@ -187,7 +214,7 @@ class DocumentService { * @throws DoesNotExistException * @throws VersionMismatchException */ - public function addStep($documentId, $sessionId, $steps, $version) { + public function addStep($documentId, $sessionId, $steps, $version): array { // TODO check cache $document = $this->documentMapper->find($documentId); if ($version !== $document->getCurrentVersion()) { @@ -200,7 +227,7 @@ class DocumentService { $newVersion = $document->getCurrentVersion() + count($steps); $document->setCurrentVersion($newVersion); $this->documentMapper->update($document); - $step = new Step($stepsJson); + $step = new Step(); $step->setData($stepsJson); $step->setSessionId($sessionId); $step->setDocumentId($documentId); @@ -222,20 +249,27 @@ class DocumentService { * @param $autoaveDocument * @param bool $force * @param bool $manualSave + * @param null $token * @return Document * @throws DocumentSaveConflictException * @throws DoesNotExistException + * @throws GenericFileException * @throws InvalidPathException * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\GenericFileException + * @throws ShareNotFound */ - public function autosave($documentId, $version, $autoaveDocument, $force = false, $manualSave = false) { + public function autosave($documentId, $version, $autoaveDocument, $force = false, $manualSave = false, $token = null): Document { /** @var Document $document */ $document = $this->documentMapper->find($documentId); /** @var File $file */ - $file = $this->rootFolder->getUserFolder($this->userId)->getById($documentId)[0]; + if (!$token) { + $file = $this->rootFolder->getUserFolder($this->userId)->getById($documentId)[0]; + } else { + $share = $this->shareManager->getShareByToken($token); + $file = $share->getNode(); + } $lastMTime = $document->getLastSavedVersionTime(); if ($lastMTime > 0 && $file->getEtag() !== $document->getLastSavedVersionEtag() && $force === false) { @@ -278,4 +312,50 @@ class DocumentService { } } + public function getFileById($fileId) { + /** @var File $file */ + return $this->rootFolder->getUserFolder($this->userId)->getById($fileId)[0]; + } + + /** + * @param $shareToken + * @param null|string $path + * @return \OCP\Files\File|Folder|\OCP\Files\Node + * @throws NotFoundException + */ + public function getFileByShareToken($shareToken, $path = null) { + try { + $share = $this->shareManager->getShareByToken($shareToken); + + } catch (ShareNotFound $e) { + throw new NotFoundException(); + } + + $node = $share->getNode(); + if ($node instanceof \OCP\Files\File) { + return $node; + } + if ($node instanceof Folder) { + return $node->get($path); + } + throw new \InvalidArgumentException('No proper share data'); + } + + /** + * @param $shareToken + * @return void + * @throws NotFoundException + */ + public function checkSharePermissions($shareToken, $permission = Constants::PERMISSION_READ): void { + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound $e) { + throw new NotFoundException(); + } + + if (($share->getPermissions() & $permission) === 0) { + throw new NotFoundException(); + } + } + } diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index 94bc145ad..c46a1fef4 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -46,7 +46,7 @@ class SessionService { $this->sessionMapper = $sessionMapper; $this->secureRandom = $secureRandom; $this->timeFactory = $timeFactory; - $this->userId = $userId; + $this->userId = $userId ?? 'Guest'; } public function initSession($documentId): Session { |