v3.0.0.2
FlatlyPage
- <?php
- require_once __DIR__ . '/../config.php';
- require_once __DIR__ . '/sidebar.php';
- require_once __DIR__ . '/updater-lib.php';
-
- require_login();
-
- $currentVersion = Updater::currentVersion();
- $updaterEnabled = Updater::enabled();
-
- $message = '';
- $message_type = 'success';
- $result = null;
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $message = 'Your session timed out while the page was open. Please try again.';
- $message_type = 'error';
- } else {
- $action = (string) ($_POST['action'] ?? '');
-
- if ($action === 'toggle_updater') {
- $updaterEnabled = ($_POST['enabled'] ?? '0') === '1';
- if (Updater::setEnabled($updaterEnabled)) {
- $message = $updaterEnabled ? 'Updater enabled.' : 'Updater disabled.';
- } else {
- $updaterEnabled = !$updaterEnabled;
- $message = "Couldn't save the setting.";
- $message_type = 'error';
- }
- }
-
- if ($action === 'toggle_cleanup') {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- if (Updater::setAutoCleanup($turnOn)) {
- $message = $turnOn
- ? 'Old backups will now be deleted automatically.'
- : 'Old backups will be kept.';
- if ($turnOn) {
- $removed = Updater::cleanupBackups();
- $message .= $removed > 0 ? ' Removed ' . $removed . ' old backup(s).' : '';
- }
- } else {
- $message = "Couldn't save the setting.";
- $message_type = 'error';
- }
- }
-
- if ($action === 'toggle_auto_update') {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- if ($turnOn && !$updaterEnabled) {
- $message = 'Turn on the updater first.';
- $message_type = 'error';
- } elseif ($turnOn && ($_POST['accept_risk'] ?? '') !== '1') {
- $message = 'Please tick the box to confirm you understand the risks.';
- $message_type = 'error';
- } elseif (Updater::setAutoUpdate($turnOn)) {
- $message = $turnOn
- ? "Automatic updates are on. Add the cron job below, or they won't run."
- : 'Automatic updates are off.';
- } else {
- $message = "Couldn't save the setting.";
- $message_type = 'error';
- }
- }
-
- if ($action === 'regenerate_cron_token') {
- Updater::regenerateCronToken();
- $message = "New cron link created. Update it in your cron job, the old one won't work anymore.";
- }
-
- if ($action === 'check_updates' || $action === 'download_release') {
- if (!$updaterEnabled) {
- $message = 'The updater is turned off.';
- $message_type = 'error';
- } else {
- session_write_close();
- set_time_limit(180);
- $result = $action === 'download_release' ? Updater::compare($currentVersion) : Updater::check($currentVersion);
- if (!$result['ok']) {
- $message = $result['error'];
- $message_type = 'error';
- $result = null;
- }
- }
- }
-
- if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
- if ($action === 'install_release' && !$updaterEnabled) {
- $message = 'The updater is turned off.';
- $message_type = 'error';
- } else {
- session_write_close();
- set_time_limit(300);
- $backupId = (string) ($_POST['backup_id'] ?? '');
-
- if ($action === 'install_release') {
- $outcome = Updater::install($currentVersion, ($_POST['overwrite_htaccess'] ?? '') === '1');
- $message = $outcome['ok']
- ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files). Your old files were backed up as ' . $outcome['backup'] . '.'
- : $outcome['error'];
- } elseif ($action === 'restore_backup') {
- $outcome = Updater::restore($backupId);
- $message = $outcome['ok']
- ? 'Backup restored (' . $outcome['restored'] . ' files). You are now on version ' . $outcome['version'] . '.'
- : $outcome['error'];
- } else {
- $outcome = ['ok' => Updater::deleteBackup($backupId)];
- $message = $outcome['ok'] ? 'Backup deleted.' : "Couldn't find that backup.";
- }
-
- $message_type = $outcome['ok'] ? 'success' : 'error';
- $currentVersion = Updater::currentVersion();
- }
- }
- }
- }
-
- $autoUpdate = Updater::autoUpdate();
- $lastAutoRun = Updater::lastAutomaticRun();
- $autoRunning = $autoUpdate && $updaterEnabled;
- $cronUrl = $autoRunning ? SITE_URL . '/cron-update?token=' . Updater::cronToken() : '';
- $autoCleanup = Updater::autoCleanup();
- if ($autoCleanup) {
- Updater::cleanupBackups();
- }
- $backups = Updater::backups();
- $csrf_token = generate_csrf_token();
-
- $statusLabels = ['added' => 'Added', 'modified' => 'Edited'];
- ?>
- <!DOCTYPE html>
- <html lang="en">
-
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Updater - FlatlyPage CMS</title>
- <link rel="icon" href="admin.ico?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>" type="image/x-icon">
- <?= admin_font_head() ?>
- <link rel="stylesheet" href="/css/admin.css?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>">
- <script src="/assets/js/admin-theme.js?v=5"></script>
- </head>
-
- <body>
- <div class="app">
- <?php admin_sidebar('updater'); ?>
-
- <main class="main">
- <header class="main-header">
- <button class="mobile-nav-toggle" onclick="document.querySelector('.sidebar').classList.add('open')">
- <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
- <line x1="3" y1="6" x2="21" y2="6" />
- <line x1="3" y1="12" x2="21" y2="12" />
- <line x1="3" y1="18" x2="21" y2="18" />
- </svg>
- </button>
-
- <div class="main-header-inner">
- <h1>Updater</h1>
- <div class="header-actions">
- <a class="btn btn-secondary btn-sm" href="javascript:void(0)" onclick="toggleTheme()" id="theme-toggle-btn">
- <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16">
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
- </svg>
- <span>Theme</span>
- </a>
- </div>
- </div>
- </header>
-
- <div class="main-content">
- <?php if ($message): ?>
- <div class="message <?= e($message_type) ?>">
- <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
- <?php if ($message_type === 'success'): ?>
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
- <polyline points="22 4 12 14.01 9 11.01" />
- <?php else: ?>
- <circle cx="12" cy="12" r="10" />
- <line x1="12" y1="8" x2="12" y2="12" />
- <line x1="12" y1="16" x2="12.01" y2="16" />
- <?php endif; ?>
- </svg>
- <?= e($message) ?>
- </div>
- <?php endif; ?>
-
- <div class="card" style="margin-bottom: 24px;">
- <div class="card-header">
- <h3 class="card-title">Status</h3>
- </div>
- <div class="card-body">
- <p>Installed version: <strong><?= e($currentVersion) ?></strong>
- <span class="upd-pill <?= $updaterEnabled ? 'upd-pill-added' : 'upd-pill-muted' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></p>
-
- <div class="upd-actions">
- <?php if ($updaterEnabled): ?>
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="check_updates">
- <button type="submit" class="btn btn-primary">Check for updates</button>
- </form>
- <?php endif; ?>
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="toggle_updater">
- <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
- <button type="submit" class="btn <?= $updaterEnabled ? 'btn-danger' : 'btn-secondary' ?>"><?= $updaterEnabled ? 'Disable updater' : 'Enable updater' ?></button>
- </form>
- </div>
- <?php if (!$updaterEnabled): ?>
- <p class="form-hint">The updater is off by default. While it's off, your site never contacts the update server.</p>
- <?php endif; ?>
-
- <form method="POST" class="upd-actions">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="toggle_cleanup">
- <input type="hidden" name="enabled" value="<?= $autoCleanup ? '0' : '1' ?>">
- <button type="submit" class="btn btn-secondary"><?= $autoCleanup ? 'Turn off backup cleanup' : 'Turn on backup cleanup' ?></button>
- </form>
- <p class="form-hint"><?= $autoCleanup ? 'Backup cleanup is on, so backups older than 30 days are deleted automatically.' : 'Backup cleanup is off. Turn it on to delete backups older than 30 days automatically.' ?></p>
- </div>
- </div>
-
- <div class="card" style="margin-bottom: 24px;">
- <div class="card-header">
- <h3 class="card-title">Automatic updates</h3>
- <span class="upd-pill <?= $autoRunning ? 'upd-pill-deleted' : 'upd-pill-muted' ?>"><?= $autoRunning ? 'On' : 'Off' ?></span>
- </div>
- <div class="card-body">
- <div class="upd-warning" role="note">
- <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
- <line x1="12" y1="9" x2="12" y2="13" />
- <line x1="12" y1="17" x2="12.01" y2="17" />
- </svg>
- <div>
- <strong>Automatic updates can break your site.</strong>
- <p>When this is on, every new FlatlyPage version is installed on its own, <strong>without asking you and without telling you</strong>. Nobody checks the changes before they go live.</p>
- <ul>
- <li>An update can break your site, or parts of it.</li>
- <li>It can overwrite files you changed yourself, like edited templates, fixes or your own code, and change how things work without you noticing.</li>
- <li>You'll only find out when you open this page or spot a problem on your site.</li>
- </ul>
- <p>To limit the damage, your files are backed up before each update, .htaccess is never touched, and if the homepage or admin stops working right after an update, the backup is put back automatically. This won't catch every problem.</p>
- <p>Only turn this on if you don't edit the CMS files and you check your site regularly.</p>
- </div>
- </div>
-
- <?php if ($autoRunning): ?>
- <form method="POST" class="upd-actions">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="toggle_auto_update">
- <input type="hidden" name="enabled" value="0">
- <button type="submit" class="btn btn-danger">Turn off automatic updates</button>
- </form>
-
- <h4 class="upd-heading">Cron job</h4>
- <p class="form-hint">Add one of these as a cron job in your hosting panel, for example once a day. Without a cron job, nothing will run.</p>
- <label class="form-label" for="updCronCli">Command (recommended)</label>
- <input type="text" id="updCronCli" class="form-input upd-copy" readonly value="<?= e('php ' . BASE_DIR . '/cron-update.php') ?>" onclick="this.select()">
- <label class="form-label" for="updCronUrl" style="margin-top: 12px;">Or use this link (keep it private)</label>
- <input type="text" id="updCronUrl" class="form-input upd-copy" readonly value="<?= e($cronUrl) ?>" onclick="this.select()">
- <p class="form-hint">Example: <code>0 4 * * * wget -qO- "<?= e($cronUrl) ?>"</code></p>
- <form method="POST" class="upd-actions" onsubmit="return confirm('Create a new cron link? The current one will stop working.');">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="regenerate_cron_token">
- <button type="submit" class="btn btn-secondary btn-sm">Create new link</button>
- </form>
- <?php elseif (!$updaterEnabled): ?>
- <p class="form-hint">Turn on the updater above to use automatic updates.<?= $autoUpdate ? ' Automatic updates are on, but they won\'t run while the updater is off.' : '' ?></p>
- <?php else: ?>
- <form method="POST" class="upd-auto-form">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="toggle_auto_update">
- <input type="hidden" name="enabled" value="1">
- <label class="checkbox-label">
- <input type="checkbox" name="accept_risk" value="1" required>
- <span>I understand that updates will be installed without asking me, and could break my site or overwrite my changes.</span>
- </label>
- <button type="submit" class="btn btn-secondary">Turn on automatic updates</button>
- </form>
- <?php endif; ?>
-
- <?php if ($lastAutoRun !== null): ?>
- <p class="form-hint upd-last-run">
- Last automatic run: <?= e(date('Y-m-d H:i', strtotime($lastAutoRun['at']) ?: time())) ?>
- <span class="upd-pill <?= $lastAutoRun['ok'] ? 'upd-pill-added' : 'upd-pill-deleted' ?>"><?= $lastAutoRun['ok'] ? 'OK' : 'Problem' ?></span><br>
- <?= e($lastAutoRun['message']) ?>
- </p>
- <?php endif; ?>
- </div>
- </div>
-
- <?php if ($result !== null): ?>
- <div class="card" style="margin-bottom: 24px;">
- <div class="card-header">
- <h3 class="card-title">Result</h3>
- </div>
- <div class="card-body">
- <?php if ($result['status'] === 'update'): ?>
- <p><strong>Update available: <?= e($result['remote_version']) ?></strong><?= $result['released_at'] !== '' ? ' (' . e($result['released_at']) . ')' : '' ?></p>
- <?php elseif ($result['status'] === 'current'): ?>
- <p><strong>You are running the latest version.</strong></p>
- <?php else: ?>
- <p>Your version (<?= e($currentVersion) ?>) is newer than the latest release (<?= e($result['remote_version']) ?>).</p>
- <?php endif; ?>
-
- <?php if (!empty($result['changelog'])): ?>
- <h4 class="upd-heading">Changes</h4>
- <ul class="upd-changelog">
- <?php foreach ($result['changelog'] as $entry): ?>
- <li><?= e($entry) ?></li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
-
- <?php if (!$result['compared']): ?>
- <form method="POST" class="upd-actions">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="download_release">
- <button type="submit" class="btn btn-primary">Download and show changes</button>
- </form>
- <p class="form-hint">This downloads the new version and compares it with your files. Nothing is installed or changed yet.</p>
- <?php elseif (empty($result['files'])): ?>
- <p class="form-hint">Your files already match this version.</p>
- <?php elseif ($result['status'] !== 'ahead'): ?>
- <?php $installLabel = $result['status'] === 'update' ? 'Install version' : 'Sync your files with version'; ?>
- <form method="POST" class="upd-actions" onsubmit="return confirm(<?= e(json_encode($installLabel . ' ' . $result['remote_version'] . '? Your current files are backed up first, and if anything goes wrong, everything is put back the way it was.')) ?>);">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="install_release">
- <?php if (in_array('.htaccess', array_column($result['files'], 'path'), true)): ?>
- <label class="checkbox-label">
- <input type="checkbox" name="overwrite_htaccess" value="1">
- <span>Also replace .htaccess (any rules you added to it will be lost)</span>
- </label>
- <?php endif; ?>
- <button type="submit" class="btn btn-primary"><?= $result['status'] === 'update' ? 'Install update' : 'Sync files with release' ?></button>
- </form>
- <p class="form-hint">Your current files are backed up before anything is replaced. Your content, like uploaded media, pages and settings, is never touched.</p>
- <?php endif; ?>
- </div>
- </div>
-
- <?php if (!empty($result['files'])): ?>
- <h3 class="upd-heading">Files (<?= count($result['files']) ?>)</h3>
-
- <?php foreach ($result['files'] as $file): ?>
- <details class="upd-file">
- <summary>
- <span class="upd-pill upd-pill-<?= e($file['status']) ?>"><?= e($statusLabels[$file['status']]) ?></span>
- <span class="upd-path"><?= e($file['path']) ?></span>
- <span class="upd-stats">
- <?php if ($file['added'] > 0): ?><span class="upd-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
- <?php if ($file['removed'] > 0): ?><span class="upd-del">−<?= (int) $file['removed'] ?></span><?php endif; ?>
- <?php if ($file['moved'] > 0): ?><span class="upd-move">≈<?= intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
- </span>
- </summary>
-
- <?php if ($file['note'] !== ''): ?>
- <p class="form-hint upd-note"><?= e($file['note']) ?></p>
- <?php else: ?>
- <div class="upd-diff">
- <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
- <?php if ($hunkIndex > 0): ?>
- <div class="upd-gap">…</div>
- <?php endif; ?>
- <?php foreach ($hunk as $line): ?>
- <?php
- $lineClass = match ($line['type']) {
- 'add' => 'add',
- 'del' => 'del',
- 'moved_in', 'moved_out' => 'move',
- default => 'eq',
- };
- $marker = match ($line['type']) {
- 'add' => '+',
- 'del' => '−',
- 'moved_in' => '↓',
- 'moved_out' => '↑',
- default => ' ',
- };
- ?>
- <div class="upd-line upd-line-<?= $lineClass ?>">
- <span class="upd-ln"><?= $line['old'] ?? '' ?></span>
- <span class="upd-ln"><?= $line['new'] ?? '' ?></span>
- <span class="upd-marker"><?= $marker ?></span>
- <span class="upd-code"><?= e($line['text']) ?></span>
- </div>
- <?php endforeach; ?>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
- </details>
- <?php endforeach; ?>
-
- <?php if ($result['skipped'] > 0): ?>
- <p class="form-hint"><?= (int) $result['skipped'] ?> files were skipped because they already match yours or couldn't be placed safely.</p>
- <?php endif; ?>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if (!empty($backups)): ?>
- <div class="card" style="margin-bottom: 24px;">
- <div class="card-header">
- <h3 class="card-title">Backups</h3>
- </div>
- <table class="table">
- <thead>
- <tr>
- <th>Created</th>
- <th>Version</th>
- <th>Files</th>
- <th>Status</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ($backups as $backup): ?>
- <tr>
- <td><?= e(substr($backup['created_at'], 0, 19)) ?></td>
- <td><?= e($backup['from_version']) ?> → <?= e($backup['to_version']) ?></td>
- <td><?= (int) $backup['files'] ?></td>
- <td>
- <?php if ($backup['restored']): ?>
- <span class="upd-pill upd-pill-modified">Restored</span>
- <?php elseif ($backup['completed']): ?>
- <span class="upd-pill upd-pill-added">Installed</span>
- <?php else: ?>
- <span class="upd-pill upd-pill-deleted">Not completed</span>
- <?php endif; ?>
- </td>
- <td class="upd-row-actions">
- <form method="POST" onsubmit="return confirm('Restore this backup? The files from the update will be swapped back to the saved versions.');">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="restore_backup">
- <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
- <button type="submit" class="btn btn-secondary btn-sm">Restore</button>
- </form>
- <form method="POST" onsubmit="return confirm('Delete this backup? You can\'t undo this.');">
- <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
- <input type="hidden" name="action" value="delete_backup">
- <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
- <button type="submit" class="btn btn-danger btn-sm">Delete</button>
- </form>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- </div>
- <?php endif; ?>
- </div>
- </main>
- </div>
- </body>
-
- </html>
-