PHP_VERSION_ID >= 80000,
'curl' => extension_loaded('curl'),
'allow_url_fopen' => ini_get('allow_url_fopen'),
'writable' => is_writable('.'),
'empty_dir' => is_dir_empty(),
];
$req['can_download'] = $req['curl'] || $req['allow_url_fopen'];
return $req;
}
/**
* Sprawdza czy katalog jest pusty (lub zawiera tylko ten plik)
*/
function is_dir_empty(): bool {
$files = array_diff(scandir('.'), ['.', '..']);
// Jeśli tylko ten plik (install.php) — traktuj jako pusty
foreach ($files as $file) {
if ($file !== basename(__FILE__)) {
return false;
}
}
return true;
}
/**
* Pobiera dane JSON z URL
*/
function fetch_json(string $url): ?array {
$response = http_get($url);
if ($response === false) return null;
$data = json_decode($response, true);
return is_array($data) ? $data : null;
}
/**
* Wykonuje HTTP GET przez cURL lub file_get_contents
*/
function http_get(string $url): string|false {
if (extension_loaded('curl')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'FotoCMS-Installer/' . INSTALLER_VERSION,
]);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($result !== false && $code === 200) {
return $result;
}
}
if (ini_get('allow_url_fopen')) {
$ctx = stream_context_create([
'http' => [
'timeout' => 30,
'user_agent' => 'FotoCMS-Installer/' . INSTALLER_VERSION,
],
'ssl' => [
'verify_peer' => true,
],
]);
$result = @file_get_contents($url, false, $ctx);
if ($result !== false) {
return $result;
}
}
return false;
}
/**
* Pobiera plik binarny i zapisuje do dysku
* Zwraca ['success' => bool, 'http_code' => int|null]
*/
function download_file(string $url, string $dest): array {
$dir = dirname($dest);
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
if (extension_loaded('curl')) {
$fp = fopen($dest, 'wb');
if (!$fp) return ['success' => false, 'http_code' => null];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'FotoCMS-Installer/' . INSTALLER_VERSION,
]);
curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_errno($ch);
curl_close($ch);
fclose($fp);
if ($code === 200 && $err === 0) {
return ['success' => true, 'http_code' => $code];
}
@unlink($dest);
return ['success' => false, 'http_code' => $code];
}
if (ini_get('allow_url_fopen')) {
$ctx = stream_context_create([
'http' => [
'timeout' => 60,
'user_agent' => 'FotoCMS-Installer/' . INSTALLER_VERSION,
],
'ssl' => [
'verify_peer' => true,
],
]);
$data = @file_get_contents($url, false, $ctx);
if ($data !== false) {
$ok = file_put_contents($dest, $data, LOCK_EX) !== false;
return ['success' => $ok, 'http_code' => $ok ? 200 : null];
}
}
return ['success' => false, 'http_code' => null];
}
/**
* Weryfikacja SHA-256 pliku
*/
function verify_hash(string $filePath, string $expectedHash): bool {
if (!file_exists($filePath)) return false;
$actualHash = hash_file('sha256', $filePath);
return hash_equals($expectedHash, $actualHash);
}
/**
* Zapisuje manifest do pliku poza webroot (fix8: nie publikuj klucza licencji)
*/
function manifest_path(): string {
// Preferuj katalog tymczasowy systemu — jest poza webroot
$tmp = sys_get_temp_dir() . '/fotocms_install_' . substr(md5(__FILE__), 0, 12) . '.json';
return $tmp;
}
/**
* Zapisuje manifest do pliku tymczasowego
*/
function save_manifest(array $manifest): bool {
return file_put_contents(manifest_path(), json_encode($manifest, JSON_PRETTY_PRINT), LOCK_EX) !== false;
}
/**
* Odczytuje zapisany manifest
*/
function load_manifest(): ?array {
$path = manifest_path();
if (!file_exists($path)) return null;
$data = file_get_contents($path);
$manifest = json_decode($data, true);
return is_array($manifest) ? $manifest : null;
}
/**
* Czyści pliki tymczasowe
*/
function cleanup(): void {
@unlink(manifest_path());
@unlink(__FILE__);
}
// --- OBSŁUGA KROKÓW ----------------------------------------------------------
// KROK 0: Sprawdzenie wymagań
if ($step === 'check') {
$req = check_requirements();
if (!$req['php']) {
$error = 'Wymagany jest PHP 8.0 lub nowszy. Zainstalowana wersja: ' . PHP_VERSION;
} elseif (!$req['can_download']) {
$error = 'Serwer nie obsługuje pobierania plików przez HTTP. Wymagane jest rozszerzenie cURL lub allow_url_fopen.';
} elseif (!$req['writable']) {
$error = 'Brak uprawnień do zapisu w bieźącym katalogu. Sprawdż chmod/chown.';
} elseif (!$req['empty_dir']) {
$warning = 'Katalog nie jest pusty! Instalacja moźe nadpisać istniejące pliki.';
}
// Sprawdż czy API_BASE działa (ping) — fix6: nie uźywamy license-status?key=test
// bo zwraca 403 (nieprawidłowy klucz), co http_get interpretowało jako błąd połączenia.
// Uźywamy prostego GET na root API; spodziewamy się dowolnej odpowiedzi HTTP (nawet 4xx).
if (empty($error)) {
if (extension_loaded('curl')) {
$ch = curl_init(API_BASE . '/api/v1');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_NOBODY => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'FotoCMS-Installer/' . INSTALLER_VERSION,
]);
curl_exec($ch);
$apiWorking = curl_errno($ch) === 0;
curl_close($ch);
} else {
// Bez cURL — zakładamy źe działa; weryfikacja nastąpi przy manifest
$apiWorking = true;
}
if (!$apiWorking) {
$apiWarning = true;
$warning = 'Nie moźna połączyć się z serwerem API (' . API_BASE . '). Moźesz spróbować wpisać inny adres ręcznie.';
}
}
}
// KROK 1: Pobranie manifestu
if ($step === 'manifest' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$licenseKey = trim($_POST['license_key'] ?? '');
$apiBase = trim($_POST['api_base'] ?? API_BASE);
// Walidacja api_base — dopuszczamy tylko https:// (fallback: stała API_BASE)
if (!preg_match('#^https?://#i', $apiBase)) {
$apiBase = API_BASE;
}
if (empty($licenseKey)) {
$error = 'Wpisz klucz licencji.';
$step = 'check';
} else {
$tokenParam = defined('INSTALLER_DOWNLOAD_TOKEN') ? '&download_token=' . urlencode(INSTALLER_DOWNLOAD_TOKEN) : '';
$manifestUrl = $apiBase . '/api/v1/install-manifest?key=' . urlencode($licenseKey) . $tokenParam;
$manifest = fetch_json($manifestUrl);
if (!$manifest) {
$error = 'Nie moźna pobrać manifestu. Sprawdż klucz licencji i połączenie z internetem.';
$step = 'check';
} elseif (!empty($manifest['error'])) {
$error = 'Błąd API: ' . $manifest['error'];
$step = 'check';
} elseif (empty($manifest['ok']) || empty($manifest['files'])) {
$error = 'Nieprawidłowa odpowiedż serwera. Spróbuj ponownie.';
$step = 'check';
} else {
// Zapisz manifest
$manifest['_license_key'] = $licenseKey;
$manifest['_api_base'] = $apiBase;
save_manifest($manifest);
// Przekieruj do pobierania
header('Location: ?step=download');
exit;
}
}
}
// KROK 2: Pobieranie plików
if ($step === 'download') {
$manifest = load_manifest();
if (!$manifest) {
$error = 'Nie znaleziono manifestu. Rozpocznij od początku.';
$step = 'check';
} else {
$licenseKey = $manifest['_license_key'] ?? '';
$apiBase = $manifest['_api_base'] ?? API_BASE;
$files = $manifest['files'] ?? [];
// Idempotentne pobieranie — sprawdź co już istnieje (po samym pliku, bez hashy)
$toDownload = [];
$completed = 0;
foreach ($files as $file) {
$path = $file['path'] ?? '';
if (empty($path)) continue;
if (file_exists($path)) {
$completed++;
continue;
}
$toDownload[] = $file;
}
// Jeśli wszystko pobrane — przejdż do finalizacji
if (empty($toDownload)) {
header('Location: ?step=finalize');
exit;
}
// Pobierz brakujące pliki (AJAX/Stream lub jeden na raz)
if ($_SERVER['REQUEST_METHOD'] === 'POST' || isset($_GET['ajax'])) {
// AJAX mode — pobierz jeden plik po globalnym indeksie z pełnej listy
header('Content-Type: application/json; charset=utf-8');
$fileIndex = (int)($_GET['file'] ?? 0);
if ($fileIndex < count($files)) {
$file = $files[$fileIndex];
$path = $file['path'];
$hash = $file['hash'] ?? '';
$token = $file['download_token'] ?? '';
// Idempotencja — plik już istnieje, pomiń
if (file_exists($path)) {
echo json_encode(['ok' => true, 'path' => $path, 'skipped' => true, 'next' => $fileIndex + 1]);
exit;
}
$dlTokenParam = defined('INSTALLER_DOWNLOAD_TOKEN') ? '&download_token=' . urlencode(INSTALLER_DOWNLOAD_TOKEN) : '';
$downloadUrl = $apiBase . '/api/v1/install-download'
. '?key=' . urlencode($licenseKey)
. '&file=' . urlencode($path)
. '&token=' . urlencode($token)
. $dlTokenParam;
$result = download_file($downloadUrl, $path);
$httpCode = $result['http_code'];
$success = $result['success'];
if ($success) {
echo json_encode(['ok' => true, 'path' => $path, 'next' => $fileIndex + 1]);
} else {
@unlink($path);
if ($httpCode === 429) {
echo json_encode(['ok' => false, 'path' => $path, 'error' => 'rate_limited', 'retryAfter' => 900]);
} else {
echo json_encode(['ok' => false, 'path' => $path, 'error' => 'download_failed', 'http_code' => $httpCode]);
}
}
exit;
} else {
echo json_encode(['ok' => true, 'done' => true]);
exit;
}
}
// Normal mode — wyświetl progress i pobieraj przez JS
$totalFiles = count($files);
$remainingFiles = count($toDownload);
$info = "Pobieranie {$remainingFiles} plików (z {$totalFiles} całkowicie)...";
}
}
// KROK 3: Tworzenie katalogów i .htaccess
if ($step === 'finalize') {
$manifest = load_manifest();
if (!$manifest) {
$error = 'Nie znaleziono manifestu. Rozpocznij od początku.';
$step = 'check';
} else {
// Tworzenie pustych katalogów
$dirs = ['tmp', 'uploads', 'config', 'config/sessions'];
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
}
// Generowanie .htaccess dla uploads/
$htaccessUploads = "Options -Indexes\n"
. "
install/.
install/, aby skonfigurować bazę danych i konto administratora.