'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; function slugify(string $name): string { $slug = strtolower(str_replace(["'", '’'], '', $name)); return trim(preg_replace('/[^a-z0-9]+/', '-', $slug), '-'); } function deleteUpload(?string $filename): void { if ($filename && is_file(UPLOAD_DIR . basename($filename))) { unlink(UPLOAD_DIR . basename($filename)); } } // Checks an optional image upload. Returns its file extension, or null if no file was sent or it was rejected // (rejections are added to $errors). function validateImageUpload(?array $upload, string $label, int $maxBytes, array &$errors): ?string { if (!$upload || $upload['error'] === UPLOAD_ERR_NO_FILE) return null; if (in_array($upload['error'], [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) || $upload['size'] > $maxBytes) { $errors[] = "{$label}s can be up to " . ($maxBytes / 1024 / 1024) . ' MB.'; return null; } if ($upload['error'] !== UPLOAD_ERR_OK) { $errors[] = "The {$label} failed to upload. Please try again."; return null; } $mime = (new finfo(FILEINFO_MIME_TYPE))->file($upload['tmp_name']); $ext = IMAGE_TYPES[$mime] ?? null; if ($ext === null || getimagesize($upload['tmp_name']) === false) { $errors[] = "{$label}s must be JPEG, PNG or WebP."; return null; } return $ext; } $id = (int)($_GET['id'] ?? 0); $show = ['id' => 0, 'name' => '', 'slug' => '', 'description' => '', 'icon_image' => null, 'background_image' => null]; if ($id) { $stmt = $db->prepare('SELECT id, name, slug, description, icon_image, background_image FROM shows WHERE id = ?'); $stmt->execute([$id]); $show = $stmt->fetch(); if (!$show) { http_response_code(404); exit('Show not found.'); } } $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // PHP discards the whole form when a request exceeds post_max_size, so catch that before the CSRF check if (empty($_POST) && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) { $errors[] = 'That upload is too large. Background images can be up to ' . (MAX_BACKGROUND_BYTES / 1024 / 1024) . ' MB and icons up to ' . (MAX_ICON_BYTES / 1024 / 1024) . ' MB.'; } else { verifyCsrf(); if (($_POST['action'] ?? '') === 'delete' && $id) { $db->prepare('DELETE FROM shows WHERE id = ?')->execute([$id]); deleteUpload($show['background_image']); deleteUpload($show['icon_image']); flash("Deleted {$show['name']}."); header('Location: shows.php'); exit; } $show['name'] = trim((string)($_POST['name'] ?? '')); $show['slug'] = trim((string)($_POST['slug'] ?? '')) ?: slugify($show['name']); // Normalise line endings so blank-line paragraph breaks are found on index.php $show['description'] = trim(str_replace("\r\n", "\n", (string)($_POST['description'] ?? ''))); if ($show['name'] === '' || mb_strlen($show['name']) > 255) { $errors[] = 'Name is required (up to 255 characters).'; } if (!preg_match('/^[a-z0-9]+(-[a-z0-9]+)*$/', $show['slug']) || strlen($show['slug']) > 64) { $errors[] = 'Slug may only contain lowercase letters, numbers and single dashes (up to 64 characters).'; } else { $stmt = $db->prepare('SELECT COUNT(*) FROM shows WHERE slug = ? AND id != ?'); $stmt->execute([$show['slug'], $id]); if ($stmt->fetchColumn() > 0) $errors[] = "Another show already uses the slug '{$show['slug']}'."; } if (mb_strlen($show['description']) > MAX_DESCRIPTION_CHARS) { $errors[] = 'The description can be up to ' . MAX_DESCRIPTION_CHARS . ' characters.'; } // column => [form field, label, size limit, filename suffix after the slug] $images = [ 'background_image' => ['background', 'background image', MAX_BACKGROUND_BYTES, ''], 'icon_image' => ['icon', 'icon', MAX_ICON_BYTES, '-icon'], ]; $uploadExts = []; foreach ($images as $column => [$field, $label, $maxBytes]) { $uploadExts[$column] = validateImageUpload($_FILES[$field] ?? null, ucfirst($label), $maxBytes, $errors); } $oldImages = ['background_image' => $show['background_image'], 'icon_image' => $show['icon_image']]; if (!$errors) { foreach ($images as $column => [$field, $label, , $suffix]) { if ($uploadExts[$column] !== null) { $filename = $show['slug'] . $suffix . '-' . bin2hex(random_bytes(4)) . '.' . $uploadExts[$column]; if (move_uploaded_file($_FILES[$field]['tmp_name'], UPLOAD_DIR . $filename)) { $show[$column] = $filename; } else { $errors[] = "The {$label} could not be saved. Check that the web server can write to images/backgrounds/."; } } elseif (!empty($_POST["remove_{$field}"])) { $show[$column] = null; } } // Don't leave a newly saved file behind if the other one failed if ($errors) { foreach ($oldImages as $column => $old) { if ($show[$column] !== $old) deleteUpload($show[$column]); $show[$column] = $old; } } } if (!$errors) { $description = $show['description'] === '' ? null : $show['description']; if ($id) { $db->prepare('UPDATE shows SET name = ?, slug = ?, description = ?, icon_image = ?, background_image = ? WHERE id = ?') ->execute([$show['name'], $show['slug'], $description, $show['icon_image'], $show['background_image'], $id]); } else { $db->prepare('INSERT INTO shows (name, slug, description, icon_image, background_image) VALUES (?, ?, ?, ?, ?)') ->execute([$show['name'], $show['slug'], $description, $show['icon_image'], $show['background_image']]); } foreach ($oldImages as $column => $old) { if ($old !== $show[$column]) deleteUpload($old); } flash("Saved {$show['name']}."); header('Location: shows.php'); exit; } } } adminHeader($id ? 'Edit Show' : 'Add a Show', $admin); ?>