<?php
// Первый файл ядра CMS.
// Второй файл ядра это index.fn.php со вспомогательными функциями.
// В данном файле:
// — Отключается вывод ошибок на экран и задается версия ядра.
// — Определяются абсолютные пути к папкам CMS и сайта (с учетом Windows/Linux).
// — Вычисляется протокол (http/https), хост и базовый путь установки (если CMS в подпапке).
// — URL запроса очищается, декодируется и разбивается на части (хост, путь, параметры).
// — Загружается файл config.php с использованием блокировки flock.
// — Устанавливаются локаль и часовой пояс из конфига.
// — Определяются цепочки хуков (последовательность выполнения событий) и загружаются все *.fn.php и *.mod.php файлы.
// — Маршруты сортируются по длине, и текущий URL сопоставляется с регулярным выражением для выбора стартового хука.
// — Запускается цикл выполнения функций, привязанных к выбранным хукам.
// — В режиме отладки записываются время работы, потребление памяти и содержимое массива $cms["debug"] в лог-файлы.

// Измерение скорости работы CMS.
$time_start = microtime( true );

// Плохие хостинги выводят предупреждения прямо в выдачу.
// Исправляем это.
ini_set( "display_errors", "Off" );

// Релизы нумеруются: 26.05, в разработке: 26.05.001
$cms["kernel_version"] = "26.08.1";
// Произвольное слово для совместимости.
// Если поменять, то обновлять не будет, напишет что не совместимое обновление.
$cms["kernel_compat"]  = "26.08";

// Определяем абсолютный путь к директории CMS (.cms).
$cms["cms_dir"] = dirname( __FILE__ );

// Если сервер работает на Windows, заменяем обратные слэши
// на прямые для унификации путей.
if ( DIRECTORY_SEPARATOR === "\\" ) {
    $cms["cms_dir"] = str_replace( "\\", "/", $cms["cms_dir"] );
}

// Определяем корневую директорию сайта (на уровень выше, чем .cms).
$cms["site_dir"] = dirname( $cms["cms_dir"] );

// Определяем протокол (http или https), по которому был сделан запрос.
if ( ! empty( $_SERVER["HTTPS"] ) && $_SERVER["HTTPS"] !== "off" ) {
    $protocol = "https";
} else {
    $protocol = "http";
}

// Определяем хост. Используем HTTP_HOST, так как он включает порт и более надежен за прокси.
// HTTP_HOST содержит имя хоста и порт именно в том виде, в котором их видит браузер.
if ( ! empty( $_SERVER["HTTP_HOST"] ) ) {
    $host = $_SERVER["HTTP_HOST"];
} else {
    $host = $_SERVER["SERVER_NAME"];
}


// Убираем замыкающий слэш из `DOCUMENT_ROOT` для консистентности.
$_SERVER["DOCUMENT_ROOT"] = rtrim( $_SERVER["DOCUMENT_ROOT"], "/\\" );

// Вычисляем базовый путь, если CMS установлена в подпапку.
// https://coffee-cms.com/base_path/
//                       ^         ^
// Например, если сайт в /var/www/site, а CMS в /var/www/site/cms,
// base_path будет /cms/
$cms["base_path"] = substr( $cms["site_dir"], strlen( $_SERVER["DOCUMENT_ROOT"] ) ) . "/";

// Формируем "чистый" URL запроса, удаляя из него базовый путь.
// Для того чтобы правильно распарсить и искать по БД страницы.
if ( strpos( $_SERVER["REQUEST_URI"], $cms["base_path"] ) === 0 ) {
    $request_uri = substr( $_SERVER["REQUEST_URI"], strlen( $cms["base_path"] ) );
} else {
    $request_uri = substr( $_SERVER["REQUEST_URI"], 1 );
}
$url = "{$protocol}://{$host}/{$request_uri}";

// Парсим URL на компоненты (схема, хост, путь, параметры).
// Если порт не указан, то parse_url() не добавит его. Так и надо.
$cms["url"] = parse_url( $url );
// https://www.php.net/manual/ru/function.parse-url.php
// $cms["url"]["scheme"] (http или https)
// $cms["url"]["host"]
// $cms["url"]["port"]
// $cms["url"]["user"]
// $cms["url"]["pass"]
// $cms["url"]["path"]
// $cms["url"]["query"]
// $cms["url"]["fragment"]
// Тут можно было бы
// Нормализовать путь один раз
// $path = normalize($cms["base_path"] . $cms["url"]["path"]);
// Чтобы убирать /foo/bar/../baz, но лучше явное чем пропускать странные запросы.

// Предотвратить варнинги
if ( ! isset( $cms["url"]["port"] ) ) {
    $cms["url"]["port"] = "";
} else {
    $cms["url"]["port"] = ":" . $cms["url"]["port"];
}
// $cms["debug"]["url"][] = $cms["url"];
// Декодируем URL-кодированные символы в пути (например, %20 -> пробел).
$cms["url"]["path"] = urldecode( $cms["url"]["path"] );
// Формируем путь к файлу внутри директории .cms
// Если запрошен файл, нет необходимости делать SQL-запросы.
$cms["url"]["file"] = $cms["cms_dir"] . $cms["url"]["path"];
// Убираем ведущий слэш из пути для удобства дальнейшей обработки.
$cms["url"]["path"] = substr( $cms["url"]["path"], 1 );

// Устанавливаем HTTP-статус по умолчанию.
// Если страница не будет найдена, он останется 404.
$cms["status"] = "404";


// Загрузка конфигурационного файла.
$cms["config_file"] = "{$cms["cms_dir"]}/config.php";
if ( is_file( $cms["config_file"] ) ) {
    include( $cms["config_file"] );
}


// Если в конфиге задана локаль, устанавливаем ее для PHP.
if ( ! empty( $cms["config"]["locale"] ) ) {
    setlocale( LC_ALL, $cms["config"]["locale"] );
}


// Если в конфиге задана временная зона, устанавливаем ее.
if ( ! empty( $cms["config"]["timezone"] ) ) {
    date_default_timezone_set ( $cms["config"]["timezone"] );
}


// Инициализация системы хуков (событий).
// Хуки определяют последовательность выполнения кода.
// Отключить хук можно, добавив "disabled" => true.
$cms["hooks"] = array(
    // Цепочка для обычных страниц:
    // запрос -> обработка шаблона -> вывод -> запись кеша.
    "query"    => array( "next" => "template" ), 
    "template" => array( "next" => "echo"     ), 
    "echo"     => array( "next" => "write"    ), 
    "write"    => array( "next" => ""         ), 
    // Цепочка для админ-панели.
    "admin"    => array( "next" => "template" ), 
    // Цепочка для API запросов (обычно возвращают JSON и завершают работу).
    "api"      => array( "next" => ""         ), 
    // Цепочка для задач, выполняемых по расписанию (cron).
    "cron"     => array( "next" => ""         ),
);
// Стек для отслеживания рекурсивных вызовов
$cms["hooks_stack"] = array();

// Массив для хранения маршрутов (URL-шаблонов и соответствующих им хуков).
$cms["urls"] = array();

// Загрузка всех файлов с основными функциями (*.fn.php).
foreach( glob( $cms["cms_dir"] . "/*.fn.php" ) as $file ) {
    include_once( $file );
}

// Загрузка всех модулей из стандартной директории /mod.
foreach( glob( "{$cms["cms_dir"]}/mod/*.mod.php" ) as $module ) {
    include_once( $module );
}

// Загрузка модулей, специфичных для текущего активного шаблона.
if ( ! empty( $cms["config"]["template.mod.php"]["template"] ) ) {
    $selected_template = $cms["config"]["template.mod.php"]["template"];
    foreach( glob( "{$cms["cms_dir"]}/{$selected_template}/*.mod.php" ) as $module ) {
        include_once( $module );
    }
}

$cms["hook"]  = "query"; // Хук с которого начинается обработка запроса.

// Функция для сортировки маршрутов по длине в обратном порядке.
// Это необходимо, чтобы более длинные и специфичные маршруты
// (напр., /blog/post/123) проверялись раньше, чем более короткие и общие
// (напр., /blog/).
function urls_sort( $a, $b ) {
    if ( strlen( $a ) >= strlen( $b ) )
        return -1;
    else
        return 1;
}
uksort( $cms["urls"], "urls_sort" );

// Выбор маршрута. Перебираем все зарегистрированные URL-шаблоны.
foreach( $cms["urls"] as $regex => $hook ) {
    // Проверяем, соответствует ли текущий путь запроса
    // (включая base_path) предоставленному шаблону шаблону.
    if ( preg_match(
        // Выбираем во что завернуть регулярное выражение чтобы не экранировать символы
        preg_prepare( $regex ), // см. в index.fn.php
        $cms["base_path"] . $cms["url"]["path"], $cms["url"]["match"]
    ) ) {
        // Если найдено совпадение,
        // устанавливаем соответствующий хук как стартовый.
        $cms["hook"] = $hook;
        // Прерываем цикл, так как нужный маршрут найден.
        break;
    }
}
// 1.
// Экранировать тире в выражении не нужно - оно не внутри [].
// `$cms["urls"]["^{$cms['base_path']}my-module-url$"] = "my_module_hook";`
// 2.
// Казалось бы можно убрать $cms["base_path"]
// и так же избавить модули от необходимости его добавлять.
// Но с base_path больше гибкости.
// Модули смогут перехватывать запросы к корню сайта,
// даже если CMS находится не в корневой папке.
// Еще один возможный теоретический недостаток - не отличимость
// GET /users
// POST /users
// Но может и не важно - модули сами могут разобраться.
// Можно было бы:
// Разделить по HTTP-методу
// $cms["urls"][$method][$regex] = $hook;

// Основной цикл выполнения CMS.
// Работает, пока текущий хук ($cms["hook"]) не станет пустым.
while ( ! empty( $cms["hook"] ) ) {
    // Выполняем все функции, привязанные к текущему хуку.
    // Функция do_hook, определена в файле index.fn.php.
    do_hook( $cms["hook"] );
    // Переходим к следующему хуку в цепочке, определенной в $cms["hooks"].
    $cms["hook"] = $cms["hooks"][ $cms["hook"] ]["next"];
}

// Измерение времени и потребления памяти, если включен режим отладки.
if ( ! empty( $cms["config"]["debug"] ) ) {

    // Вычисляем время выполнения CMS в миллисекундах.
    $delta_time = align_right(
        number_format( ( microtime( true ) - $time_start ) * 1000 ) . " ms",
        6
    );
    // Вычисляем пиковое потребление памяти в мегабайтах.
    $mem_usage = align_right(
        number_format( memory_get_peak_usage() / 1024 / 1024, 3 ) . " MB",
        8
    );

    // Формируем URL для лога.
    $url = $cms["base_path"] . $cms["url"]["path"];

    // Если это был вызов API, то определяем имя вызванной функции
    if ( $cms["url"]["path"] == $cms["config"]["admin.mod.php"]["api_url"] ) {
        $url = "API: {$_POST["fn"]}";
    }

    // Имя лог-файла.
    $file_name = $cms["site_dir"] . "/.perf.log";
    // Записываемая в него строка.
    $line = "{$mem_usage}  {$delta_time}  {$url}\n";

    // Простая ротация лога: если файл больше 100 КБ,
    // он перезаписывается, иначе - дописывается.
    if ( is_file( $file_name ) && filesize( $file_name ) > 100 * 1024 ) {
        file_put_contents( $file_name, $line );
    } else {
        file_put_contents( $file_name, $line, FILE_APPEND );
    }

}

// Сохранение накопленной отладочной информации,
// если включен режим отладки и есть что сохранять.
// Если $cms["config"]["debug"] == true,
// То считываем файл с отладочной информацией и добавляем новую.
if ( ! empty( $cms["config"]["debug"] ) && ! empty( $cms["debug"] ) ) {
    $debug_file = $cms["site_dir"] . "/.debug.log.php";
    $new_debug = $cms["debug"];
    // Если файл с отладочной информацией уже существует, читаем его.
    if ( is_file( $debug_file ) && $handle = fopen( $debug_file, "r" ) ) {
        flock( $handle, LOCK_SH );
        include( $debug_file );
        flock( $handle, LOCK_UN );
        // Рекурсивно объединяем старую и новую отладочную информацию.
        $cms["debug"] = array_replace_recursive( $cms["debug"], $new_debug );
    }
    // Записываем объединенную информацию обратно в файл
    // с эксклюзивной блокировкой.
    // var_export генерирует PHP-код,
    // который можно будет снова подключить через include.
    file_put_contents(
        $debug_file,
        '<?php $cms["debug"] = ' . var_export( $cms["debug"], true ) . ";\n",
        LOCK_EX
    );
}

// Закомментированный код для сохранения всего глобального массива $cms в
// лог-файл. Это пример того как можно производить отладку, просматривая
// что накопилось в переменной $cms.
// file_put_contents(
//     dirname( __FILE__ ) . "/.cms.log.php",
//     '<?php $cms = ' . var_export( $cms, true ) . ";\n"
// );
