connect.php

main 511 lines · 11.9 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3$time = time();
4if (!isset($version)) {
5 $version = (string)require dirname(__DIR__) . '/version.php';
6}
7
8if (!isset($GLOBALS['__znote_start_time'])) {
9 $GLOBALS['__znote_start_time'] = microtime(true);
10}
11
12if (!function_exists('elapsedTime')) {
13 function elapsedTime(): float {
14 return round(microtime(true) - $GLOBALS['__znote_start_time'], 4);
15 }
16}
17
18if (!function_exists('znote_database_wait_screen')) {
19 function znote_database_error_reference(): string {
20 try {
21 return strtoupper(bin2hex(random_bytes(6)));
22 } catch (Throwable $e) {
23 return strtoupper(substr(hash('sha256', microtime(true) . '|' . mt_rand()), 0, 12));
24 }
25 }
26
27 function znote_database_request_context(): string {
28 if (PHP_SAPI === 'cli') {
29 return 'cli';
30 }
31
32 $method = (string)($_SERVER['REQUEST_METHOD'] ?? 'GET');
33 $uri = (string)($_SERVER['REQUEST_URI'] ?? '');
34 $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '');
35
36 return trim($method . ' ' . $uri . ($ip !== '' ? ' ip=' . $ip : ''));
37 }
38
39 function znote_database_sql_preview(?string $sql): string {
40 if ($sql === null || $sql === '') {
41 return '';
42 }
43
44 $preview = preg_replace('/\s+/', ' ', trim($sql)) ?? '';
45 if (strlen($preview) > 900) {
46 $preview = substr($preview, 0, 900) . '...';
47 }
48
49 return $preview;
50 }
51
52 function znote_database_log_error(string $type, string $message, ?string $sql = null, int|string $code = 0): string {
53 $reference = znote_database_error_reference();
54 $parts = array(
55 '[ZnoteX DB]',
56 'ref=' . $reference,
57 'type=' . $type,
58 'code=' . (string)$code,
59 'context=' . znote_database_request_context(),
60 'message=' . preg_replace('/\s+/', ' ', trim($message)),
61 );
62
63 $preview = znote_database_sql_preview($sql);
64 if ($preview !== '') {
65 $parts[] = 'sql=' . $preview;
66 }
67
68 error_log(implode(' | ', $parts));
69 return $reference;
70 }
71
72 function znote_database_wait_screen(int $errorCode, string $errorMessage): void {
73 global $config;
74
75 if (PHP_SAPI === 'cli') {
76 die("Failed to connect to MySQL: (" . $errorCode . ") " . $errorMessage . PHP_EOL);
77 }
78
79 $reference = znote_database_log_error('connection', $errorMessage, null, $errorCode);
80
81 if (!headers_sent()) {
82 http_response_code(503);
83 header('Retry-After: 5');
84 header('Content-Type: text/html; charset=UTF-8');
85 }
86
87 $siteTitle = htmlspecialchars((string)($config['site_title'] ?? 'ZnoteX'), ENT_QUOTES, 'UTF-8');
88 $safeReference = htmlspecialchars($reference, ENT_QUOTES, 'UTF-8');
89 $showDetails = !empty($config['security']['show_database_errors']);
90 $safeDetails = $showDetails
91 ? htmlspecialchars('MySQL ' . $errorCode . ': ' . $errorMessage, ENT_QUOTES, 'UTF-8')
92 : '';
93 ?>
94<!DOCTYPE html>
95<html lang="en">
96<head>
97 <meta charset="utf-8">
98 <meta name="viewport" content="width=device-width, initial-scale=1">
99 <meta http-equiv="refresh" content="5">
100 <title><?= $siteTitle ?> - Connecting</title>
101 <style>
102 :root {
103 color-scheme: dark;
104 --bg: #101319;
105 --panel: rgba(25, 31, 42, .86);
106 --text: #eef3fb;
107 --muted: #a9b4c5;
108 --line: rgba(255, 255, 255, .12);
109 --gold: #d9aa48;
110 --ember: #ef6f4f;
111 --blue: #6ea8ff;
112 }
113
114 * {
115 box-sizing: border-box;
116 }
117
118 body {
119 margin: 0;
120 min-height: 100vh;
121 display: grid;
122 place-items: center;
123 padding: 24px;
124 background:
125 radial-gradient(circle at 25% 20%, rgba(110, 168, 255, .16), transparent 34%),
126 radial-gradient(circle at 78% 72%, rgba(217, 170, 72, .14), transparent 34%),
127 linear-gradient(135deg, #101319 0%, #171b24 48%, #0f1218 100%);
128 color: var(--text);
129 font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
130 }
131
132 .db-wait {
133 width: min(560px, 100%);
134 padding: 38px 32px;
135 border: 1px solid var(--line);
136 border-radius: 8px;
137 background: var(--panel);
138 box-shadow: 0 24px 80px rgba(0, 0, 0, .38);
139 text-align: center;
140 backdrop-filter: blur(14px);
141 }
142
143 .db-orbit {
144 position: relative;
145 width: 94px;
146 height: 94px;
147 margin: 0 auto 26px;
148 border-radius: 50%;
149 background: conic-gradient(from 90deg, var(--gold), var(--ember), var(--blue), var(--gold));
150 animation: spin 1.25s linear infinite;
151 }
152
153 .db-orbit::before {
154 content: "";
155 position: absolute;
156 inset: 9px;
157 border-radius: inherit;
158 background: #151a22;
159 box-shadow: inset 0 0 24px rgba(255, 255, 255, .05);
160 }
161
162 .db-orbit::after {
163 content: "";
164 position: absolute;
165 top: 7px;
166 left: 50%;
167 width: 14px;
168 height: 14px;
169 border-radius: 50%;
170 background: #fff7d6;
171 box-shadow: 0 0 22px rgba(217, 170, 72, .92);
172 transform: translateX(-50%);
173 }
174
175 h1 {
176 margin: 0 0 12px;
177 font-size: clamp(26px, 4vw, 38px);
178 line-height: 1.12;
179 font-weight: 800;
180 letter-spacing: 0;
181 }
182
183 p {
184 margin: 0;
185 color: var(--muted);
186 }
187
188 .db-status {
189 display: inline-flex;
190 align-items: center;
191 gap: 10px;
192 margin-top: 24px;
193 padding: 10px 14px;
194 border: 1px solid var(--line);
195 border-radius: 999px;
196 background: rgba(255, 255, 255, .05);
197 color: #dce5f2;
198 font-size: 14px;
199 }
200
201 .db-pulse {
202 width: 9px;
203 height: 9px;
204 border-radius: 50%;
205 background: var(--gold);
206 box-shadow: 0 0 0 rgba(217, 170, 72, .7);
207 animation: pulse 1.45s ease-out infinite;
208 flex: 0 0 auto;
209 }
210
211 .db-details {
212 margin-top: 22px;
213 padding-top: 18px;
214 border-top: 1px solid var(--line);
215 color: #7f8a9d;
216 font-size: 13px;
217 word-break: break-word;
218 }
219
220 @keyframes spin {
221 to { transform: rotate(360deg); }
222 }
223
224 @keyframes pulse {
225 70% { box-shadow: 0 0 0 12px rgba(217, 170, 72, 0); }
226 100% { box-shadow: 0 0 0 0 rgba(217, 170, 72, 0); }
227 }
228
229 @media (max-width: 520px) {
230 body {
231 padding: 16px;
232 }
233
234 .db-wait {
235 padding: 30px 22px;
236 }
237 }
238 </style>
239</head>
240<body>
241 <main class="db-wait" role="status" aria-live="polite">
242 <div class="db-orbit" aria-hidden="true"></div>
243 <h1><?= $siteTitle ?></h1>
244 <p>The database connection is not ready yet.</p>
245 <div class="db-status"><span class="db-pulse" aria-hidden="true"></span>Retrying automatically in 5 seconds</div>
246 <div class="db-details">
247 Reference: <?= $safeReference ?>
248 <?php if ($safeDetails !== ''): ?><br><?= $safeDetails ?><?php endif; ?>
249 </div>
250 </main>
251</body>
252</html>
253 <?php
254 exit;
255 }
256}
257
258mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
259
260try {
261 $connect = new mysqli(
262 $config['sqlHost'],
263 $config['sqlUser'],
264 $config['sqlPassword'],
265 $config['sqlDatabase']
266 );
267 // Use the full UTF-8 character set for every request. This keeps accents,
268 // supplementary characters and emoji consistent regardless of the server's
269 // global MySQL/MariaDB defaults.
270 $connect->set_charset('utf8mb4');
271 $connect->query("SET collation_connection = 'utf8mb4_general_ci'");
272} catch (mysqli_sql_exception $e) {
273 znote_database_wait_screen($e->getCode(), $e->getMessage());
274}
275
276if ($connect->connect_errno) {
277 znote_database_wait_screen($connect->connect_errno, (string)$connect->connect_error);
278}
279
280if (!isset($aacQueries)) {
281 $aacQueries = 0;
282}
283if (!isset($accQueriesData)) {
284 $accQueriesData = [];
285}
286
287class ZnoteDatabase {
288 private mysqli $connection;
289
290 public function __construct(mysqli $connection) {
291 $this->connection = $connection;
292 }
293
294 public function connection(): mysqli {
295 return $this->connection;
296 }
297
298 public function fetchOne(string $sql, array $params = []): array|false {
299 $rows = $this->fetchAll($sql, $params);
300 return ($rows !== false && isset($rows[0])) ? $rows[0] : false;
301 }
302
303 public function fetchAll(string $sql, array $params = []): array|false {
304 $result = $this->query($sql, $params);
305 if (!($result instanceof mysqli_result)) {
306 return false;
307 }
308
309 $rows = [];
310 while ($row = $result->fetch_assoc()) {
311 $rows[] = $row;
312 }
313 $result->free();
314
315 return $rows ?: false;
316 }
317
318 public function execute(string $sql, array $params = []): bool {
319 $result = $this->query($sql, $params);
320 if ($result instanceof mysqli_result) {
321 $result->free();
322 }
323
324 return $result !== false;
325 }
326
327 public function insertId(): int|string {
328 return $this->connection->insert_id;
329 }
330
331 public function affectedRows(): int|string {
332 return $this->connection->affected_rows;
333 }
334
335 public function transaction(callable $callback): mixed {
336 try {
337 $this->connection->begin_transaction();
338 $result = $callback($this);
339
340 if ($result === false) {
341 $this->connection->rollback();
342 return false;
343 }
344
345 $this->connection->commit();
346 return $result;
347 } catch (Throwable $e) {
348 $this->connection->rollback();
349 znote_database_log_error('transaction', $e->getMessage(), null, (int)$e->getCode());
350 return false;
351 }
352 }
353
354 public function beginTransaction(): bool {
355 return $this->connection->begin_transaction();
356 }
357
358 public function commit(): bool {
359 return $this->connection->commit();
360 }
361
362 public function rollback(): bool {
363 return $this->connection->rollback();
364 }
365
366 public function rawFetchOne(string $sql): array|false {
367 $result = $this->rawQuery($sql);
368 if (!($result instanceof mysqli_result)) {
369 return false;
370 }
371
372 $row = $result->fetch_assoc();
373 $result->free();
374
375 return $row ?: false;
376 }
377
378 public function rawFetchAll(string $sql): array|false {
379 $result = $this->rawQuery($sql);
380 if (!($result instanceof mysqli_result)) {
381 return false;
382 }
383
384 $rows = [];
385 while ($row = $result->fetch_assoc()) {
386 $rows[] = $row;
387 }
388 $result->free();
389
390 return $rows ?: false;
391 }
392
393 public function rawExecute(string $sql): bool {
394 $result = $this->rawQuery($sql);
395 if ($result instanceof mysqli_result) {
396 $result->free();
397 }
398
399 return $result !== false;
400 }
401
402 private function query(string $sql, array $params = []): mysqli_result|bool {
403 $this->logQuery($sql, $params);
404
405 try {
406 if ($params === []) {
407 return $this->connection->query($sql);
408 }
409
410 $stmt = $this->connection->prepare($sql);
411 $this->bindParams($stmt, $params);
412 $stmt->execute();
413
414 $result = $this->statementResult($stmt);
415 $stmt->close();
416
417 return $result;
418 } catch (mysqli_sql_exception $e) {
419 znote_database_log_error('prepared-query', $e->getMessage(), $sql, (int)$e->getCode());
420 return false;
421 }
422 }
423
424 private function rawQuery(string $sql): mysqli_result|bool {
425 $this->logQuery($sql);
426
427 try {
428 return $this->connection->query($sql);
429 } catch (mysqli_sql_exception $e) {
430 znote_database_log_error('raw-query', $e->getMessage(), $sql, (int)$e->getCode());
431 return false;
432 }
433 }
434
435 private function logQuery(string $sql, array $params = []): void {
436 global $aacQueries, $accQueriesData;
437
438 $aacQueries++;
439 $accQueriesData[] = '[' . elapsedTime() . '] ' . $sql . ($params === [] ? '' : ' [prepared params: ' . count($params) . ']');
440 }
441
442 private function bindParams(mysqli_stmt $stmt, array $params): void {
443 $types = '';
444 $values = [];
445
446 foreach ($params as $param) {
447 if (is_int($param) || is_bool($param)) {
448 $types .= 'i';
449 $values[] = (int)$param;
450 } elseif (is_float($param)) {
451 $types .= 'd';
452 $values[] = $param;
453 } else {
454 $types .= 's';
455 $values[] = $param;
456 }
457 }
458
459 if ($types === '') {
460 return;
461 }
462
463 $refs = [];
464 foreach ($values as $key => &$value) {
465 $refs[$key] = &$value;
466 }
467
468 $stmt->bind_param($types, ...$refs);
469 }
470
471 private function statementResult(mysqli_stmt $stmt): mysqli_result|bool {
472 $result = $stmt->get_result();
473 if ($result instanceof mysqli_result) {
474 return $result;
475 }
476
477 return true;
478 }
479}
480
481function db(): ZnoteDatabase {
482 global $znoteDatabase, $connect;
483
484 if (!isset($znoteDatabase)) {
485 $znoteDatabase = new ZnoteDatabase($connect);
486 }
487
488 return $znoteDatabase;
489}
490
491function mysql_znote_escape_string($escapestr): string {
492 global $connect;
493 return mysqli_real_escape_string($connect, (string)($escapestr ?? ''));
494}
495
496function mysql_select_single(string $query): array|false {
497 return db()->rawFetchOne($query);
498}
499
500function mysql_select_multi(string $query): array|false {
501 return db()->rawFetchAll($query);
502}
503
504function voidQuery(string $query): bool {
505 return db()->rawExecute($query);
506}
507
508function mysql_update(string $query): bool { return voidQuery($query); }
509function mysql_insert(string $query): bool { return voidQuery($query); }
510function mysql_delete(string $query): bool { return voidQuery($query); }
511
Top