bootstrap.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <?php
  2. /**
  3. * Application bootstrap. Every public entry script includes this first.
  4. */
  5. declare(strict_types=1);
  6. define('APP_ROOT', dirname(__DIR__));
  7. define('DATA_DIR', APP_ROOT . '/data');
  8. define('MEDIA_DIR', APP_ROOT . '/media');
  9. define('CONFIG_DIR', APP_ROOT . '/config');
  10. if (!is_file(CONFIG_DIR . '/config.php')) {
  11. http_response_code(500);
  12. exit('Missing config/config.php — copy config/config.sample.php and adjust it.');
  13. }
  14. $GLOBALS['config'] = require CONFIG_DIR . '/config.php';
  15. // APP_VERSION. Its own file because the release build script rewrites it and
  16. // the manage client reads it back — see app/version.php.
  17. require APP_ROOT . '/app/version.php';
  18. date_default_timezone_set(config('site.timezone', 'UTC'));
  19. require APP_ROOT . '/app/storage.php';
  20. require APP_ROOT . '/app/csrf.php';
  21. require APP_ROOT . '/app/auth.php';
  22. require APP_ROOT . '/app/exif.php';
  23. require APP_ROOT . '/app/s3.php';
  24. require APP_ROOT . '/app/zip.php';
  25. require APP_ROOT . '/app/archive.php';
  26. require APP_ROOT . '/app/migrate.php';
  27. require APP_ROOT . '/app/markdown.php';
  28. require APP_ROOT . '/app/partials.php';
  29. require APP_ROOT . '/app/manage.php';
  30. require APP_ROOT . '/app/cron.php';
  31. /**
  32. * Read a config value by dot path, e.g. config('s3.bucket').
  33. */
  34. function config(string $path, mixed $default = null): mixed
  35. {
  36. $value = $GLOBALS['config'];
  37. foreach (explode('.', $path) as $part) {
  38. if (!is_array($value) || !array_key_exists($part, $value)) {
  39. return $default;
  40. }
  41. $value = $value[$part];
  42. }
  43. return $value;
  44. }
  45. /**
  46. * Prefix a link or asset path with the way back to the site root, so the shared
  47. * public partials work from any depth. Pages in the document root need no
  48. * prefix; pages in a subfolder (gallery/) define SITE_BASE as '../' before
  49. * including this file. Relative rather than absolute, because the app may be
  50. * installed in a subdirectory of the domain.
  51. */
  52. function base(string $path = ''): string
  53. {
  54. return (defined('SITE_BASE') ? SITE_BASE : '') . $path;
  55. }
  56. /** HTML-escape for output. */
  57. function e(?string $s): string
  58. {
  59. return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
  60. }
  61. /**
  62. * Byte count as something a client can judge at a glance. Gallery archives run
  63. * to gigabytes, so the size belongs on the download button itself.
  64. */
  65. function human_bytes(int $bytes): string
  66. {
  67. $units = ['B', 'KB', 'MB', 'GB', 'TB'];
  68. $i = 0;
  69. $value = (float)$bytes;
  70. while ($value >= 1024 && $i < count($units) - 1) {
  71. $value /= 1024;
  72. $i++;
  73. }
  74. return ($value >= 10 || $i === 0 ? round($value) : round($value, 1)) . ' ' . $units[$i];
  75. }
  76. /** Start the session with hardened cookie settings (idempotent). */
  77. function session_boot(): void
  78. {
  79. if (session_status() === PHP_SESSION_ACTIVE) {
  80. return;
  81. }
  82. session_set_cookie_params([
  83. 'lifetime' => 0,
  84. 'path' => '/',
  85. 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
  86. 'httponly' => true,
  87. 'samesite' => 'Lax',
  88. ]);
  89. session_name('fpsid');
  90. session_start();
  91. }
  92. /**
  93. * URL of a script in this installation, for the server to call itself.
  94. *
  95. * site.base_url is preferred when it has been filled in, because it does not
  96. * depend on the request's Host header. Otherwise the URL is derived from the
  97. * current request, mapping APP_ROOT against DOCUMENT_ROOT so an app installed
  98. * in a subdirectory still resolves. Null means the URL cannot be determined —
  99. * the caller then has to do the work inline instead of dispatching it.
  100. */
  101. function self_url(string $pathAndQuery): ?string
  102. {
  103. $configured = rtrim((string)config('site.base_url', ''), '/');
  104. if ($configured !== '' && !str_contains($configured, 'example.com')) {
  105. return $configured . $pathAndQuery;
  106. }
  107. $host = (string)($_SERVER['HTTP_HOST'] ?? '');
  108. $root = rtrim(str_replace('\\', '/', (string)($_SERVER['DOCUMENT_ROOT'] ?? '')), '/');
  109. $app = str_replace('\\', '/', APP_ROOT);
  110. if ($host === '' || $root === '' || !str_starts_with($app, $root)) {
  111. return null;
  112. }
  113. $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
  114. return $scheme . '://' . $host . rtrim(substr($app, strlen($root)), '/') . $pathAndQuery;
  115. }
  116. /**
  117. * Fire a request at one of this installation's own background scripts and hang
  118. * up without waiting for it. Those scripts set ignore_user_abort(), so they run
  119. * on regardless.
  120. *
  121. * A timeout is the expected, successful outcome: it means the request was
  122. * delivered and the script is busy with it. A *completed* response only counts
  123. * as success if it is the script's own 204 — anything else (a 404 from a wrong
  124. * key or a misconfigured base URL) means nothing is running, and saying so lets
  125. * the caller fall back to doing the work inline instead of silently stalling.
  126. */
  127. function self_dispatch(?string $url, int $timeoutMs = 1000): bool
  128. {
  129. if ($url === null || !function_exists('curl_init')) {
  130. return false;
  131. }
  132. $ch = curl_init($url);
  133. curl_setopt_array($ch, [
  134. CURLOPT_RETURNTRANSFER => true,
  135. CURLOPT_NOSIGNAL => true,
  136. CURLOPT_CONNECTTIMEOUT_MS => $timeoutMs,
  137. CURLOPT_TIMEOUT_MS => $timeoutMs,
  138. ]);
  139. curl_exec($ch);
  140. $errno = curl_errno($ch);
  141. $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  142. return $errno === CURLE_OPERATION_TIMEOUTED || ($errno === 0 && $status === 204);
  143. }
  144. /** Redirect and stop. */
  145. function redirect(string $url): never
  146. {
  147. header('Location: ' . $url);
  148. exit;
  149. }
  150. /** Send a JSON response and stop (used by admin/api.php). */
  151. function json_response(array $payload, int $status = 200): never
  152. {
  153. http_response_code($status);
  154. header('Content-Type: application/json; charset=utf-8');
  155. echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  156. exit;
  157. }