bootstrap.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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. /**
  30. * Read a config value by dot path, e.g. config('s3.bucket').
  31. */
  32. function config(string $path, mixed $default = null): mixed
  33. {
  34. $value = $GLOBALS['config'];
  35. foreach (explode('.', $path) as $part) {
  36. if (!is_array($value) || !array_key_exists($part, $value)) {
  37. return $default;
  38. }
  39. $value = $value[$part];
  40. }
  41. return $value;
  42. }
  43. /**
  44. * Prefix a link or asset path with the way back to the site root, so the shared
  45. * public partials work from any depth. Pages in the document root need no
  46. * prefix; pages in a subfolder (gallery/) define SITE_BASE as '../' before
  47. * including this file. Relative rather than absolute, because the app may be
  48. * installed in a subdirectory of the domain.
  49. */
  50. function base(string $path = ''): string
  51. {
  52. return (defined('SITE_BASE') ? SITE_BASE : '') . $path;
  53. }
  54. /** HTML-escape for output. */
  55. function e(?string $s): string
  56. {
  57. return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
  58. }
  59. /**
  60. * Byte count as something a client can judge at a glance. Gallery archives run
  61. * to gigabytes, so the size belongs on the download button itself.
  62. */
  63. function human_bytes(int $bytes): string
  64. {
  65. $units = ['B', 'KB', 'MB', 'GB', 'TB'];
  66. $i = 0;
  67. $value = (float)$bytes;
  68. while ($value >= 1024 && $i < count($units) - 1) {
  69. $value /= 1024;
  70. $i++;
  71. }
  72. return ($value >= 10 || $i === 0 ? round($value) : round($value, 1)) . ' ' . $units[$i];
  73. }
  74. /** Start the session with hardened cookie settings (idempotent). */
  75. function session_boot(): void
  76. {
  77. if (session_status() === PHP_SESSION_ACTIVE) {
  78. return;
  79. }
  80. session_set_cookie_params([
  81. 'lifetime' => 0,
  82. 'path' => '/',
  83. 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
  84. 'httponly' => true,
  85. 'samesite' => 'Lax',
  86. ]);
  87. session_name('fpsid');
  88. session_start();
  89. }
  90. /** Redirect and stop. */
  91. function redirect(string $url): never
  92. {
  93. header('Location: ' . $url);
  94. exit;
  95. }
  96. /** Send a JSON response and stop (used by admin/api.php). */
  97. function json_response(array $payload, int $status = 200): never
  98. {
  99. http_response_code($status);
  100. header('Content-Type: application/json; charset=utf-8');
  101. echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  102. exit;
  103. }