mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
446 lines
15 KiB
PHP
446 lines
15 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file system/App/Content/Introspection/PageContextInspector.php
|
|
| @author AVE.cms <support@ave-cms.ru>
|
|
| @copyright 2007-2026 (c) AVE.cms
|
|
| @link https://ave-cms.ru
|
|
| @version 3.3
|
|
*/
|
|
|
|
namespace App\Content\Introspection;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use App\Common\CacheKey;
|
|
use App\Common\DatabaseSchema;
|
|
use App\Content\ContentPlacementMap;
|
|
use App\Content\ContentTables;
|
|
use App\Content\Documents\RubricSchemaBuilder;
|
|
use App\Content\Fields\FieldValueCodec;
|
|
use App\Frontend\DocumentAccessPolicy;
|
|
use App\Frontend\DocumentRenderCache;
|
|
use App\Frontend\DocumentRouteRepository;
|
|
use App\Frontend\DocumentUrlParser;
|
|
use DB;
|
|
|
|
/** Builds a read-only package explaining the composition of one public page. */
|
|
class PageContextInspector
|
|
{
|
|
const MAX_COMPONENTS = 250;
|
|
|
|
public function inspect($target)
|
|
{
|
|
$resolved = $this->resolveTarget($target);
|
|
$document = $this->document($resolved['document_id']);
|
|
if (!$document) {
|
|
throw new \InvalidArgumentException('Документ не найден');
|
|
}
|
|
|
|
$rubricId = (int) $document['rubric_id'];
|
|
$schema = (new RubricSchemaBuilder())->build($rubricId);
|
|
$fields = $this->fields($document, $schema);
|
|
$requests = $this->requests($rubricId);
|
|
$placements = $this->placements(
|
|
(int) $schema['rubric']['rubric_template_id'],
|
|
$rubricId,
|
|
(int) $document['rubric_tmpl_id']
|
|
);
|
|
|
|
return array(
|
|
'contract' => 'ave.page-context',
|
|
'contract_version' => 1,
|
|
'target' => $resolved,
|
|
'document' => $this->documentSummary($document),
|
|
'rubric' => $this->rubricSummary($schema),
|
|
'fields' => $fields,
|
|
'templates' => $this->templates($schema, $document),
|
|
'requests' => $requests,
|
|
'presentations' => $this->presentations($rubricId, $requests),
|
|
'components' => $placements,
|
|
'navigation' => $this->navigation((int) $document['document_linked_navi_id']),
|
|
'cache' => $this->cache($document, $schema),
|
|
);
|
|
}
|
|
|
|
protected function resolveTarget($target)
|
|
{
|
|
$target = trim((string) $target);
|
|
if ($target === '') {
|
|
throw new \InvalidArgumentException('Укажите URL или ID документа');
|
|
}
|
|
|
|
if (ctype_digit($target)) {
|
|
return array(
|
|
'input' => $target,
|
|
'type' => 'document_id',
|
|
'document_id' => (int) $target,
|
|
'alias' => '',
|
|
);
|
|
}
|
|
|
|
$path = parse_url($target, PHP_URL_PATH);
|
|
$path = $path === null || $path === false ? $target : $path;
|
|
$suffix = defined('URL_SUFFIX') ? (string) URL_SUFFIX : '';
|
|
$parsed = (new DocumentUrlParser())->parse((string) $path, '/', $suffix);
|
|
$alias = (new DocumentRouteRepository())->normalizeAlias($parsed['alias']);
|
|
$documentId = $alias === '' ? 1 : (new DocumentRouteRepository())->idByAlias($alias);
|
|
|
|
return array(
|
|
'input' => $target,
|
|
'type' => 'public_url',
|
|
'document_id' => (int) $documentId,
|
|
'alias' => $alias,
|
|
);
|
|
}
|
|
|
|
protected function document($documentId)
|
|
{
|
|
$row = DB::query(
|
|
'SELECT * FROM %b WHERE Id=%i LIMIT 1',
|
|
ContentTables::table('documents'),
|
|
(int) $documentId
|
|
)->getAssoc();
|
|
|
|
return $row ? (array) $row : array();
|
|
}
|
|
|
|
protected function documentSummary(array $document)
|
|
{
|
|
$object = (object) $document;
|
|
$notFoundId = defined('PAGE_NOT_FOUND_ID') ? (int) PAGE_NOT_FOUND_ID : 0;
|
|
$status = (new DocumentAccessPolicy())->status($object, $notFoundId);
|
|
$alias = trim((string) $document['document_alias'], '/');
|
|
|
|
return array(
|
|
'id' => (int) $document['Id'],
|
|
'title' => $this->decode($document['document_title']),
|
|
'alias' => (string) $document['document_alias'],
|
|
'url' => $alias === '' ? '/' : '/' . $alias,
|
|
'rubric_id' => (int) $document['rubric_id'],
|
|
'alternate_template_id' => (int) $document['rubric_tmpl_id'],
|
|
'parent_id' => (int) $document['document_parent'],
|
|
'status' => $status,
|
|
'published' => (int) $document['document_status'] === 1,
|
|
'deleted' => (int) $document['document_deleted'] === 1,
|
|
'in_search' => (int) $document['document_in_search'] === 1,
|
|
'published_at' => (int) $document['document_published'],
|
|
'expires_at' => (int) $document['document_expire'],
|
|
'changed_at' => (int) $document['document_changed'],
|
|
'meta' => array(
|
|
'description' => IntrospectionSanitizer::value((string) $document['document_meta_description']),
|
|
'keywords' => IntrospectionSanitizer::value((string) $document['document_meta_keywords']),
|
|
'robots' => (string) $document['document_meta_robots'],
|
|
),
|
|
'editor_url' => '/documents/' . (int) $document['Id'] . '/edit',
|
|
);
|
|
}
|
|
|
|
protected function rubricSummary(array $schema)
|
|
{
|
|
$rubric = $schema['rubric'];
|
|
return array(
|
|
'id' => (int) $rubric['Id'],
|
|
'title' => $this->decode($rubric['rubric_title']),
|
|
'alias_pattern' => (string) $rubric['rubric_alias'],
|
|
'purpose' => (string) $rubric['rubric_purpose'],
|
|
'documents_enabled' => (int) $rubric['rubric_docs_active'] === 1,
|
|
'template_id' => (int) $rubric['rubric_template_id'],
|
|
'schema_version' => (string) $schema['version'],
|
|
'fields_count' => count($schema['fields']),
|
|
'groups_count' => count($schema['groups']),
|
|
'editor_url' => '/rubrics?edit=' . (int) $rubric['Id'],
|
|
);
|
|
}
|
|
|
|
protected function fields(array $document, array $schema)
|
|
{
|
|
$values = array();
|
|
foreach (DB::query(
|
|
'SELECT f.rubric_field_id,f.field_value,t.field_value field_value_more,f.field_number_value'
|
|
. ' FROM %b f LEFT JOIN %b t ON t.document_id=f.document_id'
|
|
. ' AND t.rubric_field_id=f.rubric_field_id WHERE f.document_id=%i',
|
|
ContentTables::table('document_fields'),
|
|
ContentTables::table('document_fields_text'),
|
|
(int) $document['Id']
|
|
)->getAll() ?: array() as $row) {
|
|
$raw = (string) $row['field_value'] . (string) $row['field_value_more'];
|
|
$values[(int) $row['rubric_field_id']] = array(
|
|
'raw' => $raw,
|
|
'numeric' => $row['field_number_value'] === null ? null : (float) $row['field_number_value'],
|
|
);
|
|
}
|
|
|
|
$result = array();
|
|
foreach ($schema['fields'] as $field) {
|
|
$fieldId = (int) $field['Id'];
|
|
$stored = isset($values[$fieldId]) ? $values[$fieldId] : array('raw' => '', 'numeric' => null);
|
|
$settings = isset($field['settings']) && is_array($field['settings']) ? $field['settings'] : array();
|
|
$structured = FieldValueCodec::decodeStructured($stored['raw'], null);
|
|
$value = is_array($structured) ? $structured : $stored['raw'];
|
|
$alias = (string) $field['rubric_field_alias'];
|
|
$title = $this->decode($field['rubric_field_title']);
|
|
$result[] = array(
|
|
'id' => $fieldId,
|
|
'alias' => $alias,
|
|
'title' => $title,
|
|
'type' => (string) $field['rubric_field_type'],
|
|
'group_id' => (int) $field['rubric_field_group'],
|
|
'required' => !empty($settings['required']),
|
|
'numeric' => !empty($field['rubric_field_numeric']),
|
|
'value' => IntrospectionSanitizer::fieldValue($value, $alias, $title),
|
|
'numeric_value' => $stored['numeric'],
|
|
'empty' => $this->emptyValue($value),
|
|
'settings' => IntrospectionSanitizer::value($settings),
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function templates(array $schema, array $document)
|
|
{
|
|
$rubric = $schema['rubric'];
|
|
$site = DB::query(
|
|
'SELECT Id,template_title,template_text FROM %b WHERE Id=%i LIMIT 1',
|
|
ContentTables::table('templates'),
|
|
(int) $rubric['rubric_template_id']
|
|
)->getAssoc();
|
|
$alternate = array();
|
|
if ((int) $document['rubric_tmpl_id'] > 0) {
|
|
$alternate = DB::query(
|
|
'SELECT id,title,template FROM %b WHERE id=%i AND rubric_id=%i LIMIT 1',
|
|
ContentTables::table('rubric_templates'),
|
|
(int) $document['rubric_tmpl_id'],
|
|
(int) $rubric['Id']
|
|
)->getAssoc() ?: array();
|
|
}
|
|
|
|
return array(
|
|
'site' => $site
|
|
? $this->templateSummary('site', (int) $site['Id'], $site['template_title'], $site['template_text'], '/templates?edit=' . (int) $site['Id'])
|
|
: null,
|
|
'rubric' => $this->templateSummary(
|
|
'rubric',
|
|
(int) $rubric['Id'],
|
|
$rubric['rubric_title'],
|
|
$rubric['rubric_template'],
|
|
'/rubrics?templates=' . (int) $rubric['Id']
|
|
),
|
|
'alternate' => $alternate
|
|
? $this->templateSummary(
|
|
'rubric_template',
|
|
(int) $alternate['id'],
|
|
$alternate['title'],
|
|
$alternate['template'],
|
|
'/rubrics?templates=' . (int) $rubric['Id'] . '&template=' . (int) $alternate['id']
|
|
)
|
|
: null,
|
|
'fragments' => array(
|
|
'header' => $this->contentDescriptor($rubric['rubric_header_template']),
|
|
'og' => $this->contentDescriptor($rubric['rubric_og_template']),
|
|
'footer' => $this->contentDescriptor($rubric['rubric_footer_template']),
|
|
),
|
|
);
|
|
}
|
|
|
|
protected function templateSummary($type, $id, $title, $content, $url)
|
|
{
|
|
return array(
|
|
'type' => $type,
|
|
'id' => (int) $id,
|
|
'title' => $this->decode($title),
|
|
'content' => $this->contentDescriptor($content),
|
|
'editor_url' => $url,
|
|
);
|
|
}
|
|
|
|
protected function contentDescriptor($content)
|
|
{
|
|
$content = (string) $content;
|
|
return array(
|
|
'empty' => trim($content) === '',
|
|
'length' => mb_strlen($content),
|
|
'hash' => 'sha256:' . hash('sha256', $content),
|
|
);
|
|
}
|
|
|
|
protected function requests($rubricId)
|
|
{
|
|
$result = array();
|
|
foreach (DB::query(
|
|
'SELECT Id,request_alias,request_title,request_executor_mode,request_items_per_page,'
|
|
. 'request_show_pagination,request_cache_lifetime,request_template_main,request_template_item'
|
|
. ' FROM %b WHERE rubric_id=%i ORDER BY Id',
|
|
ContentTables::table('request'),
|
|
(int) $rubricId
|
|
)->getAll() ?: array() as $row) {
|
|
$result[] = array(
|
|
'id' => (int) $row['Id'],
|
|
'alias' => (string) $row['request_alias'],
|
|
'title' => $this->decode($row['request_title']),
|
|
'executor' => (string) $row['request_executor_mode'],
|
|
'items_per_page' => (int) $row['request_items_per_page'],
|
|
'pagination' => (int) $row['request_show_pagination'] === 1,
|
|
'cache_lifetime' => (int) $row['request_cache_lifetime'],
|
|
'main_template' => $this->contentDescriptor($row['request_template_main']),
|
|
'item_template' => $this->contentDescriptor($row['request_template_item']),
|
|
'editor_url' => '/requests/' . (int) $row['Id'],
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function placements($templateId, $rubricId, $alternateTemplateId)
|
|
{
|
|
$roots = array(
|
|
'template:' . (int) $templateId => true,
|
|
'rubric:' . (int) $rubricId => true,
|
|
);
|
|
if ($alternateTemplateId > 0) {
|
|
$roots['rubric_template:' . (int) $alternateTemplateId] = true;
|
|
}
|
|
|
|
$pending = array_keys($roots);
|
|
$selected = array();
|
|
$rows = ContentPlacementMap::snapshot();
|
|
while ($pending && count($selected) < self::MAX_COMPONENTS) {
|
|
$sourceKey = array_shift($pending);
|
|
foreach ($rows as $row) {
|
|
if ($row['source_type'] . ':' . (int) $row['source_id'] !== $sourceKey) {
|
|
continue;
|
|
}
|
|
|
|
$key = $sourceKey . ':' . $row['source_part'] . ':' . $row['component_type'] . ':' . $row['component_key'];
|
|
if (isset($selected[$key])) {
|
|
continue;
|
|
}
|
|
|
|
$selected[$key] = IntrospectionSanitizer::value($row);
|
|
if (in_array($row['component_type'], array('block', 'request', 'navigation'), true)
|
|
&& (int) $row['component_id'] > 0) {
|
|
$childKey = $row['component_type'] . ':' . (int) $row['component_id'];
|
|
if (!isset($roots[$childKey])) {
|
|
$roots[$childKey] = true;
|
|
$pending[] = $childKey;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values($selected);
|
|
}
|
|
|
|
protected function presentations($rubricId, array $requests)
|
|
{
|
|
$table = ContentTables::table('presentations');
|
|
$assignments = ContentTables::table('presentation_assignments');
|
|
if (!DatabaseSchema::tableExists($table) || !DatabaseSchema::tableExists($assignments)) {
|
|
return array();
|
|
}
|
|
|
|
$requestIds = array();
|
|
foreach ($requests as $request) {
|
|
$requestIds[] = (string) $request['id'];
|
|
}
|
|
|
|
$requestIds = array_values(array_unique($requestIds));
|
|
|
|
$sql = 'SELECT a.id,a.presentation_id,a.context_code,a.target_type,a.target_key,a.mode,'
|
|
. 'p.title,p.code,p.kind,p.is_published,p.version'
|
|
. ' FROM ' . $assignments . ' a INNER JOIN ' . $table . ' p ON p.id=a.presentation_id'
|
|
. ' WHERE (a.target_type=%s AND a.target_key=%s)'
|
|
. ' OR (a.target_type=%s AND a.target_key=%s)';
|
|
$args = array($sql, 'global', '0', 'rubric', (string) (int) $rubricId);
|
|
if ($requestIds) {
|
|
$args[0] .= ' OR (a.target_type=%s AND a.target_key IN %ls)';
|
|
$args[] = 'request';
|
|
$args[] = $requestIds;
|
|
}
|
|
|
|
$args[0] .= ' ORDER BY a.context_code,a.target_type,a.target_key';
|
|
|
|
return IntrospectionSanitizer::value(call_user_func_array(array('DB', 'query'), $args)->getAll() ?: array());
|
|
}
|
|
|
|
protected function navigation($navigationId)
|
|
{
|
|
if ($navigationId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$row = DB::query(
|
|
'SELECT navigation_id,title,alias FROM %b WHERE navigation_id=%i LIMIT 1',
|
|
ContentTables::table('navigation'),
|
|
$navigationId
|
|
)->getAssoc();
|
|
|
|
return $row ? array(
|
|
'id' => (int) $row['navigation_id'],
|
|
'title' => $this->decode($row['title']),
|
|
'alias' => (string) $row['alias'],
|
|
'editor_url' => '/navigation?edit=' . (int) $row['navigation_id'],
|
|
) : array('id' => $navigationId, 'missing' => true, 'editor_url' => '/navigation');
|
|
}
|
|
|
|
protected function cache(array $document, array $schema)
|
|
{
|
|
$document['rubric_changed'] = (int) $schema['rubric']['rubric_changed'];
|
|
$descriptor = (new DocumentRenderCache())->descriptor(
|
|
(object) $document,
|
|
2,
|
|
(int) $document['rubric_id']
|
|
);
|
|
$files = array();
|
|
foreach ($descriptor ?: array() as $type => $path) {
|
|
$files[$type] = array(
|
|
'path' => $this->relativePath($path),
|
|
'exists' => is_file($path),
|
|
'size' => is_file($path) ? (int) filesize($path) : 0,
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'document_key' => CacheKey::document((int) $document['Id']),
|
|
'tags' => CacheKey::tags('content', 'document-' . (int) $document['Id'], array(
|
|
CacheKey::tag('rubric', (int) $document['rubric_id']),
|
|
'template',
|
|
'request',
|
|
'sysblock',
|
|
'navigation',
|
|
)),
|
|
'template_cache_enabled' => defined('CACHE_DOC_TPL') && CACHE_DOC_TPL,
|
|
'page_cache_enabled' => defined('CACHE_DOC_FULL') && CACHE_DOC_FULL,
|
|
'files' => $files,
|
|
);
|
|
}
|
|
|
|
protected function relativePath($path)
|
|
{
|
|
$root = rtrim(str_replace('\\', '/', BASEPATH), '/') . '/';
|
|
$path = str_replace('\\', '/', (string) $path);
|
|
return strpos($path, $root) === 0 ? substr($path, strlen($root)) : basename($path);
|
|
}
|
|
|
|
protected function emptyValue($value)
|
|
{
|
|
if (is_array($value)) {
|
|
return count(array_filter($value, function ($item) {
|
|
return $item !== '' && $item !== null && $item !== array();
|
|
})) === 0;
|
|
}
|
|
|
|
return trim((string) $value) === '';
|
|
}
|
|
|
|
protected function decode($value)
|
|
{
|
|
return htmlspecialchars_decode((string) $value, ENT_QUOTES);
|
|
}
|
|
}
|