0; } function isAdmin(): bool{ return !empty($_SESSION['admin']); } function requireLogin(string $back = ''): void { if (!authed()) { flash('Please log in to continue.', 'warning'); go('/login.php?next='.urlencode($back ?: $_SERVER['REQUEST_URI'])); } } function requireAdmin(): void { if (!isAdmin()) { flash('Access denied.', 'error'); go('/index.php'); } } function loginUser(array $u): void { session_regenerate_id(true); $_SESSION['uid'] = $u['id']; $_SESSION['uname'] = $u['username']; $_SESSION['admin'] = (bool)$u['is_admin']; qrun("UPDATE users SET last_login=datetime('now') WHERE id=?", [$u['id']]); } function logoutUser(): void { session_unset(); session_destroy(); session_start(); session_regenerate_id(true); } /* ── Flash ────────────────────────────────────────────── */ function flash(string $msg, string $type = 'info'): void { $_SESSION['_flash'][] = [$type, $msg]; } function getFlashes(): array { $f = $_SESSION['_flash'] ?? []; unset($_SESSION['_flash']); return $f; } /* ── Output ───────────────────────────────────────────── */ function e(mixed $v): string { return htmlspecialchars((string)($v ?? ''), ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8'); } function go(string $url): never { header('Location:'.$url); exit; } /* ── Input ────────────────────────────────────────────── */ function p(string $k, mixed $d = ''): mixed { return $_POST[$k] ?? $d; } function g(string $k, mixed $d = ''): mixed { return $_GET[$k] ?? $d; } function iget(string $k): int { return (int)($_POST[$k] ?? $_GET[$k] ?? 0); } function ps(string $k): string { return trim((string)($_POST[$k] ?? '')); } function gs(string $k): string { return trim((string)($_GET[$k] ?? '')); } function isPost(): bool { return strtoupper($_SERVER['REQUEST_METHOD']) === 'POST'; } /* ── CSRF ─────────────────────────────────────────────── */ function csrf(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24)); return $_SESSION['csrf']; } function csrfField(): string { return ''; } function csrfCheck(): void { if (!hash_equals(csrf(), p('_csrf', ''))) { http_response_code(403); die('Invalid CSRF token.'); } } /* ── Settings ─────────────────────────────────────────── */ function setting(string $k, string $def = ''): string { $v = qval("SELECT value FROM settings WHERE key=?", [$k]); return ($v !== false && $v !== null) ? (string)$v : $def; } function setSetting(string $k, string $v): void { qrun("INSERT INTO settings(key,value)VALUES(?,?)ON CONFLICT(key)DO UPDATE SET value=excluded.value", [$k,$v]); } /* ── Ownership ────────────────────────────────────────── */ function owns(int $bizId): bool { if (isAdmin()) return true; if (!authed()) return false; return (bool)qval("SELECT 1 FROM business_owners WHERE user_id=? AND business_id=?", [uid(),$bizId]); } /* ── Stars ────────────────────────────────────────────── */ function starDisplay(float $avg): string { $r = (int)round($avg); $out = ''; for ($i = 1; $i <= 5; $i++) $out .= ''; return $out.''; } function starPicker(string $name = 'rating', int $sel = 0): string { $out = '
'; for ($i = 5; $i >= 1; $i--) { $chk = $sel === $i ? 'checked' : ''; $out .= ''; $out .= ''; } return $out.'
'; } /* ── Date / Money ─────────────────────────────────────── */ function ago(mixed $dt): string { if ($dt === null || $dt === '' || $dt === false) return ''; $ts = is_int($dt) ? $dt : @strtotime((string)$dt); if (!$ts) return ''; $d = time() - $ts; if ($d < 60) return 'Just now'; if ($d < 3600) return floor($d / 60).'m ago'; if ($d < 86400) return floor($d / 3600).'h ago'; if ($d < 604800) return floor($d / 86400).'d ago'; return date('M j, Y', $ts); } function fdate(mixed $dt, string $fmt = 'M j, Y'): string { if ($dt === null || $dt === '' || $dt === false) return ''; $ts = is_int($dt) ? $dt : @strtotime((string)$dt); return $ts ? date($fmt, $ts) : ''; } function money(float $n): string { return '$'.number_format($n, 2); } /* ── Misc ─────────────────────────────────────────────── */ function alertIcon(mixed $t): string { return match((string)($t ?? '')) { 'news' => '📰', 'event' => '🎉', 'warning' => '⚠️', 'sale' => '🏷️', default => 'ℹ️', }; } function parseHours(mixed $json): array { $s = (string)($json ?? ''); if ($s === '' || $s === '[]' || $s === 'null') return []; $decoded = json_decode($s, true); if (!is_array($decoded)) return []; // Must be an object (associative), not a plain list foreach (array_keys($decoded) as $key) { if (!is_string($key)) return []; } return $decoded; } function makeSlug(string $s): string { return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($s)), '-'); } function uniqueSlug(string $base): string { $slug = makeSlug($base); $i = 0; while (qval("SELECT id FROM businesses WHERE slug=?", [$slug.($i ? "-$i" : "")])) $i++; return $slug . ($i ? "-$i" : ""); } function businessVideoProvider(string $url): string { $host = strtolower((string)(parse_url(trim($url), PHP_URL_HOST) ?? '')); $host = preg_replace('/^www\./', '', $host); if ($host === 'youtu.be' || $host === 'youtube.com' || str_ends_with($host, '.youtube.com') || $host === 'youtube-nocookie.com' || str_ends_with($host, '.youtube-nocookie.com')) return 'YouTube'; if ($host === 'vimeo.com' || str_ends_with($host, '.vimeo.com')) return 'Vimeo'; return ''; } function validBusinessVideoUrl(string $url): bool { $url = trim($url); if ($url === '') return true; $scheme = strtolower((string)(parse_url($url, PHP_URL_SCHEME) ?? '')); return in_array($scheme, ['http','https'], true) && businessVideoProvider($url) !== ''; } /* ── Email verification / Mailgun ─────────────────────── */ function requestBaseUrl(): string { $configured = trim(setting('site_base_url', '')); if ($configured !== '') return rtrim($configured, '/'); $host = (string)($_SERVER['HTTP_HOST'] ?? ''); if ($host === '') return ''; $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? '') === '443'); return ($https ? 'https' : 'http').'://'.$host; } function absoluteUrl(string $path): string { $base = requestBaseUrl(); if ($base === '') return $path; return rtrim($base, '/').'/'.ltrim($path, '/'); } function mailgunEndpoint(): string { $region = strtolower(setting('mailgun_region', 'us')); $base = $region === 'eu' ? 'https://api.eu.mailgun.net' : 'https://api.mailgun.net'; return $base.'/v3/'.rawurlencode(setting('mailgun_domain', '')).'/messages'; } function mailgunConfigured(): bool { return setting('mailgun_domain', '') !== '' && setting('mailgun_api_key', '') !== ''; } function emailFromAddress(): string { $from = trim(setting('mailgun_from_email', '')); if ($from !== '') return $from; $domain = setting('mailgun_domain', ''); return $domain !== '' ? 'postmaster@'.$domain : ''; } function formattedFromAddress(): string { $name = trim(setting('mailgun_from_name', 'My Keyser')); $email = emailFromAddress(); return $name !== '' ? "$name <$email>" : $email; } function createEmailVerificationToken(int $userId): string { qrun("UPDATE email_verification_tokens SET used_at=datetime('now') WHERE user_id=? AND purpose='email_verify' AND used_at IS NULL", [$userId]); $token = bin2hex(random_bytes(32)); qrun( "INSERT INTO email_verification_tokens(user_id,token_hash,expires_at)VALUES(?,?,datetime('now','+24 hours'))", [$userId, hash('sha256', $token)] ); return $token; } function mailgunSend(string $to, string $subject, string $text, string $html = ''): array { if (!mailgunConfigured()) return [false, 'Mailgun domain and API key are required.']; $from = formattedFromAddress(); if ($from === '' || !filter_var(emailFromAddress(), FILTER_VALIDATE_EMAIL)) { return [false, 'A valid Mailgun from email is required.']; } if (!filter_var($to, FILTER_VALIDATE_EMAIL)) return [false, 'Recipient email is invalid.']; $fields = [ 'from' => $from, 'to' => $to, 'subject' => $subject, 'text' => $text, ]; if (setting('mailgun_send_mode', 'html') === 'html' && $html !== '') { $fields['html'] = $html; } $endpoint = mailgunEndpoint(); $apiKey = setting('mailgun_api_key', ''); if (function_exists('curl_init')) { $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $fields, CURLOPT_USERPWD => 'api:'.$apiKey, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); $body = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($errno) return [false, $error ?: 'Mailgun request failed.']; if ($status < 200 || $status >= 300) return [false, 'Mailgun returned HTTP '.$status.': '.substr((string)$body, 0, 180)]; return [true, '']; } $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Authorization: Basic ".base64_encode('api:'.$apiKey)."\r\n". "Content-Type: application/x-www-form-urlencoded\r\n", 'content' => http_build_query($fields), 'timeout' => 20, 'ignore_errors' => true, ], ]); $body = @file_get_contents($endpoint, false, $context); $statusLine = $http_response_header[0] ?? ''; if (!preg_match('/\s(2\d\d)\s/', $statusLine)) { return [false, trim(($statusLine ?: 'Mailgun request failed.').' '.substr((string)$body, 0, 180))]; } return [true, '']; } function emailShellHtml(string $title, string $intro, string $bodyHtml, string $buttonText = '', string $buttonUrl = ''): string { $site = e(setting('site_name', 'My Keyser')); $titleEsc = e($title); $introEsc = e($intro); $button = ''; if ($buttonText !== '' && $buttonUrl !== '') { $button = ''.e($buttonText).''; } return ''. '
'.$introEsc.'
'. ''. '
'. '
'. '
'.$site.'
'. '

'.$titleEsc.'

'. '
'. $bodyHtml. ''.$button.'
'. '

Keyser, West Virginia - local businesses, events, and community information.

'. '
'; } function verificationEmailContent(array $user, string $verifyUrl): array { $name = (string)($user['username'] ?? 'there'); $isOwner = ($user['member_type'] ?? 'member') === 'business_owner'; $title = $isOwner ? 'Verify Your Business Owner Account' : 'Verify Your Account'; $subject = $isOwner ? 'Verify your My Keyser business owner account' : 'Verify your My Keyser account'; $text = "Hi $name,\n\n". "Thanks for registering with My Keyser. Please verify your email address before signing in:\n\n". "$verifyUrl\n\n". "This link expires in 24 hours.\n\n". ($isOwner ? "After verification, your business claim will remain queued and we will contact you within a couple of business days.\n\n" : ''). "My Keyser"; $htmlBody = '

Hi '.e($name).',

'. '

Thanks for registering with My Keyser. Please verify your email address before signing in.

'. ($isOwner ? '

After verification, your business claim will stay queued for review and our team will contact you within a couple of business days.

' : ''). '

This link expires in 24 hours.

'; return [$subject, $text, emailShellHtml($title, 'Verify your My Keyser account.', $htmlBody, 'Verify Email', $verifyUrl)]; } function registrationCompleteEmailContent(array $user): array { $name = (string)($user['username'] ?? 'there'); $isOwner = ($user['member_type'] ?? 'member') === 'business_owner'; if ($isOwner) { $subject = 'Thank you for claiming your My Keyser business page'; $text = "Hi $name,\n\nThank you for registering as a business owner with My Keyser.\n\nYour email is verified and you can now log in. Our team will contact you within a couple of business days for a verification call before assigning your business page.\n\nThank you for helping keep Keyser's directory accurate.\n\nMy Keyser"; $body = '

Hi '.e($name).',

'. '

Thank you for registering as a business owner with My Keyser. Your email is verified and you can now log in.

'. '

Our team will contact you within a couple of business days for a verification call before assigning your business page.

'. '

Thank you for helping keep Keyser’s directory accurate.

'; return [$subject, $text, emailShellHtml('Thanks, Business Owner', 'Your business owner account is verified.', $body, 'Log In', absoluteUrl('/login.php'))]; } $subject = 'Thank you for registering with My Keyser'; $text = "Hi $name,\n\nThank you for registering with My Keyser. Your email is verified and you can now log in.\n\nWe are glad to have you in the Keyser community.\n\nMy Keyser"; $body = '

Hi '.e($name).',

'. '

Thank you for registering with My Keyser. Your email is verified and you can now log in.

'. '

We are glad to have you in the Keyser community.

'; return [$subject, $text, emailShellHtml('Thanks For Registering', 'Your My Keyser account is verified.', $body, 'Log In', absoluteUrl('/login.php'))]; } function sendVerificationEmail(array $user, string $token): array { $url = absoluteUrl('/verify-email.php?token='.rawurlencode($token)); [$subject, $text, $html] = verificationEmailContent($user, $url); return mailgunSend((string)$user['email'], $subject, $text, $html); } function sendRegistrationCompleteEmail(array $user): array { [$subject, $text, $html] = registrationCompleteEmailContent($user); return mailgunSend((string)$user['email'], $subject, $text, $html); }