| @@ -0,0 +1,510 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | $time = time(); | |
| 4 | + | if (!isset($version)) { | |
| 5 | + | $version = (string)require dirname(__DIR__) . '/version.php'; | |
| 6 | + | } | |
| 7 | + | ||
| 8 | + | if (!isset($GLOBALS['__znote_start_time'])) { | |
| 9 | + | $GLOBALS['__znote_start_time'] = microtime(true); | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | if (!function_exists('elapsedTime')) { | |
| 13 | + | function elapsedTime(): float { | |
| 14 | + | return round(microtime(true) - $GLOBALS['__znote_start_time'], 4); | |
| 15 | + | } | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | if (!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 | + | ||
| 258 | + | mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); | |
| 259 | + | ||
| 260 | + | try { | |
| 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 | + | ||
| 276 | + | if ($connect->connect_errno) { | |
| 277 | + | znote_database_wait_screen($connect->connect_errno, (string)$connect->connect_error); | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | if (!isset($aacQueries)) { | |
| 281 | + | $aacQueries = 0; | |
| 282 | + | } | |
| 283 | + | if (!isset($accQueriesData)) { | |
| 284 | + | $accQueriesData = []; | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | class 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 | + | ||
| 481 | + | function db(): ZnoteDatabase { | |
| 482 | + | global $znoteDatabase, $connect; | |
| 483 | + | ||
| 484 | + | if (!isset($znoteDatabase)) { | |
| 485 | + | $znoteDatabase = new ZnoteDatabase($connect); | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | return $znoteDatabase; | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | function mysql_znote_escape_string($escapestr): string { | |
| 492 | + | global $connect; | |
| 493 | + | return mysqli_real_escape_string($connect, (string)($escapestr ?? '')); | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | function mysql_select_single(string $query): array|false { | |
| 497 | + | return db()->rawFetchOne($query); | |
| 498 | + | } | |
| 499 | + | ||
| 500 | + | function mysql_select_multi(string $query): array|false { | |
| 501 | + | return db()->rawFetchAll($query); | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | function voidQuery(string $query): bool { | |
| 505 | + | return db()->rawExecute($query); | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | function mysql_update(string $query): bool { return voidQuery($query); } | |
| 509 | + | function mysql_insert(string $query): bool { return voidQuery($query); } | |
| 510 | + | function mysql_delete(string $query): bool { return voidQuery($query); } |
| @@ -0,0 +1,10 @@ | |||
| 1 | + | <footer> | |
| 2 | + | © Znote AAC. | |
| 3 | + | <?php | |
| 4 | + | $finish = microtime(true); | |
| 5 | + | $total_time = round($finish - $start, 4); | |
| 6 | + | ||
| 7 | + | echo 'Server date and clock is: ' . getClock(false, true) . | |
| 8 | + | ' Page generated in ' . $total_time . ' seconds.'; | |
| 9 | + | ?> | |
| 10 | + | </footer> |
| @@ -0,0 +1,52 @@ | |||
| 1 | + | <?php | |
| 2 | + | /** | |
| 3 | + | * Admin action log. | |
| 4 | + | * | |
| 5 | + | * Records mutating actions taken from the admin panel - bans, skill edits, | |
| 6 | + | * points, settings changes, plugin lifecycle, etc - into `znote_admin_log`. | |
| 7 | + | * Admin modules call acp_log() right after a successful write; the table is | |
| 8 | + | * migrated separately (SQL/migrations/2.0.0_admin_log.sql), so acp_log() | |
| 9 | + | * silently no-ops on databases that have not run it yet, matching the | |
| 10 | + | * fallback pattern used by theme_menu_items() and friends. | |
| 11 | + | */ | |
| 12 | + | ||
| 13 | + | function acp_log_table_exists(): bool { | |
| 14 | + | return znote_table_exists('znote_admin_log'); | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | /** | |
| 18 | + | * @param string $action Dotted code, e.g. "player.ban" - see acp_log_action_label(). | |
| 19 | + | * @param string $target What the action was applied to, e.g. a character name. | |
| 20 | + | * @param array $details Small set of extra facts (old/new values, reason...), | |
| 21 | + | * stored as JSON and shown expanded in the log viewer. | |
| 22 | + | */ | |
| 23 | + | function acp_log(string $action, string $target = '', array $details = array()): bool { | |
| 24 | + | if (!acp_log_table_exists()) { | |
| 25 | + | return false; | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | $adminId = isset($GLOBALS['session_user_id']) ? (int)$GLOBALS['session_user_id'] : 0; | |
| 29 | + | $adminName = isset($GLOBALS['user_data']['name']) ? (string)$GLOBALS['user_data']['name'] : ''; | |
| 30 | + | ||
| 31 | + | $detailsJson = ''; | |
| 32 | + | if ($details) { | |
| 33 | + | $encoded = json_encode($details, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| 34 | + | if ($encoded !== false) { | |
| 35 | + | $detailsJson = $encoded; | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | return db()->execute( | |
| 40 | + | "INSERT INTO `znote_admin_log` (`admin_id`, `admin_name`, `action`, `target`, `details`, `ip`, `created`) | |
| 41 | + | VALUES (?, ?, ?, ?, ?, ?, ?);", | |
| 42 | + | [ | |
| 43 | + | $adminId, | |
| 44 | + | $adminName, | |
| 45 | + | substr($action, 0, 64), | |
| 46 | + | substr($target, 0, 191), | |
| 47 | + | $detailsJson, | |
| 48 | + | getIP(), | |
| 49 | + | time(), | |
| 50 | + | ] | |
| 51 | + | ); | |
| 52 | + | } |
| @@ -0,0 +1,185 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | const ZNOTE_BACKUP_DIR = 'engine/backups'; | |
| 4 | + | const ZNOTE_BACKUP_ROWS_PER_CHUNK = 500; | |
| 5 | + | ||
| 6 | + | function znote_backups_root(): string { | |
| 7 | + | return dirname(__DIR__, 2) . '/' . ZNOTE_BACKUP_DIR; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | function znote_backups_prepare_dir(): bool { | |
| 11 | + | $dir = znote_backups_root(); | |
| 12 | + | if (!is_dir($dir) && !@mkdir($dir, 0750, true) && !is_dir($dir)) { | |
| 13 | + | return false; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | $deny = $dir . '/.htaccess'; | |
| 17 | + | if (!is_file($deny)) { | |
| 18 | + | @file_put_contents($deny, "Require all denied\nDeny from all\n"); | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | return is_writable($dir); | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | /** Existing backups, newest first. */ | |
| 25 | + | function znote_backups_list(): array { | |
| 26 | + | $dir = znote_backups_root(); | |
| 27 | + | if (!is_dir($dir)) { | |
| 28 | + | return array(); | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | $out = array(); | |
| 32 | + | foreach (glob($dir . '/*.sql.gz') ?: array() as $file) { | |
| 33 | + | $out[] = array( | |
| 34 | + | 'name' => basename($file), | |
| 35 | + | 'size' => (int)filesize($file), | |
| 36 | + | 'time' => (int)filemtime($file), | |
| 37 | + | ); | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | usort($out, static fn(array $a, array $b): int => $b['time'] <=> $a['time']); | |
| 41 | + | ||
| 42 | + | return $out; | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | function znote_backups_safe_name(string $name): string { | |
| 46 | + | $name = basename($name); | |
| 47 | + | return preg_match('/^znotex-backup-\d{8}-\d{6}\.sql\.gz$/', $name) ? $name : ''; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | function znote_backups_path(string $name): ?string { | |
| 51 | + | $safe = znote_backups_safe_name($name); | |
| 52 | + | if ($safe === '') { | |
| 53 | + | return null; | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | $path = znote_backups_root() . '/' . $safe; | |
| 57 | + | return is_file($path) ? $path : null; | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | function znote_backups_delete(string $name): bool { | |
| 61 | + | $path = znote_backups_path($name); | |
| 62 | + | return $path !== null && @unlink($path); | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | function znote_backups_prune(int $keep): int { | |
| 66 | + | if ($keep <= 0) { | |
| 67 | + | return 0; | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | $all = znote_backups_list(); | |
| 71 | + | $extra = array_slice($all, $keep); | |
| 72 | + | $removed = 0; | |
| 73 | + | ||
| 74 | + | foreach ($extra as $backup) { | |
| 75 | + | if (znote_backups_delete($backup['name'])) { | |
| 76 | + | $removed++; | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | return $removed; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | function znote_backups_sql_value(mysqli $link, $value): string { | |
| 84 | + | if ($value === null) { | |
| 85 | + | return 'NULL'; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | return "'" . $link->real_escape_string((string)$value) . "'"; | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | function znote_backups_write(mysqli $link, $handle): string { | |
| 92 | + | fwrite($handle, "-- ZnoteX backup - " . gmdate('Y-m-d H:i:s') . " UTC\n"); | |
| 93 | + | fwrite($handle, "SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n\n"); | |
| 94 | + | ||
| 95 | + | $tables = array(); | |
| 96 | + | $result = $link->query('SHOW TABLES;'); | |
| 97 | + | if ($result === false) { | |
| 98 | + | return 'Could not list tables: ' . $link->error; | |
| 99 | + | } | |
| 100 | + | while ($row = $result->fetch_array(MYSQLI_NUM)) { | |
| 101 | + | $tables[] = $row[0]; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | foreach ($tables as $table) { | |
| 105 | + | $escapedTable = '`' . str_replace('`', '``', $table) . '`'; | |
| 106 | + | ||
| 107 | + | $createResult = $link->query('SHOW CREATE TABLE ' . $escapedTable . ';'); | |
| 108 | + | if ($createResult === false) { | |
| 109 | + | return 'Could not read structure of ' . $table . ': ' . $link->error; | |
| 110 | + | } | |
| 111 | + | $createRow = $createResult->fetch_assoc(); | |
| 112 | + | $createSql = $createRow['Create Table'] ?? null; | |
| 113 | + | if ($createSql === null) { | |
| 114 | + | continue; | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | fwrite($handle, "\n-- ----------------------------\n-- Table: {$table}\n-- ----------------------------\n"); | |
| 118 | + | fwrite($handle, "DROP TABLE IF EXISTS {$escapedTable};\n{$createSql};\n\n"); | |
| 119 | + | ||
| 120 | + | $countResult = $link->query('SELECT COUNT(*) AS c FROM ' . $escapedTable . ';'); | |
| 121 | + | $total = $countResult !== false ? (int)($countResult->fetch_assoc()['c'] ?? 0) : 0; | |
| 122 | + | if ($total === 0) { | |
| 123 | + | continue; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | for ($offset = 0; $offset < $total; $offset += ZNOTE_BACKUP_ROWS_PER_CHUNK) { | |
| 127 | + | $dataResult = $link->query('SELECT * FROM ' . $escapedTable . ' LIMIT ' . ZNOTE_BACKUP_ROWS_PER_CHUNK . ' OFFSET ' . $offset . ';'); | |
| 128 | + | if ($dataResult === false) { | |
| 129 | + | return 'Could not read data from ' . $table . ': ' . $link->error; | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | $columns = null; | |
| 133 | + | $rowsSql = array(); | |
| 134 | + | while ($row = $dataResult->fetch_assoc()) { | |
| 135 | + | if ($columns === null) { | |
| 136 | + | $columns = '`' . implode('`,`', array_map(static fn($c) => str_replace('`', '``', $c), array_keys($row))) . '`'; | |
| 137 | + | } | |
| 138 | + | $values = array_map(static fn($v) => znote_backups_sql_value($link, $v), array_values($row)); | |
| 139 | + | $rowsSql[] = '(' . implode(',', $values) . ')'; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | if ($rowsSql) { | |
| 143 | + | fwrite($handle, "INSERT INTO {$escapedTable} ({$columns}) VALUES\n" . implode(",\n", $rowsSql) . ";\n"); | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | fwrite($handle, "\nSET FOREIGN_KEY_CHECKS=1;\n"); | |
| 149 | + | ||
| 150 | + | return ''; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | function znote_backups_create(): array { | |
| 154 | + | if (!znote_backups_prepare_dir()) { | |
| 155 | + | return array(false, 'Could not create/write to ' . ZNOTE_BACKUP_DIR . '/.'); | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | if (!class_exists('mysqli') || !function_exists('gzopen')) { | |
| 159 | + | return array(false, 'PHP needs the mysqli and zlib extensions for backups.'); | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | $name = 'znotex-backup-' . date('Ymd-His') . '.sql.gz'; | |
| 163 | + | $path = znote_backups_root() . '/' . $name; | |
| 164 | + | ||
| 165 | + | $handle = @gzopen($path, 'wb9'); | |
| 166 | + | if ($handle === false) { | |
| 167 | + | return array(false, 'Could not open ' . $name . ' for writing.'); | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | $link = db()->connection(); | |
| 171 | + | $error = znote_backups_write($link, $handle); | |
| 172 | + | gzclose($handle); | |
| 173 | + | ||
| 174 | + | if ($error !== '') { | |
| 175 | + | @unlink($path); | |
| 176 | + | return array(false, $error); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | if (!is_file($path) || filesize($path) < 1) { | |
| 180 | + | @unlink($path); | |
| 181 | + | return array(false, 'The backup file ended up empty.'); | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | return array(true, $name); | |
| 185 | + | } |
| @@ -0,0 +1,209 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | function znote_bbcode_url(string $url): string { | |
| 4 | + | $plain = trim(html_entity_decode($url, ENT_QUOTES, 'UTF-8')); | |
| 5 | + | ||
| 6 | + | if ($plain === '' || preg_match('/[\x00-\x1f\s<>"\']/', $plain)) { | |
| 7 | + | return ''; | |
| 8 | + | } | |
| 9 | + | if (!preg_match('#^(https?://[^/]+|/(?!/))#i', $plain)) { | |
| 10 | + | return ''; | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | return htmlspecialchars($plain, ENT_QUOTES, 'UTF-8'); | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | function znote_bbcode_color(string $color): string { | |
| 17 | + | $plain = trim(html_entity_decode($color, ENT_QUOTES, 'UTF-8')); | |
| 18 | + | ||
| 19 | + | if (preg_match('/^#[0-9a-f]{3,8}$/i', $plain)) return $plain; | |
| 20 | + | if (preg_match('/^[a-z]{3,20}$/i', $plain)) return strtolower($plain); | |
| 21 | + | if (preg_match('/^rgba?\(\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*(,\s*(0|1|0?\.[0-9]+)\s*)?\)$/i', $plain)) return $plain; | |
| 22 | + | ||
| 23 | + | return ''; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | /** | |
| 27 | + | * For text stored WITHOUT HTML escaping - news and changelog, which the admin | |
| 28 | + | * panel writes through esc() (SQL escaping only). Escaping here means an admin | |
| 29 | + | * typing <script> gets text, not script, while BBCode still renders. | |
| 30 | + | * | |
| 31 | + | * Forum posts are already escaped by sanitize() on save, so they use | |
| 32 | + | * znote_bbcode() directly - running this on them would double-escape. | |
| 33 | + | */ | |
| 34 | + | function znote_bbcode_raw(?string $text): string { | |
| 35 | + | return znote_bbcode(htmlspecialchars((string)$text, ENT_QUOTES, 'UTF-8')); | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | function znote_bbcode_count_images(?string $text): int { | |
| 39 | + | return preg_match_all('/\[img(?:=[^\]]*)?\]/i', (string)$text); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /** [code]...[/code] is pulled out first (as a \x00CODE<n>\x00 placeholder) so nothing inside it is touched by any tag below - restored at the very end by znote_bbcode_restore_code_blocks(). */ | |
| 43 | + | function znote_bbcode_extract_code_blocks(string $text, array &$codes): string { | |
| 44 | + | return preg_replace_callback('/\[code\](.*?)\[\/code\]/is', static function ($m) use (&$codes) { | |
| 45 | + | $codes[] = $m[1]; | |
| 46 | + | return "\x00CODE" . (count($codes) - 1) . "\x00"; | |
| 47 | + | }, $text); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | function znote_bbcode_restore_code_blocks(string $text, array $codes): string { | |
| 51 | + | return preg_replace_callback("/\x00CODE([0-9]+)\x00/", static function ($m) use ($codes) { | |
| 52 | + | return '<pre class="zbb-code"><code>' . ($codes[(int)$m[1]] ?? '') . '</code></pre>'; | |
| 53 | + | }, $text); | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | /** [font]/[table] et al carry no useful markup here - dropped rather than rendered. */ | |
| 57 | + | function znote_bbcode_strip_unsupported_tags(string $text): string { | |
| 58 | + | $text = preg_replace('/\[font(?:=[^\]]*)?\]/i', '', $text); | |
| 59 | + | $text = preg_replace('/\[\/font\]/i', '', $text); | |
| 60 | + | $text = preg_replace('/\[\/?(?:table|tr|td|th)(?:=[^\]]*)?\]/i', '', $text); | |
| 61 | + | return $text; | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | function znote_bbcode_apply_inline_style_tags(string $text): string { | |
| 65 | + | foreach (array('b' => 'strong', 'i' => 'em', 'u' => 'u', 's' => 'del') as $tag => $html) { | |
| 66 | + | for ($i = 0; $i < 4; $i++) { | |
| 67 | + | $out = preg_replace('/\[' . $tag . '\](.*?)\[\/' . $tag . '\]/is', '<' . $html . '>$1</' . $html . '>', $text); | |
| 68 | + | if ($out === null || $out === $text) break; | |
| 69 | + | $text = $out; | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | return $text; | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | function znote_bbcode_apply_alignment(string $text): string { | |
| 76 | + | foreach (array('left', 'center', 'right', 'justify') as $align) { | |
| 77 | + | $text = preg_replace( | |
| 78 | + | '/\[' . $align . '\](.*?)\[\/' . $align . '\]/is', | |
| 79 | + | '<div class="zbb-align" style="text-align:' . $align . '">$1</div>', | |
| 80 | + | $text | |
| 81 | + | ); | |
| 82 | + | } | |
| 83 | + | return $text; | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | function znote_bbcode_apply_color(string $text): string { | |
| 87 | + | return preg_replace_callback('/\[color=([^\]]{1,30})\](.*?)\[\/color\]/is', static function ($m) { | |
| 88 | + | $color = znote_bbcode_color($m[1]); | |
| 89 | + | return ($color === '') ? $m[2] : '<span style="color:' . $color . '">' . $m[2] . '</span>'; | |
| 90 | + | }, $text); | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | function znote_bbcode_apply_size(string $text): string { | |
| 94 | + | $sizes = array(1 => '0.7em', 2 => '0.85em', 3 => '1em', 4 => '1.2em', 5 => '1.5em', 6 => '2em', 7 => '2.5em'); | |
| 95 | + | return preg_replace_callback('/\[size=([0-9]{1,2})\](.*?)\[\/size\]/is', static function ($m) use ($sizes) { | |
| 96 | + | $size = $sizes[(int)$m[1]] ?? null; | |
| 97 | + | return ($size === null) ? $m[2] : '<span style="font-size:' . $size . '">' . $m[2] . '</span>'; | |
| 98 | + | }, $text); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | function znote_bbcode_apply_images(string $text): string { | |
| 102 | + | return preg_replace_callback('/\[img(?:=([0-9]{1,4})x([0-9]{1,4}))?\]([^\[]+?)\[\/img\]/is', static function ($m) { | |
| 103 | + | $url = znote_bbcode_url($m[3]); | |
| 104 | + | if ($url === '') return ''; | |
| 105 | + | ||
| 106 | + | $w = (int)($m[1] ?? 0); | |
| 107 | + | $h = (int)($m[2] ?? 0); | |
| 108 | + | if (($w <= 0 || $h <= 0 || $w > 4000 || $h > 4000) && preg_match('~/letters/letter_martel_[a-z]\.gif(?:[?#].*)?$~i', $url)) { | |
| 109 | + | $w = 48; | |
| 110 | + | $h = 48; | |
| 111 | + | } | |
| 112 | + | $dim = ($w > 0 && $w <= 4000 && $h > 0 && $h <= 4000) ? ' width="' . $w . '" height="' . $h . '"' : ''; | |
| 113 | + | ||
| 114 | + | return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' | |
| 115 | + | . '<img src="' . $url . '" alt=""' . $dim . ' class="zbb-img" style="max-width:100%;height:auto"></a>'; | |
| 116 | + | }, $text); | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | function znote_bbcode_apply_links(string $text): string { | |
| 120 | + | $text = preg_replace_callback('/\[(?:url|link)=([^\]]+?)\](.*?)\[\/(?:url|link)\]/is', static function ($m) { | |
| 121 | + | $url = znote_bbcode_url($m[1]); | |
| 122 | + | return ($url === '') ? $m[2] : '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $m[2] . '</a>'; | |
| 123 | + | }, $text); | |
| 124 | + | ||
| 125 | + | $text = preg_replace_callback('/\[(?:url|link)\]([^\[]+?)\[\/(?:url|link)\]/is', static function ($m) { | |
| 126 | + | $url = znote_bbcode_url($m[1]); | |
| 127 | + | return ($url === '') ? $m[1] : '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>'; | |
| 128 | + | }, $text); | |
| 129 | + | ||
| 130 | + | return $text; | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | function znote_bbcode_apply_youtube(string $text): string { | |
| 134 | + | return preg_replace_callback('/\[youtube\]([^\[]+?)\[\/youtube\]/is', static function ($m) { | |
| 135 | + | $id = trim(html_entity_decode($m[1], ENT_QUOTES, 'UTF-8')); | |
| 136 | + | if (preg_match('#(?:youtu\.be/|v=|embed/)([A-Za-z0-9_-]{6,20})#', $id, $found)) { | |
| 137 | + | $id = $found[1]; | |
| 138 | + | } | |
| 139 | + | if (!preg_match('/^[A-Za-z0-9_-]{6,20}$/', $id)) { | |
| 140 | + | return ''; | |
| 141 | + | } | |
| 142 | + | return '<div class="zbb-video"><iframe src="https://www.youtube.com/embed/' . $id | |
| 143 | + | . '" frameborder="0" allowfullscreen></iframe></div>'; | |
| 144 | + | }, $text); | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | function znote_bbcode_apply_quote(string $text): string { | |
| 148 | + | return preg_replace_callback('/\[quote(?:=([^\]]{1,40}))?\](.*?)\[\/quote\]/is', static function ($m) { | |
| 149 | + | $who = trim($m[1] ?? ''); | |
| 150 | + | $head = ($who !== '') ? '<cite>' . $who . ' wrote:</cite>' : ''; | |
| 151 | + | return '<blockquote class="zbb-quote">' . $head . $m[2] . '</blockquote>'; | |
| 152 | + | }, $text); | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | /** [*]/[li] items first, then their enclosing [ul]/[list]/[ol] - each loops since tags can nest. */ | |
| 156 | + | function znote_bbcode_apply_lists(string $text): string { | |
| 157 | + | $text = preg_replace('/\[\*\](.*?)\[\/\*\]/is', '<li>$1</li>', $text); | |
| 158 | + | $text = preg_replace('/\[\*\]\s*([^\[\r\n]*)/i', '<li>$1</li>', $text); | |
| 159 | + | ||
| 160 | + | for ($i = 0; $i < 8; $i++) { | |
| 161 | + | $out = preg_replace('/\[li\]((?:(?!\[li\]|\[\/li\]).)*)\[\/li\]/is', '<li>$1</li>', $text); | |
| 162 | + | if ($out === null || $out === $text) break; | |
| 163 | + | $text = $out; | |
| 164 | + | } | |
| 165 | + | for ($i = 0; $i < 8; $i++) { | |
| 166 | + | $out = preg_replace('/\[(?:ul|list)(?:=[^\]]*)?\]((?:(?!\[(?:ul|list)(?:=[^\]]*)?\]|\[\/(?:ul|list)\]).)*)\[\/(?:ul|list)\]/is', '<ul class="zbb-list">$1</ul>', $text); | |
| 167 | + | if ($out === null || $out === $text) break; | |
| 168 | + | $text = $out; | |
| 169 | + | } | |
| 170 | + | for ($i = 0; $i < 8; $i++) { | |
| 171 | + | $out = preg_replace('/\[ol\]((?:(?!\[ol\]|\[\/ol\]).)*)\[\/ol\]/is', '<ol class="zbb-list">$1</ol>', $text); | |
| 172 | + | if ($out === null || $out === $text) break; | |
| 173 | + | $text = $out; | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | return $text; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | /** nl2br(), then undo it right next to a block element that already carries its own margin. */ | |
| 180 | + | function znote_bbcode_apply_linebreaks(string $text): string { | |
| 181 | + | $text = nl2br($text, false); | |
| 182 | + | $text = preg_replace('#<br>\s*(</?(?:ul|ol|li|blockquote|div|cite)[^>]*>)#i', '$1', $text); | |
| 183 | + | $text = preg_replace('#(</?(?:ul|ol|li|blockquote|div|cite)[^>]*>)\s*<br>#i', '$1', $text); | |
| 184 | + | return $text; | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | function znote_bbcode(?string $text): string { | |
| 188 | + | $text = (string)$text; | |
| 189 | + | if ($text === '') { | |
| 190 | + | return ''; | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | $codes = array(); | |
| 194 | + | $text = znote_bbcode_extract_code_blocks($text, $codes); | |
| 195 | + | $text = znote_bbcode_strip_unsupported_tags($text); | |
| 196 | + | $text = znote_bbcode_apply_inline_style_tags($text); | |
| 197 | + | $text = znote_bbcode_apply_alignment($text); | |
| 198 | + | $text = znote_bbcode_apply_color($text); | |
| 199 | + | $text = znote_bbcode_apply_size($text); | |
| 200 | + | $text = znote_bbcode_apply_images($text); | |
| 201 | + | $text = znote_bbcode_apply_links($text); | |
| 202 | + | $text = znote_bbcode_apply_youtube($text); | |
| 203 | + | $text = znote_bbcode_apply_quote($text); | |
| 204 | + | $text = znote_bbcode_apply_lists($text); | |
| 205 | + | $text = znote_bbcode_apply_linebreaks($text); | |
| 206 | + | $text = znote_bbcode_restore_code_blocks($text, $codes); | |
| 207 | + | ||
| 208 | + | return $text; | |
| 209 | + | } |
| @@ -0,0 +1,333 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | class Cache | |
| 4 | + | { | |
| 5 | + | protected string $_file; | |
| 6 | + | protected string $_key; | |
| 7 | + | protected string $_prefix; | |
| 8 | + | protected int $_lifespan = 0; | |
| 9 | + | protected mixed $_content = null; | |
| 10 | + | protected bool $_memory = false; | |
| 11 | + | protected bool $_canMemory = false; | |
| 12 | + | ||
| 13 | + | private const FORMAT = 'ZNOTEX_CACHE_V1:'; | |
| 14 | + | public const EXT = '.cache'; | |
| 15 | + | ||
| 16 | + | public function __construct(string $file) | |
| 17 | + | { | |
| 18 | + | $cfg = function_exists('config') | |
| 19 | + | ? config('cache') | |
| 20 | + | : ($GLOBALS['config']['cache'] ?? array()); | |
| 21 | + | ||
| 22 | + | if (!is_array($cfg)) { | |
| 23 | + | $cfg = array(); | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | $this->_lifespan = max(0, (int)($cfg['lifespan'] ?? 60)); | |
| 27 | + | $this->_prefix = self::normalizePrefix((string)($cfg['prefix'] ?? 'znote_')); | |
| 28 | + | $this->_canMemory = self::memoryAvailable(); | |
| 29 | + | $this->_memory = !empty($cfg['memory']) && $this->_canMemory; | |
| 30 | + | $this->_file = $file . self::EXT; | |
| 31 | + | $logicalName = str_replace('\\', '/', $this->_file); | |
| 32 | + | $this->_key = $this->_prefix . 'cache:' . hash('sha256', $logicalName); | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | public static function memoryAvailable(): bool | |
| 36 | + | { | |
| 37 | + | if (!function_exists('apcu_fetch') || !function_exists('apcu_store')) { | |
| 38 | + | return false; | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | if (function_exists('apcu_enabled')) { | |
| 42 | + | return apcu_enabled(); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | if (PHP_SAPI === 'cli' && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL)) { | |
| 46 | + | return false; | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | return filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOL); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | public static function configuredPrefix(): string | |
| 53 | + | { | |
| 54 | + | $cfg = function_exists('config') | |
| 55 | + | ? config('cache') | |
| 56 | + | : ($GLOBALS['config']['cache'] ?? array()); | |
| 57 | + | ||
| 58 | + | return self::normalizePrefix(is_array($cfg) ? (string)($cfg['prefix'] ?? 'znote_') : 'znote_'); | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | private static function normalizePrefix(string $prefix): string | |
| 62 | + | { | |
| 63 | + | $prefix = preg_replace('/[^a-zA-Z0-9_.:-]/', '_', trim($prefix)) ?? ''; | |
| 64 | + | return substr($prefix !== '' ? $prefix : 'znote_', 0, 64); | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | public function setExpiration(int $span): void | |
| 68 | + | { | |
| 69 | + | $this->_lifespan = max(0, $span); | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | public function useMemory(bool $bool): bool | |
| 73 | + | { | |
| 74 | + | $this->_memory = $bool && $this->_canMemory; | |
| 75 | + | return $this->_memory; | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | public function driver(): string | |
| 79 | + | { | |
| 80 | + | return $this->_memory ? 'apcu' : 'file'; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | public function setContent(mixed $content): void | |
| 84 | + | { | |
| 85 | + | $this->_content = $content; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | public function hasExpired(): bool | |
| 89 | + | { | |
| 90 | + | if ($this->_memory) { | |
| 91 | + | return !apcu_exists($this->_key); | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | if (!is_file($this->_file)) { | |
| 95 | + | return true; | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | if ($this->_lifespan === 0) { | |
| 99 | + | return false; | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | $modified = filemtime($this->_file); | |
| 103 | + | return $modified === false || time() >= $modified + $this->_lifespan; | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | public function remainingTime(): int | |
| 107 | + | { | |
| 108 | + | if ($this->hasExpired()) { | |
| 109 | + | return 0; | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | if ($this->_lifespan === 0) { | |
| 113 | + | return PHP_INT_MAX; | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | if ($this->_memory) { | |
| 117 | + | $success = false; | |
| 118 | + | $payload = apcu_fetch($this->_key, $success); | |
| 119 | + | if (!$success) { | |
| 120 | + | return 0; | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | $envelope = $this->decodeEnvelope($payload); | |
| 124 | + | return is_array($envelope) | |
| 125 | + | ? max(0, (int)$envelope['expires'] - time()) | |
| 126 | + | : 0; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | $modified = filemtime($this->_file); | |
| 130 | + | return $modified === false ? 0 : max(0, ($modified + $this->_lifespan) - time()); | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | public function save(): bool | |
| 134 | + | { | |
| 135 | + | $payload = $this->encodeContent(); | |
| 136 | + | if ($payload === false) { | |
| 137 | + | return false; | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | if ($this->_memory) { | |
| 141 | + | return apcu_store($this->_key, $payload, $this->_lifespan); | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | $directory = dirname($this->_file); | |
| 145 | + | if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) { | |
| 146 | + | return false; | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | $temporary = tempnam($directory, '.znote-cache-'); | |
| 150 | + | if ($temporary === false) { | |
| 151 | + | return false; | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | $written = file_put_contents($temporary, $payload, LOCK_EX); | |
| 155 | + | if ($written === false) { | |
| 156 | + | @unlink($temporary); | |
| 157 | + | return false; | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | if (@rename($temporary, $this->_file)) { | |
| 161 | + | clearstatcache(true, $this->_file); | |
| 162 | + | return true; | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | $saved = file_put_contents($this->_file, $payload, LOCK_EX) !== false; | |
| 166 | + | @unlink($temporary); | |
| 167 | + | clearstatcache(true, $this->_file); | |
| 168 | + | return $saved; | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | public function load(): mixed | |
| 172 | + | { | |
| 173 | + | if ($this->_memory) { | |
| 174 | + | $success = false; | |
| 175 | + | $payload = apcu_fetch($this->_key, $success); | |
| 176 | + | return $success ? $this->decodeContent($payload) : false; | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | if (!is_file($this->_file)) { | |
| 180 | + | return false; | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | $handle = @fopen($this->_file, 'rb'); | |
| 184 | + | if ($handle === false) { | |
| 185 | + | return false; | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | $payload = false; | |
| 189 | + | if (flock($handle, LOCK_SH)) { | |
| 190 | + | $payload = stream_get_contents($handle); | |
| 191 | + | flock($handle, LOCK_UN); | |
| 192 | + | } | |
| 193 | + | fclose($handle); | |
| 194 | + | ||
| 195 | + | return $payload === false || $payload === '' ? false : $this->decodeContent($payload); | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | public function delete(): bool | |
| 199 | + | { | |
| 200 | + | if ($this->_memory) { | |
| 201 | + | return !apcu_exists($this->_key) || apcu_delete($this->_key); | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | if (!is_file($this->_file)) { | |
| 205 | + | return true; | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | $deleted = @unlink($this->_file); | |
| 209 | + | clearstatcache(true, $this->_file); | |
| 210 | + | return $deleted; | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | private function encodeContent(): string|false | |
| 214 | + | { | |
| 215 | + | $envelope = array( | |
| 216 | + | 'version' => 1, | |
| 217 | + | 'expires' => $this->_lifespan === 0 ? 0 : time() + $this->_lifespan, | |
| 218 | + | 'value' => $this->_content, | |
| 219 | + | ); | |
| 220 | + | ||
| 221 | + | try { | |
| 222 | + | return self::FORMAT . base64_encode(serialize($envelope)); | |
| 223 | + | } catch (Throwable $error) { | |
| 224 | + | return false; | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | private function decodeEnvelope(mixed $payload): array|false | |
| 229 | + | { | |
| 230 | + | if (!is_string($payload) || !str_starts_with($payload, self::FORMAT)) { | |
| 231 | + | return false; | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | $decoded = base64_decode(substr($payload, strlen(self::FORMAT)), true); | |
| 235 | + | if ($decoded === false) { | |
| 236 | + | return false; | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | try { | |
| 240 | + | $envelope = @unserialize($decoded, array('allowed_classes' => false)); | |
| 241 | + | } catch (Throwable $error) { | |
| 242 | + | return false; | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | return is_array($envelope) | |
| 246 | + | && ($envelope['version'] ?? null) === 1 | |
| 247 | + | && array_key_exists('value', $envelope) | |
| 248 | + | ? $envelope | |
| 249 | + | : false; | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | private function decodeContent(mixed $payload): mixed | |
| 253 | + | { | |
| 254 | + | $envelope = $this->decodeEnvelope($payload); | |
| 255 | + | if ($envelope !== false) { | |
| 256 | + | return $envelope['value']; | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | if (!is_string($payload) || $payload === '') { | |
| 260 | + | return $payload; | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | $json = json_decode($payload, true); | |
| 264 | + | return json_last_error() === JSON_ERROR_NONE ? $json : $payload; | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | function znote_cache_stats(): array | |
| 269 | + | { | |
| 270 | + | $files = 0; | |
| 271 | + | $bytes = 0; | |
| 272 | + | $root = realpath('engine/cache'); | |
| 273 | + | ||
| 274 | + | if ($root !== false && is_dir($root)) { | |
| 275 | + | $iterator = new RecursiveIteratorIterator( | |
| 276 | + | new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) | |
| 277 | + | ); | |
| 278 | + | ||
| 279 | + | foreach ($iterator as $entry) { | |
| 280 | + | if ($entry->isFile() && str_ends_with($entry->getFilename(), Cache::EXT)) { | |
| 281 | + | $files++; | |
| 282 | + | $bytes += $entry->getSize(); | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | $cfg = function_exists('config') | |
| 288 | + | ? config('cache') | |
| 289 | + | : ($GLOBALS['config']['cache'] ?? array()); | |
| 290 | + | $requestedMemory = is_array($cfg) && !empty($cfg['memory']); | |
| 291 | + | ||
| 292 | + | return array( | |
| 293 | + | 'driver' => $requestedMemory && Cache::memoryAvailable() ? 'APCu' : 'Files', | |
| 294 | + | 'requested_memory' => $requestedMemory, | |
| 295 | + | 'apcu_available' => Cache::memoryAvailable(), | |
| 296 | + | 'files' => $files, | |
| 297 | + | 'bytes' => $bytes, | |
| 298 | + | 'prefix' => Cache::configuredPrefix(), | |
| 299 | + | ); | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | function znote_cache_flush(): int | |
| 303 | + | { | |
| 304 | + | $removed = 0; | |
| 305 | + | $root = realpath('engine/cache'); | |
| 306 | + | ||
| 307 | + | if ($root !== false && is_dir($root)) { | |
| 308 | + | $iterator = new RecursiveIteratorIterator( | |
| 309 | + | new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), | |
| 310 | + | RecursiveIteratorIterator::CHILD_FIRST | |
| 311 | + | ); | |
| 312 | + | ||
| 313 | + | foreach ($iterator as $entry) { | |
| 314 | + | if ($entry->isFile() && str_ends_with($entry->getFilename(), Cache::EXT) && @unlink($entry->getPathname())) { | |
| 315 | + | $removed++; | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | if (Cache::memoryAvailable() && function_exists('apcu_cache_info')) { | |
| 321 | + | $prefix = Cache::configuredPrefix() . 'cache:'; | |
| 322 | + | $info = apcu_cache_info(false); | |
| 323 | + | foreach (($info['cache_list'] ?? array()) as $item) { | |
| 324 | + | $key = (string)($item['info'] ?? ''); | |
| 325 | + | $legacyKey = str_replace('\\', '/', $key); | |
| 326 | + | if ((str_starts_with($key, $prefix) || str_starts_with($legacyKey, 'engine/cache/')) && apcu_delete($key)) { | |
| 327 | + | $removed++; | |
| 328 | + | } | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | return $removed; | |
| 333 | + | } |
| @@ -0,0 +1,117 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | /** | |
| 4 | + | * config.php's old single client_download/client_download_linux keys, in the | |
| 5 | + | * same shape as a downloads.entries row - so they can be merged/normalized | |
| 6 | + | * through the exact same code path as any custom entry an admin adds. | |
| 7 | + | */ | |
| 8 | + | function znote_download_legacy_entries(array $config): array { | |
| 9 | + | $clientVersion = isset($config['client']) ? ((int)$config['client'] / 100) : ''; | |
| 10 | + | ||
| 11 | + | return array( | |
| 12 | + | 'windows_client' => array( | |
| 13 | + | 'label' => t('downloads.win', ['version' => $clientVersion]), | |
| 14 | + | 'url' => (string)($config['client_download'] ?? ''), | |
| 15 | + | 'section' => 'official', | |
| 16 | + | 'image' => '', | |
| 17 | + | 'description' => '', | |
| 18 | + | 'enabled' => !empty($config['client_download']), | |
| 19 | + | ), | |
| 20 | + | 'linux_client' => array( | |
| 21 | + | 'label' => t('downloads.linux', ['version' => $clientVersion]), | |
| 22 | + | 'url' => (string)($config['client_download_linux'] ?? ''), | |
| 23 | + | 'section' => 'unsupported', | |
| 24 | + | 'image' => '', | |
| 25 | + | 'description' => '', | |
| 26 | + | 'enabled' => !empty($config['client_download_linux']), | |
| 27 | + | ), | |
| 28 | + | ); | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | /** One config.php downloads.entries row, normalized - or null if it has no usable key. */ | |
| 32 | + | function znote_download_normalize_entry($entry, array $legacy): ?array { | |
| 33 | + | if (!is_array($entry)) { | |
| 34 | + | return null; | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | $key = preg_replace('/[^a-z0-9_]/', '', strtolower((string)($entry['key'] ?? ''))); | |
| 38 | + | if ($key === '') { | |
| 39 | + | return null; | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | $item = array( | |
| 43 | + | 'key' => $key, | |
| 44 | + | 'label' => trim((string)($entry['label'] ?? '')), | |
| 45 | + | 'url' => trim((string)($entry['url'] ?? '')), | |
| 46 | + | 'section' => trim((string)($entry['section'] ?? 'custom')), | |
| 47 | + | 'image' => trim((string)($entry['image'] ?? '')), | |
| 48 | + | 'description' => trim((string)($entry['description'] ?? '')), | |
| 49 | + | 'enabled' => !empty($entry['enabled']) && (string)$entry['enabled'] !== '0', | |
| 50 | + | ); | |
| 51 | + | ||
| 52 | + | if (isset($legacy[$key])) { | |
| 53 | + | $item = array_merge($item, $legacy[$key]); | |
| 54 | + | } | |
| 55 | + | if ($item['label'] === '') { | |
| 56 | + | $item['label'] = ucwords(str_replace('_', ' ', $key)); | |
| 57 | + | } | |
| 58 | + | if ($item['section'] === '') { | |
| 59 | + | $item['section'] = 'custom'; | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | return $item; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | /** Windows/Linux client links an admin never added to downloads.entries at all - so they still show up if config.php sets a URL for them. */ | |
| 66 | + | function znote_download_append_missing_legacy(array $normalized, array $legacy, bool $includeDisabled): array { | |
| 67 | + | foreach ($legacy as $key => $entry) { | |
| 68 | + | $exists = false; | |
| 69 | + | foreach ($normalized as $item) { | |
| 70 | + | if ($item['key'] === $key) { | |
| 71 | + | $exists = true; | |
| 72 | + | break; | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | if (!$exists && ($includeDisabled || ($entry['enabled'] && $entry['url'] !== ''))) { | |
| 76 | + | $normalized[] = array_merge(array('key' => $key), $entry); | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | return $normalized; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | function znote_download_entries(bool $includeDisabled = false): array { | |
| 84 | + | global $config; | |
| 85 | + | ||
| 86 | + | $entries = $config['downloads']['entries'] ?? array(); | |
| 87 | + | if (!is_array($entries)) { | |
| 88 | + | $entries = array(); | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | $legacy = znote_download_legacy_entries($config); | |
| 92 | + | ||
| 93 | + | $normalized = array(); | |
| 94 | + | foreach ($entries as $entry) { | |
| 95 | + | $item = znote_download_normalize_entry($entry, $legacy); | |
| 96 | + | if ($item === null) { | |
| 97 | + | continue; | |
| 98 | + | } | |
| 99 | + | if ($includeDisabled || ($item['enabled'] && $item['url'] !== '')) { | |
| 100 | + | $normalized[] = $item; | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | return znote_download_append_missing_legacy($normalized, $legacy, $includeDisabled); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | function znote_download_entries_by_section(bool $includeDisabled = false): array { | |
| 108 | + | $sections = array(); | |
| 109 | + | foreach (znote_download_entries($includeDisabled) as $entry) { | |
| 110 | + | $section = (string)$entry['section']; | |
| 111 | + | if (!isset($sections[$section])) { | |
| 112 | + | $sections[$section] = array(); | |
| 113 | + | } | |
| 114 | + | $sections[$section][] = $entry; | |
| 115 | + | } | |
| 116 | + | return $sections; | |
| 117 | + | } |
| @@ -0,0 +1,378 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | const ZNOTE_EXTENSION_API_VERSION = '1.0.0'; | |
| 4 | + | ||
| 5 | + | function znote_extension_version_matches(string $version, string $constraint): bool | |
| 6 | + | { | |
| 7 | + | $version = ltrim(trim($version), 'vV'); | |
| 8 | + | $constraint = trim($constraint); | |
| 9 | + | ||
| 10 | + | if ($version === '' || $constraint === '' || $constraint === '*') { | |
| 11 | + | return true; | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | foreach (preg_split('/\s*\|\|\s*/', $constraint) ?: array() as $alternative) { | |
| 15 | + | $alternative = preg_replace('/(>=|<=|>|<|==|=|!=)\s+/', '$1', trim($alternative)) ?? ''; | |
| 16 | + | $parts = preg_split('/[\s,]+/', $alternative, -1, PREG_SPLIT_NO_EMPTY) ?: array(); | |
| 17 | + | $matches = $parts !== array(); | |
| 18 | + | ||
| 19 | + | foreach ($parts as $part) { | |
| 20 | + | if (!znote_extension_version_part_matches($version, $part)) { | |
| 21 | + | $matches = false; | |
| 22 | + | break; | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | if ($matches) { | |
| 27 | + | return true; | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | return false; | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | function znote_extension_version_part_matches(string $version, string $constraint): bool | |
| 35 | + | { | |
| 36 | + | if ($constraint === '' || $constraint === '*') { | |
| 37 | + | return true; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | if ($constraint[0] === '^') { | |
| 41 | + | return znote_extension_version_caret_matches($version, $constraint); | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | if ($constraint[0] === '~') { | |
| 45 | + | return znote_extension_version_tilde_matches($version, $constraint); | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | if (str_contains($constraint, '*') || str_contains(strtolower($constraint), 'x')) { | |
| 49 | + | return znote_extension_version_wildcard_matches($version, $constraint); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | return znote_extension_version_operator_matches($version, $constraint); | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | function znote_extension_version_caret_matches(string $version, string $constraint): bool | |
| 56 | + | { | |
| 57 | + | $minimum = substr($constraint, 1); | |
| 58 | + | $segments = array_map('intval', explode('.', $minimum)); | |
| 59 | + | $major = $segments[0] ?? 0; | |
| 60 | + | $minor = $segments[1] ?? 0; | |
| 61 | + | $patch = $segments[2] ?? 0; | |
| 62 | + | $maximum = $major > 0 | |
| 63 | + | ? ($major + 1) . '.0.0' | |
| 64 | + | : ($minor > 0 ? '0.' . ($minor + 1) . '.0' : '0.0.' . ($patch + 1)); | |
| 65 | + | ||
| 66 | + | return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<'); | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | function znote_extension_version_tilde_matches(string $version, string $constraint): bool | |
| 70 | + | { | |
| 71 | + | $minimum = substr($constraint, 1); | |
| 72 | + | $rawSegments = explode('.', $minimum); | |
| 73 | + | $segments = array_map('intval', $rawSegments); | |
| 74 | + | $major = $segments[0] ?? 0; | |
| 75 | + | $minor = $segments[1] ?? 0; | |
| 76 | + | $maximum = count($rawSegments) >= 3 | |
| 77 | + | ? $major . '.' . ($minor + 1) . '.0' | |
| 78 | + | : ($major + 1) . '.0.0'; | |
| 79 | + | ||
| 80 | + | return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<'); | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | function znote_extension_version_wildcard_matches(string $version, string $constraint): bool | |
| 84 | + | { | |
| 85 | + | $segments = explode('.', str_replace(array('X', 'x'), '*', $constraint)); | |
| 86 | + | $fixed = array(); | |
| 87 | + | ||
| 88 | + | foreach ($segments as $segment) { | |
| 89 | + | if ($segment === '*') { | |
| 90 | + | break; | |
| 91 | + | } | |
| 92 | + | $fixed[] = max(0, (int)$segment); | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | if ($fixed === array()) { | |
| 96 | + | return true; | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | $minimumParts = array_pad($fixed, 3, 0); | |
| 100 | + | $maximumParts = $minimumParts; | |
| 101 | + | $index = count($fixed) - 1; | |
| 102 | + | $maximumParts[$index]++; | |
| 103 | + | for ($i = $index + 1; $i < 3; $i++) { | |
| 104 | + | $maximumParts[$i] = 0; | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | $minimum = implode('.', $minimumParts); | |
| 108 | + | $maximum = implode('.', $maximumParts); | |
| 109 | + | return version_compare($version, $minimum, '>=') && version_compare($version, $maximum, '<'); | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | function znote_extension_version_operator_matches(string $version, string $constraint): bool | |
| 113 | + | { | |
| 114 | + | if (!preg_match('/^(>=|<=|>|<|==|=|!=)?(.+)$/', $constraint, $match)) { | |
| 115 | + | return false; | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | $operator = $match[1] !== '' ? $match[1] : '='; | |
| 119 | + | return version_compare($version, ltrim(trim($match[2]), 'vV'), $operator); | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | function znote_extension_requirements(array $manifest): array | |
| 123 | + | { | |
| 124 | + | $requires = $manifest['requires'] ?? array(); | |
| 125 | + | ||
| 126 | + | if (is_string($requires)) { | |
| 127 | + | $requires = trim($requires); | |
| 128 | + | return $requires === '' ? array() : array('znotex' => '>=' . ltrim($requires, 'vV')); | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | if (!is_array($requires)) { | |
| 132 | + | return array(); | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | if (isset($requires['znote']) && !isset($requires['znotex'])) { | |
| 136 | + | $requires['znotex'] = $requires['znote']; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | return $requires; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | function znote_extension_compatibility(array $manifest): array | |
| 143 | + | { | |
| 144 | + | $requires = znote_extension_requirements($manifest); | |
| 145 | + | $versions = array( | |
| 146 | + | 'znotex' => (string)($GLOBALS['version'] ?? '0.0.0'), | |
| 147 | + | 'php' => PHP_VERSION, | |
| 148 | + | 'api' => ZNOTE_EXTENSION_API_VERSION, | |
| 149 | + | ); | |
| 150 | + | $errors = array(); | |
| 151 | + | ||
| 152 | + | foreach ($versions as $component => $current) { | |
| 153 | + | $declared = $requires[$component] ?? ''; | |
| 154 | + | $constraint = is_string($declared) || is_numeric($declared) ? trim((string)$declared) : ''; | |
| 155 | + | if ($declared !== '' && $constraint === '') { | |
| 156 | + | $errors[] = 'Invalid ' . $component . ' version requirement.'; | |
| 157 | + | continue; | |
| 158 | + | } | |
| 159 | + | if ($constraint !== '' && !znote_extension_version_matches($current, $constraint)) { | |
| 160 | + | $errors[] = 'Requires ' . ($component === 'znotex' ? 'ZnoteX' : strtoupper($component)) | |
| 161 | + | . ' ' . $constraint . '; running ' . $current . '.'; | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | $extensions = $requires['extensions'] ?? array(); | |
| 166 | + | if (is_string($extensions)) { | |
| 167 | + | $extensions = preg_split('/[\s,]+/', $extensions, -1, PREG_SPLIT_NO_EMPTY) ?: array(); | |
| 168 | + | } | |
| 169 | + | if (is_array($extensions)) { | |
| 170 | + | foreach ($extensions as $extension) { | |
| 171 | + | $extension = strtolower(trim((string)$extension)); | |
| 172 | + | if ($extension !== '' && !extension_loaded($extension)) { | |
| 173 | + | $errors[] = 'Requires PHP extension ' . $extension . '.'; | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | return array( | |
| 179 | + | 'compatible' => $errors === array(), | |
| 180 | + | 'errors' => $errors, | |
| 181 | + | 'requires' => $requires, | |
| 182 | + | 'versions' => $versions, | |
| 183 | + | ); | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | function znote_extension_relative_path(string $path): string | |
| 187 | + | { | |
| 188 | + | $path = trim(str_replace('\\', '/', trim($path)), '/'); | |
| 189 | + | $segments = array_values(array_filter(explode('/', $path), static fn(string $part): bool => $part !== '')); | |
| 190 | + | ||
| 191 | + | if ($path === '' | |
| 192 | + | || preg_match('/[\x00-\x1F\x7F?#%<>"\x3A;]/u', $path) | |
| 193 | + | || in_array('..', $segments, true) | |
| 194 | + | || in_array('.', $segments, true) | |
| 195 | + | ) { | |
| 196 | + | return ''; | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | return implode('/', $segments); | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | function znote_extension_url_path(string $path): string | |
| 203 | + | { | |
| 204 | + | $path = znote_extension_relative_path($path); | |
| 205 | + | return $path === '' ? '' : implode('/', array_map('rawurlencode', explode('/', $path))); | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | final class ZnoteExtensionApi | |
| 209 | + | { | |
| 210 | + | private string $kind; | |
| 211 | + | private string $name; | |
| 212 | + | ||
| 213 | + | private function __construct(string $kind, string $name) | |
| 214 | + | { | |
| 215 | + | $this->kind = $kind; | |
| 216 | + | $this->name = $name; | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | public static function plugin(string $name): self | |
| 220 | + | { | |
| 221 | + | $name = function_exists('znote_plugin_sanitize') ? znote_plugin_sanitize($name) : ''; | |
| 222 | + | if ($name === '') { | |
| 223 | + | throw new InvalidArgumentException('Invalid plugin name.'); | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | return new self('plugin', $name); | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | public static function theme(string $name): self | |
| 230 | + | { | |
| 231 | + | $name = function_exists('theme_sanitize') ? theme_sanitize($name) : ''; | |
| 232 | + | if ($name === '') { | |
| 233 | + | throw new InvalidArgumentException('Invalid theme name.'); | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | return new self('theme', $name); | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | public function apiVersion(): string | |
| 240 | + | { | |
| 241 | + | return ZNOTE_EXTENSION_API_VERSION; | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | public function znoteVersion(): string | |
| 245 | + | { | |
| 246 | + | return (string)($GLOBALS['version'] ?? '0.0.0'); | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | public function kind(): string | |
| 250 | + | { | |
| 251 | + | return $this->kind; | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | public function name(): string | |
| 255 | + | { | |
| 256 | + | return $this->name; | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | public function config(string $path = '', mixed $default = null): mixed | |
| 260 | + | { | |
| 261 | + | $value = $GLOBALS['config'] ?? array(); | |
| 262 | + | if ($path === '') { | |
| 263 | + | return $value; | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | foreach (explode('.', $path) as $segment) { | |
| 267 | + | if (!is_array($value) || !array_key_exists($segment, $value)) { | |
| 268 | + | return $default; | |
| 269 | + | } | |
| 270 | + | $value = $value[$segment]; | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | return $value; | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | public function setting(string $key, ?string $default = null): ?string | |
| 277 | + | { | |
| 278 | + | $key = $this->settingKey($key); | |
| 279 | + | return function_exists('setting') ? setting($key, $default) : $default; | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | public function setSetting(string $key, string $value): bool | |
| 283 | + | { | |
| 284 | + | return function_exists('setting_set') && setting_set($this->settingKey($key), $value); | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | public function database(): mixed | |
| 288 | + | { | |
| 289 | + | return function_exists('db') ? db() : null; | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | public function cache(string $key, ?int $lifespan = null, ?bool $memory = null): Cache | |
| 293 | + | { | |
| 294 | + | $key = strtolower(trim($key)); | |
| 295 | + | if (!preg_match('/^[a-z0-9_.-]{1,100}$/', $key)) { | |
| 296 | + | throw new InvalidArgumentException('Invalid cache key.'); | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | $cache = new Cache('engine/cache/extensions/' . $this->kind . '/' . $this->name . '/' . $key); | |
| 300 | + | if ($lifespan !== null) { | |
| 301 | + | $cache->setExpiration($lifespan); | |
| 302 | + | } | |
| 303 | + | if ($memory !== null) { | |
| 304 | + | $cache->useMemory($memory); | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | return $cache; | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | public function on(string $hook, callable $callback, int $priority = 10): void | |
| 311 | + | { | |
| 312 | + | znote_hook_register($hook, $callback, $priority); | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | public function dispatch(string $hook, array $data = array()): void | |
| 316 | + | { | |
| 317 | + | znote_hook($hook, $data); | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | public function collect(string $hook, array $data = array()): string | |
| 321 | + | { | |
| 322 | + | return znote_hook_collect($hook, $data); | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | public function filter(string $hook, mixed $value, array $data = array()): mixed | |
| 326 | + | { | |
| 327 | + | return znote_hook_filter($hook, $value, $data); | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | public function allows(string $hook, array $data = array()): bool | |
| 331 | + | { | |
| 332 | + | return znote_hook_allows($hook, $data); | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | public function url(string $page = ''): string | |
| 336 | + | { | |
| 337 | + | return $this->kind === 'plugin' | |
| 338 | + | ? znote_plugin_url($this->name, $page) | |
| 339 | + | : theme_url($this->name); | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | public function asset(string $file): string | |
| 343 | + | { | |
| 344 | + | $file = znote_extension_relative_path($file); | |
| 345 | + | if ($file === '') { | |
| 346 | + | return ''; | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | return $this->kind === 'plugin' | |
| 350 | + | ? znote_plugin_asset($this->name, $file) | |
| 351 | + | : 'layouts/' . $this->name . '/assets/' . znote_extension_url_path($file); | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | public function themeOption(string $key, string $default = ''): string | |
| 355 | + | { | |
| 356 | + | return function_exists('theme_option') ? theme_option($key, $default, $this->kind === 'theme' ? $this->name : null) : $default; | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | private function settingKey(string $key): string | |
| 360 | + | { | |
| 361 | + | $key = strtolower(trim($key)); | |
| 362 | + | if (!preg_match('/^[a-z0-9_.-]{1,100}$/', $key)) { | |
| 363 | + | throw new InvalidArgumentException('Invalid setting key.'); | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | return $this->kind . ':' . $this->name . ':setting:' . $key; | |
| 367 | + | } | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | function znote_plugin_api(string $plugin): ZnoteExtensionApi | |
| 371 | + | { | |
| 372 | + | return ZnoteExtensionApi::plugin($plugin); | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | function znote_theme_api(?string $theme = null): ZnoteExtensionApi | |
| 376 | + | { | |
| 377 | + | return ZnoteExtensionApi::theme($theme ?? theme_active()); | |
| 378 | + | } |
| @@ -0,0 +1,736 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | function setSession($key, $data) { | |
| 4 | + | global $sessionPrefix; | |
| 5 | + | $_SESSION[$sessionPrefix.$key] = $data; | |
| 6 | + | } | |
| 7 | + | function getSession($key) { | |
| 8 | + | global $sessionPrefix; | |
| 9 | + | return (isset($_SESSION[$sessionPrefix.$key])) ? $_SESSION[$sessionPrefix.$key] : false; | |
| 10 | + | } | |
| 11 | + | // Fetch and sanitize POST and GET values | |
| 12 | + | function getValue($value) { | |
| 13 | + | return (!empty($value)) ? sanitize($value) : false; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | function SendGet($getArray, $location = 'error.php') { | |
| 17 | + | $string = ""; | |
| 18 | + | $count = 0; | |
| 19 | + | foreach ($getArray as $getKey => $getValue) { | |
| 20 | + | if ($count > 0) $string .= '&'; | |
| 21 | + | header('Location: ' . $location . '?' . http_build_query($getArray)); | |
| 22 | + | exit; | |
| 23 | + | } | |
| 24 | + | header("Location: {$location}?{$string}"); | |
| 25 | + | exit(); | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Sweet error reporting | |
| 29 | + | function data_dump($print = false, $var = false, $title = false) { | |
| 30 | + | if ($title !== false) echo "<pre><font color='red' size='5'>$title</font><br>"; | |
| 31 | + | else echo '<pre>'; | |
| 32 | + | if ($print !== false) { | |
| 33 | + | echo 'Print: - '; | |
| 34 | + | print_r($print); | |
| 35 | + | echo "<br>"; | |
| 36 | + | } | |
| 37 | + | if ($var !== false) { | |
| 38 | + | echo 'Var_dump: - '; | |
| 39 | + | var_dump($var); | |
| 40 | + | } | |
| 41 | + | echo '</pre><br>'; | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | function accountAccess($accountId, $TFS) { | |
| 45 | + | $accountId = (int)$accountId; | |
| 46 | + | $access = 0; | |
| 47 | + | ||
| 48 | + | // TFS 0.3/4 | |
| 49 | + | $yourChars = db()->fetchAll("SELECT `name`, `group_id`, `account_id` FROM `players` WHERE `account_id` = ?;", [$accountId]); | |
| 50 | + | if ($yourChars !== false) { | |
| 51 | + | foreach ($yourChars as $char) { | |
| 52 | + | if ($TFS === 'TFS_03' || $TFS === 'OTHIRE') { | |
| 53 | + | if ($char['group_id'] > $access) $access = $char['group_id']; | |
| 54 | + | } else { | |
| 55 | + | if ($char['group_id'] > 1) { | |
| 56 | + | if ($access == 0) { | |
| 57 | + | $acc = db()->fetchOne("SELECT `type` FROM `accounts` WHERE `id` = ? LIMIT 1;", [$char['account_id']]); | |
| 58 | + | $access = $acc['type']; | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | if ($access == 0) $access++; | |
| 64 | + | return $access; | |
| 65 | + | } else return false; | |
| 66 | + | // | |
| 67 | + | } | |
| 68 | + | // Generate recovery key | |
| 69 | + | function generate_recovery_key($length) { | |
| 70 | + | return substr(bin2hex(random_bytes(32)), 0, (int)$length); | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Calculate discount | |
| 74 | + | function calculate_discount($orig, $new) { | |
| 75 | + | $orig = (int)$orig; | |
| 76 | + | $new = (int)$new; | |
| 77 | + | ||
| 78 | + | $tmp = ''; | |
| 79 | + | if ($new >= $orig) { | |
| 80 | + | if ($new != $orig) { | |
| 81 | + | $calc = ($new/$orig) - 1; | |
| 82 | + | $calc *= 100; | |
| 83 | + | $tmp = '+'. floor($calc) .'%'; | |
| 84 | + | } else $tmp = '0%'; | |
| 85 | + | } else { | |
| 86 | + | $calc = 1 - ($new/$orig); | |
| 87 | + | $calc *= 100; | |
| 88 | + | $tmp = '-'. floor($calc) .'%'; | |
| 89 | + | } | |
| 90 | + | return $tmp; | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Proper URLs | |
| 94 | + | function url($path = false) { | |
| 95 | + | $folder = dirname($_SERVER['SCRIPT_NAME']); | |
| 96 | + | return config('site_url') . '/' . $path; | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | function getCache() { | |
| 100 | + | $results = db()->fetchOne("SELECT `cached` FROM `znote`;"); | |
| 101 | + | return ($results !== false) ? $results['cached'] : false; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | function setCache($time) { | |
| 105 | + | db()->execute("UPDATE `znote` set `cached` = ?", [(int)$time]); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // Get visitor basic data | |
| 109 | + | function znote_visitors_get_data() { | |
| 110 | + | return db()->fetchAll("SELECT `ip`, `value` FROM `znote_visitors` ORDER BY `id` DESC LIMIT 1000;"); | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // Set visitor basic data | |
| 114 | + | function znote_visitor_set_data($visitor_data) { | |
| 115 | + | $exist = false; | |
| 116 | + | $ip = getIPLong(); | |
| 117 | + | ||
| 118 | + | foreach ((array)$visitor_data as $row) { | |
| 119 | + | if ($ip == $row['ip']) { | |
| 120 | + | $exist = true; | |
| 121 | + | $value = $row['value']; | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | if ($exist && isset($value)) { | |
| 126 | + | // Update the value | |
| 127 | + | $value++; | |
| 128 | + | db()->execute("UPDATE `znote_visitors` SET `value` = ? WHERE `ip` = ?", [$value, $ip]); | |
| 129 | + | } else { | |
| 130 | + | // Insert new row | |
| 131 | + | db()->execute("INSERT INTO `znote_visitors` (`ip`, `value`) VALUES (?, 1)", [$ip]); | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // Get visitor basic data | |
| 136 | + | function znote_visitors_get_detailed_data($cache_time) { | |
| 137 | + | $period = (int)time() - (int)$cache_time; | |
| 138 | + | return db()->fetchAll("SELECT `ip`, `time`, `type`, `account_id` FROM `znote_visitors_details` WHERE `time` >= ? LIMIT 0, 50", [$period]); | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | function znote_visitor_insert_detailed_data($type) { | |
| 142 | + | $type = (int)$type; | |
| 143 | + | /* | |
| 144 | + | type 0 = normal visits | |
| 145 | + | type 1 = register form | |
| 146 | + | type 2 = character creation | |
| 147 | + | type 3 = fetch highscores | |
| 148 | + | type 4 = search character | |
| 149 | + | */ | |
| 150 | + | $time = time(); | |
| 151 | + | $ip = getIPLong(); | |
| 152 | + | $acc = user_logged_in() ? (int)getSession('user_id') : 0; | |
| 153 | + | db()->execute("INSERT INTO `znote_visitors_details` (`ip`, `time`, `type`, `account_id`) VALUES (?, ?, ?, ?)", [$ip, $time, $type, $acc]); | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | function something () { | |
| 157 | + | // Make acc data compatible: | |
| 158 | + | $ip = getIPLong(); | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // Secret token | |
| 162 | + | function create_token() { | |
| 163 | + | if (empty($_SESSION['token'])) { | |
| 164 | + | $_SESSION['token'] = bin2hex(random_bytes(32)); | |
| 165 | + | } | |
| 166 | + | echo '<input type="hidden" name="token" value="'.$_SESSION['token'].'">'; | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | function reset_token() { | |
| 170 | + | echo 'Reseting token<br />'; | |
| 171 | + | unset($_SESSION['token']); | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // Time based functions | |
| 175 | + | // 60 seconds to 1 minute | |
| 176 | + | function second_to_minute($seconds) { | |
| 177 | + | return ($seconds / 60); | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // 1 minute to 60 seconds | |
| 181 | + | function minute_to_seconds($minutes) { | |
| 182 | + | return ($minutes * 60); | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // 60 minutes to 1 hour | |
| 186 | + | function minute_to_hour($minutes) { | |
| 187 | + | return ($minutes / 60); | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // 1 hour to 60 minutes | |
| 191 | + | function hour_to_minute($hours) { | |
| 192 | + | return $hours * 60; | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // seconds / 60 / 60 = hours. | |
| 196 | + | function seconds_to_hours($seconds) { | |
| 197 | + | $minutes = second_to_minute($seconds); | |
| 198 | + | $hours = minute_to_hour($minutes); | |
| 199 | + | return $hours; | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | function remaining_seconds_to_clock($seconds) { | |
| 203 | + | return date("(H:i)",time() + $seconds); | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | /** | |
| 207 | + | * Check if name contains more than configured max words | |
| 208 | + | * | |
| 209 | + | * @param string $string | |
| 210 | + | * @return string|boolean | |
| 211 | + | */ | |
| 212 | + | function validate_name($string) { | |
| 213 | + | $max = config('maxW') ?? 3; | |
| 214 | + | return (str_word_count(trim($string)) > $max) ? false : trim($string); | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // Checks if an IPv4(or localhost IPv6) address is valid | |
| 218 | + | function validate_ip($ip) { | |
| 219 | + | $ipL = safeIp2Long($ip); | |
| 220 | + | $ipR = long2ip((int)$ipL); | |
| 221 | + | ||
| 222 | + | if ($ip === $ipR) { | |
| 223 | + | return true; | |
| 224 | + | } elseif ($ip=='::1') { | |
| 225 | + | return true; | |
| 226 | + | } else { | |
| 227 | + | return false; | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // Fetch a config value. Etc config('vocations') will return vocation array from config.php. | |
| 232 | + | function config($value) { | |
| 233 | + | global $config; | |
| 234 | + | return $config[$value] ?? null; | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | function serverEngineReal() { | |
| 238 | + | global $config; | |
| 239 | + | return $config['ServerEngineReal'] ?? ($config['ServerEngine'] ?? 'TFS_10'); | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | function engineIsCanary() { | |
| 243 | + | return serverEngineReal() === 'CANARY'; | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | function engineIsTFS16() { | |
| 247 | + | return serverEngineReal() === 'TFS_16'; | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | function accountField($field) { | |
| 251 | + | if ($field === 'premium_ends_at') { | |
| 252 | + | if (function_exists('znote_column_exists') && znote_column_exists('accounts', 'premium_ends_at')) { | |
| 253 | + | return '`premium_ends_at`'; | |
| 254 | + | } | |
| 255 | + | if (function_exists('znote_column_exists') && znote_column_exists('accounts', 'lastday') && znote_column_exists('accounts', 'premdays')) { | |
| 256 | + | return '(`lastday` + (`premdays` * 86400)) AS `premium_ends_at`'; | |
| 257 | + | } | |
| 258 | + | if (function_exists('znote_column_exists') && znote_column_exists('accounts', 'premdays')) { | |
| 259 | + | return '(CASE WHEN `premdays` > 0 THEN UNIX_TIMESTAMP() + (`premdays` * 86400) ELSE 0 END) AS `premium_ends_at`'; | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | if (engineIsCanary()) { | |
| 264 | + | if ($field === 'secret') return 'NULL AS `secret`'; | |
| 265 | + | } | |
| 266 | + | return '`' . $field . '`'; | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | function accountFieldList(array $fields) { | |
| 270 | + | return implode(', ', array_map('accountField', $fields)); | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | function houseCol($name) { | |
| 274 | + | if (!engineIsCanary()) return $name; | |
| 275 | + | $map = array( | |
| 276 | + | 'bid' => 'internal_bid', | |
| 277 | + | 'bid_end' => 'bid_end_date', | |
| 278 | + | 'last_bid' => 'highest_bid', | |
| 279 | + | 'highest_bidder' => 'bidder', | |
| 280 | + | ); | |
| 281 | + | return $map[$name] ?? $name; | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | function houseSelect(array $fields, $prefix = '') { | |
| 285 | + | $p = ($prefix !== '') ? '`' . $prefix . '`.' : ''; | |
| 286 | + | $out = array(); | |
| 287 | + | foreach ($fields as $f) { | |
| 288 | + | $phys = houseCol($f); | |
| 289 | + | $out[] = ($phys === $f) ? $p . '`' . $f . '`' : $p . '`' . $phys . '` AS `' . $f . '`'; | |
| 290 | + | } | |
| 291 | + | return implode(', ', $out); | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | function sqlIpWrite($ipLong) { | |
| 295 | + | if (engineIsTFS16()) { | |
| 296 | + | return "INET6_ATON('" . mysql_znote_escape_string(long2ip((int)$ipLong)) . "')"; | |
| 297 | + | } | |
| 298 | + | return "'" . (int)$ipLong . "'"; | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | function sqlIpSelect($column, $alias, $prefix = '') { | |
| 302 | + | $p = ($prefix !== '') ? '`' . $prefix . '`.' : ''; | |
| 303 | + | if (engineIsTFS16()) { | |
| 304 | + | return 'INET_ATON(INET6_NTOA(' . $p . '`' . $column . '`)) AS `' . $alias . '`'; | |
| 305 | + | } | |
| 306 | + | return $p . '`' . $column . '` AS `' . $alias . '`'; | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | // Some functions uses several configurations from config.php, so it sounds | |
| 310 | + | // smarter to give them the whole array instead of calling the function all the time. | |
| 311 | + | function fullConfig() { | |
| 312 | + | global $config; | |
| 313 | + | return $config; | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | // Capitalize Every Word In String. | |
| 317 | + | function format_character_name($name) { | |
| 318 | + | return ucwords(strtolower($name)); | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | // Gets you the actual IP address even from users behind ISP proxies and so on. | |
| 322 | + | function getIP() { | |
| 323 | + | /* | |
| 324 | + | $IP = ''; | |
| 325 | + | if (getenv('HTTP_CLIENT_IP')) { | |
| 326 | + | $IP =getenv('HTTP_CLIENT_IP'); | |
| 327 | + | } elseif (getenv('HTTP_X_FORWARDED_FOR')) { | |
| 328 | + | $IP =getenv('HTTP_X_FORWARDED_FOR'); | |
| 329 | + | } elseif (getenv('HTTP_X_FORWARDED')) { | |
| 330 | + | $IP =getenv('HTTP_X_FORWARDED'); | |
| 331 | + | } elseif (getenv('HTTP_FORWARDED_FOR')) { | |
| 332 | + | $IP =getenv('HTTP_FORWARDED_FOR'); | |
| 333 | + | } elseif (getenv('HTTP_FORWARDED')) { | |
| 334 | + | $IP = getenv('HTTP_FORWARDED'); | |
| 335 | + | } else { | |
| 336 | + | $IP = $_SERVER['REMOTE_ADDR']; | |
| 337 | + | } */ | |
| 338 | + | return $_SERVER['REMOTE_ADDR']; | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | function safeIp2Long($ip) { | |
| 342 | + | return sprintf('%u', ip2long($ip)); | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | // Gets you the actual IP address even from users in long type | |
| 346 | + | function getIPLong() { | |
| 347 | + | return safeIp2Long(getIP()); | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | // Deprecated, just use count($array) instead. | |
| 351 | + | function array_length($ar) { | |
| 352 | + | return count($ar); | |
| 353 | + | } | |
| 354 | + | // Parameter: level, returns experience for that level from an experience table. | |
| 355 | + | function level_to_experience($level) { | |
| 356 | + | return 50/3*(pow($level, 3) - 6*pow($level, 2) + 17*$level - 12); | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // Parameter: players.hide_char returns: Status word inside a font with class identifier so it can be designed later on by CSS. | |
| 360 | + | function hide_char_to_name($id) { | |
| 361 | + | $id = (int)$id; | |
| 362 | + | if ($id == 1) { | |
| 363 | + | return 'hidden'; | |
| 364 | + | } else { | |
| 365 | + | return 'visible'; | |
| 366 | + | } | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // Parameter: players.online returns: Status word inside a font with class identifier so it can be designed later on by CSS. | |
| 370 | + | function online_id_to_name($id) { | |
| 371 | + | $id = (int)$id; | |
| 372 | + | if ($id == 1) { | |
| 373 | + | return '<font class="status_online">ONLINE</font>'; | |
| 374 | + | } else { | |
| 375 | + | return '<font class="status_offline">offline</font>'; | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // Parameter: players.vocation_id. Returns: Configured vocation name. | |
| 380 | + | function vocation_id_to_name($id) { | |
| 381 | + | $vocations = config('vocations'); | |
| 382 | + | return (isset($vocations[$id]['name'])) ? $vocations[$id]['name'] : "{$id} - Unknown"; | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | // Parameter: players.name. Returns: Configured vocation id. | |
| 386 | + | function vocation_name_to_id($name) { | |
| 387 | + | $vocations = config('vocations'); | |
| 388 | + | foreach ($vocations as $id => $vocation) | |
| 389 | + | if ($vocation['name'] == $name) | |
| 390 | + | return $id; | |
| 391 | + | return false; | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | // Parameter: players.group_id. Returns: Configured group name. | |
| 395 | + | function group_id_to_name($id) { | |
| 396 | + | $positions = config('ingame_positions'); | |
| 397 | + | return $positions[$id] ?? false; | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | function gender_exist($gender) { | |
| 401 | + | // Range of allowed gender ids, fromid toid | |
| 402 | + | if ($gender >= 0 && $gender <= 1) { | |
| 403 | + | return true; | |
| 404 | + | } else { | |
| 405 | + | return false; | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | function skillid_to_name($skillid) { | |
| 410 | + | $skillname = [ | |
| 411 | + | 0 => 'fist fighting', | |
| 412 | + | 1 => 'club fighting', | |
| 413 | + | 2 => 'sword fighting', | |
| 414 | + | 3 => 'axe fighting', | |
| 415 | + | 4 => 'distance fighting', | |
| 416 | + | 5 => 'shielding', | |
| 417 | + | 6 => 'fishing', | |
| 418 | + | 7 => 'experience', | |
| 419 | + | 8 => 'magic level' | |
| 420 | + | ]; | |
| 421 | + | ||
| 422 | + | return $skillname[$skillid] ?? false; | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | // Parameter: players.town_id. Returns: Configured town name. | |
| 426 | + | function town_id_to_name($id) { | |
| 427 | + | $towns = config('towns'); | |
| 428 | + | return (array_key_exists($id, $towns)) ? $towns[$id] : 'Missing Town'; | |
| 429 | + | } | |
| 430 | + | ||
| 431 | + | // On this version we included From, because without that nowdays the email is classed directly as spam, using a From prevents that. | |
| 432 | + | function email($to, $subject, $body) { | |
| 433 | + | $headers = "From: noreply@znoteaac.com\r\nContent-Type: text/plain; charset=UTF-8"; | |
| 434 | + | mail($to, $subject, $body, $headers); | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | function logged_in_redirect() { | |
| 438 | + | if (user_logged_in() === true) { | |
| 439 | + | header('Location: myaccount.php'); | |
| 440 | + | exit; | |
| 441 | + | } | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | function protect_page() { | |
| 445 | + | if (user_logged_in() === false) { | |
| 446 | + | header('Location: protected.php'); | |
| 447 | + | exit; | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | // When function is called, you will be redirected to protect_page and deny access to rest of page, as long as you are not admin. | |
| 452 | + | function admin_only($user_data) { | |
| 453 | + | // Chris way | |
| 454 | + | $gotAccess = is_admin($user_data); | |
| 455 | + | ||
| 456 | + | if ($gotAccess == false) { | |
| 457 | + | logged_in_redirect(); | |
| 458 | + | exit(); | |
| 459 | + | } | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | function admin_roles($user_data): array { | |
| 463 | + | if (!is_array($user_data)) return array(); | |
| 464 | + | ||
| 465 | + | $identity = config('ServerEngine') === 'OTHIRE' | |
| 466 | + | ? ($user_data['id'] ?? null) | |
| 467 | + | : ($user_data['name'] ?? null); | |
| 468 | + | $owners = (array)config('page_admin_access'); | |
| 469 | + | if (in_array($identity, $owners)) { | |
| 470 | + | return array('owner'); | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | $assignments = (array)config('page_admin_roles'); | |
| 474 | + | foreach ($assignments as $account => $assigned) { | |
| 475 | + | if ((string)$account === (string)$identity) { | |
| 476 | + | $modules = is_array($assigned) ? $assigned : array($assigned); | |
| 477 | + | return array_values(array_unique(array_filter(array_map( | |
| 478 | + | static fn($module) => strtolower(trim((string)$module)), | |
| 479 | + | $modules | |
| 480 | + | ), static fn($module) => $module !== ''))); | |
| 481 | + | } | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | return array(); | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | function has_admin_panel_access($user_data): bool { | |
| 488 | + | return admin_roles($user_data) !== array(); | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // Legacy full-admin checks deliberately remain owner-only. This prevents a | |
| 492 | + | // scoped ACP role from inheriting unrestricted forum or maintenance powers. | |
| 493 | + | function is_admin($user_data) { | |
| 494 | + | return in_array('owner', admin_roles($user_data), true); | |
| 495 | + | } | |
| 496 | + | ||
| 497 | + | function array_sanitize(&$item) { | |
| 498 | + | $item = htmlentities(strip_tags(mysql_znote_escape_string($item))); | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | function sanitize($data) { | |
| 502 | + | return htmlspecialchars(strip_tags((string)($data ?? '')), ENT_QUOTES, 'UTF-8'); | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | function output_errors($errors) { | |
| 506 | + | return '<ul><li>'. implode('</li><li>', $errors) .'</li></ul>'; | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | // Resize images and create image | |
| 510 | + | function resize_imagex($file, $width, $height) { | |
| 511 | + | list($w, $h) = getimagesize($file['tmp']); | |
| 512 | + | if (!function_exists('imagecreatefromstring')) { | |
| 513 | + | return false; | |
| 514 | + | } | |
| 515 | + | $ratio = max($width/$w, $height/$h); | |
| 516 | + | $h = ceil($height / $ratio); | |
| 517 | + | $x = ($w - $width / $ratio) / 2; | |
| 518 | + | $w = ceil($width / $ratio); | |
| 519 | + | ||
| 520 | + | $path = 'engine/guildimg/'.$file['new_name']; | |
| 521 | + | ||
| 522 | + | $imgString = file_get_contents($file['tmp']); | |
| 523 | + | ||
| 524 | + | $image = imagecreatefromstring($imgString); | |
| 525 | + | $tmp = imagecreatetruecolor($width, $height); | |
| 526 | + | imagecopyresampled($tmp, $image, | |
| 527 | + | 0, 0, | |
| 528 | + | $x, 0, | |
| 529 | + | $width, $height, | |
| 530 | + | $w, $h | |
| 531 | + | ); | |
| 532 | + | ||
| 533 | + | imagegif($tmp, $path); | |
| 534 | + | imagedestroy($image); | |
| 535 | + | imagedestroy($tmp); | |
| 536 | + | ||
| 537 | + | return true; | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | function guild_logo_safe_name(string $name): ?string { | |
| 541 | + | $name = trim($name); | |
| 542 | + | if ($name === '' || strlen($name) > 60) { | |
| 543 | + | return null; | |
| 544 | + | } | |
| 545 | + | if (preg_match('#[\\\\/\x00]#', $name) || strpos($name, '..') !== false) { | |
| 546 | + | return null; | |
| 547 | + | } | |
| 548 | + | return $name; | |
| 549 | + | } | |
| 550 | + | ||
| 551 | + | // Validate guild logo | |
| 552 | + | function check_image($image) { | |
| 553 | + | $rawName = (string)($_GET['name'] ?? ''); | |
| 554 | + | $safeName = guild_logo_safe_name($rawName); | |
| 555 | + | if ($safeName === null) { | |
| 556 | + | header('Location: guilds.php?error=Invalid guild name.'); | |
| 557 | + | exit; | |
| 558 | + | } | |
| 559 | + | ||
| 560 | + | $image_data = array( | |
| 561 | + | 'new_name' => $safeName.'.gif', | |
| 562 | + | 'name' => $image['name'], | |
| 563 | + | 'tmp' => $image['tmp_name'], | |
| 564 | + | 'error' => $image['error'], | |
| 565 | + | 'size' => $image['size'], | |
| 566 | + | 'type' => $image['type'] | |
| 567 | + | ); | |
| 568 | + | ||
| 569 | + | if ($image_data['type'] !== 'image/gif') { | |
| 570 | + | header('Location: guilds.php?error=Only gif images are accepted, you uploaded:['.$image_data['type'].'].&name='. urlencode($rawName)); | |
| 571 | + | exit; | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | $check = getimagesize($image_data['tmp']); | |
| 575 | + | if (!$check) { | |
| 576 | + | header('Location: guilds.php?error=Uploaded image is invalid.&name='. urlencode($rawName)); | |
| 577 | + | exit; | |
| 578 | + | } | |
| 579 | + | ||
| 580 | + | if ($check['mime'] !== 'image/gif') { | |
| 581 | + | header('Location: guilds.php?error=Only gif images accepted, you uploaded:['.$check['mime'].'].&name='. urlencode($rawName)); | |
| 582 | + | exit; | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | $path_info = pathinfo($image_data['name']); | |
| 586 | + | if ($path_info['extension'] !== 'gif') { | |
| 587 | + | header('Location: guilds.php?error=Only gif images accepted, you uploaded:['.$path_info['extension'].'].&name='. urlencode($rawName)); | |
| 588 | + | exit; | |
| 589 | + | } | |
| 590 | + | ||
| 591 | + | // Resize image | |
| 592 | + | if (resize_imagex($image_data, 100, 100)) { | |
| 593 | + | header('Location: guilds.php?name='. urlencode($rawName)); | |
| 594 | + | exit; | |
| 595 | + | } | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | function generateRandomString($length = 16) { | |
| 599 | + | return substr(strtoupper(bin2hex(random_bytes($length))), 0, $length); | |
| 600 | + | } | |
| 601 | + | ||
| 602 | + | function verifyGoogleReCaptcha($postResponse = null) { | |
| 603 | + | if(!isset($postResponse) || empty($postResponse)) { | |
| 604 | + | return false; | |
| 605 | + | } | |
| 606 | + | ||
| 607 | + | $recaptcha_api_url = 'https://www.google.com/recaptcha/api/siteverify'; | |
| 608 | + | $secretKey = config('captcha_secret_key'); | |
| 609 | + | $ip = $_SERVER['REMOTE_ADDR']; | |
| 610 | + | $params = 'secret='.$secretKey.'&response='.$postResponse.'&remoteip='.$ip; | |
| 611 | + | ||
| 612 | + | $useCurl = config('captcha_use_curl'); | |
| 613 | + | if($useCurl) { | |
| 614 | + | $curl_connection = curl_init($recaptcha_api_url); | |
| 615 | + | ||
| 616 | + | curl_setopt($curl_connection, CURLOPT_CONNECTTIMEOUT, 5); | |
| 617 | + | curl_setopt($curl_connection, CURLOPT_RETURNTRANSFER, true); | |
| 618 | + | curl_setopt($curl_connection, CURLOPT_SSL_VERIFYPEER, false); | |
| 619 | + | curl_setopt($curl_connection, CURLOPT_FOLLOWLOCATION, 0); | |
| 620 | + | curl_setopt($curl_connection, CURLOPT_POSTFIELDS, $params); | |
| 621 | + | ||
| 622 | + | $response = curl_exec($curl_connection); | |
| 623 | + | curl_close($curl_connection); | |
| 624 | + | } else { | |
| 625 | + | $response = file_get_contents($recaptcha_api_url . '?' . $params); | |
| 626 | + | } | |
| 627 | + | ||
| 628 | + | $json = json_decode($response); | |
| 629 | + | return isset($json->success) && $json->success; | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | // html encoding function (encode any string to valid UTF-8 HTML) | |
| 633 | + | function hhb_tohtml(/*string*/ $str)/*:string*/ { | |
| 634 | + | return htmlentities($str, ENT_QUOTES | ENT_HTML401 | ENT_SUBSTITUTE | ENT_DISALLOWED, 'UTF-8', true); | |
| 635 | + | } | |
| 636 | + | ||
| 637 | + | // php5-compatibile version of php7's random_bytes() | |
| 638 | + | // $crypto_strong: a boolean value that determines if the algorithm used was "cryptographically strong" | |
| 639 | + | function random_bytes_compat($length, &$crypto_strong = null) { | |
| 640 | + | $crypto_strong = false; | |
| 641 | + | if (!is_int($length)) { | |
| 642 | + | throw new \InvalidArgumentException("argument 1 must be an int, is " . gettype($length)); | |
| 643 | + | } | |
| 644 | + | if ($length < 0) { | |
| 645 | + | throw new \InvalidArgumentException("length must be >= 0"); | |
| 646 | + | } | |
| 647 | + | if (is_callable("random_bytes")) { | |
| 648 | + | $crypto_strong = true; | |
| 649 | + | return random_bytes($length); | |
| 650 | + | } | |
| 651 | + | if (is_callable("openssl_random_pseudo_bytes")) { | |
| 652 | + | return openssl_random_pseudo_bytes($length, $crypto_strong); | |
| 653 | + | } | |
| 654 | + | $ret = @file_get_contents("/dev/urandom", false, null, 0, $length); | |
| 655 | + | if (is_string($ret) && strlen($ret) === $length) { | |
| 656 | + | $crypto_strong = true; | |
| 657 | + | return $ret; | |
| 658 | + | } | |
| 659 | + | // fallback to non-cryptographically-secure mt_rand() implementation... | |
| 660 | + | $crypto_strong = false; | |
| 661 | + | $ret = ""; | |
| 662 | + | for ($i = 0; $i < $length; ++$i) { | |
| 663 | + | $ret .= chr(mt_rand(0, 255)); | |
| 664 | + | } | |
| 665 | + | return $ret; | |
| 666 | + | } | |
| 667 | + | ||
| 668 | + | // hash_equals legacy support < 5.6 | |
| 669 | + | if(!function_exists('hash_equals')) { | |
| 670 | + | function hash_equals($str1, $str2) { | |
| 671 | + | if(strlen($str1) != strlen($str2)) { | |
| 672 | + | return false; | |
| 673 | + | } | |
| 674 | + | $res = $str1 ^ $str2; | |
| 675 | + | $ret = 0; | |
| 676 | + | for($i = strlen($res) - 1; $i >= 0; $i--) { | |
| 677 | + | $ret |= ord($res[$i]); | |
| 678 | + | } | |
| 679 | + | return !$ret; | |
| 680 | + | } | |
| 681 | + | } | |
| 682 | + | /** | |
| 683 | + | * Path to the CA bundle shipped with ZnoteX, or '' when the PHP install | |
| 684 | + | * already has one configured. | |
| 685 | + | * | |
| 686 | + | * PHP on Windows ships without a certificate store: unless curl.cainfo is set | |
| 687 | + | * in php.ini, every HTTPS request fails with curl error 60. The repo carries a | |
| 688 | + | * bundle for exactly this reason - ipn.php has always pointed CURLOPT_CAINFO | |
| 689 | + | * at it - so anything making outbound HTTPS calls should use this rather than | |
| 690 | + | * asking the admin to edit php.ini. | |
| 691 | + | * | |
| 692 | + | * Absolute on purpose: callers may run with any working directory. | |
| 693 | + | */ | |
| 694 | + | function znote_cainfo() { | |
| 695 | + | if (ini_get('curl.cainfo') !== '' || ini_get('openssl.cafile') !== '') { | |
| 696 | + | return ''; | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | $bundle = dirname(__DIR__) . '/cert/cacert.pem'; | |
| 700 | + | ||
| 701 | + | return is_file($bundle) ? $bundle : ''; | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | function znote_media_base(string $srv): string { | |
| 705 | + | $srv = trim($srv); | |
| 706 | + | if ($srv === '') return ''; | |
| 707 | + | if (preg_match('#^(https?:)?//#i', $srv)) return rtrim($srv, '/'); | |
| 708 | + | if (preg_match('#^[a-zA-Z]:[\\/]#', $srv) || strncmp($srv, '\\', 2) === 0) return ''; | |
| 709 | + | if ($srv[0] === '/') return rtrim($srv, '/'); | |
| 710 | + | return 'http://' . rtrim($srv, '/'); | |
| 711 | + | } | |
| 712 | + | ||
| 713 | + | function znote_item_image_dir(): string { | |
| 714 | + | static $dir = null; | |
| 715 | + | if ($dir !== null) return $dir; | |
| 716 | + | global $config; | |
| 717 | + | $srv = trim((string) ($config['shop']['imageServer'] ?? '')); | |
| 718 | + | if ($srv === '' || preg_match('#^(https?:)?//#i', $srv) || (isset($srv[0]) && $srv[0] === '/' && !@is_dir($srv))) return $dir = ''; | |
| 719 | + | $real = @realpath($srv); | |
| 720 | + | return $dir = ($real !== false && @is_dir($real)) ? $real : ''; | |
| 721 | + | } | |
| 722 | + | ||
| 723 | + | function znote_item_image_url(int $id, ?string $type = null): string { | |
| 724 | + | if ($id <= 0) return ''; | |
| 725 | + | global $config; | |
| 726 | + | if (znote_item_image_dir() !== '') return 'index.php?znote_item_img=' . $id; | |
| 727 | + | $type = ($type !== null && $type !== '') ? $type : (string) ($config['shop']['imageType'] ?? 'gif'); | |
| 728 | + | $type = preg_replace('/[^a-z0-9]/i', '', $type) ?: 'gif'; | |
| 729 | + | $base = znote_media_base((string) ($config['shop']['imageServer'] ?? '')); | |
| 730 | + | return $base === '' ? '' : $base . '/' . $id . '.' . $type; | |
| 731 | + | } | |
| 732 | + | ||
| 733 | + | function znote_outfit_image_base(): string { | |
| 734 | + | global $config; | |
| 735 | + | return znote_media_base((string) ($config['show_outfits']['imageServer'] ?? '')); | |
| 736 | + | } |
| @@ -0,0 +1,36 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | function znote_health_issues(): array | |
| 4 | + | { | |
| 5 | + | $issues = array(); | |
| 6 | + | ||
| 7 | + | if (!extension_loaded('mysqli')) { | |
| 8 | + | $issues[] = array('label' => 'PHP extension', 'detail' => 'mysqli is not loaded.'); | |
| 9 | + | } | |
| 10 | + | ||
| 11 | + | $root = dirname(__DIR__, 2); | |
| 12 | + | $free = @disk_free_space($root); | |
| 13 | + | $total = @disk_total_space($root); | |
| 14 | + | if ($free !== false && $total !== false && $total > 0) { | |
| 15 | + | $percentFree = ($free / $total) * 100; | |
| 16 | + | if ($percentFree < 5 || $free < 524288000) { | |
| 17 | + | $issues[] = array( | |
| 18 | + | 'label' => 'Disk space', | |
| 19 | + | 'detail' => number_format($free / 1048576, 0) . ' MB free (' . number_format($percentFree, 1) . '%).', | |
| 20 | + | ); | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | if (function_exists('znote_migrations_status')) { | |
| 25 | + | foreach (znote_migrations_status() as $name => $migration) { | |
| 26 | + | if (($migration['state'] ?? '') === 'changed') { | |
| 27 | + | $issues[] = array( | |
| 28 | + | 'label' => 'Migration changed', | |
| 29 | + | 'detail' => (string) $name . ' was applied but the file no longer matches what ran.', | |
| 30 | + | ); | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | return $issues; | |
| 36 | + | } |
| @@ -0,0 +1,49 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | function getItemList(): array { | |
| 4 | + | return parseItems(); | |
| 5 | + | } | |
| 6 | + | ||
| 7 | + | function getItemById(int $id): string|false { | |
| 8 | + | static $items = null; | |
| 9 | + | ||
| 10 | + | if ($items === null) { | |
| 11 | + | $items = parseItems(); | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | return $items[$id] ?? false; | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | function parseItems(): array { | |
| 18 | + | // ZnoteAAC compatible | |
| 19 | + | $serverPath = function_exists('Config') | |
| 20 | + | ? Config('server_path') | |
| 21 | + | : ($GLOBALS['config']['server_path'] ?? null); | |
| 22 | + | ||
| 23 | + | if (!$serverPath) { | |
| 24 | + | return []; | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | $file = $serverPath . '/data/items/items.xml'; | |
| 28 | + | ||
| 29 | + | if (!file_exists($file)) { | |
| 30 | + | return []; | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | libxml_use_internal_errors(true); | |
| 34 | + | $xml = simplexml_load_file($file); | |
| 35 | + | ||
| 36 | + | if ($xml === false) { | |
| 37 | + | return []; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | $itemList = []; | |
| 41 | + | ||
| 42 | + | foreach ($xml->children() as $item) { | |
| 43 | + | if (isset($item['id'], $item['name'])) { | |
| 44 | + | $itemList[(int)$item['id']] = (string)$item['name']; | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | return $itemList; | |
| 49 | + | } |
| @@ -0,0 +1,132 @@ | |||
| 1 | + | <?php | |
| 2 | + | ||
| 3 | + | const ZNOTE_LANDING_DIR = 'landing'; | |
| 4 | + | ||
| 5 | + | function landing_root(): string | |
| 6 | + | { | |
| 7 | + | return dirname(__DIR__, 2) . '/' . ZNOTE_LANDING_DIR; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | function landing_enabled(): bool | |
| 11 | + | { | |
| 12 | + | return function_exists('setting') && (string)setting('landing.enabled', '') === '1'; | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | /** The file inside landing/ that is served. */ | |
| 16 | + | function 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/. */ | |
| 25 | + | function 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. */ | |
| 35 | + | function 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 | + | ||
| 51 | + | function landing_path(): string | |
| 52 | + | { | |
| 53 | + | return landing_root() . '/' . landing_file(); | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | function 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 | + | */ | |
| 65 | + | function 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 | + | */ | |
| 78 | + | function 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 | + | */ | |
| 93 | + | function 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 | + | */ | |
| 118 | + | function 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 | + | } |