Files
ave-cms/adminx/modules/Media/Controller.php
T
2026-08-05 21:40:21 +03:00

584 lines
20 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Media/Controller.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Adminx\Media;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\AdminAssets;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\Permission;
use App\Adminx\Support\SavedViews;
use App\Content\Documents\DocumentMediaDraft;
use App\Helpers\Request;
class Controller extends BaseController
{
protected function savedViewFields() { return array('dir', 'q', 'type', 'view'); }
protected function savedViewGuard() { if (($error = $this->csrfGuard()) !== null) { return $error; } return Permission::check('view_media') ? null : $this->error('Недостаточно прав', array(), 403); }
public function index(array $params = array())
{
AdminAssets::addStyle($this->base() . '/modules/Media/assets/media.css', 50);
AdminAssets::addScript($this->base() . '/modules/Media/assets/media.js', 50);
$dir = Model::normalize(Request::getStr('dir', Model::ROOT), Model::ROOT);
$q = trim(Request::getStr('q', ''));
$type = Request::getStr('type', '');
$type = in_array($type, array('image', 'file'), true) ? $type : '';
$view = Request::getStr('view', 'grid') === 'table' ? 'table' : 'grid';
$page = max(1, Request::getInt('page', 1));
$perPage = 48;
$listing = Model::listing($dir, $q, $type, $page, $perPage);
return $this->render('@media/index.twig', array(
'title' => 'Медиа',
'dir' => $listing['dir'],
'breadcrumbs' => Model::breadcrumbs($listing['dir']),
'parent_dir' => Model::parentDir($listing['dir']),
'folders' => $listing['folders'],
'files' => $listing['files'],
'stats' => $listing['stats'],
'filters' => array('q' => $q, 'type' => $type, 'view' => $view),
'page' => $listing['page'],
'pages' => $listing['pages'],
'total' => $listing['total'],
'per_page' => $perPage,
'can_manage' => Permission::check('manage_media'),
'saved_views' => SavedViews::all('media', Auth::id(), $this->savedViewFields()),
));
}
public function saveSavedView(array $params = array())
{
if (($error = $this->savedViewGuard()) !== null) { return $error; }
$filters = json_decode(Request::postStr('filters', '{}'), true);
if (!is_array($filters)) { return $this->error('Некорректный набор фильтров', array(), 422); }
try { $views = SavedViews::save('media', Auth::id(), Request::postStr('title', ''), $filters, $this->savedViewFields()); }
catch (\InvalidArgumentException $e) { return $this->error($e->getMessage(), array(), 422); }
return $this->success('Представление сохранено', array('data' => array('views' => $views)));
}
public function deleteSavedView(array $params = array())
{
if (($error = $this->savedViewGuard()) !== null) { return $error; }
try { $views = SavedViews::delete('media', Auth::id(), isset($params['id']) ? $params['id'] : '', $this->savedViewFields()); }
catch (\InvalidArgumentException $e) { return $this->error($e->getMessage(), array(), 404); }
return $this->success('Представление удалено', array('data' => array('views' => $views)));
}
public function audit(array $params = array())
{
AdminAssets::addStyle($this->base() . '/modules/Media/assets/media.css', 50);
AdminAssets::addScript($this->base() . '/modules/Media/assets/media.js', 50);
$report = MediaAudit::report();
$kind = Request::getStr('kind', 'unused');
$allowedKinds = array('unused', 'duplicates', 'missing_originals', 'missing_previews', 'large', 'invalid', 'files');
if (!in_array($kind, $allowedKinds, true)) {
$kind = 'unused';
}
$query = trim(Request::getStr('q', ''));
$page = max(1, Request::getInt('page', 1));
$perPage = 100;
$rows = $report && isset($report[$kind]) && is_array($report[$kind]) ? $report[$kind] : array();
if ($query !== '') {
$needle = function_exists('mb_strtolower') ? mb_strtolower($query) : strtolower($query);
$rows = array_values(array_filter($rows, function ($row) use ($needle) {
$haystack = isset($row['path']) ? (string) $row['path'] : json_encode($row);
$haystack = function_exists('mb_strtolower') ? mb_strtolower($haystack) : strtolower($haystack);
return strpos($haystack, $needle) !== false;
}));
}
$total = count($rows);
$pages = max(1, (int) ceil($total / $perPage));
$page = min($page, $pages);
$rows = array_slice($rows, ($page - 1) * $perPage, $perPage);
return $this->render('@media/audit.twig', array(
'title' => 'Анализатор медиа',
'report' => $report,
'kind' => $kind,
'rows' => $rows,
'filters' => array('q' => $query),
'total' => $total,
'page' => $page,
'pages' => $pages,
'can_manage' => Permission::check('manage_media'),
));
}
public function runAudit(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$report = MediaAudit::run();
return $this->success('Анализ медиа завершён', array(
'data' => array('summary' => $report['summary']),
'redirect' => $this->base() . '/media/audit',
));
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
}
public function file(array $params = array())
{
AdminAssets::addStyle($this->base() . '/modules/Media/assets/media.css', 50);
AdminAssets::addScript($this->base() . '/modules/Media/assets/media.js', 50);
$file = Model::file(Request::getStr('path', ''));
if (!$file) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Файл не найден'), 404);
}
return $this->render('@media/file.twig', array(
'title' => 'Медиа: ' . $file['name'],
'file' => $file,
'parent_dir' => Model::parentDir($file['path']),
'can_manage' => Permission::check('manage_media'),
'supports_webp' => Model::supportsWebp(),
'presets' => ImagePresets::options(),
'usage' => MediaUsageIndex::usage($file['path']),
));
}
public function trash(array $params = array())
{
AdminAssets::addStyle($this->base() . '/modules/Media/assets/media.css', 50);
AdminAssets::addScript($this->base() . '/modules/Media/assets/media.js', 50);
return $this->render('@media/trash.twig', array(
'title' => 'Корзина медиа',
'items' => MediaTrash::all(),
'can_manage' => Permission::check('manage_media'),
));
}
public function picker(array $params = array())
{
$dir = Model::normalize(Request::getStr('dir', Model::ROOT), Model::ROOT);
$q = trim(Request::getStr('q', ''));
$type = Request::getStr('type', 'image');
$type = in_array($type, array('image', 'file', 'all'), true) ? $type : 'image';
$listingType = $type === 'all' ? '' : $type;
$page = max(1, Request::getInt('page', 1));
$perPage = Request::getInt('per_page', 36);
$perPage = max(12, min(60, $perPage));
$listing = Model::listing($dir, $q, $listingType, $page, $perPage);
return $this->success('', array(
'data' => array(
'dir' => $listing['dir'],
'type' => $type,
'parent_dir' => Model::parentDir($listing['dir']),
'breadcrumbs' => Model::breadcrumbs($listing['dir']),
'folders' => $this->pickerFolders($listing['folders']),
'files' => $this->pickerFiles($listing['files']),
'page' => $listing['page'],
'pages' => $listing['pages'],
'total' => $listing['total'],
'per_page' => $perPage,
),
));
}
public function folderFiles(array $params = array())
{
$dir = Model::normalize(Request::getStr('dir', Model::ROOT), Model::ROOT);
$type = Request::getStr('type', 'image');
$type = in_array($type, array('image', 'file', 'all'), true) ? $type : 'image';
$limit = Request::getInt('limit', 500);
$limit = max(1, min(1000, $limit));
try {
$files = Model::folderFiles($dir, $type === 'all' ? '' : $type, $limit);
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('', array(
'data' => array(
'dir' => $dir,
'type' => $type,
'files' => $this->pickerFiles($files),
'count' => count($files),
),
));
}
public function upload(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
if (empty($_FILES['files']) || empty($_FILES['files']['name'])) {
return $this->error('Выберите файлы для загрузки');
}
try {
$draftToken = Request::postStr('media_draft_token', '');
$dir = $draftToken !== ''
? DocumentMediaDraft::uploadDirectory(
$draftToken,
Request::postInt('field_id', 0),
Auth::id(),
Request::postInt('document_id', 0)
)
: Request::postStr('dir', Model::ROOT);
$result = Model::upload($dir, $_FILES['files']);
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
if (empty($result['uploaded'])) {
return $this->error('Файлы не загружены', array('files' => implode(', ', $result['errors'])));
}
$message = 'Загружено файлов: ' . count($result['uploaded']);
if (!empty($result['errors'])) {
$message .= '. Пропущено: ' . count($result['errors']);
}
return $this->success($message, array(
'data' => array('files' => $result['uploaded']),
'redirect' => $this->base() . '/media?dir=' . rawurlencode($result['dir']),
));
}
public function createFolder(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$path = Model::createFolder(Request::postStr('dir', Model::ROOT), Request::postStr('name', ''));
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Папка создана', array(
'redirect' => $this->base() . '/media?dir=' . rawurlencode($path),
));
}
public function rename(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$target = Model::rename(Request::postStr('path', ''), Request::postStr('name', ''));
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$redirect = is_file(Model::abs($target))
? $this->base() . '/media/file?path=' . rawurlencode($target)
: $this->base() . '/media?dir=' . rawurlencode($target);
return $this->success('Переименовано', array('redirect' => $redirect));
}
public function delete(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$path = Request::postStr('path', '');
$parent = Model::parentDir($path);
try {
$usage = MediaAudit::inspectUsage($path);
if ((int) $usage['use_count'] > 0 && Request::postInt('confirm_usage', 0) !== 1) {
throw new \RuntimeException('Объект используется. Подтвердите перенос в корзину после просмотра связей');
}
Model::trash($path);
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Перемещено в корзину', array(
'redirect' => $this->base() . '/media?dir=' . rawurlencode($parent ?: Model::ROOT),
));
}
public function deleteCheck(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
$path = Request::postStr('path', '');
if (Model::isProtectedPath($path)) { return $this->error('Этот путь нельзя удалить', array(), 422); }
try { $usage = MediaAudit::inspectUsage($path); }
catch (\RuntimeException $e) { return $this->error($e->getMessage(), array(), 422); }
return $this->success('', array('data' => array('usage' => $usage)));
}
public function emptyFolder(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
$path = Request::postStr('path', '');
try {
$usage = MediaAudit::inspectUsage($path);
if ((int) $usage['use_count'] > 0 && Request::postInt('confirm_usage', 0) !== 1) {
throw new \RuntimeException('В папке есть используемые файлы. Подтвердите очистку после просмотра предупреждения');
}
$entry = Model::trashContents($path);
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Содержимое папки перемещено в корзину', array(
'data' => array('count' => isset($entry['count']) ? (int) $entry['count'] : 0),
'redirect' => $this->base() . '/media?dir=' . rawurlencode($path),
));
}
public function restoreTrash(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
try {
$entry = MediaTrash::restore(isset($params['token']) ? $params['token'] : '');
foreach ($entry['paths'] as $path) { MediaSearchIndex::indexPath($path); }
}
catch (\RuntimeException $e) { return $this->error($e->getMessage(), array(), 422); }
return $this->success('Объект восстановлен', array('redirect' => $this->base() . '/media/trash'));
}
public function purgeTrash(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
try { MediaTrash::purge(isset($params['token']) ? $params['token'] : ''); }
catch (\RuntimeException $e) { return $this->error($e->getMessage(), array(), 422); }
return $this->success('Объект удалён окончательно', array('redirect' => $this->base() . '/media/trash'));
}
public function clearThumbnails(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$dir = Request::postStr('path', Model::ROOT);
try {
$result = Model::clearThumbnails($dir);
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$message = $result['roots'] > 0
? 'Удалено папок превью: ' . $result['roots'] . ', файлов: ' . $result['files']
: 'В этой папке превью не найдены';
return $this->success($message, array(
'data' => $result,
'redirect' => $this->base() . '/media?dir=' . rawurlencode($dir),
));
}
public function transform(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$path = Request::postStr('path', '');
try {
$out = Model::transform($path, array(
'mode' => Request::postStr('mode', 'fit'),
'width' => Request::postInt('width', 320),
'height' => Request::postInt('height', 240),
'format' => Request::postStr('format', 'original'),
'quality' => Request::postInt('quality', 82),
'webp_twin' => Request::postInt('webp_twin', 0),
'crop_enabled' => Request::postStr('crop_enabled', ''),
'crop_x' => Request::postInt('crop_x', 0),
'crop_y' => Request::postInt('crop_y', 0),
'crop_w' => Request::postInt('crop_w', 0),
'crop_h' => Request::postInt('crop_h', 0),
));
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Изображение создано', array(
'data' => array('path' => $out),
'redirect' => $this->base() . '/media?dir=' . rawurlencode(Model::parentDir($out)),
));
}
public function preview(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$path = Request::postStr('path', '');
try {
$preview = Model::previewTransform($path, array(
'mode' => Request::postStr('mode', 'fit'),
'width' => Request::postInt('width', 320),
'height' => Request::postInt('height', 240),
'format' => Request::postStr('format', 'original'),
'quality' => Request::postInt('quality', 82),
'crop_enabled' => Request::postStr('crop_enabled', ''),
'crop_x' => Request::postInt('crop_x', 0),
'crop_y' => Request::postInt('crop_y', 0),
'crop_w' => Request::postInt('crop_w', 0),
'crop_h' => Request::postInt('crop_h', 0),
));
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
$meta = trim(($preview['width'] && $preview['height'] ? $preview['width'] . 'x' . $preview['height'] : '')
. ' · ' . strtoupper($preview['format'])
. ($preview['size_label'] !== '' ? ' · ' . $preview['size_label'] : ''), ' ·');
return $this->success('', array('data' => array(
'path' => $preview['path'],
'url' => $preview['path'],
'meta' => $meta,
)));
}
public function previewClear(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
Model::deletePreview(Request::postStr('path', ''));
return $this->success('');
}
public function convertWebp(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$out = Model::convertWebp(Request::postStr('path', ''), Request::postInt('quality', 82));
} catch (\RuntimeException $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('WebP-копия создана', array(
'data' => array('path' => $out),
'redirect' => $this->base() . '/media/file?path=' . rawurlencode($out),
));
}
public function presets(array $params = array())
{
if (!Permission::check('view_media')) {
return $this->error('Недостаточно прав', array(), 403);
}
return $this->success('', array('data' => array(
'items' => ImagePresets::all(),
'modes' => ImagePresets::modes(),
'formats' => ImagePresets::formats(),
'can_manage' => Permission::check('manage_media'),
)));
}
public function storePreset(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
list($errors, $data) = ImagePresets::validate(Request::postAll(), 0);
if (!empty($errors)) { return $this->error('Проверьте поля', $errors, 422); }
$id = ImagePresets::save(0, $data);
return $this->success('Вид создан', array('data' => array('id' => $id)));
}
public function updatePreset(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
$id = isset($params['id']) ? (int) $params['id'] : 0;
if (!ImagePresets::one($id)) { return $this->error('Вид не найден', array(), 404); }
list($errors, $data) = ImagePresets::validate(Request::postAll(), $id);
if (!empty($errors)) { return $this->error('Проверьте поля', $errors, 422); }
ImagePresets::save($id, $data);
return $this->success('Вид сохранён', array('data' => array('id' => $id)));
}
public function deletePreset(array $params = array())
{
if (($err = $this->guard()) !== null) { return $err; }
if (!ImagePresets::delete(isset($params['id']) ? (int) $params['id'] : 0)) {
return $this->error('Вид не найден', array(), 404);
}
return $this->success('Вид удалён');
}
protected function guard()
{
return $this->guardPermission('manage_media');
}
protected function pickerFolders(array $folders)
{
$out = array();
foreach ($folders as $folder) {
$out[] = array(
'name' => isset($folder['name']) ? $folder['name'] : '',
'path' => isset($folder['path']) ? $folder['path'] : '',
'parent' => isset($folder['parent']) ? $folder['parent'] : '',
'count' => isset($folder['count']) ? (int) $folder['count'] : 0,
);
}
return $out;
}
protected function pickerFiles(array $files)
{
$out = array();
foreach ($files as $file) {
$out[] = array(
'name' => isset($file['name']) ? $file['name'] : '',
'path' => isset($file['path']) ? $file['path'] : '',
'url' => isset($file['url']) ? $file['url'] : '',
'thumb_url' => isset($file['thumb_url']) ? $file['thumb_url'] : '',
'preview_url' => isset($file['preview_url']) ? $file['preview_url'] : '',
'extension' => isset($file['extension']) ? $file['extension'] : '',
'size_label' => isset($file['size_label']) ? $file['size_label'] : '',
'width' => isset($file['width']) ? (int) $file['width'] : 0,
'height' => isset($file['height']) ? (int) $file['height'] : 0,
'webp_url' => !empty($file['has_webp']) && !empty($file['webp_path']) ? $file['webp_path'] : '',
);
}
return $out;
}
}