| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/includes/polana_backup_v182.php';
require_admin_role();
header('Cache-Control: private, no-store, max-age=0');
header('X-Robots-Tag: noindex, nofollow');
header('X-Content-Type-Options: nosniff');
$notice = ''; $error = ''; $cfg = [];
try {
$cfg = pbk_cfg();
if (isset($_GET['code'], $_GET['state'])) {
start_session();
$expected = (string)($_SESSION['pbk_oauth_state'] ?? '');
$verifier = (string)($_SESSION['pbk_oauth_verifier'] ?? '');
unset($_SESSION['pbk_oauth_state'], $_SESSION['pbk_oauth_verifier']);
if ($expected === '' || !hash_equals($expected, (string)$_GET['state']) || strlen($verifier) < 43) throw new RuntimeException('Błędny stan sesji Google OAuth.');
$secret = pbk_secret_decrypt((string)($cfg['drive_client_secret'] ?? ''));
[$code, $body] = pbk_http('https://oauth2.googleapis.com/token', 'POST', ['Content-Type: application/x-www-form-urlencoded'], http_build_query([
'code' => (string)$_GET['code'], 'client_id' => $cfg['drive_client_id'], 'client_secret' => $secret,
'redirect_uri' => pbk_oauth_redirect_uri(), 'grant_type' => 'authorization_code', 'code_verifier' => $verifier
]));
$response = json_decode($body, true);
if ($code !== 200 || empty($response['refresh_token'])) throw new RuntimeException('Google nie zwrócił tokenu odświeżania. Sprawdź aplikację OAuth, tryb produkcyjny i zgodę dostępu. HTTP ' . $code);
$cfg['drive_refresh_token'] = pbk_secret_encrypt((string)$response['refresh_token']);
$cfg['drive_folder_id'] = '';
pbk_save_cfg($cfg);
header('Location: /admin/polana-backup-automat.php?connected=1', true, 303); exit;
}
if (isset($_GET['error'])) $error = 'Google odrzucił autoryzację: ' . htmlspecialchars(substr((string)$_GET['error'], 0, 100), ENT_QUOTES);
if (isset($_GET['connected'])) $notice = 'Konto Google Drive zostało połączone. Włącz przesyłanie na Dysk i zapisz ustawienia.';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
verify_csrf();
$action = (string)($_POST['action'] ?? '');
if ($action === 'save') {
$email = trim((string)($_POST['email'] ?? ''));
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) throw new RuntimeException('Niepoprawny adres e-mail.');
$mailEnabled = !empty($_POST['email_enabled']);
if ($mailEnabled && $email === '') throw new RuntimeException('Podaj adres e-mail do wysyłania kopii SQL.');
$clientId = trim((string)($_POST['drive_client_id'] ?? ''));
if ($clientId !== '' && (!str_ends_with($clientId, '.apps.googleusercontent.com') || strlen($clientId) > 250)) throw new RuntimeException('Niepoprawny Google OAuth Client ID.');
if ($clientId !== (string)($cfg['drive_client_id'] ?? '')) { $cfg['drive_refresh_token'] = ''; $cfg['drive_folder_id'] = ''; }
$secret = trim((string)($_POST['drive_client_secret'] ?? ''));
if ($secret !== '') $cfg['drive_client_secret'] = pbk_secret_encrypt($secret);
$cfg['drive_client_id'] = $clientId;
$cfg['email'] = $email;
$cfg['email_enabled'] = $mailEnabled;
$cfg['drive_enabled'] = !empty($_POST['drive_enabled']);
$cfg['enabled'] = !empty($_POST['enabled']);
if ($cfg['enabled'] && empty($cfg['key_exported'])) throw new RuntimeException('Najpierw pobierz klucz odzyskiwania i przechowaj go poza serwerem.');
if ($cfg['drive_enabled'] && empty($cfg['drive_refresh_token'])) throw new RuntimeException('Najpierw połącz konto Google Drive.');
pbk_save_cfg($cfg);
$notice = 'Zapisano ustawienia. Automatyczne wykonanie wymaga skonfigurowania zadania CRON na hostingu.';
} elseif ($action === 'download_key') {
$key = pbk_key();
$cfg['key_exported'] = true;
pbk_save_cfg($cfg);
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="POLANA-KLUCZ-ODZYSKIWANIA.key"');
header('Content-Length: 32');
echo $key; exit;
} elseif ($action === 'connect') {
$clientId = trim((string)($cfg['drive_client_id'] ?? ''));
if ($clientId === '' || empty($cfg['drive_client_secret'])) throw new RuntimeException('Zapisz najpierw Google Client ID i Client Secret.');
start_session();
$state = bin2hex(random_bytes(24));
$verifier = rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '=');
$_SESSION['pbk_oauth_state'] = $state;
$_SESSION['pbk_oauth_verifier'] = $verifier;
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$params = http_build_query([
'client_id' => $clientId, 'redirect_uri' => pbk_oauth_redirect_uri(), 'response_type' => 'code',
'scope' => 'https://www.googleapis.com/auth/drive.file', 'access_type' => 'offline', 'prompt' => 'consent',
'state' => $state, 'code_challenge' => $challenge, 'code_challenge_method' => 'S256'
]);
header('Location: https://accounts.google.com/o/oauth2/v2/auth?' . $params, true, 303); exit;
} elseif ($action === 'test_sql') {
$result = pbk_run('sql');
$notice = 'Wykonano próbę SQL: ' . implode('; ', $result) . '. Sprawdź historię oraz dostarczenie e-maila.';
} elseif ($action === 'test_drive') {
$token = pbk_drive_access_token($cfg);
pbk_drive_folder($cfg, $token);
$notice = 'Połączenie Google Drive działa. Folder kopii jest gotowy.';
} elseif ($action === 'download_archive') {
$type = (string)($_POST['type'] ?? '');
$name = basename((string)($_POST['filename'] ?? ''));
if (!in_array($type, ['daily', 'weekly'], true) || !preg_match('/^polana-(sql|strona)-\d{4}-\d{2}-\d{2}\.(sql\.gz|tar)\.pbackup$/', $name)) throw new RuntimeException('Nieprawidłowa nazwa archiwum.');
$path = pbk_private() . '/' . $type . '/' . $name;
if (!is_file($path)) throw new RuntimeException('Nie znaleziono archiwum.');
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . filesize($path));
readfile($path); exit;
} else throw new RuntimeException('Nieznana operacja.');
$cfg = pbk_cfg();
}
} catch (Throwable $e) { $error = $e->getMessage(); }
function pbk_e(mixed $v): string { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
$private = ''; $recent = []; $history = [];
try {
$private = pbk_private();
foreach (['daily', 'weekly'] as $kind) {
foreach (glob($private . '/' . $kind . '/*.pbackup') ?: [] as $file) {
$recent[] = ['file' => $file, 'kind' => $kind, 'size' => filesize($file), 'status' => pbk_status($file)];
}
}
usort($recent, static fn($a, $b) => strcmp($b['file'], $a['file']));
$lines = is_file($private . '/history.log') ? file($private . '/history.log', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : [];
foreach (array_reverse(array_slice($lines ?: [], -15)) as $line) { $decoded = json_decode($line, true); if (is_array($decoded)) $history[] = $decoded; }
} catch (Throwable $e) { $error = $error ?: $e->getMessage(); }
render_admin_header('Automatyczne kopie zapasowe — POLANA');
?>
<style>
.pbk{max-width:1150px;margin:18px auto 65px;padding:0 16px;color:#22352b}.pbk *{box-sizing:border-box}.pbk h1{font-size:clamp(23px,3vw,34px)}.pbk h2{font-size:20px}.pbk section{border:1px solid #d8e3d9;background:#fff;border-radius:16px;padding:22px;margin:18px 0}.pbk .cols{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.pbk label{display:block;margin:13px 0 5px;font-weight:700}.pbk input[type=email],.pbk input[type=text],.pbk input[type=password]{width:100%;border:1px solid #b8c9bd;border-radius:10px;padding:12px;font:inherit}.pbk button{background:#163c2b;color:#fff;border:0;border-radius:10px;padding:11px 16px;font:inherit;font-weight:700;cursor:pointer;margin:8px 5px 4px 0}.pbk .secondary{background:#e7efe7;color:#163c2b}.pbk .warn{background:#fff3dc;border:1px solid #e4b968;border-radius:10px;padding:13px}.pbk .good{background:#e9f7e8;border:1px solid #92c793;border-radius:10px;padding:13px}.pbk .err{background:#ffe9e7;border:1px solid #e6a49b;border-radius:10px;padding:13px}.pbk small{color:#586a60}.pbk code{overflow-wrap:anywhere}.pbk table{width:100%;border-collapse:collapse}.pbk th,.pbk td{text-align:left;padding:10px;border-bottom:1px solid #e4ece5;font-size:13px}.pbk .scroll{overflow-x:auto}.pbk .pill{padding:4px 9px;background:#eef4ee;border-radius:20px;display:inline-block}@media(max-width:720px){.pbk .cols{grid-template-columns:1fr}.pbk section{padding:15px}}
</style>
<div class="pbk">
<h1>Automatyczne kopie zapasowe</h1>
<p>SQL codziennie • pełne pliki strony co niedzielę • szyfrowanie AES-256-GCM • Google Drive i poczta. Wszystkie archiwa są szyfrowane przed wysyłką.</p>
<?php if ($notice): ?><div class="good"><?=pbk_e($notice)?></div><?php endif; ?>
<?php if ($error): ?><div class="err"><?=pbk_e($error)?></div><?php endif; ?>
<div class="cols">
<section><h2>1. Klucz odzyskiwania</h2><p><b><?=!empty($cfg['key_exported'])?'Klucz został już pobrany':'Pobierz klucz przed uruchomieniem automatu'?></b></p>
<p>Bez tego klucza nie odszyfrujesz kopii. Zapisz go w bezpiecznym miejscu <b>poza hostingiem i Dyskiem z kopiami</b>.</p>
<form method="post"><?=csrf_field()?><input type="hidden" name="action" value="download_key"><button type="submit">Pobierz klucz .key</button></form>
</section>
<section><h2>2. Harmonogram</h2><p><b>Codziennie:</b> pełny SQL, szyfrowany i wysyłany e-mailem. <b>Niedziela:</b> zaszyfrowane archiwum strony na Google Drive.</p>
<p>Przechowywanie lokalne: 14 ostatnich SQL i 4 ostatnie kopie strony. Kopie w Drive nie są automatycznie usuwane.</p>
<small>Automatyzacja wymaga zadania CRON w home.pl; samo włączenie przełącznika nie uruchamia harmonogramu.</small>
</section></div>
<section><h2>3. Ustawienia</h2>
<form method="post"><?=csrf_field()?><input type="hidden" name="action" value="save">
<div class="cols"><div>
<label><input type="checkbox" name="enabled" value="1" <?=!empty($cfg['enabled'])?'checked':''?>> Włącz automatyczne backupy</label>
<label><input type="checkbox" name="email_enabled" value="1" <?=!empty($cfg['email_enabled'])?'checked':''?>> Wysyłaj codzienny SQL na e-mail</label>
<label for="pbkemail">Adres odbiorcy (najlepiej inna skrzynka niż hosting strony)</label>
<input id="pbkemail" type="email" name="email" value="<?=pbk_e($cfg['email']??'')?>" placeholder="[email protected]">
</div><div>
<label><input type="checkbox" name="drive_enabled" value="1" <?=!empty($cfg['drive_enabled'])?'checked':''?>> Wysyłaj kopie na Google Drive</label>
<label for="clientid">Google OAuth Client ID (aplikacja Web)</label>
<input id="clientid" type="text" name="drive_client_id" value="<?=pbk_e($cfg['drive_client_id']??'')?>" placeholder="...apps.googleusercontent.com" autocomplete="off">
<label for="secret">Google OAuth Client Secret (puste = zachowaj)</label>
<input id="secret" type="password" name="drive_client_secret" placeholder="••••••••" autocomplete="new-password">
<small>OAuth callback: <code><?=pbk_e(pbk_oauth_redirect_uri())?></code></small>
</div></div>
<button type="submit">Zapisz ustawienia</button></form>
<div class="cols"><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="connect"><button class="secondary" type="submit">Połącz Google Drive (OAuth)</button></form>
<form method="post"><?=csrf_field()?><input type="hidden" name="action" value="test_drive"><button class="secondary" type="submit">Sprawdź połączenie Drive</button></form></div>
<p>Google Drive: <b><?=!empty($cfg['drive_refresh_token'])?'konto połączone':'konto niepołączone'?></b>. Do integracji wymagany jest osobny zakres uprawnień <code>drive.file</code>; logowanie Google ani sam Kalendarz nie dają automatycznie dostępu do Dysku.</p>
</section>
<section><h2>4. Uruchamianie i test</h2>
<div class="warn">CRON (codziennie około 03:15):<p><code>15 3 * * * /usr/bin/php <?=pbk_e(pbk_root())?>/cron/polana-backup-v182.php</code></p>Ścieżkę interpretera PHP i sposób dodania zadania potwierdź w panelu home.pl. Zadanie musi wykonywać PHP w trybie CLI.</div>
<form method="post" onsubmit="return confirm('Wykonać pełny SQL i wysłać go na e-mail, jeśli wysyłka jest włączona?')"><?=csrf_field()?><input type="hidden" name="action" value="test_sql"><button type="submit">Test: wykonaj SQL teraz</button></form>
<p><small>Dużą kopię całej witryny uruchamiamy przez CRON, żeby nie przekroczyć limitu czasu żądania HTTP.</small></p>
</section>
<section><h2>5. Ostatnie kopie</h2><div class="scroll"><table><thead><tr><th>Archiwum</th><th>Rozmiar</th><th>E-mail</th><th>Drive</th><th>Odzyskanie</th></tr></thead><tbody>
<?php foreach(array_slice($recent,0,30) as $row): ?><tr><td><?=pbk_e(basename($row['file']))?></td><td><?=pbk_e(round($row['size']/1048576,2))?> MB</td><td><?=!empty($row['status']['mail'])?'OK':'—'?></td><td><?=!empty($row['status']['drive'])?'OK':'—'?></td><td><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="download_archive"><input type="hidden" name="type" value="<?=pbk_e($row['kind'])?>"><input type="hidden" name="filename" value="<?=pbk_e(basename($row['file']))?>"><button class="secondary" type="submit">Pobierz</button></form></td></tr><?php endforeach; ?>
<?php if (!$recent): ?><tr><td colspan="5">Brak wykonanych kopii.</td></tr><?php endif; ?></tbody></table></div></section>
<section><h2>6. Historia operacji</h2><div class="scroll"><table><thead><tr><th>Data</th><th>Operacja</th><th>Wynik</th><th>Informacja</th></tr></thead><tbody>
<?php foreach($history as $row): ?><tr><td><?=pbk_e($row['time']??'')?></td><td><?=pbk_e($row['kind']??'')?></td><td><?=pbk_e($row['status']??'')?></td><td><?=pbk_e($row['message']??'')?></td></tr><?php endforeach; ?>
<?php if (!$history): ?><tr><td colspan="4">Brak historii.</td></tr><?php endif; ?></tbody></table></div></section>
</div>
<?php render_admin_footer(); ?>