diff options
Diffstat (limited to 'lib/private/Preview')
44 files changed, 1063 insertions, 1228 deletions
diff --git a/lib/private/Preview/BMP.php b/lib/private/Preview/BMP.php index c429f31f0e2..f275aecf0cf 100644 --- a/lib/private/Preview/BMP.php +++ b/lib/private/Preview/BMP.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php index ab40aeaaa79..3138abb1bf9 100644 --- a/lib/private/Preview/BackgroundCleanupJob.php +++ b/lib/private/Preview/BackgroundCleanupJob.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; -use OC\BackgroundJob\TimedJob; use OC\Preview\Storage\Root; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\IMimeTypeLoader; use OCP\Files\NotFoundException; @@ -35,29 +19,17 @@ use OCP\IDBConnection; class BackgroundCleanupJob extends TimedJob { - /** @var IDBConnection */ - private $connection; - - /** @var Root */ - private $previewFolder; - - /** @var bool */ - private $isCLI; - - /** @var IMimeTypeLoader */ - private $mimeTypeLoader; - - public function __construct(IDBConnection $connection, - Root $previewFolder, - IMimeTypeLoader $mimeTypeLoader, - bool $isCLI) { + public function __construct( + ITimeFactory $timeFactory, + private IDBConnection $connection, + private Root $previewFolder, + private IMimeTypeLoader $mimeTypeLoader, + private bool $isCLI, + ) { + parent::__construct($timeFactory); // Run at most once an hour - $this->setInterval(3600); - - $this->connection = $connection; - $this->previewFolder = $previewFolder; - $this->isCLI = $isCLI; - $this->mimeTypeLoader = $mimeTypeLoader; + $this->setInterval(60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } public function run($argument) { @@ -79,6 +51,11 @@ class BackgroundCleanupJob extends TimedJob { } private function getOldPreviewLocations(): \Iterator { + if ($this->connection->getShardDefinition('filecache')) { + // sharding is new enough that we don't need to support this + return; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('a.name') ->from('filecache', 'a') @@ -86,18 +63,20 @@ class BackgroundCleanupJob extends TimedJob { $qb->expr()->castColumn('a.name', IQueryBuilder::PARAM_INT), 'b.fileid' )) ->where( - $qb->expr()->isNull('b.fileid') - )->andWhere( - $qb->expr()->eq('a.parent', $qb->createNamedParameter($this->previewFolder->getId())) - )->andWhere( - $qb->expr()->like('a.name', $qb->createNamedParameter('__%')) + $qb->expr()->andX( + $qb->expr()->isNull('b.fileid'), + $qb->expr()->eq('a.storage', $qb->createNamedParameter($this->previewFolder->getStorageId())), + $qb->expr()->eq('a.parent', $qb->createNamedParameter($this->previewFolder->getId())), + $qb->expr()->like('a.name', $qb->createNamedParameter('__%')), + $qb->expr()->eq('a.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))) + ) ); if (!$this->isCLI) { $qb->setMaxResults(10); } - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($row = $cursor->fetch()) { yield $row['name']; @@ -111,7 +90,7 @@ class BackgroundCleanupJob extends TimedJob { $qb->select('path', 'mimetype') ->from('filecache') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($this->previewFolder->getId()))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -119,6 +98,15 @@ class BackgroundCleanupJob extends TimedJob { return []; } + if ($this->connection->getShardDefinition('filecache')) { + $chunks = $this->getAllPreviewIds($data['path'], 1000); + foreach ($chunks as $chunk) { + yield from $this->findMissingSources($chunk); + } + + return; + } + /* * This lovely like is the result of the way the new previews are stored * We take the md5 of the name (fileid) and split the first 7 chars. That way @@ -126,6 +114,21 @@ class BackgroundCleanupJob extends TimedJob { */ $like = $this->connection->escapeLikeParameter($data['path']) . '/_/_/_/_/_/_/_/%'; + /* + * Deleting a file will not delete related previews right away. + * + * A delete request is usually an HTTP request. + * The preview deleting is done by a background job to avoid timeouts. + * + * Previews for a file are stored within a folder in appdata_/preview using the fileid as folder name. + * Preview folders in oc_filecache are identified by a.storage, a.path (cf. $like) and a.mimetype. + * + * To find preview folders to delete, we query oc_filecache for a preview folder in app data, matching the preview folder structure + * and use the name to left join oc_filecache on a.name = b.fileid. A left join returns all rows from the left table (a), + * even if there are no matches in the right table (b). + * + * If the related file is deleted, b.fileid will be null and the preview folder can be deleted. + */ $qb = $this->connection->getQueryBuilder(); $qb->select('a.name') ->from('filecache', 'a') @@ -145,7 +148,7 @@ class BackgroundCleanupJob extends TimedJob { $qb->setMaxResults(10); } - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($row = $cursor->fetch()) { yield $row['name']; @@ -153,4 +156,46 @@ class BackgroundCleanupJob extends TimedJob { $cursor->closeCursor(); } + + private function getAllPreviewIds(string $previewRoot, int $chunkSize): \Iterator { + // See `getNewPreviewLocations` for some more info about the logic here + $like = $this->connection->escapeLikeParameter($previewRoot) . '/_/_/_/_/_/_/_/%'; + + $qb = $this->connection->getQueryBuilder(); + $qb->select('name', 'fileid') + ->from('filecache') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('storage', $qb->createNamedParameter($this->previewFolder->getStorageId())), + $qb->expr()->like('path', $qb->createNamedParameter($like)), + $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))), + $qb->expr()->gt('fileid', $qb->createParameter('min_id')), + ) + ) + ->orderBy('fileid', 'ASC') + ->setMaxResults($chunkSize); + + $minId = 0; + while (true) { + $qb->setParameter('min_id', $minId); + $rows = $qb->executeQuery()->fetchAll(); + if (count($rows) > 0) { + $minId = $rows[count($rows) - 1]['fileid']; + yield array_map(function ($row) { + return (int)$row['name']; + }, $rows); + } else { + break; + } + } + } + + private function findMissingSources(array $ids): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('fileid') + ->from('filecache') + ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + return array_diff($ids, $found); + } } diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php index 3a4108664dd..a3d5fbfd4ec 100644 --- a/lib/private/Preview/Bitmap.php +++ b/lib/private/Preview/Bitmap.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -37,6 +18,17 @@ use Psr\Log\LoggerInterface; * @package OC\Preview */ abstract class Bitmap extends ProviderV2 { + /** + * List of MIME types that this preview provider is allowed to process. + * + * These should correspond to the MIME types *identified* by Imagemagick + * for files to be processed by this provider. These do / will not + * necessarily need to match the MIME types stored in the database + * (which are identified by IMimeTypeDetector). + * + * @return string Regular expression + */ + abstract protected function getAllowedMimeTypes(): string; /** * {@inheritDoc} @@ -55,7 +47,7 @@ abstract class Bitmap extends ProviderV2 { try { $bp = $this->getResizedPreview($tmpPath, $maxX, $maxY); } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->error( + \OC::$server->get(LoggerInterface::class)->info( 'File: ' . $file->getPath() . ' Imagick says:', [ 'exception' => $e, @@ -68,8 +60,8 @@ abstract class Bitmap extends ProviderV2 { $this->cleanTmpFiles(); //new bitmap image object - $image = new \OC_Image(); - $image->loadFromData((string) $bp); + $image = new \OCP\Image(); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } @@ -87,10 +79,19 @@ abstract class Bitmap extends ProviderV2 { * @param int $maxY * * @return \Imagick + * + * @throws \Exception */ private function getResizedPreview($tmpPath, $maxX, $maxY) { $bp = new Imagick(); + // Validate mime type + $bp->pingImage($tmpPath . '[0]'); + $mimeType = $bp->getImageMimeType(); + if (!preg_match($this->getAllowedMimeTypes(), $mimeType)) { + throw new \Exception('File mime type does not match the preview provider: ' . $mimeType); + } + // Layer 0 contains either the bitmap or a flat representation of all vector layers $bp->readImage($tmpPath . '[0]'); diff --git a/lib/private/Preview/Bundled.php b/lib/private/Preview/Bundled.php index 063c69ba5dd..6100e8262a4 100644 --- a/lib/private/Preview/Bundled.php +++ b/lib/private/Preview/Bundled.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; @@ -31,6 +15,10 @@ use OCP\IImage; */ abstract class Bundled extends ProviderV2 { protected function extractThumbnail(File $file, string $path): ?IImage { + if ($file->getSize() === 0) { + return null; + } + $sourceTmp = \OC::$server->getTempManager()->getTemporaryFile(); $targetTmp = \OC::$server->getTempManager()->getTemporaryFile(); $this->tmpFiles[] = $sourceTmp; @@ -43,7 +31,7 @@ abstract class Bundled extends ProviderV2 { $zip = new ZIP($sourceTmp); $zip->extractFile($path, $targetTmp); - $image = new \OC_Image(); + $image = new \OCP\Image(); $image->loadFromFile($targetTmp); $image->fixOrientation(); diff --git a/lib/private/Preview/EMF.php b/lib/private/Preview/EMF.php new file mode 100644 index 00000000000..a3c48e7d8aa --- /dev/null +++ b/lib/private/Preview/EMF.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Preview; + +class EMF extends Office { + public function getMimeType(): string { + return '/image\/emf/'; + } +} diff --git a/lib/private/Preview/Font.php b/lib/private/Preview/Font.php index baf29e1defc..79e537f6ffb 100644 --- a/lib/private/Preview/Font.php +++ b/lib/private/Preview/Font.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -30,4 +15,11 @@ class Font extends Bitmap { public function getMimeType(): string { return '/application\/(?:font-sfnt|x-font$)/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/(application|image)\/(?:font-sfnt|x-font|x-otf|x-ttf|x-pfb$)/'; + } } diff --git a/lib/private/Preview/GIF.php b/lib/private/Preview/GIF.php index 482f014e3dc..941ef68648a 100644 --- a/lib/private/Preview/GIF.php +++ b/lib/private/Preview/GIF.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index cef3fa4039a..4a7341896ef 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -1,81 +1,41 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Elijah Martin-Merrill <elijah@nyp-itsours.com> - * @author J0WI <J0WI@users.noreply.github.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Scott Dutton <scott@exussum.co.uk> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IAppData; +use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\InMemoryFile; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; use OCP\IImage; use OCP\IPreview; use OCP\IStreamImage; +use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Preview\IProviderV2; use OCP\Preview\IVersionedPreviewFile; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +use Psr\Log\LoggerInterface; class Generator { + public const SEMAPHORE_ID_ALL = 0x0a11; + public const SEMAPHORE_ID_NEW = 0x07ea; - /** @var IPreview */ - private $previewManager; - /** @var IConfig */ - private $config; - /** @var IAppData */ - private $appData; - /** @var GeneratorHelper */ - private $helper; - /** @var EventDispatcherInterface */ - private $eventDispatcher; - - /** - * @param IConfig $config - * @param IPreview $previewManager - * @param IAppData $appData - * @param GeneratorHelper $helper - * @param EventDispatcherInterface $eventDispatcher - */ public function __construct( - IConfig $config, - IPreview $previewManager, - IAppData $appData, - GeneratorHelper $helper, - EventDispatcherInterface $eventDispatcher + private IConfig $config, + private IPreview $previewManager, + private IAppData $appData, + private GeneratorHelper $helper, + private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger, ) { - $this->config = $config; - $this->previewManager = $previewManager; - $this->appData = $appData; - $this->helper = $helper; - $this->eventDispatcher = $eventDispatcher; } /** @@ -84,45 +44,59 @@ class Generator { * The cache is searched first and if nothing usable was found then a preview is * generated by one of the providers * - * @param File $file - * @param int $width - * @param int $height - * @param bool $crop - * @param string $mode - * @param string $mimeType * @return ISimpleFile * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) { + public function getPreview( + File $file, + int $width = -1, + int $height = -1, + bool $crop = false, + string $mode = IPreview::MODE_FILL, + ?string $mimeType = null, + bool $cacheResult = true, + ): ISimpleFile { $specification = [ 'width' => $width, 'height' => $height, 'crop' => $crop, 'mode' => $mode, ]; - $this->eventDispatcher->dispatch( - IPreview::EVENT, - new GenericEvent($file, $specification) - ); + + $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent( + $file, + $width, + $height, + $crop, + $mode, + $mimeType, + )); + + $this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [ + 'path' => $file->getPath(), + 'width' => $width, + 'height' => $height, + 'crop' => $crop, + 'mode' => $mode, + 'mimeType' => $mimeType, + ]); + // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want - return $this->generatePreviews($file, [$specification], $mimeType); + return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult); } /** * Generates previews of a file * - * @param File $file - * @param array $specifications - * @param string $mimeType - * @return ISimpleFile the last preview that was generated * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - public function generatePreviews(File $file, array $specifications, $mimeType = null) { + public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile { //Make sure that we can read the file if (!$file->isReadable()) { + $this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]); throw new NotFoundException('Cannot read file'); } @@ -131,39 +105,29 @@ class Generator { } $previewFolder = $this->getPreviewFolder($file); + // List every existing preview first instead of trying to find them one by one + $previewFiles = $previewFolder->getDirectoryListing(); $previewVersion = ''; if ($file instanceof IVersionedPreviewFile) { $previewVersion = $file->getPreviewVersion() . '-'; } - // If imaginary is enabled, and we request a small thumbnail, - // let's not generate the max preview for performance reasons - if (count($specifications) === 1 - && ($specifications[0]['width'] <= 256 || $specifications[0]['height'] <= 256) - && preg_match(Imaginary::supportedMimeTypes(), $mimeType) - && $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') { - $crop = $specifications[0]['crop'] ?? false; - $preview = $this->getSmallImagePreview($previewFolder, $file, $mimeType, $previewVersion, $crop); - - if ($preview->getSize() === 0) { - $preview->delete(); - throw new NotFoundException('Cached preview size 0, invalid!'); - } - - return $preview; - } - // Get the max preview and infer the max preview sizes from that - $maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType, $previewVersion); + $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion); $maxPreviewImage = null; // only load the image when we need it if ($maxPreview->getSize() === 0) { $maxPreview->delete(); + $this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]); throw new NotFoundException('Max preview size 0, invalid!'); } [$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion); + if ($maxWidth <= 0 || $maxHeight <= 0) { + throw new NotFoundException('The maximum preview sizes are zero or less pixels'); + } + $preview = null; foreach ($specifications as $specification) { @@ -191,7 +155,7 @@ class Generator { // Try to get a cached preview. Else generate (and store) one try { try { - $preview = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); + $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion); } catch (NotFoundException $e) { if (!$this->previewManager->isMimeSupported($mimeType)) { throw new NotFoundException(); @@ -201,10 +165,13 @@ class Generator { $maxPreviewImage = $this->helper->getImage($maxPreview); } - $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion); + $this->logger->debug('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]); + $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult); + // New file, augment our array + $previewFiles[] = $preview; } } catch (\InvalidArgumentException $e) { - throw new NotFoundException("", 0, $e); + throw new NotFoundException('', 0, $e); } if ($preview->getSize() === 0) { @@ -212,10 +179,11 @@ class Generator { throw new NotFoundException('Cached preview size 0, invalid!'); } } + assert($preview !== null); // Free memory being used by the embedded image resource. Without this the image is kept in memory indefinitely. // Garbage Collection does NOT free this memory. We have to do it ourselves. - if ($maxPreviewImage instanceof \OC_Image) { + if ($maxPreviewImage instanceof \OCP\Image) { $maxPreviewImage->destroy(); } @@ -223,86 +191,129 @@ class Generator { } /** - * Generate a small image straight away without generating a max preview first - * Preview generated is 256x256 + * Acquire a semaphore of the specified id and concurrency, blocking if necessary. + * Return an identifier of the semaphore on success, which can be used to release it via + * {@see Generator::unguardWithSemaphore()}. + * + * @param int $semId + * @param int $concurrency + * @return false|\SysvSemaphore the semaphore on success or false on failure */ - private function getSmallImagePreview(ISimpleFolder $previewFolder, File $file, string $mimeType, string $prefix, bool $crop) { - $nodes = $previewFolder->getDirectoryListing(); - - foreach ($nodes as $node) { - $name = $node->getName(); - if (($prefix === '' || strpos($name, $prefix) === 0) - && (str_starts_with($name, '256-256-crop') && $crop || str_starts_with($name, '256-256') && !$crop)) { - return $node; - } + public static function guardWithSemaphore(int $semId, int $concurrency) { + if (!extension_loaded('sysvsem')) { + return false; } + $sem = sem_get($semId, $concurrency); + if ($sem === false) { + return false; + } + if (!sem_acquire($sem)) { + return false; + } + return $sem; + } - $previewProviders = $this->previewManager->getProviders(); - foreach ($previewProviders as $supportedMimeType => $providers) { - // Filter out providers that does not support this mime - if (!preg_match($supportedMimeType, $mimeType)) { - continue; - } - - foreach ($providers as $providerClosure) { - $provider = $this->helper->getProvider($providerClosure); - if (!($provider instanceof IProviderV2)) { - continue; - } - - if (!$provider->isAvailable($file)) { - continue; - } - - $preview = $this->helper->getThumbnail($provider, $file, 256, 256, true); - - if (!($preview instanceof IImage)) { - continue; - } + /** + * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}. + * + * @param false|\SysvSemaphore $semId the semaphore identifier returned by guardWithSemaphore + * @return bool + */ + public static function unguardWithSemaphore(false|\SysvSemaphore $semId): bool { + if ($semId === false || !($semId instanceof \SysvSemaphore)) { + return false; + } + return sem_release($semId); + } - // Try to get the extension. - try { - $ext = $this->getExtention($preview->dataMimeType()); - } catch (\InvalidArgumentException $e) { - // Just continue to the next iteration if this preview doesn't have a valid mimetype - continue; + /** + * Get the number of concurrent threads supported by the host. + * + * @return int number of concurrent threads, or 0 if it cannot be determined + */ + public static function getHardwareConcurrency(): int { + static $width; + + if (!isset($width)) { + if (function_exists('ini_get')) { + $openBasedir = ini_get('open_basedir'); + if (empty($openBasedir) || strpos($openBasedir, '/proc/cpuinfo') !== false) { + $width = is_readable('/proc/cpuinfo') ? substr_count(file_get_contents('/proc/cpuinfo'), 'processor') : 0; + } else { + $width = 0; } + } else { + $width = 0; + } + } + return $width; + } - $path = $this->generatePath(256, 256, $crop, $preview->dataMimeType(), $prefix); - try { - $file = $previewFolder->newFile($path); - if ($preview instanceof IStreamImage) { - $file->putContent($preview->resource()); - } else { - $file->putContent($preview->data()); - } - } catch (NotPermittedException $e) { - throw new NotFoundException(); - } + /** + * Get number of concurrent preview generations from system config + * + * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`, + * are available. If not set, the default values are determined with the hardware concurrency + * of the host. In case the hardware concurrency cannot be determined, or the user sets an + * invalid value, fallback values are: + * For new images whose previews do not exist and need to be generated, 4; + * For all preview generation requests, 8. + * Value of `preview_concurrency_all` should be greater than or equal to that of + * `preview_concurrency_new`, otherwise, the latter is returned. + * + * @param string $type either `preview_concurrency_new` or `preview_concurrency_all` + * @return int number of concurrent preview generations, or -1 if $type is invalid + */ + public function getNumConcurrentPreviews(string $type): int { + static $cached = []; + if (array_key_exists($type, $cached)) { + return $cached[$type]; + } - return $file; - } + $hardwareConcurrency = self::getHardwareConcurrency(); + switch ($type) { + case 'preview_concurrency_all': + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; + $concurrency_all = $this->config->getSystemValueInt($type, $fallback); + $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $cached[$type] = max($concurrency_all, $concurrency_new); + break; + case 'preview_concurrency_new': + $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; + $cached[$type] = $this->config->getSystemValueInt($type, $fallback); + break; + default: + return -1; } + return $cached[$type]; } /** * @param ISimpleFolder $previewFolder + * @param ISimpleFile[] $previewFiles * @param File $file * @param string $mimeType * @param string $prefix * @return ISimpleFile * @throws NotFoundException */ - private function getMaxPreview(ISimpleFolder $previewFolder, File $file, $mimeType, $prefix) { - $nodes = $previewFolder->getDirectoryListing(); - - foreach ($nodes as $node) { + private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) { + // We don't know the max preview size, so we can't use getCachedPreview. + // It might have been generated with a higher resolution than the current value. + foreach ($previewFiles as $node) { $name = $node->getName(); - if (($prefix === '' || strpos($name, $prefix) === 0) && strpos($name, 'max')) { + if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) { return $node; } } + $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); + $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); + + return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix); + } + + private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) { $previewProviders = $this->previewManager->getProviders(); foreach ($previewProviders as $supportedMimeType => $providers) { // Filter out providers that does not support this mime @@ -320,30 +331,29 @@ class Generator { continue; } - $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096); - $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096); - - $preview = $this->helper->getThumbnail($provider, $file, $maxWidth, $maxHeight); - - if (!($preview instanceof IImage)) { - continue; + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + try { + $this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [ + 'mimeType' => $mimeType, + 'width' => $width, + 'height' => $height, + ]); + $preview = $this->helper->getThumbnail($provider, $file, $width, $height); + } finally { + self::unguardWithSemaphore($sem); } - // Try to get the extention. - try { - $ext = $this->getExtention($preview->dataMimeType()); - } catch (\InvalidArgumentException $e) { - // Just continue to the next iteration if this preview doesn't have a valid mimetype + if (!($preview instanceof IImage)) { continue; } - $path = $prefix . (string)$preview->width() . '-' . (string)$preview->height() . '-max.' . $ext; + $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix); try { - $file = $previewFolder->newFile($path); if ($preview instanceof IStreamImage) { - $file->putContent($preview->resource()); + return $previewFolder->newFile($path, $preview->resource()); } else { - $file->putContent($preview->data()); + return $previewFolder->newFile($path, $preview->data()); } } catch (NotPermittedException $e) { throw new NotFoundException(); @@ -353,7 +363,7 @@ class Generator { } } - throw new NotFoundException(); + throw new NotFoundException('No provider successfully handled the preview generation'); } /** @@ -370,17 +380,21 @@ class Generator { * @param int $width * @param int $height * @param bool $crop + * @param bool $max * @param string $mimeType * @param string $prefix * @return string */ - private function generatePath($width, $height, $crop, $mimeType, $prefix) { + private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) { $path = $prefix . (string)$width . '-' . (string)$height; if ($crop) { $path .= '-crop'; } + if ($max) { + $path .= '-max'; + } - $ext = $this->getExtention($mimeType); + $ext = $this->getExtension($mimeType); $path .= '.' . $ext; return $path; } @@ -396,7 +410,6 @@ class Generator { * @return int[] */ private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) { - /* * If we are not cropping we have to make sure the requested image * respects the aspect ratio of the original. @@ -475,60 +488,69 @@ class Generator { } /** - * @param ISimpleFolder $previewFolder - * @param ISimpleFile $maxPreview - * @param int $width - * @param int $height - * @param bool $crop - * @param int $maxWidth - * @param int $maxHeight - * @param string $prefix - * @return ISimpleFile * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) { + private function generatePreview( + ISimpleFolder $previewFolder, + IImage $maxPreview, + int $width, + int $height, + bool $crop, + int $maxWidth, + int $maxHeight, + string $prefix, + bool $cacheResult, + ): ISimpleFile { $preview = $maxPreview; if (!$preview->valid()) { throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); } - if ($crop) { - if ($height !== $preview->height() && $width !== $preview->width()) { - //Resize - $widthR = $preview->width() / $width; - $heightR = $preview->height() / $height; - - if ($widthR > $heightR) { - $scaleH = $height; - $scaleW = $maxWidth / $heightR; - } else { - $scaleH = $maxHeight / $widthR; - $scaleW = $width; + $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); + $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); + try { + if ($crop) { + if ($height !== $preview->height() && $width !== $preview->width()) { + //Resize + $widthR = $preview->width() / $width; + $heightR = $preview->height() / $height; + + if ($widthR > $heightR) { + $scaleH = $height; + $scaleW = $maxWidth / $heightR; + } else { + $scaleH = $maxHeight / $widthR; + $scaleW = $width; + } + $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); } - $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH)); + $cropX = (int)floor(abs($width - $preview->width()) * 0.5); + $cropY = (int)floor(abs($height - $preview->height()) * 0.5); + $preview = $preview->cropCopy($cropX, $cropY, $width, $height); + } else { + $preview = $maxPreview->resizeCopy(max($width, $height)); } - $cropX = (int)floor(abs($width - $preview->width()) * 0.5); - $cropY = (int)floor(abs($height - $preview->height()) * 0.5); - $preview = $preview->cropCopy($cropX, $cropY, $width, $height); - } else { - $preview = $maxPreview->resizeCopy(max($width, $height)); + } finally { + self::unguardWithSemaphore($sem); } - $path = $this->generatePath($width, $height, $crop, $preview->dataMimeType(), $prefix); + $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix); try { - $file = $previewFolder->newFile($path); - $file->putContent($preview->data()); + if ($cacheResult) { + return $previewFolder->newFile($path, $preview->data()); + } else { + return new InMemoryFile($path, $preview->data()); + } } catch (NotPermittedException $e) { throw new NotFoundException(); } - return $file; } /** - * @param ISimpleFolder $previewFolder + * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing() * @param int $width * @param int $height * @param bool $crop @@ -538,10 +560,15 @@ class Generator { * * @throws NotFoundException */ - private function getCachedPreview(ISimpleFolder $previewFolder, $width, $height, $crop, $mimeType, $prefix) { - $path = $this->generatePath($width, $height, $crop, $mimeType, $prefix); - - return $previewFolder->getFile($path); + private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) { + $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix); + foreach ($files as $file) { + if ($file->getName() === $path) { + $this->logger->debug('Found cached preview: {path}', ['path' => $path]); + return $file; + } + } + throw new NotFoundException(); } /** @@ -549,12 +576,19 @@ class Generator { * * @param File $file * @return ISimpleFolder + * + * @throws InvalidPathException + * @throws NotFoundException + * @throws NotPermittedException */ private function getPreviewFolder(File $file) { + // Obtain file id outside of try catch block to prevent the creation of an existing folder + $fileId = (string)$file->getId(); + try { - $folder = $this->appData->getFolder($file->getId()); + $folder = $this->appData->getFolder($fileId); } catch (NotFoundException $e) { - $folder = $this->appData->newFolder($file->getId()); + $folder = $this->appData->newFolder($fileId); } return $folder; @@ -565,12 +599,14 @@ class Generator { * @return null|string * @throws \InvalidArgumentException */ - private function getExtention($mimeType) { + private function getExtension($mimeType) { switch ($mimeType) { case 'image/png': return 'png'; case 'image/jpeg': return 'jpg'; + case 'image/webp': + return 'webp'; case 'image/gif': return 'gif'; default: diff --git a/lib/private/Preview/GeneratorHelper.php b/lib/private/Preview/GeneratorHelper.php index 6a94b948241..e914dcc2002 100644 --- a/lib/private/Preview/GeneratorHelper.php +++ b/lib/private/Preview/GeneratorHelper.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; @@ -38,7 +19,6 @@ use OCP\Preview\IProviderV2; * Very small wrapper class to make the generator fully unit testable */ class GeneratorHelper { - /** @var IRootFolder */ private $rootFolder; diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index 7ce6b93ba3b..64eb48e58df 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -3,35 +3,16 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, ownCloud GmbH - * @copyright Copyright (c) 2018, Sebastian Steinmetz (me@sebastiansteinmetz.ch) - * - * @author J0WI <J0WI@users.noreply.github.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sebastian Steinmetz <462714+steiny2k@users.noreply.github.com> - * @author Sebastian Steinmetz <me@sebastiansteinmetz.ch> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2018 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -44,14 +25,14 @@ class HEIC extends ProviderV2 { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/hei(f|c)/'; + return '/image\/(x-)?hei(f|c)/'; } /** * {@inheritDoc} */ public function isAvailable(FileInfo $file): bool { - return in_array('HEIC', \Imagick::queryFormats("HEI*")); + return in_array('HEIC', \Imagick::queryFormats('HEI*')); } /** @@ -64,8 +45,8 @@ class HEIC extends ProviderV2 { $tmpPath = $this->getLocalFile($file); if ($tmpPath === false) { - \OC::$server->get(LoggerInterface::class)->error( - 'Failed to get thumbnail for: ' . $file->getPath(), + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), ['app' => 'core'] ); return null; @@ -89,8 +70,8 @@ class HEIC extends ProviderV2 { $this->cleanTmpFiles(); //new bitmap image object - $image = new \OC_Image(); - $image->loadFromData((string) $bp); + $image = new \OCP\Image(); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } @@ -108,13 +89,26 @@ class HEIC extends ProviderV2 { * @param int $maxY * * @return \Imagick + * + * @throws \Exception */ private function getResizedPreview($tmpPath, $maxX, $maxY) { $bp = new \Imagick(); + // Some HEIC files just contain (or at least are identified as) other formats + // like JPEG. We just need to check if the image is safe to process. + $bp->pingImage($tmpPath . '[0]'); + $mimeType = $bp->getImageMimeType(); + if (!preg_match('/^image\/(x-)?(png|jpeg|gif|bmp|tiff|webp|hei(f|c)|avif)$/', $mimeType)) { + throw new \Exception('File mime type does not match the preview provider: ' . $mimeType); + } + // Layer 0 contains either the bitmap or a flat representation of all vector layers $bp->readImage($tmpPath . '[0]'); + // Fix orientation from EXIF + $bp->autoOrient(); + $bp->setImageFormat('jpg'); $bp = $this->resize($bp, $maxX, $maxY); diff --git a/lib/private/Preview/IMagickSupport.php b/lib/private/Preview/IMagickSupport.php new file mode 100644 index 00000000000..8225b1e7644 --- /dev/null +++ b/lib/private/Preview/IMagickSupport.php @@ -0,0 +1,44 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Preview; + +use OCP\ICache; +use OCP\ICacheFactory; + +class IMagickSupport { + private ICache $cache; + private ?\Imagick $imagick; + + public function __construct(ICacheFactory $cacheFactory) { + $this->cache = $cacheFactory->createLocal('imagick'); + + if (extension_loaded('imagick')) { + $this->imagick = new \Imagick(); + } else { + $this->imagick = null; + } + } + + public function hasExtension(): bool { + return !is_null($this->imagick); + } + + public function supportsFormat(string $format): bool { + if (is_null($this->imagick)) { + return false; + } + + $cached = $this->cache->get($format); + if (!is_null($cached)) { + return $cached; + } + + $formatSupported = count($this->imagick->queryFormats($format)) === 1; + $this->cache->set($format, $cached); + return $formatSupported; + } +} diff --git a/lib/private/Preview/Illustrator.php b/lib/private/Preview/Illustrator.php index 3d926c304c5..bff556a3177 100644 --- a/lib/private/Preview/Illustrator.php +++ b/lib/private/Preview/Illustrator.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -31,4 +15,11 @@ class Illustrator extends Bitmap { public function getMimeType(): string { return '/application\/illustrator/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/(illustrator|pdf)/'; + } } diff --git a/lib/private/Preview/Image.php b/lib/private/Preview/Image.php index 4bf0cb7a3d4..78a402c636a 100644 --- a/lib/private/Preview/Image.php +++ b/lib/private/Preview/Image.php @@ -1,39 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author josh4trunks <joshruehlig@gmail.com> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; use OCP\Files\File; use OCP\IImage; +use OCP\Server; +use Psr\Log\LoggerInterface; abstract class Image extends ProviderV2 { - /** * {@inheritDoc} */ @@ -45,9 +24,16 @@ abstract class Image extends ProviderV2 { return null; } - $image = new \OC_Image(); + $image = new \OCP\Image(); $fileName = $this->getLocalFile($file); + if ($fileName === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } $image->loadFromFile($fileName); $image->fixOrientation(); diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index 4da88f1ab26..d421da74ac8 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -1,34 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2020, Nextcloud, GmbH. - * - * @author Vincent Petry <vincent@nextcloud.com> - * @author Carl Schwan <carl@carlschwan.eu> - * - * @license AGPL-3.0-or-later - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; +use OC\StreamImage; use OCP\Files\File; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IImage; -use OC\StreamImage; +use OCP\Image; use Psr\Log\LoggerInterface; class Imaginary extends ProviderV2 { @@ -56,11 +41,11 @@ class Imaginary extends ProviderV2 { } public static function supportedMimeTypes(): string { - return '/image\/(bmp|x-bitmap|png|jpeg|gif|heic|svg|webp)/'; + return '/(image\/(bmp|x-bitmap|png|jpeg|gif|heic|heif|svg\+xml|tiff|webp)|application\/illustrator)/'; } public function getCroppedThumbnail(File $file, int $maxX, int $maxY, bool $crop): ?IImage { - $maxSizeForImages = $this->config->getSystemValue('preview_max_filesize_image', 50); + $maxSizeForImages = $this->config->getSystemValueInt('preview_max_filesize_image', 50); $size = $file->getSize(); @@ -77,61 +62,123 @@ class Imaginary extends ProviderV2 { // Object store $stream = $file->fopen('r'); + if (!$stream || !is_resource($stream) || feof($stream)) { + return null; + } $httpClient = $this->service->newClient(); + $convert = false; + $autorotate = true; + switch ($file->getMimeType()) { + case 'image/heic': + // Autorotate seems to be broken for Heic so disable for that + $autorotate = false; + $mimeType = 'jpeg'; + break; case 'image/gif': case 'image/png': $mimeType = 'png'; break; + case 'image/svg+xml': + case 'application/pdf': + case 'application/illustrator': + $convert = true; + // Converted files do not need to be autorotated + $autorotate = false; + $mimeType = 'png'; + break; default: $mimeType = 'jpeg'; } - $operations = [ - [ - 'operation' => 'autorotate', - ], - [ - 'operation' => ($crop ? 'smartcrop' : 'fit'), + $preview_format = $this->config->getSystemValueString('preview_format', 'jpeg'); + + switch ($preview_format) { // Change the format to the correct one + case 'webp': + $mimeType = 'webp'; + break; + default: + } + + $operations = []; + + if ($convert) { + $operations[] = [ + 'operation' => 'convert', 'params' => [ - 'width' => $maxX, - 'height' => $maxY, - 'stripmeta' => 'true', 'type' => $mimeType, - 'norotation' => 'true', ] + ]; + } elseif ($autorotate) { + $operations[] = [ + 'operation' => 'autorotate', + ]; + } + + switch ($mimeType) { + case 'jpeg': + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + break; + case 'webp': + $quality = $this->config->getAppValue('preview', 'webp_quality', '80'); + break; + default: + $quality = $this->config->getAppValue('preview', 'jpeg_quality', '80'); + } + + $operations[] = [ + 'operation' => ($crop ? 'smartcrop' : 'fit'), + 'params' => [ + 'width' => $maxX, + 'height' => $maxY, + 'stripmeta' => 'true', + 'type' => $mimeType, + 'norotation' => 'true', + 'quality' => $quality, ] ]; try { + $imaginaryKey = $this->config->getSystemValueString('preview_imaginary_key', ''); $response = $httpClient->post( $imaginaryUrl . '/pipeline', [ - 'query' => ['operations' => json_encode($operations)], + 'query' => ['operations' => json_encode($operations), 'key' => $imaginaryKey], 'stream' => true, 'content-type' => $file->getMimeType(), 'body' => $stream, 'nextcloud' => ['allow_local_address' => true], + 'timeout' => 120, + 'connect_timeout' => 3, ]); - } catch (\Exception $e) { - $this->logger->error('Imaginary preview generation failed: ' . $e->getMessage(), [ + } catch (\Throwable $e) { + $this->logger->info('Imaginary preview generation failed: ' . $e->getMessage(), [ 'exception' => $e, ]); return null; } if ($response->getStatusCode() !== 200) { - $this->logger->error('Imaginary preview generation failed: ' . json_decode($response->getBody())['message']); + $this->logger->info('Imaginary preview generation failed: ' . json_decode($response->getBody())['message']); return null; } - if ($response->getHeader('X-Image-Width') && $response->getHeader('X-Image-Height')) { - $maxX = (int)$response->getHeader('X-Image-Width'); - $maxY = (int)$response->getHeader('X-Image-Height'); + // This is not optimal but previews are distorted if the wrong width and height values are + // used. Both dimension headers are only sent when passing the option "-return-size" to + // Imaginary. + if ($response->getHeader('Image-Width') && $response->getHeader('Image-Height')) { + $image = new StreamImage( + $response->getBody(), + $response->getHeader('Content-Type'), + (int)$response->getHeader('Image-Width'), + (int)$response->getHeader('Image-Height'), + ); + } else { + $image = new Image(); + $image->loadFromFileHandle($response->getBody()); } - $image = new StreamImage($response->getBody(), $response->getHeader('Content-Type'), $maxX, $maxY); return $image->valid() ? $image : null; } diff --git a/lib/private/Preview/ImaginaryPDF.php b/lib/private/Preview/ImaginaryPDF.php new file mode 100644 index 00000000000..d26c6d1b7ff --- /dev/null +++ b/lib/private/Preview/ImaginaryPDF.php @@ -0,0 +1,14 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Preview; + +class ImaginaryPDF extends Imaginary { + public static function supportedMimeTypes(): string { + return '/application\/pdf/'; + } +} diff --git a/lib/private/Preview/JPEG.php b/lib/private/Preview/JPEG.php index 14ae95690c0..a1a394f81c9 100644 --- a/lib/private/Preview/JPEG.php +++ b/lib/private/Preview/JPEG.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/Krita.php b/lib/private/Preview/Krita.php index eb25db9928c..e96fac993aa 100644 --- a/lib/private/Preview/Krita.php +++ b/lib/private/Preview/Krita.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; diff --git a/lib/private/Preview/MP3.php b/lib/private/Preview/MP3.php index dec838b6e5a..add0028738e 100644 --- a/lib/private/Preview/MP3.php +++ b/lib/private/Preview/MP3.php @@ -1,37 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; -use ID3Parser\ID3Parser; - use OCP\Files\File; use OCP\IImage; +use OCP\Server; +use Psr\Log\LoggerInterface; +use wapmorgan\Mp3Info\Mp3Info; +use function OCP\Log\logger; class MP3 extends ProviderV2 { /** @@ -45,18 +26,31 @@ class MP3 extends ProviderV2 { * {@inheritDoc} */ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { - $getID3 = new ID3Parser(); - $tmpPath = $this->getLocalFile($file); - $tags = $getID3->analyze($tmpPath); - $this->cleanTmpFiles(); - $picture = isset($tags['id3v2']['APIC'][0]['data']) ? $tags['id3v2']['APIC'][0]['data'] : null; - if (is_null($picture) && isset($tags['id3v2']['PIC'][0]['data'])) { - $picture = $tags['id3v2']['PIC'][0]['data']; + if ($tmpPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } + + try { + $audio = new Mp3Info($tmpPath, true); + /** @var string|null|false $picture */ + $picture = $audio->getCover(); + } catch (\Throwable $e) { + logger('core')->info('Error while getting cover from mp3 file: ' . $e->getMessage(), [ + 'fileId' => $file->getId(), + 'filePath' => $file->getPath(), + ]); + return null; + } finally { + $this->cleanTmpFiles(); } - if (!is_null($picture)) { - $image = new \OC_Image(); + if (is_string($picture)) { + $image = new \OCP\Image(); $image->loadFromData($picture); if ($image->valid()) { diff --git a/lib/private/Preview/MSOffice2003.php b/lib/private/Preview/MSOffice2003.php index 34cee499ff8..a52e618d484 100644 --- a/lib/private/Preview/MSOffice2003.php +++ b/lib/private/Preview/MSOffice2003.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/MSOffice2007.php b/lib/private/Preview/MSOffice2007.php index 3d3049e1c12..317f2dcc7f1 100644 --- a/lib/private/Preview/MSOffice2007.php +++ b/lib/private/Preview/MSOffice2007.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/MSOfficeDoc.php b/lib/private/Preview/MSOfficeDoc.php index e04503629e1..2e1044395f1 100644 --- a/lib/private/Preview/MSOfficeDoc.php +++ b/lib/private/Preview/MSOfficeDoc.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/MarkDown.php b/lib/private/Preview/MarkDown.php index 929f319f57d..c20433a1ac0 100644 --- a/lib/private/Preview/MarkDown.php +++ b/lib/private/Preview/MarkDown.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -70,7 +52,7 @@ class MarkDown extends TXT { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; + $fontSize = $maxX ? (int)((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; $image = imagecreate($maxX, $maxY); imagecolorallocate($image, 255, 255, 255); @@ -137,7 +119,7 @@ class MarkDown extends TXT { } } - $imageObject = new \OC_Image(); + $imageObject = new \OCP\Image(); $imageObject->setResource($image); return $imageObject->valid() ? $imageObject : null; diff --git a/lib/private/Preview/MimeIconProvider.php b/lib/private/Preview/MimeIconProvider.php new file mode 100644 index 00000000000..d1963fe882b --- /dev/null +++ b/lib/private/Preview/MimeIconProvider.php @@ -0,0 +1,83 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Preview; + +use OCA\Theming\ThemingDefaults; +use OCP\App\IAppManager; +use OCP\Files\IMimeTypeDetector; +use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\Preview\IMimeIconProvider; + +class MimeIconProvider implements IMimeIconProvider { + public function __construct( + protected IMimeTypeDetector $mimetypeDetector, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IAppManager $appManager, + protected ThemingDefaults $themingDefaults, + ) { + } + + public function getMimeIconUrl(string $mime): ?string { + if (!$mime) { + return null; + } + + // Fetch all the aliases + $aliases = $this->mimetypeDetector->getAllAliases(); + + // Remove comments + $aliases = array_filter($aliases, static function (string $key) { + return !($key === '' || $key[0] === '_'); + }, ARRAY_FILTER_USE_KEY); + + // Map all the aliases recursively + foreach ($aliases as $alias => $value) { + if ($alias === $mime) { + $mime = $value; + } + } + + $fileName = str_replace('/', '-', $mime); + if ($url = $this->searchfileName($fileName)) { + return $url; + } + + $mimeType = explode('/', $mime)[0]; + if ($url = $this->searchfileName($mimeType)) { + return $url; + } + + return null; + } + + private function searchfileName(string $fileName): ?string { + // If the file exists in the current enabled legacy + // custom theme, let's return it + $theme = $this->config->getSystemValue('theme', ''); + if (!empty($theme)) { + $path = "/themes/$theme/core/img/filetypes/$fileName.svg"; + if (file_exists(\OC::$SERVERROOT . $path)) { + return $this->urlGenerator->getAbsoluteURL($path); + } + } + + // Previously, we used to pass this through Theming + // But it was only used to colour icons containing + // 0082c9. Since with vue we moved to inline svg icons, + // we can just use the default core icons. + + // Finally, if the file exists in core, let's return it + $path = "/core/img/filetypes/$fileName.svg"; + if (file_exists(\OC::$SERVERROOT . $path)) { + return $this->urlGenerator->getAbsoluteURL($path); + } + + return null; + } +} diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php index 781cbad1954..47895f999d8 100644 --- a/lib/private/Preview/Movie.php +++ b/lib/private/Preview/Movie.php @@ -1,59 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Alexander A. Klimov <grandmaster@al2klimov.de> - * @author Daniel Schneider <daniel@schneidoa.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; +use OCP\IConfig; use OCP\IImage; +use OCP\ITempManager; +use OCP\Server; use Psr\Log\LoggerInterface; class Movie extends ProviderV2 { + private IConfig $config; - /** - * @deprecated 23.0.0 pass option to \OCP\Preview\ProviderV2 - * @var string - */ - public static $avconvBinary; - - /** - * @deprecated 23.0.0 pass option to \OCP\Preview\ProviderV2 - * @var string - */ - public static $ffmpegBinary; + private ?string $binary = null; - /** @var string */ - private $binary; + public function __construct(array $options = []) { + parent::__construct($options); + $this->config = Server::get(IConfig::class); + } - /** - * {@inheritDoc} - */ public function getMimeType(): string { return '/video\/.*/'; } @@ -62,14 +34,9 @@ class Movie extends ProviderV2 { * {@inheritDoc} */ public function isAvailable(FileInfo $file): bool { - // TODO: remove when avconv is dropped if (is_null($this->binary)) { if (isset($this->options['movieBinary'])) { $this->binary = $this->options['movieBinary']; - } elseif (is_string(self::$avconvBinary)) { - $this->binary = self::$avconvBinary; - } elseif (is_string(self::$ffmpegBinary)) { - $this->binary = self::$ffmpegBinary; } } return is_string($this->binary); @@ -87,10 +54,15 @@ class Movie extends ProviderV2 { $result = null; if ($this->useTempFile($file)) { - // try downloading 5 MB first as it's likely that the first frames are present there - // in some cases this doesn't work for example when the moov atom is at the - // end of the file, so if it fails we fall back to getting the full file - $sizeAttempts = [5242880, null]; + // Try downloading 5 MB first, as it's likely that the first frames are present there. + // In some cases this doesn't work, for example when the moov atom is at the + // end of the file, so if it fails we fall back to getting the full file. + // Unless the file is not local (e.g. S3) as we do not want to download the whole (e.g. 37Gb) file + if ($file->getStorage()->isLocal()) { + $sizeAttempts = [5242880, null]; + } else { + $sizeAttempts = [5242880]; + } } else { // size is irrelevant, only attempt once $sizeAttempts = [null]; @@ -98,15 +70,19 @@ class Movie extends ProviderV2 { foreach ($sizeAttempts as $size) { $absPath = $this->getLocalFile($file, $size); + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } - $result = null; - if (is_string($absPath)) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); + if ($result === null) { + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); - if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); - } + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); } } @@ -120,32 +96,84 @@ class Movie extends ProviderV2 { return $result; } + private function useHdr(string $absPath): bool { + // load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path + $ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe'); + // run ffprobe on the video file to get value of "color_transfer" + $test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0', + '-show_entries', 'stream=color_transfer', + '-of', 'default=noprint_wrappers=1:nokey=1', + $absPath]; + $test_hdr_proc = proc_open($test_hdr_cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $test_hdr_pipes); + if ($test_hdr_proc === false) { + return false; + } + $test_hdr_stdout = trim(stream_get_contents($test_hdr_pipes[1])); + $test_hdr_stderr = trim(stream_get_contents($test_hdr_pipes[2])); + proc_close($test_hdr_proc); + // search build options for libzimg (provides zscale filter) + $ffmpeg_libzimg_installed = strpos($test_hdr_stderr, '--enable-libzimg'); + // Only values of "smpte2084" and "arib-std-b67" indicate an HDR video. + // Only return true if video is detected as HDR and libzimg is installed. + if (($test_hdr_stdout === 'smpte2084' || $test_hdr_stdout === 'arib-std-b67') && $ffmpeg_libzimg_installed !== false) { + return true; + } else { + return false; + } + } + private function generateThumbNail(int $maxX, int $maxY, string $absPath, int $second): ?IImage { - $tmpPath = \OC::$server->getTempManager()->getTemporaryFile(); + $tmpPath = Server::get(ITempManager::class)->getTemporaryFile(); + + if ($tmpPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $absPath, + ['app' => 'core'] + ); + return null; + } $binaryType = substr(strrchr($this->binary, '/'), 1); if ($binaryType === 'avconv') { - $cmd = $this->binary . ' -y -ss ' . escapeshellarg((string)$second) . - ' -i ' . escapeshellarg($absPath) . - ' -an -f mjpeg -vframes 1 -vsync 1 ' . escapeshellarg($tmpPath) . - ' 2>&1'; + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-an', '-f', 'mjpeg', '-vframes', '1', '-vsync', '1', + $tmpPath]; } elseif ($binaryType === 'ffmpeg') { - $cmd = $this->binary . ' -y -ss ' . escapeshellarg((string)$second) . - ' -i ' . escapeshellarg($absPath) . - ' -f mjpeg -vframes 1' . - ' ' . escapeshellarg($tmpPath) . - ' 2>&1'; + if ($this->useHdr($absPath)) { + // Force colorspace to '2020_ncl' because some videos are + // tagged incorrectly as 'reserved' resulting in fail if not forced. + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-f', 'mjpeg', '-vframes', '1', + '-vf', 'zscale=min=2020_ncl:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p', + $tmpPath]; + } else { + // always default to generating preview using non-HDR command + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-f', 'mjpeg', '-vframes', '1', + $tmpPath]; + } } else { // Not supported unlink($tmpPath); return null; } - exec($cmd, $output, $returnCode); + $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + $returnCode = -1; + $output = ''; + if (is_resource($proc)) { + $stderr = trim(stream_get_contents($pipes[2])); + $stdout = trim(stream_get_contents($pipes[1])); + $returnCode = proc_close($proc); + $output = $stdout . $stderr; + } if ($returnCode === 0) { - $image = new \OC_Image(); + $image = new \OCP\Image(); $image->loadFromFile($tmpPath); if ($image->valid()) { unlink($tmpPath); @@ -156,8 +184,8 @@ class Movie extends ProviderV2 { } if ($second === 0) { - $logger = \OC::$server->get(LoggerInterface::class); - $logger->error('Movie preview generation failed Output: {output}', ['app' => 'core', 'output' => $output]); + $logger = Server::get(LoggerInterface::class); + $logger->info('Movie preview generation failed Output: {output}', ['app' => 'core', 'output' => $output]); } unlink($tmpPath); diff --git a/lib/private/Preview/Office.php b/lib/private/Preview/Office.php index 570988aa684..ffba0211de2 100644 --- a/lib/private/Preview/Office.php +++ b/lib/private/Preview/Office.php @@ -1,36 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tor Lillqvist <tml@collabora.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; +use OCP\ITempManager; +use OCP\Server; use Psr\Log\LoggerInterface; abstract class Office extends ProviderV2 { @@ -49,51 +30,67 @@ abstract class Office extends ProviderV2 { return null; } - $absPath = $this->getLocalFile($file); - - $tmpDir = \OC::$server->getTempManager()->getTempBaseDir(); + $tempManager = Server::get(ITempManager::class); - $defaultParameters = ' -env:UserInstallation=file://' . escapeshellarg($tmpDir . '/owncloud-' . \OC_Util::getInstanceId() . '/') . ' --headless --nologo --nofirststartwizard --invisible --norestore --convert-to png --outdir '; - $clParameters = \OC::$server->getConfig()->getSystemValue('preview_office_cl_parameters', $defaultParameters); + // The file to generate the preview for. + $absPath = $this->getLocalFile($file); + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } - $cmd = $this->options['officeBinary'] . $clParameters . escapeshellarg($tmpDir) . ' ' . escapeshellarg($absPath); + // The destination for the LibreOffice user profile. + // LibreOffice can rune once per user profile and therefore instance id and file id are included. + $profile = $tempManager->getTemporaryFolder( + 'nextcloud-office-profile-' . \OC_Util::getInstanceId() . '-' . $file->getId() + ); - exec($cmd, $output, $returnCode); + // The destination for the LibreOffice convert result. + $outdir = $tempManager->getTemporaryFolder( + 'nextcloud-office-preview-' . \OC_Util::getInstanceId() . '-' . $file->getId() + ); - if ($returnCode !== 0) { + if ($profile === false || $outdir === false) { $this->cleanTmpFiles(); return null; } - //create imagick object from png - $pngPreview = null; - try { - [$dirname, , , $filename] = array_values(pathinfo($absPath)); - $pngPreview = $tmpDir . '/' . $filename . '.png'; + $parameters = [ + $this->options['officeBinary'], + '-env:UserInstallation=file://' . escapeshellarg($profile), + '--headless', + '--nologo', + '--nofirststartwizard', + '--invisible', + '--norestore', + '--convert-to png', + '--outdir ' . escapeshellarg($outdir), + escapeshellarg($absPath), + ]; + + $cmd = implode(' ', $parameters); + exec($cmd, $output, $returnCode); - $png = new \imagick($pngPreview . '[0]'); - $png->setImageFormat('jpg'); - } catch (\Exception $e) { + if ($returnCode !== 0) { $this->cleanTmpFiles(); - unlink($pngPreview); - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [ - 'exception' => $e, - 'app' => 'core', - ]); return null; } - $image = new \OC_Image(); - $image->loadFromData((string) $png); + $preview = $outdir . pathinfo($absPath, PATHINFO_FILENAME) . '.png'; + + $image = new \OCP\Image(); + $image->loadFromFile($preview); $this->cleanTmpFiles(); - unlink($pngPreview); if ($image->valid()) { $image->scaleDownToFit($maxX, $maxY); - return $image; } + return null; } } diff --git a/lib/private/Preview/OpenDocument.php b/lib/private/Preview/OpenDocument.php index 5f27e325d31..f590eb6a59c 100644 --- a/lib/private/Preview/OpenDocument.php +++ b/lib/private/Preview/OpenDocument.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/PDF.php b/lib/private/Preview/PDF.php index 405fd1545f9..9de14685925 100644 --- a/lib/private/Preview/PDF.php +++ b/lib/private/Preview/PDF.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -31,4 +15,11 @@ class PDF extends Bitmap { public function getMimeType(): string { return '/application\/pdf/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/pdf/'; + } } diff --git a/lib/private/Preview/PNG.php b/lib/private/Preview/PNG.php index 5a3f2136575..49d0cd059ba 100644 --- a/lib/private/Preview/PNG.php +++ b/lib/private/Preview/PNG.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/Photoshop.php b/lib/private/Preview/Photoshop.php index 9d79abf8191..b7209120530 100644 --- a/lib/private/Preview/Photoshop.php +++ b/lib/private/Preview/Photoshop.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -31,4 +15,11 @@ class Photoshop extends Bitmap { public function getMimeType(): string { return '/application\/x-photoshop/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/(application|image)\/(x-photoshop|x-psd)/'; + } } diff --git a/lib/private/Preview/Postscript.php b/lib/private/Preview/Postscript.php index 06ea7fc8037..04c667926aa 100644 --- a/lib/private/Preview/Postscript.php +++ b/lib/private/Preview/Postscript.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -31,4 +15,11 @@ class Postscript extends Bitmap { public function getMimeType(): string { return '/application\/postscript/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/postscript/'; + } } diff --git a/lib/private/Preview/Provider.php b/lib/private/Preview/Provider.php index e87aded4786..26f0ac09f08 100644 --- a/lib/private/Preview/Provider.php +++ b/lib/private/Preview/Provider.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/ProviderV1Adapter.php b/lib/private/Preview/ProviderV1Adapter.php index 22db380d30d..ba8826ef765 100644 --- a/lib/private/Preview/ProviderV1Adapter.php +++ b/lib/private/Preview/ProviderV1Adapter.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; @@ -55,7 +37,7 @@ class ProviderV1Adapter implements IProviderV2 { } private function getViewAndPath(File $file) { - $view = new View($file->getParent()->getPath()); + $view = new View(dirname($file->getPath())); $path = $file->getName(); return [$view, $path]; diff --git a/lib/private/Preview/ProviderV2.php b/lib/private/Preview/ProviderV2.php index 0cb7eb59e21..556d1099d2d 100644 --- a/lib/private/Preview/ProviderV2.php +++ b/lib/private/Preview/ProviderV2.php @@ -3,47 +3,26 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; +use OCP\ITempManager; use OCP\Preview\IProviderV2; +use OCP\Server; +use Psr\Log\LoggerInterface; abstract class ProviderV2 implements IProviderV2 { - /** @var array */ - protected $options; - - /** @var array */ - protected $tmpFiles = []; + protected array $tmpFiles = []; - /** - * Constructor - * - * @param array $options - */ - public function __construct(array $options = []) { - $this->options = $options; + public function __construct( + protected array $options = [], + ) { } /** @@ -67,7 +46,7 @@ abstract class ProviderV2 implements IProviderV2 { * @param File $file * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the shape of the image * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the shape of the image - * @return null|\OCP\IImage false if no preview was generated + * @return null|\OCP\IImage null if no preview was generated * @since 17.0.0 */ abstract public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage; @@ -80,14 +59,24 @@ abstract class ProviderV2 implements IProviderV2 { * Get a path to either the local file or temporary file * * @param File $file - * @param int $maxSize maximum size for temporary files - * @return string|false + * @param ?int $maxSize maximum size for temporary files */ - protected function getLocalFile(File $file, int $maxSize = null) { + protected function getLocalFile(File $file, ?int $maxSize = null): string|false { if ($this->useTempFile($file)) { - $absPath = \OC::$server->getTempManager()->getTemporaryFile(); + $absPath = Server::get(ITempManager::class)->getTemporaryFile(); + + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return false; + } $content = $file->fopen('r'); + if ($content === false) { + return false; + } if ($maxSize) { $content = stream_get_contents($content, $maxSize); diff --git a/lib/private/Preview/SGI.php b/lib/private/Preview/SGI.php index 9f392d2252f..78b1ea5828a 100644 --- a/lib/private/Preview/SGI.php +++ b/lib/private/Preview/SGI.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; @@ -28,6 +12,13 @@ class SGI extends Bitmap { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/sgi/'; + return '/image\/(x-)?sgi/'; + } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/(x-)?sgi/'; } } diff --git a/lib/private/Preview/SVG.php b/lib/private/Preview/SVG.php index a4ce4899c43..d9f7701f411 100644 --- a/lib/private/Preview/SVG.php +++ b/lib/private/Preview/SVG.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -44,32 +24,41 @@ class SVG extends ProviderV2 { */ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { try { - $svg = new \Imagick(); - $svg->setBackgroundColor(new \ImagickPixel('transparent')); - $content = stream_get_contents($file->fopen('r')); if (substr($content, 0, 5) !== '<?xml') { $content = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . $content; } // Do not parse SVG files with references - if (stripos($content, 'xlink:href') !== false) { + if (preg_match('/["\s](xlink:)?href\s*=/i', $content)) { return null; } + $svg = new \Imagick(); + + $svg->pingImageBlob($content); + $mimeType = $svg->getImageMimeType(); + if (!preg_match($this->getMimeType(), $mimeType)) { + throw new \Exception('File mime type does not match the preview provider: ' . $mimeType); + } + + $svg->setBackgroundColor(new \ImagickPixel('transparent')); $svg->readImageBlob($content); $svg->setImageFormat('png32'); } catch (\Exception $e) { - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), [ - 'exception' => $e, - 'app' => 'core', - ]); + \OC::$server->get(LoggerInterface::class)->error( + 'File: ' . $file->getPath() . ' Imagick says:', + [ + 'exception' => $e, + 'app' => 'core', + ] + ); return null; } //new image object - $image = new \OC_Image(); - $image->loadFromData((string) $svg); + $image = new \OCP\Image(); + $image->loadFromData((string)$svg); //check if image object is valid if ($image->valid()) { $image->scaleDownToFit($maxX, $maxY); diff --git a/lib/private/Preview/StarOffice.php b/lib/private/Preview/StarOffice.php index 58f210d2ff6..9ea540dc912 100644 --- a/lib/private/Preview/StarOffice.php +++ b/lib/private/Preview/StarOffice.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; diff --git a/lib/private/Preview/Storage/Root.php b/lib/private/Preview/Storage/Root.php index c4191228ec7..41378653962 100644 --- a/lib/private/Preview/Storage/Root.php +++ b/lib/private/Preview/Storage/Root.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview\Storage; diff --git a/lib/private/Preview/TGA.php b/lib/private/Preview/TGA.php index cb591be2231..675907b4e49 100644 --- a/lib/private/Preview/TGA.php +++ b/lib/private/Preview/TGA.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; @@ -28,6 +12,13 @@ class TGA extends Bitmap { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/t(ar)?ga/'; + return '/image\/(x-)?t(ar)?ga/'; + } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/(x-)?t(ar)?ga/'; } } diff --git a/lib/private/Preview/TIFF.php b/lib/private/Preview/TIFF.php index 1a5d957d3ee..cd81e611d0b 100644 --- a/lib/private/Preview/TIFF.php +++ b/lib/private/Preview/TIFF.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -31,4 +15,11 @@ class TIFF extends Bitmap { public function getMimeType(): string { return '/image\/tiff/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/tiff/'; + } } diff --git a/lib/private/Preview/TXT.php b/lib/private/Preview/TXT.php index bb3af6b90de..1a1d64f3e08 100644 --- a/lib/private/Preview/TXT.php +++ b/lib/private/Preview/TXT.php @@ -1,31 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Nmz <nemesiz@nmz.lt> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; @@ -72,7 +50,7 @@ class TXT extends ProviderV2 { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / 32) * $maxX) : 5; //5px + $fontSize = $maxX ? (int)((1 / 32) * $maxX) : 5; //5px $lineSize = ceil($fontSize * 1.5); $image = imagecreate($maxX, $maxY); @@ -89,7 +67,7 @@ class TXT extends ProviderV2 { $index = $index + 1; $x = 1; - $y = (int) ($index * $lineSize); + $y = (int)($index * $lineSize); if ($canUseTTF === true) { imagettftext($image, $fontSize, 0, $x, $y, $textColor, $fontFile, $line); @@ -103,7 +81,7 @@ class TXT extends ProviderV2 { } } - $imageObject = new \OC_Image(); + $imageObject = new \OCP\Image(); $imageObject->setResource($image); return $imageObject->valid() ? $imageObject : null; diff --git a/lib/private/Preview/Watcher.php b/lib/private/Preview/Watcher.php index 7f4593f9fe3..21f040d8342 100644 --- a/lib/private/Preview/Watcher.php +++ b/lib/private/Preview/Watcher.php @@ -3,28 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; +use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\IAppData; use OCP\Files\Node; @@ -54,13 +38,16 @@ class Watcher { $this->deleteNode($node); } - protected function deleteNode(Node $node) { + protected function deleteNode(FileInfo $node) { // We only handle files if ($node instanceof Folder) { return; } try { + if (is_null($node->getId())) { + return; + } $folder = $this->appData->getFolder((string)$node->getId()); $folder->delete(); } catch (NotFoundException $e) { diff --git a/lib/private/Preview/WatcherConnector.php b/lib/private/Preview/WatcherConnector.php index 4d038c9a2b7..c34dd1dde4d 100644 --- a/lib/private/Preview/WatcherConnector.php +++ b/lib/private/Preview/WatcherConnector.php @@ -3,67 +3,39 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; use OC\SystemConfig; +use OCA\Files_Versions\Events\VersionRestoredEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\Files\Node; class WatcherConnector { - - /** @var IRootFolder */ - private $root; - - /** @var SystemConfig */ - private $config; - - /** - * WatcherConnector constructor. - * - * @param IRootFolder $root - * @param SystemConfig $config - */ - public function __construct(IRootFolder $root, - SystemConfig $config) { - $this->root = $root; - $this->config = $config; + public function __construct( + private IRootFolder $root, + private SystemConfig $config, + private IEventDispatcher $dispatcher, + ) { } - /** - * @return Watcher - */ private function getWatcher(): Watcher { - return \OC::$server->query(Watcher::class); + return \OCP\Server::get(Watcher::class); } - public function connectWatcher() { + public function connectWatcher(): void { // Do not connect if we are not setup yet! if ($this->config->getValue('instanceid', null) !== null) { $this->root->listen('\OC\Files', 'postWrite', function (Node $node) { $this->getWatcher()->postWrite($node); }); - \OC_Hook::connect('\OCP\Versions', 'rollback', $this->getWatcher(), 'versionRollback'); + $this->dispatcher->addListener(VersionRestoredEvent::class, function (VersionRestoredEvent $event) { + $this->getWatcher()->versionRollback(['node' => $event->getVersion()->getSourceFile()]); + }); } } } diff --git a/lib/private/Preview/WebP.php b/lib/private/Preview/WebP.php index c8f8d11c393..25b922e9190 100644 --- a/lib/private/Preview/WebP.php +++ b/lib/private/Preview/WebP.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Preview; diff --git a/lib/private/Preview/XBitmap.php b/lib/private/Preview/XBitmap.php index e0adb48b881..c8337cc252d 100644 --- a/lib/private/Preview/XBitmap.php +++ b/lib/private/Preview/XBitmap.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Preview; |