mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
453 lines
17 KiB
PHP
453 lines
17 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file system/App/Content/Requests/RequestMatchExplainer.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\Requests;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use App\Common\Session;
|
|
use App\Content\ContentTables;
|
|
use App\Frontend\PublicSettings;
|
|
use App\Frontend\RequestRenderer;
|
|
use DB;
|
|
|
|
/** Explains why a document is included in or excluded from a saved request. */
|
|
class RequestMatchExplainer
|
|
{
|
|
public function explain($requestId, $documentId, array $parameters = array())
|
|
{
|
|
$request = $this->request((int) $requestId);
|
|
$document = $this->document((int) $documentId);
|
|
if (!$request || !$document) {
|
|
throw new \InvalidArgumentException(!$request ? 'Запрос не найден' : 'Документ не найден');
|
|
}
|
|
|
|
$parameters = $this->normalizeParameters($parameters);
|
|
$previousRequest = $_REQUEST;
|
|
$previousGet = $_GET;
|
|
$_REQUEST = $parameters;
|
|
$_GET = $parameters;
|
|
|
|
try {
|
|
$matched = $this->matchesExecutor((int) $request['Id'], (int) $document['Id']);
|
|
$base = $this->baseChecks($request, $document);
|
|
$fields = $this->fieldDefinitions((int) $request['rubric_id']);
|
|
$values = $this->documentValues((int) $document['Id']);
|
|
$groups = $this->conditionTree((int) $request['Id'], $request, $document, $fields, $values);
|
|
|
|
return array(
|
|
'matched' => $matched,
|
|
'request' => array(
|
|
'id' => (int) $request['Id'],
|
|
'title' => htmlspecialchars_decode((string) $request['request_title'], ENT_QUOTES),
|
|
'alias' => (string) $request['request_alias'],
|
|
'rubric_id' => (int) $request['rubric_id'],
|
|
),
|
|
'document' => array(
|
|
'id' => (int) $document['Id'],
|
|
'title' => htmlspecialchars_decode((string) $document['document_title'], ENT_QUOTES),
|
|
'alias' => (string) $document['document_alias'],
|
|
'rubric_id' => (int) $document['rubric_id'],
|
|
),
|
|
'parameters' => $parameters,
|
|
'base' => $base,
|
|
'groups' => $groups ? array($groups) : array(),
|
|
'notes' => array(
|
|
'Итог всегда определяется текущим публичным executor.',
|
|
$parameters
|
|
? 'Условия проверены с указанными параметрами публичной страницы.'
|
|
: 'Параметры публичной страницы не переданы.',
|
|
),
|
|
);
|
|
} finally {
|
|
$_REQUEST = $previousRequest;
|
|
$_GET = $previousGet;
|
|
}
|
|
}
|
|
|
|
protected function normalizeParameters(array $parameters)
|
|
{
|
|
$result = array();
|
|
foreach ($parameters as $key => $value) {
|
|
$key = preg_replace('/[^a-z0-9_.-]+/i', '', (string) $key);
|
|
if ($key === '' || $this->sensitiveKey($key)) {
|
|
continue;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
$items = array();
|
|
foreach (array_slice($value, 0, 50) as $item) {
|
|
if (is_scalar($item) || $item === null) {
|
|
$items[] = mb_substr((string) $item, 0, 500);
|
|
}
|
|
}
|
|
|
|
$result[$key] = $items;
|
|
} elseif (is_scalar($value) || $value === null) {
|
|
$result[$key] = mb_substr((string) $value, 0, 500);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function sensitiveKey($key)
|
|
{
|
|
return (bool) preg_match(
|
|
'/(?:pass|secret|token|cookie|session|authorization|credential|api[_-]?key|парол|секрет|токен|ключ[_ -]?api)/iu',
|
|
(string) $key
|
|
);
|
|
}
|
|
|
|
protected function matchesExecutor($requestId, $documentId)
|
|
{
|
|
$result = (new RequestRenderer())->render($requestId, array(
|
|
'RETURN_DATA' => 1,
|
|
'LIMIT' => 1,
|
|
'START' => 0,
|
|
'USER_WHERE' => 'a.Id = ' . (int) $documentId,
|
|
));
|
|
|
|
return is_array($result) && !empty($result['rows']);
|
|
}
|
|
|
|
protected function baseChecks(array $request, array $document)
|
|
{
|
|
$now = time();
|
|
$notFoundId = defined('PAGE_NOT_FOUND_ID') ? (int) PAGE_NOT_FOUND_ID : 0;
|
|
$checks = array(
|
|
$this->check('rubric', 'Рубрика запроса', (int) $document['rubric_id'] === (int) $request['rubric_id'],
|
|
'Документ #' . (int) $document['rubric_id'] . ', запрос #' . (int) $request['rubric_id']),
|
|
$this->check('deleted', 'Не удалён', empty($document['document_deleted']),
|
|
empty($document['document_deleted']) ? 'Документ доступен' : 'Документ помечен удалённым'),
|
|
$this->check('status', 'Опубликован', (int) $document['document_status'] === 1,
|
|
(int) $document['document_status'] === 1 ? 'Состояние: опубликован' : 'Состояние: снят с публикации'),
|
|
$this->check('system', 'Не системная страница', (int) $document['Id'] !== 1 && ($notFoundId <= 0 || (int) $document['Id'] !== $notFoundId),
|
|
'Главная и страница 404 исключаются из запросов'),
|
|
);
|
|
|
|
if (PublicSettings::get('use_doctime')) {
|
|
$published = (int) $document['document_published'];
|
|
$expires = (int) $document['document_expire'];
|
|
$checks[] = $this->check('published_at', 'Срок публикации', $published <= $now && ($expires === 0 || $expires >= $now),
|
|
'Начало: ' . ($published > 0 ? date('d.m.Y H:i', $published) : 'без ограничения')
|
|
. '; окончание: ' . ($expires > 0 ? date('d.m.Y H:i', $expires) : 'без ограничения'));
|
|
}
|
|
|
|
if (!empty($request['request_only_owner'])) {
|
|
$ownerId = (int) Session::get('user_id');
|
|
$checks[] = $this->check('owner', 'Только документы текущего автора', (int) $document['document_author_id'] === $ownerId,
|
|
'Автор документа: #' . (int) $document['document_author_id'] . '; текущий пользователь: #' . $ownerId);
|
|
}
|
|
|
|
return $checks;
|
|
}
|
|
|
|
protected function conditionTree($requestId, array $request, array $document, array $fields, array $values)
|
|
{
|
|
$groups = DB::query(
|
|
'SELECT * FROM %b WHERE request_id=%i ORDER BY parent_id,group_position,Id',
|
|
ContentTables::table('request_condition_groups'), $requestId
|
|
)->getAll() ?: array();
|
|
$conditions = DB::query(
|
|
"SELECT * FROM %b WHERE request_id=%i AND condition_status='1' ORDER BY condition_position,Id",
|
|
ContentTables::table('request_conditions'), $requestId
|
|
)->getAll() ?: array();
|
|
|
|
if (!$groups) { return array(); }
|
|
|
|
$nodes = array();
|
|
foreach ($groups as $group) {
|
|
$group = (array) $group;
|
|
$nodes[(int) $group['Id']] = array(
|
|
'id' => (int) $group['Id'],
|
|
'parent_id' => (int) $group['parent_id'],
|
|
'title' => trim((string) $group['group_title']) !== '' ? (string) $group['group_title'] : 'Группа #' . (int) $group['Id'],
|
|
'operator' => (string) $group['group_operator'] === 'OR' ? 'OR' : 'AND',
|
|
'conditions' => array(),
|
|
'children' => array(),
|
|
'state' => 'empty',
|
|
);
|
|
}
|
|
|
|
foreach ($conditions as $condition) {
|
|
$condition = (array) $condition;
|
|
$groupId = isset($nodes[(int) $condition['condition_group_id']])
|
|
? (int) $condition['condition_group_id']
|
|
: (int) array_key_first($nodes);
|
|
$nodes[$groupId]['conditions'][] = $this->condition($condition, $request, $document, $fields, $values);
|
|
}
|
|
|
|
$rootId = 0;
|
|
foreach ($nodes as $id => $node) {
|
|
if ($node['parent_id'] === 0 && $rootId === 0) { $rootId = $id; }
|
|
}
|
|
|
|
return $rootId > 0 ? $this->buildGroup($rootId, $nodes) : array();
|
|
}
|
|
|
|
protected function buildGroup($groupId, array $nodes)
|
|
{
|
|
$group = $nodes[$groupId];
|
|
foreach ($nodes as $childId => $node) {
|
|
if ((int) $node['parent_id'] === (int) $groupId) {
|
|
$group['children'][] = $this->buildGroup($childId, $nodes);
|
|
}
|
|
}
|
|
|
|
$states = array();
|
|
foreach ($group['conditions'] as $condition) {
|
|
if ($condition['state'] !== 'skipped') { $states[] = $condition['state']; }
|
|
}
|
|
|
|
foreach ($group['children'] as $child) {
|
|
if ($child['state'] !== 'empty') { $states[] = $child['state']; }
|
|
}
|
|
|
|
$group['state'] = $this->groupState($group['operator'], $states);
|
|
|
|
return $group;
|
|
}
|
|
|
|
protected function groupState($operator, array $states)
|
|
{
|
|
if (!$states) { return 'empty'; }
|
|
if ($operator === 'OR') {
|
|
if (in_array('passed', $states, true)) { return 'passed'; }
|
|
return in_array('unknown', $states, true) ? 'unknown' : 'failed';
|
|
}
|
|
|
|
if (in_array('failed', $states, true)) { return 'failed'; }
|
|
return in_array('unknown', $states, true) ? 'unknown' : 'passed';
|
|
}
|
|
|
|
protected function condition(array $condition, array $request, array $document, array $fields, array $values)
|
|
{
|
|
$fieldId = (int) $condition['condition_field_id'];
|
|
$field = isset($fields[$fieldId]) ? $fields[$fieldId] : array();
|
|
$value = isset($values[$fieldId]) ? $values[$fieldId] : null;
|
|
$actual = $value === null
|
|
? null
|
|
: (!empty($field['numeric']) ? $value['number'] : $value['value']);
|
|
$expected = '';
|
|
$error = '';
|
|
$template = trim((string) $condition['condition_value']);
|
|
if ($template === '') {
|
|
return array(
|
|
'id' => (int) $condition['Id'],
|
|
'field_id' => $fieldId,
|
|
'field' => isset($field['title']) ? $field['title'] : 'Поле #' . $fieldId,
|
|
'alias' => isset($field['alias']) ? $field['alias'] : '',
|
|
'operator' => (string) $condition['condition_compare'],
|
|
'actual' => $actual,
|
|
'expected' => '',
|
|
'state' => 'skipped',
|
|
'note' => 'Пустое условие не включается в SQL выборки.',
|
|
);
|
|
}
|
|
|
|
try {
|
|
$descriptor = RequestConditionValue::descriptor($condition);
|
|
if ($descriptor !== null) {
|
|
$resolved = RequestConditionValue::resolve($descriptor, $_REQUEST);
|
|
if ($resolved === null) {
|
|
return array(
|
|
'id' => (int) $condition['Id'],
|
|
'field_id' => $fieldId,
|
|
'field' => isset($field['title']) ? $field['title'] : 'Поле #' . $fieldId,
|
|
'alias' => isset($field['alias']) ? $field['alias'] : '',
|
|
'operator' => (string) $condition['condition_compare'],
|
|
'actual' => $actual,
|
|
'expected' => RequestConditionValue::tag($descriptor['key']),
|
|
'state' => 'skipped',
|
|
'note' => 'Публичный параметр не передан, условие не участвует в выборке.',
|
|
);
|
|
}
|
|
|
|
$expected = $this->resolvedLabel($resolved);
|
|
$passed = $this->compareResolved((string) $condition['condition_compare'], $actual, $resolved, $value !== null);
|
|
} else {
|
|
$expected = $this->runtimeValue($template);
|
|
$passed = $this->compare((string) $condition['condition_compare'], $actual, $expected, $value !== null);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$passed = null;
|
|
$error = 'Не удалось разобрать значение: ' . $e->getMessage();
|
|
}
|
|
|
|
return array(
|
|
'id' => (int) $condition['Id'],
|
|
'field_id' => $fieldId,
|
|
'field' => isset($field['title']) ? $field['title'] : 'Поле #' . $fieldId,
|
|
'alias' => isset($field['alias']) ? $field['alias'] : '',
|
|
'operator' => (string) $condition['condition_compare'],
|
|
'actual' => $actual,
|
|
'expected' => $expected,
|
|
'state' => $passed === null ? 'unknown' : ($passed ? 'passed' : 'failed'),
|
|
'note' => $error !== '' ? $error : ($passed === null ? 'Условие проверено итоговым executor, но не разбирается локально.' : ''),
|
|
);
|
|
}
|
|
|
|
protected function compareResolved($operator, $actual, array $resolved, $exists)
|
|
{
|
|
if (!$exists || $actual === null) { return false; }
|
|
if ($resolved['kind'] === 'range') {
|
|
$number = (float) $actual;
|
|
return ($resolved['min'] === null || $number >= (float) $resolved['min'])
|
|
&& ($resolved['max'] === null || $number <= (float) $resolved['max']);
|
|
}
|
|
|
|
if ($resolved['kind'] === 'list') {
|
|
foreach ($resolved['values'] as $item) {
|
|
$expected = isset($resolved['transform']) && $resolved['transform'] === 'pipe_member'
|
|
? '|' . $item . '|'
|
|
: $item;
|
|
if ($this->compare(
|
|
isset($resolved['transform']) && $resolved['transform'] === 'pipe_member' ? '%%' : '==',
|
|
$actual,
|
|
$expected,
|
|
true
|
|
)) { return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return $this->compare($operator, $actual, isset($resolved['value']) ? $resolved['value'] : '', true);
|
|
}
|
|
|
|
protected function resolvedLabel(array $resolved)
|
|
{
|
|
if ($resolved['kind'] === 'range') {
|
|
return (string) $resolved['min'] . ' … ' . (string) $resolved['max'];
|
|
}
|
|
|
|
if ($resolved['kind'] === 'list') { return implode(', ', $resolved['values']); }
|
|
return isset($resolved['value']) ? (string) $resolved['value'] : '';
|
|
}
|
|
|
|
protected function runtimeValue($template)
|
|
{
|
|
if (strpos($template, '<?') !== false || strpos($template, '?>') !== false) {
|
|
throw new \RuntimeException('PHP-выражение не исполняется повторно в инспекторе');
|
|
}
|
|
|
|
return trim((string) $template);
|
|
}
|
|
|
|
protected function compare($operator, $actual, $expected, $exists)
|
|
{
|
|
if (!$exists || $actual === null) { return false; }
|
|
if ($operator === 'FRE' || $operator === 'ANY') { return null; }
|
|
if (in_array($operator, array('N>', '>', 'N<', '<', 'N>=', '>=', 'N<=', '<='), true)) {
|
|
$left = (float) $actual;
|
|
$right = (float) str_replace(',', '.', (string) $expected);
|
|
if (in_array($operator, array('N>', '>'), true)) { return $left > $right; }
|
|
if (in_array($operator, array('N<', '<'), true)) { return $left < $right; }
|
|
if (in_array($operator, array('N>=', '>='), true)) { return $left >= $right; }
|
|
return $left <= $right;
|
|
}
|
|
|
|
if (in_array($operator, array('SEGMENT', 'INTERVAL'), true)) {
|
|
$range = array_map('trim', explode(',', (string) $expected, 2));
|
|
if (count($range) !== 2 || $range[0] === '' || $range[1] === '') { return null; }
|
|
$number = (float) $actual;
|
|
return $operator === 'SEGMENT'
|
|
? $number >= (float) $range[0] && $number <= (float) $range[1]
|
|
: $number > (float) $range[0] && $number < (float) $range[1];
|
|
}
|
|
|
|
$left = $this->lower((string) $actual);
|
|
$right = $this->lower((string) $expected);
|
|
if ($operator === '!=') { return $left !== $right; }
|
|
if ($operator === '%%') { return strpos($left, $right) !== false; }
|
|
if ($operator === '%') { return strpos($left, $right) === 0; }
|
|
if ($operator === '--') { return strpos($left, $right) === false; }
|
|
if ($operator === '!-') { return strpos($left, $right) !== 0; }
|
|
if (in_array($operator, array('IN=', 'NOTIN='), true)) {
|
|
$list = $this->listValues($expected);
|
|
$contains = in_array($left, $list, true);
|
|
return $operator === 'NOTIN=' ? !$contains : $contains;
|
|
}
|
|
|
|
return $left === $right;
|
|
}
|
|
|
|
protected function listValues($value)
|
|
{
|
|
$result = array();
|
|
foreach (str_getcsv((string) $value, ',', "'") as $item) {
|
|
$item = trim((string) $item, " \t\n\r\0\x0B'\"");
|
|
if ($item !== '') { $result[] = $this->lower($item); }
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function lower($value)
|
|
{
|
|
return function_exists('mb_strtolower') ? mb_strtolower((string) $value, 'UTF-8') : strtolower((string) $value);
|
|
}
|
|
|
|
protected function check($code, $label, $passed, $detail)
|
|
{
|
|
return array('code' => $code, 'label' => $label, 'state' => $passed ? 'passed' : 'failed', 'detail' => $detail);
|
|
}
|
|
|
|
protected function request($id)
|
|
{
|
|
return DB::query('SELECT * FROM %b WHERE Id=%i LIMIT 1', ContentTables::table('request'), $id)->getAssoc();
|
|
}
|
|
|
|
protected function document($id)
|
|
{
|
|
return DB::query('SELECT * FROM %b WHERE Id=%i LIMIT 1', ContentTables::table('documents'), $id)->getAssoc();
|
|
}
|
|
|
|
protected function fieldDefinitions($rubricId)
|
|
{
|
|
$result = array();
|
|
foreach (DB::query(
|
|
'SELECT Id,rubric_field_title,rubric_field_alias,rubric_field_numeric FROM %b WHERE rubric_id=%i',
|
|
ContentTables::table('rubric_fields'), $rubricId
|
|
)->getAll() ?: array() as $field) {
|
|
$field = (array) $field;
|
|
$result[(int) $field['Id']] = array(
|
|
'title' => (string) $field['rubric_field_title'],
|
|
'alias' => (string) $field['rubric_field_alias'],
|
|
'numeric' => !empty($field['rubric_field_numeric']),
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function documentValues($documentId)
|
|
{
|
|
$result = array();
|
|
foreach (DB::query(
|
|
'SELECT rubric_field_id,field_value,field_number_value FROM %b WHERE document_id=%i',
|
|
ContentTables::table('document_fields'), $documentId
|
|
)->getAll() ?: array() as $row) {
|
|
$row = (array) $row;
|
|
$result[(int) $row['rubric_field_id']] = array(
|
|
'value' => (string) $row['field_value'],
|
|
'number' => $row['field_number_value'] === null ? null : (float) $row['field_number_value'],
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|