HEX
Server: Apache
System: Linux server1.ariadata.co 4.18.0-553.120.1.el8_10.x86_64 #1 SMP Mon Apr 20 18:04:27 EDT 2026 x86_64
User: nepi (1051)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system,show_source,popen,proc_open
Upload Files
File: /home/nepi/public_html/src/core/Directus/Filesystem/Thumbnailer.php
<?php

namespace Directus\Filesystem;

use function Directus\add_default_thumbnail_dimensions;
use function Directus\filename_put_ext;
use Directus\Util\ArrayUtils;
use Directus\Util\StringUtils;
use Intervention\Image\ImageManagerStatic as Image;
use Exception;
use Imagick;

class Thumbnailer
{

    /**
     * Thumbnail params extracted from url
     *
     * @var array
     */
    private $thumbnailParams = [];

    /**
     * Main Filesystem
     *
     * @var Filesystem
     */
    private $filesystem;

    /**
     * Main Filesystem
     *
     * @var Filesystem
     */
    private $filesystemThumb;

    /**
     * Thumbnailer config
     *
     * @var array
     */
    private $config = [];

    /**
     * Constructor
     *
     * @param Filesystem $main
     * @param Filesystem $thumb
     * @param array $config
     * @param string $path in the form '_/100/100/crop/good/path/to/some-image.jpg'
     *
     * @throws Exception
     */
    public function __construct(Filesystem $main, Filesystem $thumb, array $config, $path = '', array $params)
    {
        try {
            // $this->files = $files;
            $this->filesystem = $main;
            $this->filesystemThumb = $thumb;
            $this->config = $config;
            $this->params = $params;

            $this->thumbnailParams = $this->validateThumbnailParams($path, $params);

            // // check if the original file exists in storage
            if (!$this->filesystem->exists($this->fileName)) {
                throw new Exception($this->fileName . ' does not exist.'); // original file doesn't exist
            }

            $this->validateThumbnailWhitelist($params);

            $otherParams = $params;
            unset($otherParams['width'], $otherParams['height'], $otherParams['fit'], $otherParams['quality']);

            if (count($otherParams) > 0) {
                ksort($otherParams);
                $paramsString = '';
                foreach ($otherParams as $key => $value) {
                    $paramsString .= ',' . substr($key, 0, 1) . $value;
                }
            }

            $this->thumbnailDir = 'w' . $params['width'] . ',h' . $params['height'] . ',f' . $params['fit'] . ',q' . $params['quality'] . $paramsString;
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * Magic getter for thumbnailParams
     *
     * @param string $key
     * @return string|int
     */
    public function __get($key)
    {
        return ArrayUtils::get($this->thumbnailParams, $key, null);
    }

    /**
     * Return thumbnail as data
     *
     * @throws Exception
     * @return string|null
     */
    public function get()
    {
        try {
            if ($this->filesystemThumb->exists($this->thumbnailDir . '/' . $this->thumbnailFileName)) {
                $img = $this->filesystemThumb->read($this->thumbnailDir . '/' . $this->thumbnailFileName);
            }

            return isset($img) && $img ? $img : null;
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * Get thumbnail mime type
     *
     * @throws Exception
     * @return string
     */
    public function getThumbnailMimeType()
    {
        try {
            if ($this->filesystemThumb->exists($this->thumbnailDir . '/' . $this->thumbnailFileName)) {
                if (strtolower(pathinfo($this->thumbnailFileName, PATHINFO_EXTENSION)) === 'webp') {
                    return 'image/webp';
                }
                $img = Image::make($this->filesystemThumb->read($this->thumbnailDir . '/' . $this->thumbnailFileName));
                return $img->mime();
            }

            return 'application/octet-stream';
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * Replace PDF files with a JPG thumbnail
     * @throws Exception
     * @return string image content
     */
    public function load()
    {
        $content = $this->filesystem->read($this->fileName);
        $ext = pathinfo($this->fileName, PATHINFO_EXTENSION);
        if (Thumbnail::isNonImageFormatSupported($ext)) {
            $content = Thumbnail::createImageFromNonImage($content);
        }
        return Image::make($content);
    }

    /**
     * Create thumbnail from image and `contain`
     * http://image.intervention.io/api/resize
     * https://css-tricks.com/almanac/properties/o/object-fit/
     *
     * @throws Exception
     * @return string
     */
    public function contain()
    {
        try {
            // action options
            $options = $this->getSupportedActionOptions($this->$params);

            // open file image resource
            $img = $this->load();

            // crop image
            $img->resize($this->width, $this->height, function ($constraint) {
                $constraint->aspectRatio();
                if (ArrayUtils::get($options, 'preventUpsize')) {
                    $constraint->upsize();
                }
            });

            if (ArrayUtils::get($options, 'resizeCanvas')) {
                $img->resizeCanvas($this->width, $this->height, ArrayUtils::get($options, 'position', 'center'), ArrayUtils::get($options, 'resizeRelative', false), ArrayUtils::get($options, 'canvasBackground', [255, 255, 255, 0]));
            }

            $encodedImg = (string) $img->encode($this->format, ($this->quality ? $this->quality : null));
            $this->filesystemThumb->write($this->thumbnailDir . '/' . $this->thumbnailFileName, $encodedImg);

            return $encodedImg;
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * Create thumbnail from image and `crop`
     * http://image.intervention.io/api/fit
     * https://css-tricks.com/almanac/properties/o/object-fit/
     *
     * @throws Exception
     * @return string
     */
    public function crop()
    {
        try {

            // action options
            $options = $this->getSupportedActionOptions($this->action);

            // open file image resource
            $img = $this->load();

            // resize/crop image
            $img->fit($this->width, $this->height, function ($constraint) {
            }, ArrayUtils::get($options, 'position', 'center'));

            $encodedImg = (string) $img->encode($this->format, ($this->quality ? $this->quality : null));
            $this->filesystemThumb->write($this->thumbnailDir . '/' . $this->thumbnailFileName, $encodedImg);

            return $encodedImg;
        } catch (Exception $e) {
            throw $e;
        }
    }

    /**
     * validate params and file extensions and return it
     *
     * @param string $thumbnailUrlPath
     * @param array $params
     * @throws Exception
     * @return array
     */
    public function validateThumbnailParams($thumbnailUrlPath, $params)
    {
        // cache
        if ($this->thumbnailParams) {
            return $this->thumbnailParams;
        }

        // get URL parts
        // https://docs.directus.io/guides/thumbnailer.html#url-syntax
        $urlSegments = explode('/', $thumbnailUrlPath);

        // pull the env out of the segments
        array_shift($urlSegments);

        // set thumbnail parameters
        $thumbnailParams = [
            'fit' => filter_var($params['fit'], FILTER_SANITIZE_STRING),
            'height' => filter_var($params['height'], FILTER_SANITIZE_NUMBER_INT),
            'quality' => filter_var($params['quality'], FILTER_SANITIZE_STRING),
            'width' => filter_var($params['width'], FILTER_SANITIZE_NUMBER_INT),
        ];

        if ($thumbnailParams['width'] == null || $thumbnailParams['width'] == '') {
            throw new Exception('No width is provided.');
        }
        if ($thumbnailParams['height'] == null || $thumbnailParams['height'] == '') {
            throw new Exception('No height is provided.');
        }
        if ($thumbnailParams['fit'] == null || $thumbnailParams['fit'] == '') {
            throw new Exception('No fit is provided.');
        }
        if ($thumbnailParams['quality'] == null || $thumbnailParams['quality'] == '') {
            throw new Exception('No quality is provided.');
        }

        // make sure filename is valid
        $filename = $urlSegments[1];
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
        $name = pathinfo($filename, PATHINFO_FILENAME);
        if (!$this->isSupportedFileExtension($ext)) {
            throw new Exception('Invalid file extension.');
        }

        // Set thumbnail filename parameters
        $thumbnailParams['fileExt'] = $ext;
        $thumbnailParams['fileName'] = $filename;
        $format = strtolower(ArrayUtils::get($this->params, 'format') ?: $ext);

        if (!$this->isSupportedFileExtension($format)) {
            throw new Exception('Invalid file format.');
        }

        // Check format against image extension
        if (
            $format !== NULL &&
            strtolower($ext) !== $format &&
            !(
                ($format === 'jpeg' || $format === 'jpg') &&
                (strtolower($ext) === 'jpeg' || strtolower($ext) === 'jpg'))
        ) {
            $thumbnailParams['thumbnailFileName'] = $name . '.' . $format;
        } else {
            $thumbnailParams['thumbnailFileName'] = $filename;
        }

        return $thumbnailParams;
    }

    /**
     * validate params against thumbnail whitelist
     *
     * @param array $params
     * @throws Exception
     * @return boolean
     */

    public function validateThumbnailWhitelist($params)
    {
        $userWhitelist = json_decode(ArrayUtils::get($this->getConfig(), 'asset_whitelist'), true);

        $thumbnailWhitelistEnabled = empty($userWhitelist) === false;

        if ($thumbnailWhitelistEnabled) {
            if (is_string($userWhitelist)) {
                $userWhitelist = json_decode($userWhitelist, true);
            }
            $result = false;
            foreach ($userWhitelist as $key => $value) {
                if (
                    $value['width'] == $params['width'] &&
                    $value['height'] == $params['height'] &&
                    $value['fit'] == $params['fit'] &&
                    $value['quality'] == $params['quality']
                ) {
                    $result = true;
                }
            }
            if (!$result) {
                throw new Exception();
            }
        }
    }


    /**
     * Translate quality text to number and return
     *
     * @param string $qualityText
     * @return number
     */
    public function translateQuality($qualityText)
    {
        $quality = 60;
        if (!is_numeric($qualityText)) {
            $quality = ArrayUtils::get($this->getSupportedQualityTags(), $qualityText, $quality);
        }

        return $quality;
    }

    /**
     * Check if given file extension is supported
     *
     * @param int $ext
     * @return boolean
     */
    public function isSupportedFileExtension($ext)
    {
        return in_array(strtolower($ext), $this->getSupportedFileExtensions());
    }

    /**
     * Return supported image file types
     *
     * @return array
     */
    public function getSupportedFileExtensions()
    {
        return Thumbnail::getFormatsSupported();
    }

    /**
     * Check if given dimension is supported
     *
     * @param int $width
     * @param int $height
     * @return boolean
     */
    public function isSupportedThumbnailDimension($width, $height)
    {
        return in_array($width . 'x' . $height, $this->getSupportedThumbnailDimensions());
    }

    /**
     * Return supported thumbnail file dimesions
     *
     * @return array
     */
    public function getSupportedThumbnailDimensions()
    {
        $dimensions =  $this->parseCSV(
            ArrayUtils::get($this->getConfig(), 'thumbnail_dimensions')
        );

        add_default_thumbnail_dimensions($dimensions);

        return $dimensions;
    }

    /**
     * Check if given action is supported
     *
     * @param int $action
     * @return boolean
     */
    public function isSupportedAction($action)
    {
        return ArrayUtils::has($this->getSupportedActions(), $action);
    }

    /**
     * Return supported actions
     *
     * @return array
     */
    public function getSupportedActions()
    {
        return $this->getActions();
    }

    /**
     * Check if given quality is supported
     *
     * @param $qualityTag
     *
     * @return bool
     */
    public function isSupportedQualityTag($qualityTag)
    {
        return ArrayUtils::has($this->getSupportedQualityTags(), $qualityTag);
    }

    /**
     * Return supported thumbnail qualities
     *
     * @return array
     */
    public function getSupportedQualityTags()
    {
        $defaultQualityTags = [
            'poor' => 25,
            'good' => 50,
            'better' => 75,
            'best' => 100,
        ];

        $qualityTags =  ArrayUtils::get($this->getConfig(), 'thumbnail_quality_tags') ?: [];
        if (is_string($qualityTags)) {
            $qualityTags = json_decode($qualityTags, true);
        }

        return array_merge($defaultQualityTags, $qualityTags);
    }

    /**
     * Return supported action options as set in config
     *
     * @param string $action
     *
     * @return array
     */
    public function getSupportedActionOptions($action)
    {
        return ArrayUtils::get($this->getActions(), $action) ?: [];
    }

    /**
     * Returns a list of supported actions
     *
     * @return array
     */
    public function getActions()
    {
        $actions =  ArrayUtils::get($this->getConfig(), 'thumbnail_actions');
        if (is_string($actions) && !empty($actions)) {
            $actions = json_decode($actions, true);
        }

        return array_merge($this->getDefaultActions(), (array) $actions);
    }

    /**
     * Return a list of the default supported actions
     *
     * @return array
     */
    public function getDefaultActions()
    {
        return [
            'contain' => [
                'options' => [
                    'resizeCanvas' => false, // http://image.intervention.io/api/resizeCanvas
                    'position' => 'center',
                    'resizeRelative' => false,
                    'canvasBackground' => 'ccc', // http://image.intervention.io/getting_started/formats
                ]
            ],
            'crop' => [
                'options' => [
                    'position' => 'center', // http://image.intervention.io/api/fit
                ]
            ]
        ];
    }

    /**
     * Merge file and thumbnailer config settings and return
     *
     * @throws Exception
     * @return array
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Parse csv string to an array
     *
     * @param string $value
     *
     * @return array
     */
    protected function parseCSV($value)
    {
        if (is_string($value)) {
            $value = StringUtils::csv(
                $value
            );
        } else {
            $value = (array) $value;
        }

        return $value;
    }
}