'ae', 'ö' => 'oe', 'ü' => 'ue', 'Ä' => 'Ae', 'Ö' => 'Oe', 'Ü' => 'Ue', 'ß' => 'ss', ]; $s = strtr($s, $map); if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); if ($converted !== false) { $s = $converted; } } return $s; } /** Turn a title into a URL slug fragment ("Wedding Müller" → "wedding-mueller"). */ function slugify(string $title): string { $s = strtolower(ascii_transliterate($title)); $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? ''; $s = trim($s, '-'); return $s !== '' ? $s : 'gallery'; } /** * Sanitize an upload filename to a safe ASCII basename, keeping the extension. * German characters are transliterated rather than replaced by dashes, so * "Straße.jpg" becomes "Strasse.jpg" instead of "Stra-e.jpg". */ function safe_filename(string $name, string $fallback = 'file'): string { $name = basename($name); $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); $base = ascii_transliterate(pathinfo($name, PATHINFO_FILENAME)); $base = preg_replace('/[^A-Za-z0-9._-]+/', '-', $base) ?? ''; $base = trim($base, '-.'); if ($base === '') { $base = $fallback; } $ext = preg_replace('/[^A-Za-z0-9]+/', '', $ext) ?? ''; return $ext !== '' ? $base . '.' . $ext : $base; } // --------------------------------------------------------------------------- // 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 = ascii_transliterate(pathinfo($file['name'], PATHINFO_FILENAME)); $base = trim(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; }