From 2d2a2527ec548d7a3720138d8f278d652a44a00a Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Wed, 25 Feb 2026 00:04:58 +0600 Subject: [PATCH 1/2] fix(manager): restore template preview image when creating document - Add getPreviewPath() and getPreviewSourceId() to modElement so phpThumb receives a filesystem path and source id instead of a URL - Template/GetList now passes path and source to phpthumb.php; fixes blank preview when base_url is not / and when Flysystem received URLs - In modMediaSource::prepareSrcForThumb(), convert local URLs to paths before fileExists() so legacy callers passing URLs still work - In PhpThumb processor: guard pathinfo extension for PHP 8.1 (strtolower null deprecation) and remove error_reporting(E_ALL) so deprecations do not corrupt image output Fixes #16256 --- .../Processors/Element/Template/GetList.php | 14 ++-- .../Revolution/Processors/System/PhpThumb.php | 4 +- .../src/Revolution/Sources/modMediaSource.php | 25 +++++++ core/src/Revolution/modElement.php | 65 +++++++++++++++++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/core/src/Revolution/Processors/Element/Template/GetList.php b/core/src/Revolution/Processors/Element/Template/GetList.php index 3caceb38d7e..0744b658163 100644 --- a/core/src/Revolution/Processors/Element/Template/GetList.php +++ b/core/src/Revolution/Processors/Element/Template/GetList.php @@ -1,4 +1,5 @@ getPreviewUrl(); - - if (!empty($preview)) { + $preview = ''; + $previewPath = $object->getPreviewPath(); + if (!empty($previewPath)) { $imageQuery = http_build_query([ - 'src' => $preview, + 'src' => $previewPath, + 'source' => $object->getPreviewSourceId(), 'w' => 335, 'h' => 236, 'HTTP_MODAUTH' => $this->modx->user->getUserToken($this->modx->context->get('key')), 'zc' => 1 ]); - $preview = $this->modx->getOption('connectors_url', MODX_CONNECTORS_URL) . 'system/phpthumb.php?' . urldecode($imageQuery); + $connectorsUrl = $this->modx->getOption('connectors_url', MODX_CONNECTORS_URL); + $preview = $connectorsUrl . 'system/phpthumb.php?' . urldecode($imageQuery); } return [ diff --git a/core/src/Revolution/Processors/System/PhpThumb.php b/core/src/Revolution/Processors/System/PhpThumb.php index 0086f023b95..6beed200424 100644 --- a/core/src/Revolution/Processors/System/PhpThumb.php +++ b/core/src/Revolution/Processors/System/PhpThumb.php @@ -45,7 +45,6 @@ public function initialize() ]); $this->unsetProperty('wctx'); $this->unsetProperty('version'); - error_reporting(E_ALL); return true; } @@ -72,7 +71,8 @@ public function process() return ''; } - if (strtolower(pathinfo($src, PATHINFO_EXTENSION)) === 'svg') { + $ext = pathinfo($src, PATHINFO_EXTENSION); + if (is_string($ext) && strtolower($ext) === 'svg') { /* Skip thumbnail generation for svg and output the file directly */ header('Content-Type: image/svg+xml'); echo file_get_contents($src); diff --git a/core/src/Revolution/Sources/modMediaSource.php b/core/src/Revolution/Sources/modMediaSource.php index 05b1cc32f9b..d3b22d854df 100644 --- a/core/src/Revolution/Sources/modMediaSource.php +++ b/core/src/Revolution/Sources/modMediaSource.php @@ -1582,6 +1582,31 @@ public function setProperties($properties, $merge = false) */ public function prepareSrcForThumb($src) { + if (substr($src, 0, 4) === 'http') { + $baseUrl = rtrim($this->ctx->getOption('base_url', null, MODX_BASE_URL), '/'); + $basePath = $this->ctx->getOption('base_path', null, MODX_BASE_PATH); + $baseUrlPath = parse_url($baseUrl, PHP_URL_PATH); + $baseUrlPath = $baseUrlPath !== null ? rtrim($baseUrlPath, '/') : ''; + $parsed = parse_url($src); + if (isset($parsed['path'])) { + $path = $parsed['path']; + if ($baseUrlPath !== '' && str_starts_with($path, $baseUrlPath)) { + $path = substr($path, strlen($baseUrlPath)); + } + $path = ltrim($path, '/'); + $resolved = $basePath . $path; + if (file_exists($resolved)) { + $sourceBase = rtrim($this->getBasePath(), DIRECTORY_SEPARATOR); + $resolvedNorm = rtrim($resolved, DIRECTORY_SEPARATOR); + if ($sourceBase !== '' && str_starts_with($resolvedNorm, $sourceBase)) { + $src = ltrim(substr($resolvedNorm, strlen($sourceBase)), DIRECTORY_SEPARATOR); + } else { + $src = $resolved; + } + } + } + } + try { if (!$this->filesystem->fileExists($src)) { return ''; diff --git a/core/src/Revolution/modElement.php b/core/src/Revolution/modElement.php index 3df81a99042..238181efa70 100644 --- a/core/src/Revolution/modElement.php +++ b/core/src/Revolution/modElement.php @@ -1202,4 +1202,69 @@ public function getPreviewUrl() return ''; } + + /** + * Get the filesystem path for the preview file for use with phpThumb. + * + * @return string Path relative to media source basePath, or absolute path. + */ + public function getPreviewPath() + { + $previewfile = $this->get('preview_file'); + if (empty($previewfile)) { + return ''; + } + + if ($this->get('source') > 0) { + $source = $this->getOne('Source'); + if ($source && $source->get('is_stream')) { + $source->initialize(); + $basePath = $source->getBasePath(); + if ($basePath !== '' && file_exists($basePath . $previewfile)) { + return $previewfile; + } + } + } + + $basePath = $this->xpdo->getOption('base_path', null, MODX_BASE_PATH); + if (file_exists($basePath . $previewfile)) { + $assetsPrefix = 'assets' . DIRECTORY_SEPARATOR; + if (strpos($previewfile, $assetsPrefix) === 0) { + return substr($previewfile, strlen($assetsPrefix)); + } + return $basePath . $previewfile; + } + + if (file_exists($previewfile)) { + return $previewfile; + } + + return ''; + } + + /** + * Get the media source ID to use for the preview file with phpThumb. + * + * @return int + */ + public function getPreviewSourceId() + { + $previewfile = $this->get('preview_file'); + if (empty($previewfile)) { + return 1; + } + + if ($this->get('source') > 0) { + $source = $this->getOne('Source'); + if ($source && $source->get('is_stream')) { + $source->initialize(); + $basePath = $source->getBasePath(); + if ($basePath !== '' && file_exists($basePath . $previewfile)) { + return (int) $this->get('source'); + } + } + } + + return 1; + } } From c63be441800b37da708440d87a0affaafbf39e8e Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Thu, 26 Feb 2026 07:33:43 +0600 Subject: [PATCH 2/2] Refactor modElement preview handling - Rename getPreviewPath() to resolvePreviewSource() for clarity and encapsulate logic for resolving the preview file source. - Update getPreviewPath() to utilize the new method, returning an empty string if no preview file is set. - Modify getPreviewSourceId() to return the source ID from the resolved preview source. - Enhance documentation for clarity on return types and functionality. --- .../src/Revolution/Sources/modMediaSource.php | 4 ++ core/src/Revolution/modElement.php | 47 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/core/src/Revolution/Sources/modMediaSource.php b/core/src/Revolution/Sources/modMediaSource.php index d3b22d854df..028744561a5 100644 --- a/core/src/Revolution/Sources/modMediaSource.php +++ b/core/src/Revolution/Sources/modMediaSource.php @@ -1576,6 +1576,10 @@ public function setProperties($properties, $merge = false) /** * Prepare a src parameter to be rendered with phpThumb * + * Converts local HTTP URLs to filesystem paths for Flysystem compatibility. + * Note: file_exists() is called when resolving URLs; for large template lists + * prefer passing path + source (getPreviewPath/getPreviewSourceId) to avoid this. + * * @param string $src * * @return string diff --git a/core/src/Revolution/modElement.php b/core/src/Revolution/modElement.php index 238181efa70..55a17145066 100644 --- a/core/src/Revolution/modElement.php +++ b/core/src/Revolution/modElement.php @@ -1204,15 +1204,15 @@ public function getPreviewUrl() } /** - * Get the filesystem path for the preview file for use with phpThumb. + * Resolve preview file source: media source with file, or default. * - * @return string Path relative to media source basePath, or absolute path. + * @return array|null ['sourceId' => int, 'path' => string] when resolved, path non-empty if in media source; null when preview_file empty */ - public function getPreviewPath() + private function resolvePreviewSource(): ?array { $previewfile = $this->get('preview_file'); if (empty($previewfile)) { - return ''; + return null; } if ($this->get('source') > 0) { @@ -1221,11 +1221,30 @@ public function getPreviewPath() $source->initialize(); $basePath = $source->getBasePath(); if ($basePath !== '' && file_exists($basePath . $previewfile)) { - return $previewfile; + return ['sourceId' => (int) $this->get('source'), 'path' => $previewfile]; } } } + return ['sourceId' => 1, 'path' => '']; + } + + /** + * Get the filesystem path for the preview file for use with phpThumb. + * + * @return string Path relative to media source basePath, or absolute path. + */ + public function getPreviewPath() + { + $resolved = $this->resolvePreviewSource(); + if ($resolved === null) { + return ''; + } + if ($resolved['path'] !== '') { + return $resolved['path']; + } + + $previewfile = $this->get('preview_file'); $basePath = $this->xpdo->getOption('base_path', null, MODX_BASE_PATH); if (file_exists($basePath . $previewfile)) { $assetsPrefix = 'assets' . DIRECTORY_SEPARATOR; @@ -1249,22 +1268,10 @@ public function getPreviewPath() */ public function getPreviewSourceId() { - $previewfile = $this->get('preview_file'); - if (empty($previewfile)) { + $resolved = $this->resolvePreviewSource(); + if ($resolved === null) { return 1; } - - if ($this->get('source') > 0) { - $source = $this->getOne('Source'); - if ($source && $source->get('is_stream')) { - $source->initialize(); - $basePath = $source->getBasePath(); - if ($basePath !== '' && file_exists($basePath . $previewfile)) { - return (int) $this->get('source'); - } - } - } - - return 1; + return $resolved['sourceId']; } }