317 lines
12 KiB
PHP
317 lines
12 KiB
PHP
<?php global /** @var response $response */
|
|
$DEBUG, $CONFIG_DB, $ENCRYPTION_KEY, $CORS, $WD, $db, $response, $request, $router, $redis;
|
|
ini_set('output_buffering', 'off');
|
|
ini_set('zlib.output_compression', false);
|
|
|
|
/**
|
|
* This is the main entry point to the Truck Wash API.
|
|
*/
|
|
const WD = __DIR__;
|
|
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
require_once 'config.php';
|
|
require_once __DIR__ . '/classes/cors_policy.php';
|
|
|
|
/** CORS */
|
|
// OPTIONS requests are preflight requests for CORS, we can just return a 200 OK response
|
|
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
|
|
$preflight = \classes\cors_policy::preflightResponse($_SERVER['HTTP_ORIGIN'] ?? '', (string)($CORS ?? ''));
|
|
\classes\cors_policy::emitHeaders($preflight['headers']);
|
|
http_response_code($preflight['status']);
|
|
echo $preflight['body'];
|
|
exit;
|
|
}
|
|
\classes\cors_policy::applyResponseHeaders((string)($CORS ?? ''));
|
|
/** Debug */
|
|
if ($DEBUG) {
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
} else {
|
|
ini_set('display_errors', 0);
|
|
ini_set('display_startup_errors', 0);
|
|
error_reporting(0);
|
|
}
|
|
|
|
/** Autoload */
|
|
require_once __DIR__ . '/vendor/autoload.php';
|
|
|
|
// Initialize Redis for autoload caching
|
|
require_once WD . '/interfaces/redis_i.php';
|
|
require_once WD . '/traits/redis_t.php';
|
|
require_once WD . '/classes/redis.php';
|
|
try {
|
|
$redis_instance = (new classes\redis())->connect();
|
|
define("redis", $redis_instance);
|
|
} catch (Exception $e) {
|
|
// Redis might not be available, autoloader will fall back to manual search
|
|
}
|
|
|
|
/**
|
|
* Lightweight project autoloader: load classes/interfaces/traits/modules on demand.
|
|
* Maps top-level namespaces to folders under the app root (WD).
|
|
* - classes\* -> WD/classes/*
|
|
* - interfaces\* -> WD/interfaces/*
|
|
* - traits\* -> WD/traits/*
|
|
* - objects\* -> WD/objects/*
|
|
* - statistics\* -> WD/statistics/*
|
|
* - modules\...\* -> WD/modules/...
|
|
* - <module>\...\* -> WD/modules/<module>/... (e.g., bird\bird_c)
|
|
*/
|
|
spl_autoload_register(function (string $class): void {
|
|
$class = ltrim($class, '\\');
|
|
$cache_key = 'autoload:' . $class;
|
|
$wdReal = rtrim((string) realpath(WD), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
$modulesRoot = $wdReal . 'modules' . DIRECTORY_SEPARATOR;
|
|
$isPathInside = static function (string $path, string $root): bool {
|
|
$resolved = realpath($path);
|
|
if ($resolved === false) {
|
|
return false;
|
|
}
|
|
|
|
$resolved = rtrim($resolved, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
return str_starts_with($resolved, $root);
|
|
};
|
|
$is_loaded = static function (string $candidate): bool {
|
|
return class_exists($candidate, false)
|
|
|| interface_exists($candidate, false)
|
|
|| trait_exists($candidate, false)
|
|
|| (function_exists('enum_exists') && enum_exists($candidate, false));
|
|
};
|
|
|
|
// Check Redis cache first
|
|
if (defined('redis')) {
|
|
try {
|
|
$cached = redis->get($cache_key);
|
|
if (is_string($cached) && $cached !== '' && is_file($cached)) {
|
|
if ($isPathInside($cached, $wdReal)) {
|
|
require_once $cached;
|
|
if ($is_loaded($class)) {
|
|
return;
|
|
}
|
|
}
|
|
// Stale, invalid, or unsafe class mapping in cache; continue with normal lookup.
|
|
redis->delete($cache_key);
|
|
} elseif (is_string($cached) && $cached !== '') {
|
|
// Remove non-existing cached path to avoid repeated failed lookups.
|
|
redis->delete($cache_key);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Fall back to manual search on Redis error
|
|
}
|
|
}
|
|
|
|
$parts = explode('\\', $class);
|
|
$top = strtolower($parts[0] ?? '');
|
|
$relative = implode(DIRECTORY_SEPARATOR, array_slice($parts, 1));
|
|
|
|
$base = WD . DIRECTORY_SEPARATOR;
|
|
$candidates = [];
|
|
|
|
// 1. Core folders: classes, interfaces, traits, objects, statistics
|
|
$core_folders = ['classes', 'interfaces', 'traits', 'objects', 'statistics'];
|
|
if (in_array($top, $core_folders)) {
|
|
if ($top === 'classes' && str_starts_with(strtolower($relative), 'edge_gateway_')) {
|
|
$candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . 'edgegateway' . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . $relative;
|
|
}
|
|
$candidates[] = $base . $top . DIRECTORY_SEPARATOR . $relative;
|
|
}
|
|
// 2. Modules folder: explicitly starting with 'modules'
|
|
elseif ($top === 'modules') {
|
|
$candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . $relative;
|
|
}
|
|
// 3. Fallback for module-specific classes without 'modules' prefix or nested in modules
|
|
else {
|
|
// Try directly under modules (e.g. bird\bird_c -> modules/bird/bird_c.php)
|
|
$candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class);
|
|
|
|
// Try inside each module subfolder (e.g. customers\economicCustomers -> modules/economic/customers/economicCustomers.php)
|
|
static $module_dirs = null;
|
|
if ($module_dirs === null) {
|
|
if (defined('redis')) {
|
|
try {
|
|
$module_dirs = redis->get_array('autoload:module_dirs');
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
|
|
if (is_array($module_dirs)) {
|
|
$module_dirs = array_values(array_filter($module_dirs, static function ($item) use ($base, $modulesRoot, $isPathInside): bool {
|
|
if (!is_string($item) || $item === '' || str_contains($item, DIRECTORY_SEPARATOR) || str_contains($item, '..')) {
|
|
return false;
|
|
}
|
|
|
|
$candidate = $base . 'modules' . DIRECTORY_SEPARATOR . $item;
|
|
return is_dir($candidate) && $isPathInside($candidate, $modulesRoot);
|
|
}));
|
|
}
|
|
|
|
if ($module_dirs === null) {
|
|
$module_dirs = array_filter(scandir($base . 'modules'), function($item) use ($base) {
|
|
return $item !== '.' && $item !== '..' && is_dir($base . 'modules' . DIRECTORY_SEPARATOR . $item);
|
|
});
|
|
if (defined('redis')) {
|
|
try {
|
|
redis->set_array('autoload:module_dirs', $module_dirs);
|
|
redis->expire('autoload:module_dirs', 3600); // Cache for 1 hour
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
}
|
|
}
|
|
foreach ($module_dirs as $mod) {
|
|
$candidates[] = $base . 'modules' . DIRECTORY_SEPARATOR . $mod . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class);
|
|
}
|
|
}
|
|
|
|
$suffixes = ['', '_t', '_o', '_s', '_i', '_c', '_m'];
|
|
foreach ($candidates as $path) {
|
|
if (empty($path)) continue;
|
|
foreach ($suffixes as $suffix) {
|
|
$file = $path . $suffix . '.php';
|
|
if (is_file($file)) {
|
|
require_once $file;
|
|
if ($is_loaded($class)) {
|
|
if (defined('redis')) {
|
|
try {
|
|
redis->setEx($cache_key, $file, 86400); // Cache for 24 hours
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
use classes\application_write_freeze;
|
|
use classes\db;
|
|
use classes\replication_bootstrap_config;
|
|
use classes\replication_manager;
|
|
use classes\release_manager;
|
|
use classes\redis;
|
|
use classes\request;
|
|
use classes\response;
|
|
use classes\router;
|
|
|
|
// Start the session
|
|
$response = new response();
|
|
$request = new request();
|
|
$db = new db($CONFIG_DB);
|
|
|
|
// Ensure Redis is connected
|
|
if (!defined('redis')) {
|
|
try {
|
|
define("redis", (new redis())->connect());
|
|
} catch (Exception $e) {
|
|
$response->error($e->getMessage(), 500);
|
|
}
|
|
}
|
|
|
|
// Connect to the database, and ensure the connection is successful
|
|
try {
|
|
$db->connect();
|
|
} catch (Exception $e) {
|
|
$response->error($e->getMessage(), 500);
|
|
}
|
|
|
|
try {
|
|
release_manager::initializeRequestContext();
|
|
$releaseIngressPath = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH) ?: '');
|
|
release_manager::normalizeReleaseApiIngressPath(
|
|
preg_match('#^/[A-Za-z0-9_-]{1,64}/api(?:/|$)#', $releaseIngressPath) === 1
|
|
? (new release_manager())->enabledReleaseChannelSlugs()
|
|
: []
|
|
);
|
|
} catch (Throwable $e) {
|
|
error_log('[release-manager] Could not initialize request context or normalize ingress path: ' . $e->getMessage());
|
|
}
|
|
|
|
$router = new router();
|
|
|
|
try {
|
|
$replicationBootstrapSnapshotForRequest = replication_bootstrap_config::loadSnapshot();
|
|
$pendingStartupFailovers = is_array($replicationBootstrapSnapshotForRequest['pending_failovers'] ?? null)
|
|
? $replicationBootstrapSnapshotForRequest['pending_failovers']
|
|
: [];
|
|
if ($pendingStartupFailovers !== []) {
|
|
(new replication_manager())->syncStartupFailoversFromSnapshot();
|
|
}
|
|
} catch (Throwable $e) {
|
|
error_log('[replication-bootstrap] Could not sync startup failover metadata: ' . $e->getMessage());
|
|
}
|
|
|
|
if (application_write_freeze::shouldBlock(
|
|
$_SERVER['REQUEST_METHOD'] ?? 'GET',
|
|
$_SERVER['REQUEST_URI'] ?? '/',
|
|
php_sapi_name() === 'cli' || isset($_GET['internalCronCall'])
|
|
)) {
|
|
$freezeState = application_write_freeze::state();
|
|
if (php_sapi_name() === 'cli') {
|
|
fwrite(STDERR, 'Application writes are frozen: ' . (string)($freezeState['reason'] ?? 'replication promotion') . PHP_EOL);
|
|
exit(75);
|
|
}
|
|
|
|
$response->error([
|
|
'message' => 'Application writes are temporarily frozen.',
|
|
'reason' => $freezeState['reason'] ?? null,
|
|
'expires_at' => $freezeState['expires_at'] ?? null,
|
|
], 503);
|
|
}
|
|
|
|
// If the program was called from the command line, run the cli script
|
|
if (php_sapi_name() === 'cli' || isset($_GET['internalCronCall'])) {
|
|
require_once 'cli.php';
|
|
exit;
|
|
}
|
|
|
|
// If the route ends with a MIME type, then require the file_server.php
|
|
if ((preg_match('/\.(jpg|jpeg|png)$/', $_SERVER['REQUEST_URI']) || str_contains($_SERVER['REQUEST_URI'], '/files/'))) {
|
|
require_once 'file_server.php';
|
|
exit;
|
|
}
|
|
|
|
// Load enabled module routes before the global route scan.
|
|
$load_enabled_module_routes = static function (): void {
|
|
$modules_path = WD . DIRECTORY_SEPARATOR . 'modules';
|
|
if (!is_dir($modules_path)) {
|
|
return;
|
|
}
|
|
|
|
$module_dirs = array_filter(scandir($modules_path), static function (string $item) use ($modules_path): bool {
|
|
return $item !== '.' && $item !== '..' && is_dir($modules_path . DIRECTORY_SEPARATOR . $item);
|
|
});
|
|
|
|
foreach ($module_dirs as $module_dir) {
|
|
$routes_path = $modules_path . DIRECTORY_SEPARATOR . $module_dir . DIRECTORY_SEPARATOR . 'routes';
|
|
if (!is_dir($routes_path)) {
|
|
continue;
|
|
}
|
|
|
|
$module_class = 'classes\\' . $module_dir;
|
|
if (!class_exists($module_class)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$module = new $module_class();
|
|
if (method_exists($module, 'isEnabled') && !$module->isEnabled()) {
|
|
continue;
|
|
}
|
|
} catch (\Throwable $exception) {
|
|
continue;
|
|
}
|
|
|
|
foreach (scandir($routes_path) as $file) {
|
|
if ($file === '.' || $file === '..') {
|
|
continue;
|
|
}
|
|
require_once $routes_path . DIRECTORY_SEPARATOR . $file;
|
|
}
|
|
}
|
|
};
|
|
|
|
$load_enabled_module_routes();
|
|
|
|
// Autoload all the routes
|
|
/** @var router $router */
|
|
$router->auto_load_routes(WD . '/routes');
|