mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
361 lines
12 KiB
PHP
361 lines
12 KiB
PHP
<?php
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------------------
|
||
| AVE.cms
|
||
|--------------------------------------------------------------------------------------
|
||
| @package AVE.cms
|
||
| @file adminx/modules/Documents/Revisions.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\Documents;
|
||
|
||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||
|
||
use DB;
|
||
use App\Common\SystemTables;
|
||
use App\Content\Documents\DocumentRevisionPayload;
|
||
|
||
class Revisions
|
||
{
|
||
const FORMAT = 'ave.document-revision';
|
||
const VERSION = 2;
|
||
|
||
public static function listForDocument($documentId, $limit = 50)
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT * FROM ' . Model::revisionsTable() . ' WHERE doc_id = %i ORDER BY doc_revision DESC, Id DESC LIMIT %i',
|
||
(int) $documentId,
|
||
max(1, min(200, (int) $limit))
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = self::format($row, false);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function one($id)
|
||
{
|
||
$row = DB::query('SELECT * FROM ' . Model::revisionsTable() . ' WHERE Id = %i LIMIT 1', (int) $id)->getAssoc();
|
||
return $row ? self::format($row, true) : null;
|
||
}
|
||
|
||
public static function capture($documentId, $authorId = 0)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$document = Model::one($documentId);
|
||
if (!$document) { return 0; }
|
||
$values = Model::currentFieldValues($documentId);
|
||
$payload = self::payload($document, $values);
|
||
|
||
$last = DB::query(
|
||
'SELECT doc_data FROM ' . Model::revisionsTable() . ' WHERE doc_id = %i ORDER BY doc_revision DESC, Id DESC LIMIT 1',
|
||
$documentId
|
||
)->getValue();
|
||
$lastPayload = self::decodePayload((string) $last);
|
||
if (!empty($lastPayload['full']) && self::sameValues($payload, $lastPayload['raw'])) {
|
||
return 0;
|
||
}
|
||
|
||
$revisionTime = !empty($document['document_changed']) ? (int) $document['document_changed'] : time();
|
||
DB::Insert(Model::revisionsTable(), array(
|
||
'doc_id' => $documentId,
|
||
'doc_revision' => $revisionTime,
|
||
'doc_data' => serialize($payload),
|
||
'user_id' => (int) $authorId,
|
||
));
|
||
return (int) DB::insertId();
|
||
}
|
||
|
||
public static function restore($revisionId, $authorId = 0)
|
||
{
|
||
return self::restoreData($revisionId, (int) $authorId, null, null);
|
||
}
|
||
|
||
public static function restoreSelected($revisionId, array $documentKeys, array $fieldIds, $authorId = 0)
|
||
{
|
||
$documentKeys = array_values(array_unique(array_filter(array_map('strval', $documentKeys), 'strlen')));
|
||
$fieldIds = array_values(array_unique(array_filter(array_map('intval', $fieldIds), function ($id) { return $id > 0; })));
|
||
if (count($documentKeys) > 64 || count($fieldIds) > 1000) {
|
||
throw new \RuntimeException('В ревизии выбрано слишком много элементов');
|
||
}
|
||
|
||
if (empty($documentKeys) && empty($fieldIds)) {
|
||
throw new \RuntimeException('Выберите хотя бы один параметр или поле');
|
||
}
|
||
|
||
return self::restoreData($revisionId, (int) $authorId, $documentKeys, $fieldIds);
|
||
}
|
||
|
||
protected static function restoreData($revisionId, $authorId, $documentKeys, $fieldIds)
|
||
{
|
||
$revision = self::one($revisionId);
|
||
if (!$revision || !is_array($revision['values'])) {
|
||
throw new \RuntimeException('Ревизия не найдена');
|
||
}
|
||
|
||
$partial = is_array($documentKeys) || is_array($fieldIds);
|
||
$documentState = !empty($revision['document']) ? $revision['document'] : array();
|
||
$fieldValues = is_array($revision['values']) ? $revision['values'] : array();
|
||
if ($partial) {
|
||
$selection = DocumentRevisionPayload::select(
|
||
$documentState,
|
||
$fieldValues,
|
||
is_array($documentKeys) ? $documentKeys : array(),
|
||
is_array($fieldIds) ? $fieldIds : array()
|
||
);
|
||
$documentState = $selection['document'];
|
||
$fieldValues = $selection['fields'];
|
||
if (empty($documentState) && empty($fieldValues)) {
|
||
throw new \RuntimeException('Выбранных данных нет в этой ревизии');
|
||
}
|
||
}
|
||
|
||
$documentId = (int) $revision['doc_id'];
|
||
$ownsTransaction = !DB::$transaction_in_progress;
|
||
if ($ownsTransaction) { DB::startTransaction(); }
|
||
try {
|
||
self::capture($documentId, (int) $authorId);
|
||
if (!empty($documentState)) {
|
||
Model::restoreDocumentState($documentId, $documentState, (int) $authorId);
|
||
}
|
||
|
||
if (!empty($fieldValues)) {
|
||
Model::restoreFieldValues($documentId, $fieldValues);
|
||
}
|
||
|
||
if ($ownsTransaction) { DB::commit(); }
|
||
} catch (\Throwable $e) {
|
||
if ($ownsTransaction) { DB::rollback(); }
|
||
throw $e;
|
||
}
|
||
|
||
return $documentId;
|
||
}
|
||
|
||
public static function delete($revisionId)
|
||
{
|
||
$revision = self::one($revisionId);
|
||
if (!$revision) {
|
||
return false;
|
||
}
|
||
|
||
DB::Delete(Model::revisionsTable(), 'Id = %i', (int) $revisionId);
|
||
return (int) $revision['doc_id'];
|
||
}
|
||
|
||
public static function deleteForDocument($documentId)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
if ($documentId <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$count = (int) DB::query('SELECT COUNT(*) FROM ' . Model::revisionsTable() . ' WHERE doc_id = %i', $documentId)->getValue();
|
||
DB::Delete(Model::revisionsTable(), 'doc_id = %i', $documentId);
|
||
return $count;
|
||
}
|
||
|
||
protected static function format(array $row, $withData)
|
||
{
|
||
$created = isset($row['doc_revision']) ? (int) $row['doc_revision'] : 0;
|
||
$payload = self::decodePayload((string) $row['doc_data']);
|
||
$values = $withData ? $payload['fields'] : null;
|
||
$document = $withData ? $payload['document'] : null;
|
||
return array(
|
||
'id' => (int) $row['Id'],
|
||
'doc_id' => (int) $row['doc_id'],
|
||
'doc_revision' => $created,
|
||
'created_label' => $created > 0 ? date('d.m.Y H:i:s', $created) : '-',
|
||
'user_id' => (int) $row['user_id'],
|
||
'author_name' => self::authorName((int) $row['user_id']),
|
||
'fields_count' => count($payload['fields']),
|
||
'format_version' => $payload['full'] ? self::VERSION : 1,
|
||
'format_label' => $payload['full'] ? 'Полный снимок' : 'Только поля',
|
||
'size_label' => self::formatBytes(strlen((string) $row['doc_data'])),
|
||
'values' => $values,
|
||
'document' => $document,
|
||
'document_preview' => $withData ? self::documentPreview($payload['document']) : array(),
|
||
'preview' => $withData && is_array($values) ? self::preview($values) : array(),
|
||
);
|
||
}
|
||
|
||
protected static function payload(array $document, array $fields)
|
||
{
|
||
return DocumentRevisionPayload::create($document, $fields);
|
||
}
|
||
|
||
protected static function decodePayload($serialized)
|
||
{
|
||
return DocumentRevisionPayload::decodeSerialized($serialized);
|
||
}
|
||
|
||
protected static function documentPreview(array $document)
|
||
{
|
||
$labels = array(
|
||
'document_title' => 'Название', 'document_alias' => 'Alias (URL)', 'document_short_alias' => 'Короткий alias',
|
||
'document_breadcrumb_title' => 'Хлебная крошка', 'document_excerpt' => 'Краткое описание',
|
||
'document_meta_keywords' => 'Meta keywords', 'document_meta_description' => 'Meta description',
|
||
'document_meta_robots' => 'Meta robots', 'document_tags' => 'Теги', 'document_property' => 'Артикул / свойство',
|
||
'guid' => 'GUID', 'document_status' => 'Статус', 'document_in_search' => 'Поиск',
|
||
'document_parent' => 'Родитель', 'rubric_tmpl_id' => 'Шаблон', 'document_linked_navi_id' => 'Навигация',
|
||
'document_position' => 'Позиция', 'document_published' => 'Дата публикации', 'document_expire' => 'Дата окончания',
|
||
'document_author_id' => 'Автор', 'document_sitemap_freq' => 'Sitemap: частота', 'document_sitemap_pr' => 'Sitemap: приоритет',
|
||
);
|
||
$out = array();
|
||
foreach ($document as $key => $value) {
|
||
if (!isset($labels[$key])) { continue; }
|
||
if (in_array($key, array('document_published', 'document_expire'), true)) {
|
||
$value = (int) $value > 0 ? date('d.m.Y H:i:s', (int) $value) : 'не задано';
|
||
} elseif ($key === 'document_status') {
|
||
$value = (int) $value === 1 ? 'Опубликован' : 'Черновик';
|
||
} elseif ($key === 'document_in_search') {
|
||
$value = (int) $value === 1 ? 'В поиске' : 'Скрыт';
|
||
}
|
||
|
||
$out[] = array('key' => $key, 'title' => $labels[$key], 'value' => (string) $value, 'value_preview' => self::shorten((string) $value, 420));
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
protected static function preview(array $values)
|
||
{
|
||
$fieldIds = array();
|
||
foreach (array_keys($values) as $fieldId) {
|
||
$fieldId = (int) $fieldId;
|
||
if ($fieldId > 0) { $fieldIds[] = $fieldId; }
|
||
}
|
||
|
||
$fields = array();
|
||
if (!empty($fieldIds)) {
|
||
$rows = DB::query(
|
||
'SELECT Id, rubric_field_title, rubric_field_alias, rubric_field_type FROM ' . Model::fieldsTable()
|
||
. ' WHERE Id IN (' . implode(',', $fieldIds) . ')'
|
||
)->getAll();
|
||
foreach ($rows as $row) {
|
||
$fields[(int) $row['Id']] = array(
|
||
'title' => self::decode(isset($row['rubric_field_title']) ? $row['rubric_field_title'] : ''),
|
||
'alias' => isset($row['rubric_field_alias']) ? (string) $row['rubric_field_alias'] : '',
|
||
'type' => isset($row['rubric_field_type']) ? (string) $row['rubric_field_type'] : '',
|
||
);
|
||
}
|
||
}
|
||
|
||
$out = array();
|
||
foreach ($values as $fieldId => $value) {
|
||
$meta = isset($fields[(int) $fieldId]) ? $fields[(int) $fieldId] : array();
|
||
$out[] = array(
|
||
'field_id' => (int) $fieldId,
|
||
'title' => !empty($meta['title']) ? $meta['title'] : (!empty($meta['alias']) ? $meta['alias'] : 'Поле #' . (int) $fieldId),
|
||
'alias' => isset($meta['alias']) ? $meta['alias'] : '',
|
||
'type' => isset($meta['type']) ? $meta['type'] : '',
|
||
'value' => (string) $value,
|
||
'value_preview' => self::shorten((string) $value, 420),
|
||
'size_label' => self::formatBytes(strlen((string) $value)),
|
||
);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
protected static function decode($value)
|
||
{
|
||
return htmlspecialchars_decode(stripcslashes((string) $value), ENT_QUOTES);
|
||
}
|
||
|
||
protected static function sameValues(array $a, array $b)
|
||
{
|
||
ksort($a);
|
||
ksort($b);
|
||
return serialize($a) === serialize($b);
|
||
}
|
||
|
||
protected static function authorName($id)
|
||
{
|
||
if ((int) $id <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$columns = self::userColumns();
|
||
$idField = in_array('id', $columns, true) ? 'id' : (in_array('Id', $columns, true) ? 'Id' : 'id');
|
||
$row = DB::query('SELECT * FROM ' . SystemTables::table('users') . ' WHERE ' . $idField . ' = %i LIMIT 1', (int) $id)->getAssoc();
|
||
if (!$row) {
|
||
return '#' . (int) $id;
|
||
}
|
||
|
||
$legacyName = trim((isset($row['firstname']) ? (string) $row['firstname'] : '') . ' ' . (isset($row['lastname']) ? (string) $row['lastname'] : ''));
|
||
if ($legacyName !== '') {
|
||
return $legacyName;
|
||
}
|
||
|
||
if (!empty($row['name'])) {
|
||
return (string) $row['name'];
|
||
}
|
||
|
||
if (!empty($row['login'])) {
|
||
return '@' . (string) $row['login'];
|
||
}
|
||
|
||
if (!empty($row['user_name'])) {
|
||
return '@' . (string) $row['user_name'];
|
||
}
|
||
|
||
if (!empty($row['email'])) {
|
||
return (string) $row['email'];
|
||
}
|
||
|
||
return '#' . (int) $id;
|
||
}
|
||
|
||
protected static function userColumns()
|
||
{
|
||
static $columns = null;
|
||
if ($columns !== null) {
|
||
return $columns;
|
||
}
|
||
|
||
$columns = array();
|
||
$rows = DB::query('SHOW COLUMNS FROM ' . SystemTables::table('users'))->getAll();
|
||
foreach ($rows as $row) {
|
||
$columns[] = (string) $row['Field'];
|
||
}
|
||
|
||
return $columns;
|
||
}
|
||
|
||
protected static function shorten($value, $limit)
|
||
{
|
||
$value = trim((string) $value);
|
||
if (function_exists('mb_strlen') && mb_strlen($value, 'UTF-8') > $limit) {
|
||
return mb_substr($value, 0, $limit, 'UTF-8') . '...';
|
||
}
|
||
|
||
if (strlen($value) > $limit) {
|
||
return substr($value, 0, $limit) . '...';
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
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';
|
||
}
|
||
}
|