Загрузить файлы в «/»

This commit is contained in:
2026-04-30 08:09:46 +00:00
commit 08fe53fa5c
5 changed files with 1331 additions and 0 deletions
+311
View File
@@ -0,0 +1,311 @@
<?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');
}