Files
MyKeyser/includes/functions.php
T

319 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/* ── Auth ─────────────────────────────────────────────── */
function uid(): int { return (int)($_SESSION['uid'] ?? 0); }
function uname(): string{ return (string)($_SESSION['uname'] ?? ''); }
function authed(): bool { return uid() > 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 '<input type="hidden" name="_csrf" value="'.e(csrf()).'">'; }
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 = '<span class="stars">';
for ($i = 1; $i <= 5; $i++) $out .= '<span class="star'.($i<=$r?' on':'').'">★</span>';
return $out.'</span>';
}
function starPicker(string $name = 'rating', int $sel = 0): string {
$out = '<div class="star-picker">';
for ($i = 5; $i >= 1; $i--) {
$chk = $sel === $i ? 'checked' : '';
$out .= '<input type="radio" id="sr'.$i.'" name="'.$name.'" value="'.$i.'" '.$chk.'>';
$out .= '<label for="sr'.$i.'">★</label>';
}
return $out.'</div>';
}
/* ── 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 = '<tr><td style="padding:22px 0 6px"><a href="'.e($buttonUrl).'" style="display:inline-block;background:linear-gradient(135deg,#ffd166,#26f4ff);color:#03050b;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:800;letter-spacing:1px;text-transform:uppercase;padding:13px 20px;border-radius:6px">'.e($buttonText).'</a></td></tr>';
}
return '<!doctype html><html><body style="margin:0;background:#03050b;color:#f2f4ff;font-family:Arial,sans-serif">'.
'<div style="display:none;max-height:0;overflow:hidden;color:transparent">'.$introEsc.'</div>'.
'<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background:radial-gradient(circle at 15% 0%,rgba(38,244,255,.18),transparent 340px),radial-gradient(circle at 85% 10%,rgba(255,79,216,.14),transparent 360px),#03050b;padding:34px 14px">'.
'<tr><td align="center"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:620px;background:#0e172b;border:1px solid rgba(38,244,255,.28);border-radius:8px;overflow:hidden;box-shadow:0 18px 44px rgba(0,0,0,.55)">'.
'<tr><td style="padding:24px 26px;border-bottom:1px solid rgba(38,244,255,.18);background:linear-gradient(135deg,rgba(38,244,255,.12),rgba(255,79,216,.08),rgba(255,209,102,.08))">'.
'<div style="font-size:12px;letter-spacing:4px;color:#26f4ff;text-transform:uppercase;font-weight:700">'.$site.'</div>'.
'<h1 style="margin:10px 0 0;font-family:Georgia,serif;font-size:32px;line-height:1.1;color:#ffffff">'.$titleEsc.'</h1>'.
'</td></tr><tr><td style="padding:26px;color:#aab7cf;font-size:16px;line-height:1.75">'.
$bodyHtml.
'<table role="presentation" cellspacing="0" cellpadding="0">'.$button.'</table>'.
'<p style="margin:24px 0 0;color:#65768f;font-size:13px">Keyser, West Virginia - local businesses, events, and community information.</p>'.
'</td></tr></table></td></tr></table></body></html>';
}
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 = '<p style="margin:0 0 16px;color:#f2f4ff">Hi '.e($name).',</p>'.
'<p style="margin:0 0 16px">Thanks for registering with My Keyser. Please verify your email address before signing in.</p>'.
($isOwner ? '<p style="margin:0 0 16px">After verification, your business claim will stay queued for review and our team will contact you within a couple of business days.</p>' : '').
'<p style="margin:0;color:#65768f;font-size:13px">This link expires in 24 hours.</p>';
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 = '<p style="margin:0 0 16px;color:#f2f4ff">Hi '.e($name).',</p>'.
'<p style="margin:0 0 16px">Thank you for registering as a business owner with My Keyser. Your email is verified and you can now log in.</p>'.
'<p style="margin:0 0 16px">Our team will contact you within a couple of business days for a verification call before assigning your business page.</p>'.
'<p style="margin:0">Thank you for helping keep Keyser&rsquo;s directory accurate.</p>';
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 = '<p style="margin:0 0 16px;color:#f2f4ff">Hi '.e($name).',</p>'.
'<p style="margin:0 0 16px">Thank you for registering with My Keyser. Your email is verified and you can now log in.</p>'.
'<p style="margin:0">We are glad to have you in the Keyser community.</p>';
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);
}