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.
''.
'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);
}