isAuthenticated()) { http_response_code(401); die('Not authenticated'); } $action = $_GET['action'] ?? ''; /** * Fetch remote file content with proper error handling */ function fetchRemoteFile($url) { // Use cURL for more reliable fetching $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => 60, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_HTTPHEADER => [ 'Accept: image/*, */*', 'Referer: https://www.flickr.com/', ], ]); $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $error = curl_error($ch); curl_close($ch); // Check for errors if ($content === false || $httpCode !== 200) { error_log("Flickr fetch failed: HTTP $httpCode, Error: $error, URL: $url"); return null; } // Validate it's actually an image if (strlen($content) < 1000) { error_log("Flickr fetch: Content too small (" . strlen($content) . " bytes), likely error page"); return null; } // Check magic bytes for image formats $magicBytes = substr($content, 0, 8); $isJpeg = (substr($magicBytes, 0, 2) === "\xFF\xD8"); $isPng = (substr($magicBytes, 0, 4) === "\x89PNG"); $isGif = (substr($magicBytes, 0, 3) === "GIF"); $isWebp = (substr($magicBytes, 0, 4) === "RIFF" && substr($content, 8, 4) === "WEBP"); if (!$isJpeg && !$isPng && !$isGif && !$isWebp) { error_log("Flickr fetch: Not a valid image format. First bytes: " . bin2hex(substr($content, 0, 16))); return null; } // Determine content type from magic bytes if needed if (empty($contentType) || strpos($contentType, 'image/') !== 0) { if ($isJpeg) $contentType = 'image/jpeg'; elseif ($isPng) $contentType = 'image/png'; elseif ($isGif) $contentType = 'image/gif'; elseif ($isWebp) $contentType = 'image/webp'; } return [ 'content' => $content, 'content_type' => $contentType, 'size' => strlen($content), ]; } /** * Clean filename for download */ function cleanFilename($name, $ext = 'jpg') { $name = preg_replace('/[<>:"\/\\\\|?*]/', '_', $name); $name = substr($name, 0, 100); return $name . '.' . $ext; } /** * Get file extension from URL or format */ function getExtension($url, $format = 'jpg') { // Check URL extension first $path = parse_url($url, PHP_URL_PATH); $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) { return $ext === 'jpeg' ? 'jpg' : $ext; } return $format ?: 'jpg'; } /** * Check if URL is a valid Flickr image URL */ function isFlickrUrl($url) { // Flickr uses several domain/URL formats: // - farm{N}.staticflickr.com (static images) // - live.staticflickr.com (static images, modern) // - www.flickr.com/photo_download.gne (download endpoint for originals) $patterns = [ '/^https?:\/\/farm\d+\.staticflickr\.com\//', '/^https?:\/\/live\.staticflickr\.com\//', '/^https?:\/\/farm\d+\.static\.flickr\.com\//', '/^https?:\/\/staticflickr\.com\//', '/^https?:\/\/c\d+\.staticflickr\.com\//', '/^https?:\/\/(www\.)?flickr\.com\/photo_download\.gne\?/', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $url)) { return true; } } return false; } switch ($action) { // Download single photo case 'photo': $url = $_GET['url'] ?? ''; $filename = $_GET['filename'] ?? 'photo'; $format = $_GET['format'] ?? 'jpg'; if (!$url) { http_response_code(400); die('URL required'); } // Validate URL is from Flickr (multiple domain formats) if (!isFlickrUrl($url)) { http_response_code(400); die('Invalid URL - only Flickr URLs allowed: ' . htmlspecialchars($url)); } $file = fetchRemoteFile($url); if (!$file) { http_response_code(502); die('Failed to fetch image from Flickr'); } $ext = getExtension($url, $format); $safeFilename = cleanFilename($filename, $ext); // Clear ALL output buffers (there might be multiple levels) while (ob_get_level()) { ob_end_clean(); } header('Content-Type: ' . $file['content_type']); header('Content-Length: ' . $file['size']); header('Content-Disposition: attachment; filename="' . $safeFilename . '"'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: public'); echo $file['content']; exit; // Download multiple photos as ZIP case 'zip': // Get photo data from POST $photosJson = $_POST['photos'] ?? ''; $albumName = $_POST['album_name'] ?? ''; if (!$photosJson) { http_response_code(400); die('Photos data required'); } $photos = json_decode($photosJson, true); if (!$photos || !is_array($photos) || count($photos) === 0) { http_response_code(400); die('Invalid photos data'); } // Limit to prevent server overload if (count($photos) > 500) { http_response_code(400); die('Maximum 500 photos per archive'); } // Create temp file for ZIP $tempFile = tempnam(sys_get_temp_dir(), 'vh_photos_'); $zip = new ZipArchive(); if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { http_response_code(500); die('Failed to create ZIP archive'); } // Create folder name $folderName = $albumName ? preg_replace('/[<>:"\/\\\\|?*]/', '_', $albumName) : 'flickr_photos_' . date('Y-m-d'); $folderName = substr($folderName, 0, 100); $usedNames = []; $successCount = 0; $failCount = 0; foreach ($photos as $photo) { $url = $photo['url'] ?? ''; $title = $photo['title'] ?? $photo['id'] ?? 'photo'; $format = $photo['format'] ?? 'jpg'; $photoId = $photo['id'] ?? uniqid(); if (!$url) { $failCount++; continue; } // Validate URL - use same function as single photo if (!isFlickrUrl($url)) { $failCount++; continue; } $file = fetchRemoteFile($url); if (!$file) { $failCount++; continue; } $ext = getExtension($url, $format); $filename = cleanFilename($title, $ext); // Ensure unique filename if (in_array($filename, $usedNames)) { $baseName = pathinfo($filename, PATHINFO_FILENAME); $filename = $baseName . '_' . $photoId . '.' . $ext; } $usedNames[] = $filename; $zip->addFromString($folderName . '/' . $filename, $file['content']); $successCount++; } $zip->close(); if ($successCount === 0) { // Clean up temp file if it exists if (file_exists($tempFile)) { @unlink($tempFile); } http_response_code(502); die('Failed to download any photos. Check that photo URLs are valid.'); } // Send ZIP file $zipFilename = $folderName . '.zip'; $zipSize = filesize($tempFile); // Clear ALL output buffers while (ob_get_level()) { ob_end_clean(); } header('Content-Type: application/zip'); header('Content-Length: ' . $zipSize); header('Content-Disposition: attachment; filename="' . $zipFilename . '"'); header('Cache-Control: no-cache, must-revalidate'); header('Pragma: public'); header('X-Photos-Downloaded: ' . $successCount); header('X-Photos-Failed: ' . $failCount); readfile($tempFile); unlink($tempFile); exit; default: ob_end_clean(); http_response_code(400); die('Unknown action'); }