| @copyright 2007-2026 (c) AVE.cms | @link https://ave-cms.ru | @version 3.3 */ defined('AVE_SETUP') || die('Direct access to this location is not allowed.'); class AveInstaller { const ADMIN_PASSWORD_MIN_LENGTH = 8; protected $root; protected $locale; public function __construct($root, $locale = 'ru') { $this->root = rtrim(str_replace('\\', '/', (string) $root), '/'); $this->locale = in_array((string) $locale, array('ru', 'en'), true) ? (string) $locale : 'ru'; defined('BASEPATH') || define('BASEPATH', $this->root); } public function officialRepositoryProfile() { try { $this->loadDistributionProfile(); $profile = \App\Common\DistributionProfile::official(); return array('available' => true, 'error' => '', 'profile' => $profile); } catch (Throwable $e) { return array('available' => false, 'error' => $e->getMessage(), 'profile' => null); } } public function validateRepositorySelection($mode, $customProfile = '') { $mode = strtolower(trim((string) $mode)); if ($mode === 'disabled') { return array('mode' => 'disabled', 'profile' => null); } $this->loadDistributionProfile(); if ($mode === 'official') { return array('mode' => 'official', 'profile' => \App\Common\DistributionProfile::official()); } if ($mode === 'custom') { if (trim((string) $customProfile) === '') { throw new InvalidArgumentException('Вставьте профиль подключения, полученный от издателя.'); } return array('mode' => 'custom', 'profile' => \App\Common\DistributionProfile::decode($customProfile)); } throw new InvalidArgumentException('Выберите способ подключения обновлений и модулей.'); } public function requirements() { $checks = array( $this->check('PHP 7.3 или новее', PHP_VERSION_ID >= 70300, PHP_VERSION), $this->check('Расширение mysqli', extension_loaded('mysqli')), $this->check('Расширение mbstring', extension_loaded('mbstring')), $this->check('Расширение JSON', extension_loaded('json')), $this->check('Расширение OpenSSL', extension_loaded('openssl')), $this->check('Каталог configs доступен для записи', $this->writablePath($this->root . '/configs')), $this->check('Каталог storage доступен для записи', $this->writablePath($this->root . '/storage')), $this->check('Каталог tmp доступен для записи', $this->writablePath($this->root . '/tmp')), $this->check('Каталог uploads доступен для записи', $this->writablePath($this->root . '/uploads')), $this->check('Core-схема найдена', is_file(__DIR__ . '/schema.sql')), ); $checks[] = $this->check('GD и WebP', $this->gdWebpAvailable(), 'Необязательно для установки', false); return $checks; } public function requirementsPassed(array $checks) { foreach ($checks as $check) { if (!empty($check['required']) && empty($check['passed'])) { return false; } } return true; } public function existingDatabaseConfig() { $file = $this->root . '/configs/db.config.php'; if (!is_file($file) || filesize($file) === 0) { return null; } $config = null; include $file; return is_array($config) ? $config : null; } public function installationState() { if (is_file($this->lockFile())) { return array('installed' => true, 'reason' => 'Установка уже завершена.'); } $config = $this->existingDatabaseConfig(); if (!$config) { return array('installed' => false, 'reason' => ''); } try { $db = $this->connect($config); $prefix = $this->validatePrefix(isset($config['dbpref']) ? $config['dbpref'] : ''); if ($this->tableExists($db, $prefix . '_users') && $this->tableCount($db, $prefix . '_users') > 0) { return array('installed' => true, 'reason' => 'В настроенной базе уже есть системный пользователь.'); } } catch (Exception $e) { return array('installed' => false, 'reason' => ''); } return array('installed' => false, 'reason' => ''); } public function install(array $database, array $site, array $options = array()) { $writeFiles = !isset($options['write_files']) || (bool) $options['write_files']; $progress = isset($options['progress']) && is_callable($options['progress']) ? $options['progress'] : null; $resetPrefix = !empty($options['reset_prefix']); $resetConfirmation = isset($options['reset_prefix_confirmation']) ? trim((string) $options['reset_prefix_confirmation']) : ''; $repositories = isset($options['repositories']) && is_array($options['repositories']) ? $options['repositories'] : array('mode' => 'disabled', 'profile' => null); $this->reportProgress($progress, 8, 'Проверяем данные', 'Параметры базы данных и администратора'); $database = $this->validateDatabase($database); $site = $this->validateSite($site); if ($resetPrefix && !hash_equals($database['dbpref'], $resetConfirmation)) { throw new InvalidArgumentException('Для очистки таблиц введите точный префикс: ' . $database['dbpref']); } $this->reportProgress($progress, 18, 'Подключаемся к базе данных', $database['dbhost'] . ' / ' . $database['dbname']); $db = $this->connect($database); $prefix = $database['dbpref']; $initialTables = $this->prefixedTables($db, $prefix); $hadDatabaseConfig = is_file($this->root . '/configs/db.config.php'); $adminChange = null; if ($resetPrefix) { $this->reportProgress($progress, 24, 'Очищаем таблицы префикса', 'Удаляем объекты ' . $prefix . '_* без возможности восстановления'); $removedObjects = $this->dropPrefixedObjects($db, $prefix); $initialTables = array(); $this->reportProgress($progress, 30, 'Таблицы префикса удалены', 'Удалено объектов: ' . $removedObjects); } $this->assertPristine($db, $prefix); if (!$resetPrefix) { $this->reportProgress($progress, 30, 'Подключение проверено', 'Префикс ' . $prefix . ' свободен'); } try { $this->reportProgress($progress, 38, 'Создаём структуру базы данных', 'Таблицы ядра и необходимые индексы'); $this->applySchema($db, $prefix); $this->reportProgress($progress, 64, 'Структура базы данных готова', 'Начинаем заполнение системных данных'); $db->begin_transaction(); $this->reportProgress($progress, 70, 'Создаём начальные данные', 'Администратор, настройки, рубрика и документы'); $summary = $this->seed($db, $prefix, $site, $repositories); $db->commit(); $this->reportProgress($progress, 84, 'Начальные данные созданы', 'Стартовая страница и страница 404 готовы'); if ($writeFiles) { $this->reportProgress($progress, 90, 'Записываем конфигурацию', 'Подключение к базе и адрес панели управления'); $this->writeDatabaseConfig($database); $adminChange = $this->applyAdminDirectory($site['admin_directory']); $this->reportProgress($progress, 96, 'Очищаем кеш', 'Подготавливаем первый запуск сайта'); $this->clearRuntimeCache(); $this->writeLock($prefix, $site['admin_email'], $site['admin_directory']); } $summary['tables'] = count($this->prefixedTables($db, $prefix)); $summary['modules'] = $this->tableCount($db, $prefix . '_modules'); $summary['admin_directory'] = $site['admin_directory']; $this->reportProgress($progress, 100, 'Установка завершена', 'AVE.cms готова к работе'); return $summary; } catch (Throwable $e) { @$db->rollback(); if ($adminChange) { $this->rollbackAdminDirectory($adminChange); } if (!$initialTables) { $this->dropPrefixedTables($db, $prefix); if ($writeFiles && !$hadDatabaseConfig) { @unlink($this->root . '/configs/db.config.php'); @unlink($this->lockFile()); } } throw $e; } } protected function reportProgress($callback, $percent, $title, $detail = '') { if (!$callback) { return; } call_user_func($callback, array( 'percent' => max(0, min(100, (int) $percent)), 'title' => (string) $title, 'detail' => (string) $detail, )); } public function validateDatabase(array $database) { $out = array( 'dbhost' => trim(isset($database['dbhost']) ? (string) $database['dbhost'] : ''), 'dbport' => max(0, (int) (isset($database['dbport']) ? $database['dbport'] : 3306)), 'dbname' => trim(isset($database['dbname']) ? (string) $database['dbname'] : ''), 'dbuser' => trim(isset($database['dbuser']) ? (string) $database['dbuser'] : ''), 'dbpass' => isset($database['dbpass']) ? (string) $database['dbpass'] : '', 'dbpref' => $this->validatePrefix(isset($database['dbpref']) ? $database['dbpref'] : ''), 'dbchar' => 'utf8mb4', 'dbsock' => null, ); if ($out['dbhost'] === '' || $out['dbname'] === '' || $out['dbuser'] === '') { throw new InvalidArgumentException('Заполните адрес, имя базы и пользователя БД.'); } if (!preg_match('/^[A-Za-z0-9_$-]{1,64}$/', $out['dbname'])) { throw new InvalidArgumentException('Некорректное имя базы данных.'); } return $out; } public function validateSite(array $site) { $out = array( 'site_name' => trim(isset($site['site_name']) ? (string) $site['site_name'] : ''), 'admin_name' => trim(isset($site['admin_name']) ? (string) $site['admin_name'] : ''), 'admin_login' => trim(isset($site['admin_login']) ? (string) $site['admin_login'] : ''), 'admin_email' => trim(isset($site['admin_email']) ? (string) $site['admin_email'] : ''), 'admin_password' => isset($site['admin_password']) ? (string) $site['admin_password'] : '', 'admin_directory' => $this->validateAdminDirectory(isset($site['admin_directory']) ? $site['admin_directory'] : 'adminx'), 'admin_language' => isset($site['admin_language']) && in_array($site['admin_language'], array('ru', 'en'), true) ? (string) $site['admin_language'] : $this->locale, ); if ($out['site_name'] === '' || $out['admin_name'] === '' || $out['admin_login'] === '') { throw new InvalidArgumentException('Укажите название сайта, имя и логин администратора.'); } if (!filter_var($out['admin_email'], FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException('Укажите корректный email администратора.'); } if (!preg_match('/^[A-Za-z0-9_.@-]{3,150}$/', $out['admin_login'])) { throw new InvalidArgumentException('Логин содержит недопустимые символы.'); } if (mb_strlen($out['admin_password'], 'UTF-8') < self::ADMIN_PASSWORD_MIN_LENGTH) { throw new InvalidArgumentException( 'Пароль администратора должен содержать не менее ' . self::ADMIN_PASSWORD_MIN_LENGTH . ' символов.' ); } return $out; } public function configuredAdminDirectory() { $file = $this->root . '/configs/public.config.php'; $config = is_file($file) ? include $file : array(); $directory = is_array($config) && isset($config['admin_directory']) ? $config['admin_directory'] : 'adminx'; try { return $this->validateAdminDirectory($directory, false); } catch (Throwable $e) { return 'adminx'; } } protected function validateAdminDirectory($directory, $checkTarget = true) { $directory = trim((string) $directory); if (!preg_match('/^[A-Za-z][A-Za-z0-9_-]{0,47}$/', $directory)) { throw new InvalidArgumentException('Имя папки панели: латинская буква в начале, далее буквы, цифры, - или _.'); } $reserved = array('api', 'configs', 'fields', 'install', 'modules', 'setup', 'storage', 'system', 'templates', 'tmp', 'uploads'); if (in_array(strtolower($directory), $reserved, true)) { throw new InvalidArgumentException('Это имя зарезервировано системным каталогом: ' . $directory); } $source = $this->root . '/adminx'; $target = $this->root . '/' . $directory; if ($checkTarget && !is_dir($source)) { throw new RuntimeException('В сборке отсутствует исходная папка панели управления.'); } if ($checkTarget && $directory !== 'adminx' && file_exists($target)) { throw new InvalidArgumentException('Каталог /' . $directory . ' уже существует. Выберите другое имя панели.'); } if ($checkTarget && $directory !== 'adminx' && !is_writable($this->root)) { throw new RuntimeException('Сервер не может переименовать исходную папку панели. Проверьте права на корень сайта.'); } return $directory; } protected function seed(mysqli $db, $prefix, array $site, array $repositories) { $now = time(); $date = date('Y-m-d H:i:s', $now); $password = password_hash($site['admin_password'], PASSWORD_BCRYPT, array('cost' => 12)); $roles = array( 1 => array('admin', 'Администратор'), 2 => array('guest', 'Гостевая'), 3 => array('moderator', 'Модератор'), 4 => array('user', 'Пользователи'), ); foreach ($roles as $id => $role) { $this->execute($db, 'INSERT INTO `' . $prefix . '_roles` (id,code,name,is_system,created_at,updated_at) VALUES (?,?,?,?,?,?)', 'ississ', array($id, $role[0], $role[1], 1, $date, $date)); } $this->execute($db, 'INSERT INTO `' . $prefix . '_users` (id,name,email,password_hash,role,is_active,created_at,updated_at,login,first_name,last_name,legacy_id) VALUES (1,?,?,?,?,?,?,?,?,?,?,1)', 'ssssisssss', array($site['admin_name'], $site['admin_email'], $password, 'admin', 1, $date, $date, $site['admin_login'], $site['admin_name'], '')); $permissions = array( array('admin_panel', 'core', 'Доступ в панель управления', 'Разрешает вход в панель управления.', 1), array('all_permissions', 'core', 'Полный доступ', 'Даёт администратору доступ ко всем возможностям.', 2), array('view_public_debug', 'core', 'Публичная панель отладки', 'Разрешает просмотр служебной панели публичного сайта.', 3), array('view_development_site', 'core', 'Просмотр сайта в режиме разработки', 'Разрешает видеть публичный сайт, закрытый для посетителей и поисковых систем.', 4), ); foreach ($permissions as $permission) { $this->execute($db, 'INSERT INTO `' . $prefix . '_permissions` (code,module,group_code,name,description,sort_order) VALUES (?,\'admin\',?,?,?,?)', 'ssssi', array($permission[0], $permission[1], $permission[2], $permission[3], $permission[4])); } $this->execute($db, 'INSERT INTO `' . $prefix . '_role_permissions` (role_id,permission_code) VALUES (1,\'all_permissions\')'); $this->seedSettings($db, $prefix, $site['site_name'], $site['admin_language'], $repositories); $this->seedConstants($db, $prefix, $date); $this->seedCoreMigrationLedger($db, $prefix, $date); $this->seedPublicIdentity($db, $prefix, $site['site_name'], $site['admin_name'], $site['admin_login'], $site['admin_email'], $password, $date); $this->seedContent($db, $prefix, $site['site_name'], $now); return array( 'admin_id' => 1, 'template_id' => 1, 'rubric_id' => 1, 'field_id' => 1, 'document_id' => 1, 'not_found_document_id' => 2, 'repository_mode' => isset($repositories['mode']) ? (string) $repositories['mode'] : 'disabled', 'repository_name' => isset($repositories['profile']['name']) ? (string) $repositories['profile']['name'] : '', ); } protected function seedSettings(mysqli $db, $prefix, $siteName, $adminLanguage, array $repositories) { $profile = isset($repositories['profile']) && is_array($repositories['profile']) ? $repositories['profile'] : null; $enabled = $profile !== null; $settings = array( 'site_name' => array($siteName, 'string'), 'admin_default_language' => array($adminLanguage, 'string'), 'site_access_mode' => array('public', 'string'), 'site_development_title' => array('Сайт готовится к запуску', 'string'), 'site_development_message' => array('Мы завершаем настройку сайта. Пожалуйста, зайдите немного позже.', 'string'), 'page_not_found_id' => array('2', 'int'), 'date_format' => array('%d.%m.%Y', 'string'), 'time_format' => array('%d.%m.%Y, %H:%M', 'string'), 'default_country' => array('RU', 'string'), 'mail_type' => array('mail', 'string'), 'mail_content_type' => array('text/plain', 'string'), 'mail_from' => array('', 'string'), 'mail_from_name' => array($siteName, 'string'), 'mail_new_user' => array("Здравствуйте, %NAME%!\n\nВаш аккаунт на сайте %HOST% создан. Теперь вы можете войти, используя данные регистрации.", 'string'), 'mail_signature' => array('С уважением,' . "\n" . $siteName, 'string'), 'navi_box' => array('', 'string'), 'start_label' => array('Первая', 'string'), 'end_label' => array('Последняя', 'string'), 'separator_label' => array('…', 'string'), 'next_label' => array('»', 'string'), 'prev_label' => array('«', 'string'), 'total_label' => array('Страница %d из %d', 'string'), 'link_box' => array('
  • %s
  • ', 'string'), 'total_box' => array('%s', 'string'), 'active_box' => array('
  • %s
  • ', 'string'), 'separator_box' => array('
  • %s
  • ', 'string'), 'bread_box' => array('', 'string'), 'bread_separator' => array('', 'string'), 'bread_link_box' => array('
  • %s
  • ', 'string'), 'bread_link_template' => array('[name]', 'string'), 'bread_self_box' => array('
  • %s
  • ', 'string'), 'distribution.repository_mode' => array(isset($repositories['mode']) ? (string) $repositories['mode'] : 'disabled', 'string'), 'distribution.repository_name' => array($enabled && isset($profile['name']) ? (string) $profile['name'] : '', 'string'), 'core_updates.repository_enabled' => array($enabled ? '1' : '0', 'bool'), 'core_updates.repository_url' => array($enabled ? (string) $profile['core_updates']['url'] : '', 'string'), 'core_updates.repository_public_key' => array($enabled ? (string) $profile['core_updates']['public_key'] : '', 'code'), 'modules.repository_enabled' => array($enabled ? '1' : '0', 'bool'), 'modules.repository_url' => array($enabled ? (string) $profile['modules']['url'] : '', 'string'), 'modules.repository_public_key' => array($enabled ? (string) $profile['modules']['public_key'] : '', 'code'), ); foreach ($settings as $key => $definition) { $this->execute($db, 'INSERT INTO `' . $prefix . '_settings` (param,value,type) VALUES (?,?,?)', 'sss', array($key, $definition[0], $definition[1])); } $this->execute($db, 'INSERT INTO `' . $prefix . '_paginations` (id,pagination_name,pagination_box,pagination_start_label,pagination_end_label,pagination_separator_box,pagination_separator_label,pagination_next_label,pagination_prev_label,pagination_link_box,pagination_active_link_box,pagination_link_template,pagination_link_active_template) VALUES (1,?,?,?,?,?,?,?,?,?,?,?,?)', 'ssssssssssss', array('По умолчанию', '', 'Первая', 'Последняя', '
  • %s
  • ', '…', '»', '«', '
  • %s
  • ', '
  • %s
  • ', '[name]', '[name]')); } protected function loadDistributionProfile() { if (!class_exists('App\\Common\\DistributionProfile', false)) { require_once $this->root . '/system/App/Common/DistributionProfile.php'; } } protected function seedConstants(mysqli $db, $prefix, $date) { if (!class_exists('App\\Common\\RuntimeConstantSchema', false)) { require_once $this->root . '/system/App/Common/RuntimeConstantSchema.php'; } $position = 10; foreach (\App\Common\RuntimeConstantSchema::all(false) as $name => $definition) { $value = $definition['default']; if (is_bool($value)) { $value = $value ? '1' : '0'; $type = 'bool'; } elseif (is_int($value)) { $value = (string) $value; $type = 'int'; } else { $value = (string) $value; $type = isset($definition['type']) ? (string) $definition['type'] : 'string'; } $options = !empty($definition['options']) ? json_encode(array_values($definition['options']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''; $this->execute($db, 'INSERT INTO `' . $prefix . '_constants` (name,value,type,group_code,label,description,options,is_system,sort_order,updated_at) VALUES (?,?,?,?,?,?,?,1,?,?)', 'sssssssis', array($name, $value, $type, $definition['group_code'], $name, $definition['description'], $options, $position, $date)); $position += 10; } } protected function seedCoreMigrationLedger(mysqli $db, $prefix, $date) { $manifest = require $this->root . '/setup/core-migrations.php'; foreach ($manifest as $module => $definition) { $directory = $this->root . '/adminx/modules/' . $definition['directory']; if (!is_dir($directory) && !empty($definition['optional'])) { continue; } foreach ($definition['ids'] as $migration) { $base = $directory . '/migrations/' . $migration; $file = is_file($base . '.sql') ? $base . '.sql' : $base . '.php'; if (!is_file($file)) { throw new RuntimeException('Core-миграция не найдена: ' . $module . '/' . $migration); } $checksum = sha1_file($file); $this->execute($db, 'INSERT INTO `' . $prefix . '_module_migrations` (module,migration,checksum,applied_at) VALUES (?,?,?,?)', 'ssss', array($module, $migration, $checksum, $date)); } } } protected function seedPublicIdentity(mysqli $db, $prefix, $siteName, $adminName, $adminLogin, $adminEmail, $adminPassword, $date) { $publicName = mb_substr((string) $adminName, 0, 50, 'UTF-8'); $publicLogin = mb_substr((string) $adminLogin, 0, 50, 'UTF-8'); $groups = array(1 => 'Администраторы', 2 => 'Гости', 3 => 'Модераторы', 4 => 'Пользователи'); foreach ($groups as $id => $name) { $this->execute($db, 'INSERT INTO `' . $prefix . '_public_user_groups` (user_group,user_group_name,status,set_default_avatar,default_avatar,user_group_permission) VALUES (?,?,\'1\',\'0\',\'\',\'\')', 'is', array($id, $name)); } $this->execute($db, 'INSERT INTO `' . $prefix . '_public_users` (Id,password,email,email_verified_at,firstname,user_name,user_group,reg_time,status,country,deleted) VALUES (1,?,?,?,?,?,?,?,\'1\',\'RU\',\'0\')', 'ssissii', array($adminPassword, $adminEmail, time(), $publicName, $publicLogin, 1, time())); $checkoutPolicy = 'Я согласен с политикой конфиденциальности и обработкой персональных данных'; $checkoutSubject = 'Доступ к личному кабинету'; $checkoutTemplate = '

    Личный кабинет создан

    ' . '

    {% if user.firstname %}{{ user.firstname }}, {% endif %}ваш заказ №{{ order.id }} оформлен, а личный кабинет уже доступен.

    ' . '

    Логин: {{ user.email }}

    ' . '

    Задать пароль

    ' . '

    Ссылка действует {{ expires_hours }} ч. Если она истечёт, запросите новую на странице восстановления пароля.

    '; $this->execute($db, 'INSERT INTO `' . $prefix . '_public_auth_settings` (id,registration_enabled,registration_mode,registration_gate,default_group,password_min_length,verification_ttl,reset_ttl,password_reset_enabled,require_firstname,show_lastname,require_lastname,show_phone,require_phone,show_company,require_company,deny_domains,deny_emails,checkout_registration_enabled,checkout_policy_url,checkout_policy_label,checkout_access_subject,checkout_access_template,pages_json,updated_at) VALUES (1,0,\'email\',\'email\',4,8,86400,3600,0,1,1,0,0,0,0,0,\'\',\'\',1,\'/privacy-policy\',?,?,?,NULL,?)', 'ssss', array($checkoutPolicy, $checkoutSubject, $checkoutTemplate, $date)); $message = "Здравствуйте %NAME%,\n\nВаш аккаунт на сайте %HOST% создан.\n"; $signature = 'С уважением, ' . $siteName; $forbidden = '

    Доступ запрещён

    У вас нет прав для просмотра этой страницы.

    '; $hidden = '
    Содержимое скрыто.
    '; $this->execute($db, 'INSERT INTO `' . $prefix . '_public_settings` (Id,site_name,mail_type,mail_content_type,mail_port,mail_host,mail_smtp_login,mail_smtp_pass,mail_smtp_encrypt,mail_sendmail_path,mail_word_wrap,mail_from,mail_from_name,mail_new_user,mail_signature,page_not_found_id,message_forbidden,navi_box,start_label,end_label,separator_label,next_label,prev_label,total_label,link_box,total_box,active_box,separator_box,bread_box,bread_show_main,bread_show_host,bread_separator,bread_separator_use,bread_link_box,bread_link_template,bread_self_box,bread_link_box_last,date_format,time_format,default_country,use_doctime,hidden_text) VALUES (1,?,\'mail\',\'text/plain\',25,\'\',\'\',\'\',NULL,\'/usr/sbin/sendmail\',80,?,?,?, ?,2,?,\'\',\'Первая\',\'Последняя\',\'…\',\'»\',\'«\',\'Страница %d из %d\',\'
  • %s
  • \',\'%s\',\'%s\',\'
  • %s
  • \',\'\',\'1\',\'0\',\'
  • \',\'0\',\'
  • %s
  • \',\'[name]\',\'
  • %s
  • \',\'1\',\'%d.%m.%Y\',\'%d.%m.%Y, %H:%M\',\'RU\',\'0\',?)', 'sssssss', array($siteName, $adminEmail, $siteName, $message, $signature, $forbidden, $hidden)); } protected function seedContent(mysqli $db, $prefix, $siteName, $now) { $template = '' . "\n" . '' . "\n\n" . ' ' . "\n" . ' ' . "\n" . ' [tag:title]' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . ' ' . "\n" . '' . "\n\n" . '[tag:navigation:main]' . "\n" . '[tag:maincontent]' . "\n" . '' . "\n"; $this->execute($db, 'INSERT INTO `' . $prefix . '_templates` (Id,template_title,template_text,template_author_id,template_created) VALUES (1,?,?,1,?)', 'ssi', array('Основной шаблон', $template, $now)); $this->execute($db, 'INSERT INTO `' . $prefix . '_rubrics` (Id,rubric_title,rubric_alias,rubric_alias_history,rubric_template,rubric_template_id,rubric_author_id,rubric_created,rubric_docs_active,rubric_start_code,rubric_code_start,rubric_code_end,rubric_teaser_template,rubric_header_template,rubric_og_template,rubric_footer_template,rubric_linked_rubric,rubric_description,rubric_meta_gen,rubric_position,rubric_changed,rubric_changed_fields) VALUES (1,\'Главная страница\',\'\',\'0\',\'

    [tag:fld:title]

    [tag:fld:content]
    \',1,1,?,1,\'\',\'\',\'\',\'\',\'\',\'\',\'\',\'0\',\'Стартовая рубрика чистой установки\',\'0\',10,?,?)', 'iii', array($now, $now, $now)); $titleFieldSettings = json_encode(array('width' => 'full'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $contentFieldSettings = json_encode(array('width' => 'full', 'height' => 300), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $this->execute($db, 'INSERT INTO `' . $prefix . '_rubric_fields` (Id,rubric_id,rubric_field_group,rubric_field_alias,rubric_field_title,rubric_field_type,rubric_field_numeric,rubric_field_position,rubric_field_default,rubric_field_settings,rubric_field_search,rubric_field_template,rubric_field_template_request,rubric_field_description) VALUES (1,1,NULL,\'title\',\'Заголовок страницы\',\'single_line\',\'0\',1,\'\',?,\'1\',\'\',\'\',\'Заголовок внутри содержимого страницы\')', 's', array($titleFieldSettings)); $this->execute($db, 'INSERT INTO `' . $prefix . '_rubric_fields` (Id,rubric_id,rubric_field_group,rubric_field_alias,rubric_field_title,rubric_field_type,rubric_field_numeric,rubric_field_position,rubric_field_default,rubric_field_settings,rubric_field_search,rubric_field_template,rubric_field_template_request,rubric_field_description) VALUES (2,1,NULL,\'content\',\'Содержимое страницы\',\'richtext\',\'0\',2,\'\',?,\'1\',\'\',\'\',\'Основной текст страницы с визуальным редактором\')', 's', array($contentFieldSettings)); foreach (array(1 => 'docread|alles|new|newnow|editown|delrev', 2 => 'docread', 3 => 'docread|alles|new|newnow|editown|delrev', 4 => 'docread') as $group => $permission) { $this->execute($db, 'INSERT INTO `' . $prefix . '_rubric_permissions` (rubric_id,user_group_id,rubric_permission) VALUES (1,?,?)', 'is', array($group, $permission)); } $title = 'AVE.cms установлена'; $description = 'Чистая установка ' . $siteName . ' завершена успешно.'; $this->execute($db, 'INSERT INTO `' . $prefix . '_documents` (Id,rubric_id,rubric_tmpl_id,document_parent,document_alias,document_alias_header,document_alias_history,document_short_alias,document_title,document_breadcrumb_title,document_published,document_expire,document_changed,document_author_id,document_in_search,document_meta_keywords,document_meta_description,document_meta_robots,document_sitemap_freq,document_sitemap_pr,document_status,document_deleted,document_count_print,document_count_view,document_linked_navi_id,document_excerpt,document_tags,document_property,document_position,module_catalog,guid) VALUES (1,1,0,0,\'/\',301,\'0\',\'\',?,?,?,0,?,1,\'1\',\'AVE.cms, установка\',?,\'index,follow\',3,0.5,\'1\',\'0\',0,0,0,\'\',\'\',\'\',0,\'\',\'\')', 'ssiis', array($title, 'Главная', $now, $now, $description)); $content = '

    Чистая установка завершена успешно. Войдите в панель управления, создайте рубрики и документы, настройте шаблоны и подключите нужные модули.

    Справка по разработке и возможностям системы находится в каталоге help.

    '; $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields` (Id,rubric_field_id,document_id,field_number_value,field_value,document_in_search) VALUES (1,1,1,0,\'Поздравляем с установкой AVE.cms!\',\'1\')'); $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields` (Id,rubric_field_id,document_id,field_number_value,field_value,document_in_search) VALUES (2,2,1,0,\'\',\'1\')'); $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields_text` (Id,rubric_field_id,document_id,field_value) VALUES (2,2,1,?)', 's', array($content)); $notFoundTitle = 'Страница не найдена'; $notFoundDescription = 'Запрошенная страница не найдена.'; $this->execute($db, 'INSERT INTO `' . $prefix . '_documents` (Id,rubric_id,rubric_tmpl_id,document_parent,document_alias,document_alias_header,document_alias_history,document_short_alias,document_title,document_breadcrumb_title,document_published,document_expire,document_changed,document_author_id,document_in_search,document_meta_keywords,document_meta_description,document_meta_robots,document_sitemap_freq,document_sitemap_pr,document_status,document_deleted,document_count_print,document_count_view,document_linked_navi_id,document_excerpt,document_tags,document_property,document_position,module_catalog,guid) VALUES (2,1,0,0,\'404\',301,\'0\',\'\',?,?,?,0,?,1,\'0\',\'\',?,\'noindex,nofollow\',3,0,\'1\',\'0\',0,0,0,\'\',\'\',\'\',2,\'\',\'\')', 'ssiis', array($notFoundTitle, 'Ошибка 404', $now, $now, $notFoundDescription)); $notFoundContent = '

    Запрошенная страница не существует или была перемещена.

    Перейти на главную

    '; $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields` (Id,rubric_field_id,document_id,field_number_value,field_value,document_in_search) VALUES (3,1,2,0,\'Страница не найдена\',\'0\')'); $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields` (Id,rubric_field_id,document_id,field_number_value,field_value,document_in_search) VALUES (4,2,2,0,\'\',\'0\')'); $this->execute($db, 'INSERT INTO `' . $prefix . '_document_fields_text` (Id,rubric_field_id,document_id,field_value) VALUES (4,2,2,?)', 's', array($notFoundContent)); $this->execute($db, 'INSERT INTO `' . $prefix . '_navigation` (navigation_id,alias,title,level1,level2,level3,level1_active,level2_active,level3_active,level1_begin,level1_end,level2_begin,level2_end,level3_begin,level3_end,begin,end,user_group,expand_ext) VALUES (1,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', 'ssssssssssssssssss', array( 'main', 'Основная навигация', '', '', '', '', '', '', '', '', '', '', '', '', '1,2,3,4', '1', )); $this->execute($db, 'INSERT INTO `' . $prefix . '_navigation_items` (navigation_item_id,navigation_id,document_id,alias,title,description,target,image,css_style,css_id,css_class,parent_id,level,position,status) VALUES (1,1,1,\'/\',\'Главная\',\'Главная страница сайта\',\'_self\',\'\',NULL,NULL,\'home\',0,\'1\',1,\'1\')'); } protected function applySchema(mysqli $db, $prefix) { $sql = file_get_contents(__DIR__ . '/schema.sql'); if ($sql === false || trim($sql) === '') { throw new RuntimeException('Core-схема setup/schema.sql пуста или недоступна.'); } $sql = str_replace('{{prefix}}', $prefix, $sql); if (strpos($sql, '{{') !== false) { throw new RuntimeException('В core-схеме остались неизвестные placeholder.'); } if (!$db->multi_query($sql)) { throw new RuntimeException('Ошибка применения схемы: ' . $db->error); } do { if ($result = $db->store_result()) { $result->free(); } if (!$db->more_results()) { break; } } while ($db->next_result()); if ($db->errno) { throw new RuntimeException('Ошибка применения схемы: ' . $db->error); } } protected function assertPristine(mysqli $db, $prefix) { foreach (array('users', 'documents', 'rubrics', 'templates', 'modules') as $suffix) { $table = $prefix . '_' . $suffix; if ($this->tableExists($db, $table) && $this->tableCount($db, $table) > 0) { throw new RuntimeException('Префикс «' . $prefix . '» уже содержит данные AVE.cms. Выберите пустой префикс.'); } } } protected function connect(array $config) { mysqli_report(MYSQLI_REPORT_OFF); $port = !empty($config['dbport']) ? (int) $config['dbport'] : 3306; $db = @new mysqli($config['dbhost'], $config['dbuser'], $config['dbpass'], $config['dbname'], $port); if ($db->connect_errno) { throw new RuntimeException('Не удалось подключиться к БД: ' . $db->connect_error); } if (!$db->set_charset('utf8mb4')) { throw new RuntimeException('База данных не поддерживает utf8mb4.'); } return $db; } protected function execute(mysqli $db, $sql, $types = '', array $values = array()) { $statement = $db->prepare($sql); if (!$statement) { throw new RuntimeException('Ошибка подготовки seed-запроса: ' . $db->error); } if ($types !== '') { $params = array($types); foreach ($values as $key => $value) { $params[] = &$values[$key]; } call_user_func_array(array($statement, 'bind_param'), $params); } if (!$statement->execute()) { $message = $statement->error; $statement->close(); throw new RuntimeException('Ошибка заполнения core-данных: ' . $message); } $statement->close(); } protected function writeDatabaseConfig(array $config) { $file = $this->root . '/configs/db.config.php'; $payload = array( 'dbhost' => $config['dbhost'], 'dbuser' => $config['dbuser'], 'dbpass' => $config['dbpass'], 'dbname' => $config['dbname'], 'dbpref' => $config['dbpref'], 'dbchar' => 'utf8mb4', 'dbport' => $config['dbport'] ?: null, 'dbsock' => null, ); $php = "validateAdminDirectory($directory); $source = $this->root . '/adminx'; $target = $this->root . '/' . $directory; $configFile = $this->root . '/configs/public.config.php'; $backup = is_file($configFile) ? (string) file_get_contents($configFile) : ''; $renamed = false; try { if ($directory !== 'adminx') { if (!@rename($source, $target)) { throw new RuntimeException('Не удалось переименовать папку панели управления.'); } $renamed = true; } $this->writeAdminDirectoryConfig($directory, $backup); } catch (Throwable $e) { if ($renamed && is_dir($target) && !file_exists($source)) { @rename($target, $source); } $this->restoreFile($configFile, $backup); throw $e; } return array( 'directory' => $directory, 'source' => $source, 'target' => $target, 'renamed' => $renamed, 'config_file' => $configFile, 'config_backup' => $backup, ); } protected function rollbackAdminDirectory(array $change) { if (!empty($change['renamed']) && is_dir($change['target']) && !file_exists($change['source'])) { @rename($change['target'], $change['source']); } $this->restoreFile($change['config_file'], $change['config_backup']); } protected function writeAdminDirectoryConfig($directory, $content) { $file = $this->root . '/configs/public.config.php'; $replacement = "\t\t'admin_directory' => " . var_export((string) $directory, true) . ','; $count = 0; $content = preg_replace( "/^\\s*'admin_directory'\\s*=>\\s*[^,]+,/m", $replacement, (string) $content, 1, $count ); if ($count !== 1 || !is_string($content)) { throw new RuntimeException('В configs/public.config.php отсутствует настройка admin_directory.'); } $this->writeFile($file, $content, 0644, 'Не удалось записать имя папки панели управления.'); } protected function restoreFile($file, $content) { if ((string) $content === '') { return; } $this->writeFile($file, (string) $content, 0644, ''); } protected function writeFile($file, $content, $mode, $error) { $tmp = $file . '.tmp-' . bin2hex(random_bytes(6)); if (file_put_contents($tmp, $content, LOCK_EX) === false || !@rename($tmp, $file)) { @unlink($tmp); if ($error !== '') { throw new RuntimeException($error); } return false; } @chmod($file, $mode); return true; } protected function writeLock($prefix, $email, $adminDirectory) { $release = is_file($this->root . '/system/release.php') ? require $this->root . '/system/release.php' : array(); $packageManifestFile = $this->root . '/PACKAGE-MANIFEST.json'; $packageManifest = is_file($packageManifestFile) ? json_decode((string) file_get_contents($packageManifestFile), true) : array(); $payload = json_encode(array( 'installed_at' => date(DATE_ATOM), 'version' => isset($release['version']) ? (string) $release['version'] : '3.3', 'build' => isset($release['build']) ? (string) $release['build'] : '0.20', 'prefix' => $prefix, 'administrator' => $email, 'admin_directory' => $adminDirectory, 'core_fingerprint' => is_array($packageManifest) && isset($packageManifest['core_fingerprint']) ? (string) $packageManifest['core_fingerprint'] : '', 'package_manifest_sha256' => is_file($packageManifestFile) ? hash_file('sha256', $packageManifestFile) : '', ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (file_put_contents($this->lockFile(), $payload . "\n", LOCK_EX) === false) { throw new RuntimeException('Не удалось создать storage/installed.lock.'); } @chmod($this->lockFile(), 0640); } protected function clearRuntimeCache() { foreach (array($this->root . '/tmp/cache', $this->root . '/tmp/sessions') as $directory) { $this->clearDirectory($directory); } } protected function clearDirectory($directory) { if (!is_dir($directory)) { @mkdir($directory, 0775, true); return; } foreach (scandir($directory) ?: array() as $entry) { if ($entry === '.' || $entry === '..') { continue; } $path = $directory . '/' . $entry; if (is_dir($path) && !is_link($path)) { $this->clearDirectory($path); @rmdir($path); } else { @unlink($path); } } } protected function dropPrefixedTables(mysqli $db, $prefix) { $this->dropPrefixedObjects($db, $prefix); } protected function dropPrefixedObjects(mysqli $db, $prefix) { $pattern = str_replace(array('\\', '_', '%'), array('\\\\', '\\_', '\\%'), $prefix) . '\\_%'; $statement = $db->prepare( 'SELECT table_name, table_type FROM information_schema.tables ' . 'WHERE table_schema=DATABASE() AND table_name LIKE ? ORDER BY table_type DESC, table_name' ); if (!$statement) { throw new RuntimeException('Не удалось получить объекты базы данных: ' . $db->error); } $statement->bind_param('s', $pattern); if (!$statement->execute()) { throw new RuntimeException('Не удалось получить объекты префикса: ' . $statement->error); } $statement->bind_result($tableName, $tableType); $tables = array(); $views = array(); while ($statement->fetch()) { $name = '`' . str_replace('`', '``', (string) $tableName) . '`'; if (strtoupper((string) $tableType) === 'VIEW') { $views[] = $name; } else { $tables[] = $name; } } $statement->close(); $db->query('SET FOREIGN_KEY_CHECKS=0'); try { foreach (array_chunk($views, 50) as $chunk) { if (!$db->query('DROP VIEW IF EXISTS ' . implode(',', $chunk))) { throw new RuntimeException('Не удалось удалить представления: ' . $db->error); } } foreach (array_chunk($tables, 50) as $chunk) { if (!$db->query('DROP TABLE IF EXISTS ' . implode(',', $chunk))) { throw new RuntimeException('Не удалось удалить таблицы: ' . $db->error); } } } finally { $db->query('SET FOREIGN_KEY_CHECKS=1'); } return count($tables) + count($views); } protected function prefixedTables(mysqli $db, $prefix) { $pattern = str_replace(array('\\', '_', '%'), array('\\\\', '\\_', '\\%'), $prefix) . '\\_%'; $statement = $db->prepare('SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name LIKE ?'); if (!$statement) { throw new RuntimeException('Не удалось получить список таблиц: ' . $db->error); } $statement->bind_param('s', $pattern); $statement->execute(); $statement->bind_result($tableName); $tables = array(); while ($statement->fetch()) { $tables[] = (string) $tableName; } $statement->close(); return $tables; } protected function tableExists(mysqli $db, $table) { $statement = $db->prepare('SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=? LIMIT 1'); if (!$statement) { return false; } $statement->bind_param('s', $table); $statement->execute(); $statement->store_result(); $exists = $statement->num_rows > 0; $statement->close(); return $exists; } protected function tableCount(mysqli $db, $table) { if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) { throw new InvalidArgumentException('Некорректное имя таблицы.'); } $result = $db->query('SELECT COUNT(*) AS amount FROM `' . $table . '`'); $row = $result ? $result->fetch_assoc() : null; return $row ? (int) $row['amount'] : 0; } protected function validatePrefix($prefix) { $prefix = trim((string) $prefix); if (!preg_match('/^[A-Za-z][A-Za-z0-9_]{0,39}$/', $prefix)) { throw new InvalidArgumentException('Префикс должен начинаться с буквы и содержать только буквы, цифры и _.'); } return $prefix; } protected function check($label, $passed, $detail = '', $required = true) { return array('label' => $label, 'passed' => (bool) $passed, 'detail' => (string) $detail, 'required' => (bool) $required); } protected function writablePath($path) { return is_dir($path) ? is_writable($path) : is_writable(dirname($path)); } protected function gdWebpAvailable() { if (!extension_loaded('gd') || !function_exists('imagewebp')) { return false; } $info = function_exists('gd_info') ? gd_info() : array(); return !array_key_exists('WebP Support', $info) || !empty($info['WebP Support']); } protected function lockFile() { return $this->root . '/storage/installed.lock'; } }