'ae', 'ö' => 'oe', 'ü' => 'ue', 'Ä' => 'ae', 'Ö' => 'oe', 'Ü' => 'ue', 'ß' => 'ss']; $s = strtr($title, $map); if (function_exists('iconv')) { $s = (string)@iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); } $s = strtolower($s); $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? ''; $s = trim($s, '-'); return $s !== '' ? $s : 'gallery'; } // --------------------------------------------------------------------------- // Local media (hero + showreel images in media/) // --------------------------------------------------------------------------- const MEDIA_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']; /** * Store one uploaded image in media/, full resolution, unmodified. * Returns the stored filename, or null if the upload is invalid. */ function media_store_upload(array $file): ?string { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { return null; } $ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION)); if (!in_array($ext, MEDIA_EXTENSIONS, true)) { return null; } // Cheap content sanity check without touching the image data. if (function_exists('getimagesize') && @getimagesize($file['tmp_name']) === false) { return null; } $base = pathinfo($file['name'], PATHINFO_FILENAME); $base = preg_replace('/[^A-Za-z0-9._-]+/', '-', $base) ?: 'image'; $name = substr($base, 0, 60) . '-' . random_token(6) . '.' . $ext; if (!move_uploaded_file($file['tmp_name'], MEDIA_DIR . '/' . $name)) { return null; } return $name; } /** Delete a local media file (filename only, no paths). */ function media_delete(string $name): void { if ($name !== '' && basename($name) === $name) { @unlink(MEDIA_DIR . '/' . $name); } } // --------------------------------------------------------------------------- // Site content (landing page + showreel) // --------------------------------------------------------------------------- function site_get(): array { return json_read(DATA_DIR . '/site.json', [ 'intro_title' => 'Jane Doe', 'intro_text' => "Photographer based in Berlin.\nAvailable for portraits, weddings and events.", 'hero_image' => null, 'showreel' => [], ]); } function site_save(array $site): void { json_write(DATA_DIR . '/site.json', $site); } // --------------------------------------------------------------------------- // Galleries — one JSON file per gallery in data/galleries/ // --------------------------------------------------------------------------- function gallery_file(string $slug): string { // Slugs are generated by us, but never trust a request parameter in a path. if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/', $slug) || strlen($slug) > 120) { throw new InvalidArgumentException('Invalid gallery slug'); } return DATA_DIR . '/galleries/' . $slug . '.json'; } function gallery_load(string $slug): ?array { try { $file = gallery_file($slug); } catch (InvalidArgumentException) { return null; } $g = json_read($file); return $g === [] ? null : $g; } function gallery_save(array $gallery): void { json_write(gallery_file($gallery['slug']), $gallery); } function gallery_delete(string $slug): void { $file = gallery_file($slug); if (is_file($file)) { unlink($file); } } /** All galleries, newest first. */ function galleries_all(): array { $out = []; foreach (glob(DATA_DIR . '/galleries/*.json') ?: [] as $file) { $g = json_read($file); if ($g !== []) { $out[] = $g; } } usort($out, fn($a, $b) => strcmp($b['created_at'] ?? '', $a['created_at'] ?? '')); return $out; } /** A gallery past its expiry date is treated as nonexistent for visitors. */ function gallery_is_expired(array $gallery): bool { $expires = $gallery['expires_at'] ?? null; if ($expires === null || $expires === '') { return false; } // The gallery stays visible through the whole expiry day. return date('Y-m-d') > $expires; }