Plan Your Visit landing page for visit.waterschurch.org
Next.js 16 static export + PHP 8 API for cPanel/Apache hosting. - Location-aware campus finder (IP estimate, optional precise location, ZIP search) - Three-step Plan Your Visit form with separate email/SMS consent - PHP API: validation, anti-spam, rate limiting, SQLite lead store, Mailchimp and Sinch Engage adapters, idempotent retries via cron - Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and consent-aware analytics hooks - Vitest and PHP test suites, cPanel deployment script and docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Minimal structured logger. Writes JSON lines to a private log file and
|
||||
* redacts anything that looks like personal data or a secret.
|
||||
*/
|
||||
final class Log
|
||||
{
|
||||
private static string $file = '';
|
||||
private static int $level = 1;
|
||||
private const LEVELS = ['debug' => 0, 'info' => 1, 'warn' => 2, 'error' => 3];
|
||||
private const REDACT_KEYS = ['email', 'phone', 'firstname', 'first_name', 'lastname', 'last_name', 'authorization', 'api_key', 'apikey', 'secret', 'token', 'password', 'questions', 'destination_number', 'email_address'];
|
||||
|
||||
public static function init(string $file, ?string $level = 'info'): void
|
||||
{
|
||||
self::$file = $file;
|
||||
self::$level = self::LEVELS[strtolower($level ?? 'info')] ?? 1;
|
||||
$dir = dirname($file);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0750, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $ctx */
|
||||
public static function debug(string $msg, array $ctx = []): void { self::write('debug', $msg, $ctx); }
|
||||
/** @param array<string,mixed> $ctx */
|
||||
public static function info(string $msg, array $ctx = []): void { self::write('info', $msg, $ctx); }
|
||||
/** @param array<string,mixed> $ctx */
|
||||
public static function warn(string $msg, array $ctx = []): void { self::write('warn', $msg, $ctx); }
|
||||
/** @param array<string,mixed> $ctx */
|
||||
public static function error(string $msg, array $ctx = []): void { self::write('error', $msg, $ctx); }
|
||||
|
||||
/** @param array<string,mixed> $ctx */
|
||||
private static function write(string $level, string $msg, array $ctx): void
|
||||
{
|
||||
if ((self::LEVELS[$level] ?? 1) < self::$level) {
|
||||
return;
|
||||
}
|
||||
$line = json_encode([
|
||||
'ts' => gmdate('c'),
|
||||
'level' => $level,
|
||||
'msg' => $msg,
|
||||
'ctx' => self::redact($ctx),
|
||||
], JSON_UNESCAPED_SLASHES) . "\n";
|
||||
if (self::$file !== '' && (is_writable(dirname(self::$file)) || is_writable(self::$file))) {
|
||||
@file_put_contents(self::$file, $line, FILE_APPEND | LOCK_EX);
|
||||
} else {
|
||||
error_log(rtrim($line));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
public static function redact($value, string $key = ''): mixed
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$out = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$out[$k] = self::redact($v, (string)$k);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
if (in_array(strtolower($key), self::REDACT_KEYS, true)) {
|
||||
return self::mask($value);
|
||||
}
|
||||
// Inline redaction of emails, phone numbers and bearer-like tokens inside free text
|
||||
$value = preg_replace('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', '[email]', $value) ?? $value;
|
||||
$value = preg_replace('/\+?\d[\d\s().-]{8,}\d/', '[phone]', $value) ?? $value;
|
||||
return preg_replace('/(Basic|Bearer)\s+[A-Za-z0-9+\/=._-]{8,}/', '$1 [redacted]', $value) ?? $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function mask(string $v): string
|
||||
{
|
||||
$len = strlen($v);
|
||||
if ($len === 0) {
|
||||
return '';
|
||||
}
|
||||
if ($len <= 4) {
|
||||
return str_repeat('*', $len);
|
||||
}
|
||||
return substr($v, 0, 2) . str_repeat('*', max(1, $len - 4)) . substr($v, -2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user