mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
295 lines
13 KiB
PHP
295 lines
13 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file adminx/modules/Settings/ProductionDiagnostics.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\Settings;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use DB;
|
|
use App\Common\ModuleManager;
|
|
use App\Common\SystemTables;
|
|
use App\Content\BasketTables;
|
|
use App\Content\CatalogTables;
|
|
use App\Content\ContactsTables;
|
|
use App\Content\ContentTables;
|
|
use App\Content\PublicShellTables;
|
|
use App\Content\PublicUserTables;
|
|
use App\Frontend\PublicSettings;
|
|
|
|
/** Read-only production readiness checks. No mail, payment or data mutations. */
|
|
class ProductionDiagnostics
|
|
{
|
|
public static function run()
|
|
{
|
|
$checks = array();
|
|
self::checkRuntime($checks);
|
|
self::checkPaths($checks);
|
|
self::checkMail($checks);
|
|
self::checkContacts($checks);
|
|
self::checkBasket($checks);
|
|
self::checkPublicRuntime($checks);
|
|
self::checkNativeDataTables($checks);
|
|
self::checkStoredCodePrefixes($checks);
|
|
self::checkMedia($checks);
|
|
self::checkHosting($checks);
|
|
$summary = array('ok' => 0, 'warning' => 0, 'error' => 0);
|
|
foreach ($checks as $check) {
|
|
$state = isset($summary[$check['state']]) ? $check['state'] : 'warning';
|
|
$summary[$state]++;
|
|
}
|
|
|
|
return array('checks' => $checks, 'summary' => $summary, 'ready' => $summary['error'] === 0);
|
|
}
|
|
|
|
protected static function checkRuntime(array &$checks)
|
|
{
|
|
self::add($checks, 'PHP', PHP_VERSION_ID >= 70300 && PHP_VERSION_ID < 80000 ? 'ok' : 'warning', PHP_VERSION);
|
|
foreach (array('mysqli', 'mbstring', 'json', 'gd', 'xmlwriter', 'curl', 'zip') as $extension) {
|
|
self::add($checks, 'Расширение ' . $extension, extension_loaded($extension) ? 'ok' : 'error', extension_loaded($extension) ? 'доступно' : 'не установлено');
|
|
}
|
|
}
|
|
|
|
protected static function checkPaths(array &$checks)
|
|
{
|
|
foreach (array('tmp/cache' => BASEPATH . '/tmp/cache', 'tmp/backup' => BASEPATH . '/tmp/backup', 'uploads' => BASEPATH . '/uploads') as $label => $path) {
|
|
$ok = is_dir($path) && is_writable($path);
|
|
self::add($checks, 'Запись ' . $label, $ok ? 'ok' : 'error', $ok ? 'доступна' : 'каталог отсутствует или недоступен');
|
|
}
|
|
}
|
|
|
|
protected static function checkMail(array &$checks)
|
|
{
|
|
$settings = PublicSettings::all();
|
|
$type = isset($settings['mail_type']) ? strtolower(trim((string) $settings['mail_type'])) : '';
|
|
self::add($checks, 'Почтовый транспорт', in_array($type, array('mail', 'smtp', 'sendmail'), true) ? 'ok' : 'error', $type !== '' ? $type : 'не задан');
|
|
$from = isset($settings['mail_from']) ? trim((string) $settings['mail_from']) : '';
|
|
self::add($checks, 'Email отправителя', filter_var($from, FILTER_VALIDATE_EMAIL) ? 'ok' : 'error', $from !== '' ? $from : 'не задан');
|
|
if ($type === 'smtp') {
|
|
$host = isset($settings['mail_host']) ? trim((string) $settings['mail_host']) : '';
|
|
$port = isset($settings['mail_port']) ? (int) $settings['mail_port'] : 0;
|
|
self::add($checks, 'SMTP endpoint', $host !== '' && $port > 0 ? 'ok' : 'error', $host . ($port ? ':' . $port : ''));
|
|
$login = isset($settings['mail_smtp_login']) ? trim((string) $settings['mail_smtp_login']) : '';
|
|
self::add($checks, 'SMTP авторизация', $login !== '' ? 'ok' : 'warning', $login !== '' ? 'логин задан' : 'логин не задан');
|
|
}
|
|
}
|
|
|
|
protected static function checkContacts(array &$checks)
|
|
{
|
|
$module = ModuleManager::get('contacts');
|
|
if (!$module || empty($module['enabled'])) {
|
|
return;
|
|
}
|
|
|
|
$table = ContactsTables::table('module_contacts_forms');
|
|
if (!self::tableExists($table)) { self::add($checks, 'Contacts', 'warning', 'таблица форм отсутствует'); return; }
|
|
$rows = DB::query('SELECT id, title, mail_set FROM `' . $table . '`')->getAll();
|
|
$forms = 0; $recipients = 0; $invalid = 0;
|
|
foreach ($rows ?: array() as $row) {
|
|
$forms++;
|
|
$settings = self::decode((array) $row, 'mail_set');
|
|
foreach ((array) (isset($settings['receivers']) ? $settings['receivers'] : array()) as $recipient) {
|
|
$email = is_array($recipient) && isset($recipient['email']) ? trim((string) $recipient['email']) : '';
|
|
if (filter_var($email, FILTER_VALIDATE_EMAIL)) { $recipients++; } else { $invalid++; }
|
|
}
|
|
}
|
|
|
|
self::add($checks, 'Contacts', $forms > 0 && $recipients > 0 && $invalid === 0 ? 'ok' : ($invalid ? 'error' : 'warning'), $forms . ' форм, ' . $recipients . ' получателей' . ($invalid ? ', некорректных: ' . $invalid : ''));
|
|
}
|
|
|
|
protected static function checkBasket(array &$checks)
|
|
{
|
|
$module = ModuleManager::get('commerce');
|
|
if (!$module || empty($module['enabled'])) { return; }
|
|
$table = BasketTables::table('module_basket_settings');
|
|
if (!self::tableExists($table)) { self::add($checks, 'Уведомления заказов', 'error', 'настройки корзины отсутствуют'); return; }
|
|
$row = DB::query('SELECT receivers, from_email FROM `' . $table . '` ORDER BY id LIMIT 1')->getAssoc();
|
|
$from = $row ? trim((string) $row['from_email']) : '';
|
|
$valid = 0; $invalid = 0;
|
|
foreach (preg_split('/[;,\s]+/', $row ? (string) $row['receivers'] : '') ?: array() as $email) {
|
|
if ($email === '') { continue; }
|
|
if (filter_var($email, FILTER_VALIDATE_EMAIL)) { $valid++; } else { $invalid++; }
|
|
}
|
|
|
|
self::add($checks, 'Уведомления заказов', $valid > 0 && !$invalid && filter_var($from, FILTER_VALIDATE_EMAIL) ? 'ok' : 'error', $valid . ' получателей, отправитель ' . ($from ?: 'не задан'));
|
|
}
|
|
|
|
protected static function checkPublicRuntime(array &$checks)
|
|
{
|
|
$native = array();
|
|
foreach (ModuleManager::all() as $module) {
|
|
$code = isset($module['code']) ? (string) $module['code'] : '';
|
|
if ($code !== '' && !empty($module['enabled']) && is_file(BASEPATH . '/modules/' . $code . '/app/module.php')) {
|
|
$native[] = $code;
|
|
}
|
|
}
|
|
|
|
sort($native, SORT_NATURAL | SORT_FLAG_CASE);
|
|
self::add($checks, 'Native-модули', $native ? 'ok' : 'warning', $native ? implode(', ', $native) : 'нет');
|
|
self::add($checks, 'Публичный runtime', 'ok', 'native без fallback-переключателей');
|
|
}
|
|
|
|
protected static function checkNativeDataTables(array &$checks)
|
|
{
|
|
$tables = array();
|
|
foreach (array(
|
|
'documents', 'rubrics', 'rubric_fields', 'rubric_templates', 'templates',
|
|
'request', 'request_conditions', 'navigation', 'navigation_items',
|
|
'sysblocks', 'sysblocks_groups', 'view_count', 'document_relation_edges',
|
|
) as $suffix) {
|
|
$tables[] = ContentTables::table($suffix);
|
|
}
|
|
|
|
foreach (array('module_catalog_settings', 'module_catalog_items') as $suffix) {
|
|
$tables[] = CatalogTables::table($suffix);
|
|
}
|
|
|
|
$contacts = ModuleManager::get('contacts');
|
|
if ($contacts && !empty($contacts['enabled'])) {
|
|
foreach (array('module_contacts_fields', 'module_contacts_forms', 'module_contacts_history') as $suffix) {
|
|
$tables[] = ContactsTables::table($suffix);
|
|
}
|
|
}
|
|
|
|
$commerce = ModuleManager::get('commerce');
|
|
if ($commerce && !empty($commerce['enabled'])) {
|
|
foreach (array(
|
|
'module_basket', 'module_basket_delivery', 'module_basket_history',
|
|
'module_basket_payment', 'module_basket_settings', 'module_basket_status',
|
|
) as $suffix) {
|
|
$tables[] = BasketTables::table($suffix);
|
|
}
|
|
}
|
|
|
|
foreach (array('users', 'users_session', 'auth_tokens', 'auth_settings', 'user_profile_fields', 'user_profile_values', 'user_groups') as $suffix) {
|
|
$tables[] = PublicUserTables::table($suffix);
|
|
}
|
|
|
|
foreach (array('settings', 'sessions') as $suffix) {
|
|
$tables[] = PublicShellTables::table($suffix);
|
|
}
|
|
|
|
$tables[] = SystemTables::table('users');
|
|
|
|
$missing = array();
|
|
foreach (array_unique($tables) as $table) {
|
|
if (!self::tableExists($table)) {
|
|
$missing[] = $table;
|
|
}
|
|
}
|
|
|
|
self::add($checks, 'Target-таблицы native', $missing ? 'error' : 'ok', $missing ? 'отсутствуют: ' . implode(', ', $missing) : 'все критичные владельцы данных доступны');
|
|
}
|
|
|
|
protected static function checkHosting(array &$checks)
|
|
{
|
|
self::add($checks, 'Маршрутизация панели', is_file(ADMINX_PATH . '/.htaccess') ? 'ok' : 'error', is_file(ADMINX_PATH . '/.htaccess') ? '.htaccess присутствует' : '.htaccess отсутствует');
|
|
$layout = @file_get_contents(ADMINX_PATH . '/view/main.twig');
|
|
$cdn = $layout !== false && preg_match('#https?://#', $layout);
|
|
self::add($checks, 'UI-ассеты', $cdn ? 'warning' : 'ok', $cdn ? 'есть внешние CDN-зависимости' : 'локальные');
|
|
}
|
|
|
|
protected static function checkStoredCodePrefixes(array &$checks)
|
|
{
|
|
$prefix = ContentTables::prefix();
|
|
|
|
$targets = array(
|
|
'templates' => array('template_text'),
|
|
'rubrics' => array('rubric_template', 'rubric_header_template', 'rubric_footer_template', 'rubric_start_code', 'rubric_code_start', 'rubric_code_end', 'rubric_teaser_template'),
|
|
'rubric_templates' => array('template'),
|
|
'sysblocks' => array('sysblock_text'),
|
|
'blocks' => array('block_text'),
|
|
'request' => array('request_template_item', 'request_template_main'),
|
|
);
|
|
$count = 0;
|
|
try {
|
|
foreach ($targets as $suffix => $columns) {
|
|
$table = $prefix . '_' . $suffix;
|
|
if (!self::tableExists($table)) { continue; }
|
|
foreach ($columns as $column) {
|
|
$count += (int) DB::query('SELECT COUNT(*) FROM `' . $table . '` WHERE `' . $column . '` LIKE %s', '%PREFIX%')->getValue();
|
|
}
|
|
}
|
|
|
|
self::add($checks, 'Префиксы DB-шаблонов', 'ok', $count . ' вхождений нормализуются при рендере шаблонов');
|
|
} catch (\Throwable $e) {
|
|
self::add($checks, 'Префиксы DB-шаблонов', 'warning', 'аудит не выполнен: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
protected static function checkMedia(array &$checks)
|
|
{
|
|
$cache = BASEPATH . '/tmp/cache/adminx-media-integrity.json';
|
|
$result = null;
|
|
if (is_file($cache) && filemtime($cache) > time() - 3600) {
|
|
$decoded = json_decode((string) file_get_contents($cache), true);
|
|
if (is_array($decoded)) { $result = $decoded; }
|
|
}
|
|
|
|
if ($result === null) {
|
|
$paths = array();
|
|
$prefix = ContentTables::prefix();
|
|
$sources = array(
|
|
array($prefix . '_document_fields', 'field_value', 12000),
|
|
array($prefix . '_document_fields_text', 'field_value', 5000),
|
|
array($prefix . '_blocks', 'block_text', 1000),
|
|
array($prefix . '_sysblocks', 'sysblock_text', 1000),
|
|
array($prefix . '_templates', 'template_text', 100),
|
|
array($prefix . '_request', 'request_template_item', 1000),
|
|
array($prefix . '_request', 'request_template_main', 1000),
|
|
);
|
|
foreach ($sources as $source) {
|
|
if (!self::tableExists($source[0])) { continue; }
|
|
$rows = DB::query('SELECT `' . $source[1] . '` AS content FROM `' . $source[0] . '` WHERE `' . $source[1] . '` LIKE %s LIMIT ' . (int) $source[2], '%/uploads/%')->getAll();
|
|
foreach ($rows ?: array() as $row) {
|
|
if (!preg_match_all('#/?uploads/[^\s\"\'<>|\)]+#u', (string) $row['content'], $matches)) { continue; }
|
|
foreach ($matches[0] as $path) {
|
|
$path = '/' . ltrim(rawurldecode(strtok((string) $path, '?#')), '/');
|
|
$paths[$path] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$missing = 0; $recoverable = 0;
|
|
foreach (array_keys($paths) as $path) {
|
|
if (is_file(BASEPATH . $path)) { continue; }
|
|
$original = preg_replace('#/th/([^/]+)-(?:r|c|f|t|s)\d+x\d+r?(\.[A-Za-z0-9]+)$#', '/$1$2', $path);
|
|
if ($original !== $path && is_file(BASEPATH . $original)) { $recoverable++; continue; }
|
|
$missing++;
|
|
}
|
|
|
|
$result = array('references' => count($paths), 'missing' => $missing, 'recoverable' => $recoverable);
|
|
@file_put_contents($cache, json_encode($result));
|
|
}
|
|
|
|
$production = defined('ENV_CMS') && strtolower((string) ENV_CMS) === 'production';
|
|
$state = $result['missing'] > 0 && $production ? 'warning' : 'ok';
|
|
$detail = $result['references'] . ' ссылок, отсутствуют исходники: ' . $result['missing'] . ', превью создадутся: ' . $result['recoverable'];
|
|
if (!$production && $result['missing'] > 0) { $detail .= ' · локальная медиатека помечена как неполная'; }
|
|
self::add($checks, 'Медиа-ссылки', $state, $detail);
|
|
}
|
|
|
|
protected static function decode(array $row, $key)
|
|
{
|
|
$value = isset($row[$key]) ? $row[$key] : '';
|
|
if (is_array($value)) { return $value; }
|
|
$json = json_decode((string) $value, true);
|
|
if (is_array($json)) { return $json; }
|
|
$legacy = @unserialize((string) $value, array('allowed_classes' => false));
|
|
return is_array($legacy) ? $legacy : array();
|
|
}
|
|
|
|
protected static function tableExists($table) { return (bool) DB::query('SHOW TABLES LIKE %s', $table)->getValue(); }
|
|
protected static function add(array &$checks, $label, $state, $detail) { $checks[] = array('label' => $label, 'state' => $state, 'detail' => trim((string) $detail)); }
|
|
}
|