landing.php

main 133 lines · 3.2 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3const ZNOTE_LANDING_DIR = 'landing';
4
5function landing_root(): string
6{
7 return dirname(__DIR__, 2) . '/' . ZNOTE_LANDING_DIR;
8}
9
10function landing_enabled(): bool
11{
12 return function_exists('setting') && (string)setting('landing.enabled', '') === '1';
13}
14
15/** The file inside landing/ that is served. */
16function landing_file(): string
17{
18 $file = function_exists('setting') ? (string)setting('landing.file', '') : '';
19 $file = trim($file);
20
21 return ($file !== '' && landing_is_valid_file($file)) ? $file : 'index.html';
22}
23
24/** One path segment, .html or .php, never escaping landing/. */
25function landing_is_valid_file(string $file): bool
26{
27 if (strpos($file, '/') !== false || strpos($file, '\\') !== false || strpos($file, '..') !== false) {
28 return false;
29 }
30
31 return (bool)preg_match('/^[A-Za-z0-9._-]{1,64}\.(html?|php)$/', $file);
32}
33
34/** Every page landing/ offers, for the admin picker. */
35function landing_available_files(): array
36{
37 $files = array();
38
39 foreach (glob(landing_root() . '/*.{html,htm,php}', GLOB_BRACE) ?: array() as $path) {
40 $name = basename($path);
41 if (landing_is_valid_file($name)) {
42 $files[] = $name;
43 }
44 }
45
46 sort($files);
47
48 return $files;
49}
50
51function landing_path(): string
52{
53 return landing_root() . '/' . landing_file();
54}
55
56function landing_ready(): bool
57{
58 return landing_enabled() && is_file(landing_path());
59}
60
61/**
62 * The landing folder's URL, worked out from the running script so this keeps
63 * working when ZnoteX lives in a subdirectory.
64 */
65function landing_base_url(): string
66{
67 $dir = str_replace('\\', '/', dirname((string)($_SERVER['SCRIPT_NAME'] ?? '/')));
68 $dir = ($dir === '/' || $dir === '.') ? '' : rtrim($dir, '/');
69
70 return $dir . '/' . ZNOTE_LANDING_DIR . '/';
71}
72
73/**
74 * A visitor who asked for the real site, remembered for the rest of the visit
75 * so every link back to the front page does not drop them on the landing page
76 * again.
77 */
78function landing_bypassed(): bool
79{
80 if (isset($_GET['site'])) {
81 $_SESSION['landing_bypass'] = true;
82 return true;
83 }
84
85 return !empty($_SESSION['landing_bypass']);
86}
87
88/**
89 * Serve the landing page in place of the front page, and stop. Does nothing
90 * unless the feature is on, the file is there, and the visitor has not asked
91 * for the real site with ?site=1.
92 */
93function landing_serve(): void
94{
95 if (!landing_ready() || landing_bypassed()) {
96 return;
97 }
98
99 $path = landing_path();
100
101 if (strtolower((string)pathinfo($path, PATHINFO_EXTENSION)) === 'php') {
102 ob_start();
103 include $path;
104 $html = (string)ob_get_clean();
105 } else {
106 $html = (string)file_get_contents($path);
107 }
108
109 echo landing_apply_base($html);
110 exit;
111}
112
113/**
114 * The page is served from the site root, so its own relative css/, img/ and js/
115 * would resolve one folder too high. A <base> fixes every one of them at once.
116 * Links that must leave the landing page use a leading slash and are unaffected.
117 */
118function landing_apply_base(string $html): string
119{
120 if (stripos($html, '<base ') !== false) {
121 return $html;
122 }
123
124 $base = '<base href="' . htmlspecialchars(landing_base_url(), ENT_QUOTES, 'UTF-8') . '">';
125
126 if (preg_match('~<head\b[^>]*>~i', $html, $m, PREG_OFFSET_CAPTURE)) {
127 $at = $m[0][1] + strlen($m[0][0]);
128 return substr($html, 0, $at) . "\n\t" . $base . substr($html, $at);
129 }
130
131 return $base . $html;
132}
133
Top