mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 00:45:44 +00:00
2613 lines
102 KiB
PHP
2613 lines
102 KiB
PHP
<?php
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------------------
|
||
| AVE.cms
|
||
|--------------------------------------------------------------------------------------
|
||
| @package AVE.cms
|
||
| @file adminx/modules/Documents/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\Documents;
|
||
|
||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||
|
||
use DB;
|
||
use App\Helpers\Str;
|
||
use App\Content\Fields\FieldValidator;
|
||
use App\Content\Fields\FieldConditionEvaluator;
|
||
use App\Content\Fields\FieldValueCodec;
|
||
use App\Content\Fields\FieldContext;
|
||
use App\Content\Fields\FieldRegistry;
|
||
use App\Content\Fields\FieldLifecycle;
|
||
use App\Content\Fields\FieldSettings;
|
||
use App\Content\ContentTables;
|
||
use App\Content\DocumentTerms;
|
||
use App\Content\Documents\ContentCacheInvalidator;
|
||
use App\Content\Documents\DocumentAliasRegistry;
|
||
use App\Content\Documents\DocumentAliasTemplate;
|
||
use App\Content\Documents\DocumentMediaDraft;
|
||
use App\Content\Documents\DocumentMediaPath;
|
||
use App\Content\Documents\DocumentSearch;
|
||
use App\Common\Settings;
|
||
use App\Common\SystemTables;
|
||
use App\Common\Lifecycle;
|
||
use App\Common\DatabaseSchema;
|
||
use App\Adminx\Support\AdminLocale;
|
||
use App\Adminx\Rubrics\FieldAdminEditors;
|
||
use App\Adminx\Catalog\Model as CatalogModel;
|
||
|
||
class Model
|
||
{
|
||
public static function documentsTable() { return ContentTables::table('documents'); }
|
||
public static function rubricsTable() { return ContentTables::table('rubrics'); }
|
||
public static function fieldsTable() { return ContentTables::table('rubric_fields'); }
|
||
public static function groupsTable() { return ContentTables::table('rubric_fields_group'); }
|
||
public static function docFieldsTable() { return ContentTables::table('document_fields'); }
|
||
public static function docFieldsTextTable() { return ContentTables::table('document_fields_text'); }
|
||
public static function revisionsTable() { return ContentTables::table('document_rev'); }
|
||
public static function aliasHistoryTable() { return ContentTables::table('document_alias_history'); }
|
||
public static function keywordsTable() { return ContentTables::table('document_keywords'); }
|
||
public static function tagsTable() { return ContentTables::table('document_tags'); }
|
||
public static function remarksTable() { return ContentTables::table('document_remarks'); }
|
||
public static function viewCountTable() { return ContentTables::table('view_count'); }
|
||
|
||
public static function items(array $filters)
|
||
{
|
||
$page = max(1, isset($filters['page']) ? (int) $filters['page'] : 1);
|
||
$allowedPerPage = array(25, 50, 100, 300);
|
||
$perPage = isset($filters['per_page']) ? (int) $filters['per_page'] : 25;
|
||
$limit = in_array($perPage, $allowedPerPage, true) ? $perPage : 25;
|
||
$offset = ($page - 1) * $limit;
|
||
$where = ' WHERE 1=1';
|
||
$args = array();
|
||
$score = '0';
|
||
$scoreArgs = array();
|
||
|
||
$rubricId = isset($filters['rubric_id']) ? (int) $filters['rubric_id'] : 0;
|
||
$q = trim(isset($filters['q']) ? (string) $filters['q'] : '');
|
||
$field = self::normalizeSearchField(isset($filters['field']) ? (string) $filters['field'] : '', $rubricId);
|
||
if ($q !== '') {
|
||
if ($field !== '') {
|
||
$criteria = DocumentSearch::valueCriteria($q, array('df.field_value', 'dft.field_value'));
|
||
// Поиск по значению выбранного поля рубрики (напр. «Артикул»).
|
||
$where .= ' AND EXISTS (SELECT 1 FROM ' . self::docFieldsTable() . ' df'
|
||
. ' INNER JOIN ' . self::fieldsTable() . ' rf ON rf.Id = df.rubric_field_id'
|
||
. ' LEFT JOIN ' . self::docFieldsTextTable() . ' dft ON dft.document_id = df.document_id AND dft.rubric_field_id = df.rubric_field_id'
|
||
. ' WHERE df.document_id = d.Id AND rf.rubric_field_title = %s'
|
||
. ' AND ' . $criteria['where'] . ')';
|
||
$args[] = $field;
|
||
$args = array_merge($args, $criteria['where_args']);
|
||
} else {
|
||
$criteria = DocumentSearch::criteria($q, 'd');
|
||
$where .= ' AND ' . $criteria['where'];
|
||
$args = array_merge($args, $criteria['where_args']);
|
||
$score = $criteria['score'];
|
||
$scoreArgs = $criteria['score_args'];
|
||
}
|
||
}
|
||
|
||
if ($rubricId > 0) {
|
||
$where .= ' AND d.rubric_id = %i';
|
||
$args[] = $rubricId;
|
||
}
|
||
|
||
$state = isset($filters['state']) ? (string) $filters['state'] : '';
|
||
if ($state === 'active') {
|
||
$where .= " AND d.document_status = '1' AND d.document_deleted != '1'";
|
||
} elseif ($state === 'draft') {
|
||
$where .= " AND d.document_status = '0' AND d.document_deleted != '1'";
|
||
} elseif ($state === 'deleted') {
|
||
$where .= " AND d.document_deleted = '1'";
|
||
} else {
|
||
$where .= " AND d.document_deleted != '1'";
|
||
}
|
||
|
||
$countArgs = array_merge(array('SELECT COUNT(*) FROM ' . self::documentsTable() . ' d' . $where), $args);
|
||
$total = (int) call_user_func_array(array('DB', 'query'), $countArgs)->getValue();
|
||
|
||
$sql = 'SELECT d.*, r.rubric_title, r.rubric_alias,(' . $score . ') AS search_relevance'
|
||
. ' FROM ' . self::documentsTable() . ' d'
|
||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||
. $where
|
||
. ' ORDER BY search_relevance DESC,d.document_changed DESC,d.Id DESC'
|
||
. ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset;
|
||
$rows = call_user_func_array(
|
||
array('DB', 'query'),
|
||
array_merge(array($sql), $scoreArgs, $args)
|
||
)->getAll();
|
||
|
||
$items = array();
|
||
foreach ($rows as $row) {
|
||
$items[] = self::row($row);
|
||
}
|
||
|
||
return array(
|
||
'items' => $items,
|
||
'pagination' => array(
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'total' => $total,
|
||
'pages' => max(1, (int) ceil($total / $limit)),
|
||
'from' => $total > 0 ? $offset + 1 : 0,
|
||
'to' => min($total, $offset + count($items)),
|
||
),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Поля рубрик (по названию) для выбора «искать по полю». Если задана
|
||
* рубрика — только её поля (у каждой рубрики свой набор), иначе все.
|
||
* Идентифицируем по названию, т.к. alias есть не у всех полей.
|
||
*/
|
||
public static function searchableFields($rubricId = 0)
|
||
{
|
||
$rubricId = (int) $rubricId;
|
||
$sql = 'SELECT rubric_field_title AS title'
|
||
. ' FROM ' . self::fieldsTable()
|
||
. " WHERE TRIM(rubric_field_title) <> ''";
|
||
$args = array();
|
||
if ($rubricId > 0) {
|
||
$sql .= ' AND rubric_id = %i';
|
||
$args[] = $rubricId;
|
||
}
|
||
|
||
$sql .= ' GROUP BY rubric_field_title ORDER BY rubric_field_title ASC';
|
||
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
|
||
|
||
$fields = array();
|
||
foreach ($rows as $row) {
|
||
$title = trim((string) $row['title']);
|
||
if ($title === '') { continue; }
|
||
$fields[] = array('value' => $title, 'title' => $title);
|
||
}
|
||
|
||
return $fields;
|
||
}
|
||
|
||
/** Валидирует выбранное поле поиска по списку доступных полей рубрики. */
|
||
public static function normalizeSearchField($field, $rubricId = 0)
|
||
{
|
||
$field = trim((string) $field);
|
||
if ($field === '') {
|
||
return '';
|
||
}
|
||
|
||
foreach (self::searchableFields($rubricId) as $item) {
|
||
if ($item['value'] === $field) {
|
||
return $field;
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
public static function one($id)
|
||
{
|
||
$row = DB::query(
|
||
'SELECT d.*, r.rubric_title, r.rubric_alias FROM ' . self::documentsTable() . ' d'
|
||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||
. ' WHERE d.Id = %i LIMIT 1',
|
||
(int) $id
|
||
)->getAssoc();
|
||
return $row ? self::row($row, true) : null;
|
||
}
|
||
|
||
public static function blank($rubricId = 0)
|
||
{
|
||
$rubricId = self::blankRubricId((int) $rubricId);
|
||
$rubric = $rubricId > 0 ? self::rubric($rubricId) : null;
|
||
return array(
|
||
'Id' => 0,
|
||
'rubric_id' => $rubricId,
|
||
'document_title' => '',
|
||
'document_alias' => '',
|
||
'document_alias_header' => 301,
|
||
'document_alias_history' => 0,
|
||
'document_short_alias' => '',
|
||
'document_breadcrumb_title' => '',
|
||
'document_excerpt' => '',
|
||
'document_meta_keywords' => '',
|
||
'document_meta_description' => '',
|
||
'document_meta_robots' => '',
|
||
'document_sitemap_freq' => 3,
|
||
'document_sitemap_pr' => 0.5,
|
||
'document_tags' => '',
|
||
'document_property' => '',
|
||
'guid' => '',
|
||
'document_status' => 1,
|
||
'document_in_search' => 1,
|
||
'document_author_id' => 1,
|
||
'document_parent' => 0,
|
||
'rubric_tmpl_id' => 0,
|
||
'document_linked_navi_id' => 0,
|
||
'document_position' => 0,
|
||
'document_deleted' => 0,
|
||
'document_version' => 1,
|
||
'parent_info' => null,
|
||
'published_input' => date('Y-m-d\\TH:i'),
|
||
'expire_input' => '',
|
||
'changed_label' => 'ещё не сохранён',
|
||
'published_label' => 'после сохранения',
|
||
'state_label' => 'Новый',
|
||
'state_badge' => 'blue',
|
||
'rubric_title' => $rubric ? self::decode($rubric['rubric_title']) : '',
|
||
'fields_count' => 0,
|
||
);
|
||
}
|
||
|
||
public static function blankRubricId($preferred = 0)
|
||
{
|
||
if ((int) $preferred > 0 && self::rubric((int) $preferred)) {
|
||
return (int) $preferred;
|
||
}
|
||
|
||
$rubrics = self::rubrics();
|
||
return !empty($rubrics) ? (int) $rubrics[0]['Id'] : 0;
|
||
}
|
||
|
||
public static function fieldsForDocument($documentId, $mediaDraftToken = '')
|
||
{
|
||
$doc = self::one((int) $documentId);
|
||
if (!$doc) {
|
||
return array();
|
||
}
|
||
|
||
return self::fieldsForRubric((int) $doc['rubric_id'], (int) $documentId, $mediaDraftToken);
|
||
}
|
||
|
||
public static function fieldsForRubric($rubricId, $documentId = 0, $mediaDraftToken = '', array $initialValues = array())
|
||
{
|
||
$conditionsEnabled = self::rubricConditionsEnabled((int) $rubricId);
|
||
$groupSettings = $conditionsEnabled && self::hasGroupSettingsColumn() ? ', g.group_settings' : ', NULL AS group_settings';
|
||
$rows = DB::query(
|
||
'SELECT f.*, g.group_title, g.group_description, g.group_position' . $groupSettings . ','
|
||
. ' df.Id AS document_field_id, df.field_value, df.field_number_value, df.document_in_search AS field_in_search,'
|
||
. ' dft.field_value AS field_value_more'
|
||
. ' FROM ' . self::fieldsTable() . ' f'
|
||
. ' LEFT JOIN ' . self::groupsTable() . ' g ON g.Id = f.rubric_field_group'
|
||
. ' LEFT JOIN ' . self::docFieldsTable() . ' df ON df.rubric_field_id = f.Id AND df.document_id = %i'
|
||
. ' LEFT JOIN ' . self::docFieldsTextTable() . ' dft ON dft.rubric_field_id = f.Id AND dft.document_id = %i'
|
||
. ' WHERE f.rubric_id = %i'
|
||
. ' ORDER BY COALESCE(g.group_position, 999999) ASC, f.rubric_field_position ASC, f.Id ASC',
|
||
(int) $documentId,
|
||
(int) $documentId,
|
||
(int) $rubricId
|
||
)->getAll();
|
||
|
||
if ((int) $documentId <= 0 && $initialValues) {
|
||
foreach ($rows as &$initialRow) {
|
||
$alias = (string) $initialRow['rubric_field_alias'];
|
||
if (array_key_exists($alias, $initialValues) && (is_scalar($initialValues[$alias]) || $initialValues[$alias] === null)) {
|
||
$initialRow['__initial_value'] = (string) $initialValues[$alias];
|
||
}
|
||
}
|
||
|
||
unset($initialRow);
|
||
}
|
||
|
||
$catalogAllowed = array();
|
||
foreach ($rows as $catalogRow) {
|
||
$catalogSettings = FieldSettings::effective($catalogRow);
|
||
if (array_key_exists('placed', $catalogSettings) && empty($catalogSettings['placed'])) { continue; }
|
||
if ((string) $catalogRow['rubric_field_type'] !== 'catalog') { continue; }
|
||
$catalogRaw = array_key_exists('__initial_value', $catalogRow)
|
||
? (string) $catalogRow['__initial_value']
|
||
: ($catalogRow['document_field_id'] ? (string) $catalogRow['field_value'] . (string) $catalogRow['field_value_more'] : (string) $catalogRow['rubric_field_default']);
|
||
$selected = CatalogModel::parseValue($catalogRaw);
|
||
$allowed = CatalogModel::allowedFieldIds((int) $rubricId, (int) $catalogRow['Id'], $selected['catalog_ids']);
|
||
if (!empty($allowed)) { $catalogAllowed = array_values(array_unique(array_merge($catalogAllowed, $allowed))); }
|
||
}
|
||
|
||
$groups = array();
|
||
foreach ($rows as $row) {
|
||
$settings = FieldSettings::effective($row);
|
||
if (array_key_exists('placed', $settings) && empty($settings['placed'])) { continue; }
|
||
$groupId = (int) $row['rubric_field_group'];
|
||
if (!isset($groups[$groupId])) {
|
||
$groups[$groupId] = array(
|
||
'id' => $groupId,
|
||
'title' => $groupId > 0 && !empty($row['group_title']) ? self::decode($row['group_title']) : 'Без группы',
|
||
'description' => $groupId > 0 && !empty($row['group_description']) ? self::decode($row['group_description']) : '',
|
||
'condition' => $conditionsEnabled && $groupId > 0 ? FieldConditionEvaluator::groupCondition($row) : array(),
|
||
'items' => array(),
|
||
);
|
||
}
|
||
|
||
$field = self::fieldRow($row, (int) $documentId, $mediaDraftToken, $conditionsEnabled);
|
||
$field['catalog_hidden'] = !empty($catalogAllowed) && !in_array((int) $row['Id'], $catalogAllowed, true);
|
||
$groups[$groupId]['items'][] = $field;
|
||
}
|
||
|
||
return array_values($groups);
|
||
}
|
||
|
||
/**
|
||
* Валидация значений полей документа по JSON-настройкам (rubric_field_settings.rules).
|
||
* Возвращает ошибки в формате ['fields[<Id>]' => сообщение]. Пусто, если правил нет.
|
||
*/
|
||
public static function validateFieldValues($rubricId, array $values, $documentId = 0, $fieldGroups = null)
|
||
{
|
||
$rubricId = (int) $rubricId;
|
||
if ($rubricId <= 0) {
|
||
return array();
|
||
}
|
||
|
||
$fields = array();
|
||
$effectiveValues = array();
|
||
foreach (self::resolveFieldGroups($rubricId, (int) $documentId, $fieldGroups) as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fields[] = $field;
|
||
$effectiveValues[(int) $field['Id']] = isset($field['field_value']) ? $field['field_value'] : '';
|
||
}
|
||
}
|
||
|
||
foreach ($values as $id => $value) {
|
||
$effectiveValues[(int) $id] = $value;
|
||
}
|
||
|
||
return FieldValidator::validateValues($fields, $effectiveValues, self::rubricConditionsEnabled($rubricId));
|
||
}
|
||
|
||
public static function computedFieldValues($rubricId, array $values, array $document = array(), $documentId = 0, $fieldGroups = null)
|
||
{
|
||
$definitions = array();
|
||
$effectiveValues = array();
|
||
foreach (self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups) as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fieldId = (int) $field['Id'];
|
||
$definitions[$fieldId] = $field;
|
||
$effectiveValues[$fieldId] = isset($field['field_value']) ? $field['field_value'] : '';
|
||
}
|
||
}
|
||
|
||
foreach ($values as $fieldId => $value) {
|
||
$effectiveValues[(int) $fieldId] = $value;
|
||
}
|
||
|
||
$computedValues = \App\Content\Fields\ComputedFieldEvaluator::apply($definitions, $effectiveValues, $document);
|
||
foreach ($definitions as $fieldId => $field) {
|
||
if ((string) $field['rubric_field_type'] === 'computed' && array_key_exists($fieldId, $computedValues)) {
|
||
$values[$fieldId] = $computedValues[$fieldId];
|
||
}
|
||
}
|
||
|
||
return $values;
|
||
}
|
||
|
||
/** Remove values of fields hidden or locked by rubric form conditions. */
|
||
public static function conditionalFieldValues($rubricId, array $values, $documentId = 0, $fieldGroups = null)
|
||
{
|
||
if (!self::rubricConditionsEnabled((int) $rubricId)) {
|
||
return $values;
|
||
}
|
||
|
||
$fields = array();
|
||
$fieldMap = array();
|
||
$storedValues = array();
|
||
$effectiveValues = array();
|
||
foreach (self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups) as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fields[] = $field;
|
||
$fieldId = (int) $field['Id'];
|
||
$fieldMap[$fieldId] = $field;
|
||
$storedValues[$fieldId] = (int) $documentId > 0 && isset($field['field_value']) ? $field['field_value'] : '';
|
||
$effectiveValues[$fieldId] = isset($field['field_value']) ? $field['field_value'] : '';
|
||
}
|
||
}
|
||
|
||
foreach ($values as $id => $value) {
|
||
$effectiveValues[(int) $id] = $value;
|
||
}
|
||
|
||
$actions = FieldConditionEvaluator::valueActions($fields, $storedValues, $effectiveValues, (int) $documentId <= 0);
|
||
foreach ($values as $fieldId => $value) {
|
||
if (is_array($value) && !empty($value['_condition_action_override'])) {
|
||
unset($actions[(int) $fieldId]);
|
||
}
|
||
}
|
||
|
||
foreach ($actions as $fieldId => $action) {
|
||
if (!isset($fieldMap[$fieldId])) { continue; }
|
||
$values[$fieldId] = self::conditionActionInput($fieldMap[$fieldId], $action);
|
||
$effectiveValues[$fieldId] = $values[$fieldId];
|
||
}
|
||
|
||
$states = FieldConditionEvaluator::states($fields, $effectiveValues);
|
||
foreach ($states as $id => $state) {
|
||
if ((empty($state['visible']) || !empty($state['locked'])) && !isset($actions[$id])) {
|
||
unset($values[$id], $values[(string) $id]);
|
||
}
|
||
}
|
||
|
||
return $values;
|
||
}
|
||
|
||
protected static function conditionActionInput(array $field, array $action)
|
||
{
|
||
$value = isset($action['action']) && (string) $action['action'] === 'set'
|
||
? (string) (isset($action['value']) ? $action['value'] : '') : '';
|
||
$kind = isset($field['editor_kind']) ? (string) $field['editor_kind'] : 'text';
|
||
if ($kind === 'boolean') { return array('checked' => in_array(strtolower($value), array('1', 'true', 'yes', 'on'), true)); }
|
||
if ($kind === 'date') { return array('date' => $value); }
|
||
if ($kind === 'datetime') { return array('datetime' => $value); }
|
||
if ($kind === 'number') { return array('number' => $value); }
|
||
if ($kind === 'choice') { return array('selected' => $value); }
|
||
|
||
return array('raw' => $value);
|
||
}
|
||
|
||
public static function saveFields($documentId, array $values, $fieldGroups = null)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$doc = self::one($documentId);
|
||
if (!$doc) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
$fields = self::resolveFieldGroups((int) $doc['rubric_id'], $documentId, $fieldGroups);
|
||
$values = self::conditionalFieldValues((int) $doc['rubric_id'], $values, $documentId, $fields);
|
||
$values = self::computedFieldValues((int) $doc['rubric_id'], $values, $doc, $documentId, $fields);
|
||
$catalogAllowed = array();
|
||
$catalogFields = array();
|
||
foreach ($fields as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fieldId = (int) $field['Id'];
|
||
if ((string) $field['rubric_field_type'] !== 'catalog' || !array_key_exists($fieldId, $values)) { continue; }
|
||
// A partial/legacy payload may contain the field container without
|
||
// catalog_ids. Treat it as absent instead of erasing existing links.
|
||
if (!is_array($values[$fieldId]) || !array_key_exists('catalog_ids', $values[$fieldId])) { continue; }
|
||
$selected = CatalogModel::selection((int) $doc['rubric_id'], $fieldId, isset($values[$fieldId]['catalog_ids']) ? $values[$fieldId]['catalog_ids'] : array());
|
||
$raw = CatalogModel::serializeSelection((int) $doc['rubric_id'], $fieldId, $selected);
|
||
self::saveFieldValue($documentId, $field, $raw, (int) $doc['document_in_search'] && (int) $field['rubric_field_search']);
|
||
CatalogModel::afterDocumentSave($documentId, (int) $doc['rubric_id'], $fieldId, $selected);
|
||
$allowed = CatalogModel::allowedFieldIds((int) $doc['rubric_id'], $fieldId, $selected);
|
||
if (!empty($allowed)) { $catalogAllowed = array_values(array_unique(array_merge($catalogAllowed, $allowed))); }
|
||
$catalogFields[$fieldId] = true;
|
||
}
|
||
}
|
||
|
||
foreach ($fields as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fieldId = (int) $field['Id'];
|
||
if (isset($catalogFields[$fieldId])) { continue; }
|
||
if (!empty($catalogAllowed) && !in_array($fieldId, $catalogAllowed, true)) { continue; }
|
||
if (!array_key_exists($fieldId, $values)) {
|
||
continue;
|
||
}
|
||
|
||
$raw = self::fieldInputValue($field, $values[$fieldId]);
|
||
self::saveFieldValue($documentId, $field, $raw, (int) $doc['document_in_search'] && (int) $field['rubric_field_search']);
|
||
}
|
||
}
|
||
|
||
self::clearDocumentCache($documentId);
|
||
CatalogModel::reindexDocument($documentId);
|
||
self::buildDocumentSnapshot($documentId);
|
||
}
|
||
|
||
public static function currentFieldValues($documentId)
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT df.rubric_field_id, df.field_value, dft.field_value AS field_value_more'
|
||
. ' FROM ' . self::docFieldsTable() . ' df'
|
||
. ' LEFT JOIN ' . self::docFieldsTextTable() . ' dft ON dft.rubric_field_id = df.rubric_field_id AND dft.document_id = df.document_id'
|
||
. ' WHERE df.document_id = %i ORDER BY df.rubric_field_id ASC',
|
||
(int) $documentId
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[(int) $row['rubric_field_id']] = (string) $row['field_value'] . (string) $row['field_value_more'];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/** Значения полей для управляемых пакетных операций: JSON уже декодирован. */
|
||
public static function runtimeFieldValues($documentId)
|
||
{
|
||
$values = self::currentFieldValues((int) $documentId);
|
||
foreach ($values as $fieldId => $value) {
|
||
$decoded = FieldValueCodec::decodeStructured($value, null);
|
||
$values[$fieldId] = is_array($decoded) ? $decoded : $value;
|
||
}
|
||
|
||
return $values;
|
||
}
|
||
|
||
/** Сохранить runtime-значения через те же native field-типы, что и редактор. */
|
||
public static function saveRuntimeFieldValues($documentId, array $values)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$doc = self::one($documentId);
|
||
if (!$doc) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
$fieldGroups = self::fieldsForRubric((int) $doc['rubric_id'], $documentId);
|
||
$values = self::conditionalFieldValues((int) $doc['rubric_id'], $values, $documentId, $fieldGroups);
|
||
$values = self::computedFieldValues((int) $doc['rubric_id'], $values, $doc, $documentId, $fieldGroups);
|
||
$errors = self::validateFieldValues((int) $doc['rubric_id'], $values, $documentId, $fieldGroups);
|
||
if ($errors) {
|
||
throw new \RuntimeException('Поля документа не прошли проверку: ' . implode('; ', array_values($errors)));
|
||
}
|
||
|
||
$fieldMap = array();
|
||
foreach ($fieldGroups as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fieldMap[(int) $field['Id']] = $field;
|
||
}
|
||
}
|
||
|
||
foreach ($values as $fieldId => $value) {
|
||
$fieldId = (int) $fieldId;
|
||
if (!isset($fieldMap[$fieldId])) {
|
||
continue;
|
||
}
|
||
|
||
$field = $fieldMap[$fieldId];
|
||
self::saveFieldValue(
|
||
$documentId,
|
||
$field,
|
||
$value,
|
||
(int) $doc['document_in_search'] && (int) $field['rubric_field_search']
|
||
);
|
||
}
|
||
|
||
self::touchDocument($documentId);
|
||
self::clearDocumentCache($documentId);
|
||
CatalogModel::reindexDocument($documentId);
|
||
self::buildDocumentSnapshot($documentId);
|
||
}
|
||
|
||
public static function restoreFieldValues($documentId, array $values)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$doc = self::one($documentId);
|
||
if (!$doc) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
$fieldMap = array();
|
||
foreach (self::fieldsForRubric((int) $doc['rubric_id'], $documentId) as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$fieldMap[(int) $field['Id']] = $field;
|
||
}
|
||
}
|
||
|
||
foreach ($values as $fieldId => $value) {
|
||
$fieldId = (int) $fieldId;
|
||
if (!isset($fieldMap[$fieldId])) {
|
||
continue;
|
||
}
|
||
|
||
self::saveFieldValue(
|
||
$documentId,
|
||
$fieldMap[$fieldId],
|
||
(string) $value,
|
||
(int) $doc['document_in_search'] && (int) $fieldMap[$fieldId]['rubric_field_search']
|
||
);
|
||
}
|
||
|
||
self::touchDocument($documentId);
|
||
self::clearDocumentCache($documentId);
|
||
CatalogModel::reindexDocument($documentId);
|
||
self::buildDocumentSnapshot($documentId);
|
||
}
|
||
|
||
public static function restoreDocumentState($documentId, array $state, $authorId)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$current = self::one($documentId);
|
||
if (!$current) { throw new \RuntimeException('Документ не найден'); }
|
||
$input = $current;
|
||
foreach (array(
|
||
'document_title', 'document_alias', 'document_alias_header', 'document_alias_history',
|
||
'document_short_alias', 'document_breadcrumb_title', 'document_excerpt', 'document_meta_keywords',
|
||
'document_meta_description', 'document_meta_robots', 'document_sitemap_freq', 'document_sitemap_pr',
|
||
'document_tags', 'document_property', 'guid', 'document_status', 'document_in_search',
|
||
'document_parent', 'rubric_tmpl_id', 'document_linked_navi_id', 'document_position',
|
||
'document_author_id',
|
||
) as $key) {
|
||
if (array_key_exists($key, $state)) { $input[$key] = $state[$key]; }
|
||
}
|
||
|
||
$input['document_published'] = !empty($state['document_published']) ? date('Y-m-d H:i:s', (int) $state['document_published']) : '';
|
||
$input['document_expire'] = !empty($state['document_expire']) ? date('Y-m-d H:i:s', (int) $state['document_expire']) : '';
|
||
$input['rubric_id'] = (int) $current['rubric_id'];
|
||
return self::save($documentId, $input, (int) $authorId);
|
||
}
|
||
|
||
public static function revisionCount($documentId)
|
||
{
|
||
return (int) DB::query('SELECT COUNT(*) FROM ' . self::revisionsTable() . ' WHERE doc_id = %i', (int) $documentId)->getValue();
|
||
}
|
||
|
||
public static function save($id, array $input, $authorId)
|
||
{
|
||
$id = (int) $id;
|
||
$existing = $id > 0 ? self::one($id) : null;
|
||
$rubricId = isset($input['rubric_id']) ? (int) $input['rubric_id'] : 0;
|
||
$rubric = self::rubric($rubricId);
|
||
$now = time();
|
||
$data = self::input($input);
|
||
if ($id === 1) {
|
||
$data['document_alias'] = '/';
|
||
$data['document_short_alias'] = '';
|
||
$data['document_alias_history'] = '0';
|
||
$data['document_alias_header'] = 301;
|
||
}
|
||
|
||
$data['rubric_id'] = $rubricId;
|
||
$data['document_changed'] = $now;
|
||
$data['document_version'] = $existing ? max(1, (int) $existing['document_version']) + 1 : 1;
|
||
// Language is a site-wide setting. Keep legacy columns normalized only
|
||
// because the public engine still expects them to exist.
|
||
if (isset($input['document_author_id'])) {
|
||
$data['document_author_id'] = self::validLegacyAuthorId((int) $input['document_author_id']);
|
||
}
|
||
|
||
if ($id > 0) {
|
||
DB::Update(self::documentsTable(), $data, 'Id = %i', $id);
|
||
self::syncAliasHistory($id, $existing, $data, (int) $authorId, $rubric);
|
||
self::syncSearchTerms($id, $rubricId, $data['document_meta_keywords'], $data['document_tags']);
|
||
self::clearDocumentCache($id);
|
||
return $id;
|
||
}
|
||
|
||
if ((int) $data['rubric_tmpl_id'] <= 0) {
|
||
$data['rubric_tmpl_id'] = 0;
|
||
}
|
||
|
||
if ((int) $data['document_position'] <= 0) {
|
||
$data['document_position'] = self::nextPosition();
|
||
}
|
||
|
||
if (!isset($data['document_author_id'])) {
|
||
$data['document_author_id'] = self::legacyAuthorId($authorId);
|
||
}
|
||
|
||
$data['document_deleted'] = '0';
|
||
$data['document_count_print'] = 0;
|
||
$data['document_count_view'] = 0;
|
||
$data['module_catalog'] = '';
|
||
DB::Insert(self::documentsTable(), $data);
|
||
$newId = (int) DB::insertId();
|
||
self::removeOwnedHistoryAlias($newId, $data['document_alias']);
|
||
self::syncSearchTerms($newId, $rubricId, $data['document_meta_keywords'], $data['document_tags']);
|
||
self::seedFields($newId, $rubricId);
|
||
self::clearDocumentCache($newId);
|
||
return $newId;
|
||
}
|
||
|
||
public static function assertEditVersion($id, $expectedVersion)
|
||
{
|
||
$current = DB::query(
|
||
'SELECT document_version FROM ' . self::documentsTable() . ' WHERE Id=%i LIMIT 1 FOR UPDATE',
|
||
(int) $id
|
||
)->getValue();
|
||
if ($current === null || $current === false) { throw new \RuntimeException('Документ не найден'); }
|
||
$current = max(1, (int) $current);
|
||
if ($current !== max(1, (int) $expectedVersion)) { throw new EditConflict($current); }
|
||
|
||
return $current;
|
||
}
|
||
|
||
public static function documentVersion($id)
|
||
{
|
||
return max(1, (int) DB::query(
|
||
'SELECT document_version FROM ' . self::documentsTable() . ' WHERE Id=%i LIMIT 1',
|
||
(int) $id
|
||
)->getValue());
|
||
}
|
||
|
||
public static function markChanged($id)
|
||
{
|
||
self::touchDocument((int) $id);
|
||
return self::documentVersion((int) $id);
|
||
}
|
||
|
||
protected static function touchDocument($id)
|
||
{
|
||
DB::query(
|
||
'UPDATE ' . self::documentsTable()
|
||
. ' SET document_changed=%i, document_version=GREATEST(1, document_version)+1 WHERE Id=%i',
|
||
time(),
|
||
(int) $id
|
||
);
|
||
}
|
||
|
||
public static function previewPayload($id, array $input, array $values, $authorId, $fieldGroups = null)
|
||
{
|
||
$id = (int) $id;
|
||
$existing = $id > 0 ? self::one($id) : null;
|
||
$rubricId = $existing ? (int) $existing['rubric_id'] : (isset($input['rubric_id']) ? (int) $input['rubric_id'] : 0);
|
||
$rubric = self::rubric($rubricId);
|
||
$data = self::input($input);
|
||
$data['rubric_id'] = $rubricId;
|
||
$data['document_changed'] = time();
|
||
if (isset($input['document_author_id'])) {
|
||
$data['document_author_id'] = self::validLegacyAuthorId((int) $input['document_author_id']);
|
||
}
|
||
|
||
if ($id <= 0) {
|
||
if ((int) $data['rubric_tmpl_id'] <= 0) {
|
||
$data['rubric_tmpl_id'] = 0;
|
||
}
|
||
|
||
if ((int) $data['document_position'] <= 0) {
|
||
$data['document_position'] = self::nextPosition();
|
||
}
|
||
|
||
if (!isset($data['document_author_id'])) {
|
||
$data['document_author_id'] = self::legacyAuthorId($authorId);
|
||
}
|
||
|
||
$data['document_deleted'] = 0;
|
||
$data['document_count_print'] = 0;
|
||
$data['document_count_view'] = 0;
|
||
}
|
||
|
||
$fields = array();
|
||
$skippedFields = array();
|
||
$fieldRows = self::resolveFieldGroups($rubricId, $id, $fieldGroups);
|
||
$flatFields = array();
|
||
foreach ($fieldRows as $group) {
|
||
foreach ($group['items'] as $field) {
|
||
$flatFields[(int) $field['Id']] = $field;
|
||
}
|
||
}
|
||
|
||
$append = function (array $field, $storageValue) use (&$fields, $values) {
|
||
$fieldId = (int) $field['Id'];
|
||
$storageValue = FieldLifecycle::normalizeForStorage($storageValue, $field, array(), 'adminx_preview');
|
||
$fields[] = array(
|
||
'id' => $fieldId,
|
||
'alias' => (string) $field['rubric_field_alias'],
|
||
'title' => self::decode($field['rubric_field_title']),
|
||
'type' => (string) $field['rubric_field_type'],
|
||
'input' => $values[$fieldId],
|
||
'storage_value' => $storageValue,
|
||
'storage_length' => mb_strlen($storageValue),
|
||
);
|
||
};
|
||
|
||
$catalogAllowed = array();
|
||
$catalogFields = array();
|
||
foreach ($flatFields as $fieldId => $field) {
|
||
if ((string) $field['rubric_field_type'] !== 'catalog' || !array_key_exists($fieldId, $values)) { continue; }
|
||
if (!is_array($values[$fieldId]) || !array_key_exists('catalog_ids', $values[$fieldId])) {
|
||
$skippedFields[] = array('id' => $fieldId, 'reason' => 'catalog_ids не переданы');
|
||
continue;
|
||
}
|
||
|
||
$selected = CatalogModel::selection($rubricId, $fieldId, $values[$fieldId]['catalog_ids']);
|
||
$append($field, CatalogModel::serializeSelection($rubricId, $fieldId, $selected));
|
||
$allowed = CatalogModel::allowedFieldIds($rubricId, $fieldId, $selected);
|
||
if (!empty($allowed)) { $catalogAllowed = array_values(array_unique(array_merge($catalogAllowed, $allowed))); }
|
||
$catalogFields[$fieldId] = true;
|
||
}
|
||
|
||
foreach ($flatFields as $fieldId => $field) {
|
||
if (isset($catalogFields[$fieldId]) || (string) $field['rubric_field_type'] === 'catalog') { continue; }
|
||
if (!array_key_exists($fieldId, $values)) { continue; }
|
||
if (!empty($catalogAllowed) && !in_array($fieldId, $catalogAllowed, true)) {
|
||
$skippedFields[] = array('id' => $fieldId, 'reason' => 'поле не включено в выбранных разделах каталога');
|
||
continue;
|
||
}
|
||
|
||
$append($field, self::fieldInputValue($field, $values[$fieldId]));
|
||
}
|
||
|
||
return array(
|
||
'mode' => $id > 0 ? 'update' : 'create',
|
||
'document_id' => $id ?: null,
|
||
'document' => $data,
|
||
'fields' => $fields,
|
||
'skipped_fields' => $skippedFields,
|
||
'lifecycle_hooks_executed' => false,
|
||
);
|
||
}
|
||
|
||
protected static function resolveFieldGroups($rubricId, $documentId, $fieldGroups)
|
||
{
|
||
return is_array($fieldGroups)
|
||
? $fieldGroups
|
||
: self::fieldsForRubric((int) $rubricId, (int) $documentId);
|
||
}
|
||
|
||
public static function delete($id)
|
||
{
|
||
$id = (int) $id;
|
||
if (self::isProtectedDocument($id)) {
|
||
throw new \RuntimeException(self::protectedDocumentMessage($id));
|
||
}
|
||
|
||
if (!self::one($id)) {
|
||
return false;
|
||
}
|
||
|
||
$deleting = Lifecycle::event('content.document.deleting', 'document', 'deleting', $id, array(
|
||
'document' => self::one($id),
|
||
), null, array(), 'adminx_documents');
|
||
if ($deleting->cancelled()) {
|
||
throw new \RuntimeException($deleting->message() !== '' ? $deleting->message() : 'Удаление документа отменено модулем');
|
||
}
|
||
|
||
DB::query('UPDATE ' . self::documentsTable() . " SET document_deleted='1', document_changed=%i, document_version=GREATEST(1, document_version)+1 WHERE Id=%i", time(), $id);
|
||
self::clearDocumentCache($id);
|
||
CatalogModel::reindexDocument($id);
|
||
Lifecycle::event('content.document.deleted', 'document', 'deleted', $id, array(), true, array('soft_delete' => true), 'adminx_documents');
|
||
return true;
|
||
}
|
||
|
||
public static function restore($id)
|
||
{
|
||
$id = (int) $id;
|
||
if (!self::one($id)) {
|
||
return false;
|
||
}
|
||
|
||
DB::query('UPDATE ' . self::documentsTable() . " SET document_deleted='0', document_changed=%i, document_version=GREATEST(1, document_version)+1 WHERE Id=%i", time(), $id);
|
||
self::clearDocumentCache($id);
|
||
CatalogModel::reindexDocument($id);
|
||
self::buildDocumentSnapshot($id);
|
||
return true;
|
||
}
|
||
|
||
public static function toggleStatus($id)
|
||
{
|
||
$id = (int) $id;
|
||
$document = DB::query('SELECT * FROM ' . self::documentsTable() . ' WHERE Id=%i LIMIT 1', $id)->getAssoc();
|
||
if (!$document) {
|
||
return false;
|
||
}
|
||
|
||
if (self::isProtectedDocument($id)) {
|
||
throw new \RuntimeException('Статус системного документа менять нельзя');
|
||
}
|
||
|
||
$status = !empty($document['document_status']) ? '0' : '1';
|
||
DB::query('UPDATE ' . self::documentsTable() . ' SET document_status=%s, document_changed=%i, document_version=GREATEST(1, document_version)+1 WHERE Id=%i', $status, time(), $id);
|
||
self::clearDocumentCache($id);
|
||
CatalogModel::reindexDocument($id);
|
||
self::buildDocumentSnapshot($id);
|
||
return $status;
|
||
}
|
||
|
||
public static function copy($id, $authorId)
|
||
{
|
||
$id = (int) $id;
|
||
if (self::isProtectedDocument($id)) {
|
||
throw new \RuntimeException('Системный документ копировать нельзя');
|
||
}
|
||
|
||
$source = DB::query('SELECT * FROM ' . self::documentsTable() . ' WHERE Id = %i LIMIT 1', $id)->getAssoc();
|
||
if (!$source) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
unset($source['Id']);
|
||
$source['document_title'] = htmlspecialchars('Копия: ' . self::decode($source['document_title']), ENT_QUOTES);
|
||
$source['document_alias'] = self::generateAlias(
|
||
self::decode($source['document_title']),
|
||
0,
|
||
0,
|
||
(int) $source['rubric_id'],
|
||
time()
|
||
);
|
||
$source['document_short_alias'] = '';
|
||
$source['document_status'] = '0';
|
||
$source['document_deleted'] = '0';
|
||
$source['document_count_print'] = 0;
|
||
$source['document_count_view'] = 0;
|
||
$source['document_author_id'] = self::legacyAuthorId($authorId);
|
||
$source['document_changed'] = time();
|
||
$source['document_version'] = 1;
|
||
$source['document_published'] = 0;
|
||
$source['document_expire'] = 0;
|
||
DB::Insert(self::documentsTable(), $source);
|
||
$newId = (int) DB::insertId();
|
||
|
||
foreach (DB::query('SELECT * FROM ' . self::docFieldsTable() . ' WHERE document_id = %i', $id)->getAll() as $field) {
|
||
unset($field['Id']);
|
||
$field['document_id'] = $newId;
|
||
DB::Insert(self::docFieldsTable(), $field);
|
||
}
|
||
|
||
foreach (DB::query('SELECT * FROM ' . self::docFieldsTextTable() . ' WHERE document_id = %i', $id)->getAll() as $field) {
|
||
unset($field['Id']);
|
||
$field['document_id'] = $newId;
|
||
DB::Insert(self::docFieldsTextTable(), $field);
|
||
}
|
||
|
||
self::syncSearchTerms($newId, (int) $source['rubric_id'], $source['document_meta_keywords'], $source['document_tags']);
|
||
self::clearDocumentCache($newId);
|
||
CatalogModel::reindexDocument($newId);
|
||
self::buildDocumentSnapshot($newId);
|
||
return $newId;
|
||
}
|
||
|
||
public static function purge($id)
|
||
{
|
||
$id = (int) $id;
|
||
if (self::isProtectedDocument($id)) {
|
||
throw new \RuntimeException(self::protectedDocumentMessage($id));
|
||
}
|
||
|
||
$document = DB::query('SELECT * FROM ' . self::documentsTable() . ' WHERE Id=%i LIMIT 1', $id)->getAssoc();
|
||
if (!$document) {
|
||
return false;
|
||
}
|
||
|
||
if (empty($document['document_deleted'])) {
|
||
throw new \RuntimeException('Сначала переместите документ в корзину');
|
||
}
|
||
|
||
$ownsTransaction = !DB::$transaction_in_progress;
|
||
if ($ownsTransaction) { DB::startTransaction(); }
|
||
try {
|
||
DB::Delete(self::docFieldsTextTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::docFieldsTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::revisionsTable(), 'doc_id = %i', $id);
|
||
DB::Delete(self::aliasHistoryTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::keywordsTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::tagsTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::remarksTable(), 'document_id = %i', $id);
|
||
DB::Delete(self::viewCountTable(), 'document_id = %i', $id);
|
||
\App\Content\Fields\DocumentRelationIndex::removeDocument($id);
|
||
DB::Delete(self::documentsTable(), 'Id = %i', $id);
|
||
if ($ownsTransaction) { DB::commit(); }
|
||
} catch (\Throwable $e) {
|
||
if ($ownsTransaction) { DB::rollback(); }
|
||
throw $e;
|
||
}
|
||
|
||
self::clearDocumentCache($id);
|
||
CatalogModel::reindexDocument($id);
|
||
Lifecycle::event(
|
||
'content.document.deleted',
|
||
'document',
|
||
'deleted',
|
||
$id,
|
||
array(),
|
||
true,
|
||
array('hard_delete' => true),
|
||
'adminx_documents'
|
||
);
|
||
return true;
|
||
}
|
||
|
||
public static function stats()
|
||
{
|
||
$table = self::documentsTable();
|
||
return array(
|
||
'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $table)->getValue(),
|
||
'active' => (int) DB::query("SELECT COUNT(*) FROM " . $table . " WHERE document_status = '1' AND document_deleted != '1'")->getValue(),
|
||
'draft' => (int) DB::query("SELECT COUNT(*) FROM " . $table . " WHERE document_status = '0' AND document_deleted != '1'")->getValue(),
|
||
'deleted' => (int) DB::query("SELECT COUNT(*) FROM " . $table . " WHERE document_deleted = '1'")->getValue(),
|
||
);
|
||
}
|
||
|
||
public static function viewStats($days = 30)
|
||
{
|
||
$days = max(1, min(365, (int) $days));
|
||
$from = strtotime('-' . ($days - 1) . ' days', mktime(0, 0, 0));
|
||
$dailyRows = DB::query(
|
||
'SELECT day_id, SUM(count) AS views, COUNT(DISTINCT document_id) AS documents'
|
||
. ' FROM ' . self::viewCountTable() . ' WHERE day_id >= %i GROUP BY day_id ORDER BY day_id ASC',
|
||
(int) $from
|
||
)->getAll();
|
||
$daily = array();
|
||
$total = 0;
|
||
foreach ($dailyRows as $row) {
|
||
$views = (int) $row['views'];
|
||
$total += $views;
|
||
$daily[] = array(
|
||
'day' => (int) $row['day_id'],
|
||
'date_label' => date('d.m.Y', (int) $row['day_id']),
|
||
'views' => $views,
|
||
'documents' => (int) $row['documents'],
|
||
);
|
||
}
|
||
|
||
$topRows = DB::query(
|
||
'SELECT v.document_id, SUM(v.count) AS views, d.document_title, d.document_alias'
|
||
. ' FROM ' . self::viewCountTable() . ' v'
|
||
. ' LEFT JOIN ' . self::documentsTable() . ' d ON d.Id = v.document_id'
|
||
. ' WHERE v.day_id >= %i GROUP BY v.document_id, d.document_title, d.document_alias'
|
||
. ' ORDER BY views DESC, v.document_id ASC LIMIT 25',
|
||
(int) $from
|
||
)->getAll();
|
||
$top = array();
|
||
foreach ($topRows as $row) {
|
||
$top[] = array(
|
||
'id' => (int) $row['document_id'],
|
||
'title' => !empty($row['document_title']) ? self::decode($row['document_title']) : 'Удалённый документ',
|
||
'alias' => isset($row['document_alias']) ? (string) $row['document_alias'] : '',
|
||
'views' => (int) $row['views'],
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'days' => $days,
|
||
'from' => (int) $from,
|
||
'total' => $total,
|
||
'average' => !empty($daily) ? round($total / count($daily), 1) : 0,
|
||
'tracked_documents' => (int) DB::query('SELECT COUNT(DISTINCT document_id) FROM ' . self::viewCountTable() . ' WHERE day_id >= %i', (int) $from)->getValue(),
|
||
'rows' => (int) DB::query('SELECT COUNT(*) FROM ' . self::viewCountTable())->getValue(),
|
||
'daily' => $daily,
|
||
'top' => $top,
|
||
);
|
||
}
|
||
|
||
public static function clearViewStats()
|
||
{
|
||
$count = (int) DB::query('SELECT COUNT(*) FROM ' . self::viewCountTable())->getValue();
|
||
DB::query('TRUNCATE TABLE ' . self::viewCountTable());
|
||
return $count;
|
||
}
|
||
|
||
public static function rubrics()
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT r.Id, r.rubric_title, r.rubric_alias, r.rubric_template_id, COUNT(f.Id) AS fields_count'
|
||
. ' FROM ' . self::rubricsTable() . ' r'
|
||
. ' LEFT JOIN ' . self::fieldsTable() . ' f ON f.rubric_id = r.Id'
|
||
. ' GROUP BY r.Id, r.rubric_title, r.rubric_alias, r.rubric_template_id, r.rubric_position'
|
||
. ' ORDER BY r.rubric_position ASC, r.Id ASC'
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = array(
|
||
'Id' => (int) $row['Id'],
|
||
'rubric_title' => self::decode($row['rubric_title']),
|
||
'rubric_alias' => (string) $row['rubric_alias'],
|
||
'rubric_template_id' => (int) $row['rubric_template_id'],
|
||
'fields_count' => (int) $row['fields_count'],
|
||
);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function authors()
|
||
{
|
||
$rows = DB::query('SELECT id, legacy_id, name, email, is_active FROM ' . SystemTables::table('users') . ' ORDER BY name ASC, id ASC')->getAll();
|
||
$out = array();
|
||
$seen = array();
|
||
foreach ($rows as $row) {
|
||
$id = !empty($row['legacy_id']) ? (int) $row['legacy_id'] : (int) $row['id'];
|
||
if ($id <= 0 || isset($seen[$id])) {
|
||
continue;
|
||
}
|
||
|
||
$seen[$id] = true;
|
||
$out[] = array(
|
||
'id' => $id,
|
||
'name' => trim((string) $row['name']) !== '' ? (string) $row['name'] : (string) $row['email'],
|
||
'email' => (string) $row['email'],
|
||
'active' => !empty($row['is_active']),
|
||
);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function remarks($documentId)
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT * FROM ' . self::remarksTable() . ' WHERE document_id = %i ORDER BY remark_first DESC, remark_published ASC, Id ASC',
|
||
(int) $documentId
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = array(
|
||
'id' => (int) $row['Id'],
|
||
'title' => self::decode($row['remark_title']),
|
||
'text' => self::decode($row['remark_text']),
|
||
'author_id' => (int) $row['remark_author_id'],
|
||
'email' => (string) $row['remark_author_email'],
|
||
'published' => (int) $row['remark_published'],
|
||
'published_label' => !empty($row['remark_published']) ? date('d.m.Y H:i', (int) $row['remark_published']) : '-',
|
||
'first' => (int) $row['remark_first'],
|
||
'status' => (int) $row['remark_status'],
|
||
);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function addRemark($documentId, array $input, $authorId)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
if (!self::one($documentId)) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
$title = trim(isset($input['title']) ? (string) $input['title'] : '');
|
||
$text = trim(isset($input['text']) ? (string) $input['text'] : '');
|
||
if ($text === '') {
|
||
throw new \RuntimeException('Введите текст заметки');
|
||
}
|
||
|
||
$first = (int) DB::query('SELECT COUNT(*) FROM ' . self::remarksTable() . ' WHERE document_id = %i', $documentId)->getValue() === 0 ? 1 : 0;
|
||
DB::Insert(self::remarksTable(), array(
|
||
'document_id' => $documentId,
|
||
'remark_first' => $first,
|
||
'remark_title' => htmlspecialchars($title, ENT_QUOTES),
|
||
'remark_text' => htmlspecialchars(function_exists('mb_substr') ? mb_substr($text, 0, 500, 'UTF-8') : substr($text, 0, 500), ENT_QUOTES),
|
||
'remark_author_id' => self::legacyAuthorId($authorId),
|
||
'remark_published' => time(),
|
||
'remark_status' => 1,
|
||
'remark_author_email' => self::adminEmail($authorId),
|
||
));
|
||
return (int) DB::insertId();
|
||
}
|
||
|
||
public static function deleteRemark($documentId, $remarkId)
|
||
{
|
||
$exists = (int) DB::query('SELECT Id FROM ' . self::remarksTable() . ' WHERE Id = %i AND document_id = %i LIMIT 1', (int) $remarkId, (int) $documentId)->getValue();
|
||
if ($exists <= 0) {
|
||
return false;
|
||
}
|
||
|
||
DB::Delete(self::remarksTable(), 'Id = %i AND document_id = %i', (int) $remarkId, (int) $documentId);
|
||
return true;
|
||
}
|
||
|
||
public static function rubric($id)
|
||
{
|
||
return DB::query('SELECT * FROM ' . self::rubricsTable() . ' WHERE Id = %i LIMIT 1', (int) $id)->getAssoc();
|
||
}
|
||
|
||
protected static function rubricConditionsEnabled($id)
|
||
{
|
||
static $cache = array();
|
||
$id = (int) $id;
|
||
if (!array_key_exists($id, $cache)) {
|
||
$rubric = self::rubric($id);
|
||
$cache[$id] = $rubric && !empty($rubric['rubric_form_conditions']);
|
||
}
|
||
|
||
return $cache[$id];
|
||
}
|
||
|
||
public static function aliasAvailable($alias, $exceptId)
|
||
{
|
||
return self::aliasConflict($alias, $exceptId) === null;
|
||
}
|
||
|
||
public static function aliasConflict($alias, $exceptId = 0)
|
||
{
|
||
return DocumentAliasRegistry::conflict($alias, $exceptId);
|
||
}
|
||
|
||
public static function aliasHistory($documentId)
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT h.*, u.name AS author_name FROM ' . self::aliasHistoryTable() . ' h'
|
||
. ' LEFT JOIN ' . SystemTables::table('users') . ' u ON u.id = h.document_alias_author'
|
||
. ' WHERE h.document_id = %i ORDER BY h.document_alias_changed DESC, h.id DESC',
|
||
(int) $documentId
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = array(
|
||
'id' => (int) $row['id'],
|
||
'document_id' => (int) $row['document_id'],
|
||
'alias' => (string) $row['document_alias'],
|
||
'header' => (int) $row['document_alias_header'],
|
||
'author_id' => (int) $row['document_alias_author'],
|
||
'author_name' => isset($row['author_name']) ? (string) $row['author_name'] : '',
|
||
'changed' => (int) $row['document_alias_changed'],
|
||
'changed_label' => !empty($row['document_alias_changed']) ? date('d.m.Y H:i', (int) $row['document_alias_changed']) : '-',
|
||
);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
public static function allAliasHistory(array $filters)
|
||
{
|
||
$page = max(1, isset($filters['page']) ? (int) $filters['page'] : 1);
|
||
$limit = 50;
|
||
$where = ' WHERE 1=1';
|
||
$args = array();
|
||
$q = trim(isset($filters['q']) ? (string) $filters['q'] : '');
|
||
if ($q !== '') {
|
||
$where .= ' AND (h.document_alias LIKE %ss OR d.document_alias LIKE %ss OR d.document_title LIKE %ss OR d.Id = %i)';
|
||
$args[] = $q; $args[] = $q; $args[] = $q; $args[] = (int) $q;
|
||
}
|
||
|
||
$header = isset($filters['header']) ? (int) $filters['header'] : 0;
|
||
if (in_array($header, array(301, 302, 307, 308), true)) {
|
||
$where .= ' AND h.document_alias_header = %i';
|
||
$args[] = $header;
|
||
}
|
||
|
||
$from = ' FROM ' . self::aliasHistoryTable() . ' h'
|
||
. ' INNER JOIN ' . self::documentsTable() . ' d ON d.Id = h.document_id'
|
||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||
. ' LEFT JOIN ' . SystemTables::table('users') . ' u ON u.id = h.document_alias_author';
|
||
$count = (int) call_user_func_array(array('DB', 'query'), array_merge(array('SELECT COUNT(*)' . $from . $where), $args))->getValue();
|
||
$pages = max(1, (int) ceil($count / $limit));
|
||
$page = min($page, $pages);
|
||
$offset = ($page - 1) * $limit;
|
||
$sql = 'SELECT h.*, d.document_title, d.document_alias AS target_alias, d.document_deleted,'
|
||
. ' r.rubric_title, u.name AS author_name' . $from . $where
|
||
. ' ORDER BY h.document_alias_changed DESC, h.id DESC LIMIT ' . $offset . ', ' . $limit;
|
||
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
|
||
$items = array();
|
||
foreach ($rows as $row) { $items[] = self::aliasHistoryRow($row); }
|
||
return array('items' => $items, 'pagination' => array(
|
||
'page' => $page, 'pages' => $pages, 'total' => $count,
|
||
'from' => $count ? $offset + 1 : 0, 'to' => min($offset + $limit, $count),
|
||
));
|
||
}
|
||
|
||
public static function aliasHistoryStats()
|
||
{
|
||
$table = self::aliasHistoryTable();
|
||
return array(
|
||
'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $table)->getValue(),
|
||
'documents' => (int) DB::query('SELECT COUNT(DISTINCT document_id) FROM ' . $table)->getValue(),
|
||
'permanent' => (int) DB::query('SELECT COUNT(*) FROM ' . $table . ' WHERE document_alias_header IN (301, 308)')->getValue(),
|
||
'temporary' => (int) DB::query('SELECT COUNT(*) FROM ' . $table . ' WHERE document_alias_header IN (302, 307)')->getValue(),
|
||
);
|
||
}
|
||
|
||
protected static function aliasHistoryRow(array $row)
|
||
{
|
||
return array(
|
||
'id' => (int) $row['id'], 'document_id' => (int) $row['document_id'],
|
||
'alias' => (string) $row['document_alias'], 'header' => self::redirectHeader($row['document_alias_header']),
|
||
'author_id' => (int) $row['document_alias_author'],
|
||
'author_name' => isset($row['author_name']) ? (string) $row['author_name'] : '',
|
||
'changed' => (int) $row['document_alias_changed'],
|
||
'changed_label' => !empty($row['document_alias_changed']) ? date('d.m.Y H:i', (int) $row['document_alias_changed']) : '-',
|
||
'document_title' => isset($row['document_title']) ? (string) $row['document_title'] : '',
|
||
'target_alias' => isset($row['target_alias']) ? (string) $row['target_alias'] : '',
|
||
'rubric_title' => isset($row['rubric_title']) ? (string) $row['rubric_title'] : '',
|
||
'document_deleted' => !empty($row['document_deleted']),
|
||
);
|
||
}
|
||
|
||
public static function saveAliasHistory($documentId, $historyId, array $input, $authorId)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$historyId = (int) $historyId;
|
||
if (!self::one($documentId)) {
|
||
throw new \RuntimeException('Документ не найден');
|
||
}
|
||
|
||
$alias = trim(isset($input['alias']) ? (string) $input['alias'] : '', '/ ');
|
||
if ($alias === '' || !preg_match('/^[a-zA-Z0-9_\-\/\.]+$/', $alias)) {
|
||
throw new \RuntimeException('Укажите корректный alias');
|
||
}
|
||
|
||
if (!self::historyAliasAvailable($alias, $documentId, $historyId)) {
|
||
throw new \RuntimeException('Такой alias уже используется документом или другим редиректом');
|
||
}
|
||
|
||
$data = array(
|
||
'document_id' => $documentId,
|
||
'document_alias' => $alias,
|
||
'document_alias_header' => self::redirectHeader(isset($input['header']) ? $input['header'] : 301),
|
||
'document_alias_author' => (int) $authorId > 0 ? (int) $authorId : 1,
|
||
'document_alias_changed' => time(),
|
||
);
|
||
if ($historyId > 0) {
|
||
DB::Update(self::aliasHistoryTable(), $data, 'id = %i AND document_id = %i', $historyId, $documentId);
|
||
return $historyId;
|
||
}
|
||
|
||
DB::Insert(self::aliasHistoryTable(), $data);
|
||
return (int) DB::insertId();
|
||
}
|
||
|
||
public static function deleteAliasHistory($documentId, $historyId)
|
||
{
|
||
$exists = (int) DB::query(
|
||
'SELECT id FROM ' . self::aliasHistoryTable() . ' WHERE id = %i AND document_id = %i LIMIT 1',
|
||
(int) $historyId,
|
||
(int) $documentId
|
||
)->getValue();
|
||
if ($exists <= 0) {
|
||
return false;
|
||
}
|
||
|
||
DB::Delete(self::aliasHistoryTable(), 'id = %i AND document_id = %i', (int) $historyId, (int) $documentId);
|
||
return true;
|
||
}
|
||
|
||
public static function documentPicker($q, $rubricId = 0, $limit = 20)
|
||
{
|
||
return (new \App\Content\Documents\DocumentPickerRepository())->search($q, $rubricId, $limit);
|
||
}
|
||
|
||
/** Existing keywords/tags for the searchable tag inputs in document editor. */
|
||
public static function termSuggestions($kind, $q = '', $rubricId = 0, $limit = 12)
|
||
{
|
||
$kind = $kind === 'keywords' ? 'keywords' : 'tags';
|
||
$q = trim((string) $q);
|
||
$rubricId = max(0, (int) $rubricId);
|
||
$limit = max(5, min(30, (int) $limit));
|
||
$termTable = $kind === 'keywords' ? self::keywordsTable() : self::tagsTable();
|
||
$termColumn = $kind === 'keywords' ? 'keyword' : 'tag';
|
||
|
||
$selectRubric = $rubricId > 0
|
||
? 'SUM(CASE WHEN d.rubric_id = %i THEN 1 ELSE 0 END) AS rubric_count'
|
||
: '0 AS rubric_count';
|
||
$sql = 'SELECT x.' . $termColumn . ' AS term, COUNT(*) AS usage_count, ' . $selectRubric
|
||
. ' FROM ' . $termTable . ' x'
|
||
. ' INNER JOIN ' . self::documentsTable() . ' d ON d.Id = x.document_id'
|
||
. " WHERE d.document_deleted != '1' AND TRIM(x." . $termColumn . ") <> ''";
|
||
$args = array();
|
||
if ($rubricId > 0) {
|
||
$args[] = $rubricId;
|
||
}
|
||
|
||
if ($q !== '') {
|
||
$sql .= ' AND x.' . $termColumn . ' LIKE %ss';
|
||
$args[] = $q;
|
||
}
|
||
|
||
$sql .= ' GROUP BY x.' . $termColumn
|
||
. ' ORDER BY rubric_count DESC, usage_count DESC, x.' . $termColumn . ' ASC'
|
||
. ' LIMIT ' . $limit;
|
||
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
|
||
|
||
$items = array();
|
||
foreach ($rows ?: array() as $row) {
|
||
$term = trim((string) $row['term']);
|
||
if ($term === '') { continue; }
|
||
$items[] = array(
|
||
'value' => $term,
|
||
'count' => (int) $row['usage_count'],
|
||
'rubric_count' => (int) $row['rubric_count'],
|
||
);
|
||
}
|
||
|
||
return $items;
|
||
}
|
||
|
||
protected static function input(array $input)
|
||
{
|
||
$get = function ($key) use ($input) {
|
||
return isset($input[$key]) ? (string) $input[$key] : '';
|
||
};
|
||
return array(
|
||
'document_title' => htmlspecialchars(trim($get('document_title')), ENT_QUOTES),
|
||
'document_alias' => DocumentAliasRegistry::normalize($get('document_alias')),
|
||
'document_alias_header' => self::redirectHeader($get('document_alias_header')),
|
||
'document_alias_history' => (string) self::aliasHistoryMode($get('document_alias_history')),
|
||
'document_short_alias' => substr(trim($get('document_short_alias')), 0, 10),
|
||
'document_breadcrumb_title' => htmlspecialchars(trim($get('document_breadcrumb_title')), ENT_QUOTES),
|
||
'document_excerpt' => trim($get('document_excerpt')),
|
||
'document_meta_keywords' => trim($get('document_meta_keywords')),
|
||
'document_meta_description' => trim($get('document_meta_description')),
|
||
'document_meta_robots' => self::robotsValue($get('document_meta_robots')),
|
||
'document_sitemap_freq' => self::sitemapFreqValue($get('document_sitemap_freq')),
|
||
'document_sitemap_pr' => self::sitemapPrValue($get('document_sitemap_pr')),
|
||
'document_tags' => trim($get('document_tags')),
|
||
'document_property' => trim($get('document_property')),
|
||
'guid' => substr(trim($get('guid')), 0, 100),
|
||
'document_status' => (int) $get('document_status') === 1 ? '1' : '0',
|
||
'document_in_search' => (int) $get('document_in_search') === 1 ? '1' : '0',
|
||
'document_parent' => (int) $get('document_parent'),
|
||
'rubric_tmpl_id' => (int) $get('rubric_tmpl_id'),
|
||
'document_linked_navi_id' => (int) $get('document_linked_navi_id'),
|
||
'document_position' => (int) $get('document_position'),
|
||
'document_published' => self::parseDate($get('document_published')),
|
||
'document_expire' => self::parseDate($get('document_expire')),
|
||
);
|
||
}
|
||
|
||
protected static function sitemapFreqValue($value)
|
||
{
|
||
$v = (int) $value;
|
||
return ($v >= 0 && $v <= 6) ? $v : 3;
|
||
}
|
||
|
||
protected static function sitemapPrValue($value)
|
||
{
|
||
$v = round((float) $value, 1);
|
||
if ($v < 0) { $v = 0.0; }
|
||
if ($v > 1) { $v = 1.0; }
|
||
return $v;
|
||
}
|
||
|
||
/** Транслитерация заголовка в ЧПУ-алиас (как в sample/other). */
|
||
public static function slugify($value)
|
||
{
|
||
$map = array(
|
||
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'e',
|
||
'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm',
|
||
'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u',
|
||
'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch',
|
||
'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
|
||
);
|
||
$value = function_exists('mb_strtolower') ? mb_strtolower((string) $value, 'UTF-8') : strtolower((string) $value);
|
||
$value = strtr($value, $map);
|
||
$value = preg_replace('/[^a-z0-9]+/u', '-', $value);
|
||
$value = preg_replace('/-{2,}/', '-', $value);
|
||
return trim((string) $value, '-');
|
||
}
|
||
|
||
/** Уникальный алиас из заголовка (добавляет -2, -3… при конфликте). */
|
||
/** Короткий алиас свободен (не занят ни коротким, ни обычным алиасом). */
|
||
public static function shortAliasAvailable($code, $exceptId)
|
||
{
|
||
return DocumentAliasRegistry::available($code, $exceptId);
|
||
}
|
||
|
||
/** Случайный уникальный короткий алиас (URL-shortener, как в legacy gen_short_link). */
|
||
public static function generateShortAlias($excludeId = 0, $length = 6)
|
||
{
|
||
$length = max(4, min(10, (int) $length));
|
||
for ($i = 0; $i < 50; $i++) {
|
||
$code = Str::random($length);
|
||
if (self::shortAliasAvailable($code, (int) $excludeId)) {
|
||
return $code;
|
||
}
|
||
}
|
||
|
||
return Str::random($length);
|
||
}
|
||
|
||
/** Путь алиаса родителя без крайних слэшей (иерархия ЧПУ). */
|
||
public static function parentAliasPath($parentId)
|
||
{
|
||
$parentId = (int) $parentId;
|
||
if ($parentId <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$alias = (string) DB::query(
|
||
'SELECT document_alias FROM ' . self::documentsTable() . ' WHERE Id = %i LIMIT 1',
|
||
$parentId
|
||
)->getValue();
|
||
return trim($alias, '/');
|
||
}
|
||
|
||
/** Уникальный иерархический алиас: <алиас-родителя>/<slug>, с -2/-3 при конфликте. */
|
||
public static function generateAlias($title, $excludeId = 0, $parentId = 0, $rubricId = 0, $publishedAt = 0)
|
||
{
|
||
$base = self::slugify($title);
|
||
if ($base === '') {
|
||
$base = 'document';
|
||
}
|
||
|
||
$parentPath = self::parentAliasPath($parentId);
|
||
$rubric = (int) $rubricId > 0 ? self::rubric((int) $rubricId) : null;
|
||
$pattern = $rubric && isset($rubric['rubric_alias']) ? (string) $rubric['rubric_alias'] : '';
|
||
$build = function ($leaf) use ($parentPath, $pattern, $publishedAt) {
|
||
return DocumentAliasTemplate::compose($pattern, $leaf, (int) $publishedAt, $parentPath);
|
||
};
|
||
$alias = $build($base);
|
||
$n = 2;
|
||
while (!self::aliasAvailable($alias, (int) $excludeId)) {
|
||
$alias = $build($base . '-' . $n);
|
||
$n++;
|
||
if ($n > 200) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $alias;
|
||
}
|
||
|
||
/** Дополняет короткий alias шаблоном рубрики при создании документа. */
|
||
public static function prepareAliasInput(array $input, $id = 0)
|
||
{
|
||
if ((int) $id > 0) {
|
||
return $input;
|
||
}
|
||
|
||
$rubricId = isset($input['rubric_id']) ? (int) $input['rubric_id'] : 0;
|
||
$rubric = self::rubric($rubricId);
|
||
$pattern = $rubric && isset($rubric['rubric_alias']) ? (string) $rubric['rubric_alias'] : '';
|
||
$publishedAt = self::parseDate(isset($input['document_published']) ? $input['document_published'] : '');
|
||
$parentId = isset($input['document_parent']) ? (int) $input['document_parent'] : 0;
|
||
$alias = trim(isset($input['document_alias']) ? (string) $input['document_alias'] : '', '/ ');
|
||
if ($alias === '') {
|
||
$title = isset($input['document_title']) ? self::decode($input['document_title']) : '';
|
||
$input['document_alias'] = self::generateAlias($title, 0, $parentId, $rubricId, $publishedAt);
|
||
return $input;
|
||
}
|
||
|
||
$input['document_alias'] = DocumentAliasTemplate::compose(
|
||
$pattern,
|
||
$alias,
|
||
$publishedAt,
|
||
self::parentAliasPath($parentId)
|
||
);
|
||
return $input;
|
||
}
|
||
|
||
/** Шаблоны рубрики для выбора шаблона документа. */
|
||
public static function rubricTemplates($rubricId)
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT id, title FROM ' . self::rubricTemplatesTable() . ' WHERE rubric_id = %i ORDER BY title ASC, id ASC',
|
||
(int) $rubricId
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = array('id' => (int) $row['id'], 'title' => self::decode($row['title']));
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/** Пункты навигации для привязки документа. */
|
||
public static function navigationItems()
|
||
{
|
||
$rows = DB::query(
|
||
'SELECT navigation_item_id AS id, title, navigation_id FROM ' . self::navigationItemsTable()
|
||
. " WHERE status = '1' ORDER BY navigation_id ASC, position ASC, title ASC LIMIT 500"
|
||
)->getAll();
|
||
$out = array();
|
||
foreach ($rows as $row) {
|
||
$out[] = array('id' => (int) $row['id'], 'title' => self::decode($row['title']));
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/** Короткая карточка документа-родителя. */
|
||
public static function parentInfo($id)
|
||
{
|
||
$id = (int) $id;
|
||
if ($id <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$row = DB::query(
|
||
'SELECT Id, document_title, document_alias FROM ' . self::documentsTable() . ' WHERE Id = %i LIMIT 1',
|
||
$id
|
||
)->getAssoc();
|
||
if (!$row) {
|
||
return null;
|
||
}
|
||
|
||
return array(
|
||
'id' => (int) $row['Id'],
|
||
'title' => self::decode($row['document_title']),
|
||
'alias' => (string) $row['document_alias'],
|
||
);
|
||
}
|
||
|
||
public static function rubricTemplatesTable() { return ContentTables::table('rubric_templates'); }
|
||
|
||
public static function navigationItemsTable() { return ContentTables::table('navigation_items'); }
|
||
|
||
protected static function row(array $row, $full = false)
|
||
{
|
||
$row['Id'] = (int) $row['Id'];
|
||
$row['rubric_id'] = (int) $row['rubric_id'];
|
||
$row['document_title'] = self::decode($row['document_title']);
|
||
$row['document_breadcrumb_title'] = self::decode($row['document_breadcrumb_title']);
|
||
$row['document_status'] = (int) $row['document_status'];
|
||
$row['document_deleted'] = (int) $row['document_deleted'];
|
||
$row['document_in_search'] = (int) $row['document_in_search'];
|
||
$row['document_published'] = (int) $row['document_published'];
|
||
$row['document_expire'] = (int) $row['document_expire'];
|
||
$row['document_changed'] = (int) $row['document_changed'];
|
||
$row['document_version'] = isset($row['document_version']) ? max(1, (int) $row['document_version']) : 1;
|
||
$row['document_author_id'] = isset($row['document_author_id']) ? (int) $row['document_author_id'] : 1;
|
||
$row['document_count_view'] = isset($row['document_count_view']) ? (int) $row['document_count_view'] : 0;
|
||
$row['document_count_print'] = isset($row['document_count_print']) ? (int) $row['document_count_print'] : 0;
|
||
$row['document_short_alias'] = isset($row['document_short_alias']) ? (string) $row['document_short_alias'] : '';
|
||
$row['document_alias_header'] = isset($row['document_alias_header']) ? (int) $row['document_alias_header'] : 301;
|
||
$row['document_alias_history'] = isset($row['document_alias_history']) ? (int) $row['document_alias_history'] : 0;
|
||
$row['document_excerpt'] = isset($row['document_excerpt']) ? self::decode($row['document_excerpt']) : '';
|
||
$row['document_property'] = isset($row['document_property']) ? self::decode($row['document_property']) : '';
|
||
$row['guid'] = isset($row['guid']) ? (string) $row['guid'] : '';
|
||
$row['document_parent'] = isset($row['document_parent']) ? (int) $row['document_parent'] : 0;
|
||
$row['rubric_tmpl_id'] = isset($row['rubric_tmpl_id']) ? (int) $row['rubric_tmpl_id'] : 0;
|
||
$row['document_linked_navi_id'] = isset($row['document_linked_navi_id']) ? (int) $row['document_linked_navi_id'] : 0;
|
||
$row['document_sitemap_freq'] = isset($row['document_sitemap_freq']) ? (int) $row['document_sitemap_freq'] : 3;
|
||
$row['document_sitemap_pr'] = isset($row['document_sitemap_pr']) ? (float) $row['document_sitemap_pr'] : 0.5;
|
||
$row['document_position'] = isset($row['document_position']) ? (int) $row['document_position'] : 0;
|
||
$row['parent_info'] = $full && $row['document_parent'] > 0 ? self::parentInfo($row['document_parent']) : null;
|
||
$row['published_input'] = self::dateInput($row['document_published']);
|
||
$row['expire_input'] = self::dateInput($row['document_expire']);
|
||
$row['published_label'] = $row['document_published'] > 0 ? date('d.m.Y H:i', $row['document_published']) : 'не задано';
|
||
$row['changed_label'] = $row['document_changed'] > 0 ? date('d.m.Y H:i', $row['document_changed']) : 'не задано';
|
||
$row['state_label'] = $row['document_deleted'] ? 'Удалён' : ($row['document_status'] ? 'Опубликован' : 'Черновик');
|
||
$row['state_badge'] = $row['document_deleted'] ? 'gray' : ($row['document_status'] ? 'green' : 'amber');
|
||
$row['rubric_title'] = isset($row['rubric_title']) ? self::decode($row['rubric_title']) : '';
|
||
$row['rubric_alias'] = isset($row['rubric_alias']) ? (string) $row['rubric_alias'] : '';
|
||
$row['is_protected'] = self::isProtectedDocument($row['Id']);
|
||
$row['can_delete'] = !$row['is_protected'] && !$row['document_deleted'];
|
||
$row['can_restore'] = !$row['is_protected'] && $row['document_deleted'] === 1;
|
||
$row['can_toggle'] = !$row['is_protected'] && !$row['document_deleted'];
|
||
$row['can_copy'] = !$row['is_protected'] && !$row['document_deleted'];
|
||
$row['can_purge'] = !$row['is_protected'] && $row['document_deleted'] === 1;
|
||
if ($full) {
|
||
$row['fields_count'] = (int) DB::query('SELECT COUNT(*) FROM ' . self::docFieldsTable() . ' WHERE document_id = %i', $row['Id'])->getValue();
|
||
$row['revisions_count'] = self::revisionCount($row['Id']);
|
||
$row['aliases_count'] = (int) DB::query('SELECT COUNT(*) FROM ' . self::aliasHistoryTable() . ' WHERE document_id = %i', $row['Id'])->getValue();
|
||
$row['remarks_count'] = (int) DB::query('SELECT COUNT(*) FROM ' . self::remarksTable() . ' WHERE document_id = %i', $row['Id'])->getValue();
|
||
}
|
||
|
||
return $row;
|
||
}
|
||
|
||
public static function isProtectedDocument($id)
|
||
{
|
||
$id = (int) $id;
|
||
if ($id === 1) {
|
||
return true;
|
||
}
|
||
|
||
return $id > 0 && $id === (int) Settings::get('page_not_found_id', 0);
|
||
}
|
||
|
||
protected static function protectedDocumentMessage($id)
|
||
{
|
||
return (int) $id === 1
|
||
? 'Главная страница является системной и не может быть удалена'
|
||
: 'Документ назначен страницей 404 и не может быть удалён';
|
||
}
|
||
|
||
protected static function seedFields($documentId, $rubricId)
|
||
{
|
||
$fields = DB::query('SELECT Id FROM ' . self::fieldsTable() . ' WHERE rubric_id = %i', (int) $rubricId)->getAll();
|
||
foreach ($fields as $field) {
|
||
DB::Insert(self::docFieldsTable(), array(
|
||
'rubric_field_id' => (int) $field['Id'],
|
||
'document_id' => (int) $documentId,
|
||
));
|
||
}
|
||
}
|
||
|
||
protected static function fieldRow(array $row, $documentId, $mediaDraftToken = '', $conditionsEnabled = false)
|
||
{
|
||
$type = (string) $row['rubric_field_type'];
|
||
$hasInitialValue = array_key_exists('__initial_value', $row);
|
||
$raw = $hasInitialValue
|
||
? (string) $row['__initial_value']
|
||
: ($row['document_field_id']
|
||
? (string) $row['field_value'] . (string) $row['field_value_more']
|
||
: self::initialFieldValue($type, (string) $row['rubric_field_default']));
|
||
$editor = FieldAdminEditors::describe($type);
|
||
$kind = self::documentEditorKind($editor);
|
||
$settings = FieldSettings::effective($row);
|
||
if ($type === 'choice') {
|
||
$kind = isset($settings['mode']) && (string) $settings['mode'] === 'multiple' ? 'choice_multi' : 'choice';
|
||
}
|
||
|
||
$parsed = FieldAdminEditors::parseValue($type, $raw);
|
||
$plugin = FieldRegistry::get($type);
|
||
$isDocumentMedia = $plugin instanceof \App\Content\Fields\DocumentMediaFieldType;
|
||
$mediaTargetDir = $isDocumentMedia ? self::mediaTargetDir($row, $documentId) : '';
|
||
$field = array(
|
||
'Id' => (int) $row['Id'],
|
||
'rubric_id' => (int) $row['rubric_id'],
|
||
'rubric_field_group' => (int) $row['rubric_field_group'],
|
||
'rubric_field_title' => self::decode($row['rubric_field_title']),
|
||
'rubric_field_alias' => (string) $row['rubric_field_alias'],
|
||
'rubric_field_type' => $type,
|
||
'rubric_field_numeric' => (int) $row['rubric_field_numeric'],
|
||
'rubric_field_search' => (int) $row['rubric_field_search'],
|
||
'rubric_field_description' => self::decode($row['rubric_field_description']),
|
||
'rubric_field_default' => (string) $row['rubric_field_default'],
|
||
'rubric_field_settings' => isset($row['rubric_field_settings']) ? (string) $row['rubric_field_settings'] : '',
|
||
'group_settings' => isset($row['group_settings']) ? (string) $row['group_settings'] : '',
|
||
'settings' => $settings,
|
||
'condition' => $conditionsEnabled ? FieldConditionEvaluator::condition($row) : array(),
|
||
'document_field_id' => (int) $row['document_field_id'],
|
||
'field_value' => $raw,
|
||
'field_number_value' => isset($row['field_number_value']) ? (string) $row['field_number_value'] : '0',
|
||
'field_in_search' => isset($row['field_in_search']) ? (int) $row['field_in_search'] : 1,
|
||
'editor' => $editor,
|
||
'editor_kind' => $kind,
|
||
'parsed' => $type === 'youtube' ? $parsed : self::viewValue($kind, $parsed, $raw),
|
||
'options' => self::fieldOptions($row['rubric_field_default'], $type, $settings),
|
||
'media_upload_dir' => $isDocumentMedia ? self::mediaUploadDir($row, $documentId, $mediaDraftToken) : '',
|
||
'media_target_dir' => $mediaTargetDir,
|
||
'media_picker_dir' => $isDocumentMedia && (int) $documentId > 0 ? $mediaTargetDir : '/uploads',
|
||
'relation_rubric_ids' => isset($settings['rubric']) && is_array($settings['rubric']) ? array_values($settings['rubric']) : array(),
|
||
'tag' => '[tag:fld:' . (int) $row['Id'] . ']',
|
||
);
|
||
if ($kind === 'relation_list') {
|
||
$field['relation_items'] = self::relationDocuments(
|
||
isset($parsed['document_ids']) && is_array($parsed['document_ids']) ? $parsed['document_ids'] : array()
|
||
);
|
||
}
|
||
|
||
// Одиночный «Документ из рубрики»: резолвим заголовок выбранного документа,
|
||
// чтобы в поле показывать наименование, а не только ID.
|
||
if ($kind === 'relation' && $type === 'doc_from_rub') {
|
||
$selectedId = (((int) $row['document_field_id'] > 0 || $hasInitialValue) && is_array($parsed))
|
||
? (int) (isset($parsed['document_id']) ? $parsed['document_id'] : 0)
|
||
: 0;
|
||
if ($selectedId > 0) {
|
||
$docs = self::relationDocuments(array($selectedId));
|
||
$field['relation_selected'] = !empty($docs)
|
||
? $docs[0]
|
||
: array('id' => $selectedId, 'title' => 'Документ #' . $selectedId, 'alias' => '', 'rubric_title' => '');
|
||
}
|
||
}
|
||
|
||
// Оформление поля в документе из rubric_field_settings (adminx): ширина,
|
||
// обязательность и приставка/суффикс (составное поле, напр. цена + ₽).
|
||
$field['required'] = !empty($settings['required']);
|
||
// Приставка/суффикс имеют смысл только для простых однострочных полей
|
||
// (одиночный <input>), иначе аддон не встроить в контрол плагина.
|
||
$affixTypes = array('single_line', 'single_line_numeric', 'number');
|
||
$affixOk = in_array($type, $affixTypes, true);
|
||
$field['prefix'] = ($affixOk && !empty($settings['prefix'])) ? (string) $settings['prefix'] : '';
|
||
$field['suffix'] = ($affixOk && !empty($settings['suffix'])) ? (string) $settings['suffix'] : '';
|
||
// Числовому полю без явного суффикса подставляем единицу по названию/алиасу
|
||
// (Цена → ₽, Скидка → %, Вес → кг и т.п.), как в референсе sample/other.
|
||
if ($field['suffix'] === '' && $type === 'single_line_numeric') {
|
||
$field['suffix'] = self::autoUnit($field['rubric_field_title'], $field['rubric_field_alias']);
|
||
}
|
||
|
||
if ($field['suffix'] === '' && $type === 'number') {
|
||
$format = isset($settings['format']) ? (string) $settings['format'] : 'decimal';
|
||
if ($format === 'money') {
|
||
$currencies = array('RUB' => '₽', 'USD' => '$', 'EUR' => '€');
|
||
$currency = isset($settings['currency']) ? (string) $settings['currency'] : 'RUB';
|
||
$field['suffix'] = isset($currencies[$currency]) ? $currencies[$currency] : '';
|
||
} elseif ($format === 'percent') {
|
||
$field['suffix'] = '%';
|
||
} elseif ($format === 'measurement') {
|
||
$field['suffix'] = isset($settings['unit']) ? trim((string) $settings['unit']) : '';
|
||
}
|
||
}
|
||
|
||
// Плагин типа поля сам рисует контрол (см. Rubrics\FieldEditors\*::renderEdit).
|
||
$field['has_plugin'] = isset($editor['status']) && $editor['status'] === 'native';
|
||
$field['width_class'] = self::fieldWidthClass($kind, $type, isset($settings['width']) ? (string) $settings['width'] : '');
|
||
$field['html'] = $type === 'catalog'
|
||
? self::renderCatalogField($field, CatalogModel::parseValue($raw))
|
||
: FieldAdminEditors::renderEdit($type, $field);
|
||
return $field;
|
||
}
|
||
|
||
protected static function hasGroupSettingsColumn()
|
||
{
|
||
static $has = null;
|
||
if ($has === null) {
|
||
$has = DatabaseSchema::columnExists(self::groupsTable(), 'group_settings');
|
||
}
|
||
|
||
return $has;
|
||
}
|
||
|
||
/** Ширина поля в 12-колоночной сетке документа. $pref — явный пресет из настроек. */
|
||
protected static function fieldWidthClass($kind, $type, $pref = '')
|
||
{
|
||
switch ($pref) {
|
||
case 'full': return 'is-wide';
|
||
case 'half': return 'is-half';
|
||
case 'third': return 'is-third';
|
||
case 'quarter': return 'is-quarter';
|
||
}
|
||
|
||
// Авто: узкие типы — половина, остальные — на всю ширину.
|
||
$half = array('boolean', 'date', 'datetime', 'number', 'range', 'numeric_parts', 'choice', 'relation', 'url');
|
||
if (in_array($type, array('contact', 'color'), true)) {
|
||
return 'is-half';
|
||
}
|
||
|
||
if (in_array($kind, $half, true)) {
|
||
return 'is-half';
|
||
}
|
||
|
||
return 'is-wide';
|
||
}
|
||
|
||
/** Единица измерения числового поля по названию/алиасу (₽/%/кг/см/°/мес). */
|
||
protected static function autoUnit($title, $alias)
|
||
{
|
||
$text = $title . ' ' . $alias;
|
||
$text = function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text);
|
||
if (preg_match('/(^|[^a-zа-я])(price|old|цена|стоим)/u', $text)) { return '₽'; }
|
||
if (preg_match('/(discount|скидк)/u', $text)) { return '%'; }
|
||
if (preg_match('/(вес|нагруз|weight|load)/u', $text)) { return 'кг'; }
|
||
if (preg_match('/(ширин|высот|глубин|длин|размер|колес|поролон|width|height|depth|length|diameter)/u', $text)) { return 'см'; }
|
||
if (preg_match('/(угол|наклон|поворот|тренделенбург|angle|turn)/u', $text)) { return '°'; }
|
||
if (preg_match('/(гарант|warranty)/u', $text)) { return 'мес'; }
|
||
return '';
|
||
}
|
||
|
||
protected static function documentEditorKind(array $editor)
|
||
{
|
||
$type = isset($editor['type']) ? (string) $editor['type'] : '';
|
||
if (in_array($type, array('dropdown', 'drop_down', 'drop_down_key'), true)) {
|
||
return 'choice';
|
||
}
|
||
|
||
if (in_array($type, array('multi_select', 'checkbox_multi', 'multi_checkbox'), true)) {
|
||
return 'choice_multi';
|
||
}
|
||
|
||
if ($type === 'multi_list_single') {
|
||
return 'list_single';
|
||
}
|
||
|
||
if ($type === 'multi_list_triple') {
|
||
return 'list_triple';
|
||
}
|
||
|
||
if (in_array($type, array('multi_list', 'multi_links'), true)) {
|
||
return 'list_pair';
|
||
}
|
||
|
||
if (in_array($type, array('single_line_numeric_two', 'single_line_numeric_three'), true)) {
|
||
return 'numeric_parts';
|
||
}
|
||
|
||
// В describe() default_kind описывает редактор значения документа. Для
|
||
// типов, чьи legacy defaults были конфигурацией (медиа, связи), control
|
||
// значения по умолчанию намеренно удалён, но сам editor kind не меняется.
|
||
if (isset($editor['default_kind']) && (string) $editor['default_kind'] !== '') {
|
||
return (string) $editor['default_kind'];
|
||
}
|
||
|
||
$controls = isset($editor['controls']) && is_array($editor['controls']) ? $editor['controls'] : array();
|
||
foreach ($controls as $control) {
|
||
if (isset($control['name']) && $control['name'] === 'rubric_field_default') {
|
||
return isset($control['kind']) ? (string) $control['kind'] : 'textarea';
|
||
}
|
||
}
|
||
|
||
return 'textarea';
|
||
}
|
||
|
||
protected static function viewValue($kind, array $parsed, $raw)
|
||
{
|
||
if ($kind === 'boolean') {
|
||
return !empty($parsed['checked']) ? '1' : '0';
|
||
}
|
||
|
||
if ($kind === 'date') {
|
||
return isset($parsed['date']) ? (string) $parsed['date'] : '';
|
||
}
|
||
|
||
if ($kind === 'datetime') {
|
||
return isset($parsed['datetime']) ? (string) $parsed['datetime'] : '';
|
||
}
|
||
|
||
if ($kind === 'period') {
|
||
return array(
|
||
'start' => isset($parsed['start']) ? (string) $parsed['start'] : '',
|
||
'end' => isset($parsed['end']) ? (string) $parsed['end'] : '',
|
||
);
|
||
}
|
||
|
||
if ($kind === 'number') {
|
||
return isset($parsed['number']) ? (string) $parsed['number'] : (string) $raw;
|
||
}
|
||
|
||
if ($kind === 'range') {
|
||
return array(
|
||
'min' => isset($parsed['min']) ? (string) $parsed['min'] : '',
|
||
'max' => isset($parsed['max']) ? (string) $parsed['max'] : '',
|
||
);
|
||
}
|
||
|
||
if ($kind === 'dimensions') {
|
||
return array(
|
||
'length' => isset($parsed['length']) ? (string) $parsed['length'] : '',
|
||
'width' => isset($parsed['width']) ? (string) $parsed['width'] : '',
|
||
'height' => isset($parsed['height']) ? (string) $parsed['height'] : '',
|
||
);
|
||
}
|
||
|
||
if ($kind === 'address') {
|
||
return array(
|
||
'postal_code' => isset($parsed['postal_code']) ? (string) $parsed['postal_code'] : '',
|
||
'region' => isset($parsed['region']) ? (string) $parsed['region'] : '',
|
||
'city' => isset($parsed['city']) ? (string) $parsed['city'] : '',
|
||
'street' => isset($parsed['street']) ? (string) $parsed['street'] : '',
|
||
'building' => isset($parsed['building']) ? (string) $parsed['building'] : '',
|
||
'unit' => isset($parsed['unit']) ? (string) $parsed['unit'] : '',
|
||
'latitude' => isset($parsed['latitude']) ? (string) $parsed['latitude'] : '',
|
||
'longitude' => isset($parsed['longitude']) ? (string) $parsed['longitude'] : '',
|
||
);
|
||
}
|
||
|
||
if ($kind === 'numeric_parts') {
|
||
return isset($parsed['parts']) && is_array($parsed['parts']) ? $parsed['parts'] : array();
|
||
}
|
||
|
||
if ($kind === 'choice') {
|
||
return isset($parsed['selected']) ? (string) $parsed['selected'] : (string) $raw;
|
||
}
|
||
|
||
if ($kind === 'choice_multi') {
|
||
return isset($parsed['selected']) && is_array($parsed['selected']) ? $parsed['selected'] : array();
|
||
}
|
||
|
||
if ($kind === 'media') {
|
||
return array(
|
||
'url' => isset($parsed['url']) ? (string) $parsed['url'] : '',
|
||
'description' => isset($parsed['description']) ? (string) $parsed['description'] : '',
|
||
);
|
||
}
|
||
|
||
if ($kind === 'relation') {
|
||
return isset($parsed['document_id']) ? (string) $parsed['document_id'] : (string) $raw;
|
||
}
|
||
|
||
if (in_array($kind, array('lines', 'relation_list'), true)) {
|
||
$items = isset($parsed['document_ids']) ? $parsed['document_ids'] : (isset($parsed['tags']) ? $parsed['tags'] : array());
|
||
return is_array($items) ? implode("\n", $items) : (string) $raw;
|
||
}
|
||
|
||
if (in_array($kind, array('key_value', 'pipe_list'), true)) {
|
||
return self::itemsToLines(isset($parsed['items']) ? $parsed['items'] : array(), $raw);
|
||
}
|
||
|
||
if ($kind === 'media_list') {
|
||
return array(
|
||
'raw' => (string) $raw,
|
||
'items' => isset($parsed['items']) && is_array($parsed['items']) ? $parsed['items'] : array(),
|
||
);
|
||
}
|
||
|
||
if (in_array($kind, array('list_single', 'list_pair', 'list_triple', 'packages'), true)) {
|
||
return array(
|
||
'raw' => (string) $raw,
|
||
'items' => isset($parsed['items']) && is_array($parsed['items']) ? $parsed['items'] : array(),
|
||
);
|
||
}
|
||
|
||
if ($kind === 'pipe') {
|
||
return isset($parsed['parts']) && is_array($parsed['parts']) ? implode('|', $parsed['parts']) : (string) $raw;
|
||
}
|
||
|
||
return (string) $raw;
|
||
}
|
||
|
||
protected static function fieldInputValue(array $field, $value)
|
||
{
|
||
$kind = (string) $field['editor_kind'];
|
||
$type = (string) $field['rubric_field_type'];
|
||
if (!is_array($value)) {
|
||
$value = array('raw' => (string) $value);
|
||
}
|
||
|
||
if ($type === 'catalog') {
|
||
$selected = CatalogModel::selection((int) $field['rubric_id'], (int) $field['Id'], isset($value['catalog_ids']) ? $value['catalog_ids'] : array());
|
||
return CatalogModel::serializeSelection((int) $field['rubric_id'], (int) $field['Id'], $selected);
|
||
}
|
||
|
||
if ($type === 'youtube') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'url' => isset($value['url']) ? (string) $value['url'] : '',
|
||
'width' => isset($value['width']) ? (string) $value['width'] : '',
|
||
'height' => isset($value['height']) ? (string) $value['height'] : '',
|
||
'fullscreen' => isset($value['fullscreen']) ? (string) $value['fullscreen'] : '',
|
||
'source' => isset($value['source']) ? (string) $value['source'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'boolean') {
|
||
return FieldAdminEditors::serializeValue($type, array('checked' => !empty($value['checked'])));
|
||
}
|
||
|
||
if ($kind === 'date') {
|
||
return FieldAdminEditors::serializeValue($type, array('date' => isset($value['date']) ? (string) $value['date'] : ''));
|
||
}
|
||
|
||
if ($kind === 'datetime') {
|
||
return FieldAdminEditors::serializeValue($type, array('datetime' => isset($value['datetime']) ? (string) $value['datetime'] : ''));
|
||
}
|
||
|
||
if ($kind === 'period') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'start' => isset($value['start']) ? (string) $value['start'] : '',
|
||
'end' => isset($value['end']) ? (string) $value['end'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'number') {
|
||
return FieldAdminEditors::serializeValue($type, array('number' => isset($value['number']) ? (string) $value['number'] : ''));
|
||
}
|
||
|
||
if ($kind === 'range') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'min' => isset($value['min']) ? (string) $value['min'] : '',
|
||
'max' => isset($value['max']) ? (string) $value['max'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'dimensions') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'length' => isset($value['length']) ? (string) $value['length'] : '',
|
||
'width' => isset($value['width']) ? (string) $value['width'] : '',
|
||
'height' => isset($value['height']) ? (string) $value['height'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'address') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'postal_code' => isset($value['postal_code']) ? (string) $value['postal_code'] : '',
|
||
'region' => isset($value['region']) ? (string) $value['region'] : '',
|
||
'city' => isset($value['city']) ? (string) $value['city'] : '',
|
||
'street' => isset($value['street']) ? (string) $value['street'] : '',
|
||
'building' => isset($value['building']) ? (string) $value['building'] : '',
|
||
'unit' => isset($value['unit']) ? (string) $value['unit'] : '',
|
||
'latitude' => isset($value['latitude']) ? (string) $value['latitude'] : '',
|
||
'longitude' => isset($value['longitude']) ? (string) $value['longitude'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'numeric_parts') {
|
||
return FieldAdminEditors::serializeValue($type, array('parts' => isset($value['parts']) && is_array($value['parts']) ? $value['parts'] : array()));
|
||
}
|
||
|
||
if ($kind === 'choice') {
|
||
return FieldAdminEditors::serializeValue($type, array('selected' => isset($value['selected']) ? (string) $value['selected'] : ''));
|
||
}
|
||
|
||
if ($kind === 'choice_multi') {
|
||
return FieldAdminEditors::serializeValue($type, array('selected' => isset($value['selected']) && is_array($value['selected']) ? $value['selected'] : array()));
|
||
}
|
||
|
||
if ($kind === 'media') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'url' => isset($value['url']) ? (string) $value['url'] : '',
|
||
'description' => isset($value['description']) ? (string) $value['description'] : '',
|
||
));
|
||
}
|
||
|
||
if ($kind === 'relation') {
|
||
return FieldAdminEditors::serializeValue($type, array('document_id' => isset($value['document_id']) ? (int) $value['document_id'] : 0));
|
||
}
|
||
|
||
if ($kind === 'relation_list') {
|
||
$ids = isset($value['document_ids']) && is_array($value['document_ids'])
|
||
? $value['document_ids']
|
||
: self::linesArray(isset($value['lines']) ? $value['lines'] : '');
|
||
return FieldAdminEditors::serializeValue($type, array('document_ids' => $ids));
|
||
}
|
||
|
||
if ($kind === 'lines') {
|
||
if ($type === 'tags') {
|
||
return FieldAdminEditors::serializeValue($type, array('tags' => self::linesArray(isset($value['lines']) ? $value['lines'] : '')));
|
||
}
|
||
|
||
return isset($value['raw']) ? (string) $value['raw'] : (isset($value['lines']) ? (string) $value['lines'] : '');
|
||
}
|
||
|
||
if ($kind === 'media_list') {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'items' => isset($value['items']) && is_array($value['items']) ? $value['items'] : array(),
|
||
));
|
||
}
|
||
|
||
if (in_array($kind, array('list_single', 'list_pair', 'list_triple', 'packages'), true)) {
|
||
return FieldAdminEditors::serializeValue($type, array(
|
||
'items' => isset($value['items']) && is_array($value['items']) ? $value['items'] : array(),
|
||
));
|
||
}
|
||
|
||
if (in_array($kind, array('key_value', 'pipe_list'), true)) {
|
||
return self::serializeLinesByKind($type, $kind, isset($value['lines']) ? (string) $value['lines'] : '');
|
||
}
|
||
|
||
if ($kind === 'pipe') {
|
||
return FieldAdminEditors::serializeValue($type, array('parts' => preg_split('/[|,\r\n]+/', isset($value['raw']) ? (string) $value['raw'] : '')));
|
||
}
|
||
|
||
return isset($value['raw']) ? (string) $value['raw'] : '';
|
||
}
|
||
|
||
protected static function saveFieldValue($documentId, array $field, $value, $inSearch)
|
||
{
|
||
$documentId = (int) $documentId;
|
||
$fieldId = (int) $field['Id'];
|
||
$type = isset($field['rubric_field_type']) ? (string) $field['rubric_field_type'] : '';
|
||
$value = FieldLifecycle::normalizeForStorage($value, $field, array('Id' => $documentId), 'adminx_document');
|
||
$saving = FieldLifecycle::saving($value, $field, $documentId, 'adminx_document');
|
||
if ($saving->cancelled()) { return; }
|
||
$value = $saving->result();
|
||
$substr = mb_substr($value, 501, 1) ? 499 : 500;
|
||
$first = mb_substr($value, 0, $substr);
|
||
$slash = mb_substr($first, 498, 1) === '\\';
|
||
if ($slash) {
|
||
$first = rtrim($first, '\\');
|
||
}
|
||
|
||
$data = array(
|
||
'field_value' => $first,
|
||
'field_number_value' => ((int) $field['rubric_field_numeric'] === 1)
|
||
? ($type === 'period' ? \App\Content\Fields\Types\PeriodValue::indexValue($value) : self::numericValue($value))
|
||
: 0,
|
||
'document_in_search' => (int) $inSearch ? '1' : '0',
|
||
);
|
||
$exists = (int) DB::query('SELECT Id FROM ' . self::docFieldsTable() . ' WHERE document_id = %i AND rubric_field_id = %i LIMIT 1', $documentId, $fieldId)->getValue();
|
||
if ($exists > 0) {
|
||
DB::Update(self::docFieldsTable(), $data, 'Id = %i', $exists);
|
||
} else {
|
||
$data['document_id'] = $documentId;
|
||
$data['rubric_field_id'] = $fieldId;
|
||
DB::Insert(self::docFieldsTable(), $data);
|
||
}
|
||
|
||
if (mb_strlen($value) > $substr) {
|
||
$more = mb_substr($value, $substr);
|
||
if ($slash) {
|
||
$more = '\\' . $more;
|
||
}
|
||
|
||
$textId = (int) DB::query('SELECT Id FROM ' . self::docFieldsTextTable() . ' WHERE document_id = %i AND rubric_field_id = %i LIMIT 1', $documentId, $fieldId)->getValue();
|
||
if ($textId > 0) {
|
||
DB::Update(self::docFieldsTextTable(), array('field_value' => $more), 'Id = %i', $textId);
|
||
} else {
|
||
DB::Insert(self::docFieldsTextTable(), array('document_id' => $documentId, 'rubric_field_id' => $fieldId, 'field_value' => $more));
|
||
}
|
||
} else {
|
||
DB::Delete(self::docFieldsTextTable(), 'document_id = %i AND rubric_field_id = %i', $documentId, $fieldId);
|
||
}
|
||
|
||
FieldLifecycle::saved($value, $field, $documentId, 'adminx_document');
|
||
}
|
||
|
||
protected static function relationDocuments(array $ids)
|
||
{
|
||
$ordered = array();
|
||
foreach ($ids as $id) {
|
||
$id = (int) $id;
|
||
if ($id > 0 && !isset($ordered[$id])) { $ordered[$id] = null; }
|
||
}
|
||
|
||
if (empty($ordered)) { return array(); }
|
||
|
||
$rows = DB::query(
|
||
'SELECT d.Id, d.document_title, d.document_alias, r.rubric_title'
|
||
. ' FROM ' . self::documentsTable() . ' d'
|
||
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
|
||
. ' WHERE d.Id IN (' . implode(',', array_keys($ordered)) . ')'
|
||
)->getAll();
|
||
foreach ($rows as $row) {
|
||
$id = (int) $row['Id'];
|
||
$ordered[$id] = array(
|
||
'id' => $id,
|
||
'title' => self::decode($row['document_title']),
|
||
'alias' => (string) $row['document_alias'],
|
||
'rubric_title' => self::decode(isset($row['rubric_title']) ? $row['rubric_title'] : ''),
|
||
);
|
||
}
|
||
|
||
$out = array();
|
||
foreach ($ordered as $id => $item) {
|
||
$out[] = $item ?: array('id' => (int) $id, 'title' => 'Документ недоступен', 'alias' => '', 'rubric_title' => '');
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
protected static function renderCatalogField(array $field, array $parsed)
|
||
{
|
||
$selectedIds = array();
|
||
foreach (isset($parsed['catalog_ids']) ? $parsed['catalog_ids'] : array() as $cid) {
|
||
$selectedIds[] = (int) $cid;
|
||
}
|
||
|
||
$selected = array_fill_keys($selectedIds, true);
|
||
$tree = CatalogModel::tree((int) $field['rubric_id'], (int) $field['Id'], false);
|
||
$settings = CatalogModel::settings((int) $field['rubric_id'], (int) $field['Id']);
|
||
$id = (int) $field['Id'];
|
||
|
||
$flat = array();
|
||
self::flattenCatalog($tree, $flat);
|
||
$tokens = '';
|
||
foreach ($selectedIds as $cid) {
|
||
if (isset($flat[$cid])) {
|
||
$tokens .= self::catalogTokenHtml($id, $cid, $flat[$cid]['name'], $flat[$cid]['fields']);
|
||
}
|
||
}
|
||
|
||
$emptyLabel = AdminLocale::translateMarkup('Разделы не выбраны.');
|
||
$addLabel = AdminLocale::translateMarkup('Добавить раздел');
|
||
$searchLabel = AdminLocale::translateMarkup('Найти раздел');
|
||
$html = '<div class="documents-catalog-field" data-document-catalog-field="' . $id . '" data-catalog-limits-fields="' . ((int) $settings['doc_fileds'] === 1 ? '1' : '0') . '">'
|
||
. '<input type="hidden" name="fields[' . $id . '][catalog_ids][]" value="">'
|
||
. '<div class="documents-catalog-tokens" data-document-catalog-tokens>' . $tokens . '</div>'
|
||
. '<p class="documents-catalog-empty"' . (!empty($selectedIds) ? ' hidden' : '') . ' data-document-catalog-empty>' . $emptyLabel . '</p>'
|
||
. '<div class="dropdown documents-catalog-dropdown">'
|
||
. '<button class="btn btn-secondary btn-sm" type="button" data-dropdown data-document-catalog-add><i class="ti ti-plus"></i>' . $addLabel . '</button>'
|
||
. '<div class="dropdown-menu documents-catalog-menu">'
|
||
. '<label class="input-wrap documents-catalog-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="' . $searchLabel . '" data-document-catalog-search></label>'
|
||
. '<ol class="documents-catalog-tree">' . self::renderCatalogNodes($tree, $id, $selected) . '</ol>'
|
||
. '</div></div></div>';
|
||
return $html;
|
||
}
|
||
|
||
protected static function flattenCatalog(array $items, array &$flat)
|
||
{
|
||
foreach ($items as $item) {
|
||
$flat[(int) $item['id']] = array(
|
||
'name' => (string) $item['name'],
|
||
'fields' => implode(',', isset($item['fields_use']) ? $item['fields_use'] : array()),
|
||
);
|
||
if (!empty($item['children'])) {
|
||
self::flattenCatalog($item['children'], $flat);
|
||
}
|
||
}
|
||
}
|
||
|
||
protected static function catalogTokenHtml($fieldId, $catalogId, $name, $fields)
|
||
{
|
||
$removeLabel = AdminLocale::translateMarkup('Убрать раздел');
|
||
return '<span class="documents-catalog-token" data-document-catalog-token data-catalog-id="' . (int) $catalogId
|
||
. '" data-catalog-fields="' . self::e($fields) . '">'
|
||
. '<i class="ti ti-folder documents-catalog-token-icon" aria-hidden="true"></i>'
|
||
. '<span class="documents-catalog-token-name">' . self::e($name) . '</span>'
|
||
. '<input type="hidden" name="fields[' . (int) $fieldId . '][catalog_ids][]" value="' . (int) $catalogId . '">'
|
||
. '<button class="documents-catalog-token-remove" type="button" data-document-catalog-remove aria-label="' . $removeLabel . '"><i class="ti ti-x"></i></button>'
|
||
. '</span>';
|
||
}
|
||
|
||
protected static function renderCatalogNodes(array $items, $fieldId, array $selected)
|
||
{
|
||
$html = '';
|
||
$documentLabel = AdminLocale::translateMarkup('документ #');
|
||
$hiddenLabel = AdminLocale::translateMarkup('скрыт');
|
||
foreach ($items as $item) {
|
||
$fields = implode(',', isset($item['fields_use']) ? $item['fields_use'] : array());
|
||
$isSelected = isset($selected[$item['id']]);
|
||
$html .= '<li data-document-catalog-node data-search="' . self::e($item['name']) . '">'
|
||
. '<button class="documents-catalog-option' . ($isSelected ? ' is-picked' : '') . '" type="button" data-document-catalog-option'
|
||
. ' data-catalog-id="' . (int) $item['id'] . '" data-catalog-name="' . self::e($item['name']) . '" data-catalog-fields="' . self::e($fields) . '">'
|
||
. '<span class="documents-catalog-option-main"><b>' . self::e($item['name']) . '</b><small>#' . (int) $item['id']
|
||
. ($item['document_id'] > 0 ? ' · ' . $documentLabel . (int) $item['document_id'] : '') . '</small></span>'
|
||
. ((int) $item['status'] === 1 ? '' : '<span class="badge badge-gray">' . $hiddenLabel . '</span>') . '</button>';
|
||
if (!empty($item['children'])) { $html .= '<ol>' . self::renderCatalogNodes($item['children'], $fieldId, $selected) . '</ol>'; }
|
||
$html .= '</li>';
|
||
}
|
||
|
||
return $html;
|
||
}
|
||
|
||
protected static function e($value)
|
||
{
|
||
return Str::escape((string) $value);
|
||
}
|
||
|
||
/**
|
||
* Legacy field default is usually type configuration, not a document value.
|
||
* Only scalar/content fields use it as the initial value of a new document.
|
||
*/
|
||
protected static function initialFieldValue($type, $default)
|
||
{
|
||
$type = (string) $type;
|
||
$default = (string) $default;
|
||
if (in_array($type, array(
|
||
'single_line', 'single_line_numeric', 'single_line_numeric_two', 'single_line_numeric_three', 'number', 'range', 'dimensions', 'packages', 'address', 'date_time', 'period', 'choice',
|
||
'multi_line', 'multi_line_simple', 'multi_line_slim', 'content', 'text_to_html', 'text_to_image', 'checkbox', 'date', 'link', 'contact', 'color', 'code'
|
||
), true)) {
|
||
return $default;
|
||
}
|
||
|
||
if (in_array($type, array('dropdown', 'drop_down'), true)) {
|
||
foreach (preg_split('/\r\n|\r|\n|,/', $default) ?: array() as $option) {
|
||
$option = trim((string) $option);
|
||
if ($option !== '') {
|
||
return $option;
|
||
}
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
protected static function fieldOptions($default, $type = '', array $settings = array())
|
||
{
|
||
$default = (string) $default;
|
||
$type = (string) $type;
|
||
if (!empty($settings['options']) && is_array($settings['options'])) {
|
||
$out = array();
|
||
foreach ($settings['options'] as $key => $option) {
|
||
if (is_array($option)) {
|
||
$value = isset($option['value']) ? (string) $option['value'] : (isset($option['key']) ? (string) $option['key'] : (string) $key);
|
||
$label = isset($option['label']) ? (string) $option['label'] : (isset($option['title']) ? (string) $option['title'] : $value);
|
||
} else {
|
||
$value = is_int($key) ? (string) $option : (string) $key;
|
||
$label = (string) $option;
|
||
}
|
||
|
||
if ($value !== '' && $label !== '') {
|
||
$out[] = array('value' => $value, 'label' => $label);
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
$lines = preg_split('/\r\n|\r|\n/', $default);
|
||
if (count($lines) <= 1) { $lines = explode(',', $default); }
|
||
$indexed = in_array($type, array('drop_down_key', 'checkbox_multi', 'multi_checkbox'), true);
|
||
$out = array();
|
||
foreach ($lines ?: array() as $index => $line) {
|
||
$line = trim((string) $line);
|
||
if ($line === '') {
|
||
continue;
|
||
}
|
||
|
||
$parts = explode('|', $line, 2);
|
||
$label = isset($parts[1]) && trim($parts[1]) !== '' ? trim($parts[1]) : trim($parts[0]);
|
||
$value = trim($parts[0]);
|
||
if ($indexed) {
|
||
$value = (string) ((int) $index + ($type === 'drop_down_key' ? 0 : 1));
|
||
$label = $line;
|
||
}
|
||
|
||
$out[] = array('value' => $value, 'label' => $label);
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
protected static function mediaUploadDir(array $field, $documentId, $mediaDraftToken = '')
|
||
{
|
||
$type = isset($field['rubric_field_type']) ? (string) $field['rubric_field_type'] : '';
|
||
$plugin = FieldRegistry::get($type);
|
||
if (!$plugin instanceof \App\Content\Fields\DocumentMediaFieldType) {
|
||
return '/uploads';
|
||
}
|
||
|
||
if ($mediaDraftToken !== '') {
|
||
return DocumentMediaDraft::directory($mediaDraftToken, (int) $field['Id']);
|
||
}
|
||
|
||
return (int) $documentId > 0 ? self::mediaTargetDir($field, $documentId) : '/uploads';
|
||
}
|
||
|
||
protected static function mediaTargetDir(array $field, $documentId)
|
||
{
|
||
$rubric = self::rubric(isset($field['rubric_id']) ? (int) $field['rubric_id'] : 0);
|
||
return (int) $documentId > 0
|
||
? DocumentMediaPath::resolve($field, (int) $documentId, is_array($rubric) ? $rubric : array())
|
||
: DocumentMediaPath::describe($field, is_array($rubric) ? $rubric : array());
|
||
}
|
||
|
||
protected static function linesArray($value)
|
||
{
|
||
$items = preg_split('/\r\n|\r|\n|,/', (string) $value);
|
||
$out = array();
|
||
foreach ($items ?: array() as $item) {
|
||
$item = trim((string) $item);
|
||
if ($item !== '') {
|
||
$out[] = $item;
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
protected static function serializeLinesByKind($type, $kind, $lines)
|
||
{
|
||
$items = array();
|
||
foreach (preg_split('/\r\n|\r|\n/', (string) $lines) ?: array() as $line) {
|
||
$line = trim((string) $line);
|
||
if ($line === '') {
|
||
continue;
|
||
}
|
||
|
||
$parts = explode('|', $line);
|
||
if ($kind === 'media_list') {
|
||
$items[] = array('url' => isset($parts[0]) ? $parts[0] : '', 'description' => isset($parts[1]) ? implode('|', array_slice($parts, 1)) : '');
|
||
} elseif ($type === 'multi_list_single') {
|
||
$items[] = array('value' => $line);
|
||
} elseif ($type === 'multi_list_triple') {
|
||
$items[] = array('param' => isset($parts[0]) ? $parts[0] : '', 'value' => isset($parts[1]) ? $parts[1] : '', 'value2' => isset($parts[2]) ? $parts[2] : '');
|
||
} else {
|
||
$items[] = array('param' => isset($parts[0]) ? $parts[0] : '', 'value' => isset($parts[1]) ? implode('|', array_slice($parts, 1)) : '');
|
||
}
|
||
}
|
||
|
||
return FieldAdminEditors::serializeValue($type, array('items' => $items));
|
||
}
|
||
|
||
protected static function itemsToLines(array $items, $raw)
|
||
{
|
||
if (empty($items)) {
|
||
return (string) $raw;
|
||
}
|
||
|
||
$out = array();
|
||
foreach ($items as $item) {
|
||
if (!is_array($item)) {
|
||
continue;
|
||
}
|
||
|
||
if (isset($item['url'])) {
|
||
$out[] = (string) $item['url'] . (!empty($item['description']) ? '|' . $item['description'] : '');
|
||
} elseif (isset($item['value2'])) {
|
||
$out[] = (string) $item['param'] . '|' . (string) $item['value'] . '|' . (string) $item['value2'];
|
||
} elseif (isset($item['param'])) {
|
||
$out[] = (string) $item['param'] . (isset($item['value']) && $item['value'] !== '' ? '|' . $item['value'] : '');
|
||
} elseif (isset($item['value'])) {
|
||
$out[] = (string) $item['value'];
|
||
}
|
||
}
|
||
|
||
return implode("\n", $out);
|
||
}
|
||
|
||
protected static function nextPosition()
|
||
{
|
||
return (int) DB::query('SELECT COALESCE(MAX(document_position), 0) + 1 FROM ' . self::documentsTable())->getValue();
|
||
}
|
||
|
||
protected static function systemLanguage()
|
||
{
|
||
if (defined('DEFAULT_LANGUAGE') && trim((string) DEFAULT_LANGUAGE) !== '') {
|
||
return trim((string) DEFAULT_LANGUAGE);
|
||
}
|
||
|
||
return 'ru';
|
||
}
|
||
|
||
protected static function legacyAuthorId($adminId)
|
||
{
|
||
$adminId = (int) $adminId;
|
||
if ($adminId <= 0) {
|
||
return 1;
|
||
}
|
||
|
||
$legacyId = 0;
|
||
try {
|
||
$hasLegacyId = DB::query('SHOW COLUMNS FROM ' . SystemTables::table('users') . ' LIKE %s', 'legacy_id')->numRows() > 0;
|
||
if ($hasLegacyId) { $legacyId = (int) DB::query('SELECT legacy_id FROM ' . SystemTables::table('users') . ' WHERE id = %i LIMIT 1', $adminId)->getValue(); }
|
||
} catch (\Throwable $e) { $legacyId = 0; }
|
||
return $legacyId > 0 ? $legacyId : $adminId;
|
||
}
|
||
|
||
protected static function validLegacyAuthorId($id)
|
||
{
|
||
$id = (int) $id;
|
||
foreach (self::authors() as $author) {
|
||
if ((int) $author['id'] === $id) {
|
||
return $id;
|
||
}
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
protected static function adminEmail($adminId)
|
||
{
|
||
return (string) DB::query('SELECT email FROM ' . SystemTables::table('users') . ' WHERE id = %i LIMIT 1', (int) $adminId)->getValue();
|
||
}
|
||
|
||
protected static function aliasHistoryMode($value)
|
||
{
|
||
$value = (int) $value;
|
||
return in_array($value, array(0, 1, 2), true) ? $value : 0;
|
||
}
|
||
|
||
protected static function redirectHeader($value)
|
||
{
|
||
$value = (int) $value;
|
||
return in_array($value, array(301, 302, 307, 308), true) ? $value : 301;
|
||
}
|
||
|
||
protected static function syncAliasHistory($documentId, $existing, array $data, $authorId, $rubric)
|
||
{
|
||
if (!$existing) {
|
||
return;
|
||
}
|
||
|
||
$oldAlias = trim(isset($existing['document_alias']) ? (string) $existing['document_alias'] : '', '/ ');
|
||
$newAlias = trim(isset($data['document_alias']) ? (string) $data['document_alias'] : '', '/ ');
|
||
self::removeOwnedHistoryAlias($documentId, $newAlias);
|
||
if ($oldAlias === '' || $oldAlias === $newAlias) {
|
||
return;
|
||
}
|
||
|
||
$mode = isset($data['document_alias_history']) ? (int) $data['document_alias_history'] : 0;
|
||
$rubricEnabled = $rubric && !empty($rubric['rubric_alias_history']);
|
||
if ($mode === 2 || ($mode === 0 && !$rubricEnabled)) {
|
||
return;
|
||
}
|
||
|
||
if (!self::historyAliasAvailable($oldAlias, $documentId, 0)) {
|
||
return;
|
||
}
|
||
|
||
DB::Insert(self::aliasHistoryTable(), array(
|
||
'document_id' => (int) $documentId,
|
||
'document_alias' => $oldAlias,
|
||
'document_alias_header' => self::redirectHeader(isset($existing['document_alias_header']) ? $existing['document_alias_header'] : 301),
|
||
'document_alias_author' => (int) $authorId > 0 ? (int) $authorId : 1,
|
||
'document_alias_changed' => time(),
|
||
));
|
||
}
|
||
|
||
protected static function historyAliasAvailable($alias, $documentId, $exceptHistoryId)
|
||
{
|
||
$document = (int) DB::query(
|
||
'SELECT Id FROM ' . self::documentsTable() . ' WHERE document_alias = %s AND Id != %i LIMIT 1',
|
||
(string) $alias,
|
||
(int) $documentId
|
||
)->getValue();
|
||
if ($document > 0) {
|
||
return false;
|
||
}
|
||
|
||
$history = (int) DB::query(
|
||
'SELECT id FROM ' . self::aliasHistoryTable() . ' WHERE document_alias = %s AND id != %i LIMIT 1',
|
||
(string) $alias,
|
||
(int) $exceptHistoryId
|
||
)->getValue();
|
||
return $history <= 0;
|
||
}
|
||
|
||
protected static function removeOwnedHistoryAlias($documentId, $alias)
|
||
{
|
||
$alias = trim((string) $alias, '/ ');
|
||
if ($alias !== '') {
|
||
DB::Delete(self::aliasHistoryTable(), 'document_id = %i AND document_alias = %s', (int) $documentId, $alias);
|
||
}
|
||
}
|
||
|
||
protected static function syncSearchTerms($documentId, $rubricId, $keywords, $tags)
|
||
{
|
||
DocumentTerms::sync($documentId, $rubricId, $keywords, $tags);
|
||
}
|
||
|
||
protected static function parseDate($value)
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return 0;
|
||
}
|
||
|
||
$time = strtotime($value);
|
||
return $time ? (int) $time : 0;
|
||
}
|
||
|
||
protected static function robotsValue($value)
|
||
{
|
||
$value = trim((string) $value);
|
||
return in_array($value, array('index,follow', 'index,nofollow', 'noindex,nofollow'), true) ? $value : 'index,follow';
|
||
}
|
||
|
||
protected static function numericValue($value)
|
||
{
|
||
$value = preg_replace('/[^\d.]/', '', (string) $value);
|
||
return $value === '' ? 0 : $value;
|
||
}
|
||
|
||
protected static function dateInput($time)
|
||
{
|
||
return (int) $time > 0 ? date('Y-m-d\\TH:i', (int) $time) : '';
|
||
}
|
||
|
||
protected static function decode($value)
|
||
{
|
||
return htmlspecialchars_decode(stripslashes((string) $value), ENT_QUOTES);
|
||
}
|
||
|
||
protected static function clearDocumentCache($documentId)
|
||
{
|
||
ContentCacheInvalidator::document($documentId, false);
|
||
}
|
||
|
||
protected static function buildDocumentSnapshot($documentId)
|
||
{
|
||
return ContentCacheInvalidator::document($documentId, true);
|
||
}
|
||
|
||
protected static function tableExists($table)
|
||
{
|
||
return (bool) DB::query('SHOW TABLES LIKE %s', (string) $table)->getValue();
|
||
}
|
||
}
|