Files
ave-cms/system/App/Common/CompiledModuleRegistry.php
2026-07-27 12:58:44 +03:00

354 lines
9.8 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Common/CompiledModuleRegistry.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Common;
use App\Common\Loader\Load;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
/** JSON cache for discovered module descriptors. */
class CompiledModuleRegistry
{
const FORMAT_VERSION = 1;
const VALIDATE_INTERVAL = 60;
protected static $payload;
protected function __construct()
{
//
}
/** Return a compiled scope, or null when discovery must run. */
public static function load($scope, array $expectedRoots = array())
{
$scope = self::scopeName($scope);
$payload = self::payload();
if ($scope === '' || empty($payload['scopes'][$scope]) || !is_array($payload['scopes'][$scope])) {
return null;
}
$data = $payload['scopes'][$scope];
if (!self::containsRoots($data, $expectedRoots)) {
self::invalidate($scope);
return null;
}
$validatedAt = isset($data['validated_at']) ? (int) $data['validated_at'] : 0;
$validateNow = $scope === 'adminx' || $validatedAt < time() - self::VALIDATE_INTERVAL;
if ($validateNow) {
if (!self::validateScope($data)) {
self::invalidate($scope);
return null;
}
self::touch($scope);
}
return isset($data['entries']) && is_array($data['entries']) ? $data['entries'] : array();
}
/** Persist one independently discoverable module scope. */
public static function store($scope, array $entries, array $roots)
{
$scope = self::scopeName($scope);
if ($scope === '') {
return false;
}
$rootState = array();
foreach ($roots as $root) {
$relative = self::relativePath($root);
if ($relative === '' || isset($rootState[$relative])) {
continue;
}
$rootState[$relative] = array(
'path' => $relative,
'exists' => is_dir($root),
'mtime' => is_dir($root) ? (int) @filemtime($root) : 0,
);
}
$now = time();
return self::mutate(function (array $payload) use ($scope, $entries, $rootState, $now) {
$payload['scopes'][$scope] = array(
'generated_at' => $now,
'validated_at' => $now,
'roots' => array_values($rootState),
'entries' => array_values($entries),
);
return $payload;
});
}
/** Remove one scope or the complete registry. */
public static function invalidate($scope = null)
{
if ($scope === null) {
self::$payload = null;
$file = self::file();
return !is_file($file) || @unlink($file);
}
$scope = self::scopeName($scope);
if ($scope === '') {
return false;
}
return self::mutate(function (array $payload) use ($scope) {
unset($payload['scopes'][$scope]);
return $payload;
});
}
/** Build a portable entry from a descriptor already evaluated by discovery. */
public static function entry($folder, $basePath, $manifest, array $namespaces, array $descriptor)
{
$cached = $descriptor;
unset($cached['_base_path']);
$namespacePaths = array();
foreach ($namespaces as $namespace => $path) {
$relative = self::relativePath($path);
if ($relative !== '') {
$namespacePaths[trim((string) $namespace, '\\')] = $relative;
}
}
$manifestPath = self::relativePath($manifest);
$cacheable = empty($cached['registry']['dynamic']) && self::isCacheable($cached);
if ($cacheable && !is_string(json_encode($cached))) {
$cacheable = false;
}
return array(
'folder' => (string) $folder,
'code' => isset($descriptor['code']) ? (string) $descriptor['code'] : (string) $folder,
'base_path' => self::relativePath($basePath),
'manifest' => $manifestPath,
'manifest_mtime' => (int) @filemtime($manifest),
'manifest_size' => (int) @filesize($manifest),
'namespaces' => $namespacePaths,
'descriptor' => $cacheable ? $cached : null,
'dynamic' => !$cacheable,
);
}
/** Register namespaces and activate one compiled descriptor. */
public static function activate(array $entry)
{
$descriptor = self::descriptor($entry);
if (!$descriptor) {
return null;
}
ModuleManager::register($descriptor);
return $descriptor;
}
/** Read one descriptor without registering its runtime contributions. */
public static function descriptor(array $entry)
{
$basePath = self::absolutePath(isset($entry['base_path']) ? $entry['base_path'] : '');
$manifest = self::absolutePath(isset($entry['manifest']) ? $entry['manifest'] : '');
if ($basePath === '' || !is_dir($basePath) || $manifest === '' || !is_file($manifest)) {
return null;
}
foreach (isset($entry['namespaces']) && is_array($entry['namespaces']) ? $entry['namespaces'] : array() as $namespace => $path) {
$path = self::absolutePath($path);
if ($namespace !== '' && $path !== '' && is_dir($path)) {
Load::regNamespace((string) $namespace, $path);
}
}
if (isset($entry['descriptor']) && is_array($entry['descriptor'])) {
$descriptor = $entry['descriptor'];
$descriptor['_base_path'] = $basePath;
return $descriptor;
}
$descriptor = include $manifest;
if (!is_array($descriptor)) {
return null;
}
$descriptor['_base_path'] = $basePath;
return $descriptor;
}
public static function file()
{
return rtrim(str_replace('\\', '/', BASEPATH), '/') . '/storage/runtime/module-registry.json';
}
protected static function validateScope(array $scope)
{
foreach (isset($scope['roots']) && is_array($scope['roots']) ? $scope['roots'] : array() as $root) {
$path = self::absolutePath(isset($root['path']) ? $root['path'] : '');
$exists = !empty($root['exists']);
if ($path === '' || is_dir($path) !== $exists || ($exists && (int) @filemtime($path) !== (int) $root['mtime'])) {
return false;
}
}
foreach (isset($scope['entries']) && is_array($scope['entries']) ? $scope['entries'] : array() as $entry) {
$manifest = self::absolutePath(isset($entry['manifest']) ? $entry['manifest'] : '');
if ($manifest === '' || !is_file($manifest)
|| (int) @filemtime($manifest) !== (int) $entry['manifest_mtime']
|| (int) @filesize($manifest) !== (int) $entry['manifest_size']) {
return false;
}
}
return true;
}
protected static function containsRoots(array $scope, array $expectedRoots)
{
if (!$expectedRoots) {
return true;
}
$known = array();
foreach (isset($scope['roots']) && is_array($scope['roots']) ? $scope['roots'] : array() as $root) {
if (isset($root['path'])) {
$known[(string) $root['path']] = true;
}
}
foreach ($expectedRoots as $root) {
$relative = self::relativePath($root);
if ($relative === '' || !isset($known[$relative])) {
return false;
}
}
return true;
}
protected static function isCacheable($value)
{
if (is_array($value)) {
foreach ($value as $item) {
if (!self::isCacheable($item)) {
return false;
}
}
return true;
}
return $value === null || is_scalar($value);
}
protected static function touch($scope)
{
$now = time();
self::mutate(function (array $payload) use ($scope, $now) {
if (isset($payload['scopes'][$scope])) {
$payload['scopes'][$scope]['validated_at'] = $now;
}
return $payload;
});
}
protected static function payload($fresh = false)
{
if (!$fresh && is_array(self::$payload)) {
return self::$payload;
}
$payload = array('format' => self::FORMAT_VERSION, 'scopes' => array());
$file = self::file();
if (is_file($file)) {
$decoded = json_decode((string) @file_get_contents($file), true);
if (is_array($decoded) && isset($decoded['format']) && (int) $decoded['format'] === self::FORMAT_VERSION
&& isset($decoded['scopes']) && is_array($decoded['scopes'])) {
$payload = $decoded;
}
}
self::$payload = $payload;
return $payload;
}
protected static function mutate($callback)
{
$directory = dirname(self::file());
if (!RuntimeDirectoryGuard::protect($directory)) {
return false;
}
$lock = @fopen($directory . '/module-registry.lock', 'c');
if (!$lock || !@flock($lock, LOCK_EX)) {
if ($lock) { @fclose($lock); }
return false;
}
try {
$payload = self::payload(true);
$payload = call_user_func($callback, $payload);
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if (!is_string($json)) {
return false;
}
$tmp = self::file() . '.tmp-' . str_replace('.', '', uniqid('', true));
if (@file_put_contents($tmp, $json, LOCK_EX) === false || !@rename($tmp, self::file())) {
@unlink($tmp);
return false;
}
@chmod(self::file(), 0664);
self::$payload = $payload;
return true;
} finally {
@flock($lock, LOCK_UN);
@fclose($lock);
}
}
protected static function relativePath($path)
{
$base = rtrim(str_replace('\\', '/', BASEPATH), '/');
$path = rtrim(str_replace('\\', '/', (string) $path), '/');
if ($path === $base) {
return '.';
}
return strpos($path, $base . '/') === 0 ? substr($path, strlen($base) + 1) : '';
}
protected static function absolutePath($path)
{
$path = trim(str_replace('\\', '/', (string) $path), '/');
if ($path === '' || $path === '.' || strpos('/' . $path . '/', '/../') !== false || strpos($path, "\0") !== false) {
return $path === '.' ? rtrim(str_replace('\\', '/', BASEPATH), '/') : '';
}
return rtrim(str_replace('\\', '/', BASEPATH), '/') . '/' . $path;
}
protected static function scopeName($scope)
{
$scope = (string) $scope;
return preg_match('/^[a-z][a-z0-9_]{0,31}$/', $scope) ? $scope : '';
}
}