2018-04-01

This commit is contained in:
Visman 2018-04-01 18:16:58 +07:00
parent 6c44946e39
commit 40600704f4
9 changed files with 933 additions and 7 deletions

View file

@ -0,0 +1,9 @@
<?php
namespace ForkBB\Core\Exceptions;
use ForkBB\Core\Exceptions\ForkException;
class FileException extends ForkException
{
}

305
app/Core/File.php Normal file
View file

@ -0,0 +1,305 @@
<?php
namespace ForkBB\Core;
use ForkBB\Core\Exceptions\FileException;
use InvalidArgumentException;
class File
{
/**
* Текст ошибки
* @var null|string
*/
protected $error;
/**
* Путь до файла
* @var null|string
*/
protected $path;
/**
* Содержимое файла
* @var null|string
*/
protected $data;
/**
* Оригинальное имя файла без расширения
* @var null|string
*/
protected $name;
/**
* Оригинальное расширение файла
* @var null|string
*/
protected $ext;
/**
* Размер оригинального файла
*/
protected $size;
/**
* Флаг автопереименования файла
* @var bool
*/
protected $rename = false;
/**
* Флаг перезаписи файла
* @var bool
*/
protected $rewrite = false;
/**
* Паттерн для pathinfo
* @var string
*/
protected $pattern = '%^(?!.*?\.\.)([\w.\x5C/:-]*[\x5C/])?(\*|[\w.-]+)\.(\*|[a-z\d]+)$%i';
/**
* Конструктор
*
* @param string $path
* @param array $options
*
* @throws FileException
*/
public function __construct($path, $options)
{
if (! \is_file($path)) {
throw new FileException('File not found');
}
if (! \is_readable($path)) {
throw new FileException('File can not be read');
}
$this->path = $path;
$this->data = null;
$name = null;
$ext = null;
if (isset($options['basename'])) {
if (false === ($pos = \strrpos($options['basename'], '.'))) {
$name = $options['basename'];
} else {
$name = \substr($options['basename'], 0, $pos);
$ext = \substr($options['basename'], $pos + 1);
}
}
$this->name = isset($options['filename']) && \is_string($options['filename']) ? $options['filename'] : $name;
$this->ext = isset($options['extension']) && \is_string($options['extension']) ? $options['extension'] : $ext;
$this->size = \is_string($this->data) ? \strlen($this->data) : \filesize($path);
if (! $this->size) {
throw new FileException('File size is undefined');
}
}
/**
* Возвращает текст ошибки
*
* @return null|string
*/
public function error()
{
return $this->error;
}
/**
* Фильрует и переводит в латиницу(?) имя файла
*
* @param string $name
*
* @return string
*/
protected function filterName($name)
{
if (\function_exists('\transliterator_transliterate')) {
$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;
}
/**
* Возвращает информацию о пути к сохраняемому файлу с учетом подстановок
*
* @param string $path
*
* @return false|array
*/
protected function pathinfo($path)
{
if (! \preg_match($this->pattern, $path, $matches)) {
$this->error = 'The path/name format is broken';
return false;
}
if ('*' === $matches[2]) {
$matches[2] = $this->filterName($this->name);
}
if ('*' === $matches[3]) {
$matches[3] = $this->ext;
} elseif ('(' === $matches[3]{0} && ')' === $matches[3]{\strlen($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],
];
}
/**
* Устанавливает флаг автопереименования файла
*
* @param bool $rename
*
* @return File
*/
public function rename($rename)
{
$this->rename = $rename;
return $this;
}
/**
* Устанавливает флаг перезаписи файла
*
* @param bool $rewrite
*
* @return File
*/
public function rewrite($rewrite)
{
$this->rewrite = $rewrite;
return $this;
}
/**
* Создает/проверяет на запись директорию
*
* @param string $dirname
*
* @return bool
*/
protected function dirProc($dirname)
{
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;
}
/**
* Создает/устанавливает права на файл
*
* @param string $path
*
* @return bool
*/
protected function fileProc($path)
{
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;
}
/**
* Сохраняет файл по указанному шаблону пути
*
* @param string $path
*
* @return bool
*/
public function toFile($path)
{
$info = $this->pathinfo($path);
if (false === $info || ! $this->dirProc($info['dirname'])) {
return false;
}
if ($this->rename) {
$old = $info['filename'];
$i = 1;
while (\file_exists($info['dirname'] . $info['filename'] . '.' . $info['extension'])) {
++$i;
$info['filename'] = $old . '_' . $i;
}
} elseif (! $this->rewrite && \file_exists($info['dirname'] . $info['filename'] . '.' . $info['extension'])) {
$this->error = 'Such file already exists';
return false;
}
$path = $info['dirname'] . $info['filename'] . '.' . $info['extension'];
if ($this->fileProc($path)) {
$this->path = $path;
$this->name = $info['filename'];
$this->ext = $info['extension'];
$this->size = \filesize($path);
return true;
} else {
return false;
}
}
public function name()
{
return $this->name;
}
public function ext()
{
return $this->ext;
}
public function size()
{
return $this->size;
}
public function path()
{
return $this->path;
}
}

326
app/Core/Files.php Normal file
View file

@ -0,0 +1,326 @@
<?php
namespace ForkBB\Core;
use ForkBB\Core\File;
use ForkBB\Core\Image;
use ForkBB\Core\Exceptions\FileException;
use InvalidArgumentException;
class Files
{
/**
* Максимальный размер для картинок
* @var int
*/
protected $maxImgSize;
/**
* Максимальный размер для файлов
* @var int
*/
protected $maxFileSize;
/**
* Текст ошибки
* @var null|string
*/
protected $error;
/**
* Список кодов типов картинок и расширений для них
* @var array
*/
protected $imageType = [
1 => 'gif',
2 => 'jpg',
3 => 'png',
4 => 'swf',
5 => 'psd',
6 => 'bmp',
7 => 'tiff',
8 => 'tiff',
9 => 'jpc',
10 => 'jp2',
11 => 'jpx',
12 => 'jb2',
13 => 'swc',
14 => 'iff',
15 => 'wbmp',
16 => 'xbm',
17 => 'ico',
18 => 'webp',
];
/**
* Конструктор
*
* @param string|int $maxFileSize
* @param string|int $maxImgSize
*
*/
public function __construct($maxFileSize, $maxImgSize)
{
$init = \min(
$this->size(\ini_get('upload_max_filesize')),
$this->size(\ini_get('post_max_size'))
);
$this->maxImgSize = \min(
$this->size($maxImgSize),
$init
);
$this->maxFileSize = \min(
$this->size($maxFileSize),
$init
);
}
/**
* Возвращает максимальный размер картинки для загрузки
*
* @param string $unit
*
* @return int
*/
public function maxImgSize($unit = null)
{
return $this->size($this->maxImgSize, $unit);
}
/**
* Возвращает максимальный размер файла для загрузки
*
* @param string $unit
*
* @return int
*/
public function maxFileSize($unit = null)
{
return $this->size($this->maxFileSize, $unit);
}
/**
* Переводит объем информации из одних единиц в другие
*
* @param int|string $value
* @param string $to
*
* @return int
*/
public function size($value, $to = null)
{
if (\is_string($value)) {
$value = \trim($value, "Bb \t\n\r\0\x0B");
if (! isset($value{0})) {
return 0;
}
$from = $value{\strlen($value) - 1};
$value = (int) $value;
switch ($from) {
case 'G':
case 'g':
$value *= 1024;
case 'M':
case 'm':
$value *= 1024;
case 'K':
case 'k':
$value *= 1024;
}
}
if (\is_string($to)) {
$to = \trim($to, "Bb \t\n\r\0\x0B");
switch ($to) {
case 'G':
case 'g':
$value /= 1024;
case 'M':
case 'm':
$value /= 1024;
case 'K':
case 'k':
$value /= 1024;
}
}
return (int) $value;
}
/**
* Возвращает текст ошибки
*
* @return null|string
*/
public function error()
{
return $this->error;
}
/**
* Определяет по содержимому файла расширение картинки
*
* @param mixed $file
*
* @return false|string
*/
public function isImage($file)
{
if (\is_string($file)) {
if (\function_exists('\exif_imagetype')) {
$type = \exif_imagetype($file);
} elseif (false !== ($type = @\getimagesize($file)) && $type[0] > 0 && $type[1] > 0) {
$type = $type[2];
} else {
$type = 0;
}
return isset($this->imageType[$type]) ? $this->imageType[$type] : false;
}
return $file instanceof Image ? $file->ext() : false;
}
/**
* Получает файл(ы) из формы
*
* @param array $file
*
* @return mixed
*/
public function upload(array $file)
{
$this->error = null;
if (! isset($file['tmp_name'])
|| ! isset($file['name'])
|| ! isset($file['type'])
|| ! isset($file['error'])
|| ! isset($file['size'])
) {
$this->error = 'Expected file description array';
return false;
}
if (\is_array($file['tmp_name'])) {
$result = [];
foreach ($file['tmp_name'] as $key => $value) {
// изображение не было отправлено
if ('' === $file['name'][$key] && empty($file['size'][$key])) {
continue;
}
$cur = $this->uploadOneFile([
'tmp_name' => $value,
'name' => $file['name'][$key],
'type' => $file['type'][$key],
'error' => $file['error'][$key],
'size' => $file['size'][$key],
]);
if (false === $cur) {
return false;
}
$result[] = $cur;
}
return empty($result) ? null : $result;
} else {
return '' === $file['name'] && empty($file['size']) ? null : $this->uploadOneFile($file);
}
}
/**
* Получает один файл из формы
*
* @param array $file
*
* @return mixed
*/
protected function uploadOneFile(array $file)
{
if (\UPLOAD_ERR_OK !== $file['error']) {
switch ($file['error']) {
case \UPLOAD_ERR_INI_SIZE:
$this->error = 'The uploaded file exceeds the upload_max_filesize';
break;
case \UPLOAD_ERR_FORM_SIZE:
$this->error = 'The uploaded file exceeds the MAX_FILE_SIZE';
break;
case \UPLOAD_ERR_PARTIAL:
$this->error = 'The uploaded file was only partially uploaded';
break;
case \UPLOAD_ERR_NO_FILE:
$this->error = 'No file was uploaded';
break;
case \UPLOAD_ERR_NO_TMP_DIR:
$this->error = 'Missing a temporary folder';
break;
case \UPLOAD_ERR_CANT_WRITE:
$this->error = 'Failed to write file to disk';
break;
case \UPLOAD_ERR_EXTENSION:
$this->error = 'A PHP extension stopped the file upload';
break;
default:
$this->error = 'Unknown upload error';
break;
}
return false;
}
if (! \is_uploaded_file($file['tmp_name'])) {
$this->error = 'The specified file was not uploaded';
return false;
}
if (false === ($pos = \strrpos($file['name'], '.'))) {
$name = $file['name'];
$ext = null;
} else {
$name = \substr($file['name'], 0, $pos);
$ext = \substr($file['name'], $pos + 1);
}
$isImage = $this->isImage($file['tmp_name']);
if (false !== $isImage) {
$ext = $isImage;
$isImage = 'swf' !== $isImage; // флеш не будет картинкой
}
if ($isImage) {
if ($file['size'] > $this->maxImgSize) {
$this->error = 'The image too large';
return false;
}
} else {
if ($file['size'] > $this->maxFileSize) {
$this->error = 'The file too large';
return false;
}
}
$options = [
'filename' => $name,
'extension' => $ext,
'basename' => $name . '.' . $ext,
'mime' => $file['type'],
// 'size' => $file['size'],
];
try {
if ($isImage) {
return new Image($file['tmp_name'], $options);
} else {
return new File($file['tmp_name'], $options);
}
} catch (FileException $e) {
$this->error = $e->getMessage();
return false;
}
}
}

168
app/Core/Image.php Normal file
View file

@ -0,0 +1,168 @@
<?php
namespace ForkBB\Core;
use ForkBB\Core\Files;
use ForkBB\Core\File;
use ForkBB\Core\Exceptions\FileException;
use InvalidArgumentException;
class Image extends File
{
/**
* Изображение
* @var false|resource
*/
protected $image;
/**
* Качество изображения
* @var int
*/
protected $quality = 100;
/**
* Паттерн для pathinfo
* @var string
*/
protected $pattern = '%^(?!.*?\.\.)([\w.\x5C/:-]*[\x5C/])?(\*|[\w.-]+)\.(\*|[a-z\d]+|\([a-z\d]+(?:\|[a-z\d]+)*\))$%i';
/**
* Конструктор
*
* @param string $path
* @param array $options
*
* @throws FileException
*/
public function __construct($path, $options)
{
parent::__construct($path, $options);
if (! \extension_loaded('gd') || ! \function_exists('\imagecreatetruecolor')) {
throw new FileException('GD library not connected');
}
if (\is_string($this->data)) {
$this->image = @\imagecreatefromstring($this->data);
} else {
$this->image = @\imagecreatefromstring(\file_get_contents($this->path));
}
if (false === $this->image) {
throw new FileException('Invalid image data');
}
}
/**
* Изменяет размер изображения при необходимости
*
* @param int $maxW
* @param int $maxH
*
* @throws FileException
*
* @return Image
*/
public function resize($maxW, $maxH)
{
$oldW = \imagesx($this->image);
$oldH = \imagesy($this->image);
$wr = ($maxW < 1) ? 1 : $maxW / $oldW;
$hr = ($maxH < 1) ? 1 : $maxH / $oldH;
$r = \min($wr, $hr, 1);
$width = \round($oldW * $r);
$height = \round($oldH * $r);
if (false === ($image = \imagecreatetruecolor($width, $height))) {
throw new FileException('Failed to create new truecolor image');
}
if (false === ($color = \imagecolorallocatealpha($image, 255, 255, 255, 127))) {
throw new FileException('Failed to create color for image');
}
if (false === \imagefill($image, 0, 0, $color)) {
throw new FileException('Failed to fill image with color');
}
\imagecolortransparent($image, $color);
$palette = \imagecolorstotal($this->image);
if ($palette > 0 && ! \imagetruecolortopalette($image, true, $palette)) {
throw new FileException('Failed to convert image to palette');
}
if (false === \imagecopyresampled($image, $this->image, 0, 0, 0, 0, $width, $height, $oldW, $oldH)) {
throw new FileException('Failed to resize image');
}
if (false === \imagealphablending($image, false) || false === \imagesavealpha($image, true)) {
throw new FileException('Failed to adjust image');
}
$this->image = $image;
return $this;
}
/**
* Возвращает информацию о пути к сохраняемой картинке с учетом подстановок
*
* @param string $path
*
* @return false|array
*/
protected function pathinfo($path)
{
$info = parent::pathinfo($path);
if (false === $info) {
return false;
}
if (\is_array($info['extension'])) {
if (\in_array($this->ext, $info['extension'])) {
$info['extension'] = $this->ext;
} else {
$info['extension'] = \reset($info['extension']); // ???? выбор расширения?
}
}
return $info;
}
/**
* Создает/устанавливает права на картинку
*
* @param string $path
*
* @return bool
*/
protected function fileProc($path)
{
switch (\pathinfo($path, \PATHINFO_EXTENSION)) {
case 'jpg':
$result = @\imagejpeg($this->image, $path, $this->quality);
break;
case 'png':
$quality = \floor((100 - $this->quality) / 11);
$result = @\imagepng($this->image, $path, $quality);
break;
case 'gif':
$result = @\imagegif($this->image, $path);
break;
default:
$this->error = 'File type not supported';
return false;
}
if (! $result) {
$this->error = 'Error writing file';
return false;
}
@\chmod($path, 0644);
return true;
}
public function __destruct() {
if (\is_resource($this->image)) {
\imagedestroy($this->image);
}
}
}

View file

@ -3,6 +3,7 @@
namespace ForkBB\Core;
use ForkBB\Core\Container;
use ForkBB\Core\File;
use RuntimeException;
class Validator
@ -108,6 +109,8 @@ class Validator
'array' => [$this, 'vArray'],
'checkbox' => [$this, 'vCheckbox'],
'email' => [$this, 'vEmail'],
'file' => [$this, 'vFile'],
'image' => [$this, 'vImage'],
'in' => [$this, 'vIn'],
'integer' => [$this, 'vInteger'],
'login' => [$this, 'vLogin'],
@ -616,9 +619,22 @@ class Validator
$this->addError('The :alias maximum is :attr');
}
} elseif (\is_array($value)) {
if (\count($value) > $attr) {
if (\reset($value) instanceof File) {
$attr *= 1024;
foreach ($value as $file) {
if ($file->size() > $attr) {
$this->addError('The :alias contains too large a file');
return null;
}
}
} elseif (\count($value) > $attr) {
$this->addError('The :alias maximum is :attr elements');
}
} elseif ($value instanceof File) {
if ($value->size() > $attr * 1024) {
$this->addError('The :alias contains too large a file');
return null;
}
} elseif (null !== $value) {
$this->addError('The :alias maximum is :attr'); //????
return null;
@ -713,4 +729,49 @@ class Validator
}
return $value;
}
protected function vFile($v, $value, $attr)
{
if (null === $value) {
return null;
}
if (! \is_array($value)) {
$this->addError('The :alias not contains file');
return null;
}
$value = $this->c->Files->upload($value);
if (null === $value) {
return null;
} elseif (false === $value) {
$this->addError($this->c->Files->error());
return null;
} elseif ('multiple' === $attr) {
if (! \is_array($value)) {
$value = [$value];
}
} elseif (\is_array($value)) {
$this->addError('The :alias contains more than one file');
return null;
}
return $value;
}
protected function vImage($v, $value, $attr)
{
$value = $this->vFile($v, $value, $attr);
if (\is_array($value)) {
foreach ($value as $file) {
if (false === $this->c->Files->isImage($file)) {
$this->addError('The :alias not contains image');
return null;
}
}
} elseif (null !== $value && false === $this->c->Files->isImage($value)) {
$this->addError('The :alias not contains image');
return null;
}
return $value;
}
}

View file

@ -2,6 +2,7 @@
namespace ForkBB\Models\Pages;
use ForkBB\Core\Image;
use ForkBB\Models\Page;
use ForkBB\Models\User\Model as User;
@ -52,16 +53,45 @@ class Profile extends Page
$this->c->Lang->load('profile');
if ($isEdit && 'POST' === $method) {
$v = $this->c->Validator->reset()
->addValidators([
])->addRules([
'token' => 'token:EditUserProfile',
'upload_avatar' => $rules->useAvatar ? 'image|max:' . $this->c->Files->maxImgSize('K') : 'absent',
])->addAliases([
])->addArguments([
'token' => ['id' => $curUser->id],
])->addMessages([
]);
if ($v->validation($_FILES + $_POST)) {
if ($v->upload_avatar instanceof Image) {
$curUser->deleteAvatar();
$v->upload_avatar
->rename(false)
->rewrite(true)
->resize((int) $this->c->config->o_avatars_width, (int) $this->c->config->o_avatars_height)
->toFile($this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$curUser->id}.(jpg|png|gif)");
# var_dump(
# $v->upload_avatar->path(),
# $v->upload_avatar->name(),
# $v->upload_avatar->ext(),
# $v->upload_avatar->size(),
# $v->upload_avatar->error()
# );
}
}
$this->fIswev = $v->getErrors();
}
$clSuffix = $isEdit ? '-edit' : '';
if ($isEdit) {
$form = [
'action' => $this->c->Router->link('EditUserProfile', ['id' => $curUser->id]),
'action' => $this->c->Router->link('EditUserProfile', ['id' => $curUser->id]),
'hidden' => [
'token' => $this->c->Csrf->create('EditUserProfile', ['id' => $curUser->id]),
'token' => $this->c->Csrf->create('EditUserProfile', ['id' => $curUser->id]),
],
'sets' => [],
'btns' => [
@ -141,7 +171,7 @@ class Profile extends Page
}
if ($isEdit) {
$form['enctype'] = 'multipart/form-data';
$form['hidden']['MAX_FILE_SIZE'] = 999999999;
$form['hidden']['MAX_FILE_SIZE'] = $this->c->Files->maxImgSize();
$fields['upload_avatar'] = [
'id' => 'upload_avatar',

View file

@ -9,6 +9,12 @@ use RuntimeException;
class Model extends DataModel
{
/**
* Типы аватарок
* @var array
*/
protected $avatarTypes = ['jpg', 'gif', 'png'];
/**
* Статус неподтвержденного
*
@ -157,9 +163,7 @@ class Model extends DataModel
*/
protected function getavatar()
{
$filetypes = ['jpg', 'gif', 'png'];
foreach ($filetypes as $type) {
foreach ($this->avatarTypes as $type) {
$path = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$this->id}.{$type}";
if (\is_file($path) && \getimagesize($path)) {
@ -170,6 +174,20 @@ class Model extends DataModel
return null;
}
/**
* Удаляет аватару пользователя
*/
public function deleteAvatar()
{
foreach ($this->avatarTypes as $type) {
$path = $this->c->DIR_PUBLIC . "{$this->c->config->o_avatars_dir}/{$this->id}.{$type}";
if (\is_file($path)) {
@\unlink($path);
}
}
}
/**
* Титул пользователя
*

View file

@ -35,6 +35,8 @@ return [
'smTplBl' => ['url'],
],
'MAX_POST_SIZE' => 65536,
'MAX_IMG_SIZE' => '2M',
'MAX_FILE_SIZE' => '2M',
'shared' => [
'DB' => [
@ -110,6 +112,11 @@ return [
'class' => \ForkBB\Core\Parser::class,
'flag' => ENT_HTML5,
],
'Files' => [
'class' => \ForkBB\Core\Files::class,
'file' => '%MAX_FILE_SIZE%',
'img' => '%MAX_IMG_SIZE%',
],
],
'multiple' => [

View file

@ -18,6 +18,8 @@
},
"require": {
"php": ">=5.6.0",
"ext-gd": "*",
"ext-mbstring": "*",
"artoodetoo/dirk": "dev-master",
"miovisman/parserus": "dev-master"
}