Files
Grav/index.php
Andy Miller b7ae93bfc1 Add fast static asset serving for plugin-bundled SPA apps
Reads user/config/plugin-asset-map.php to map URL prefixes to physical
directories. Matching requests are served with readfile() and exit
before Grav boots, avoiding the full framework overhead for static
JS, CSS, fonts, and images.
2026-04-01 17:33:25 -06:00

173 lines
6.5 KiB
PHP

<?php
/**
* @package Grav.Core
*
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav;
\define('GRAV_REQUEST_TIME', microtime(true));
\define('GRAV_PHP_MIN', '8.3.0');
if (PHP_SAPI === 'cli-server') {
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
}
}
if (PHP_SAPI !== 'cli') {
if (!isset($_SERVER['argv']) && !ini_get('register_argc_argv')) {
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$_SERVER['argv'] = $queryString !== '' ? [$queryString] : [];
$_SERVER['argc'] = $queryString !== '' ? 1 : 0;
}
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$path = parse_url($requestUri, PHP_URL_PATH) ?? '/';
$path = str_replace('\\', '/', $path);
$scriptDir = str_replace('\\', '/', dirname($scriptName));
if ($scriptDir && $scriptDir !== '/' && $scriptDir !== '.') {
if (strpos($path, $scriptDir) === 0) {
$path = substr($path, strlen($scriptDir));
$path = $path === '' ? '/' : $path;
}
}
// Fast static asset serving for plugins that bundle SPA apps.
// Checks for a plugin-asset-map.php file that maps route prefixes to
// physical directories, serving files directly without booting Grav.
$assetMapFile = __DIR__ . '/user/config/plugin-asset-map.php';
if (is_file($assetMapFile)) {
$assetMap = require $assetMapFile;
foreach ($assetMap as $routePrefix => $diskPath) {
if (str_starts_with($path, $routePrefix)) {
$relPath = substr($path, strlen($routePrefix));
$filePath = __DIR__ . '/' . ltrim($diskPath, '/') . $relPath;
$realFile = realpath($filePath);
$realBase = realpath(__DIR__ . '/' . ltrim($diskPath, '/'));
if ($realFile && $realBase && str_starts_with($realFile, $realBase) && is_file($realFile)) {
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
$mimeMap = [
'js' => 'text/javascript', 'mjs' => 'text/javascript',
'css' => 'text/css', 'json' => 'application/json',
'svg' => 'image/svg+xml', 'png' => 'image/png',
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
'webp' => 'image/webp', 'avif' => 'image/avif',
'woff2' => 'font/woff2', 'woff' => 'font/woff',
'ico' => 'image/x-icon',
];
header('Content-Type: ' . ($mimeMap[$ext] ?? 'application/octet-stream'));
header('Content-Length: ' . filesize($realFile));
header('Cache-Control: ' . (str_contains($relPath, '/immutable/') ? 'public, max-age=31536000, immutable' : 'public, max-age=3600'));
readfile($realFile);
exit;
}
}
}
}
if ($path === '/___safe-upgrade-status') {
$statusEndpoint = __DIR__ . '/user/plugins/admin/safe-upgrade-status.php';
if (!\defined('GRAV_ROOT')) {
// Minimal bootstrap so the status script has the expected constants.
require_once __DIR__ . '/system/defines.php';
}
header('Content-Type: application/json; charset=utf-8');
if (is_file($statusEndpoint)) {
require $statusEndpoint;
} else {
http_response_code(404);
echo json_encode([
'status' => 'error',
'message' => 'Safe upgrade status endpoint unavailable.',
]);
}
exit;
}
}
// Maintenance mode during core upgrade
if (file_exists(__DIR__ . '/.upgrading')) {
if (time() - filemtime(__DIR__ . '/.upgrading') > 300) {
@unlink(__DIR__ . '/.upgrading'); // Stale flag (>5 min), remove it
} else {
http_response_code(503);
header('Retry-After: 60');
echo '<!DOCTYPE html><html><head><title>Upgrading</title></head>';
echo '<body><h1>Site Upgrading</h1><p>Please try again in a moment.</p></body></html>';
exit;
}
}
// Ensure vendor libraries exist
$autoload = __DIR__ . '/vendor/autoload.php';
if (!is_file($autoload)) {
die('Please run: <i>bin/grav install</i>');
}
// Register the auto-loader.
$loader = require $autoload;
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());
// Set internal encoding.
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
// Use getcwd() for paths to support symlinked index.php (GRAV_ROOT uses getcwd())
$gravRoot = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', getenv('GRAV_ROOT') ?: getcwd()), '/');
// Helper function to check if recovery mode is enabled in config (updates.recovery_mode)
$isRecoveryEnabled = static function () use ($gravRoot) {
$userConfig = $gravRoot . '/user/config/system.yaml';
if (!is_file($userConfig)) {
return true; // Default enabled
}
$content = file_get_contents($userConfig);
if ($content === false) {
return true;
}
if (preg_match('/^\s*updates:\s*\n(?:\s+\w+:.*\n)*?\s+recovery_mode:\s*(true|false|1|0)\s*$/m', $content, $matches)) {
return in_array(strtolower($matches[1]), ['true', '1'], true);
}
return true; // Default enabled
};
$recoveryFlag = $gravRoot . '/user/data/recovery.flag';
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag) && $isRecoveryEnabled()) {
if (!defined('GRAV_ROOT')) {
define('GRAV_ROOT', $gravRoot);
}
require __DIR__ . '/system/recovery.php';
return 0;
}
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;
// Get the Grav instance
$grav = Grav::instance(['loader' => $loader]);
// Process the page
try {
$grav->process();
} catch (\Error|\Exception $e) {
$grav->fireEvent('onFatalException', new Event(['exception' => $e]));
if (PHP_SAPI !== 'cli' && is_file($recoveryFlag) && $isRecoveryEnabled()) {
require __DIR__ . '/system/recovery.php';
return 0;
}
throw $e;
}