- [index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/index.php): events now appear before featured businesses, plus forum pretty-route fallback.
[events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/events.php) and [admin/events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/events.php): event image uploads. [menu.php](/Users/tyemeclifford/Documents/GH/MyKeyser/menu.php), [admin/menus.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/menus.php), and [business.php](/Users/tyemeclifford/Documents/GH/MyKeyser/business.php): one configurable menu item image slot. [account.php](/Users/tyemeclifford/Documents/GH/MyKeyser/account.php): circular avatar upload and crop controls. [forum.php](/Users/tyemeclifford/Documents/GH/MyKeyser/forum.php), [admin/forum.php](/Users/tyemeclifford/Documents/GH/MyKeyser/admin/forum.php), [.htaccess](/Users/tyemeclifford/Documents/GH/MyKeyser/.htaccess), and [forum/index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/forum/index.php): forum, categories, topic/reply uploads, permalinks, toggle/moderation. [includes/db.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/db.php) and [includes/functions.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/functions.php): schema upgrades and shared upload/image helpers. [assets/css/style.css](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/css/style.css): neon styling for forum, images, avatars, attachments.
This commit is contained in:
+137
-1
@@ -48,9 +48,18 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
if (!_hasColumn($db, 'users', 'member_type')) {
|
||||
$db->exec("ALTER TABLE users ADD COLUMN member_type TEXT NOT NULL DEFAULT 'member'");
|
||||
}
|
||||
if (!_hasColumn($db, 'users', 'avatar_path')) {
|
||||
$db->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (!_hasColumn($db, 'businesses', 'video_url')) {
|
||||
$db->exec("ALTER TABLE businesses ADD COLUMN video_url TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (!_hasColumn($db, 'events', 'image_path')) {
|
||||
$db->exec("ALTER TABLE events ADD COLUMN image_path TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (!_hasColumn($db, 'menu_items', 'image_path')) {
|
||||
$db->exec("ALTER TABLE menu_items ADD COLUMN image_path TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS business_edit_requests (
|
||||
@@ -86,6 +95,51 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_tokens_user ON email_verification_tokens(user_id, purpose, used_at);
|
||||
CREATE TABLE IF NOT EXISTS forum_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_topics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL REFERENCES forum_categories(id) ON DELETE RESTRICT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_replies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
topic_id INTEGER NOT NULL REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_attachments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
topic_id INTEGER REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||||
reply_id INTEGER REFERENCES forum_replies(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
file_path TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL DEFAULT '',
|
||||
mime_type TEXT NOT NULL DEFAULT '',
|
||||
file_size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
CHECK (topic_id IS NOT NULL OR reply_id IS NOT NULL)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_forum_topics_category ON forum_topics(category_id, updated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_forum_replies_topic ON forum_replies(topic_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_forum_attachments_topic ON forum_attachments(topic_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_forum_attachments_reply ON forum_attachments(reply_id);
|
||||
");
|
||||
|
||||
$defaults = [
|
||||
@@ -97,9 +151,30 @@ function _ensureAppUpgrades(PDO $db): void {
|
||||
'mailgun_from_name' => 'Discover Keyser WV',
|
||||
'mailgun_from_email' => '',
|
||||
'mailgun_send_mode' => 'html',
|
||||
'event_upload_max_mb' => '5',
|
||||
'menu_item_image_limit' => '1',
|
||||
'menu_upload_max_mb' => '5',
|
||||
'avatar_upload_max_mb' => '3',
|
||||
'forum_enabled' => '1',
|
||||
'forum_upload_max_mb' => '5',
|
||||
'forum_allowed_filetypes' => 'jpg,jpeg,png,gif,webp,pdf,txt,doc,docx',
|
||||
'forum_image_resize_threshold_mb' => '1',
|
||||
'forum_image_max_width' => '1600',
|
||||
'forum_image_quality' => '82',
|
||||
];
|
||||
$stmt = $db->prepare("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)");
|
||||
foreach ($defaults as $k => $v) $stmt->execute([$k, $v]);
|
||||
|
||||
$catCount = (int)$db->query("SELECT COUNT(*) FROM forum_categories")->fetchColumn();
|
||||
if ($catCount === 0) {
|
||||
$stmt = $db->prepare("INSERT INTO forum_categories(name,slug,description,sort_order,is_active)VALUES(?,?,?,?,1)");
|
||||
foreach ([
|
||||
['General', 'general', 'Neighborhood conversation, questions, and introductions.', 1],
|
||||
['Local Questions', 'local-questions', 'Ask for Keyser recommendations, resources, and local tips.', 2],
|
||||
['Business Talk', 'business-talk', 'Business owner updates, ideas, and community feedback.', 3],
|
||||
['Events', 'events', 'Discuss upcoming events and things happening around town.', 4],
|
||||
] as $cat) $stmt->execute($cat);
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════
|
||||
@@ -119,6 +194,7 @@ function _schema(PDO $db): void {
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
email_verified_at TEXT,
|
||||
member_type TEXT NOT NULL DEFAULT 'member',
|
||||
avatar_path TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
last_login TEXT
|
||||
);
|
||||
@@ -206,6 +282,7 @@ function _schema(PDO $db): void {
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
price REAL NOT NULL DEFAULT 0,
|
||||
image_path TEXT NOT NULL DEFAULT '',
|
||||
is_available INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
@@ -246,6 +323,7 @@ function _schema(PDO $db): void {
|
||||
contact_name TEXT NOT NULL DEFAULT '',
|
||||
contact_email TEXT NOT NULL DEFAULT '',
|
||||
contact_phone TEXT NOT NULL DEFAULT '',
|
||||
image_path TEXT NOT NULL DEFAULT '',
|
||||
submitted_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
is_featured INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -261,6 +339,47 @@ function _schema(PDO $db): void {
|
||||
admin_notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_topics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL REFERENCES forum_categories(id) ON DELETE RESTRICT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_replies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
topic_id INTEGER NOT NULL REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS forum_attachments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
topic_id INTEGER REFERENCES forum_topics(id) ON DELETE CASCADE,
|
||||
reply_id INTEGER REFERENCES forum_replies(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
file_path TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL DEFAULT '',
|
||||
mime_type TEXT NOT NULL DEFAULT '',
|
||||
file_size INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
CHECK (topic_id IS NOT NULL OR reply_id IS NOT NULL)
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
@@ -281,7 +400,17 @@ function _seed(PDO $db): void {
|
||||
'mailgun_api_key'=>'',
|
||||
'mailgun_from_name'=>'Discover Keyser WV',
|
||||
'mailgun_from_email'=>'',
|
||||
'mailgun_send_mode'=>'html'] as $k=>$v)
|
||||
'mailgun_send_mode'=>'html',
|
||||
'event_upload_max_mb'=>'5',
|
||||
'menu_item_image_limit'=>'1',
|
||||
'menu_upload_max_mb'=>'5',
|
||||
'avatar_upload_max_mb'=>'3',
|
||||
'forum_enabled'=>'1',
|
||||
'forum_upload_max_mb'=>'5',
|
||||
'forum_allowed_filetypes'=>'jpg,jpeg,png,gif,webp,pdf,txt,doc,docx',
|
||||
'forum_image_resize_threshold_mb'=>'1',
|
||||
'forum_image_max_width'=>'1600',
|
||||
'forum_image_quality'=>'82'] as $k=>$v)
|
||||
qrun("INSERT OR IGNORE INTO settings(key,value)VALUES(?,?)",[$k,$v]);
|
||||
|
||||
/* Admin user — password "password123" */
|
||||
@@ -299,6 +428,13 @@ function _seed(PDO $db): void {
|
||||
['Legal Services','⚖️',12],['Churches & Faith','⛪',13],
|
||||
] as $c) qrun("INSERT OR IGNORE INTO categories(name,icon,sort_order)VALUES(?,?,?)",$c);
|
||||
|
||||
foreach ([
|
||||
['General', 'general', 'Neighborhood conversation, questions, and introductions.', 1],
|
||||
['Local Questions', 'local-questions', 'Ask for Keyser recommendations, resources, and local tips.', 2],
|
||||
['Business Talk', 'business-talk', 'Business owner updates, ideas, and community feedback.', 3],
|
||||
['Events', 'events', 'Discuss upcoming events and things happening around town.', 4],
|
||||
] as $fc) qrun("INSERT OR IGNORE INTO forum_categories(name,slug,description,sort_order,is_active)VALUES(?,?,?,?,1)", $fc);
|
||||
|
||||
$cid = fn(string $n): int => (int)qval("SELECT id FROM categories WHERE name=?",[$n]);
|
||||
$h = fn(array $d): string => json_encode($d);
|
||||
$std = $h(['Mon'=>'9am–5pm','Tue'=>'9am–5pm','Wed'=>'9am–5pm','Thu'=>'9am–5pm','Fri'=>'9am–5pm','Sat'=>'Closed','Sun'=>'Closed']);
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<?php if (setting('registration_enabled','1')==='1'): ?>
|
||||
<li><a href="/register.php">Create Account</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if ((authed() && setting('forum_enabled','1')==='1') || isAdmin()): ?>
|
||||
<li><a href="/forum">Community Forum</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="/events.php?submit=1">Submit an Event</a></li>
|
||||
<li><a href="https://govisitmineralwv.com" target="_blank" rel="noopener">Mineral County Tourism ↗</a></li>
|
||||
<li><a href="https://cityofkeyser.com" target="_blank" rel="noopener">City of Keyser ↗</a></li>
|
||||
|
||||
@@ -150,6 +150,233 @@ function validBusinessVideoUrl(string $url): bool {
|
||||
return in_array($scheme, ['http','https'], true) && businessVideoProvider($url) !== '';
|
||||
}
|
||||
|
||||
/* ── Uploads / Images ─────────────────────────────────── */
|
||||
function uploadRoot(): string { return dirname(__DIR__).'/uploads'; }
|
||||
function uploadPublicPath(string $subdir, string $filename): string {
|
||||
return '/uploads/'.trim($subdir, '/').'/'.$filename;
|
||||
}
|
||||
function cleanUploadSubdir(string $subdir): string {
|
||||
$subdir = trim(str_replace('\\', '/', $subdir), '/');
|
||||
return preg_replace('/[^a-zA-Z0-9_\/-]+/', '', $subdir) ?: 'misc';
|
||||
}
|
||||
function ensureUploadDir(string $subdir): string {
|
||||
$subdir = cleanUploadSubdir($subdir);
|
||||
$dir = uploadRoot().'/'.$subdir;
|
||||
if (!is_dir($dir)) mkdir($dir, 0775, true);
|
||||
return $dir;
|
||||
}
|
||||
function mbToBytes(mixed $mb, float $default = 5): int {
|
||||
$n = (float)$mb;
|
||||
if ($n <= 0) $n = $default;
|
||||
return max(1, (int)round($n * 1024 * 1024));
|
||||
}
|
||||
function uploadAllowedExts(string $csv, array $fallback): array {
|
||||
$raw = array_filter(array_map('trim', explode(',', strtolower($csv))));
|
||||
$exts = $raw ?: $fallback;
|
||||
return array_values(array_unique(array_map(fn($e) => ltrim($e, '.'), $exts)));
|
||||
}
|
||||
function imageExts(): array { return ['jpg','jpeg','png','gif','webp']; }
|
||||
function isImageExt(string $ext): bool { return in_array(strtolower($ext), imageExts(), true); }
|
||||
function isImagePath(string $path): bool {
|
||||
return isImageExt(strtolower(pathinfo(parse_url($path, PHP_URL_PATH) ?: $path, PATHINFO_EXTENSION)));
|
||||
}
|
||||
function uploadErrorText(int $code): string {
|
||||
return match($code) {
|
||||
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The uploaded file is too large.',
|
||||
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially received.',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'The server is missing a temporary upload directory.',
|
||||
UPLOAD_ERR_CANT_WRITE => 'The server could not write the uploaded file.',
|
||||
UPLOAD_ERR_EXTENSION => 'A server extension stopped the upload.',
|
||||
default => 'The file could not be uploaded.',
|
||||
};
|
||||
}
|
||||
function resizeImageFile(string $path, int $thresholdBytes, int $maxWidth = 1600, int $quality = 82): bool {
|
||||
if ($thresholdBytes <= 0 || !file_exists($path) || filesize($path) <= $thresholdBytes) return false;
|
||||
if (!function_exists('getimagesize') || !function_exists('imagecreatetruecolor')) return false;
|
||||
$info = @getimagesize($path);
|
||||
if (!$info || empty($info[0]) || empty($info[1]) || empty($info['mime'])) return false;
|
||||
|
||||
[$w, $h] = $info;
|
||||
$mime = $info['mime'];
|
||||
$loader = match($mime) {
|
||||
'image/jpeg' => 'imagecreatefromjpeg',
|
||||
'image/png' => 'imagecreatefrompng',
|
||||
'image/gif' => 'imagecreatefromgif',
|
||||
'image/webp' => 'imagecreatefromwebp',
|
||||
default => '',
|
||||
};
|
||||
if ($loader === '' || !function_exists($loader)) return false;
|
||||
$src = @$loader($path);
|
||||
if (!$src) return false;
|
||||
|
||||
$scale = min(1, $maxWidth / max($w, $h));
|
||||
$nw = max(1, (int)round($w * $scale));
|
||||
$nh = max(1, (int)round($h * $scale));
|
||||
$dst = imagecreatetruecolor($nw, $nh);
|
||||
if ($mime === 'image/png' || $mime === 'image/webp') {
|
||||
imagealphablending($dst, false);
|
||||
imagesavealpha($dst, true);
|
||||
$transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127);
|
||||
imagefilledrectangle($dst, 0, 0, $nw, $nh, $transparent);
|
||||
}
|
||||
imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
|
||||
$quality = max(40, min(95, $quality));
|
||||
$ok = match($mime) {
|
||||
'image/jpeg' => imagejpeg($dst, $path, $quality),
|
||||
'image/png' => imagepng($dst, $path, 6),
|
||||
'image/gif' => imagegif($dst, $path),
|
||||
'image/webp' => function_exists('imagewebp') ? imagewebp($dst, $path, $quality) : false,
|
||||
default => false,
|
||||
};
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
if ($ok) @chmod($path, 0664);
|
||||
return (bool)$ok;
|
||||
}
|
||||
function normalizeUploadFiles(string $field): array {
|
||||
if (empty($_FILES[$field])) return [];
|
||||
$file = $_FILES[$field];
|
||||
if (!is_array($file['name'])) return [$file];
|
||||
$out = [];
|
||||
foreach ($file['name'] as $i => $name) {
|
||||
$out[] = [
|
||||
'name' => $name,
|
||||
'type' => $file['type'][$i] ?? '',
|
||||
'tmp_name' => $file['tmp_name'][$i] ?? '',
|
||||
'error' => $file['error'][$i] ?? UPLOAD_ERR_NO_FILE,
|
||||
'size' => $file['size'][$i] ?? 0,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
function storeUploadedFile(array $file, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return ['ok'=>true, 'path'=>''];
|
||||
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) return ['ok'=>false, 'error'=>uploadErrorText((int)$file['error'])];
|
||||
if ((int)($file['size'] ?? 0) > $maxBytes) return ['ok'=>false, 'error'=>'The uploaded file exceeds the configured size limit.'];
|
||||
|
||||
$original = (string)($file['name'] ?? 'upload');
|
||||
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
|
||||
$allowedExts = array_map('strtolower', $allowedExts);
|
||||
if ($ext === '' || !in_array($ext, $allowedExts, true)) {
|
||||
return ['ok'=>false, 'error'=>'This file type is not allowed.'];
|
||||
}
|
||||
if (!empty($opts['image_only']) && !isImageExt($ext)) return ['ok'=>false, 'error'=>'Please upload an image file.'];
|
||||
if (!empty($opts['image_only']) && function_exists('getimagesize') && !@getimagesize((string)$file['tmp_name'])) {
|
||||
return ['ok'=>false, 'error'=>'The selected file is not a valid image.'];
|
||||
}
|
||||
|
||||
$dir = ensureUploadDir($subdir);
|
||||
$filename = date('YmdHis').'-'.bin2hex(random_bytes(5)).'.'.$ext;
|
||||
$dest = $dir.'/'.$filename;
|
||||
$tmp = (string)$file['tmp_name'];
|
||||
$moved = is_uploaded_file($tmp) ? move_uploaded_file($tmp, $dest) : @rename($tmp, $dest);
|
||||
if (!$moved) return ['ok'=>false, 'error'=>'The uploaded file could not be saved.'];
|
||||
@chmod($dest, 0664);
|
||||
|
||||
if (isImageExt($ext)) {
|
||||
resizeImageFile(
|
||||
$dest,
|
||||
(int)($opts['resize_over_bytes'] ?? 0),
|
||||
(int)($opts['image_max_width'] ?? 1600),
|
||||
(int)($opts['image_quality'] ?? 82)
|
||||
);
|
||||
}
|
||||
|
||||
$mime = '';
|
||||
if (function_exists('finfo_open')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
if ($finfo) {
|
||||
$mime = (string)finfo_file($finfo, $dest);
|
||||
finfo_close($finfo);
|
||||
}
|
||||
}
|
||||
return [
|
||||
'ok' => true,
|
||||
'path' => uploadPublicPath($subdir, $filename),
|
||||
'name' => $original,
|
||||
'mime' => $mime,
|
||||
'size' => file_exists($dest) ? filesize($dest) : (int)($file['size'] ?? 0),
|
||||
];
|
||||
}
|
||||
function storeSingleUpload(string $field, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
$files = normalizeUploadFiles($field);
|
||||
return $files ? storeUploadedFile($files[0], $subdir, $allowedExts, $maxBytes, $opts) : ['ok'=>true, 'path'=>''];
|
||||
}
|
||||
function storeMultipleUploads(string $field, string $subdir, array $allowedExts, int $maxBytes, array $opts = []): array {
|
||||
$saved = [];
|
||||
foreach (normalizeUploadFiles($field) as $file) {
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) continue;
|
||||
$res = storeUploadedFile($file, $subdir, $allowedExts, $maxBytes, $opts);
|
||||
if (!$res['ok']) return ['ok'=>false, 'error'=>$res['error'] ?? 'Upload failed.', 'files'=>$saved];
|
||||
if (!empty($res['path'])) $saved[] = $res;
|
||||
}
|
||||
return ['ok'=>true, 'files'=>$saved];
|
||||
}
|
||||
function storeAvatarUpload(string $field, int $maxBytes): array {
|
||||
$files = normalizeUploadFiles($field);
|
||||
if (!$files || ($files[0]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return ['ok'=>true, 'path'=>''];
|
||||
$file = $files[0];
|
||||
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) return ['ok'=>false, 'error'=>uploadErrorText((int)$file['error'])];
|
||||
if ((int)($file['size'] ?? 0) > $maxBytes) return ['ok'=>false, 'error'=>'The avatar exceeds the configured size limit.'];
|
||||
$ext = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
|
||||
if (!isImageExt($ext)) return ['ok'=>false, 'error'=>'Please choose a JPG, PNG, GIF, or WebP avatar.'];
|
||||
|
||||
if (!function_exists('getimagesize') || !function_exists('imagecreatetruecolor')) {
|
||||
return storeUploadedFile($file, 'avatars', imageExts(), $maxBytes, ['image_only'=>true, 'resize_over_bytes'=>mbToBytes(1), 'image_max_width'=>512, 'image_quality'=>86]);
|
||||
}
|
||||
$info = @getimagesize((string)$file['tmp_name']);
|
||||
if (!$info || empty($info[0]) || empty($info[1]) || empty($info['mime'])) return ['ok'=>false, 'error'=>'The selected avatar is not a valid image.'];
|
||||
$loader = match($info['mime']) {
|
||||
'image/jpeg' => 'imagecreatefromjpeg',
|
||||
'image/png' => 'imagecreatefrompng',
|
||||
'image/gif' => 'imagecreatefromgif',
|
||||
'image/webp' => 'imagecreatefromwebp',
|
||||
default => '',
|
||||
};
|
||||
if ($loader === '' || !function_exists($loader)) return ['ok'=>false, 'error'=>'That avatar image type is not supported by this server.'];
|
||||
$src = @$loader((string)$file['tmp_name']);
|
||||
if (!$src) return ['ok'=>false, 'error'=>'The avatar could not be opened.'];
|
||||
|
||||
$w = (int)$info[0]; $h = (int)$info[1];
|
||||
$size = (int)p('avatar_crop_size', min($w, $h));
|
||||
$x = (int)p('avatar_crop_x', max(0, (int)(($w - $size) / 2)));
|
||||
$y = (int)p('avatar_crop_y', max(0, (int)(($h - $size) / 2)));
|
||||
$size = max(1, min($size, $w, $h));
|
||||
$x = max(0, min($x, $w - $size));
|
||||
$y = max(0, min($y, $h - $size));
|
||||
|
||||
$dst = imagecreatetruecolor(512, 512);
|
||||
imagecopyresampled($dst, $src, 0, 0, $x, $y, 512, 512, $size, $size);
|
||||
$dir = ensureUploadDir('avatars');
|
||||
$filename = date('YmdHis').'-'.bin2hex(random_bytes(5)).'.jpg';
|
||||
$dest = $dir.'/'.$filename;
|
||||
$ok = imagejpeg($dst, $dest, 86);
|
||||
imagedestroy($src);
|
||||
imagedestroy($dst);
|
||||
if (!$ok) return ['ok'=>false, 'error'=>'The cropped avatar could not be saved.'];
|
||||
@chmod($dest, 0664);
|
||||
return ['ok'=>true, 'path'=>uploadPublicPath('avatars', $filename)];
|
||||
}
|
||||
function formatBytes(int $bytes): string {
|
||||
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1).' MB';
|
||||
if ($bytes >= 1024) return number_format($bytes / 1024, 1).' KB';
|
||||
return $bytes.' B';
|
||||
}
|
||||
function userAvatarHtml(?array $user, string $class = 'avatar-sm'): string {
|
||||
$name = (string)($user['username'] ?? 'User');
|
||||
$path = (string)($user['avatar_path'] ?? '');
|
||||
if ($path !== '') return '<img src="'.e($path).'" alt="'.e($name).'" class="avatar '.$class.'">';
|
||||
$initial = strtoupper(substr($name, 0, 1) ?: 'U');
|
||||
return '<span class="avatar avatar-initial '.$class.'" aria-hidden="true">'.e($initial).'</span>';
|
||||
}
|
||||
function uniqueForumSlug(string $title, int $id): string {
|
||||
$base = makeSlug($title) ?: 'topic';
|
||||
return $base.'-'.$id;
|
||||
}
|
||||
function forumTopicUrl(array $topic): string {
|
||||
return '/forum/'.rawurlencode((string)$topic['slug']).'/';
|
||||
}
|
||||
|
||||
/* ── Email verification / Mailgun ─────────────────────── */
|
||||
function requestBaseUrl(): string {
|
||||
$configured = trim(setting('site_base_url', ''));
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
<li><a href="/directory.php" class="nl<?=($activeNav??'')==='dir'?' active':''?>">Directory</a></li>
|
||||
<li><a href="/attractions.php" class="nl<?=($activeNav??'')==='attr'?' active':''?>">Attractions</a></li>
|
||||
<li><a href="/events.php" class="nl<?=($activeNav??'')==='events'?' active':''?>">Events</a></li>
|
||||
<?php if ((authed() && setting('forum_enabled','1')==='1') || isAdmin()): ?>
|
||||
<li><a href="/forum" class="nl<?=($activeNav??'')==='forum'?' active':''?>">Forum</a></li>
|
||||
<?php endif; ?>
|
||||
<li><a href="/about.php" class="nl<?=($activeNav??'')==='about'?' active':''?>">About</a></li>
|
||||
<?php if (authed()): ?>
|
||||
<?php if (isAdmin()): ?>
|
||||
|
||||
Reference in New Issue
Block a user