api/update.php

<?php
declare(strict_types=1);

require_once __DIR__ . '/../includes/core.php';

$versionFile = __DIR__ . '/../includes/version.php';
$moduleUpdateFile = __DIR__ . '/../includes/modules/update.php';

if (is_file($versionFile)) {
    require_once $versionFile;
}

require_once $moduleUpdateFile;

header('Content-Type: application/json; charset=utf-8');

brivaciaLog('update/ping.log', 'api/update.php reached: ' . ($_SERVER['REQUEST_METHOD'] ?? ''));

function update_blocked_dirs(): array {
    return ['archives', 'backup', 'corrupt', 'data', 'logs', 'static', 'update'];
}

function update_blocked_files(): array {
    return ['.htaccess', '.htpasswd'];
}

function update_progress(string $step): void {
    brivaciaLog('update/info.log', 'Step: ' . $step);

    file_put_contents(
        updateDir() . '/progress.json',
        json_encode([
            'step' => $step,
            'time' => time(),
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        LOCK_EX
    );
}

function update_cleanup(string $dir, string $zipFile, string $extractDir): void {
    brivaciaLog('update/info.log', 'Cleanup update files');

    @unlink($dir . '/progress.json');
    update_rrmdir($extractDir);
    @unlink($zipFile);
}

function update_json(array $data, int $code = 200): never {
    if ($code >= 400) {
        brivaciaLog('update/failed.log', ($data['error'] ?? 'Unknown update error') . ' HTTP ' . $code);
    }

    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

if (($_GET['progress'] ?? '') === '1') {
    $file = updateDir() . '/progress.json';

    if (!is_file($file)) {
        update_json(['ok' => true, 'running' => false]);
    }

    $data = json_decode((string)file_get_contents($file), true);

    update_json([
        'ok' => true,
        'running' => true,
        'step' => is_array($data) ? (string)($data['step'] ?? '') : '',
        'time' => is_array($data) ? (int)($data['time'] ?? 0) : 0,
    ]);
}

function update_rrmdir(string $dir): void {
    if (!is_dir($dir)) return;

    foreach (scandir($dir) ?: [] as $item) {
        if ($item === '.' || $item === '..') continue;

        $path = $dir . '/' . $item;

        if (is_dir($path) && !is_link($path)) {
            update_rrmdir($path);
        } else {
            @unlink($path);
        }
    }

    @rmdir($dir);
}

function update_copy_dir(string $from, string $to): void {
    foreach (scandir($from) ?: [] as $item) {
        if ($item === '.' || $item === '..') continue;
        if (in_array($item, update_blocked_dirs(), true)) continue;
        if (in_array($item, update_blocked_files(), true)) continue;

        $src = $from . '/' . $item;
        $dst = $to . '/' . $item;

        if (is_dir($src) && !is_link($src)) {
            if (!is_dir($dst)) {
                brivaciaLog('update/info.log', 'Create directory: ' . $dst);
                mkdir($dst, 0755, true);
            }

            update_copy_dir($src, $dst);
            continue;
        }

        if (!copy($src, $dst)) {
            throw new RuntimeException('Unable to copy ' . $src);
        }
    }
}

function update_verify_package(string $source): void {
    $required = [
        'api',
        'assets',
        'includes',
        'index.php',
    ];

    foreach ($required as $path) {
        $full = $source . '/' . $path;

        if (!file_exists($full)) {
            throw new RuntimeException('Invalid update package: missing ' . $path);
        }
    }
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    update_json(['ok' => false, 'error' => 'Method not allowed'], 405);
}

brivaciaLog('update/info.log', 'Starting update');

$info = brivaciaUpdateInfo();

if (empty($info['available']) && !brivaciaTestUpdate()) {
    update_json(['ok' => false, 'error' => 'No update available'], 400);
}

$download = (string)($info['download'] ?? '');

if ($download === '' || !str_starts_with($download, 'https://')) {
    update_json(['ok' => false, 'error' => 'Invalid download URL'], 400);
}

$dir = updateDir();
$zipFile = $dir . '/brivacia.zip';
$extractDir = $dir . '/extracted';

@unlink($dir . '/progress.json');

update_rrmdir($extractDir);
@unlink($zipFile);

update_progress('download');
brivaciaLog('update/info.log', 'Download URL: ' . $download);

$zipData = @file_get_contents($download);

if (!is_string($zipData) || $zipData === '') {
    update_json(['ok' => false, 'error' => 'Unable to download update'], 500);
}

brivaciaLog('update/info.log', 'Downloaded bytes: ' . strlen($zipData));

if (file_put_contents($zipFile, $zipData, LOCK_EX) === false) {
    update_json(['ok' => false, 'error' => 'Unable to save update zip'], 500);
}

update_progress('extract');

$zip = new ZipArchive();

if ($zip->open($zipFile) !== true) {
    update_cleanup($dir, $zipFile, $extractDir);
    update_json(['ok' => false, 'error' => 'Unable to open update zip'], 500);
}

mkdir($extractDir, 0755, true);

if (!$zip->extractTo($extractDir)) {
    $zip->close();
    update_cleanup($dir, $zipFile, $extractDir);
    update_json(['ok' => false, 'error' => 'Unable to extract update zip'], 500);
}

$zip->close();

$source = $extractDir;

if (is_dir($extractDir . '/brivacia')) {
    $source = $extractDir . '/brivacia';
}

brivaciaLog('update/info.log', 'Extracted source: ' . $source);

update_progress('verify');

try {
    update_verify_package($source);
} catch (Throwable $e) {
    update_cleanup($dir, $zipFile, $extractDir);
    update_json([
        'ok' => false,
        'error' => $e->getMessage(),
    ], 500);
}

update_progress('install');

try {
    update_copy_dir($source, brivacia_root_path());
} catch (Throwable $e) {
    update_cleanup($dir, $zipFile, $extractDir);
    update_json([
        'ok' => false,
        'error' => $e->getMessage(),
    ], 500);
}

update_progress('cleanup');
update_rrmdir($extractDir);
@unlink($zipFile);
@unlink($dir . '/progress.json');

brivaciaLog('update/info.log', 'Update done: ' . (string)($info['latest'] ?? ''));

update_json([
    'ok' => true,
    'version' => (string)($info['latest'] ?? ''),
]);