settings.php

main 224 lines · 5.9 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Settings stored in the database (znote_config), so the admin panel can
4 * change them without config.php having to be writable.
5 *
6 * config.php stays the place for things an admin edits by hand (database
7 * credentials, server engine, vocations). This is for things the panel writes.
8 *
9 * The whole table is read once per request and kept in memory - it is a
10 * handful of short rows, so one query covers every lookup on the page.
11 */
12
13/**
14 * Does this table exist?
15 *
16 * SHOW TABLES is the only honest answer. Selecting a row and treating an empty
17 * result as "missing" gets it wrong on every fresh install, where the table is
18 * there and simply has nothing in it yet.
19 */
20function znote_table_exists(string $table): bool {
21 static $known = array();
22
23 if (isset($known[$table])) {
24 return $known[$table];
25 }
26
27 $db = db();
28 $escaped = $db->connection()->real_escape_string($table);
29 return $known[$table] = ($db->rawFetchAll("SHOW TABLES LIKE '{$escaped}';") !== false);
30}
31
32function znote_column_exists(string $table, string $column): bool {
33 static $known = array();
34
35 $cacheKey = $table . '.' . $column;
36
37 if (isset($known[$cacheKey])) {
38 return $known[$cacheKey];
39 }
40
41 if (!znote_table_exists($table)) {
42 return $known[$cacheKey] = false;
43 }
44
45 if (!preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
46 return $known[$cacheKey] = false;
47 }
48
49 $escaped = db()->connection()->real_escape_string($column);
50 return $known[$cacheKey] = (db()->rawFetchOne("SHOW COLUMNS FROM `{$table}` LIKE '{$escaped}';") !== false);
51}
52
53function znote_settings_all(bool $refresh = false): array {
54 static $settings = null;
55
56 if ($settings !== null && !$refresh) {
57 return $settings;
58 }
59
60 $settings = array();
61
62 $rows = db()->fetchAll("SELECT `key`, `value` FROM `znote_config`;");
63 if (is_array($rows)) {
64 foreach ($rows as $row) {
65 $settings[(string)$row['key']] = (string)$row['value'];
66 }
67 }
68 // A false result means the table is missing (migration not run yet).
69 // Callers fall back to their defaults, so the site keeps working.
70
71 return $settings;
72}
73
74function setting(string $key, ?string $default = null): ?string {
75 $settings = znote_settings_all();
76 return array_key_exists($key, $settings) ? $settings[$key] : $default;
77}
78
79function setting_set(string $key, string $value): bool {
80 $ok = db()->execute("
81 INSERT INTO `znote_config` (`key`, `value`)
82 VALUES (?, ?)
83 ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
84 ", [$key, $value]);
85
86 if ($ok !== false) {
87 znote_settings_all(true); // drop the in-memory copy
88 return true;
89 }
90
91 return false;
92}
93
94/**
95 * Apply the settings saved from the admin panel over $config.
96 *
97 * config.php keeps every default; anything an admin has changed in
98 * Admin Panel > Settings is stored as "config:<key>" and wins here. That way
99 * updating config.php never loses a setting, and a key nobody has touched in
100 * the panel keeps following the file.
101 *
102 * Booleans and integers are cast back, because znote_config stores strings.
103 */
104function znote_apply_settings(): void {
105 global $config;
106
107 $settings = znote_settings_all();
108 if (!$settings) {
109 return;
110 }
111
112 foreach ($settings as $key => $value) {
113 if (strpos($key, 'config:') !== 0) {
114 continue;
115 }
116
117 $name = substr($key, 7);
118 if ($name === '') {
119 continue;
120 }
121
122 znote_setting_apply($config, explode('.', $name), $value);
123 }
124}
125
126/**
127 * Write one value into $config at $path, keeping the type config.php declared.
128 *
129 * Nested paths are stored dotted - "config:shop.enabled" reaches
130 * $config['shop']['enabled'] - because most of what the panel edits lives one
131 * or two levels down. A path is only followed while it already exists, so a
132 * stale row from a removed setting can never invent a key.
133 */
134function znote_setting_apply(array &$config, array $path, string $value): void {
135 $leaf = array_pop($path);
136 $node = &$config;
137
138 foreach ($path as $step) {
139 if (!is_array($node) || !array_key_exists($step, $node) || !is_array($node[$step])) {
140 return;
141 }
142 $node = &$node[$step];
143 }
144
145 if (!is_array($node) || !array_key_exists($leaf, $node)) {
146 return;
147 }
148
149 if (is_array($node[$leaf])) {
150 // Whole lists - the shop price tiers, for instance - are stored as JSON,
151 // since one znote_config row holds a string. A malformed row is ignored
152 // so config.php keeps providing the list.
153 $decoded = json_decode($value, true);
154 if (is_array($decoded)) {
155 $node[$leaf] = $decoded;
156 }
157 return;
158 }
159
160 if (is_bool($node[$leaf])) {
161 $node[$leaf] = ($value !== '' && $value !== '0');
162 } elseif (is_int($node[$leaf])) {
163 $node[$leaf] = (int)$value;
164 } elseif (is_float($node[$leaf])) {
165 $node[$leaf] = (float)$value;
166 } else {
167 $node[$leaf] = $value;
168 }
169}
170
171/** Read one dotted path out of $config, or $default when it is not there. */
172function znote_config_path(array $config, string $path, $default = null) {
173 $node = $config;
174
175 foreach (explode('.', $path) as $step) {
176 if (!is_array($node) || !array_key_exists($step, $node)) {
177 return $default;
178 }
179 $node = $node[$step];
180 }
181
182 return $node;
183}
184
185/**
186 * Players-online record.
187 *
188 * Kept in znote_config rather than a table of its own: it is two integers, and
189 * znote_config already exists for exactly this kind of thing.
190 *
191 * znote_record_update() is called from the pages that already know the current
192 * online count, so this costs no extra query on a normal page load.
193 */
194function znote_record_get(): array {
195 return array(
196 'players' => (int)setting('record:players', '0'),
197 'time' => (int)setting('record:time', '0'),
198 );
199}
200
201/**
202 * Store $online as the new record if it beats the stored one.
203 * Returns true when a new record was set.
204 */
205function znote_record_update(int $online): bool {
206 if ($online <= 0) {
207 return false;
208 }
209
210 $record = znote_record_get();
211 if ($online <= $record['players']) {
212 return false;
213 }
214
215 setting_set('record:players', (string)$online);
216 setting_set('record:time', (string)time());
217
218 if (function_exists('znote_hook')) {
219 znote_hook('server.online_record', array('players' => $online));
220 }
221
222 return true;
223}
224
Top