'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); ?>

Short name used by cli/set_show.php. Lowercase letters, numbers and dashes.

Shown in the sidebar of the main page while this show is queued or live. Plain text; leave a blank line between paragraphs.

Current icon

JPEG, PNG or WebP, up to MB. The show logo shown next to the description on the main page.

Current background

JPEG, PNG or WebP, up to MB. Shown behind the player on the live page during this show.