Files
ave-cms/adminx/modules/Blocks/Controller.php
T
2026-07-27 12:58:44 +03:00

468 lines
16 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Blocks/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\Blocks;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Adminx\Support\CodeEditor;
use App\Common\AdminAssets;
use App\Common\AuditLog;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\Permission;
use App\Helpers\Request;
class Controller extends BaseController
{
public function index(array $params = array())
{
if (!Permission::check('view_blocks')) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
}
AdminAssets::addStyle($this->base() . '/modules/Blocks/assets/blocks.css', 50);
AdminAssets::addScript($this->base() . '/modules/Blocks/assets/blocks.js', 50);
CodeEditor::useRichEditor();
$q = Request::getStr('q', '');
$group = Request::getInt('group', 0);
$state = Request::getStr('state', '');
$state = in_array($state, array('', 'active', 'inactive', 'external', 'ajax', 'visual'), true) ? $state : '';
$blocks = Model::all($q, $group, $state);
$groups = Model::groups();
return $this->render('@blocks/index.twig', array(
'blocks' => $blocks,
'block_groups' => Model::groupedBlocks($blocks, $groups),
'groups' => $groups,
'group_map' => Model::groupMap(),
'stats' => Model::stats(),
'filters' => array('q' => $q, 'group' => $group, 'state' => $state),
'can_manage' => Permission::check('manage_blocks'),
'can_manage_php' => Permission::check('manage_php_blocks'),
'initial_tab' => Request::getStr('tab', '') === 'groups' ? 'groups' : 'blocks',
));
}
public function show(array $params = array())
{
$item = Model::one(isset($params['id']) ? (int) $params['id'] : 0);
if (!$item) {
return $this->error('Системный блок не найден', array(), 404);
}
if (($err = $this->guardPhpItem($item)) !== null) { return $err; }
return $this->success('', array('data' => $item));
}
public function aliasCheck(array $params = array())
{
if (!Permission::check('manage_blocks')) {
return $this->error('Недостаточно прав', array(), 403);
}
$alias = trim(Request::getStr('alias', ''));
$id = Request::getInt('id', 0);
$valid = true;
$available = true;
$message = 'Алиас свободен';
if ($alias === '') {
$valid = false;
$available = false;
$message = 'Укажите алиас';
} elseif (preg_match('/^[A-Za-z0-9-_]{1,20}$/i', $alias) !== 1 || is_numeric($alias)) {
$valid = false;
$available = false;
$message = 'Только A-Z, 0-9, дефис и подчёркивание, до 20 символов; не число';
} elseif (Model::aliasExists($alias, $id)) {
$available = false;
$message = 'Такой алиас уже используется';
}
return $this->success($message, array(
'data' => array(
'alias' => $alias,
'id' => $id,
'valid' => $valid,
'available' => $available,
),
));
}
public function store(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$input = Request::postAll();
if (($err = $this->guardPhpInput($input)) !== null) { return $err; }
$errors = $this->validate($input, 0);
if (!empty($errors)) {
return $this->error('Проверьте поля формы', $errors);
}
$id = Model::save(0, $input, Auth::id());
$this->audit('block.created', $id, array('editor' => $this->editor($input), 'php' => $this->editor($input) === 'php'));
return $this->success('Системный блок создан', array(
'data' => array('id' => $id),
'redirect' => $this->base() . '/blocks',
));
}
public function update(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$id = isset($params['id']) ? (int) $params['id'] : 0;
$before = Model::one($id);
if (!$before) {
return $this->error('Системный блок не найден', array(), 404);
}
if (($err = $this->guardPhpItem($before)) !== null) { return $err; }
$input = Request::postAll();
if (($err = $this->guardPhpInput($input)) !== null) { return $err; }
$errors = $this->validate($input, $id);
if (!empty($errors)) {
return $this->error('Проверьте поля формы', $errors);
}
Model::save($id, $input, Auth::id());
$this->audit('block.updated', $id, array(
'editor_before' => (string) $before['sysblock_editor'],
'editor_after' => $this->editor($input),
'code_changed' => !hash_equals(hash('sha256', (string) $before['sysblock_text']), hash('sha256', isset($input['sysblock_text']) ? (string) $input['sysblock_text'] : '')),
));
return $this->success('Системный блок сохранён', array('redirect' => $this->base() . '/blocks'));
}
public function copy(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$source = Model::one(isset($params['id']) ? (int) $params['id'] : 0);
if (!$source) { return $this->error('Системный блок не найден', array(), 404); }
if (($err = $this->guardPhpItem($source)) !== null) { return $err; }
try {
$newId = Model::copy((int) $source['id'], Request::postStr('name', ''), Auth::id());
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('block.copied', $newId, array('source_id' => (int) $source['id'], 'editor' => (string) $source['sysblock_editor']));
return $this->success('Копия создана', array('redirect' => $this->base() . '/blocks'));
}
public function revisions(array $params = array())
{
$id = isset($params['id']) ? (int) $params['id'] : 0;
$item = Model::one($id);
if (!$item) {
return $this->error('Системный блок не найден', array(), 404);
}
if (($err = $this->guardPhpItem($item)) !== null) { return $err; }
return $this->success('', array(
'data' => array(
'block' => array(
'id' => $item['id'],
'name' => $item['sysblock_name'],
'alias' => $item['sysblock_alias'],
),
'revisions' => Revisions::listForBlock($id),
),
));
}
public function revision(array $params = array())
{
$revision = Revisions::one(isset($params['revision']) ? (int) $params['revision'] : 0);
if (!$revision) {
return $this->error('Ревизия не найдена', array(), 404);
}
if (($err = $this->guardPhpSnapshot(isset($revision['snapshot']) ? $revision['snapshot'] : array())) !== null) { return $err; }
return $this->success('', array('data' => $revision));
}
public function restoreRevision(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$revisionId = isset($params['revision']) ? (int) $params['revision'] : 0;
$revision = Revisions::one($revisionId);
if (!$revision) { return $this->error('Ревизия не найдена', array(), 404); }
if (($err = $this->guardPhpSnapshot(isset($revision['snapshot']) ? $revision['snapshot'] : array())) !== null) { return $err; }
try {
$blockId = Revisions::restore($revisionId, Auth::id());
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('block.revision_restored', $blockId, array('revision_id' => $revisionId));
return $this->success('Блок восстановлен из ревизии', array(
'data' => array('id' => $blockId),
'redirect' => $this->base() . '/blocks',
));
}
public function deleteRevision(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$revisionId = isset($params['revision']) ? (int) $params['revision'] : 0;
$revision = Revisions::one($revisionId);
if (!$revision) { return $this->error('Ревизия не найдена', array(), 404); }
if (($err = $this->guardPhpSnapshot(isset($revision['snapshot']) ? $revision['snapshot'] : array())) !== null) { return $err; }
$blockId = Revisions::delete($revisionId);
if (!$blockId) {
return $this->error('Ревизия не найдена', array(), 404);
}
return $this->success('Ревизия удалена', array('data' => array('id' => $blockId)));
}
public function deleteRevisions(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$id = isset($params['id']) ? (int) $params['id'] : 0;
$item = Model::one($id);
if (!$item) {
return $this->error('Системный блок не найден', array(), 404);
}
if (($err = $this->guardPhpItem($item)) !== null) { return $err; }
$count = Revisions::deleteForBlock($id);
return $this->success('Ревизии удалены', array('data' => array('id' => $id, 'count' => $count)));
}
public function lint(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$result = Syntax::check(Request::postStr('sysblock_text', ''), Request::postBool('sysblock_eval', false) ? '1' : '0');
if (!$result['ok']) {
return $this->error($result['message'], array('sysblock_text' => $result['output']), 422);
}
return $this->success($result['message'], array('data' => $result));
}
public function clearCache(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$item = Model::one(isset($params['id']) ? (int) $params['id'] : 0);
if (!$item) {
return $this->error('Системный блок не найден', array(), 404);
}
Model::clearCache($item['id'], $item['sysblock_alias']);
return $this->success('Кеш системного блока очищен');
}
public function destroy(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
$id = isset($params['id']) ? (int) $params['id'] : 0;
$item = Model::one($id);
if (!$item) { return $this->error('Системный блок не найден', array(), 404); }
if (($err = $this->guardPhpItem($item)) !== null) { return $err; }
if (!Model::delete($id, Auth::id())) {
return $this->error('Системный блок не найден', array(), 404);
}
$this->audit('block.deleted', $id, array('editor' => (string) $item['sysblock_editor']));
return $this->success('Системный блок удалён');
}
public function group(array $params = array())
{
$item = Model::group(isset($params['id']) ? (int) $params['id'] : 0);
if (!$item) {
return $this->error('Группа не найдена', array(), 404);
}
return $this->success('', array('data' => $item));
}
public function storeGroup(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$id = Model::saveGroup(0, Request::postAll());
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Группа создана', array('data' => array('id' => $id), 'redirect' => $this->base() . '/blocks'));
}
public function updateGroup(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
Model::saveGroup(isset($params['id']) ? (int) $params['id'] : 0, Request::postAll());
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Группа сохранена', array('redirect' => $this->base() . '/blocks'));
}
public function reorderGroups(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
try {
$order = Request::post('order', array());
$ids = is_array($order) ? $order : array();
$count = Model::reorderGroups($ids);
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
return $this->success('Порядок групп сохранён', array('data' => array('count' => $count)));
}
public function destroyGroup(array $params = array())
{
if (($err = $this->guard()) !== null) {
return $err;
}
if (!Model::deleteGroup(isset($params['id']) ? (int) $params['id'] : 0)) {
return $this->error('Группа не найдена', array(), 404);
}
return $this->success('Группа удалена', array('redirect' => $this->base() . '/blocks'));
}
protected function guard()
{
return $this->guardPermission('manage_blocks');
}
protected function guardPhpInput(array $input)
{
return $this->editor($input) === 'php' && !Permission::check('manage_php_blocks')
? $this->error('Недостаточно прав для изменения исполняемого PHP-блока', array(), 403)
: null;
}
protected function guardPhpItem(array $item)
{
return isset($item['sysblock_editor']) && (string) $item['sysblock_editor'] === 'php'
? $this->guardPhpInput(array('sysblock_editor' => 'php'))
: null;
}
protected function guardPhpSnapshot($snapshot)
{
return is_array($snapshot) ? $this->guardPhpItem($snapshot) : null;
}
protected function editor(array $input)
{
$editor = isset($input['sysblock_editor']) ? (string) $input['sysblock_editor'] : '';
return in_array($editor, array('php', 'html', 'rich', 'text'), true) ? $editor : 'html';
}
protected function audit($action, $targetId, array $meta)
{
$user = Auth::user();
AuditLog::record($action, array(
'actor_id' => Auth::id(),
'actor_name' => $user && isset($user['name']) ? (string) $user['name'] : '',
'target_type' => 'block',
'target_id' => (int) $targetId,
'meta' => $meta,
));
}
protected function validate(array $input, $id)
{
$errors = array();
$name = trim(isset($input['sysblock_name']) ? (string) $input['sysblock_name'] : '');
$alias = trim(isset($input['sysblock_alias']) ? (string) $input['sysblock_alias'] : '');
if ($name === '') {
$errors['sysblock_name'] = 'Укажите название';
}
$aliasState = $this->validateAlias($alias, (int) $id);
if (!$aliasState['valid'] || !$aliasState['available']) {
$errors['sysblock_alias'] = $aliasState['message'];
}
$syntax = Syntax::check(
isset($input['sysblock_text']) ? (string) $input['sysblock_text'] : '',
!empty($input['sysblock_eval']) ? '1' : '0'
);
if (!empty($syntax['checked']) && empty($syntax['ok'])) {
$errors['sysblock_text'] = $syntax['line'] > 0
? 'PHP-синтаксис: ошибка на строке ' . (int) $syntax['line']
: 'PHP-синтаксис: ошибка в коде блока';
}
return $errors;
}
protected function validateAlias($alias, $id)
{
if ($alias === '') {
return array('valid' => false, 'available' => false, 'message' => 'Укажите алиас');
}
if (preg_match('/^[A-Za-z0-9-_]{1,20}$/i', $alias) !== 1 || is_numeric($alias)) {
return array('valid' => false, 'available' => false, 'message' => 'Только A-Z, 0-9, дефис и подчёркивание, до 20 символов; не число');
}
if (Model::aliasExists($alias, (int) $id)) {
return array('valid' => true, 'available' => false, 'message' => 'Такой алиас уже используется');
}
return array('valid' => true, 'available' => true, 'message' => 'Алиас свободен');
}
}