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/Files.php
<?php

namespace Directus\Filesystem;

use Directus\Application\Application;
use Slim\Http\UploadedFile;
use function Directus\filename_put_ext;
use function Directus\generate_uuid4;
use function Directus\is_a_url;
use Directus\Util\ArrayUtils;
use Directus\Util\Formatting;
use Directus\Util\MimeTypeUtils;

class Files
{
    /**
     * @var array
     */
    private $config = [];

    /**
     * @var array
     */
    private $filesSettings = [];

    /**
     * @var Filesystem
     */
    private $filesystem = null;

    /**
     * @var array
     */
    private $defaults = [
        'description' => '',
        'tags' => '',
        'location' => '',
        'charset' => ''
    ];

    /**
     * Hook Emitter Instance
     *
     * @var \Directus\Hook\Emitter
     */
    protected $emitter;

    public function __construct($filesystem, $config, array $settings, $emitter)
    {
        $this->filesystem = $filesystem;
        $this->config = $config;
        $this->emitter = $emitter;
        $this->filesSettings = $settings;
    }

    // @TODO: remove exists() and rename() method
    // and move it to Directus\Filesystem Wrapper
    public function exists($path)
    {
        return $this->filesystem->getAdapter()->has($path);
    }

    public function rename($path, $newPath, $replace = false)
    {
        if ($replace === true && $this->filesystem->exists($newPath)) {
            $this->filesystem->getAdapter()->delete($newPath);
        }

        return $this->filesystem->getAdapter()->rename($path, $newPath);
    }

    public function delete($file)
    {
        if ($this->exists($file['filename_disk'])) {
            $this->emitter->run('file.delete', [$file]);
            $this->filesystem->getAdapter()->delete($file['filename_disk']);
            $this->emitter->run('file.delete:after', [$file]);
        }
    }

    public function deleteThumb($file)
    {
        $ignoreableFiles = ['.DS_Store', '..', '.'];
        $scannedDirectory = array_values(array_diff(scandir($this->filesystem->getPath()), $ignoreableFiles));

        foreach ($scannedDirectory as $directory) {
            $fileName = $directory . '/' . $file['filename_disk'];
            if ($this->exists($fileName)) {
                $this->filesystem->getAdapter()->delete($fileName);
            }
        }
    }

    /**
     * Get URL info
     *
     * @param string $url
     *
     * @return array
     */
    public function getLink($url)
    {
        // @TODO: use oEmbed
        // @TODO: better provider url validation
        // checking for 'youtube.com' for a valid youtube video is wrong
        // we can also be using youtube.com/img/a/youtube/image.jpg
        // which should fallback to ImageProvider
        // instead checking for a url with 'youtube.com/watch' with v param or youtu.be/
        $app = Application::getInstance();
        $embedManager = $app->getContainer()->get('embed_manager');
        try {
            $info = $embedManager->parse($url);
        } catch (\Exception $e) {
            $info = $this->getImageFromURL($url);
        }

        if ($info) {
            $info['storage'] = $this->getConfig('adapter');
            $info['charset'] = isset($info['charset']) ? $info['charset'] : '';
        }

        return $info;
    }

    /**
     * Gets the mime-type from the content type
     *
     * @param $contentType
     *
     * @return string
     */
    protected function getMimeTypeFromContentType($contentType)
    {
        // split the data type if it has charset or boundaries set
        // ex: image/jpg;charset=UTF8
        if (strpos($contentType, ';') !== false) {
            $contentType = array_map('trim', explode(';', $contentType));
        }

        if (is_array($contentType)) {
            $contentType = $contentType[0];
        }

        return $contentType;
    }

    /**
     * Check strpos in array
     *
     * @param string $haystack - The string to search in.
     * @param array $needle - Array to search from string
     *
     * @return boolean
     */
    function strposarray($haystack, $needle)
    {
        if (!is_array($needle)) $needle = array($needle);
        foreach ($needle as $query) {
            if (strpos($haystack, $query) !== false) return true;
        }
        return false;
    }

    /**
     * Get Image from URL
     *
     * @param $url
     * @return array
     */
    protected function getImageFromURL($url)
    {
        stream_context_set_default([
            'http' => [
                'method' => 'HEAD'
            ]
        ]);

        $urlHeaders = get_headers($url, 1);

        stream_context_set_default([
            'http' => [
                'method' => 'GET'
            ]
        ]);

        $info = [];

        $contentType = $urlHeaders['Content-Type'];
        if (is_array($contentType)) {
            $contentType = array_shift($contentType);
        }

        $contentType = $this->getMimeTypeFromContentType($contentType);
        $comparableContentType = ['image/', 'pdf'];
        if (!$this->strposarray($contentType, $comparableContentType)) {
            return $info;
        }

        $urlInfo = \Directus\parse_url_file($url);
        $content = file_get_contents($url);
        if (!$content) {
            return $info;
        }

        list($width, $height) = getimagesizefromstring($content);

        if (isset($urlInfo['filename']) && !empty($urlInfo['filename'])) {
            $filename = $urlInfo['filename'];
        } else {
            $filename = md5(time() . $url);
        }

        $data = base64_encode($content);
        $info['filename'] = filename_put_ext($filename, MimeTypeUtils::getFromMimeType($contentType));
        $info['title'] = $urlInfo['filename'];
        $info['name'] = $urlInfo['basename'];
        $info['size'] = isset($urlHeaders['Content-Length']) ? $urlHeaders['Content-Length'] : 0;
        $info['type'] = $contentType;
        $info['width'] = $width;
        $info['height'] = $height;
        $info['data'] = $data;
        $info['charset'] = 'binary';

        return $info;
    }

    /**
     * Get base64 data information
     *
     * @param $data
     *
     * @return array
     */
    public function getDataInfo($data)
    {
        if (strpos($data, 'data:') === 0) {
            $parts = explode(',', $data);
            $data = $parts[1];
        }

        $info = $this->getFileInfoFromData(base64_decode($data));

        return array_merge(['data' => $data], $info);
    }

    /**
     * Copy base64 data into Directus Media
     *
     * @param string|UploadedFile $fileData - base64 data or directus_files object
     * @param string $fileName - name of the file
     * @param bool $replace
     *
     * @return array
     *
     * @todo Refactor this to be clearer what's going on. $fileData can be anything,
     *       all kinds of things are happening, and nothing is documented
     */
    public function saveData($fileData, $fileName, $replace = false)
    {
        // When file is uploaded via multipart form data then We will get object of Slim\Http\UploadFile
        // When file is uploaded via URL (Youtube, Vimeo, or image link) then we will get base64 encode string.
        $size = null;

        $title = $fileName;

        if (is_object($fileData)) {
            $size = $fileData->getSize();
            $checksum = hash_file('md5', $fileData->file);
        } else {
            $fileData = base64_decode($this->getDataInfo($fileData)['data']);
            $checksum = md5($fileData);
            $size = strlen($fileData);
        }

        $fileName = $this->getFileName($fileName, $replace !== true);
        $filePath = $this->getConfig('root') . '/' . $fileName;

        $event = $replace ? 'file.update' : 'file.save';
        $this->emitter->run($event, ['name' => $fileName, 'size' => $size]);

        // On name change, the file would be overwritten with the empty file data.
        // This prevents you can't update a file to a zero-byte file.
        if (!empty($fileData)) {
            // This is where the actual bytes of the file are actually written to storage,
            // which may be the local filesystem or remote.
            $this->write($fileName, $fileData, $replace);
        }

        $this->emitter->run($event . ':after', ['name' => $fileName, 'size' => $size]);

        #open local tmp file since s3 bucket is private
        // Changed to use the existing temporary uploaded file on disk. As far as I can tell,
        // this method is only ever called from FilesServices::create and ::update (via
        // FilesServices::getSaveData) and $fileData will only ever be an object with a
        // "file" property if it's an UploadedFile, so "file" is always already the path to
        // a temporary file on disk.
        $tmp = null;
        if (is_object($fileData) && isset($fileData->file)) {
            // Keep track of the local temporary uploaded file so we can extract extra
            // metadata from it later, if possible.
            $tmp = $fileData->file;
        }

        unset($fileData);

        $fileData = $this->getFileInfo($fileName, false, $tmp);
        $fileData['title'] = Formatting::fileNameToFileTitle($title);
        $fileData['filename_disk'] = basename($filePath);
        $fileData['storage'] = $this->config['adapter'];

        $fileData = array_merge($this->defaults, $fileData);

        # Updates for file meta data tags
        // Only try to extract extra metadata if we know we have a local copy on the filesystem.
        // @TODO This should probably be moved to the appropriate getFileInfo methods
        if (isset($tmp) && strpos($fileData['type'], 'video') !== false) {
            #use ffprobe on local file, can't stream data to it or reference
            $output = shell_exec("ffprobe {$tmp} -show_entries format=duration:stream=height,width -v quiet -of json");
            #echo($output);
            $media = json_decode($output);
            if($media && is_object($media)) {
                $width = $media->streams[0]->width;
                $height = $media->streams[0]->height;
                $duration = $media->format->duration;   #seconds
            }

        } elseif (isset($tmp) && strpos($fileData['type'], 'audio') !== false) {
            $output = shell_exec("ffprobe {$tmp} -show_entries format=duration -v quiet -of json");
            $media = json_decode($output);

            if($media && is_object($media)) {
                $duration = $media->format->duration;
            }
        }
        if (isset($handle)) {
            fclose($handle);
        }

        $response = [
            // The MIME type will be based on its extension, rather than its extension
            'type' => MimeTypeUtils::getFromFilename($fileData['filename_disk']),
            'filename_disk' => $fileData['filename_disk'],
            'tags' => $fileData['tags'],
            'description' => $fileData['description'],
            'location' => $fileData['location'],
            'charset' => $fileData['charset'],
            'filesize' => $fileData['size'],
            'width' => isset($width) ? $width : $fileData['width'],
            'height' => isset($height) ? $height : $fileData['height'],
            'metadata' => isset($file['metadata']) ? $file['metadata'] : null,
            'storage' => $fileData['storage'],
            'checksum' => $checksum,
            'duration' => isset($duration) ? $duration : 0
        ];

        if (!$replace) {
            $response['title'] = $fileData['title'];
        }

        return $response;
    }

    /**
     * Save embed url into Directus Media
     *
     * @param array $fileInfo - File Data/Info
     *
     * @return array - file info
     */
    public function saveEmbedData(array $fileInfo)
    {
        if (!array_key_exists('type', $fileInfo) || strpos($fileInfo['type'], 'embed/') !== 0) {
            return [];
        }

        $fileName = isset($fileInfo['filename_disk']) ? $fileInfo['filename_disk'] : md5(time()) . '.jpg';
        $thumbnailData = $this->saveData($fileInfo['data'], $fileName, $fileInfo['fileId']);

        return array_merge(
            $fileInfo,
            ArrayUtils::pick($thumbnailData, [
                'filename_disk',
            ])
        );
    }

    /**
     * Get file info
     *
     * @param string $path - file path
     * @param bool $outside - if the $path is outside of the adapter root path.
     * @param string|null $localCopy - path to local copy of the file, if available
     *
     * @throws \RuntimeException
     *
     * @return array file information
     */
    public function getFileInfo($path, $outside = false, $localCopy = null)
    {
        if (!is_null($localCopy)) {
            // If there is a local copy, we can collect all the metadata we need without
            // loading the data into memory.
            return $this->getFileInfoFromLocalCopy($path, $localCopy);
        }
        if ($outside === true) {
            $buffer = file_get_contents($path);
            $fileData = $this->getFileInfoFromData($buffer);
        } else {
            $fileData = $this->getFileInfoFromPath($path);
        }

        return $fileData;
    }

    /**
     * Collect metadata about a file for which there exists a local copy on disk
     *
     * @param string $path
     * @param string $localCopy
     * @return array
     */
    public function getFileInfoFromLocalCopy($path, $localCopy)
    {
        $mime = MimeTypeUtils::getFromFilename($path);

        $typeTokens = explode('/', $mime);

        $size = filesize($localCopy);
        $info = [
            'type' => $mime,
            'format' => $typeTokens[1],
            'size' => $size,
            'width' => null,
            'height' => null
        ];

        if ($typeTokens[0] == 'image') {
            $meta = [];
            $imageInfo = getimagesize($localCopy, $meta);
            $this->collectImageInfo($imageInfo, $meta, $info);
        }

        return $info;
    }

    /**
     * Collect metadata about a file in storage
     *
     * @param string $path
     * @return array
     * @throws \League\Flysystem\FileNotFoundException
     */
    public function getFileInfoFromPath($path)
    {
        $mime = $this->filesystem->getAdapter()->getMimetype($path);

        $typeTokens = explode('/', $mime);

        if ($typeTokens[0] == 'image') {
            $buffer = $this->filesystem->getAdapter()->read($path);
            $info = $this->getFileInfoFromData($buffer);
        } else {
            $size = $this->filesystem->getAdapter()->getSize($path);
            $info = [
                'type' => $mime,
                'format' => $typeTokens[1],
                'size' => $size,
                'width' => null,
                'height' => null
            ];
        }

        return $info;
    }

    /**
     * Extract metadata from file contents
     *
     * @param string $data
     * @return array
     */
    public function getFileInfoFromData($data)
    {
        if (!class_exists('\finfo')) {
            throw new \RuntimeException('PHP File Information extension was not loaded.');
        }

        $finfo = new \finfo(FILEINFO_MIME);
        $type = explode('; charset=', $finfo->buffer($data));

        $mime = $type[0];
        $charset = $type[1];
        $typeTokens = explode('/', $mime);

        $info = [
            'type' => $mime,
            'format' => $typeTokens[1],
            'charset' => $charset,
            'size' => strlen($data),
            'width' => null,
            'height' => null
        ];

        if ($typeTokens[0] == 'image') {
            $meta = [];
            // @TODO: use this as fallback for finfo?
            $imageInfo = getimagesizefromstring($data, $meta);
            $this->collectImageInfo($imageInfo, $meta, $info);
        }

        return $info;
    }

    /**
     * Combine info from getimagesize[fromstring] with existing info array
     *
     * @param array|false $imageInfo As returned by getimagesize[fromstring]
     * @param array $meta As provided via the second argument to getimagesize[fromstring]
     * @param array $info Passed by reference
     */
    protected function collectImageInfo($imageInfo, $meta, &$info)
    {
        if (is_array($imageInfo)) {
            $info['width'] = $imageInfo[0];
            $info['height'] = $imageInfo[1];
        }

        if (is_array($meta) && isset($meta['APP13'])) {
            $iptc = iptcparse($meta['APP13']);

            if (isset($iptc['2#120'])) {
                $info['description'] = $iptc['2#120'][0];
            }

            if (isset($iptc['2#005']) && $iptc['2#005'][0] != '') {
                $info['title'] = $iptc['2#005'][0];
            }

            if (isset($iptc['2#025'])) {
                $info['tags'] = implode(',', $iptc['2#025']);
            }

            $location = [];

            if (isset($iptc['2#090']) && $iptc['2#090'][0] != '') {
                $location[] = $iptc['2#090'][0];
            }

            if (isset($iptc['2#095'][0]) && $iptc['2#095'][0] != '') {
                $location[] = $iptc['2#095'][0];
            }

            if (isset($iptc['2#101']) && $iptc['2#101'][0] != '') {
                $location[] = $iptc['2#101'][0];
            }

            $info['location'] = implode(', ', $location);
        }

        return $info;
    }

    /**
     * Get file settings
     *
     * @param string $key - Optional setting key name
     *
     * @return mixed
     */
    public function getSettings($key = '')
    {
        if (!$key) {
            return $this->filesSettings;
        } else if (array_key_exists($key, $this->filesSettings)) {
            return $this->filesSettings[$key];
        }

        return false;
    }

    /**
     * Get filesystem config
     *
     * @param string $key - Optional config key name
     *
     * @return mixed
     */
    public function getConfig($key = '')
    {
        if (!$key) {
            return $this->config;
        } else if (array_key_exists($key, $this->config)) {
            return $this->config[$key];
        }

        return false;
    }

    /**
     * Writes the given data in the given location
     *
     * @param $location
     * @param $data
     * @param bool $replace
     *
     * @throws \RuntimeException
     */
    public function write($location, $data, $replace = false)
    {
        $this->filesystem->write($location, $data, $replace);
    }

    /**
     * Reads and returns data from the given location
     *
     * @param $location
     *
     * @return bool|false|string
     *
     * @throws \Exception
     */
    public function read($location)
    {
        try {
            return $this->filesystem->getAdapter()->read($location);
        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * Sanitize title name from file name
     *
     * @param string $fileName
     *
     * @return string
     */
    private function sanitizeName($fileName)
    {
        // Swap out Non "Letters" with a -
        $fileName = preg_replace('/[^_\\pL\d^\.]+/u', '-', $fileName);

        // Trim out extra -'s
        $fileName = trim($fileName, '-');

        // Make text lowercase
        $fileName = strtolower($fileName);

        // Strip out anything we haven't been able to convert
        $fileName = preg_replace('/[^-\w\.]+/', '', $fileName);

        return $fileName;
    }

    /**
     * Add suffix number to file name if already exists.
     *
     * @param string $fileName
     * @param string $targetPath
     * @param int $attempt - Optional
     *
     * @return bool
     */
    public function uniqueName($fileName, $targetPath = null, $attempt = 0)
    {
        if (!$targetPath) {
            $targetPath = $this->filesystem->getPath();
        }

        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
        $name = pathinfo($fileName, PATHINFO_FILENAME);
        $newName = $this->sanitizeName(filename_put_ext($name, $ext));

        if ($this->filesystem->exists($newName)) {
            $matches = [];
            $format = '/\-(\d)%s$/';
            $withExtension = '';
            if ($ext) {
                $withExtension = '\.(' . $ext . ')';
            }

            $trailingDigit = sprintf($format, $withExtension);

            if (preg_match($trailingDigit, $fileName, $matches)) {
                // Convert "fname-1.jpg" to "fname-2.jpg"
                $attempt = 1 + (int) $matches[1];
                $newName = preg_replace(
                    $trailingDigit,
                    filename_put_ext("-{$attempt}", $ext),
                    $newName
                );
            } else {
                if ($attempt) {
                    $name = rtrim($name, $attempt);
                    $name = rtrim($name, '-');
                }

                $attempt++;
                $newName = filename_put_ext($name . '-' . $attempt, $ext);
            }

            return $this->uniqueName($newName, $targetPath, $attempt);
        }

        return $newName;
    }

    /**
     * Get file name based on file naming setting
     *
     * @param string $fileName
     * @param bool $unique
     *
     * @return string
     */
    private function getFileName($fileName, $unique = true)
    {
        if ($unique) {
            switch ($this->getSettings('file_naming')) {
                case 'uuid':
                    $fileName = $this->uuidFileName($fileName);
                    break;
            }
            $fileName = $this->uniqueName($fileName, $this->filesystem->getPath());
        }
        return $fileName;
    }

    /**
     * Hash file name
     *
     * @param string $fileName
     *
     * @return string
     */
    private function uuidFileName($fileName)
    {
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
        $fileHashName = generate_uuid4();

        return $fileHashName . '.' . $ext;
    }

    /**
     * Get a file size and type info from base64 data , URL ,multipart form data
     *
     * @param string|UploadedFile $data
     *
     * @return array file size and type
     */
    public function getFileSizeType($data)
    {
        $result = [];
        if (is_a_url($data)) {
            $dataInfo = $this->getLink($data);
            $result['mimeType'] = isset($dataInfo['type']) ? $dataInfo['type'] : null;
            $result['size'] = isset($dataInfo['filesize']) ? $dataInfo['filesize'] : (isset($dataInfo['size']) ? $dataInfo['size'] : null);
        } else if (is_object($data)) {
            $result['mimeType'] = $data->getClientMediaType();
            $result['size'] = $data->getSize();
        } else if (strpos($data, 'data:') === 0) {
            $parts = explode(',', $data);
            $file = $parts[1];
            $dataInfo = $this->getFileInfoFromData(base64_decode($file));
            $result['mimeType'] = isset($dataInfo['type']) ? $dataInfo['type'] : null;
            $result['size'] = isset($dataInfo['size']) ? $dataInfo['size'] : null;
        }
        return $result;
    }
}