Files
2026-07-27 12:58:44 +03:00

336 lines
10 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/Templates/Model.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\Templates;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use DB;
use App\Common\FileCacheInvalidator;
use App\Common\SystemTables;
use App\Content\ContentTables;
use App\Content\Templates\TemplateMutationService;
use App\Frontend\PageTemplateRepository;
use App\Helpers\File;
class Model
{
public static function table()
{
return ContentTables::table('templates');
}
public static function rubricsTable()
{
return ContentTables::table('rubrics');
}
public static function all($q = '')
{
$sql = 'SELECT t.*, u.name AS author_name, u.login AS author_login, u.email AS author_email'
. ', ' . self::usedCountExpression() . ' AS used_count'
. ' FROM ' . self::table() . ' t'
. ' LEFT JOIN ' . SystemTables::table('users') . ' u ON u.id = t.template_author_id'
. ' WHERE 1=1';
$args = array();
$q = trim((string) $q);
if ($q !== '') {
$sql .= ' AND (t.template_title LIKE %ss OR t.template_text LIKE %ss OR t.Id = %i)';
$args[] = $q;
$args[] = $q;
$args[] = (int) $q;
}
$sql .= ' ORDER BY t.Id ASC';
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
$out = array();
foreach ($rows as $row) {
$out[] = self::row($row);
}
return $out;
}
public static function one($id)
{
$row = DB::query(
'SELECT t.*, u.name AS author_name, u.login AS author_login, u.email AS author_email'
. ', ' . self::usedCountExpression() . ' AS used_count'
. ' FROM ' . self::table() . ' t'
. ' LEFT JOIN ' . SystemTables::table('users') . ' u ON u.id = t.template_author_id'
. ' WHERE t.Id = %i LIMIT 1',
(int) $id
)->getAssoc();
return $row ? self::row($row, true) : null;
}
public static function raw($id)
{
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE Id = %i LIMIT 1', (int) $id)->getAssoc();
return $row ? $row : null;
}
public static function revisionFields()
{
return array('Id', 'template_title', 'template_text', 'template_author_id', 'template_created');
}
public static function snapshot(array $row)
{
$out = array();
foreach (self::revisionFields() as $field) {
$out[$field] = isset($row[$field]) ? $row[$field] : '';
}
$out['Id'] = (int) $out['Id'];
$out['template_author_id'] = (int) $out['template_author_id'];
$out['template_created'] = (int) $out['template_created'];
return $out;
}
public static function applySnapshot($id, array $snapshot, $authorId)
{
$id = (int) $id;
$current = self::raw($id);
if (!$current) {
throw new \RuntimeException('Шаблон не найден');
}
$data = self::snapshot($snapshot);
unset($data['Id']);
$data['template_author_id'] = (int) $authorId > 0 ? (int) $authorId : (int) $data['template_author_id'];
DB::Update(self::table(), $data, 'Id = %i', $id);
self::writeCache($id, $data['template_text']);
return $id;
}
public static function stats()
{
return array(
'total' => (int) DB::query('SELECT COUNT(*) FROM ' . self::table())->getValue(),
'used' => self::tableExists(self::rubricsTable()) ? (int) DB::query('SELECT COUNT(DISTINCT rubric_template_id) FROM ' . self::rubricsTable())->getValue() : 0,
'php' => (int) DB::query('SELECT COUNT(*) FROM ' . self::table() . ' WHERE template_text LIKE %s', '%<?%')->getValue(),
'bytes' => (int) DB::query('SELECT COALESCE(SUM(CHAR_LENGTH(template_text)), 0) FROM ' . self::table())->getValue(),
);
}
public static function rebuildCaches()
{
$rows = DB::query('SELECT Id, template_text FROM ' . self::table())->getAll();
$count = 0;
foreach ($rows as $row) {
self::writeCache((int) $row['Id'], (string) $row['template_text']);
$count++;
}
return $count;
}
public static function save($id, array $input, $authorId)
{
$isUpdate = (int) $id > 0;
$templateId = (new TemplateMutationService())->save((int) $id, array(
'template_title' => isset($input['template_title']) ? $input['template_title'] : '',
'template_text' => isset($input['template_text']) ? $input['template_text'] : '',
), (int) $authorId);
Revisions::capture(
$templateId,
$isUpdate ? 'update' : 'create',
(int) $authorId,
$isUpdate ? 'Сохранение шаблона' : 'Создание шаблона'
);
return $templateId;
}
public static function copy($id, $title, $authorId)
{
$row = self::one($id);
if (!$row) {
throw new \RuntimeException('Шаблон не найден');
}
$title = trim((string) $title);
if ($title === '') {
$title = $row['template_title'] . ' (копия)';
}
$data = array(
'template_title' => self::uniqueTitle($title),
'template_text' => self::encodeText((string) $row['template_text']),
'template_author_id' => (int) $authorId > 0 ? (int) $authorId : 1,
'template_created' => time(),
);
DB::Insert(self::table(), $data);
$newId = (int) DB::insertId();
self::writeCache($newId, $data['template_text']);
Revisions::capture($newId, 'copy', (int) $authorId, 'Копия шаблона #' . (int) $id);
return $newId;
}
public static function delete($id, $authorId = 0)
{
$id = (int) $id;
if ($id === 1) {
throw new \RuntimeException('Шаблон #1 является системным и не может быть удалён');
}
if (self::usedCount($id) > 0) {
throw new \RuntimeException('Шаблон используется рубриками и не может быть удалён');
}
$raw = self::raw($id);
if (!$raw) {
return false;
}
Revisions::capture($id, 'delete', (int) $authorId, 'Удаление шаблона', $raw);
return (new TemplateMutationService())->delete($id);
}
public static function clearCache($id)
{
self::removeCache((int) $id);
}
public static function usedCount($id)
{
if (!self::tableExists(self::rubricsTable())) {
return 0;
}
return (int) DB::query('SELECT COUNT(*) FROM ' . self::rubricsTable() . ' WHERE rubric_template_id = %i', (int) $id)->getValue();
}
protected static function usedCountExpression()
{
if (!self::tableExists(self::rubricsTable())) {
return '0';
}
return '(SELECT COUNT(*) FROM ' . self::rubricsTable() . ' r WHERE r.rubric_template_id = t.Id)';
}
protected static function tableExists($table)
{
return (bool) DB::query('SHOW TABLES LIKE %s', (string) $table)->getValue();
}
protected static function input(array $input)
{
return array(
'template_title' => trim(isset($input['template_title']) ? (string) $input['template_title'] : ''),
'template_text' => self::encodeText(isset($input['template_text']) ? (string) $input['template_text'] : ''),
);
}
protected static function row(array $row, $withText = false)
{
$author = '';
if (!empty($row['author_name'])) {
$author = (string) $row['author_name'];
} elseif (!empty($row['author_login'])) {
$author = '@' . (string) $row['author_login'];
} elseif (!empty($row['author_email'])) {
$author = (string) $row['author_email'];
}
$cacheText = self::readCache((int) $row['Id']);
// Legacy templates are escaped in SQL. The editor must always return
// the database value decoded; an old cache may still contain slashes.
$text = self::decodeText((string) $row['template_text']);
$row['template_text'] = $text;
$row['Id'] = (int) $row['Id'];
$row['id'] = (int) $row['Id'];
$row['template_author_id'] = (int) $row['template_author_id'];
$row['template_created'] = (int) $row['template_created'];
$row['used_count'] = isset($row['used_count']) ? (int) $row['used_count'] : self::usedCount($row['Id']);
$row['author'] = $author !== '' ? $author : ('#' . $row['template_author_id']);
$row['created_label'] = $row['template_created'] > 0 ? date('d.m.Y H:i', $row['template_created']) : '-';
$row['text_size'] = strlen($text);
$row['text_size_label'] = self::formatBytes($row['text_size']);
$row['has_php'] = Syntax::hasPhp($text);
$row['cache_exists'] = $cacheText !== null;
$row['can_delete'] = $row['Id'] !== 1 && $row['used_count'] <= 0;
return $row;
}
protected static function uniqueTitle($base)
{
$base = trim((string) $base);
if ($base === '') {
$base = 'Новый шаблон';
}
$title = $base;
$i = 2;
while (DB::query('SELECT Id FROM ' . self::table() . ' WHERE template_title = %s LIMIT 1', $title)->getValue()) {
$title = $base . ' #' . $i;
$i++;
}
return $title;
}
protected static function cacheFile($id)
{
return PageTemplateRepository::cacheFilePath((int) $id);
}
protected static function readCache($id)
{
$file = self::cacheFile((int) $id);
return is_file($file) ? File::getContent($file) : null;
}
protected static function writeCache($id, $text)
{
$file = self::cacheFile((int) $id);
PageTemplateRepository::prepareCacheDirectory($file);
File::putAtomic($file, self::decodeText((string) $text));
FileCacheInvalidator::pageTemplates();
}
public static function decodeText($text)
{
return stripslashes((string) $text);
}
protected static function encodeText($text)
{
return addslashes((string) $text);
}
protected static function formatBytes($bytes)
{
$bytes = (int) $bytes;
if ($bytes < 1024) {
return $bytes . ' Б';
}
if ($bytes < 1048576) {
return round($bytes / 1024, 1) . ' KB';
}
return round($bytes / 1048576, 1) . ' MB';
}
protected static function removeCache($id)
{
$file = self::cacheFile((int) $id);
if ($file !== null && is_file($file)) {
File::delete($file);
}
}
}