288 lines
6.8 KiB
PHP
288 lines
6.8 KiB
PHP
<?php
|
||
/**
|
||
* This file is part of the ForkBB <https://github.com/forkbb>.
|
||
*
|
||
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
|
||
* @license The MIT License (MIT)
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ForkBB\Core;
|
||
|
||
use ForkBB\Core\Files;
|
||
use ForkBB\Core\Exceptions\FileException;
|
||
use InvalidArgumentException;
|
||
|
||
class File
|
||
{
|
||
/**
|
||
* Текст ошибки
|
||
*/
|
||
protected ?string $error = null;
|
||
|
||
/**
|
||
* Содержимое файла
|
||
*/
|
||
protected ?string $data;
|
||
|
||
/**
|
||
* Размер оригинального файла
|
||
*/
|
||
protected int|false $size;
|
||
|
||
/**
|
||
* Флаг автопереименования файла
|
||
*/
|
||
protected bool $rename = false;
|
||
|
||
/**
|
||
* Флаг перезаписи файла
|
||
*/
|
||
protected bool $rewrite = false;
|
||
|
||
/**
|
||
* Паттерн для pathinfo
|
||
*/
|
||
protected string $pattern = '%^(?!.*?\.\.)([\w.\x5C/:-]*[\x5C/])?(\*|[\w.-]+)\.(\*|[a-z\d]+)$%iD';
|
||
|
||
public function __construct(protected string $path, protected string $name, protected string $ext, protected Files $files)
|
||
{
|
||
if ($files->isBadPath($path)) {
|
||
throw new FileException('Bad path to file');
|
||
}
|
||
if (! \is_file($path)) {
|
||
throw new FileException('File not found');
|
||
}
|
||
if (! \is_readable($path)) {
|
||
throw new FileException('File can not be read');
|
||
}
|
||
|
||
$this->data = null;
|
||
$this->size = \is_string($this->data) ? \strlen($this->data) : \filesize($path);
|
||
|
||
if (! $this->size) {
|
||
throw new FileException('File size is undefined');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Возвращает текст ошибки
|
||
*/
|
||
public function error(): ?string
|
||
{
|
||
return $this->error;
|
||
}
|
||
|
||
/**
|
||
* Фильрует и переводит в латиницу(?) имя файла
|
||
*/
|
||
protected function filterName(string $name): string
|
||
{
|
||
$name = \transliterator_transliterate(
|
||
"Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove; Lower();",
|
||
$name
|
||
);
|
||
|
||
$name = \trim(\preg_replace('%[^\w.-]+%', '-', $name), '-');
|
||
|
||
if (! isset($name[0])) {
|
||
$name = (string) \time();
|
||
}
|
||
|
||
return $name;
|
||
}
|
||
|
||
/**
|
||
* Возвращает информацию о пути к сохраняемому файлу с учетом подстановок
|
||
*/
|
||
protected function pathinfo(string $path): ?array
|
||
{
|
||
if (! \preg_match($this->pattern, $path, $matches)) {
|
||
$this->error = 'The path/name format is broken';
|
||
|
||
return null;
|
||
}
|
||
|
||
if ('*' === $matches[2]) {
|
||
$matches[2] = $this->filterName($this->name);
|
||
}
|
||
|
||
if ('*' === $matches[3]) {
|
||
$matches[3] = $this->ext;
|
||
} elseif (
|
||
'(' === $matches[3][0]
|
||
&& ')' === $matches[3][-1]
|
||
) {
|
||
$matches[3] = \explode('|', \substr($matches[3], 1, -1));
|
||
|
||
if (1 === \count($matches[3])) {
|
||
$matches[3] = \array_pop($matches[3]);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'dirname' => $matches[1],
|
||
'filename' => $matches[2],
|
||
'extension' => $matches[3],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Устанавливает флаг автопереименования файла
|
||
*/
|
||
public function rename(bool $rename): File
|
||
{
|
||
$this->rename = $rename;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Устанавливает флаг перезаписи файла
|
||
*/
|
||
public function rewrite(bool $rewrite): File
|
||
{
|
||
$this->rewrite = $rewrite;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Создает/проверяет на запись директорию
|
||
*/
|
||
protected function dirProc(string $dirname): bool
|
||
{
|
||
if (! \is_dir($dirname)) {
|
||
if (! \mkdir($dirname, 0755)) {
|
||
$this->error = 'Can not create directory';
|
||
|
||
return false;
|
||
}
|
||
}
|
||
if (! \is_writable($dirname)) {
|
||
$this->error = 'No write access for directory';
|
||
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Создает/устанавливает права на файл
|
||
*/
|
||
protected function fileProc(string $path): bool
|
||
{
|
||
if (\is_string($this->data)) {
|
||
if (! \file_put_contents($this->path, $path)) {
|
||
$this->error = 'Error writing file';
|
||
|
||
return false;
|
||
}
|
||
} else {
|
||
if (! \copy($this->path, $path)) {
|
||
$this->error = 'Error copying file';
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
\chmod($path, 0644);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Сохраняет файл по указанному шаблону пути
|
||
*/
|
||
public function toFile(string $path, int $maxSize = null): bool
|
||
{
|
||
$info = $this->pathinfo($path);
|
||
|
||
if (empty($info)) {
|
||
return false;
|
||
}
|
||
if ($this->files->isBadPath($info['dirname'])) {
|
||
$this->error = 'Bad path to file';
|
||
|
||
return false;
|
||
}
|
||
if (
|
||
! $this->dirProc($info['dirname'])
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
$name = $info['filename'];
|
||
$i = 1;
|
||
|
||
while (true) {
|
||
$path = $info['dirname'] . $info['filename'] . '.' . $info['extension'];
|
||
|
||
if ($this->files->isBadPath($path)) {
|
||
$this->error = 'Bad path to file';
|
||
|
||
return false;
|
||
}
|
||
|
||
if (\file_exists($path)) {
|
||
if ($this->rename) {
|
||
++$i;
|
||
$info['filename'] = $name . '_' . $i;
|
||
|
||
continue;
|
||
} elseif (! $this->rewrite) {
|
||
$this->error = 'Such file already exists';
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
if ($this->fileProc($path)) {
|
||
$this->size = \filesize($path);
|
||
|
||
if (
|
||
null !== $maxSize
|
||
&& $this->size > $maxSize
|
||
) {
|
||
$this->error = 'File is larger than the allowed size';
|
||
|
||
\unlink($path);
|
||
|
||
return false;
|
||
}
|
||
|
||
$this->path = $path;
|
||
$this->name = $info['filename'];
|
||
$this->ext = $info['extension'];
|
||
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public function name(): string
|
||
{
|
||
return $this->name;
|
||
}
|
||
|
||
public function ext(): string
|
||
{
|
||
return $this->ext;
|
||
}
|
||
|
||
public function size(): int
|
||
{
|
||
return $this->size;
|
||
}
|
||
|
||
public function path(): string
|
||
{
|
||
return $this->path;
|
||
}
|
||
}
|