commit 08fe53fa5c8971d45b84b814169054ad45867a46 Author: zuevav Date: Thu Apr 30 08:09:46 2026 +0000 Загрузить файлы в «/» diff --git a/HV.ico b/HV.ico new file mode 100644 index 0000000..ca2c956 Binary files /dev/null and b/HV.ico differ diff --git a/download.php b/download.php new file mode 100644 index 0000000..7d51541 --- /dev/null +++ b/download.php @@ -0,0 +1,311 @@ +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'); +} diff --git a/flickr_auth.php b/flickr_auth.php new file mode 100644 index 0000000..4924126 --- /dev/null +++ b/flickr_auth.php @@ -0,0 +1,183 @@ +handleCallback($_GET['oauth_token'], $_GET['oauth_verifier']); + + $message = "Авторизация успешна! Пользователь: " . htmlspecialchars($result['username'] ?? $result['user_nsid']); + $success = true; + } catch (Exception $e) { + $message = "Ошибка авторизации: " . htmlspecialchars($e->getMessage()); + $success = false; + } + + // Show result + ?> + + + + Flickr OAuth + + + +
+

+ +

Теперь приложение может загружать фотографии в оригинальном качестве.

+ + Вернуться в приложение +
+ + + clearTokens(); + header('Location: flickr_auth.php'); + exit; +} + +// Check current status or start auth +$isAuthorized = $oauth->isAuthorized(); + +if ($action === 'authorize' && !$isAuthorized) { + try { + $callbackUrl = $baseUrl . '/flickr_auth.php'; + $authUrl = $oauth->getAuthorizationUrl($callbackUrl); + header('Location: ' . $authUrl); + exit; + } catch (Exception $e) { + $error = $e->getMessage(); + } +} + +?> + + + + Flickr OAuth Authorization + + + +
+

🔐 Flickr OAuth Authorization

+ + +
Ошибка:
+ + + +
+ ✓ Авторизован — доступ к оригиналам фото включён +
+

Приложение авторизовано и может загружать фотографии в оригинальном качестве.

+

+ Открыть приложение + Выйти +

+ +
+ ⚠ Не авторизован — доступны только уменьшенные версии +
+ +

Для загрузки фотографий в оригинальном качестве необходимо авторизовать приложение:

+ + + +

+ Авторизовать через Flickr + Назад +

+ +
+ + diff --git a/index.php b/index.php new file mode 100644 index 0000000..d4ca10f --- /dev/null +++ b/index.php @@ -0,0 +1,735 @@ +hasUsers()) { + header('Location: setup.php'); + exit; +} + +// Check authentication +if (!$auth->isAuthenticated()) { + header('Location: login.php'); + exit; +} + +// Get current user +$currentUser = $auth->getCurrentUser(); + +// Handle logout +if (isset($_GET['logout'])) { + $auth->logout(); + header('Location: login.php'); + exit; +} + +?> + + + + + + VH Posting System + + + + + +
+ +
+

VH Posting System

+
+ + + Выход +
+
+ + + + + + + + +
+
+

Публикация в социальные сети

+

Выберите платформы и опубликуйте одним нажатием

+ + +
+ +
+ + + +
+
+

Нажмите кнопку выше чтобы добавить фото

+
+
+ + +
+ +
+
+ + + + + + + +
+ +
+
+ + +
+ +
+
+
+ +
+
+
+
+ Быстрые теги: +
+ + +
+
+ + +
+ +
+ +
+
+ +
+ Telegram + Не подключён +
+
+ +
+ + +
+
+ +
+ ВКонтакте + Не подключён +
+
+ +
+ + +
+
+ +
+ Instagram + Требует настройки +
+
+

Требуется Facebook Business API

+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + + + + +
+ +
+ +
+ + +
+
+

📅 Отложенные публикации

+ 0 +
+
+

Нет запланированных постов

+
+
+ + +
+
+

✓ Архив публикаций

+ +
+
+

Загрузка...

+
+
+
+
+ + +
+
+

Конвертер ссылок Flickr

+

Преобразование ссылок в различные форматы для форумов и соцсетей

+ +
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ +
+
+ + + + + +
+ +
+
+ +
+ +
+
+
+ +
+
+
+
+ Быстрые: +
+ + +
+
+
+
+ + +
+
+ + +
+ + + +
+
+
+
+
+ + +
+
+

Виджет для WordPress

+

Настройте мозаику фотографий для отображения на вашем сайте

+ + +
+

Статус виджета

+
+ +
+
+ + + +
+
+ + +
+

Выбор альбомов

+

Выберите альбомы для отображения в виджете. Если ничего не выбрано — показываются последние фото.

+
+ +
+
+

Нажмите «Загрузить альбомы» для выбора

+
+
+ + +
+

Параметры

+
+
+ + +
+
+ + + 3600 = 1 час +
+
+
+ + + + + +
+

Установка на WordPress

+
+

1. Скачайте плагин:

+ Скачать плагин +
+
+

2. Установите плагин в WordPress (Плагины → Добавить новый → Загрузить)

+
+
+

3. В настройках плагина укажите API URL:

+ +
+
+
+
+ + +
+
+

Настройки

+ + +
+

Flickr API

+
+ + + + + +
+
+ + +
+
+ + Проверка... + Авторизовать + Требуется для загрузки оригиналов +
+
+ + +
+

Telegram

+
+ + Проверка... +
+
+ + +
+
+ + +
+

ВКонтакте

+
+ + Проверка... +
+
+ + + + +
+
+ +
+
+
+ Как получить пользовательский токен (для загрузки фото)? +
    +
  1. Перейдите на vkhost.github.io
  2. +
  3. Нажмите "VK Admin"
  4. +
  5. Разрешите доступ приложению
  6. +
  7. Скопируйте access_token из адресной строки браузера
  8. +
  9. Вставьте токен в поле выше и нажмите "Сохранить"
  10. +
+

+ Важно: Пользовательский токен позволяет загружать фото напрямую в VK. + Community-токен (ключ сообщества) может только постить текст и ссылки. +

+
+
+
+ + +
+

Кросс-промо каналов

+

Ссылки на ваши каналы для автоматического добавления в посты

+
+ + + Будет добавлена в посты VK +
+
+ + + Будет добавлена в посты Telegram +
+
+ + +
+
+ + +
+ + +
+ + +
+

Оформление

+
+ + +
+
+ + +
+

Смена пароля

+
+ + +
+
+ + + Минимум 8 символов +
+
+ + +
+ +
+
+
+
+ + + + + + + diff --git a/login.php b/login.php new file mode 100644 index 0000000..c316fbb --- /dev/null +++ b/login.php @@ -0,0 +1,102 @@ +hasUsers()) { + header('Location: setup.php'); + exit; +} + +// If already logged in, redirect to main page +if ($auth->isAuthenticated()) { + header('Location: index.php'); + exit; +} + +$error = ''; +$message = ''; + +// Handle login form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $ip = Auth::getClientIP(); + + if (empty($username) || empty($password)) { + $error = 'Введите имя пользователя и пароль'; + } else { + $result = $auth->login($username, $password, $ip); + + if ($result['success']) { + $auth->startSession($result['username'], $result['token']); + header('Location: index.php'); + exit; + } else { + $error = $result['message']; + } + } +} + +// CSRF token +$csrfToken = bin2hex(random_bytes(32)); +$_SESSION['csrf_token'] = $csrfToken; +?> + + + + + + Вход - VH Posting System + + + + + +
+ +
+ +