From e52793c69ef3633f23a93f4358c361431f401569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 14 Oct 2019 16:55:39 +0200 Subject: [PATCH 1/9] Direct editing API to allow file editing using a one-time token for mobile apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/appinfo/info.xml | 2 +- apps/files/appinfo/routes.php | 27 ++ .../composer/composer/autoload_classmap.php | 3 + .../composer/composer/autoload_static.php | 3 + .../Controller/DirectEditingController.php | 156 ++++++++++++ .../DirectEditingViewController.php | 72 ++++++ .../Version18000Date20191014105105.php | 95 +++++++ core/routes.php | 3 +- lib/composer/composer/autoload_classmap.php | 10 + lib/composer/composer/autoload_static.php | 10 + lib/private/DirectEditing/Manager.php | 232 ++++++++++++++++++ lib/private/DirectEditing/Token.php | 76 ++++++ lib/private/Server.php | 2 + lib/public/DirectEditing/ACreateEmpty.php | 71 ++++++ .../DirectEditing/ACreateFromTemplate.php | 39 +++ lib/public/DirectEditing/ATemplate.php | 71 ++++++ lib/public/DirectEditing/IEditor.php | 99 ++++++++ lib/public/DirectEditing/IManager.php | 88 +++++++ lib/public/DirectEditing/IToken.php | 77 ++++++ .../RegisterDirectEditorEvent.php | 57 +++++ version.php | 2 +- 21 files changed, 1192 insertions(+), 3 deletions(-) create mode 100644 apps/files/lib/Controller/DirectEditingController.php create mode 100644 apps/files/lib/Controller/DirectEditingViewController.php create mode 100644 core/Migrations/Version18000Date20191014105105.php create mode 100644 lib/private/DirectEditing/Manager.php create mode 100644 lib/private/DirectEditing/Token.php create mode 100644 lib/public/DirectEditing/ACreateEmpty.php create mode 100644 lib/public/DirectEditing/ACreateFromTemplate.php create mode 100644 lib/public/DirectEditing/ATemplate.php create mode 100644 lib/public/DirectEditing/IEditor.php create mode 100644 lib/public/DirectEditing/IManager.php create mode 100644 lib/public/DirectEditing/IToken.php create mode 100644 lib/public/DirectEditing/RegisterDirectEditorEvent.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index 7f3b3a5fca..d8c61d0f45 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -5,7 +5,7 @@ Files File Management File Management - 1.13.0 + 1.13.1 agpl Robin Appelman Vincent Petry diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 26fce8d171..6f8a6878aa 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -92,6 +92,33 @@ $application->registerRoutes( 'url' => '/api/v1/quickaccess/get/NodeType', 'verb' => 'GET', ], + [ + 'name' => 'DirectEditingView#edit', + 'url' => '/directEditing/{token}', + 'verb' => 'GET' + ], + ], + 'ocs' => [ + [ + 'name' => 'DirectEditing#get', + 'url' => '/api/v1/directEditing', + 'verb' => 'GET' + ], + [ + 'name' => 'DirectEditing#templates', + 'url' => '/api/v1/directEditing/templates/{editorId}/{creatorId}', + 'verb' => 'GET' + ], + [ + 'name' => 'DirectEditing#open', + 'url' => '/api/v1/directEditing/open', + 'verb' => 'POST' + ], + [ + 'name' => 'DirectEditing#create', + 'url' => '/api/v1/directEditing/create', + 'verb' => 'POST' + ], ] ] ); diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 6a6584b013..462e51ffe0 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -19,6 +19,7 @@ return array( 'OCA\\Files\\Activity\\Settings\\FileRestored' => $baseDir . '/../lib/Activity/Settings/FileRestored.php', 'OCA\\Files\\App' => $baseDir . '/../lib/App.php', 'OCA\\Files\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => $baseDir . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php', 'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => $baseDir . '/../lib/BackgroundJob/CleanupFileLocks.php', 'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => $baseDir . '/../lib/BackgroundJob/DeleteOrphanedItems.php', 'OCA\\Files\\BackgroundJob\\ScanFiles' => $baseDir . '/../lib/BackgroundJob/ScanFiles.php', @@ -31,6 +32,8 @@ return array( 'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\AjaxController' => $baseDir . '/../lib/Controller/AjaxController.php', 'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', + 'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', + 'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php', 'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => $baseDir . '/../lib/Event/LoadAdditionalScriptsEvent.php', 'OCA\\Files\\Event\\LoadSidebar' => $baseDir . '/../lib/Event/LoadSidebar.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index b1ba7fdc54..b41faa6f8c 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -34,6 +34,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Activity\\Settings\\FileRestored' => __DIR__ . '/..' . '/../lib/Activity/Settings/FileRestored.php', 'OCA\\Files\\App' => __DIR__ . '/..' . '/../lib/App.php', 'OCA\\Files\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php', 'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupFileLocks.php', 'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOrphanedItems.php', 'OCA\\Files\\BackgroundJob\\ScanFiles' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScanFiles.php', @@ -46,6 +47,8 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\AjaxController' => __DIR__ . '/..' . '/../lib/Controller/AjaxController.php', 'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', + 'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', + 'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', 'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php', 'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => __DIR__ . '/..' . '/../lib/Event/LoadAdditionalScriptsEvent.php', 'OCA\\Files\\Event\\LoadSidebar' => __DIR__ . '/..' . '/../lib/Event/LoadSidebar.php', diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php new file mode 100644 index 0000000000..cf948d20f7 --- /dev/null +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -0,0 +1,156 @@ + + * + * @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\Files\Controller; + + +use Exception; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\DirectEditing\ACreateEmpty; +use OCP\DirectEditing\ACreateFromTemplate; +use OCP\DirectEditing\IEditor; +use OCP\DirectEditing\IManager; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IURLGenerator; + +class DirectEditingController extends OCSController { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IManager */ + private $directEditingManager; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var ILogger */ + private $logger; + + public function __construct($appName, IRequest $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge, + IEventDispatcher $eventDispatcher, IURLGenerator $urlGenerator, IManager $manager, ILogger $logger) { + parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge); + + $this->eventDispatcher = $eventDispatcher; + $this->directEditingManager = $manager; + $this->logger = $logger; + $this->urlGenerator = $urlGenerator; + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function get(): DataResponse { + $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + + $capabilities = [ + 'editors' => [], + 'creators' => [] + ]; + + /** + * @var string $id + * @var IEditor $editor + */ + foreach ($this->directEditingManager->getEditors() as $id => $editor) { + $capabilities['editors'][$id] = [ + 'name' => $editor->getName(), + 'mimetypes' => $editor->getMimetypes(), + 'optionalMimetypes' => $editor->getMimetypesOptional(), + 'secure' => $editor->isSecure(), + ]; + /** @var ACreateEmpty|ACreateFromTemplate $creator */ + foreach ($editor->getCreators() as $creator) { + $id = $creator->getId(); + $capabilities['creators'][$id] = [ + 'id' => $id, + 'name' => $creator->getName(), + 'extension' => $creator->getExtension(), + 'templates' => false + ]; + if ($creator instanceof ACreateFromTemplate) { + $capabilities['creators'][$id]['templates'] = true; + } + + } + } + return new DataResponse($capabilities); + } + + /** + * @NoAdminRequired + */ + public function create(string $path, string $editorId, string $creatorId, string $templateId = null): DataResponse { + $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + + try { + $token = $this->directEditingManager->create($path, $editorId, $creatorId, $templateId); + return new DataResponse([ + 'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', ['token' => $token]) + ]); + } catch (Exception $e) { + $this->logger->logException($e, ['message' => 'Exception when creating a new file through direct editing']); + return new DataResponse('Failed to create file', Http::STATUS_FORBIDDEN); + } + } + + /** + * @NoAdminRequired + */ + public function open(int $fileId, string $editorId = null): DataResponse { + $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + + try { + $token = $this->directEditingManager->open($fileId, $editorId); + return new DataResponse([ + 'url' => $this->urlGenerator->linkToRouteAbsolute('files.DirectEditingView.edit', ['token' => $token]) + ]); + } catch (Exception $e) { + $this->logger->logException($e, ['message' => 'Exception when opening a file through direct editing']); + return new DataResponse('Failed to open file', Http::STATUS_FORBIDDEN); + } + } + + + + /** + * @NoAdminRequired + */ + public function templates(string $editorId, string $creatorId): DataResponse { + $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + + try { + return new DataResponse($this->directEditingManager->getTemplates($editorId, $creatorId)); + } catch (Exception $e) { + $this->logger->logException($e); + return new DataResponse('Failed to open file', Http::STATUS_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/apps/files/lib/Controller/DirectEditingViewController.php b/apps/files/lib/Controller/DirectEditingViewController.php new file mode 100644 index 0000000000..9fbce4ece1 --- /dev/null +++ b/apps/files/lib/Controller/DirectEditingViewController.php @@ -0,0 +1,72 @@ + + * + * @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\Files\Controller; + + +use Exception; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\Response; +use OCP\DirectEditing\IManager; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ILogger; +use OCP\IRequest; + +class DirectEditingViewController extends Controller { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IManager */ + private $directEditingManager; + + /** @var ILogger */ + private $logger; + + public function __construct($appName, IRequest $request, IEventDispatcher $eventDispatcher, IManager $manager, ILogger $logger) { + parent::__construct($appName, $request); + + $this->eventDispatcher = $eventDispatcher; + $this->directEditingManager = $manager; + $this->logger = $logger; + } + + /** + * @PublicPage + * @NoCSRFRequired + * + * @param string $token + * @return Response + */ + public function edit(string $token): Response { + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); + try { + return $this->directEditingManager->edit($token); + } catch (Exception $e) { + $this->logger->logException($e); + return new NotFoundResponse(); + } + } +} diff --git a/core/Migrations/Version18000Date20191014105105.php b/core/Migrations/Version18000Date20191014105105.php new file mode 100644 index 0000000000..b291c0b5e4 --- /dev/null +++ b/core/Migrations/Version18000Date20191014105105.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 . + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use Doctrine\DBAL\Types\Type; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version18000Date20191014105105 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @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): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if (!$schema->hasTable('direct_edit')) { + $table = $schema->createTable('direct_edit'); + + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('editor_id', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('token', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('file_id', Type::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('user_id', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('share_id', Type::BIGINT, [ + 'notnull' => false + ]); + $table->addColumn('timestamp', Type::BIGINT, [ + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $table->addColumn('accessed', Type::BOOLEAN, [ + 'notnull' => true, + 'default' => false + ]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['token']); + } + + return $schema; + } + +} diff --git a/core/routes.php b/core/routes.php index 0e6efae59b..51be391b54 100644 --- a/core/routes.php +++ b/core/routes.php @@ -118,7 +118,8 @@ $application->registerRoutes($this, [ ['root' => '/collaboration', 'name' => 'CollaborationResources#removeResource', 'url' => '/resources/collections/{collectionId}', 'verb' => 'DELETE'], ['root' => '/collaboration', 'name' => 'CollaborationResources#getCollectionsByResource', 'url' => '/resources/{resourceType}/{resourceId}', 'verb' => 'GET'], - ['root' => '/collaboration', 'name' => 'CollaborationResources#createCollectionOnResource', 'url' => '/resources/{baseResourceType}/{baseResourceId}', 'verb' => 'POST'], + ['root' => '/collaboration', 'name' => 'CollaborationResources#createCollectionOnResource', 'url' => '/resources/{baseResourceType}/{baseResourceId}', 'verb' => 'POST'] + ], ]); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 3d73c7b690..900a03a0f3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -170,6 +170,13 @@ return array( 'OCP\\Diagnostics\\IEventLogger' => $baseDir . '/lib/public/Diagnostics/IEventLogger.php', 'OCP\\Diagnostics\\IQuery' => $baseDir . '/lib/public/Diagnostics/IQuery.php', 'OCP\\Diagnostics\\IQueryLogger' => $baseDir . '/lib/public/Diagnostics/IQueryLogger.php', + 'OCP\\DirectEditing\\ACreateEmpty' => $baseDir . '/lib/public/DirectEditing/ACreateEmpty.php', + 'OCP\\DirectEditing\\ACreateFromTemplate' => $baseDir . '/lib/public/DirectEditing/ACreateFromTemplate.php', + 'OCP\\DirectEditing\\ATemplate' => $baseDir . '/lib/public/DirectEditing/ATemplate.php', + 'OCP\\DirectEditing\\IEditor' => $baseDir . '/lib/public/DirectEditing/IEditor.php', + 'OCP\\DirectEditing\\IManager' => $baseDir . '/lib/public/DirectEditing/IManager.php', + 'OCP\\DirectEditing\\IToken' => $baseDir . '/lib/public/DirectEditing/IToken.php', + 'OCP\\DirectEditing\\RegisterDirectEditorEvent' => $baseDir . '/lib/public/DirectEditing/RegisterDirectEditorEvent.php', 'OCP\\Encryption\\Exceptions\\GenericEncryptionException' => $baseDir . '/lib/public/Encryption/Exceptions/GenericEncryptionException.php', 'OCP\\Encryption\\IEncryptionModule' => $baseDir . '/lib/public/Encryption/IEncryptionModule.php', 'OCP\\Encryption\\IFile' => $baseDir . '/lib/public/Encryption/IFile.php', @@ -789,6 +796,7 @@ return array( 'OC\\Core\\Migrations\\Version16000Date20190428150708' => $baseDir . '/core/Migrations/Version16000Date20190428150708.php', 'OC\\Core\\Migrations\\Version17000Date20190514105811' => $baseDir . '/core/Migrations/Version17000Date20190514105811.php', 'OC\\Core\\Migrations\\Version18000Date20190920085628' => $baseDir . '/core/Migrations/Version18000Date20190920085628.php', + 'OC\\Core\\Migrations\\Version18000Date20191014105105' => $baseDir . '/core/Migrations/Version18000Date20191014105105.php', 'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => $baseDir . '/core/Notification/RemoveLinkSharesNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', @@ -840,6 +848,8 @@ return array( 'OC\\Diagnostics\\EventLogger' => $baseDir . '/lib/private/Diagnostics/EventLogger.php', 'OC\\Diagnostics\\Query' => $baseDir . '/lib/private/Diagnostics/Query.php', 'OC\\Diagnostics\\QueryLogger' => $baseDir . '/lib/private/Diagnostics/QueryLogger.php', + 'OC\\DirectEditing\\Manager' => $baseDir . '/lib/private/DirectEditing/Manager.php', + 'OC\\DirectEditing\\Token' => $baseDir . '/lib/private/DirectEditing/Token.php', 'OC\\Encryption\\DecryptAll' => $baseDir . '/lib/private/Encryption/DecryptAll.php', 'OC\\Encryption\\EncryptionWrapper' => $baseDir . '/lib/private/Encryption/EncryptionWrapper.php', 'OC\\Encryption\\Exceptions\\DecryptionFailedException' => $baseDir . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index e00acfbfdb..84532fabf5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -199,6 +199,13 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Diagnostics\\IEventLogger' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IEventLogger.php', 'OCP\\Diagnostics\\IQuery' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IQuery.php', 'OCP\\Diagnostics\\IQueryLogger' => __DIR__ . '/../../..' . '/lib/public/Diagnostics/IQueryLogger.php', + 'OCP\\DirectEditing\\ACreateEmpty' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/ACreateEmpty.php', + 'OCP\\DirectEditing\\ACreateFromTemplate' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/ACreateFromTemplate.php', + 'OCP\\DirectEditing\\ATemplate' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/ATemplate.php', + 'OCP\\DirectEditing\\IEditor' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/IEditor.php', + 'OCP\\DirectEditing\\IManager' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/IManager.php', + 'OCP\\DirectEditing\\IToken' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/IToken.php', + 'OCP\\DirectEditing\\RegisterDirectEditorEvent' => __DIR__ . '/../../..' . '/lib/public/DirectEditing/RegisterDirectEditorEvent.php', 'OCP\\Encryption\\Exceptions\\GenericEncryptionException' => __DIR__ . '/../../..' . '/lib/public/Encryption/Exceptions/GenericEncryptionException.php', 'OCP\\Encryption\\IEncryptionModule' => __DIR__ . '/../../..' . '/lib/public/Encryption/IEncryptionModule.php', 'OCP\\Encryption\\IFile' => __DIR__ . '/../../..' . '/lib/public/Encryption/IFile.php', @@ -818,6 +825,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version16000Date20190428150708' => __DIR__ . '/../../..' . '/core/Migrations/Version16000Date20190428150708.php', 'OC\\Core\\Migrations\\Version17000Date20190514105811' => __DIR__ . '/../../..' . '/core/Migrations/Version17000Date20190514105811.php', 'OC\\Core\\Migrations\\Version18000Date20190920085628' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20190920085628.php', + 'OC\\Core\\Migrations\\Version18000Date20191014105105' => __DIR__ . '/../../..' . '/core/Migrations/Version18000Date20191014105105.php', 'OC\\Core\\Notification\\RemoveLinkSharesNotifier' => __DIR__ . '/../../..' . '/core/Notification/RemoveLinkSharesNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', @@ -869,6 +877,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Diagnostics\\EventLogger' => __DIR__ . '/../../..' . '/lib/private/Diagnostics/EventLogger.php', 'OC\\Diagnostics\\Query' => __DIR__ . '/../../..' . '/lib/private/Diagnostics/Query.php', 'OC\\Diagnostics\\QueryLogger' => __DIR__ . '/../../..' . '/lib/private/Diagnostics/QueryLogger.php', + 'OC\\DirectEditing\\Manager' => __DIR__ . '/../../..' . '/lib/private/DirectEditing/Manager.php', + 'OC\\DirectEditing\\Token' => __DIR__ . '/../../..' . '/lib/private/DirectEditing/Token.php', 'OC\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/lib/private/Encryption/DecryptAll.php', 'OC\\Encryption\\EncryptionWrapper' => __DIR__ . '/../../..' . '/lib/private/Encryption/EncryptionWrapper.php', 'OC\\Encryption\\Exceptions\\DecryptionFailedException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php', diff --git a/lib/private/DirectEditing/Manager.php b/lib/private/DirectEditing/Manager.php new file mode 100644 index 0000000000..085a7de557 --- /dev/null +++ b/lib/private/DirectEditing/Manager.php @@ -0,0 +1,232 @@ + + * + * @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 OC\DirectEditing; + +use Doctrine\DBAL\FetchMode; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DirectEditing\ACreateFromTemplate; +use OCP\DirectEditing\IEditor; +use \OCP\DirectEditing\IManager; +use OCP\DirectEditing\IToken; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Share\IShare; + +class Manager implements IManager { + + private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ; + + public const TABLE_TOKENS = 'direct_edit'; + + /** @var IEditor[] */ + private $editors; + + /** @var IDBConnection */ + private $connection; + /** + * @var ISecureRandom + */ + private $random; + private $userId; + private $rootFolder; + + public function __construct( + ISecureRandom $random, + IDBConnection $connection, + IUserSession $userSession, + IRootFolder $rootFolder + ) { + $this->random = $random; + $this->connection = $connection; + $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; + $this->rootFolder = $rootFolder; + } + + public function registerDirectEditor(IEditor $directEditor): void { + $this->editors[$directEditor->getId()] = $directEditor; + } + + public function getEditors(): array { + return $this->editors; + } + + public function getTemplates(string $editor, string $type): array { + if (!array_key_exists($editor, $this->editors)) { + throw new \RuntimeException('No matching editor found'); + } + $templates = []; + foreach ($this->editors[$editor]->getCreators() as $creator) { + if ($creator instanceof ACreateFromTemplate && $creator->getId() === $type) { + $templates = $creator->getTemplates(); + } + } + return $templates; + } + + public function create(string $path, string $editorId, string $creatorId, $templateId = null): string { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $file = $userFolder->newFile($path); + $editor = $this->getEditor($editorId); + $creators = $editor->getCreators(); + foreach ($creators as $creator) { + if ($creator->getId() === $creatorId) { + $creator->create($file, $creatorId, $templateId); + return $this->createToken($editorId, $file); + } + } + throw new \RuntimeException('No creator found'); + } + + public function open(int $fileId, string $editorId = null): string { + $file = $this->rootFolder->getUserFolder($this->userId)->getById($fileId); + if (count($file) === 0 || !($file[0] instanceof File) || $file === null) { + throw new NotFoundException(); + } + /** @var File $file */ + $file = $file[0]; + + if ($editorId === null) { + $editorId = $this->findEditorForFile($file); + } + + return $this->createToken($editorId, $file); + } + + private function findEditorForFile(File $file) { + foreach ($this->editors as $editor) { + if (in_array($file->getMimeType(), $editor->getMimetypes())) { + return $editor->getId(); + } + } + throw new \RuntimeException('No default editor found for files mimetype'); + } + + public function edit(string $token): Response { + try { + /** @var IEditor $editor */ + $tokenObject = $this->getToken($token); + if ($tokenObject->hasBeenAccessed()) { + throw new \RuntimeException('Token has already been used and can only be used for followup requests'); + } + $editor = $this->getEditor($tokenObject->getEditor()); + $this->accessToken($token); + + } catch (\Throwable $throwable) { + $this->invalidateToken($token); + return new NotFoundResponse(); + } + return $editor->open($tokenObject); + } + + public function editSecure(File $file, string $editorId): TemplateResponse { + // TODO: Implementation in follow up + } + + private function getEditor($editorId): IEditor { + if (!array_key_exists($editorId, $this->editors)) { + throw new \RuntimeException('No editor found'); + } + return $this->editors[$editorId]; + } + + public function getToken(string $token): IToken { + $query = $this->connection->getQueryBuilder(); + $query->select('*')->from(self::TABLE_TOKENS) + ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR))); + $result = $query->execute(); + if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) { + return new Token($this, $tokenRow); + } + throw new \RuntimeException('Failed to validate the token'); + } + + public function cleanup(): int { + $query = $this->connection->getQueryBuilder(); + $query->delete(self::TABLE_TOKENS) + ->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME))); + return $query->execute(); + } + + public function refreshToken(string $token): bool { + $query = $this->connection->getQueryBuilder(); + $query->update(self::TABLE_TOKENS) + ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR))); + $result = $query->execute(); + return $result !== 0; + } + + + public function invalidateToken(string $token): bool { + $query = $this->connection->getQueryBuilder(); + $query->delete(self::TABLE_TOKENS) + ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR))); + $result = $query->execute(); + return $result !== 0; + } + + public function accessToken(string $token): bool { + $query = $this->connection->getQueryBuilder(); + $query->update(self::TABLE_TOKENS) + ->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR))); + $result = $query->execute(); + return $result !== 0; + } + + public function invokeTokenScope($userId): void { + \OC_User::setIncognitoMode(true); + \OC_User::setUserId($userId); + } + + public function createToken($editorId, File $file, IShare $share = null): string { + $token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE); + $query = $this->connection->getQueryBuilder(); + $query->insert(self::TABLE_TOKENS) + ->values([ + 'token' => $query->createNamedParameter($token), + 'editor_id' => $query->createNamedParameter($editorId), + 'file_id' => $query->createNamedParameter($file->getId()), + 'user_id' => $query->createNamedParameter($this->userId), + 'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null), + 'timestamp' => $query->createNamedParameter(time()) + ]); + $query->execute(); + return $token; + } + + public function getFileForToken($userId, $fileId) { + $userFolder = $this->rootFolder->getUserFolder($userId); + return $userFolder->getById($fileId)[0]; + } + +} diff --git a/lib/private/DirectEditing/Token.php b/lib/private/DirectEditing/Token.php new file mode 100644 index 0000000000..946699900b --- /dev/null +++ b/lib/private/DirectEditing/Token.php @@ -0,0 +1,76 @@ + + * + * @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 OC\DirectEditing; + + +use OCP\DirectEditing\IToken; +use OCP\Files\File; + +class Token implements IToken { + + /** @var Manager */ + private $manager; + private $data; + + public function __construct(Manager $manager, $data) { + $this->manager = $manager; + $this->data = $data; + } + + public function extend(): void { + $this->manager->refreshToken($this->data['token']); + } + + public function invalidate(): void { + $this->manager->invalidateToken($this->data['token']); + } + + public function getFile(): File { + if ($this->data['share_id'] !== null) { + return $this->manager->getShareForToken($this->data['share_id']); + } + return $this->manager->getFileForToken($this->data['user_id'], $this->data['file_id']); + } + + public function getToken(): string { + return $this->data['token']; + } + + public function useTokenScope(): void { + $this->manager->invokeTokenScope($this->data['user_id']); + } + + public function hasBeenAccessed(): bool { + return $this->data['accessed'] === '1'; + } + + public function getEditor(): string { + return $this->data['editor_id']; + } + + public function getUser(): string { + return $this->data['user_id']; + } + +} diff --git a/lib/private/Server.php b/lib/private/Server.php index b4af17ba28..9fb197fcb1 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -199,6 +199,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\Contacts\IManager::class, \OC\ContactsManager::class); $this->registerAlias('ContactsManager', \OCP\Contacts\IManager::class); + $this->registerAlias(\OCP\DirectEditing\IManager::class, \OC\DirectEditing\Manager::class); + $this->registerAlias(IActionFactory::class, ActionFactory::class); diff --git a/lib/public/DirectEditing/ACreateEmpty.php b/lib/public/DirectEditing/ACreateEmpty.php new file mode 100644 index 0000000000..79684e33b8 --- /dev/null +++ b/lib/public/DirectEditing/ACreateEmpty.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 . + * + */ + +namespace OCP\DirectEditing; + + +use OCP\Files\File; + +/** + * @since 18.0.0 + */ +abstract class ACreateEmpty { + + /** + * Unique id for the creator to filter templates + * + * e.g. document/spreadsheet/presentation + * + * @since 18.0.0 + * @return string + */ + abstract public function getId(): string; + + /** + * Descriptive name for the create action + * + * e.g Create a new document + * + * @since 18.0.0 + * @return string + */ + abstract public function getName(): string; + + /** + * Default file extension for the new file + * + * @since 18.0.0 + * @return string + */ + abstract public function getExtension(): string; + + /** + * Add content when creating empty files + * + * @since 18.0.0 + * @param File $file + */ + public function create(File $file, string $creatorId = null, string $templateId = null): void { + + } +} diff --git a/lib/public/DirectEditing/ACreateFromTemplate.php b/lib/public/DirectEditing/ACreateFromTemplate.php new file mode 100644 index 0000000000..a731e8be59 --- /dev/null +++ b/lib/public/DirectEditing/ACreateFromTemplate.php @@ -0,0 +1,39 @@ + + * + * @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 OCP\DirectEditing; + +/** + * @since 18.0.0 + */ +abstract class ACreateFromTemplate extends ACreateEmpty { + + /** + * List of available templates for the create from template action + * + * @since 18.0.0 + * @return array + */ + abstract public function getTemplates(): array; + +} diff --git a/lib/public/DirectEditing/ATemplate.php b/lib/public/DirectEditing/ATemplate.php new file mode 100644 index 0000000000..734317eebe --- /dev/null +++ b/lib/public/DirectEditing/ATemplate.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 . + * + */ + +namespace OCP\DirectEditing; + +use JsonSerializable; + +/** + * Class ATemplate + * + * @package OCP\DirectEditing + * @since 18.0.0 + */ +abstract class ATemplate implements JsonSerializable { + + /** + * Return a unique id so the app can identify the template + * + * @since 18.0.0 + * @return string + */ + abstract public function getId(): string; + + /** + * Return a title that is displayed to the user + * + * @since 18.0.0 + * @return string + */ + abstract public function getTitle(): string; + + /** + * Return a link to the template preview image + * + * @since 18.0.0 + * @return string + */ + abstract public function getPreview(): string; + + /** + * @since 18.0.0 + * @return array|mixed + */ + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'title' => $this->getTitle(), + 'preview' => $this->getPreview(), + ]; + } +} diff --git a/lib/public/DirectEditing/IEditor.php b/lib/public/DirectEditing/IEditor.php new file mode 100644 index 0000000000..a4fc87f7e1 --- /dev/null +++ b/lib/public/DirectEditing/IEditor.php @@ -0,0 +1,99 @@ + + * + * @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 OCP\DirectEditing; + + +use OCP\AppFramework\Http\Response; + +/** + * @since 18.0.0 + */ +interface IEditor { + + /** + * Return a unique identifier for the editor + * + * e.g. richdocuments + * + * @since 18.0.0 + * @return string + */ + public function getId(): string; + + /** + * Return a readable name for the editor + * + * e.g. Collabora Online + * + * @since 18.0.0 + * @return string + */ + public function getName(): string; + + /** + * A list of mimetypes that should open the editor by default + * + * @since 18.0.0 + * @return array + */ + public function getMimetypes(): array; + + /** + * A list of mimetypes that can be opened in the editor optionally + * + * @since 18.0.0 + * @return array + */ + public function getMimetypesOptional(): array; + + /** + * Return a list of file creation options to be presented to the user + * + * @since 18.0.0 + * @return array of ICreateFromTemplate|ICreateEmpty + */ + public function getCreators(): array; + + /** + * Return if the view is able to securely view a file without downloading it to the browser + * + * @since 18.0.0 + * @return bool + */ + public function isSecure(): bool; + + /** + * 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 + * + * @since 18.0.0 + * @return Response + */ + public function open(IToken $token): Response; +} diff --git a/lib/public/DirectEditing/IManager.php b/lib/public/DirectEditing/IManager.php new file mode 100644 index 0000000000..07b9c5a1e4 --- /dev/null +++ b/lib/public/DirectEditing/IManager.php @@ -0,0 +1,88 @@ + + * + * @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 OCP\DirectEditing; + +use OCP\AppFramework\Http\Response; +use OCP\Files\NotPermittedException; +use RuntimeException; + +/** + * Interface IManager + * + * @package OCP\DirectEditing + * @since 18.0.0 + */ +interface IManager { + + /** + * Register a new editor + * + * @since 18.0.0 + * @param IEditor $directEditor + */ + public function registerDirectEditor(IEditor $directEditor): void; + + /** + * Open the editing page for a provided token + * + * @since 18.0.0 + * @param string $token + * @return Response + */ + public function edit(string $token): Response; + + /** + * Create a new token based on the file path and editor details + * + * @since 18.0.0 + * @param string $path + * @param string $editorId + * @param string $creatorId + * @param null $templateId + * @return string + * @throws NotPermittedException + * @throws RuntimeException + */ + public function create(string $path, string $editorId, string $creatorId, $templateId = null): string; + + /** + * Get the token details for a given token + * + * @since 18.0.0 + * @param string $token + * @return IToken + */ + public function getToken(string $token): IToken; + + /** + * Cleanup expired tokens + * + * @since 18.0.0 + * @return int number of deleted tokens + */ + public function cleanup(): int; + +} + diff --git a/lib/public/DirectEditing/IToken.php b/lib/public/DirectEditing/IToken.php new file mode 100644 index 0000000000..a730493d76 --- /dev/null +++ b/lib/public/DirectEditing/IToken.php @@ -0,0 +1,77 @@ + + * + * @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 OCP\DirectEditing; + + +use OCP\Files\File; + +/** + * @since 18.0.0 + */ +interface IToken { + + /** + * Extend the token validity time + * + * @since 18.0.0 + */ + public function extend(): void; + + /** + * Invalidate the token + * + * @since 18.0.0 + */ + public function invalidate(): void; + + /** + * Check if the token has already been used + * + * @since 18.0.0 + * @return bool + */ + public function hasBeenAccessed(): bool; + + /** + * Change to the user scope of the token + * + * @since 18.0.0 + */ + public function useTokenScope(): void; + + /** + * Get the file that is related to the token + * + * @since 18.0.0 + * @return File + */ + public function getFile(): File; + + /** + * @since 18.0.0 + * @return string + */ + public function getEditor(): string; + +} diff --git a/lib/public/DirectEditing/RegisterDirectEditorEvent.php b/lib/public/DirectEditing/RegisterDirectEditorEvent.php new file mode 100644 index 0000000000..801e9f8fb1 --- /dev/null +++ b/lib/public/DirectEditing/RegisterDirectEditorEvent.php @@ -0,0 +1,57 @@ + + * + * @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 OCP\DirectEditing; + +use OCP\EventDispatcher\Event; + +/** + * @since 18.0.0 + */ +class RegisterDirectEditorEvent extends Event { + + /** + * @var IManager + */ + private $manager; + + /** + * RegisterDirectEditorEvent constructor. + * + * @param IManager $manager + * @since 18.0.0 + */ + public function __construct(IManager $manager) { + parent::__construct(); + $this->manager = $manager; + } + + /** + * @since 18.0.0 + * @param IEditor $editor + */ + public function register(IEditor $editor): void { + $this->manager->registerDirectEditor($editor); + } + +} diff --git a/version.php b/version.php index abbcca4d03..d5452bb0ab 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(18, 0, 0, 1); +$OC_Version = array(18, 0, 0, 2); // The human readable string $OC_VersionString = '18.0.0 Alpha'; From bccf2367384f3d093b2988eb6d09e4f9c9a899a4 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 31 Oct 2019 09:18:05 +0100 Subject: [PATCH 2/9] encapsulate templates Signed-off-by: tobiasKaminsky --- lib/private/DirectEditing/Manager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/private/DirectEditing/Manager.php b/lib/private/DirectEditing/Manager.php index 085a7de557..353faedf7e 100644 --- a/lib/private/DirectEditing/Manager.php +++ b/lib/private/DirectEditing/Manager.php @@ -88,7 +88,9 @@ class Manager implements IManager { $templates = $creator->getTemplates(); } } - return $templates; + $return = []; + $return['templates'] = $templates; + return $return; } public function create(string $path, string $editorId, string $creatorId, $templateId = null): string { From 103c6fb39eab36063b16767abb33da0395b2b9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 6 Nov 2019 10:41:08 +0100 Subject: [PATCH 3/9] Add background job for token cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/appinfo/info.xml | 1 + .../CleanupDirectEditingTokens.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index d8c61d0f45..67c589ed75 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -26,6 +26,7 @@ OCA\Files\BackgroundJob\ScanFiles OCA\Files\BackgroundJob\DeleteOrphanedItems OCA\Files\BackgroundJob\CleanupFileLocks + OCA\Files\BackgroundJob\CleanupDirectEditingTokens diff --git a/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php new file mode 100644 index 0000000000..77907fab28 --- /dev/null +++ b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php @@ -0,0 +1,31 @@ +interval = self::INTERVAL_MINUTES; + $this->manager = $manager; + } + + /** + * Makes the background job do its work + * + * @param array $argument unused argument + * @throws \Exception + */ + public function run($argument) { + $this->manager->cleanup(); + } +} From c8d3e32190d8672c9fc42cd30a63182623006431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 14 Nov 2019 16:19:58 +0100 Subject: [PATCH 4/9] Direct editing token tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- tests/lib/DirectEditing/ManagerTest.php | 152 ++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/lib/DirectEditing/ManagerTest.php diff --git a/tests/lib/DirectEditing/ManagerTest.php b/tests/lib/DirectEditing/ManagerTest.php new file mode 100644 index 0000000000..b2e58efd8e --- /dev/null +++ b/tests/lib/DirectEditing/ManagerTest.php @@ -0,0 +1,152 @@ +editor = new Editor(); + + $this->random = $this->createMock(ISecureRandom::class); + $this->connection = \OC::$server->getDatabaseConnection(); + $this->userSession = $this->createMock(IUserSession::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->userFolder = $this->createMock(Folder::class); + + + $this->rootFolder->expects($this->any()) + ->method('getUserFolder') + ->willReturn($this->userFolder); + + $this->manager = new Manager( + $this->random, $this->connection, $this->userSession, $this->rootFolder + ); + + $this->manager->registerDirectEditor($this->editor); + } + + public function testEditorRegistration() { + $this->assertEquals($this->manager->getEditors(), ['testeditor' => $this->editor]); + } + + + public function testCreateToken() { + $expectedToken = 'TOKEN' . time(); + $file = $this->createMock(File::class); + $file->expects($this->any()) + ->method('getId') + ->willReturn(123); + $this->random->expects($this->once()) + ->method('generate') + ->willReturn($expectedToken); + $this->userFolder->expects($this->once()) + ->method('newFile') + ->willReturn($file); + $token = $this->manager->create('/File.txt', 'testeditor', 'createEmpty'); + $this->assertEquals($token, $expectedToken); + } + + public function testCreateTokenAccess() { + $expectedToken = 'TOKEN' . time(); + $file = $this->createMock(File::class); + $file->expects($this->any()) + ->method('getId') + ->willReturn(123); + $this->random->expects($this->once()) + ->method('generate') + ->willReturn($expectedToken); + $this->userFolder->expects($this->once()) + ->method('newFile') + ->willReturn($file); + $this->manager->create('/File.txt', 'testeditor', 'createEmpty'); + $firstResult = $this->manager->edit($expectedToken); + $secondResult = $this->manager->edit($expectedToken); + $this->assertInstanceOf(DataResponse::class, $firstResult); + $this->assertInstanceOf(NotFoundResponse::class, $secondResult); + } + +} From bc36cc808fb9ed9ff22c42246fe68f06cedb902e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 25 Nov 2019 13:56:22 +0100 Subject: [PATCH 5/9] Move editor list to capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/appinfo/routes.php | 5 --- apps/files/lib/Capabilities.php | 43 ++++++++++++++++++- .../Controller/DirectEditingController.php | 42 ------------------ lib/private/DirectEditing/Manager.php | 9 +++- 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 6f8a6878aa..f900b5ffbd 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -99,11 +99,6 @@ $application->registerRoutes( ], ], 'ocs' => [ - [ - 'name' => 'DirectEditing#get', - 'url' => '/api/v1/directEditing', - 'verb' => 'GET' - ], [ 'name' => 'DirectEditing#templates', 'url' => '/api/v1/directEditing/templates/{editorId}/{creatorId}', diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 2b6bf57b90..c37e32b6b5 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -25,7 +25,11 @@ namespace OCA\Files; +use OC\DirectEditing\Manager; use OCP\Capabilities\ICapability; +use OCP\DirectEditing\ACreateEmpty; +use OCP\DirectEditing\ACreateFromTemplate; +use OCP\DirectEditing\IEditor; use OCP\IConfig; /** @@ -42,8 +46,9 @@ class Capabilities implements ICapability { * * @param IConfig $config */ - public function __construct(IConfig $config) { + public function __construct(IConfig $config, Manager $manager) { $this->config = $config; + $this->directEditingManager = $manager; } /** @@ -56,7 +61,43 @@ class Capabilities implements ICapability { 'files' => [ 'bigfilechunking' => true, 'blacklisted_files' => $this->config->getSystemValue('blacklisted_files', ['.htaccess']), + 'directEditing' => $this->getDirectEditingCapabilitites() ], ]; } + + private function getDirectEditingCapabilitites() { + $capabilities = [ + 'editors' => [], + 'creators' => [] + ]; + + /** + * @var string $id + * @var IEditor $editor + */ + foreach ($this->directEditingManager->getEditors() as $id => $editor) { + $capabilities['editors'][$id] = [ + 'name' => $editor->getName(), + 'mimetypes' => $editor->getMimetypes(), + 'optionalMimetypes' => $editor->getMimetypesOptional(), + 'secure' => $editor->isSecure(), + ]; + /** @var ACreateEmpty|ACreateFromTemplate $creator */ + foreach ($editor->getCreators() as $creator) { + $id = $creator->getId(); + $capabilities['creators'][$id] = [ + 'id' => $id, + 'name' => $creator->getName(), + 'extension' => $creator->getExtension(), + 'templates' => false + ]; + if ($creator instanceof ACreateFromTemplate) { + $capabilities['creators'][$id]['templates'] = true; + } + + } + } + return $capabilities; + } } diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index cf948d20f7..e879131644 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -62,48 +62,6 @@ class DirectEditingController extends OCSController { $this->urlGenerator = $urlGenerator; } - /** - * @NoAdminRequired - * - * @return DataResponse - */ - public function get(): DataResponse { - $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); - - $capabilities = [ - 'editors' => [], - 'creators' => [] - ]; - - /** - * @var string $id - * @var IEditor $editor - */ - foreach ($this->directEditingManager->getEditors() as $id => $editor) { - $capabilities['editors'][$id] = [ - 'name' => $editor->getName(), - 'mimetypes' => $editor->getMimetypes(), - 'optionalMimetypes' => $editor->getMimetypesOptional(), - 'secure' => $editor->isSecure(), - ]; - /** @var ACreateEmpty|ACreateFromTemplate $creator */ - foreach ($editor->getCreators() as $creator) { - $id = $creator->getId(); - $capabilities['creators'][$id] = [ - 'id' => $id, - 'name' => $creator->getName(), - 'extension' => $creator->getExtension(), - 'templates' => false - ]; - if ($creator instanceof ACreateFromTemplate) { - $capabilities['creators'][$id]['templates'] = true; - } - - } - } - return new DataResponse($capabilities); - } - /** * @NoAdminRequired */ diff --git a/lib/private/DirectEditing/Manager.php b/lib/private/DirectEditing/Manager.php index 353faedf7e..fdf0a1f0f0 100644 --- a/lib/private/DirectEditing/Manager.php +++ b/lib/private/DirectEditing/Manager.php @@ -32,6 +32,8 @@ use OCP\DirectEditing\ACreateFromTemplate; use OCP\DirectEditing\IEditor; use \OCP\DirectEditing\IManager; use OCP\DirectEditing\IToken; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; @@ -47,7 +49,7 @@ class Manager implements IManager { public const TABLE_TOKENS = 'direct_edit'; /** @var IEditor[] */ - private $editors; + private $editors = []; /** @var IDBConnection */ private $connection; @@ -62,12 +64,15 @@ class Manager implements IManager { ISecureRandom $random, IDBConnection $connection, IUserSession $userSession, - IRootFolder $rootFolder + IRootFolder $rootFolder, + IEventDispatcher $eventDispatcher ) { $this->random = $random; $this->connection = $connection; $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; $this->rootFolder = $rootFolder; + $eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this)); + } public function registerDirectEditor(IEditor $directEditor): void { From 9a2694fcb0093afee13d0cd91de15b34b747cc62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 25 Nov 2019 14:09:38 +0100 Subject: [PATCH 6/9] Code style fixes and cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- .../CleanupDirectEditingTokens.php | 2 +- apps/files/lib/Capabilities.php | 16 ++++- .../Controller/DirectEditingController.php | 6 +- .../Version18000Date20191014105105.php | 70 +++++++++---------- lib/private/DirectEditing/Manager.php | 5 +- .../DirectEditing/ACreateFromTemplate.php | 2 +- lib/public/DirectEditing/IEditor.php | 6 +- tests/lib/DirectEditing/ManagerTest.php | 20 +++++- 8 files changed, 75 insertions(+), 52 deletions(-) diff --git a/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php index 77907fab28..8d4a3f2378 100644 --- a/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php +++ b/apps/files/lib/BackgroundJob/CleanupDirectEditingTokens.php @@ -7,7 +7,7 @@ use OCP\DirectEditing\IManager; class CleanupDirectEditingTokens extends TimedJob { - const INTERVAL_MINUTES = 15 * 60; + private const INTERVAL_MINUTES = 15 * 60; /** * @var IManager diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index c37e32b6b5..19b59971c4 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -30,6 +30,8 @@ use OCP\Capabilities\ICapability; use OCP\DirectEditing\ACreateEmpty; use OCP\DirectEditing\ACreateFromTemplate; use OCP\DirectEditing\IEditor; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; /** @@ -38,17 +40,25 @@ use OCP\IConfig; * @package OCA\Files */ class Capabilities implements ICapability { + /** @var IConfig */ protected $config; + /** @var Manager */ + protected $directEditingManager; + + /** @var IEventDispatcher */ + protected $eventDispatcher; + /** * Capabilities constructor. * * @param IConfig $config */ - public function __construct(IConfig $config, Manager $manager) { + public function __construct(IConfig $config, Manager $manager, IEventDispatcher $eventDispatcher) { $this->config = $config; $this->directEditingManager = $manager; + $this->eventDispatcher = $eventDispatcher; } /** @@ -66,7 +76,9 @@ class Capabilities implements ICapability { ]; } - private function getDirectEditingCapabilitites() { + private function getDirectEditingCapabilitites(): array { + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); + $capabilities = [ 'editors' => [], 'creators' => [] diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index e879131644..11d09e2f07 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -66,7 +66,7 @@ class DirectEditingController extends OCSController { * @NoAdminRequired */ public function create(string $path, string $editorId, string $creatorId, string $templateId = null): DataResponse { - $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); try { $token = $this->directEditingManager->create($path, $editorId, $creatorId, $templateId); @@ -83,7 +83,7 @@ class DirectEditingController extends OCSController { * @NoAdminRequired */ public function open(int $fileId, string $editorId = null): DataResponse { - $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); try { $token = $this->directEditingManager->open($fileId, $editorId); @@ -102,7 +102,7 @@ class DirectEditingController extends OCSController { * @NoAdminRequired */ public function templates(string $editorId, string $creatorId): DataResponse { - $this->eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this->directEditingManager)); + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); try { return new DataResponse($this->directEditingManager->getTemplates($editorId, $creatorId)); diff --git a/core/Migrations/Version18000Date20191014105105.php b/core/Migrations/Version18000Date20191014105105.php index b291c0b5e4..634f9f91fa 100644 --- a/core/Migrations/Version18000Date20191014105105.php +++ b/core/Migrations/Version18000Date20191014105105.php @@ -50,44 +50,42 @@ class Version18000Date20191014105105 extends SimpleMigrationStep { public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - if (!$schema->hasTable('direct_edit')) { - $table = $schema->createTable('direct_edit'); + $table = $schema->createTable('direct_edit'); - $table->addColumn('id', Type::BIGINT, [ - 'autoincrement' => true, - 'notnull' => true, - ]); - $table->addColumn('editor_id', Type::STRING, [ - 'notnull' => true, - 'length' => 64, - ]); - $table->addColumn('token', Type::STRING, [ - 'notnull' => true, - 'length' => 64, - ]); - $table->addColumn('file_id', Type::BIGINT, [ - 'notnull' => true, - ]); - $table->addColumn('user_id', Type::STRING, [ - 'notnull' => false, - 'length' => 64, - ]); - $table->addColumn('share_id', Type::BIGINT, [ - 'notnull' => false - ]); - $table->addColumn('timestamp', Type::BIGINT, [ - 'notnull' => true, - 'length' => 20, - 'unsigned' => true, - ]); - $table->addColumn('accessed', Type::BOOLEAN, [ - 'notnull' => true, - 'default' => false - ]); + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('editor_id', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('token', Type::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('file_id', Type::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('user_id', Type::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('share_id', Type::BIGINT, [ + 'notnull' => false + ]); + $table->addColumn('timestamp', Type::BIGINT, [ + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $table->addColumn('accessed', Type::BOOLEAN, [ + 'notnull' => true, + 'default' => false + ]); - $table->setPrimaryKey(['id']); - $table->addIndex(['token']); - } + $table->setPrimaryKey(['id']); + $table->addIndex(['token']); return $schema; } diff --git a/lib/private/DirectEditing/Manager.php b/lib/private/DirectEditing/Manager.php index fdf0a1f0f0..26adad9e57 100644 --- a/lib/private/DirectEditing/Manager.php +++ b/lib/private/DirectEditing/Manager.php @@ -64,15 +64,12 @@ class Manager implements IManager { ISecureRandom $random, IDBConnection $connection, IUserSession $userSession, - IRootFolder $rootFolder, - IEventDispatcher $eventDispatcher + IRootFolder $rootFolder ) { $this->random = $random; $this->connection = $connection; $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; $this->rootFolder = $rootFolder; - $eventDispatcher->dispatch(RegisterDirectEditorEvent::class, new RegisterDirectEditorEvent($this)); - } public function registerDirectEditor(IEditor $directEditor): void { diff --git a/lib/public/DirectEditing/ACreateFromTemplate.php b/lib/public/DirectEditing/ACreateFromTemplate.php index a731e8be59..89420a6374 100644 --- a/lib/public/DirectEditing/ACreateFromTemplate.php +++ b/lib/public/DirectEditing/ACreateFromTemplate.php @@ -32,7 +32,7 @@ abstract class ACreateFromTemplate extends ACreateEmpty { * List of available templates for the create from template action * * @since 18.0.0 - * @return array + * @return ATemplate[] */ abstract public function getTemplates(): array; diff --git a/lib/public/DirectEditing/IEditor.php b/lib/public/DirectEditing/IEditor.php index a4fc87f7e1..a2bc0d7425 100644 --- a/lib/public/DirectEditing/IEditor.php +++ b/lib/public/DirectEditing/IEditor.php @@ -56,7 +56,7 @@ interface IEditor { * A list of mimetypes that should open the editor by default * * @since 18.0.0 - * @return array + * @return string[] */ public function getMimetypes(): array; @@ -64,7 +64,7 @@ interface IEditor { * A list of mimetypes that can be opened in the editor optionally * * @since 18.0.0 - * @return array + * @return string[] */ public function getMimetypesOptional(): array; @@ -72,7 +72,7 @@ interface IEditor { * Return a list of file creation options to be presented to the user * * @since 18.0.0 - * @return array of ICreateFromTemplate|ICreateEmpty + * @return ACreateFromTemplate[]|ACreateEmpty[] */ public function getCreators(): array; diff --git a/tests/lib/DirectEditing/ManagerTest.php b/tests/lib/DirectEditing/ManagerTest.php index b2e58efd8e..a3d29efbce 100644 --- a/tests/lib/DirectEditing/ManagerTest.php +++ b/tests/lib/DirectEditing/ManagerTest.php @@ -7,7 +7,6 @@ use OC\Files\Node\File; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\Response; -use OCP\AppFramework\Http\TemplateResponse; use OCP\DirectEditing\ACreateEmpty; use OCP\DirectEditing\IEditor; use OCP\DirectEditing\IToken; @@ -16,6 +15,7 @@ use OCP\Files\IRootFolder; use OCP\IDBConnection; use OCP\IUserSession; use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class CreateEmpty extends ACreateEmpty { @@ -82,9 +82,25 @@ class ManagerTest extends TestCase { */ private $editor; /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var MockObject|ISecureRandom */ private $random; + /** + * @var IDBConnection + */ + private $connection; + /** + * @var MockObject|IUserSession + */ + private $userSession; + /** + * @var MockObject|IRootFolder + */ + private $rootFolder; + /** + * @var MockObject|Folder + */ + private $userFolder; protected function setUp() { parent::setUp(); From 329e8c2604a35bded6afac75b59f611002d6db5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 26 Nov 2019 13:07:15 +0100 Subject: [PATCH 7/9] Add mimetype to creators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/lib/Capabilities.php | 7 ++----- lib/public/DirectEditing/ACreateEmpty.php | 8 ++++++++ tests/lib/DirectEditing/ManagerTest.php | 4 ++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 19b59971c4..270c6954a1 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -102,12 +102,9 @@ class Capabilities implements ICapability { 'id' => $id, 'name' => $creator->getName(), 'extension' => $creator->getExtension(), - 'templates' => false + 'templates' => $creator instanceof ACreateFromTemplate, + 'mimetype' => $creator->getMimetype() ]; - if ($creator instanceof ACreateFromTemplate) { - $capabilities['creators'][$id]['templates'] = true; - } - } } return $capabilities; diff --git a/lib/public/DirectEditing/ACreateEmpty.php b/lib/public/DirectEditing/ACreateEmpty.php index 79684e33b8..ab7f7fd3ae 100644 --- a/lib/public/DirectEditing/ACreateEmpty.php +++ b/lib/public/DirectEditing/ACreateEmpty.php @@ -59,6 +59,14 @@ abstract class ACreateEmpty { */ abstract public function getExtension(): string; + /** + * Mimetype of the resulting created file + * + * @since 18.0.0 + * @return string + */ + abstract public function getMimetype(): string; + /** * Add content when creating empty files * diff --git a/tests/lib/DirectEditing/ManagerTest.php b/tests/lib/DirectEditing/ManagerTest.php index a3d29efbce..9a56c3307e 100644 --- a/tests/lib/DirectEditing/ManagerTest.php +++ b/tests/lib/DirectEditing/ManagerTest.php @@ -31,6 +31,10 @@ class CreateEmpty extends ACreateEmpty { public function getExtension(): string { return '.txt'; } + + public function getMimetype(): string { + return 'text/plain'; + } } class Editor implements IEditor { From fb1b88ac63bbc171104559ce3a5ea0d84791249e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 26 Nov 2019 13:56:38 +0100 Subject: [PATCH 8/9] Expose editor id for creators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/lib/Capabilities.php | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 270c6954a1..1318a56f71 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -100,6 +100,7 @@ class Capabilities implements ICapability { $id = $creator->getId(); $capabilities['creators'][$id] = [ 'id' => $id, + 'editor' => $editor->getId(), 'name' => $creator->getName(), 'extension' => $creator->getExtension(), 'templates' => $creator instanceof ACreateFromTemplate, From bde624b07423de4a6b9e3aaae6371cd4f886c2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 27 Nov 2019 14:36:07 +0100 Subject: [PATCH 9/9] Only expose link to info endpoint in capabilties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- apps/files/appinfo/routes.php | 5 ++ .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files/lib/Capabilities.php | 54 +++--------- .../Controller/DirectEditingController.php | 16 +++- .../lib/Service/DirectEditingService.php | 85 +++++++++++++++++++ 6 files changed, 120 insertions(+), 42 deletions(-) create mode 100644 apps/files/lib/Service/DirectEditingService.php diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index f900b5ffbd..9d889fe0e6 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -99,6 +99,11 @@ $application->registerRoutes( ], ], 'ocs' => [ + [ + 'name' => 'DirectEditing#info', + 'url' => '/api/v1/directEditing', + 'verb' => 'GET' + ], [ 'name' => 'DirectEditing#templates', 'url' => '/api/v1/directEditing/templates/{editorId}/{creatorId}', diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 462e51ffe0..b350bfae07 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -40,6 +40,7 @@ return array( 'OCA\\Files\\Exception\\TransferOwnershipException' => $baseDir . '/../lib/Exception/TransferOwnershipException.php', 'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', + 'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php', 'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php', ); diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index b41faa6f8c..34a64f3253 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -55,6 +55,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Exception\\TransferOwnershipException' => __DIR__ . '/..' . '/../lib/Exception/TransferOwnershipException.php', 'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', + 'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php', 'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php', ); diff --git a/apps/files/lib/Capabilities.php b/apps/files/lib/Capabilities.php index 1318a56f71..20ef0c4d22 100644 --- a/apps/files/lib/Capabilities.php +++ b/apps/files/lib/Capabilities.php @@ -26,6 +26,7 @@ namespace OCA\Files; use OC\DirectEditing\Manager; +use OCA\Files\Service\DirectEditingService; use OCP\Capabilities\ICapability; use OCP\DirectEditing\ACreateEmpty; use OCP\DirectEditing\ACreateFromTemplate; @@ -33,6 +34,7 @@ use OCP\DirectEditing\IEditor; use OCP\DirectEditing\RegisterDirectEditorEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; +use OCP\IURLGenerator; /** * Class Capabilities @@ -44,21 +46,21 @@ class Capabilities implements ICapability { /** @var IConfig */ protected $config; - /** @var Manager */ - protected $directEditingManager; + /** @var DirectEditingService */ + protected $directEditingService; - /** @var IEventDispatcher */ - protected $eventDispatcher; + /** @var IURLGenerator */ + private $urlGenerator; /** * Capabilities constructor. * * @param IConfig $config */ - public function __construct(IConfig $config, Manager $manager, IEventDispatcher $eventDispatcher) { + public function __construct(IConfig $config, DirectEditingService $directEditingService, IURLGenerator $urlGenerator) { $this->config = $config; - $this->directEditingManager = $manager; - $this->eventDispatcher = $eventDispatcher; + $this->directEditingService = $directEditingService; + $this->urlGenerator = $urlGenerator; } /** @@ -71,43 +73,13 @@ class Capabilities implements ICapability { 'files' => [ 'bigfilechunking' => true, 'blacklisted_files' => $this->config->getSystemValue('blacklisted_files', ['.htaccess']), - 'directEditing' => $this->getDirectEditingCapabilitites() + 'directEditing' => [ + 'url' => $this->urlGenerator->linkToOCSRouteAbsolute('files.DirectEditing.info'), + 'etag' => $this->directEditingService->getDirectEditingETag() + ] ], ]; } - private function getDirectEditingCapabilitites(): array { - $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); - $capabilities = [ - 'editors' => [], - 'creators' => [] - ]; - - /** - * @var string $id - * @var IEditor $editor - */ - foreach ($this->directEditingManager->getEditors() as $id => $editor) { - $capabilities['editors'][$id] = [ - 'name' => $editor->getName(), - 'mimetypes' => $editor->getMimetypes(), - 'optionalMimetypes' => $editor->getMimetypesOptional(), - 'secure' => $editor->isSecure(), - ]; - /** @var ACreateEmpty|ACreateFromTemplate $creator */ - foreach ($editor->getCreators() as $creator) { - $id = $creator->getId(); - $capabilities['creators'][$id] = [ - 'id' => $id, - 'editor' => $editor->getId(), - 'name' => $creator->getName(), - 'extension' => $creator->getExtension(), - 'templates' => $creator instanceof ACreateFromTemplate, - 'mimetype' => $creator->getMimetype() - ]; - } - } - return $capabilities; - } } diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index 11d09e2f07..0a086c3da6 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -25,6 +25,7 @@ namespace OCA\Files\Controller; use Exception; +use OCA\Files\Service\DirectEditingService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; @@ -52,16 +53,29 @@ class DirectEditingController extends OCSController { /** @var ILogger */ private $logger; + /** @var DirectEditingService */ + private $directEditingService; + public function __construct($appName, IRequest $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge, - IEventDispatcher $eventDispatcher, IURLGenerator $urlGenerator, IManager $manager, ILogger $logger) { + IEventDispatcher $eventDispatcher, IURLGenerator $urlGenerator, IManager $manager, DirectEditingService $directEditingService, ILogger $logger) { parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge); $this->eventDispatcher = $eventDispatcher; $this->directEditingManager = $manager; + $this->directEditingService = $directEditingService; $this->logger = $logger; $this->urlGenerator = $urlGenerator; } + /** + * @NoAdminRequired + */ + public function info(): DataResponse { + $response = new DataResponse($this->directEditingService->getDirectEditingCapabilitites()); + $response->setETag($this->directEditingService->getDirectEditingETag()); + return $response; + } + /** * @NoAdminRequired */ diff --git a/apps/files/lib/Service/DirectEditingService.php b/apps/files/lib/Service/DirectEditingService.php new file mode 100644 index 0000000000..7e906238d8 --- /dev/null +++ b/apps/files/lib/Service/DirectEditingService.php @@ -0,0 +1,85 @@ + + * + * @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\Files\Service; + + +use OCP\DirectEditing\ACreateEmpty; +use OCP\DirectEditing\ACreateFromTemplate; +use OCP\DirectEditing\IEditor; +use OCP\DirectEditing\IManager; +use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\EventDispatcher\IEventDispatcher; + +class DirectEditingService { + + /** @var IManager */ + private $directEditingManager; + /** @var IEventDispatcher */ + private $eventDispatcher; + + public function __construct(IEventDispatcher $eventDispatcher, IManager $directEditingManager) { + $this->directEditingManager = $directEditingManager; + $this->eventDispatcher = $eventDispatcher; + } + + public function getDirectEditingETag(): string { + return \md5(\json_encode($this->getDirectEditingCapabilitites())); + } + + public function getDirectEditingCapabilitites(): array { + $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager)); + + $capabilities = [ + 'editors' => [], + 'creators' => [] + ]; + + /** + * @var string $id + * @var IEditor $editor + */ + foreach ($this->directEditingManager->getEditors() as $id => $editor) { + $capabilities['editors'][$id] = [ + 'name' => $editor->getName(), + 'mimetypes' => $editor->getMimetypes(), + 'optionalMimetypes' => $editor->getMimetypesOptional(), + 'secure' => $editor->isSecure(), + ]; + /** @var ACreateEmpty|ACreateFromTemplate $creator */ + foreach ($editor->getCreators() as $creator) { + $id = $creator->getId(); + $capabilities['creators'][$id] = [ + 'id' => $id, + 'editor' => $editor->getId(), + 'name' => $creator->getName(), + 'extension' => $creator->getExtension(), + 'templates' => $creator instanceof ACreateFromTemplate, + 'mimetype' => $creator->getMimetype() + ]; + } + } + return $capabilities; + } + +}