This commit is contained in:
root
2025-11-13 19:04:05 +03:00
commit 240d0aba5f
75129 changed files with 11118122 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
<?php
namespace Bitrix\UI\FileUploader;
class CanUploadResult extends \Bitrix\Main\Result implements \JsonSerializable
{
public function __construct()
{
parent::__construct();
}
public static function reject(): self
{
$canUploadResult = new self();
$canUploadResult->addError(new UploaderError(UploaderError::FILE_UPLOAD_ACCESS_DENIED));
return $canUploadResult;
}
public function jsonSerialize(): array
{
return [
'errors' => $this->getErrors(),
'success' => $this->isSuccess(),
];
}
}

View File

@@ -0,0 +1,413 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Error;
use Bitrix\Main\File;
use Bitrix\Main\HttpRequest;
use Bitrix\Main\IO;
use Bitrix\Main\Result;
use Bitrix\Main\Text\Encoding;
class Chunk
{
protected int $size = 0;
protected ?int $fileSize = null;
protected ?int $startRange = null;
protected ?int $endRange = null;
protected string $type = '';
protected string $name = '';
protected int $width = 0;
protected int $height = 0;
protected IO\File $file;
protected function __construct(IO\File $file)
{
$this->setFile($file);
}
public static function createFromRequest(HttpRequest $request): Result
{
$result = new Result();
$fileMimeType = (string)$request->getHeader('Content-Type');
if (!preg_match('~\w+/[-+.\w]+~', $fileMimeType))
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_TYPE));
}
$contentLength = $request->getHeader('Content-Length');
if ($contentLength === null)
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_LENGTH));
}
$contentLength = (int)$contentLength;
$filename = static::normalizeFilename((string)$request->getHeader('X-Upload-Content-Name'));
if (empty($filename))
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_NAME));
}
if (!static::isValidFilename($filename))
{
return $result->addError(new UploaderError(UploaderError::INVALID_FILENAME));
}
$contentRangeResult = static::getContentRange($request);
if (!$contentRangeResult->isSuccess())
{
return $result->addErrors($contentRangeResult->getErrors());
}
$file = static::getFileFromHttpInput();
$contentRange = $contentRangeResult->getData();
$rangeChunkSize = empty($contentRange) ? 0 : ($contentRange['endRange'] - $contentRange['startRange'] + 1);
if ($rangeChunkSize && $contentLength !== $rangeChunkSize)
{
return $result->addError(new UploaderError(
UploaderError::INVALID_RANGE_SIZE,
[
'rangeChunkSize' => $rangeChunkSize,
'contentLength' => $contentLength,
]
));
}
$chunk = new Chunk($file);
if ($chunk->getSize() !== $contentLength)
{
return $result->addError(new UploaderError(
UploaderError::INVALID_CHUNK_SIZE,
[
'chunkSize' => $chunk->getSize(),
'contentLength' => $contentLength,
]
));
}
$chunk->setName($filename);
$chunk->setType($fileMimeType);
if (!empty($contentRange))
{
$chunk->setStartRange($contentRange['startRange']);
$chunk->setEndRange($contentRange['endRange']);
$chunk->setFileSize($contentRange['fileSize']);
}
$result->setData(['chunk' => $chunk]);
return $result;
}
private static function getFileFromHttpInput(): IO\File
{
// This file will be automatically removed on shutdown
$tmpFilePath = TempFile::generateLocalTempFile();
$file = new IO\File($tmpFilePath);
$file->putContents(HttpRequest::getInput());
return $file;
}
private static function getContentRange(HttpRequest $request): Result
{
$contentRange = $request->getHeader('Content-Range');
if ($contentRange === null)
{
return new Result();
}
$result = new Result();
if (!preg_match('/(\d+)-(\d+)\/(\d+)$/', $contentRange, $match))
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_RANGE));
}
[$startRange, $endRange, $fileSize] = [(int)$match[1], (int)$match[2], (int)$match[3]];
if ($startRange > $endRange)
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_RANGE));
}
if ($fileSize <= $endRange)
{
return $result->addError(new UploaderError(UploaderError::INVALID_CONTENT_RANGE));
}
$result->setData([
'startRange' => $startRange,
'endRange' => $endRange,
'fileSize' => $fileSize,
]);
return $result;
}
private static function normalizeFilename(string $filename): string
{
$filename = urldecode($filename);
$filename = Encoding::convertEncodingToCurrent($filename);
return \getFileName($filename);
}
private static function isValidFilename(string $filename): bool
{
if (mb_strlen($filename) > 255)
{
return false;
}
if (mb_strpos($filename, '\0') !== false)
{
return false;
}
return true;
}
public function getFile(): IO\File
{
return $this->file;
}
/**
* @internal
*/
public function setFile(IO\File $file): void
{
$this->file = $file;
$this->size = $file->getSize();
}
public function getSize(): int
{
return $this->size;
}
public function getFileSize(): int
{
return $this->fileSize ?? $this->size;
}
protected function setFileSize(int $fileSize): void
{
$this->fileSize = $fileSize;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): void
{
$this->type = $type;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): void
{
$this->width = $width;
}
public function getHeight(): int
{
return $this->height;
}
public function setHeight(int $height): void
{
$this->height = $height;
}
public function getStartRange(): ?int
{
return $this->startRange;
}
protected function setStartRange(int $startRange): void
{
$this->startRange = $startRange;
}
public function getEndRange(): ?int
{
return $this->endRange;
}
protected function setEndRange(int $endRange): void
{
$this->endRange = $endRange;
}
public function isFirst(): bool
{
return $this->startRange === null || $this->startRange === 0;
}
public function isLast(): bool
{
return $this->endRange === null || ($this->endRange + 1) === $this->fileSize;
}
public function isOnlyOne(): bool
{
return (
$this->startRange === null
|| ($this->startRange === 0 && ($this->endRange - $this->startRange + 1) === $this->fileSize)
);
}
public function validate(Configuration $config): Result
{
$result = new Result();
if (in_array(mb_strtolower($this->getName()), $config->getIgnoredFileNames()))
{
return $result->addError(new UploaderError(UploaderError::FILE_NAME_NOT_ALLOWED));
}
if ($config->getMaxFileSize() !== null && $this->getFileSize() > $config->getMaxFileSize())
{
return $result->addError(
new UploaderError(
UploaderError::MAX_FILE_SIZE_EXCEEDED,
[
'maxFileSize' => \CFile::formatSize($config->getMaxFileSize()),
'maxFileSizeInBytes' => $config->getMaxFileSize(),
]
)
);
}
if ($this->getFileSize() < $config->getMinFileSize())
{
return $result->addError(
new UploaderError(
UploaderError::MIN_FILE_SIZE_EXCEEDED,
[
'minFileSize' => \CFile::formatSize($config->getMinFileSize()),
'minFileSizeInBytes' => $config->getMinFileSize(),
]
)
);
}
if (!$this->validateFileType($config->getAcceptedFileTypes()))
{
return $result->addError(new UploaderError(UploaderError::FILE_TYPE_NOT_ALLOWED));
}
$width = 0;
$height = 0;
if (\CFile::isImage($this->getName(), $this->getType()))
{
$image = new File\Image($this->getFile()->getPhysicalPath());
$imageInfo = $image->getInfo(false);
if (!$imageInfo)
{
if ($config->getIgnoreUnknownImageTypes())
{
$result->setData(['width' => $width, 'height' => $height]);
return $result;
}
else
{
return $result->addError(new UploaderError(UploaderError::IMAGE_TYPE_NOT_SUPPORTED));
}
}
$width = $imageInfo->getWidth();
$height = $imageInfo->getHeight();
if ($imageInfo->getFormat() === File\Image::FORMAT_JPEG)
{
$exifData = $image->getExifData();
if (isset($exifData['Orientation']) && $exifData['Orientation'] >= 5 && $exifData['Orientation'] <= 8)
{
[$width, $height] = [$height, $width];
}
}
if (!$config->shouldTreatOversizeImageAsFile())
{
$imageData = new FileData($this->getName(), $this->getType(), $this->getSize());
$imageData->setWidth($width);
$imageData->setHeight($height);
$validationResult = $config->validateImage($imageData);
if (!$validationResult->isSuccess())
{
return $result->addErrors($validationResult->getErrors());
}
}
}
$result->setData(['width' => $width, 'height' => $height]);
return $result;
}
private function validateFileType(array $fileTypes): bool
{
if (count($fileTypes) === 0)
{
return true;
}
$mimeType = $this->getType();
$baseMimeType = preg_replace('/\/.*$/', '', $mimeType);
foreach ($fileTypes as $type)
{
if (!is_string($type) || mb_strlen($type) === 0)
{
continue;
}
$type = mb_strtolower(trim($type));
if ($type[0] === '.') // extension case
{
$filename = mb_strtolower($this->getName());
$offset = mb_strlen($filename) - mb_strlen($type);
if (mb_strpos($filename, $type, $offset) !== false)
{
return true;
}
}
elseif (preg_match('/\/\*$/', $type)) // image/* mime type case
{
if ($baseMimeType === preg_replace('/\/.*$/', '', $type))
{
return true;
}
}
elseif ($mimeType === $type)
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Bitrix\UI\FileUploader;
class CommitOptions
{
protected string $moduleId = '';
protected string $savePath = '';
protected bool $forceRandom = true;
protected bool $skipExtension = false;
protected string $addDirectory = '';
public function __construct(array $options = [])
{
$optionNames = [
'moduleId',
'savePath',
'forceRandom',
'skipExtension',
'addDirectory',
];
foreach ($optionNames as $optionName)
{
if (array_key_exists($optionName, $options))
{
$optionValue = $options[$optionName];
$setter = 'set' . ucfirst($optionName);
$this->$setter($optionValue);
}
}
}
public function getModuleId(): string
{
return $this->moduleId;
}
public function setModuleId(string $moduleId): self
{
$this->moduleId = $moduleId;
return $this;
}
public function getSavePath(): string
{
return $this->savePath;
}
public function setSavePath(string $savePath): self
{
$this->savePath = $savePath;
return $this;
}
public function isForceRandom(): bool
{
return $this->forceRandom;
}
public function setForceRandom(bool $forceRandom): self
{
$this->forceRandom = $forceRandom;
return $this;
}
public function isSkipExtension(): bool
{
return $this->skipExtension;
}
public function setSkipExtension(bool $skipExtension): self
{
$this->skipExtension = $skipExtension;
return $this;
}
public function getAddDirectory(): string
{
return $this->addDirectory;
}
public function setAddDirectory(string $addDirectory): self
{
$this->addDirectory = $addDirectory;
return $this;
}
}

View File

@@ -0,0 +1,378 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Config\Ini;
use Bitrix\Main\Result;
class Configuration
{
protected ?int $maxFileSize = 256 * 1024 * 1024;
protected int $minFileSize = 0;
protected bool $acceptOnlyImages = false;
protected array $acceptedFileTypes = [];
protected array $ignoredFileNames = ['.ds_store', 'thumbs.db', 'desktop.ini'];
protected int $imageMinWidth = 1;
protected int $imageMinHeight = 1;
protected int $imageMaxWidth = 7000;
protected int $imageMaxHeight = 7000;
protected ?int $imageMaxFileSize = 48 * 1024 * 1024;
protected int $imageMinFileSize = 0;
protected bool $treatOversizeImageAsFile = false;
protected bool $ignoreUnknownImageTypes = false;
public function __construct(array $options = [])
{
$optionNames = [
'maxFileSize',
'minFileSize',
'imageMinWidth',
'imageMinHeight',
'imageMaxWidth',
'imageMaxHeight',
'imageMaxFileSize',
'imageMinFileSize',
'acceptOnlyImages',
'acceptedFileTypes',
'ignoredFileNames',
];
$globalSettings = static::getGlobalSettings();
foreach ($optionNames as $optionName)
{
$setter = 'set' . ucfirst($optionName);
if (array_key_exists($optionName, $options))
{
$optionValue = $options[$optionName];
$this->$setter($optionValue);
}
else if (array_key_exists($optionName, $globalSettings))
{
$optionValue = $globalSettings[$optionName];
if (is_string($optionValue) && preg_match('/FileSize/i', $optionName))
{
$optionValue = Ini::unformatInt($optionValue);
}
$this->$setter($optionValue);
}
}
if (isset($options['ignoreUnknownImageTypes']) && is_bool($options['ignoreUnknownImageTypes']))
{
$this->setIgnoreUnknownImageTypes($options['ignoreUnknownImageTypes']);
}
if (isset($options['treatOversizeImageAsFile']) && is_bool($options['treatOversizeImageAsFile']))
{
$this->setTreatOversizeImageAsFile($options['treatOversizeImageAsFile']);
}
}
public static function getGlobalSettings(): array
{
$settings = [];
$configuration = \Bitrix\Main\Config\Configuration::getValue('ui');
if (isset($configuration['uploader']['settings']) && is_array($configuration['uploader']['settings']))
{
$settings = $configuration['uploader']['settings'];
}
return $settings;
}
public function shouldTreatImageAsFile(FileData | array $fileData): bool
{
if (!$this->shouldTreatOversizeImageAsFile())
{
return false;
}
if (!$fileData->isImage())
{
return true;
}
$result = $this->validateImage($fileData);
return !$result->isSuccess();
}
public function validateImage(FileData $fileData): Result
{
$result = new Result();
if (($fileData->getWidth() === 0 || $fileData->getHeight() === 0) && !$this->getIgnoreUnknownImageTypes())
{
return $result->addError(new UploaderError(UploaderError::IMAGE_TYPE_NOT_SUPPORTED));
}
if ($this->getImageMaxFileSize() !== null && $fileData->getSize() > $this->getImageMaxFileSize())
{
return $result->addError(
new UploaderError(
UploaderError::IMAGE_MAX_FILE_SIZE_EXCEEDED,
[
'imageMaxFileSize' => \CFile::formatSize($this->getImageMaxFileSize()),
'imageMaxFileSizeInBytes' => $this->getImageMaxFileSize(),
]
)
);
}
if ($fileData->getSize() < $this->getImageMinFileSize())
{
return $result->addError(
new UploaderError(
UploaderError::IMAGE_MIN_FILE_SIZE_EXCEEDED,
[
'imageMinFileSize' => \CFile::formatSize($this->getImageMinFileSize()),
'imageMinFileSizeInBytes' => $this->getImageMinFileSize(),
]
)
);
}
if ($fileData->getWidth() < $this->getImageMinWidth() || $fileData->getHeight() < $this->getImageMinHeight())
{
return $result->addError(
new UploaderError(
UploaderError::IMAGE_IS_TOO_SMALL,
[
'minWidth' => $this->getImageMinWidth(),
'minHeight' => $this->getImageMinHeight(),
]
)
);
}
if ($fileData->getWidth() > $this->getImageMaxWidth() || $fileData->getHeight() > $this->getImageMaxHeight())
{
return $result->addError(
new UploaderError(
UploaderError::IMAGE_IS_TOO_BIG,
[
'maxWidth' => $this->getImageMaxWidth(),
'maxHeight' => $this->getImageMaxHeight(),
]
)
);
}
return $result;
}
public function getMaxFileSize(): ?int
{
return $this->maxFileSize;
}
public function setMaxFileSize(?int $maxFileSize): self
{
$this->maxFileSize = $maxFileSize;
return $this;
}
public function getMinFileSize(): int
{
return $this->minFileSize;
}
public function setMinFileSize(int $minFileSize): self
{
$this->minFileSize = $minFileSize;
return $this;
}
public function shouldAcceptOnlyImages(): bool
{
return $this->acceptOnlyImages;
}
public function getAcceptedFileTypes(): array
{
return $this->acceptedFileTypes;
}
public function setAcceptedFileTypes(array $acceptedFileTypes): self
{
$this->acceptedFileTypes = $acceptedFileTypes;
$this->acceptOnlyImages = false;
return $this;
}
public function setAcceptOnlyImages(bool $flag = true): self
{
$this->acceptOnlyImages = $flag;
if ($flag)
{
$this->acceptOnlyImages();
}
return $this;
}
public function acceptOnlyImages(): self
{
$imageExtensions = static::getImageExtensions();
$this->setAcceptedFileTypes($imageExtensions);
$this->acceptOnlyImages = true;
return $this;
}
public static function getImageExtensions(bool $withDot = true): array
{
$imageExtensions = explode(',', \CFile::getImageExtensions());
return array_map(function($extension) use($withDot) {
return ($withDot ? '.' : '') . trim($extension);
}, $imageExtensions);
}
public static function getVideoExtensions(bool $withDot = true): array
{
$extensions = [
'avi',
'wmv',
'mp4',
'mov',
'webm',
'flv',
'm4v',
'mkv',
'vob',
'3gp',
'ogv',
'h264',
];
if ($withDot)
{
return array_map(function($extension) {
return '.' . $extension;
}, $extensions);
}
return $extensions;
}
public function getIgnoredFileNames(): array
{
return $this->ignoredFileNames;
}
public function setIgnoredFileNames(array $fileNames): self
{
$this->ignoredFileNames = [];
foreach ($fileNames as $fileName)
{
if (is_string($fileName) && mb_strlen($fileName) > 0)
{
$this->ignoredFileNames[] = mb_strtolower($fileName);
}
}
return $this;
}
public function getImageMinWidth(): int
{
return $this->imageMinWidth;
}
public function setImageMinWidth(int $imageMinWidth): self
{
$this->imageMinWidth = $imageMinWidth;
return $this;
}
public function getImageMinHeight(): int
{
return $this->imageMinHeight;
}
public function setImageMinHeight(int $imageMinHeight): self
{
$this->imageMinHeight = $imageMinHeight;
return $this;
}
public function getImageMaxWidth(): int
{
return $this->imageMaxWidth;
}
public function setImageMaxWidth(int $imageMaxWidth): self
{
$this->imageMaxWidth = $imageMaxWidth;
return $this;
}
public function getImageMaxHeight(): int
{
return $this->imageMaxHeight;
}
public function setImageMaxHeight(int $imageMaxHeight): self
{
$this->imageMaxHeight = $imageMaxHeight;
return $this;
}
public function getImageMaxFileSize(): ?int
{
return $this->imageMaxFileSize;
}
public function setImageMaxFileSize(?int $imageMaxFileSize): self
{
$this->imageMaxFileSize = $imageMaxFileSize;
return $this;
}
public function getImageMinFileSize(): int
{
return $this->imageMinFileSize;
}
public function setImageMinFileSize(int $imageMinFileSize): self
{
$this->imageMinFileSize = $imageMinFileSize;
return $this;
}
public function getIgnoreUnknownImageTypes(): bool
{
return $this->ignoreUnknownImageTypes;
}
public function setIgnoreUnknownImageTypes(bool $flag): self
{
$this->ignoreUnknownImageTypes = $flag;
return $this;
}
public function shouldTreatOversizeImageAsFile(): bool
{
return $this->treatOversizeImageAsFile;
}
public function setTreatOversizeImageAsFile(bool $flag): self
{
$this->treatOversizeImageAsFile = $flag;
return $this;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\UI\FileUploader\Contracts;
interface CustomFingerprint
{
public function getFingerprint(): string;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\UI\FileUploader\Contracts;
use Bitrix\UI\FileUploader\LoadResultCollection;
interface CustomLoad
{
public function load(array $ids): LoadResultCollection;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\UI\FileUploader\Contracts;
use Bitrix\UI\FileUploader\RemoveResultCollection;
interface CustomRemove
{
public function remove(array $ids): RemoveResultCollection;
}

View File

@@ -0,0 +1,186 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\HttpApplication;
use Bitrix\Main\Loader;
class ControllerResolver
{
const DEFAULT_VENDOR = 'bitrix';
public static function createController(string $controllerName, array $options = []): ?UploaderController
{
[$moduleId, $className] = self::resolveName($controllerName);
if (!is_string($className))
{
return null;
}
if (is_string($moduleId) && self::canIncludeModule($moduleId))
{
Loader::includeModule($moduleId);
}
try
{
$controllerClass = new \ReflectionClass($className);
if ($controllerClass->isAbstract())
{
return null;
}
if (!$controllerClass->isSubclassOf(UploaderController::class))
{
return null;
}
/** @var UploaderController $controller */
$controller = $controllerClass->newInstance($options);
// $baseClass = new \ReflectionClass(UploaderController::class);
// $moduleIdProperty = $baseClass->getProperty('moduleId');
// $moduleIdProperty->setAccessible(true);
// $moduleIdProperty->setValue($controller, $moduleId);
//
// $nameProperty = $baseClass->getProperty('name');
// $nameProperty->setAccessible(true);
// $nameProperty->setValue($controller, $controllerName);
if (!$controller->isAvailable())
{
return null;
}
return $controller;
}
catch (\ReflectionException $exception)
{
$application = HttpApplication::getInstance();
$exceptionHandler = $application->getExceptionHandler();
$exceptionHandler->writeToLog($exception);
}
return null;
}
public static function resolveName(string $controllerName): array
{
$controllerName = trim($controllerName);
if (mb_strlen($controllerName) < 1)
{
return [null, null];
}
[$vendor, $controllerName] = self::resolveVendor($controllerName);
[$moduleId, $className] = self::resolveModuleAndClass($controllerName);
$moduleId = self::refineModuleName($vendor, $moduleId);
$className = self::buildClassName($vendor, $moduleId, $className);
return [$moduleId, $className];
}
public static function getNameByController(UploaderController $controller): string
{
$parts = explode('\\', get_class($controller));
$vendor = mb_strtolower(array_shift($parts));
$moduleId = mb_strtolower(array_shift($parts));
$parts = array_map(
function ($part) {
return lcfirst($part);
},
$parts
);
if ($vendor === self::DEFAULT_VENDOR)
{
return $moduleId . '.' . implode('.', $parts);
}
else
{
return $vendor . ':' . $moduleId . '.' . implode('.', $parts);
}
}
private static function buildClassName(string $vendor, string $moduleId, string $className): string
{
if ($vendor === self::DEFAULT_VENDOR)
{
$moduleId = ucfirst($moduleId);
$namespace = "\\Bitrix\\{$moduleId}";
}
else
{
$moduleParts = explode('.', $moduleId);
$moduleParts = array_map(
function ($part) {
return ucfirst(trim(trim($part), '\\'));
},
$moduleParts
);
$namespace = "\\" . join('\\', $moduleParts);
}
$classNameParts = explode('.', $className);
$classNameParts = array_map(
function ($part) {
return ucfirst(trim(trim($part), '\\'));
},
$classNameParts
);
if (!$classNameParts)
{
return $namespace;
}
return "{$namespace}\\" . join('\\', $classNameParts);
}
private static function resolveModuleAndClass(string $controllerName): array
{
$parts = explode('.', $controllerName);
$moduleId = array_shift($parts);
$className = implode('.', $parts);
return [$moduleId, $className];
}
private static function resolveVendor(string $controllerName): array
{
[$vendor, $controllerName] = explode(':', $controllerName) + [null, null];
if (!$controllerName)
{
$controllerName = $vendor;
$vendor = self::DEFAULT_VENDOR;
}
return [$vendor, $controllerName];
}
private static function refineModuleName($vendor, $moduleId): string
{
if ($vendor === self::DEFAULT_VENDOR)
{
return mb_strtolower($moduleId);
}
return mb_strtolower($vendor . '.' . $moduleId);
}
private static function canIncludeModule(string $moduleId): bool
{
$settings = \Bitrix\Main\Config\Configuration::getInstance($moduleId)->get('ui.uploader');
if (empty($settings) || !is_array($settings))
{
return false;
}
return isset($settings['allowUseControllers']) && $settings['allowUseControllers'] === true;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Bitrix\UI\FileUploader;
class FileData
{
protected string $name;
protected string $contentType = '';
protected int $size = 0;
protected int $width = 0;
protected int $height = 0;
public function __construct(string $name, string $contentType, int $size)
{
$this->name = $name;
$this->contentType = $contentType;
$this->size = $size;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getContentType(): string
{
return $this->contentType;
}
public function getSize(): int
{
return $this->size;
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): void
{
if ($width > 0)
{
$this->width = $width;
}
}
public function getHeight(): int
{
return $this->height;
}
public function setHeight(int $height): void
{
if ($height > 0)
{
$this->height = $height;
}
}
public function isImage(): bool
{
return \CFile::isImage($this->getName()) && $this->getWidth() > 0 && $this->getHeight() > 0;
}
public function isVideo(): bool
{
$extension = strtolower(getFileExtension($this->getName()));
return in_array($extension, Configuration::getVideoExtensions(withDot: false));
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Type\Dictionary;
class FileInfo extends FileData implements \JsonSerializable
{
protected $id;
protected int $fileId = 0;
protected bool $treatImageAsFile = false;
protected ?string $downloadUrl = null;
protected ?string $previewUrl = null;
protected int $previewWidth = 0;
protected int $previewHeight = 0;
protected ?Dictionary $customData = null;
protected ?Dictionary $viewerAttrs = null;
/**
* @param {string | int} $id
* @param string $name
* @param string $contentType
* @param int $size
*/
public function __construct($id, string $name, string $contentType, int $size)
{
parent::__construct($name, $contentType, $size);
$this->id = $id;
}
public static function createFromBFile(int $id): ?FileInfo
{
$file = \CFile::getFileArray($id);
if (!is_array($file))
{
return null;
}
$fileName = !empty($file['ORIGINAL_NAME']) ? $file['ORIGINAL_NAME'] : $file['FILE_NAME'];
$fileInfo = new static($id, $fileName, $file['CONTENT_TYPE'], (int)$file['FILE_SIZE']);
$fileInfo->setWidth($file['WIDTH']);
$fileInfo->setHeight($file['HEIGHT']);
$fileInfo->setFileId($id);
return $fileInfo;
}
public static function createFromTempFile(string $tempFileId): ?FileInfo
{
[$guid, $signature] = explode('.', $tempFileId);
$tempFile = TempFileTable::getList([
'filter' => [
'=GUID' => $guid,
'=UPLOADED' => true,
],
])->fetchObject();
if (!$tempFile)
{
return null;
}
$fileInfo = new static($tempFileId, $tempFile->getFilename(), $tempFile->getMimetype(), $tempFile->getSize());
$fileInfo->setWidth($tempFile->getWidth());
$fileInfo->setHeight($tempFile->getHeight());
$fileInfo->setFileId($tempFile->getFileId());
return $fileInfo;
}
/**
* @return int|string
*/
public function getId()
{
return $this->id;
}
public function setId($id): void
{
if (is_int($id) || is_string($id))
{
$this->id = $id;
}
}
public function getFileId(): int
{
return $this->fileId;
}
public function setFileId(int $fileId): void
{
$this->fileId = $fileId;
}
public function shouldTreatImageAsFile(): bool
{
return $this->treatImageAsFile;
}
public function setTreatImageAsFile(bool $flag): void
{
$this->treatImageAsFile = $flag;
}
public function getDownloadUrl(): ?string
{
return $this->downloadUrl;
}
public function setDownloadUrl(string $downloadUrl): void
{
$this->downloadUrl = $downloadUrl;
}
public function getPreviewUrl(): ?string
{
return $this->previewUrl;
}
public function setPreviewUrl(string $previewUrl, int $previewWidth, int $previewHeight): void
{
$this->previewUrl = $previewUrl;
$this->previewWidth = $previewWidth;
$this->previewHeight = $previewHeight;
}
public function getPreviewWidth(): int
{
return $this->previewWidth;
}
public function getPreviewHeight(): int
{
return $this->previewHeight;
}
public function setCustomData(array $customData): self
{
$this->getCustomData()->setValues($customData);
return $this;
}
/**
* @return Dictionary
*/
public function getCustomData(): Dictionary
{
if ($this->customData === null)
{
$this->customData = new Dictionary();
}
return $this->customData;
}
public function setViewerAttrs(array $viewerAttrs): self
{
$this->getViewerAttrs()->setValues($viewerAttrs);
return $this;
}
public function getViewerAttrs(): Dictionary
{
if ($this->viewerAttrs === null)
{
$this->viewerAttrs = new Dictionary();
}
return $this->viewerAttrs;
}
public function jsonSerialize(): array
{
return [
'serverFileId' => $this->getId(),
'serverId' => $this->getId(), // compatibility
'type' => $this->getContentType(),
'name' => $this->getName(),
'size' => $this->getSize(),
'width' => $this->getWidth(),
'height' => $this->getHeight(),
'isImage' => $this->isImage(),
'isVideo' => $this->isVideo(),
'treatImageAsFile' => $this->isImage() && $this->shouldTreatImageAsFile(),
'downloadUrl' => $this->getDownloadUrl(),
'serverPreviewUrl' => $this->getPreviewUrl(),
'serverPreviewWidth' => $this->getPreviewWidth(),
'serverPreviewHeight' => $this->getPreviewHeight(),
'customData' => $this->customData !== null ? $this->getCustomData()->getValues() : [],
'viewerAttrs' => $this->viewerAttrs !== null ? $this->getViewerAttrs()->getValues() : [],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Bitrix\UI\FileUploader;
class FileOwnership
{
private int $id;
private bool $own = false;
public function __construct(int $id)
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
}
public function isOwn(): bool
{
return $this->own;
}
public function markAsOwn(bool $flag = true): void
{
$this->own = $flag;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Bitrix\UI\FileUploader;
class FileOwnershipCollection implements \IteratorAggregate
{
/** @var FileOwnership[] */
private array $items = [];
public function __construct(array $ids)
{
foreach ($ids as $id)
{
$this->items[] = new FileOwnership($id);
}
}
/**
* @return FileOwnership[]
*/
public function getAll(): array
{
return $this->items;
}
public function count(): int
{
return count($this->items);
}
/**
* @return \ArrayIterator|FileOwnership[]
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Bitrix\UI\FileUploader;
class LoadResult extends \Bitrix\Main\Result implements \JsonSerializable
{
/** @var string|int */
protected $id;
protected ?FileInfo $file = null;
public function __construct($id)
{
$this->id = $id;
parent::__construct();
}
public function getId()
{
return $this->id;
}
public function getFile(): ?FileInfo
{
return $this->file;
}
public function setFile(FileInfo $file): void
{
$this->file = $file;
}
public function jsonSerialize(): array
{
return [
'id' => $this->getId(),
'errors' => $this->getErrors(),
'success' => $this->isSuccess(),
'data' => [
'file' => $this->getFile(),
],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Bitrix\UI\FileUploader;
class LoadResultCollection implements \IteratorAggregate, \JsonSerializable
{
/** @var LoadResult[] */
private array $results = [];
public function add(LoadResult $result): void
{
$this->results[] = $result;
}
/**
* @return LoadResult[]
*/
public function getAll(): array
{
return $this->results;
}
/**
* @return \ArrayIterator|LoadResult[]
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->results);
}
public function jsonSerialize(): array
{
return $this->getAll();
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Error;
use Bitrix\Main\ErrorCollection;
class PendingFile
{
private string $id;
private ?TempFile $tempFile = null;
private ErrorCollection $errors;
private string $status = PendingFileStatus::INIT;
public function __construct(string $id)
{
$this->id = $id;
$this->errors = new ErrorCollection();
}
public function getId(): string
{
return $this->id;
}
public function getGuid(): ?string
{
return $this->tempFile !== null ? $this->tempFile->getGuid() : null;
}
public function getFileId(): ?int
{
return $this->isValid() && $this->tempFile !== null ? $this->tempFile->getFileId() : null;
}
public function setTempFile(TempFile $tempFile): void
{
$this->status = PendingFileStatus::PENDING;
$this->tempFile = $tempFile;
}
protected function getTempFile(): ?TempFile
{
return $this->tempFile;
}
public function getStatus(): string
{
return $this->status;
}
public function makePersistent(): void
{
if ($this->getStatus() === PendingFileStatus::PENDING)
{
$this->getTempFile()->makePersistent();
$this->status = PendingFileStatus::COMMITTED;
}
}
public function remove(): void
{
if ($this->getStatus() === PendingFileStatus::PENDING)
{
$this->getTempFile()->delete();
$this->status = PendingFileStatus::REMOVED;
}
}
public function addError(Error $error): void
{
$this->status = PendingFileStatus::ERROR;
$this->errors[] = $error;
}
public function getErrors(): array
{
return $this->errors->toArray();
}
public function isValid(): bool
{
return (
$this->getStatus() === PendingFileStatus::PENDING
|| $this->getStatus() === PendingFileStatus::COMMITTED
);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Bitrix\UI\FileUploader;
class PendingFileCollection implements \IteratorAggregate
{
/**
* @var array<string, PendingFile>
*/
private array $files = [];
public function add(PendingFile $pendingFile): void
{
$this->files[$pendingFile->getId()] = $pendingFile;
}
public function get(string $tempFileId): ?PendingFile
{
return $this->files[$tempFileId] ?? null;
}
public function getByFileId(int $fileId): ?PendingFile
{
foreach ($this->files as $file)
{
if ($file->getFileId() === $fileId)
{
return $file;
}
}
return null;
}
/**
* @return int[]
*/
public function getFileIds(): array
{
$ids = [];
foreach ($this->files as $file)
{
$id = $file->getFileId();
if ($id !== null)
{
$ids[] = $id;
}
}
return $ids;
}
public function makePersistent(): void
{
foreach ($this->files as $file)
{
$file->makePersistent();
}
}
public function remove(): void
{
foreach ($this->files as $file)
{
$file->remove();
}
}
/**
* @return array<string, PendingFile>
*/
public function getAll(): array
{
return $this->files;
}
/**
* @return \ArrayIterator|array<string, PendingFile>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->files);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\UI\FileUploader;
class PendingFileStatus
{
const INIT = 'INIT';
const PENDING = 'PENDING';
const ERROR = 'ERROR';
const COMMITTED = 'COMMITTED';
const REMOVED = 'REMOVED';
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\File\Image;
use Bitrix\Main\File\Image\Rectangle;
class PreviewImage
{
public static function getSize(FileInfo $fileInfo, PreviewImageOptions $options = null): Rectangle
{
$previewWidth = 0;
$previewHeight = 0;
if ($fileInfo->isImage())
{
// Sync with \Bitrix\UI\Controller\FileUploader::previewAction
$previewWidth = $options ? $options->getWidth() : 300;
$previewHeight = $options ? $options->getHeight() : 300;
$previewMode = $options ? $options->getMode() : Image::RESIZE_PROPORTIONAL;
$destinationRectangle = new Rectangle($previewWidth, $previewHeight);
$sourceRectangle = new Rectangle($fileInfo->getWidth(), $fileInfo->getHeight());
$needResize = $sourceRectangle->resize($destinationRectangle, $previewMode);
if ($needResize)
{
$previewWidth = $destinationRectangle->getWidth();
$previewHeight = $destinationRectangle->getHeight();
}
else
{
$previewWidth = $sourceRectangle->getWidth();
$previewHeight = $sourceRectangle->getHeight();
}
}
return new Rectangle($previewWidth, $previewHeight);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\File\Image;
class PreviewImageOptions
{
protected int $width = 300;
protected int $height = 300;
protected int $mode = Image::RESIZE_PROPORTIONAL;
public function __construct(array $options = [])
{
$optionNames = [
'width',
'height',
'mode',
];
foreach ($optionNames as $optionName)
{
if (array_key_exists($optionName, $options))
{
$optionValue = $options[$optionName];
$setter = 'set' . ucfirst($optionName);
$this->$setter($optionValue);
}
}
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): self
{
$this->width = $width;
return $this;
}
public function getHeight(): int
{
return $this->height;
}
public function setHeight(int $height): self
{
$this->height = $height;
return $this;
}
public function getMode(): int
{
return $this->mode;
}
public function setMode(int $mode): self
{
$this->mode = $mode;
return $this;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Bitrix\UI\FileUploader;
class RemoveResult extends \Bitrix\Main\Result implements \JsonSerializable
{
/** @var string|int */
protected $id;
public function __construct($id)
{
$this->id = $id;
parent::__construct();
}
public function getId()
{
return $this->id;
}
public function jsonSerialize(): array
{
return [
'id' => $this->getId(),
'errors' => $this->getErrors(),
'success' => $this->isSuccess(),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Bitrix\UI\FileUploader;
class RemoveResultCollection implements \IteratorAggregate, \JsonSerializable
{
/** @var RemoveResult[] */
private array $results = [];
public function add(RemoveResult $result): void
{
$this->results[] = $result;
}
/**
* @return RemoveResult[]
*/
public function getAll(): array
{
return $this->results;
}
/**
* @return \ArrayIterator|RemoveResult[]
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->results);
}
public function jsonSerialize(): array
{
return $this->getAll();
}
}

View File

@@ -0,0 +1,471 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Loader;
use Bitrix\Main\Security;
use Bitrix\Main\IO;
use Bitrix\Main\Result;
final class TempFile extends EO_TempFile
{
private ?\CCloudStorageBucket $bucket = null;
public static function create(Chunk $chunk, UploaderController $controller): Result
{
$result = new Result();
$file = $chunk->getFile();
if (!$file->isExists())
{
return $result->addError(new UploaderError(UploaderError::CHUNK_NOT_FOUND));
}
if (mb_strpos($file->getPhysicalPath(), \CTempFile::getAbsoluteRoot()) !== 0)
{
// A chunk file could be saved in any folder.
// Copy it to the temporary directory. We need to normalize the absolute path.
$tempFilePath = self::generateLocalTempFile();
if (!copy($chunk->getFile()->getPhysicalPath(), $tempFilePath))
{
return $result->addError(new UploaderError(UploaderError::CHUNK_COPY_FAILED));
}
$newFile = new IO\File($tempFilePath);
if (!$newFile->isExists())
{
return $result->addError(new UploaderError(UploaderError::CHUNK_COPY_FAILED));
}
$chunk->setFile($newFile);
}
$tempFile = null;
if ($chunk->isOnlyOne())
{
// Cloud and local files are processed by CFile::SaveFile.
$tempFile = self::createTempFile($chunk, $controller);
}
else
{
// Multipart upload
$bucket = self::findBucketForFile($chunk, $controller);
if ($bucket)
{
// cloud file
$tempFile = self::createTempFile($chunk, $controller, $bucket);
$appendResult = $tempFile->appendToCloud($chunk);
if (!$appendResult->isSuccess())
{
$chunk->getFile()->delete();
$tempFile->delete();
return $result->addErrors($appendResult->getErrors());
}
}
else
{
// local file
$localTempDir = self::generateLocalTempDir();
if (!$file->rename($localTempDir))
{
return $result->addError(new UploaderError(UploaderError::FILE_MOVE_FAILED));
}
$tempFile = self::createTempFile($chunk, $controller);
}
}
$result->setData(['tempFile' => $tempFile]);
return $result;
}
protected static function createTempFile(Chunk $chunk, UploaderController $controller, $bucket = null): TempFile
{
$tempFile = new TempFile();
$tempFile->setFilename($chunk->getName());
$tempFile->setMimetype($chunk->getType());
$tempFile->setSize($chunk->getFileSize());
$tempFile->setReceivedSize($chunk->getSize());
$tempFile->setWidth($chunk->getWidth());
$tempFile->setHeight($chunk->getHeight());
$tempFile->setModuleId($controller->getModuleId());
$tempFile->setController($controller->getName());
if ($bucket)
{
$path = self::generateCloudTempDir($bucket);
}
else
{
$path = $chunk->getFile()->getPhysicalPath();
$tempRoot = \CTempFile::getAbsoluteRoot();
$path = mb_substr($path, mb_strlen($tempRoot));
}
$tempFile->setPath($path);
if ($bucket)
{
$tempFile->setCloud(true);
$tempFile->setBucketId($bucket->ID);
}
$tempFile->save();
return $tempFile;
}
public function append(Chunk $chunk): Result
{
$result = new Result();
if ($chunk->getEndRange() < $this->getReceivedSize())
{
// We already have this part of the file
return $result;
}
if ($this->getReceivedSize() !== $chunk->getStartRange())
{
return $result->addError(new UploaderError(UploaderError::INVALID_CHUNK_OFFSET));
}
if ($this->getReceivedSize() + $chunk->getSize() > $this->getSize())
{
return $result->addError(new UploaderError(UploaderError::CHUNK_TOO_BIG));
}
$result = $this->isCloud() ? $this->appendToCloud($chunk) : $this->appendToFile($chunk);
if ($result->isSuccess())
{
$this->increaseReceivedSize($chunk->getSize());
}
// Remove a temporary chunk file immediately
$chunk->getFile()->delete();
return $result;
}
public function commit(CommitOptions $commitOptions): Result
{
$fileAbsolutePath = $this->getAbsolutePath();
$fileId = \CFile::saveFile(
[
'name' => $this->getFilename(),
'tmp_name' => $fileAbsolutePath,
'type' => $this->getMimetype(),
'MODULE_ID' => $commitOptions->getModuleId(),
'width' => $this->getWidth(),
'height' => $this->getHeight(),
'size' => $this->getSize(),
],
$commitOptions->getSavePath(),
$commitOptions->isForceRandom(),
$commitOptions->isSkipExtension(),
$commitOptions->getAddDirectory()
);
$result = new Result();
if (!$fileId)
{
$this->delete();
return $result->addError(new UploaderError(UploaderError::SAVE_FILE_FAILED));
}
$this->setFileId($fileId);
$this->setUploaded(true);
$this->save();
$this->fillFile();
$this->removeActualTempFile();
return $result;
}
public function isCloud(): bool
{
return $this->getCloud() && $this->getBucketId() > 0;
}
public function makePersistent(): void
{
$this->customData->set('deleteBFile', false);
$this->delete();
}
public function deleteContent($deleteBFile = true): void
{
$this->removeActualTempFile();
if ($deleteBFile)
{
\CFile::delete($this->getFileId());
}
}
private function removeActualTempFile(): bool
{
if ($this->getDeleted())
{
return true;
}
$success = false;
if ($this->isCloud())
{
$bucket = $this->getBucket();
if ($bucket)
{
$success = $bucket->deleteFile($this->getPath());
}
}
else
{
$success = IO\File::deleteFile($this->getAbsolutePath());
}
if ($success)
{
$this->setDeleted(true);
$this->save();
}
return $success;
}
private function getAbsoluteCloudPath(): ?string
{
$bucket = $this->getBucket();
if (!$bucket)
{
return null;
}
return $bucket->getFileSRC($this->getPath());
}
private function getAbsoluteLocalPath(): string
{
return \CTempFile::getAbsoluteRoot() . $this->getPath();
}
private function getAbsolutePath(): ?string
{
if ($this->isCloud())
{
return $this->getAbsoluteCloudPath();
}
return $this->getAbsoluteLocalPath();
}
private function appendToFile(Chunk $chunk): Result
{
$result = new Result();
$file = new IO\File($this->getAbsoluteLocalPath());
if ($chunk->isFirst() || !$file->isExists())
{
return $result->addError(new UploaderError(UploaderError::FILE_APPEND_NOT_FOUND));
}
if ($chunk->getEndRange() < $file->getSize())
{
// We already have this part of the file
return $result;
}
if (!$chunk->getFile()->isExists())
{
return $result->addError(new UploaderError(UploaderError::CHUNK_APPEND_NOT_FOUND));
}
if ($file->putContents($chunk->getFile()->getContents(), IO\File::APPEND) === false)
{
return $result->addError(new UploaderError(UploaderError::CHUNK_APPEND_FAILED));
}
return $result;
}
private function appendToCloud(Chunk $chunk): Result
{
$result = new Result();
$bucket = $this->getBucket();
if (!$bucket)
{
return $result->addError(new UploaderError(UploaderError::CLOUD_EMPTY_BUCKET));
}
$minUploadSize = $bucket->getService()->getMinUploadPartSize();
if ($chunk->getSize() < $minUploadSize && !$chunk->isLast())
{
$postMaxSize = \CUtil::unformat(ini_get('post_max_size'));
$uploadMaxFileSize = \CUtil::unformat(ini_get('upload_max_filesize'));
return $result->addError(
new UploaderError(
UploaderError::CLOUD_INVALID_CHUNK_SIZE,
[
'chunkSize' => $chunk->getSize(),
'minUploadSize' => $minUploadSize,
'postMaxSize' => $postMaxSize,
'uploadMaxFileSize' => $uploadMaxFileSize,
]
)
);
}
$cloudUpload = new \CCloudStorageUpload($this->getPath());
if (!$cloudUpload->isStarted() && !$cloudUpload->start($bucket->ID, $chunk->getFileSize(), $chunk->getType()))
{
return $result->addError(new UploaderError(UploaderError::CLOUD_START_UPLOAD_FAILED));
}
if ($cloudUpload->getPos() === doubleval($chunk->getEndRange() + 1))
{
// We already have this part of the file.
if ($chunk->isLast() && !$cloudUpload->finish())
{
return $result->addError(new UploaderError(UploaderError::CLOUD_FINISH_UPLOAD_FAILED));
}
return $result;
}
$fileContent = $chunk->getFile()->isExists() ? $chunk->getFile()->getContents() : false;
if ($fileContent === false)
{
return $result->addError(new UploaderError(UploaderError::CLOUD_GET_CONTENTS_FAILED));
}
$fails = 0;
$success = false;
while ($cloudUpload->hasRetries())
{
if ($cloudUpload->next($fileContent))
{
$success = true;
break;
}
$fails++;
}
if (!$success)
{
// TODO: CCloudStorageUpload::CleanUp
return $result->addError(new UploaderError(UploaderError::CLOUD_UPLOAD_FAILED, ['fails' => $fails]));
}
if ($chunk->isLast() && !$cloudUpload->finish())
{
// TODO: CCloudStorageUpload::CleanUp
return $result->addError(new UploaderError(UploaderError::CLOUD_FINISH_UPLOAD_FAILED));
}
return $result;
}
private function increaseReceivedSize(int $bytes): void
{
$receivedSize = $this->getReceivedSize();
$this->setReceivedSize($receivedSize + $bytes);
$this->save();
}
private static function findBucketForFile(Chunk $chunk, UploaderController $controller): ?\CCloudStorageBucket
{
if (!Loader::includeModule('clouds'))
{
return null;
}
$bucket = \CCloudStorage::findBucketForFile(
[
'FILE_SIZE' => $chunk->getFileSize(),
'MODULE_ID' => $controller->getCommitOptions()->getModuleId(),
],
$chunk->getName()
);
if (!$bucket || !$bucket->init())
{
return null;
}
return $bucket;
}
public static function generateLocalTempDir(int $hoursToKeepFile = 12): string
{
$directory = \CTempFile::getDirectoryName(
$hoursToKeepFile,
[
'file-uploader',
Security\Random::getString(32),
]
);
if (!IO\Directory::isDirectoryExists($directory))
{
IO\Directory::createDirectory($directory);
}
$tempName = md5(mt_rand() . mt_rand());
return $directory . $tempName;
}
public static function generateLocalTempFile(): string
{
$tmpFilePath = \CTempFile::getFileName('file-uploader' . uniqid(md5(mt_rand() . mt_rand()), true));
$directory = IO\Path::getDirectory($tmpFilePath);
if (!IO\Directory::isDirectoryExists($directory))
{
IO\Directory::createDirectory($directory);
}
return $tmpFilePath;
}
public static function generateCloudTempDir(\CCloudStorageBucket $bucket, int $hoursToKeepFile = 12): string
{
$directory = \CCloudTempFile::getDirectoryName(
$bucket,
$hoursToKeepFile,
[
'file-uploader',
Security\Random::getString(32),
]
);
$tempName = md5(mt_rand() . mt_rand());
return $directory . $tempName;
}
private function getBucket(): ?\CCloudStorageBucket
{
if ($this->bucket !== null)
{
return $this->bucket;
}
if (!$this->getBucketId() || !Loader::includeModule('clouds'))
{
return null;
}
$bucket = new \CCloudStorageBucket($this->getBucketId());
if ($bucket->init())
{
$this->bucket = $bucket;
}
return $this->bucket;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Type\DateTime;
class TempFileAgent
{
const AGENT_MAX_INTERVAL = 1800;
const AGENT_MIN_INTERVAL = 360;
public static function clearOldRecords(): string
{
$expired = new DateTime();
$expired->add('-1 days');
$limit = 20;
$tempFiles = TempFileTable::getList([
'filter' => ['<CREATED_AT' => $expired->toString()],
'limit' => $limit,
'order' => ['CREATED_AT' => 'ASC']
])->fetchCollection();
foreach ($tempFiles as $tempFile)
{
$tempFile->delete();
}
$agentName = '\\' . __METHOD__ . '();';
$agents = \CAgent::getList(['ID' => 'DESC'], [
'MODULE_ID' => 'ui',
'NAME' => $agentName,
]);
if ($agent = $agents->fetch())
{
$interval = $tempFiles->count() < $limit ? static::AGENT_MAX_INTERVAL : static::AGENT_MIN_INTERVAL;
if ((int)$agent['AGENT_INTERVAL'] !== $interval)
{
\CAgent::update($agent['ID'], ['AGENT_INTERVAL' => $interval]);
}
}
return $agentName;
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\DB\SqlExpression;
use Bitrix\Main\ORM\Data;
use Bitrix\Main\ORM\Event;
use Bitrix\Main\ORM\Fields;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\Type\DateTime;
/**
* Class TempFileTable
*
* DO NOT WRITE ANYTHING BELOW THIS
*
* <<< ORMENTITYANNOTATION
* @method static EO_TempFile_Query query()
* @method static EO_TempFile_Result getByPrimary($primary, array $parameters = [])
* @method static EO_TempFile_Result getById($id)
* @method static EO_TempFile_Result getList(array $parameters = [])
* @method static EO_TempFile_Entity getEntity()
* @method static \Bitrix\UI\FileUploader\TempFile createObject($setDefaultValues = true)
* @method static \Bitrix\UI\FileUploader\EO_TempFile_Collection createCollection()
* @method static \Bitrix\UI\FileUploader\TempFile wakeUpObject($row)
* @method static \Bitrix\UI\FileUploader\EO_TempFile_Collection wakeUpCollection($rows)
*/
class TempFileTable extends Data\DataManager
{
public static function getTableName()
{
return 'b_ui_file_uploader_temp_file';
}
public static function getObjectClass()
{
return TempFile::class;
}
public static function getMap()
{
return [
(new Fields\IntegerField('ID'))
->configurePrimary()
->configureAutocomplete()
,
(new Fields\StringField("GUID"))
->configureUnique(true)
->configureNullable(false)
->configureDefaultValue(static function () {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
})
->configureSize(36)
,
new Fields\IntegerField('FILE_ID'),
(new Fields\StringField('FILENAME'))
->configureRequired()
->configureSize(255)
,
(new Fields\IntegerField('SIZE'))
->configureRequired()
->configureSize(8)
,
(new Fields\StringField('PATH'))
->configureRequired()
->configureSize(255)
,
(new Fields\StringField('MIMETYPE'))
->configureRequired()
->configureSize(255)
,
(new Fields\IntegerField('RECEIVED_SIZE'))
->configureSize(8)
,
new Fields\IntegerField('WIDTH'),
new Fields\IntegerField('HEIGHT'),
new Fields\IntegerField('BUCKET_ID'),
(new Fields\StringField('MODULE_ID'))
->configureRequired()
->configureSize(50)
,
(new Fields\StringField('CONTROLLER'))
->configureRequired()
->configureSize(255)
,
(new Fields\BooleanField('CLOUD'))
->configureValues(0, 1)
->configureDefaultValue(0)
,
(new Fields\BooleanField('UPLOADED'))
->configureValues(0, 1)
->configureDefaultValue(0)
,
(new Fields\BooleanField('DELETED'))
->configureValues(0, 1)
->configureDefaultValue(0)
,
(new Fields\IntegerField('CREATED_BY'))
->configureRequired()
->configureDefaultValue(static function () {
global $USER;
if (is_object($USER) && method_exists($USER, 'getId'))
{
return (int)$USER->getId();
}
return 0;
})
,
(new Fields\DatetimeField('CREATED_AT'))
->configureDefaultValue(static function () {
return new DateTime();
})
,
(new Reference(
'FILE',
\Bitrix\Main\FileTable::class,
Join::on('this.FILE_ID', 'ref.ID'),
['join_type' => Join::TYPE_INNER]
)),
];
}
public static function onDelete(Event $event)
{
$tempFile = $event->getParameter('object');
if (!$tempFile)
{
$id = $event->getParameter('primary')['ID'];
$tempFile = self::getById($id)->fetchObject();
}
if ($tempFile)
{
$tempFile->fill();
$deleteBFile = $tempFile->customData->get('deleteBFile') !== false;
$tempFile->deleteContent($deleteBFile);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Bitrix\UI\FileUploader;
class UploadRequest
{
protected string $name;
protected string $contentType = '';
protected int $size = 0;
protected int $width = 0;
protected int $height = 0;
public function __construct(string $name, string $contentType, int $size)
{
$this->name = $name;
$this->contentType = $contentType;
$this->size = $size;
}
public function getName(): string
{
return $this->name;
}
public function getContentType(): string
{
return $this->contentType;
}
public function getSize(): int
{
return $this->size;
}
public function getWidth(): int
{
return $this->width;
}
public function setWidth(int $width): void
{
$this->width = $width;
}
public function getHeight(): int
{
return $this->height;
}
public function setHeight(int $height): void
{
$this->height = $height;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Error;
class UploadResult extends \Bitrix\Main\Result implements \JsonSerializable
{
protected ?TempFile $tempFile = null;
protected ?FileInfo $file = null;
protected ?string $token = null;
protected bool $done = false;
public static function reject(Error $error): self
{
$result = new static();
$result->addError($error);
return $result;
}
/**
* The Uploader retries an upload request if a server returns a non-uploader error.
* That's why we need to convert an error from Error to UploaderError.
*/
public function addError(Error $error)
{
if ($error instanceof UploaderError)
{
return parent::addError($error);
}
else
{
return parent::addError(new UploaderError(
$error->getCode(),
$error->getMessage(),
$error->getCustomData()
));
}
}
public function addErrors(array $errors)
{
foreach ($errors as $error)
{
$this->addError($error);
}
return $this;
}
public function getTempFile(): ?TempFile
{
return $this->tempFile;
}
public function setTempFile(TempFile $tempFile): void
{
$this->tempFile = $tempFile;
}
public function getFileInfo(): ?FileInfo
{
return $this->file;
}
public function setFileInfo(?FileInfo $file): void
{
$this->file = $file;
}
public function getToken(): ?string
{
return $this->token;
}
public function setToken(string $token)
{
$this->token = $token;
}
public function setDone(bool $done): void
{
$this->done = $done;
}
public function isDone(): bool
{
return $this->done;
}
public function jsonSerialize(): array
{
return [
'token' => $this->getToken(),
'done' => $this->isDone(),
'file' => $this->getFileInfo(),
];
}
}

View File

@@ -0,0 +1,536 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\ORM\Objectify\State;
use Bitrix\Main\Security\Sign\Signer;
use Bitrix\Main\UI\Viewer\ItemAttributes;
use Bitrix\UI\FileUploader\Contracts\CustomFingerprint;
use Bitrix\UI\FileUploader\Contracts\CustomLoad;
use Bitrix\UI\FileUploader\Contracts\CustomRemove;
class Uploader
{
protected UploaderController $controller;
public function __construct(UploaderController $controller)
{
$this->controller = $controller;
}
public function getController(): UploaderController
{
return $this->controller;
}
public function upload(Chunk $chunk, string $token = null): UploadResult
{
$controller = $this->getController();
$uploadResult = new UploadResult();
if ($chunk->isFirst())
{
// Common file validation (uses in CFile::SaveFile)
$commitOptions = $controller->getCommitOptions();
$error = \CFile::checkFile(
[
'name' => $chunk->getName(),
'size' => $chunk->getFileSize(),
'type' => $chunk->getType()
],
0,
false,
false,
$commitOptions->isForceRandom(),
$commitOptions->isSkipExtension()
);
if ($error !== '')
{
return $this->handleUploadError(
$uploadResult->addError(new UploaderError('CHECK_FILE_FAILED', $error)),
$controller
);
}
// Controller Validation
$validationResult = $chunk->validate($controller->getConfiguration());
if (!$validationResult->isSuccess())
{
return $this->handleUploadError($uploadResult->addErrors($validationResult->getErrors()), $controller);
}
['width' => $width, 'height' => $height] = $validationResult->getData();
$chunk->setWidth((int)$width);
$chunk->setHeight((int)$height);
$uploadRequest = new UploadRequest($chunk->getName(), $chunk->getType(), $chunk->getSize());
$uploadRequest->setWidth($chunk->getWidth());
$uploadRequest->setHeight($chunk->getHeight());
// Temporary call for compatibility
// $canUploadResult = $controller->canUpload($uploadRequest);
$canUploadResult = call_user_func([$controller, 'canUpload'], $uploadRequest);
if (($canUploadResult instanceof CanUploadResult) && !$canUploadResult->isSuccess())
{
return $this->handleUploadError($uploadResult->addErrors($canUploadResult->getErrors()), $controller);
}
else if (!is_bool($canUploadResult) || $canUploadResult === false)
{
return $this->handleUploadError(
$uploadResult->addError(new UploaderError(UploaderError::FILE_UPLOAD_ACCESS_DENIED)),
$controller
);
}
$createResult = TempFile::create($chunk, $controller);
if (!$createResult->isSuccess())
{
return $this->handleUploadError($uploadResult->addErrors($createResult->getErrors()), $controller);
}
/** @var TempFile $tempFile */
$tempFile = $createResult->getData()['tempFile'];
$uploadResult->setTempFile($tempFile);
$uploadResult->setToken($this->generateToken($tempFile));
$controller->onUploadStart($uploadResult);
if (!$uploadResult->isSuccess())
{
return $this->handleUploadError($uploadResult, $controller);
}
}
else
{
if (empty($token))
{
return $this->handleUploadError(
$uploadResult->addError(new UploaderError(UploaderError::EMPTY_TOKEN)),
$controller
);
}
$guid = $this->getGuidFromToken($token);
if (!$guid)
{
return $this->handleUploadError(
$uploadResult->addError(new UploaderError(UploaderError::INVALID_SIGNATURE)),
$controller
);
}
$tempFile = TempFileTable::getList([
'filter' => [
'=GUID' => $guid,
'=UPLOADED' => false,
],
])->fetchObject();
if (!$tempFile)
{
return $this->handleUploadError(
$uploadResult->addError(new UploaderError(UploaderError::UNKNOWN_TOKEN)),
$controller
);
}
$uploadResult->setTempFile($tempFile);
$uploadResult->setToken($token);
$appendResult = $tempFile->append($chunk);
if (!$appendResult->isSuccess())
{
return $this->handleUploadError($uploadResult->addErrors($appendResult->getErrors()), $controller);
}
}
if ($uploadResult->isSuccess() && $chunk->isLast())
{
$commitResult = $tempFile->commit($controller->getCommitOptions());
if (!$commitResult->isSuccess())
{
return $this->handleUploadError($uploadResult->addErrors($commitResult->getErrors()), $controller);
}
$fileInfo = $this->createFileInfo($uploadResult->getToken());
$uploadResult->setFileInfo($fileInfo);
$uploadResult->setDone(true);
$controller->onUploadComplete($uploadResult);
if (!$uploadResult->isSuccess())
{
return $this->handleUploadError($uploadResult, $controller);
}
}
return $uploadResult;
}
private function handleUploadError(UploadResult $uploadResult, UploaderController $controller): UploadResult
{
$controller->onUploadError($uploadResult);
if (!$uploadResult->isSuccess())
{
$tempFile = $uploadResult->getTempFile();
if ($tempFile !== null && $tempFile->state !== State::DELETED)
{
$tempFile->delete();
}
}
return $uploadResult;
}
public function generateToken(TempFile $tempFile): string
{
$guid = $tempFile->getGuid();
$salt = $this->getTokenSalt([$guid]);
$signer = new Signer();
return $signer->sign($guid, $salt);
}
private function getGuidFromToken(string $token): ?string
{
$parts = explode('.', $token, 2);
if (count($parts) !== 2)
{
return null;
}
[$guid, $signature] = $parts;
if (empty($guid) || empty($signature))
{
return null;
}
$salt = $this->getTokenSalt([$guid]);
$signer = new Signer();
if (!$signer->validate($guid, $signature, $salt))
{
return null;
}
return $guid;
}
private function getTokenSalt($params = []): string
{
$controller = $this->getController();
$options = $controller->getOptions();
ksort($options);
$fingerprint =
$controller instanceof CustomFingerprint
? $controller->getFingerprint()
: (string)\bitrix_sessid()
;
return md5(serialize(
array_merge(
$params,
[
$controller->getName(),
$options,
$fingerprint,
]
)
));
}
public function load(array $ids): LoadResultCollection
{
$controller = $this->getController();
if ($controller instanceof CustomLoad)
{
return $controller->load($ids);
}
$results = new LoadResultCollection();
[$bfileIds, $tempFileIds] = $this->splitIds($ids);
$fileOwnerships = new FileOwnershipCollection($bfileIds);
// Files from b_file
if ($fileOwnerships->count() > 0)
{
$controller = $this->getController();
if ($controller->canView())
{
$controller->verifyFileOwner($fileOwnerships);
}
foreach ($fileOwnerships as $fileOwnership)
{
if ($fileOwnership->isOwn())
{
$loadResult = $this->loadFile($fileOwnership->getId());
}
else
{
$loadResult = new LoadResult($fileOwnership->getId());
$loadResult->addError(new UploaderError(UploaderError::FILE_LOAD_ACCESS_DENIED));
}
$results->add($loadResult);
}
}
// Temp Files
if (count($tempFileIds) > 0)
{
foreach ($tempFileIds as $tempFileId)
{
$loadResult = $this->loadTempFile($tempFileId);
$results->add($loadResult);
}
}
return $results;
}
public function getFileInfo(array $ids): array
{
$result = [];
$loadResults = $this->load(array_unique($ids));
foreach ($loadResults as $loadResult)
{
if ($loadResult->isSuccess() && $loadResult->getFile() !== null)
{
$result[] = $loadResult->getFile()->jsonSerialize();
}
}
return $result;
}
public function remove(array $ids): RemoveResultCollection
{
$controller = $this->getController();
if ($controller instanceof CustomRemove)
{
return $controller->remove($ids);
}
$results = new RemoveResultCollection();
[$bfileIds, $tempFileIds] = $this->splitIds($ids);
// Files from b_file
if (count($bfileIds) > 0)
{
$fileOwnerships = new FileOwnershipCollection($bfileIds);
if ($controller->canRemove())
{
$controller->verifyFileOwner($fileOwnerships);
}
foreach ($fileOwnerships as $fileOwnership)
{
$removeResult = new RemoveResult($fileOwnership->getId());
if ($fileOwnership->isOwn())
{
// TODO: remove file
}
else
{
$removeResult->addError(new UploaderError(UploaderError::FILE_REMOVE_ACCESS_DENIED));
}
$results->add($removeResult);
}
}
// Temp Files
if (count($tempFileIds) > 0)
{
foreach ($tempFileIds as $tempFileId)
{
$removeResult = new RemoveResult($tempFileId);
$results->add($removeResult);
$guid = $this->getGuidFromToken($tempFileId);
if (!$guid)
{
$removeResult->addError(new UploaderError(UploaderError::INVALID_SIGNATURE));
continue;
}
$tempFile = TempFileTable::getList([
'filter' => [
'=GUID' => $guid,
],
])->fetchObject();
if ($tempFile)
{
$tempFile->delete();
}
}
}
return $results;
}
public function getPendingFiles(array $tempFileIds): PendingFileCollection
{
$pendingFiles = new PendingFileCollection();
foreach ($tempFileIds as $tempFileId)
{
if (!is_string($tempFileId) || empty($tempFileId))
{
continue;
}
$pendingFile = new PendingFile($tempFileId);
$pendingFiles->add($pendingFile);
$guid = $this->getGuidFromToken($tempFileId);
if (!$guid)
{
$pendingFile->addError(new UploaderError(UploaderError::INVALID_SIGNATURE));
continue;
}
$tempFile = TempFileTable::getList([
'filter' => [
'=GUID' => $guid,
'=UPLOADED' => true,
],
])->fetchObject();
if (!$tempFile)
{
$pendingFile->addError(new UploaderError(UploaderError::UNKNOWN_TOKEN));
continue;
}
$pendingFile->setTempFile($tempFile);
}
return $pendingFiles;
}
private function loadFile(int $fileId): LoadResult
{
$result = new LoadResult($fileId);
if ($fileId < 1)
{
return $result->addError(new UploaderError(UploaderError::FILE_LOAD_FAILED));
}
$fileInfo = $this->createFileInfo($fileId);
if ($fileInfo)
{
$result->setFile($fileInfo);
}
else
{
return $result->addError(new UploaderError(UploaderError::FILE_LOAD_FAILED));
}
return $result;
}
private function loadTempFile(string $tempFileId): LoadResult
{
$result = new LoadResult($tempFileId);
$guid = $this->getGuidFromToken($tempFileId);
if (!$guid)
{
return $result->addError(new UploaderError(UploaderError::INVALID_SIGNATURE));
}
$tempFile = TempFileTable::getList([
'filter' => [
'=GUID' => $guid,
'=UPLOADED' => true,
],
])->fetchObject();
if (!$tempFile)
{
return $result->addError(new UploaderError(UploaderError::UNKNOWN_TOKEN));
}
$fileInfo = $this->createFileInfo($tempFileId);
if ($fileInfo)
{
$result->setFile($fileInfo);
}
else
{
return $result->addError(new UploaderError(UploaderError::FILE_LOAD_FAILED));
}
return $result;
}
private function createFileInfo($fileId): ?FileInfo
{
$fileInfo = is_int($fileId) ? FileInfo::createFromBFile($fileId) : FileInfo::createFromTempFile($fileId);
if ($fileInfo)
{
$downloadUrl = (string)UrlManager::getDownloadUrl($this->getController(), $fileInfo);
$fileInfo->setDownloadUrl($downloadUrl);
$fileInfo->setViewerAttrs($this->prepareViewerAttrs($fileInfo, $downloadUrl));
if ($fileInfo->isImage())
{
$config = $this->getController()->getConfiguration();
if ($config->shouldTreatOversizeImageAsFile())
{
$treatImageAsFile = $config->shouldTreatImageAsFile($fileInfo);
$fileInfo->setTreatImageAsFile($treatImageAsFile);
}
if (!$fileInfo->shouldTreatImageAsFile())
{
$rectangle = PreviewImage::getSize($fileInfo);
$previewUrl = (string)UrlManager::getPreviewUrl($this->getController(), $fileInfo);
$fileInfo->setPreviewUrl($previewUrl, $rectangle->getWidth(), $rectangle->getHeight());
}
}
}
return $fileInfo;
}
private function prepareViewerAttrs(FileInfo $fileInfo, string $downloadUrl): array
{
$fileData = [
'ID' => $fileInfo->getId(),
'CONTENT_TYPE' => $fileInfo->getContentType(),
'ORIGINAL_NAME' => $fileInfo->getName(),
'WIDTH' => $fileInfo->getWidth(),
'HEIGHT' => $fileInfo->getHeight(),
'FILE_SIZE' => $fileInfo->getSize(),
];
return ItemAttributes::buildByFileData($fileData, $downloadUrl)
->setTitle($fileInfo->getName())
->toDataSet()
;
}
private function splitIds(array $ids): array
{
$fileIds = [];
$tempFileIds = [];
foreach ($ids as $id)
{
if (is_numeric($id))
{
$fileIds[] = (int)$id;
}
else
{
$tempFileIds[] = (string)$id;
}
}
return [$fileIds, $tempFileIds];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Bitrix\UI\FileUploader;
abstract class UploaderController
{
protected array $options = [];
private string $moduleId;
private string $name;
private ?string $filePath = null;
protected function __construct(array $options)
{
// You have to validate $options in a derived class constructor
$this->options = $options;
$this->moduleId = getModuleId($this->getFilePath());
$this->name = ControllerResolver::getNameByController($this);
}
abstract public function isAvailable(): bool;
abstract public function getConfiguration(): Configuration;
/**
* @param UploadRequest $uploadRequest
*
* @return bool | CanUploadResult
*/
abstract public function canUpload();
abstract public function canView(): bool;
abstract public function verifyFileOwner(FileOwnershipCollection $files): void;
abstract public function canRemove(): bool;
// Events
public function onUploadStart(UploadResult $uploadResult): void {}
public function onUploadComplete(UploadResult $uploadResult): void {}
public function onUploadError(UploadResult $uploadResult): void {}
public function getCommitOptions(): CommitOptions
{
// Default commit options
return new CommitOptions([
'moduleId' => $this->getModuleId(),
'savePath' => $this->getModuleId(),
]);
}
final public function getOptions(): array
{
return $this->options;
}
final public function getOption(string $option, $defaultValue = null)
{
return array_key_exists($option, $this->options) ? $this->options[$option] : $defaultValue;
}
final public function getName(): string
{
return $this->name;
}
final public function getModuleId(): string
{
return $this->moduleId;
}
final protected function getFilePath(): string
{
if (!$this->filePath)
{
$reflector = new \ReflectionClass($this);
$this->filePath = preg_replace('#[\\\/]+#', '/', $reflector->getFileName());
}
return $this->filePath;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Localization\Loc;
Loc::loadLanguageFile(__DIR__ . '/UserErrors.php');
class UploaderError extends \Bitrix\Main\Error
{
protected string $description = '';
protected bool $system = false;
public const MAX_FILE_SIZE_EXCEEDED = 'MAX_FILE_SIZE_EXCEEDED';
public const MIN_FILE_SIZE_EXCEEDED = 'MIN_FILE_SIZE_EXCEEDED';
public const FILE_TYPE_NOT_ALLOWED = 'FILE_TYPE_NOT_ALLOWED';
public const FILE_NAME_NOT_ALLOWED = 'FILE_NAME_NOT_ALLOWED';
public const IMAGE_MAX_FILE_SIZE_EXCEEDED = 'IMAGE_MAX_FILE_SIZE_EXCEEDED';
public const IMAGE_MIN_FILE_SIZE_EXCEEDED = 'IMAGE_MIN_FILE_SIZE_EXCEEDED';
public const IMAGE_TYPE_NOT_SUPPORTED = 'IMAGE_TYPE_NOT_SUPPORTED';
public const IMAGE_IS_TOO_SMALL = 'IMAGE_IS_TOO_SMALL';
public const IMAGE_IS_TOO_BIG = 'IMAGE_IS_TOO_BIG';
public const SAVE_FILE_FAILED = 'SAVE_FILE_FAILED';
public const FILE_LOAD_FAILED = 'FILE_LOAD_FAILED';
public const FILE_UPLOAD_ACCESS_DENIED = 'FILE_UPLOAD_ACCESS_DENIED';
public const FILE_LOAD_ACCESS_DENIED = 'FILE_LOAD_ACCESS_DENIED';
public const FILE_REMOVE_ACCESS_DENIED = 'FILE_REMOVE_ACCESS_DENIED';
public const INVALID_CONTENT_RANGE = 'INVALID_CONTENT_RANGE';
public const INVALID_CONTENT_TYPE = 'INVALID_CONTENT_TYPE';
public const INVALID_CONTENT_LENGTH = 'INVALID_CONTENT_LENGTH';
public const INVALID_CONTENT_NAME = 'INVALID_CONTENT_NAME';
public const INVALID_FILENAME = 'INVALID_FILENAME';
public const INVALID_RANGE_SIZE = 'INVALID_RANGE_SIZE';
public const INVALID_CHUNK_SIZE = 'INVALID_CHUNK_SIZE';
public const INVALID_CHUNK_OFFSET = 'INVALID_CHUNK_OFFSET';
public const TOO_BIG_REQUEST = 'TOO_BIG_REQUEST';
public const FILE_FIND_FAILED = 'FILE_FIND_FAILED';
public const FILE_MOVE_FAILED = 'FILE_MOVE_FAILED';
public const FILE_APPEND_NOT_FOUND = 'FILE_APPEND_NOT_FOUND';
public const CHUNK_NOT_FOUND = 'CHUNK_NOT_FOUND';
public const CHUNK_COPY_FAILED = 'CHUNK_COPY_FAILED';
public const CHUNK_TOO_BIG = 'CHUNK_TOO_BIG';
public const CHUNK_APPEND_NOT_FOUND = 'CHUNK_APPEND_NOT_FOUND';
public const CHUNK_APPEND_FAILED = 'CHUNK_APPEND_FAILED';
public const CLOUD_EMPTY_BUCKET = 'CLOUD_EMPTY_BUCKET';
public const CLOUD_INVALID_CHUNK_SIZE = 'CLOUD_INVALID_CHUNK_SIZE';
public const CLOUD_GET_CONTENTS_FAILED = 'CLOUD_GET_CONTENTS_FAILED';
public const CLOUD_START_UPLOAD_FAILED = 'CLOUD_START_UPLOAD_FAILED';
public const CLOUD_FINISH_UPLOAD_FAILED = 'CLOUD_FINISH_UPLOAD_FAILED';
public const CLOUD_UPLOAD_FAILED = 'CLOUD_UPLOAD_FAILED';
public const EMPTY_TOKEN = 'EMPTY_TOKEN';
public const UNKNOWN_TOKEN = 'UNKNOWN_TOKEN';
public const INVALID_SIGNATURE = 'INVALID_SIGNATURE';
private static array $systemErrors = [
self::INVALID_CONTENT_RANGE => 'Content-Range header is invalid',
self::INVALID_CONTENT_TYPE => 'Content-Type header is required.',
self::INVALID_CONTENT_LENGTH => 'Content-Length header is required.',
self::INVALID_CONTENT_NAME => 'X-Upload-Content-Name header is required.',
self::INVALID_FILENAME => 'Filename is invalid.',
self::INVALID_RANGE_SIZE => 'Range chunk file size (#rangeChunkSize#) is not equal Content-Length (#contentLength#).',
self::INVALID_CHUNK_SIZE => 'Chunk file size (#chunkSize#) is not equal Content-Length (#contentLength#).',
self::INVALID_CHUNK_OFFSET => 'Chunk offset is invalid.',
self::TOO_BIG_REQUEST => 'The content length is too big to process the request.',
self::FILE_FIND_FAILED => 'Could not find a file.',
self::FILE_MOVE_FAILED => 'Could not move file.',
self::FILE_APPEND_NOT_FOUND => 'File not found.',
self::CHUNK_NOT_FOUND => 'Could not find chunk file.',
self::CHUNK_COPY_FAILED => 'Could not copy chunk file.',
self::CHUNK_TOO_BIG => 'You cannot upload a chunk more than the file size.',
self::CHUNK_APPEND_NOT_FOUND => 'Could not find chunk.',
self::CHUNK_APPEND_FAILED => 'Could not put contents to file.',
self::CLOUD_EMPTY_BUCKET => 'Could not get the cloud bucket.',
self::CLOUD_INVALID_CHUNK_SIZE => 'Cannot upload file to cloud. The size of the chunk (#chunkSize#) must be more than #minUploadSize#. Check "post_max_size" (#postMaxSize#) and "upload_max_filesize" (#uploadMaxFileSize#) options in php.ini.',
self::CLOUD_GET_CONTENTS_FAILED => 'Could not get file contents.',
self::CLOUD_START_UPLOAD_FAILED => 'Could not start cloud upload.',
self::CLOUD_FINISH_UPLOAD_FAILED => 'Could not finish cloud upload.',
self::CLOUD_UPLOAD_FAILED => 'Could not upload file for #fails# times.',
self::EMPTY_TOKEN => 'Could not append content to file. Have to set token parameter.',
self::UNKNOWN_TOKEN => 'Could not find file by token.',
self::INVALID_SIGNATURE => 'Token signature is invalid.',
];
public function __construct(string $code, ...$args)
{
$message = isset($args[0]) && is_string($args[0]) ? $args[0] : null;
$description = isset($args[1]) && is_string($args[1]) ? $args[1] : null;
$lastIndex = count($args) - 1;
$customData = isset($args[$lastIndex]) && is_array($args[$lastIndex]) ? $args[$lastIndex] : [];
$replacements = [];
foreach ($customData as $key => $value)
{
$replacements["#{$key}#"] = $value;
}
if (isset(self::$systemErrors[$code]))
{
$message = self::$systemErrors[$code];
foreach ($replacements as $search => $repl)
{
$message = str_replace($search, $repl, $message);
}
$this->setSystem(true);
$description = '';
}
if (!is_string($message))
{
$message = Loc::getMessage("UPLOADER_{$code}", $replacements);
}
if (is_string($message) && mb_strlen($message) > 0 && !is_string($description))
{
$description = Loc::getMessage("UPLOADER_{$code}_DESC", $replacements);
}
if (!is_string($message) || mb_strlen($message) === 0)
{
$message = $code;
}
parent::__construct($message, $code, $customData);
if (is_string($description))
{
$this->setDescription($description);
}
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function isSystem(): bool
{
return $this->system;
}
public function setSystem(bool $system): void
{
$this->system = $system;
}
public function jsonSerialize()
{
return [
'message' => $this->getMessage(),
'code' => $this->getCode(),
'type' => 'file-uploader',
'system' => $this->isSystem(),
'description' => $this->getDescription(),
'customData' => $this->getCustomData(),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Bitrix\UI\FileUploader;
use Bitrix\Main\Web\Json;
use Bitrix\Main\Web\Uri;
class UrlManager
{
public static function getDownloadUrl(UploaderController $controller, FileInfo $fileInfo): Uri
{
$uri = self::getActionUrl($controller, 'download');
$uri->addParams(['fileId' => $fileInfo->getId()]);
return $uri;
}
public static function getPreviewUrl(UploaderController $controller, FileInfo $fileInfo): Uri
{
$uri = self::getActionUrl($controller, 'preview');
$uri->addParams(['fileId' => $fileInfo->getId()]);
return $uri;
}
private static function getActionUrl(UploaderController $controller, string $actionName): Uri
{
return \Bitrix\Main\Engine\UrlManager::getInstance()->create(
"ui.fileuploader.{$actionName}",
[
'controller' => $controller->getName(),
'controllerOptions' => Json::encode($controller->getOptions()),
]
);
}
}