From 0f909c6f5686a649da7473eeb41b3a9198939125 Mon Sep 17 00:00:00 2001 From: Bernhard Rusch Date: Tue, 12 Mar 2024 15:08:58 +0100 Subject: [PATCH] Added Gotenberg support for creating page and version previews (#16608) * Added Gotenberg support for creating page and version previews * Update lib/Image/HtmlToImage.php Co-authored-by: JiaJia Ji * Update lib/Image/HtmlToImage.php Co-authored-by: JiaJia Ji * Making Stan happy * added upgrade notes * docs * Update composer.json Co-authored-by: JiaJia Ji * deprected Chromium support * Update lib/Image/HtmlToImage.php Co-authored-by: JiaJia Ji * Added compatibility layer and deprecation notice * make PhpStan happy --------- Co-authored-by: JiaJia Ji --- .../src/DependencyInjection/Configuration.php | 4 + composer.json | 6 +- .../06_Additional_Tools_Installation.md | 36 +-- .../09_Upgrade_Notes/README.md | 2 + lib/Document/Adapter/Gotenberg.php | 25 +-- lib/Helper/GotenbergHelper.php | 57 +++++ lib/Image/Chromium.php | 104 +-------- lib/Image/HtmlToImage.php | 205 ++++++++++++++++++ lib/Tool/Requirements.php | 26 ++- models/Document/Service.php | 4 +- phpstan-baseline.neon | 4 + 11 files changed, 325 insertions(+), 148 deletions(-) create mode 100644 lib/Helper/GotenbergHelper.php create mode 100644 lib/Image/HtmlToImage.php diff --git a/bundles/CoreBundle/src/DependencyInjection/Configuration.php b/bundles/CoreBundle/src/DependencyInjection/Configuration.php index 10f1b7ff1c6..507c5ce3a90 100644 --- a/bundles/CoreBundle/src/DependencyInjection/Configuration.php +++ b/bundles/CoreBundle/src/DependencyInjection/Configuration.php @@ -1971,11 +1971,15 @@ private function addGotenbergNode(ArrayNodeDefinition $rootNode): void ->end(); } + /** + * @deprecated + */ private function addChromiumNode(ArrayNodeDefinition $rootNode): void { $rootNode ->children() ->arrayNode('chromium') + ->setDeprecated('pimcore/pimcore', '11.2', 'Chromium service is deprecated and will be removed in Pimcore 12. Use Gotenberg instead.') ->addDefaultsIfNotSet() ->children() ->scalarNode('uri') diff --git a/composer.json b/composer.json index 0d34d2993f0..31baf5b5470 100644 --- a/composer.json +++ b/composer.json @@ -141,7 +141,7 @@ "phpstan/phpstan": "1.10.59", "phpstan/phpstan-symfony": "^1.3.5", "phpunit/phpunit": "^9.3", - "gotenberg/gotenberg-php": "^1.1", + "gotenberg/gotenberg-php": "^1.1 || ^2.0", "composer/composer": "*", "chrome-php/chrome": "^1.4.0", "webmozarts/console-parallelization": "^2.1", @@ -152,9 +152,9 @@ "ext-sockets": "*", "ext-imagick": "^3.4.0", "ext-redis": "*", - "gotenberg/gotenberg-php": "^1.1 - Required for generating pdf via Gotenberg in assets preview (LibreOffice), page preview, version diff and web2print", + "gotenberg/gotenberg-php": "^2.0 - Required for generating pdf via Gotenberg in assets preview (LibreOffice), page preview, version diff and web2print", "elasticsearch/elasticsearch": "Required for Elastic Search service", - "chrome-php/chrome": "Required for Documents Page Previews", + "chrome-php/chrome": "Optional for Documents Page Previews when requiring gotenberg-php v2, but required if you opt for gotenberg-php v1", "webmozarts/console-parallelization": "Required for parallelization of console commands", "symfony/dotenv": "Required for loading environment vars from .env files", "pimcore/admin-ui-classic-bundle": "^1.4.0" diff --git a/doc/23_Installation_and_Upgrade/03_System_Setup_and_Hosting/06_Additional_Tools_Installation.md b/doc/23_Installation_and_Upgrade/03_System_Setup_and_Hosting/06_Additional_Tools_Installation.md index 40446612d23..ce46b65cb47 100644 --- a/doc/23_Installation_and_Upgrade/03_System_Setup_and_Hosting/06_Additional_Tools_Installation.md +++ b/doc/23_Installation_and_Upgrade/03_System_Setup_and_Hosting/06_Additional_Tools_Installation.md @@ -31,8 +31,26 @@ It's possible to either choose to install LibreOffice/Chromium or to use them vi apt-get install libreoffice libreoffice-script-provider-python libreoffice-math xfonts-75dpi poppler-utils inkscape libxrender1 libfontconfig1 ghostscript ``` -### Chromium (Chrome Headless) +### Gotenberg + +To install it, please add it in your Docker Compose services stack as [https://gotenberg.dev/docs/getting-started/installation#docker-compose](https://gotenberg.dev/docs/getting-started/installation#docker-compose). + +Configure the Docker services accordingly: + +- `pimcore.gotenberg.base_url` which by default to `http://gotenberg:3000` +- `pimcore.documents.preview_url_prefix` for example to `nginx:80` + +Make sure to add and install the required library via composer: +```bash +composer require gotenberg/gotenberg-php ^2.0 +``` + + +### Chromium (Chrome Headless) - deprecated +> Chromium is used to generate previews of document pages. +> This functionality is now also provided by Gotenberg, therefore Chromium support has been deprecated in favour of Gotenberg. + First of all, you need to add and install the required library via composer: ```bash composer require chrome-php/chrome @@ -51,23 +69,9 @@ Add a new service as: image: browserless/chrome ``` and set accordingly: -- config `pimcore.chromium.uri` value (e.g. `ws://chrome:3000/`) +- config `pimcore.chromium.uri` value (e.g. `ws://chrome:3000/`) - web2print settings hostUrl as the Docker web server service (e.g. `http://nginx:80`) -### Gotenberg - -To install it, please add it in your Docker Compose services stack as [https://gotenberg.dev/docs/getting-started/installation#docker-compose](https://gotenberg.dev/docs/getting-started/installation#docker-compose). - -Configure the Docker services accordingly: - -- `pimcore.gotenberg.base_url` which by default to `http://gotenberg:3000` -- `pimcore.documents.preview_url_prefix` for example to `nginx:80` - -Make sure to add and install the required library via composer: -```bash -composer require gotenberg/gotenberg-php ^1.1 -``` - ## Image Optimizers ### JPEGOptim diff --git a/doc/23_Installation_and_Upgrade/09_Upgrade_Notes/README.md b/doc/23_Installation_and_Upgrade/09_Upgrade_Notes/README.md index 6f4a7aba1fb..8d0b1d1354e 100644 --- a/doc/23_Installation_and_Upgrade/09_Upgrade_Notes/README.md +++ b/doc/23_Installation_and_Upgrade/09_Upgrade_Notes/README.md @@ -5,6 +5,8 @@ #### [Documents]: - Using `outputFormat` config for `Pimcore\Model\Document\Editable\Date` editable is deprecated, use `outputIsoFormat` config instead. - Service `Pimcore\Document\Renderer\DocumentRenderer` is deprecated, use `Pimcore\Document\Renderer\DocumentRendererInterface` instead. +- Page previews and version comparisons can now be rendered using Gotenberg v8. + To replace Headless Chrome, upgrade to Gotenberg v8 and upgrade the client library: `composer require gotenberg/gotenberg-php:^2` #### [Data Objects]: - Methods `getAsIntegerCast()` and `getAsFloatCast()` of the `Pimcore\Model\DataObject\Data` class are deprecated now. - `MultiSelectOptionsProviderInterface` is deprecated, please use `SelectOptionsProviderInterface` instead. diff --git a/lib/Document/Adapter/Gotenberg.php b/lib/Document/Adapter/Gotenberg.php index eb4c16aa664..8c672579fde 100644 --- a/lib/Document/Adapter/Gotenberg.php +++ b/lib/Document/Adapter/Gotenberg.php @@ -16,10 +16,10 @@ namespace Pimcore\Document\Adapter; -use Gotenberg\Exceptions\GotenbergApiErroed; use Gotenberg\Gotenberg as GotenbergAPI; use Gotenberg\Stream; use Pimcore\Config; +use Pimcore\Helper\GotenbergHelper; use Pimcore\Logger; use Pimcore\Model\Asset; use Pimcore\Tool\Storage; @@ -29,8 +29,6 @@ */ class Gotenberg extends Ghostscript { - protected static bool $validPing = false; - public function isAvailable(): bool { try { @@ -61,24 +59,7 @@ public function isFileTypeSupported(string $fileType): bool */ public static function checkGotenberg(): bool { - if (self::$validPing) { - return true; - } - - if (!class_exists(GotenbergAPI::class, true)) { - return false; - } - $request = GotenbergAPI::chromium(Config::getSystemConfiguration('gotenberg')['base_url']) - ->html(Stream::string('dummy.html', '')); - - try { - GotenbergAPI::send($request); - self::$validPing = true; - - return true; - } catch (GotenbergApiErroed $e) { - return false; - } + return GotenbergHelper::isAvailable(); } public function load(Asset\Document $asset): static @@ -151,7 +132,7 @@ public function getPdf(?Asset\Document $asset = null) rewind($stream); return $stream; - } catch (GotenbergApiErroed $e) { + } catch (\Exception $e) { $message = "Couldn't convert document to PDF: " . $asset->getRealFullPath() . ' with Gotenberg: '; Logger::error($message. $e->getMessage()); diff --git a/lib/Helper/GotenbergHelper.php b/lib/Helper/GotenbergHelper.php new file mode 100644 index 00000000000..d6b4796b085 --- /dev/null +++ b/lib/Helper/GotenbergHelper.php @@ -0,0 +1,57 @@ +html(Stream::string('dummy.html', '')); + } elseif(method_exists($chrome, 'screenshot')) { + $request = $chrome->screenshot()->html(Stream::string('dummy.html', '')); + } + + if($request) { + try { + GotenbergAPI::send($request); + self::$validPing = true; + + return true; + } catch (\Exception $e) { + // nothing to do + } + } + + return false; + } +} diff --git a/lib/Image/Chromium.php b/lib/Image/Chromium.php index eb547671bda..a9b218b64ac 100644 --- a/lib/Image/Chromium.php +++ b/lib/Image/Chromium.php @@ -16,105 +16,21 @@ namespace Pimcore\Image; -use HeadlessChromium\BrowserFactory; -use HeadlessChromium\Communication\Connection; -use HeadlessChromium\Communication\Message; -use Pimcore\Logger; -use Pimcore\Tool\Console; +use function class_alias; +use function class_exists; +use function trigger_deprecation; -/** - * @internal - */ -class Chromium -{ - public static function isSupported(): bool - { - if (!class_exists(BrowserFactory::class)) { - return false; - } - - $chromiumUri = \Pimcore\Config::getSystemConfiguration('chromium')['uri']; - if (!empty($chromiumUri)) { - try { - return (new Connection($chromiumUri))->connect(); - } catch (\Exception $e) { - Logger::debug((string) $e); - - return false; - } - } +trigger_deprecation('pimcore/pimcore', '11.2', 'The "%s" class is deprecated, use "%s" instead.', Chromium::class, HtmlToImage::class); - return (bool) self::getChromiumBinary(); - } - - public static function getChromiumBinary(): ?string - { - foreach (['chromium', 'chrome'] as $app) { - $chromium = Console::getExecutable($app); - if ($chromium) { - return $chromium; - } - } - - return null; - } +if (!class_exists(Chromium::class, false)) { + class_alias(HtmlToImage::class, Chromium::class); +} +if (false) { /** - * @throws \Exception + * @deprecated since Pimcore 11.2, use HtmlToImage instead */ - public static function convert(string $url, string $outputFile, ?string $sessionName = null, ?string $sessionId = null, string $windowSize = '1280,1024'): bool + class Chromium extends HtmlToImage { - $chromiumUri = \Pimcore\Config::getSystemConfiguration('chromium')['uri']; - if (!empty($chromiumUri)) { - try { - $browser = BrowserFactory::connectToBrowser($chromiumUri); - } catch (\Exception $e) { - Logger::debug((string) $e); - - return false; - } - } else { - $binary = self::getChromiumBinary(); - if (!$binary) { - return false; - } - $browserFactory = new BrowserFactory($binary); - $browser = $browserFactory->createBrowser([ - 'noSandbox' => file_exists('/.dockerenv'), - 'startupTimeout' => 120, - 'windowSize' => explode(',', $windowSize), - ]); - } - - try { - $headers = []; - if (null !== $sessionId && null !== $sessionName) { - $headers['Cookie'] = $sessionName . '=' . $sessionId; - } - - $page = $browser->createPage(); - - if (!empty($headers)) { - $page->getSession()->sendMessageSync(new Message( - 'Network.setExtraHTTPHeaders', - ['headers' => $headers] - )); - } - - $page->navigate($url)->waitForNavigation(); - - $page->screenshot([ - 'captureBeyondViewport' => true, - 'clip' => $page->getFullPageClip(), - ])->saveToFile($outputFile); - } catch (\Throwable $e) { - Logger::debug('Could not create image from url ' . $url . ': ' . $e); - - return false; - } finally { - $browser->close(); - } - - return true; } } diff --git a/lib/Image/HtmlToImage.php b/lib/Image/HtmlToImage.php new file mode 100644 index 00000000000..a4fd9cbd2b9 --- /dev/null +++ b/lib/Image/HtmlToImage.php @@ -0,0 +1,205 @@ +connect()) { + self::$supportedAdapter = 'chromium'; + } + } catch (\Exception $e) { + Logger::debug((string) $e); + // nothing to do + } + } + + if(self::getChromiumBinary()) { + self::$supportedAdapter = 'chromium'; + } + } + + return self::$supportedAdapter; + } + + public static function getChromiumBinary(): ?string + { + foreach (['chromium', 'chrome'] as $app) { + $chromium = Console::getExecutable($app); + if ($chromium) { + return $chromium; + } + } + + return null; + } + + /** + * @throws \Exception + */ + public static function convert(string $url, string $outputFile, ?string $sessionName = null, ?string $sessionId = null, string $windowSize = '1280,1024'): bool + { + $adapter = self::getSupportedAdapter(); + if($adapter === 'gotenberg') { + return self::convertGotenberg(...func_get_args()); + } elseif ($adapter === 'chromium') { + return self::convertChromium(...func_get_args()); + } + + return false; + } + + /** + * @throws \Exception + */ + public static function convertGotenberg(string $url, string $outputFile, ?string $sessionName = null, ?string $sessionId = null, string $windowSize = '1280,1024'): bool + { + try { + + $extraHeaders = [ + 'X-Foo' => 'Bar' // required, as extraHttpHeaders() requires at least one entry + ]; + + if (null !== $sessionId && null !== $sessionName) { + $extraHeaders['Cookie'] = $sessionName . '=' . $sessionId; + } + + /** @var GotenbergAPI|object $request */ + $request = GotenbergAPI::chromium(Config::getSystemConfiguration('gotenberg')['base_url']); + if(method_exists($request, 'screenshot')) { + $urlResponse = $request->screenshot() + ->png() + ->extraHttpHeaders($extraHeaders) + ->url($url); + + $file = GotenbergAPI::save($urlResponse, PIMCORE_SYSTEM_TEMP_DIRECTORY); + return rename(PIMCORE_SYSTEM_TEMP_DIRECTORY . '/' . $file, $outputFile); + } + + } catch (\Exception $e) { + // nothing to do + } + + return false; + } + + /** + * @throws \Exception + */ + public static function convertChromium(string $url, string $outputFile, ?string $sessionName = null, ?string $sessionId = null, string $windowSize = '1280,1024'): bool + { + trigger_deprecation('pimcore/pimcore', '11.2.0', 'Chromium service is deprecated and will be removed in Pimcore 12. Use Gotenberg instead.'); + + $chromiumUri = \Pimcore\Config::getSystemConfiguration('chromium')['uri']; + if (!empty($chromiumUri)) { + try { + $browser = BrowserFactory::connectToBrowser($chromiumUri); + } catch (\Exception $e) { + Logger::debug((string) $e); + + return false; + } + } else { + $binary = self::getChromiumBinary(); + if (!$binary) { + return false; + } + $browserFactory = new BrowserFactory($binary); + $browser = $browserFactory->createBrowser([ + 'noSandbox' => file_exists('/.dockerenv'), + 'startupTimeout' => 120, + 'windowSize' => explode(',', $windowSize), + ]); + } + + $headers = []; + if (null !== $sessionId && null !== $sessionName) { + $headers['Cookie'] = $sessionName . '=' . $sessionId; + } + + $page = $browser->createPage(); + + try { + + if (!empty($headers)) { + $page->getSession()->sendMessageSync(new Message( + 'Network.setExtraHTTPHeaders', + ['headers' => $headers] + )); + } + + $page->navigate($url)->waitForNavigation(); + + $page->screenshot([ + 'captureBeyondViewport' => true, + 'clip' => $page->getFullPageClip(), + ])->saveToFile($outputFile); + } catch (\Throwable $e) { + Logger::debug('Could not create image from url ' . $url . ': ' . $e); + + return false; + } finally { + $page->close(); + } + + return true; + } +} diff --git a/lib/Tool/Requirements.php b/lib/Tool/Requirements.php index f9c866529f3..da97b50742f 100644 --- a/lib/Tool/Requirements.php +++ b/lib/Tool/Requirements.php @@ -17,6 +17,7 @@ namespace Pimcore\Tool; use Doctrine\DBAL\Connection; +use Pimcore\Helper\GotenbergHelper; use Pimcore\Image; use Pimcore\Tool\Requirements\Check; use Symfony\Component\Filesystem\Filesystem; @@ -383,16 +384,16 @@ public static function checkExternalApplications(): array 'state' => $ffmpegBin ? Check::STATE_OK : Check::STATE_WARNING, ]); - // Chromium BIN + // Chromium or Gotenberg try { - $chromiumBin = (bool) \Pimcore\Image\Chromium::getChromiumBinary(); + $htmlToImage = \Pimcore\Image\HtmlToImage::isSupported(); } catch (\Exception $e) { - $chromiumBin = false; + $htmlToImage = false; } $checks[] = new Check([ - 'name' => 'Chromium', - 'state' => $chromiumBin ? Check::STATE_OK : Check::STATE_WARNING, + 'name' => 'Gotenberg/Chromium', + 'state' => $htmlToImage ? Check::STATE_OK : Check::STATE_WARNING, ]); // ghostscript BIN @@ -408,15 +409,18 @@ public static function checkExternalApplications(): array ]); // LibreOffice BIN - try { - $libreofficeBin = (bool) \Pimcore\Document\Adapter\LibreOffice::getLibreOfficeCli(); - } catch (\Exception $e) { - $libreofficeBin = false; + $libreofficeGotenberg = GotenbergHelper::isAvailable(); + if(!$libreofficeGotenberg) { + try { + $libreofficeGotenberg = (bool)\Pimcore\Document\Adapter\LibreOffice::getLibreOfficeCli(); + } catch (\Exception $e) { + $libreofficeGotenberg = false; + } } $checks[] = new Check([ - 'name' => 'LibreOffice', - 'state' => $libreofficeBin ? Check::STATE_OK : Check::STATE_WARNING, + 'name' => 'Gotenberg / LibreOffice', + 'state' => $libreofficeGotenberg ? Check::STATE_OK : Check::STATE_WARNING, ]); // image optimizer diff --git a/models/Document/Service.php b/models/Document/Service.php index 274f54d93b2..b31129edc28 100644 --- a/models/Document/Service.php +++ b/models/Document/Service.php @@ -20,7 +20,7 @@ use Pimcore\Document\Renderer\DocumentRendererInterface; use Pimcore\Event\DocumentEvents; use Pimcore\Event\Model\DocumentEvent; -use Pimcore\Image\Chromium; +use Pimcore\Image\HtmlToImage; use Pimcore\Model; use Pimcore\Model\Document; use Pimcore\Model\Document\Editable\IdRewriterInterface; @@ -570,7 +570,7 @@ public static function generatePagePreview(int $id, Request $request = null, str $filesystem->mkdir(dirname($file), 0775); - if (Chromium::convert($url, $tmpFile)) { + if (HtmlToImage::convert($url, $tmpFile)) { $im = \Pimcore\Image::getInstance(); $im->load($tmpFile); $im->scaleByWidth(800); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3829721159c..4efb3bffbf2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -953,3 +953,7 @@ parameters: message: "#^File is missing a \"declare\\(strict_types\\=1\\)\" declaration\\.$#" count: 1 path: models/WebsiteSetting/Listing/Dao.php + - + message: "#^If condition is always false\\.$#" + count: 1 + path: lib/Image/Chromium.php \ No newline at end of file