diff --git a/appinfo/info.xml b/appinfo/info.xml index eaf4f9d877..7419dd7571 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,7 +6,7 @@ - 5.0.0-beta1 + 6.0.0-dev.1 agpl Collabora Productivity based on work of Frank Karlitschek, Victor Dubiniuk @@ -31,6 +31,7 @@ You can also edit your documents off-line with the Collabora Office app from the OCA\Richdocuments\Backgroundjobs\ObtainCapabilities + OCA\Richdocuments\Backgroundjobs\Cleanup OCA\Richdocuments\Command\ActivateConfig diff --git a/emptyTemplates/odttemplate.odt b/emptyTemplates/odttemplate.odt deleted file mode 100644 index d2a8dc274c..0000000000 Binary files a/emptyTemplates/odttemplate.odt and /dev/null differ diff --git a/emptyTemplates/docxtemplate.docx b/emptyTemplates/template.docx similarity index 100% rename from emptyTemplates/docxtemplate.docx rename to emptyTemplates/template.docx diff --git a/emptyTemplates/odgtemplate.otg b/emptyTemplates/template.odg similarity index 100% rename from emptyTemplates/odgtemplate.otg rename to emptyTemplates/template.odg diff --git a/emptyTemplates/pptxtemplate.pptx b/emptyTemplates/template.pptx similarity index 100% rename from emptyTemplates/pptxtemplate.pptx rename to emptyTemplates/template.pptx diff --git a/emptyTemplates/xlsxtemplate.xlsx b/emptyTemplates/template.xlsx similarity index 100% rename from emptyTemplates/xlsxtemplate.xlsx rename to emptyTemplates/template.xlsx diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 696fb4e6d1..a8af2239fe 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,6 +31,7 @@ use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\Capabilities; use OCA\Richdocuments\Middleware\WOPIMiddleware; +use OCA\Richdocuments\Listener\FileCreatedFromTemplateListener; use OCA\Richdocuments\PermissionManager; use OCA\Richdocuments\Preview\MSExcel; use OCA\Richdocuments\Preview\MSWord; @@ -48,6 +49,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Template\FileCreatedFromTemplateEvent; use OCP\Files\Template\ITemplateManager; use OCP\Files\Template\TemplateFileCreator; use OCP\IConfig; @@ -68,6 +70,7 @@ public function register(IRegistrationContext $context): void { $context->registerTemplateProvider(CollaboraTemplateProvider::class); $context->registerCapability(Capabilities::class); $context->registerMiddleWare(WOPIMiddleware::class); + $context->registerEventListener(FileCreatedFromTemplateEvent::class, FileCreatedFromTemplateListener::class); } public function boot(IBootContext $context): void { diff --git a/lib/Backgroundjobs/Cleanup.php b/lib/Backgroundjobs/Cleanup.php new file mode 100644 index 0000000000..e5adb55acb --- /dev/null +++ b/lib/Backgroundjobs/Cleanup.php @@ -0,0 +1,49 @@ + + * + * @author Roeland Jago Douma + * + * @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\Richdocuments\Backgroundjobs; + +use OC\BackgroundJob\TimedJob; +use OCA\Richdocuments\Service\CapabilitiesService; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class Cleanup extends TimedJob { + + /** @var IDBConnection */ + private $db; + + public function __construct(IDBConnection $db) { + $this->db = $db; + + $this->setInterval(60*60); + } + + protected function run($argument) { + // Expire template mappings for file creation + $query = $this->db->getQueryBuilder(); + $query->delete('richdocuments_template') + ->where($query->expr()->lte('timestamp', $query->createNamedParameter(time() - 60, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } +} diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index 9f1f7cec1c..8e1932a20b 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -11,6 +11,7 @@ namespace OCA\Richdocuments\Controller; +use OCA\Richdocuments\AppInfo\Application; use OCA\Richdocuments\Events\BeforeFederationRedirectEvent; use OCA\Richdocuments\Service\FederationService; use OCA\Richdocuments\Service\InitialStateService; @@ -210,7 +211,14 @@ public function index($fileId, $path = null) { return $response; } - list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($item->getId()); + $templateFile = $this->templateManager->getTemplateSource($item->getId()); + if ($templateFile) { + list($urlSrc, $wopi) = $this->tokenManager->getTokenForTemplate($templateFile, $this->uid, $item->getId()); + $token = $wopi->getToken(); + } else { + list($urlSrc, $token, $wopi) = $this->tokenManager->getToken($item->getId()); + } + $params = [ 'permissions' => $item->getPermissions(), 'title' => $item->getName(), @@ -575,6 +583,7 @@ public function create($mimetype, } if (!$content){ + // FIXME: see if this is used, $content = file_get_contents(dirname(dirname(__DIR__)) . self::ODT_TEMPLATE_PATH); } diff --git a/lib/Listener/FileCreatedFromTemplateListener.php b/lib/Listener/FileCreatedFromTemplateListener.php new file mode 100644 index 0000000000..6cc4d472bc --- /dev/null +++ b/lib/Listener/FileCreatedFromTemplateListener.php @@ -0,0 +1,71 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Richdocuments\Listener; + + +use OCA\Richdocuments\AppInfo\Application; +use OCA\Richdocuments\TemplateManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Template\FileCreatedFromTemplateEvent; +use OCP\IConfig; + +class FileCreatedFromTemplateListener implements IEventListener { + + /** @var TemplateManager */ + private $templateManager; + + public function __construct( + TemplateManager $templateManager + ) { + $this->templateManager = $templateManager; + } + + public function handle(Event $event): void { + if (!($event instanceof FileCreatedFromTemplateEvent)) { + return; + } + + $templateFile = $event->getTemplate(); + + // Empty template + if ($templateFile === null) { + $event->getTarget()->putContent($this->templateManager->getEmptyFileContent($event->getTarget()->getExtension())); + return; + } + + if ($this->templateManager->isSupportedTemplateSource($templateFile->getExtension())) { + // Only use TemplateSource if supported filetype + $this->templateManager->setTemplateSource($event->getTarget()->getId(), $templateFile->getId()); + } + + // Avoid having the mimetype of the source file set + $event->getTarget()->getStorage()->getCache()->update($event->getTarget()->getId(), [ + 'mimetype' => $event->getTarget()->getMimeType() + ]); + } +} diff --git a/lib/Migration/Version50200Date20211220212457.php b/lib/Migration/Version50200Date20211220212457.php new file mode 100644 index 0000000000..5eb408c12f --- /dev/null +++ b/lib/Migration/Version50200Date20211220212457.php @@ -0,0 +1,51 @@ +hasTable('richdocuments_template')) { + $table = $schema->createTable('richdocuments_template'); + $table->addColumn('id', 'bigint', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $table->addColumn('userid', 'string', [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('fileid', 'bigint', [ + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('templateid', 'bigint', [ + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('timestamp', 'bigint', [ + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['userid', 'fileid'], 'rd_t_user_file'); + } + + return $schema; + } +} diff --git a/lib/Template/CollaboraTemplateProvider.php b/lib/Template/CollaboraTemplateProvider.php index 4246f98b80..aea8bd8b91 100644 --- a/lib/Template/CollaboraTemplateProvider.php +++ b/lib/Template/CollaboraTemplateProvider.php @@ -73,8 +73,4 @@ public function getCustomTemplates(string $mimetype): array { public function getCustomTemplate(string $template): File { return $this->templateManager->get((int)$template); } - - public function createFromTemplate(File $template, File $target): void { - // TODO: Implement createFromTemplate() method. - } } diff --git a/lib/TemplateManager.php b/lib/TemplateManager.php index a907f9aa76..39dcbd2417 100644 --- a/lib/TemplateManager.php +++ b/lib/TemplateManager.php @@ -24,6 +24,8 @@ namespace OCA\Richdocuments; +use OCA\Richdocuments\AppInfo\Application; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IAppData; @@ -31,16 +33,14 @@ use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\IConfig; +use OCP\IDBConnection; use OCP\IL10N; -use OCP\IPreview; use OCP\IURLGenerator; -use OC\Files\AppData\Factory; +use Psr\Log\LoggerInterface; +use Throwable; class TemplateManager { - /** @var string */ - protected $appName; - /** @var string */ protected $userId; @@ -56,6 +56,15 @@ class TemplateManager { /** @var IL10N */ private $l; + /** @var IDBConnection */ + private $db; + + /** @var IAppData */ + private $appData; + + /** @var LoggerInterface */ + private $logger; + /** Accepted templates mime types */ const MIMES_DOCUMENTS = [ 'application/vnd.oasis.opendocument.text-template', @@ -97,39 +106,27 @@ class TemplateManager { 'presentation' => 'pptx', ]; - /** - * Template manager - * - * @param string $appName - * @param string $userId - * @param IConfig $config - * @param Factory $appDataFactory - * @param IURLGenerator $urlGenerator - * @param IRootFolder $rootFolder - * @param IL10N $l - * @throws \OCP\Files\NotPermittedException - */ - public function __construct($appName, - $userId, - IConfig $config, - IAppData $appData, - IURLGenerator $urlGenerator, - IRootFolder $rootFolder, - IL10N $l) { - $this->appName = $appName; - $this->userId = $userId; - $this->config = $config; - $this->rootFolder = $rootFolder; - $this->urlGenerator = $urlGenerator; - - + public function __construct( + $userId, + IConfig $config, + IAppData $appData, + IURLGenerator $urlGenerator, + IRootFolder $rootFolder, + IL10N $l, + IDBConnection $connection, + LoggerInterface $logger + ) { + $this->userId = $userId; + $this->config = $config; + $this->rootFolder = $rootFolder; + $this->urlGenerator = $urlGenerator; + $this->db = $connection; + $this->logger = $logger; $this->appData = $appData; - $this->createAppDataFolders(); - $this->l = $l; } - private function createAppDataFolders() { + private function ensureAppDataFolders() { /* * Init the appdata folder * We need an actual folder for the fileid and previews. @@ -200,7 +197,7 @@ private function filterTemplates($templates, $type = null) { }); } - private function getEmpty($type = null) { + public function getEmpty($type = null) { $folder = $this->getEmptyTemplateDir(); $templateFiles = $folder->getDirectoryListing(); @@ -228,6 +225,7 @@ private function getEmpty($type = null) { * Remove empty_templates in appdata and recreate it from the apps templates */ public function updateEmptyTemplates() { + $this->ensureAppDataFolders(); try { $folder = $this->getEmptyTemplateDir(); $folder->delete(); @@ -393,7 +391,7 @@ private function getUserTemplateDir() { } // has the user manually set a directory as the default template dir ? - $templateDirPath = $this->config->getUserValue($this->userId, $this->appName, 'templateFolder', false); + $templateDirPath = $this->config->getUserValue($this->userId, Application::APPNAME, 'templateFolder', false); $userFolder = $this->rootFolder->getUserFolder($this->userId); if ($templateDirPath !== false) { @@ -418,6 +416,7 @@ private function getUserTemplateDir() { * @return Folder */ private function getSystemTemplateDir() { + $this->ensureAppDataFolders(); $path = 'appdata_' . $this->config->getSystemValue('instanceid', null) . '/richdocuments/templates'; return $this->rootFolder->get($path); } @@ -426,6 +425,7 @@ private function getSystemTemplateDir() { * @return Folder */ private function getEmptyTemplateDir() { + $this->ensureAppDataFolders(); $path = 'appdata_' . $this->config->getSystemValue('instanceid', null) . '/richdocuments/empty_templates'; return $this->rootFolder->get($path); } @@ -437,7 +437,7 @@ private function getEmptyTemplateDir() { * @return array */ public function formatNodeReturn(File $template) { - $ooxml = $this->config->getAppValue($this->appName, 'doc_format', '') === 'ooxml'; + $ooxml = $this->config->getAppValue(Application::APPNAME, 'doc_format', '') === 'ooxml'; $documentType = $this->flipTypes()[$template->getMimeType()]; return [ 'id' => $template->getId(), @@ -466,7 +466,7 @@ public function isTemplate($fileId) { } public function formatEmpty(File $template) { - $ooxml = $this->config->getAppValue($this->appName, 'doc_format', '') === 'ooxml'; + $ooxml = $this->config->getAppValue(Application::APPNAME, 'doc_format', '') === 'ooxml'; $documentType = $this->flipTypes()[$template->getMimeType()]; return [ 'id' => $template->getId(), @@ -490,4 +490,79 @@ public function isValidTemplateMime($mime, $type = null) { return true; } + + /** + * Return default content for empty files of a given filename by file extension + */ + public function getEmptyFileContent(string $extension): string { + $supportedExtensions = ['odt', 'ods', 'odp', 'odg', 'docx', 'xlsx', 'pptx']; + $emptyPath = __DIR__ . '/../emptyTemplates/template.' . $extension; + + if (in_array($extension, $supportedExtensions, true) && file_exists($emptyPath)) { + return file_get_contents($emptyPath); + } + + return ''; + } + + public function isSupportedTemplateSource(string $extension): bool { + $supportedExtensions = ['ott', 'otg', 'otp', 'ots']; + return in_array($extension, $supportedExtensions, true); + } + + public function setTemplateSource(int $fileId, int $templateId): void { + try { + $query = $this->db->getQueryBuilder(); + $query->insert('richdocuments_template') + ->values([ + 'userid' => $query->createNamedParameter($this->userId), + 'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT), + 'templateid' => $query->createNamedParameter($templateId, IQueryBuilder::PARAM_INT), + 'timestamp' => $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT) + ]); + $query->executeStatement(); + } catch (Throwable $e) { + $this->logger->warning('Could not store template source', ['exception' => $e]); + // Ignore failure and proceed with empty template + } + } + + public function getTemplateSource(int $fileId): ?File { + $templateId = 0; + try { + $query = $this->db->getQueryBuilder(); + $query->select('templateid') + ->from('richdocuments_template') + ->where($query->expr()->eq('userid', $query->createNamedParameter($this->userId))) + ->andWhere($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + $result = $query->executeQuery(); + $templateId = (int)$result->fetchOne(); + + $query->delete('richdocuments_template') + ->where($query->expr()->eq('userid', $query->createNamedParameter($this->userId))) + ->andWhere($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } catch (Throwable $e) { + // Ignore failure and proceed with empty template + $this->logger->warning('Could not retrieve template source', ['exception' => $e]); + return null; + } + + if ($templateId !== 0) { + try { + $template = $this->get($templateId); + } catch (NotFoundException $e) { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + try { + $template = $userFolder->getById($templateId); + } catch (NotFoundException $e) { + $this->logger->warning('Could not retrieve template source file', ['exception' => $e]); + return null; + } + } + return $template; + } + + return null; + } }