HEX
Server: nginx/1.18.0
System: Linux test-ipsremont 5.4.0-214-generic #234-Ubuntu SMP Fri Mar 14 23:50:27 UTC 2025 x86_64
User: ips (1000)
PHP: 8.0.30
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/heifetz/heifetz-app/models/File.php
<?php

namespace Models;

use App\Exceptions\CrmException;
use Core\DbLib\NewDbModel;
use Core\Models\Acl;
use Core\Models\CoreHelper;
use Core\Models\ErrorLog;
use Exception;
use finfo;
use Gumlet\ImageResize;
use Gumlet\ImageResizeException;
use Helpers\ArrayHelper;
use Helpers\Data;
use Helpers\FileHelper;
use JetBrains\PhpStorm\NoReturn;
use Traits\ModelAddByArrayReturnsId;
use Traits\ModelGetQuery;

/**
 * @property int $id
 * @property string $name
 * @property string $extension
 * @property int $size
 * @property string $path
 * @property string $created_at
 * @property string $updated_at
 * @property string $deleted_at
 */
class File extends NewDbModel
{

    use ModelGetQuery;
    use ModelAddByArrayReturnsId;

    static string $tableName = 'files';

    protected static bool $hasSoftDelete = true;

    const STORAGE_TYPE_LOCAL = 0;

    public static array $storageTypes = [
        self::STORAGE_TYPE_LOCAL => 'Локальное хранилище',
    ];

    const LOCAL_STORAGE_TYPES = [
        self::STORAGE_TYPE_LOCAL,
    ];

    static array $imageExtensions = ['jpg', 'jpeg', 'gif', 'png'];

    public static function upload($fileData, $typeId = null)
    {
        $tempFile = $fileData['tmp_name'];
        if (empty(filesize($tempFile))) {
            return 0;
        }

        $pathInfo = pathinfo($fileData['name']);

        $targetPath = UPLOADS_DIR . '/';
        $targetPath = str_replace('//', '/', $targetPath);
        $targetPath .= date('Y/m/d/');

        $extension = $pathInfo['extension'] ?? '';
        if (mb_strlen($extension) > 5) {
            $extension = '';
        }
        $fileName = md5($fileData['name']) . '.' . $extension;
        $fileName = date('YmdHis') . str_replace(' ', '_', $fileName);
        $targetFile = $targetPath . $fileName;
        $targetFile = str_replace('//', '/', $targetFile);

        try {
            FileHelper::checkAndCreateDir($targetPath, 0777);
            move_uploaded_file($tempFile, $targetFile);
        } catch (Exception) {
            throw new CrmException('Ошибка загрузки файла');
        }
        chmod($targetFile, 0775);

        $file = new self();
        $file->name = $fileData['name'];
        $file->extension = $extension;
        $file->size = (int) filesize($targetFile);
        $file->path = str_replace(ROOT, '', $targetFile);
        $file->save();

        return $file->id;
    }

    public static function uploadMultiply($fieldName, $typeIdName = null)
    {
        $files = [];
        if (empty($_FILES[$fieldName]['name']) || empty($_FILES[$fieldName]['tmp_name'])) {
            return $files;
        }

        if (isset($typeIdName)) {
            $typesIds = Data::getVar($typeIdName);
        }
        if (!is_array($_FILES[$fieldName]['name']) || !is_array($_FILES[$fieldName]['tmp_name'])) {
            $_FILES[$fieldName]['tmp_name'][] = $_FILES[$fieldName]['tmp_name'];
            $_FILES[$fieldName]['name'][] = $_FILES[$fieldName]['name'];
        }

        foreach ($_FILES[$fieldName]['name'] as $key => $value) {
            if (empty($_FILES[$fieldName]['name'][$key]) || empty($_FILES[$fieldName]['tmp_name'][$key])) {
                continue;
            }
            $fileData = [
                'tmp_name' => $_FILES[$fieldName]['tmp_name'][$key],
                'name' => $_FILES[$fieldName]['name'][$key],
            ];
            $fileId = self::upload($fileData, $typesIds[$key] ?? null);
            if (!empty($fileId)) {
                $files[] = $fileId;
            }
        }

        return $files;
    }

    public static function preRender($list)
    {
        $files = [];
        foreach ($list as $row) {
            $files[] = [
                'filesize' => Data::humanSize($row->size),
                'basename' => $row->name,
                'link' => '/' . Acl::PRESENTER_FILES . $row->path,
                'key' => $row->id,
                'delete_request' => $row->delete_request ?? null,
            ];
        }

        return $files;
    }


    public static function saveFile($name, $fileContent, $subPath = null, $path = ROOT . '/storage/', int $storageType = self::STORAGE_TYPE_LOCAL, bool $generateName = false)
    {
        $path .= $subPath ? '/' . $subPath . '/' : '';
        $path .= date('Y/m/d/');

        $pathInfo = pathinfo($name);
        if ($generateName) {
            $oldName = $name;
            $name = md5($name) . '.' . $pathInfo['extension'];
            $name = date('YmdHis') . str_replace(' ', '_', $name);
        }

        $targetFile = str_replace('//', '/', $path . $name);

        try {
            FileHelper::checkAndCreateDir($path, 0777);
            file_put_contents($targetFile, $fileContent);
        } catch (Exception) {
            throw new CrmException('Ошибка загрузки файла');
        }

        $fileSize = filesize($targetFile);
        if (empty($fileSize)) {
            return false;
        }

        return self::create(
            [
                'name' => $generateName ? $oldName : basename($targetFile),
                'extension' => $pathInfo['extension'],
                'size' => $fileSize,
                'path' => str_replace('//', '/', str_replace(ROOT, '/', $targetFile)),
                'storage_type' => $storageType,
                'company_id' => CoreHelper::$companyId,
                'created_at' => date('Y-m-d H:i:s'),
            ]
        );
    }

    public static function isImage($file)
    {
        if (is_object($file)) {
            return in_array(mb_strtolower($file->extension), self::$imageExtensions);
        }

        $path_parts = pathinfo($file);
        if (isset($path_parts['extension'])) {
            return in_array(mb_strtolower($path_parts['extension']), self::$imageExtensions);
        }

        return false;
    }

    #[NoReturn]
    public static function rangeDownload(self $file, string $filename): void
    {
        $isUrl = filter_var($filename, FILTER_VALIDATE_URL);
        $length = 0;
        $size = 0;
        $fp = fopen($filename, 'rb');

        if ($isUrl === false) {
            $size = filesize($filename);
            $length = $size;
        } else {
            $responseHeaders = get_headers($filename, true);
            $size = $responseHeaders['Content-Length'];
            $length = $size;
        }

        $start = 0;
        $end = $size - 1;

        header("Accept-Ranges: 0-$length");
        if (isset($_SERVER['HTTP_RANGE'])) {
            $c_end = $end;
            [, $range] = explode('=', $_SERVER['HTTP_RANGE'], 2);
            if (str_contains($range, ',')) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header("Content-Range: bytes $start-$end/$size");
                exit;
            }
            if ($range == '-') {
                $c_start = $size - substr($range, 1);
            } else {
                $range = explode('-', $range);
                $c_start = $range[0];
                $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
            }
            $c_end = ($c_end > $end) ? $end : $c_end;
            if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header("Content-Range: bytes $start-$end/$size");
                exit;
            }
            $start = $c_start;
            $end = $c_end;
            $length = $end - $start + 1; // Calculate new content length
            $meta = stream_get_meta_data($fp);
            if ($meta['seekable']) {
                fseek($fp, $start);
            }
            header('HTTP/1.1 206 Partial Content');
        }
        header("Content-Range: bytes $start-$end/$size");
        header("Content-Length: $length");

        $buffer = 1024 * 8;
        while (!feof($fp) && ($p = ftell($fp)) <= $end) {
            if ($p + $buffer > $end) {
                $buffer = $end - $p + 1;
            }
            set_time_limit(0); // Reset time limit for big files
            echo fread($fp, $buffer);
            flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
        }

        fclose($fp);
        exit;
    }

    #[NoReturn]
    public static function load(?self $file, string $filename, ?string $realFileName = null, bool $view = false): void
    {
        $realFilename = $realFileName ?? $filename;
        $isUrl = filter_var($filename, FILTER_VALIDATE_URL);

        // Открываем искомый файл
        $f = fopen($filename, 'r');

        if (!empty($f)) {
            if ($isUrl === false) {
                $headers = [mime_content_type($filename), filesize($filename)];
            } else {
                $finfo = new finfo(FILEINFO_MIME_TYPE);
                $type = $finfo->buffer(file_get_contents($filename));
                $responseHeaders = get_headers($filename, true);
                $headers = [$type, $responseHeaders['Content-Length']];
            }

            header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
            header('Content-Type: ' . $headers[0]);
            header('Content-Length: ' . $headers[1]);

            if (!$view && !empty($filename)) {
                header('Content-Disposition: attachment; filename="' . $realFilename . '"');
            }

            if (!empty($file) && 'mp3' === $file->extension) {
                header('Accept-Ranges: bytes');
                header('Transfer-Encoding: chunked');
            }

            while (!feof($f)) {
                // Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
                echo fread($f, 1024);
                flush();
            }
            // Закрываем файл
            fclose($f);
        } else {
            header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
            header('Status: 404 Not Found');
            echo '404 Not Found';
        }
        exit;
    }

    #[NoReturn]
    public static function download(?self $file, string $filename, ?string $realFileName = null, string $mimetype = 'application/octet-stream'): void
    {
        $isUrl = filter_var($filename, FILTER_VALIDATE_URL);
        $headers = [];

        // Открываем искомый файл
        $f = fopen($filename, 'r');

        if (!empty($filename)) {
            if ($isUrl === false) {
                $headers = [
                    gmdate('r', filemtime($filename)),
                    sprintf('%x-%x-%x', fileinode($filename), filesize($filename), filemtime($filename)),
                    filesize($filename),
                    $realFileName ?? basename($filename),
                ];
            } else {
                $responseHeaders = get_headers($filename, true);
                $headers = [
                    $responseHeaders['Last-Modified'],
                    $responseHeaders['ETag'],
                    $responseHeaders['Content-Length'],
                    $realFileName ?? $file->name,
                ];
            }
            header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
            header('Content-Type: ' . $mimetype);
            header('Last-Modified: ' . $headers[0]);
            header('ETag: ' . $headers[1]);
            header('Content-Length: ' . $headers[2]);
            header('Connection: close');
            header('Content-Disposition: attachment; filename="' . $headers[3] . '";');

            while (!feof($f)) {
                // Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
                echo fread($f, 1024);
                flush();
            }
            // Закрываем файл
            fclose($f);
        } else {
            header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
            header('Status: 404 Not Found');
            echo '404 Not Found';
        }
        exit;
    }

    /** @deprecated use FilesRepository::getDownloadLink() */
    public static function getDownloadLink(object|array|string $file, bool $view = false): string
    {
        if (is_object($file)) {
            $file = ArrayHelper::toArray($file);
        }

        if (is_string($file)) {
            return '/' . Acl::PRESENTER_FILES . $file . ($view ? '?view=1' : '');
        }

        return '/' . Acl::PRESENTER_FILES . $file['path'] . '?file_id=' . $file['id'] . ($view ? '&view=1' : '');
    }

    public static function rename($fileId, $name)
    {
        self::update(['name' => $name], ['id' => $fileId]);

        UserLogs::logUpdate(self::$tableName, $fileId, 'name', $name);
    }

    public static function changeType($fileId, $typeId)
    {
        self::update(['type_id' => $typeId], ['id' => $fileId]);

        UserLogs::logUpdate(self::$tableName, $fileId, 'type_id', $typeId);
    }

    /**
     * @param int|self $file
     * @param int $width
     * @param int $height
     *
     * @return string
     */
    public static function getThumb($file, int $width, int $height): ?string
    {
        if (!is_object($file)) {
            $file = self::find($file);
        }

        if (!File::isImage($file)) {
            return null;
        }

        $thumbDir = '/thumbnails/' . $file->id . '/';
        $thumbFileName = $width . 'x' . $height . '-' . basename($file->path);

        if (!file_exists(TEMP_DIR . $thumbDir . $thumbFileName)) {
            $fileName = str_replace('//', '/', ROOT . '/' . $file->path);
            if (!file_exists($fileName) || filesize($fileName) > 6 * 1024 * 1024) {
                return '';
            }
            try {
                $image = new ImageResize($fileName);
                $image->resizeToBestFit($width, $height);
                FileHelper::checkAndCreateDir(TEMP_DIR . $thumbDir);
                $image->save(TEMP_DIR . $thumbDir . $thumbFileName);
            } catch (Exception $exception) {
                $exception = new CrmException($exception->getMessage() . ' ' . print_r($file, true));
                ErrorLog::processException($exception);

                return null;
            }
        }

        return '/files/tmp' . $thumbDir . $thumbFileName;
    }

}