312 lines
9.0 KiB
PHP
312 lines
9.0 KiB
PHP
<?php
|
|
/**
|
|
* Server-side photo download handler
|
|
* Bypasses CORS restrictions by proxying through the server
|
|
*/
|
|
|
|
// Prevent any output before headers
|
|
ob_start();
|
|
|
|
session_start();
|
|
|
|
// Load configuration
|
|
$configFile = __DIR__ . '/config.php';
|
|
if (!file_exists($configFile)) {
|
|
http_response_code(500);
|
|
die('Configuration not found');
|
|
}
|
|
$config = require $configFile;
|
|
|
|
// Autoload classes
|
|
spl_autoload_register(function ($class) {
|
|
$file = __DIR__ . '/classes/' . $class . '.php';
|
|
if (file_exists($file)) {
|
|
require_once $file;
|
|
}
|
|
});
|
|
|
|
// Check authentication
|
|
$auth = new Auth();
|
|
if (!$auth->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');
|
|
}
|