v3.0.0.0
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 = 'Invalid request. 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 = 'Could not save the updater setting.';
- $message_type = 'error';
- }
- }
-
- if ($action === 'toggle_cleanup') {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- if (Updater::setAutoCleanup($turnOn)) {
- $message = $turnOn
- ? 'Automatic cleanup enabled.'
- : 'Automatic cleanup disabled.';
- if ($turnOn) {
- $removed = Updater::cleanupBackups();
- $message .= $removed > 0 ? ' Removed ' . $removed . ' old backup(s).' : '';
- }
- } else {
- $message = 'Could not save the setting.';
- $message_type = 'error';
- }
- }
-
- if ($action === 'toggle_auto_update') {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- if ($turnOn && !$updaterEnabled) {
- $message = 'Enable the updater first.';
- $message_type = 'error';
- } elseif ($turnOn && ($_POST['accept_risk'] ?? '') !== '1') {
- $message = 'Confirm that you understand the risks to turn on automatic updates.';
- $message_type = 'error';
- } elseif (Updater::setAutoUpdate($turnOn)) {
- $message = $turnOn
- ? 'Automatic updates enabled. Add the cron job below, otherwise nothing will run.'
- : 'Automatic updates disabled.';
- } else {
- $message = 'Could not save the setting.';
- $message_type = 'error';
- }
- }
-
- if ($action === 'regenerate_cron_token') {
- Updater::regenerateCronToken();
- $message = 'A new cron URL was generated. Update the URL in your cron job; the old one no longer works.';
- }
-
- if ($action === 'check_updates' || $action === 'download_release') {
- if (!$updaterEnabled) {
- $message = 'The updater is disabled.';
- $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 disabled.';
- $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). A backup was saved as ' . $outcome['backup'] . '.'
- : $outcome['error'];
- } elseif ($action === 'restore_backup') {
- $outcome = Updater::restore($backupId);
- $message = $outcome['ok']
- ? 'Backup restored (' . $outcome['restored'] . ' files). Version is now ' . $outcome['version'] . '.'
- : $outcome['error'];
- } else {
- $outcome = ['ok' => Updater::deleteBackup($backupId)];
- $message = $outcome['ok'] ? 'Backup deleted.' : 'Backup not found.';
- }
-
- $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>
- <meta name="generator" content="FlatlyPage CMS">
- <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 is off, this site sends no requests to 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">Backup cleanup: <?= $autoCleanup ? 'on. Backups older than 30 days are deleted automatically.' : '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>Warning: automatic updates can be dangerous for your site.</strong>
- <p>When this is on, a cron job installs every new FlatlyPage release by itself, <strong>without asking anyone and without notifying anyone</strong>. Nobody reviews the changes before they go live.</p>
- <ul>
- <li>An update can break the site or parts of it.</li>
- <li>It can overwrite files you changed yourself (edited templates, custom fixes, your own code in CMS files) and replace or change existing functions and behaviour, silently.</li>
- <li>You will only find out when you open this page or notice a problem on the site.</li>
- </ul>
- <p>Safety nets: the replaced files are backed up before every update, .htaccess is never overwritten, and if the homepage or admin page returns a server error right after the update, the backup is restored automatically. These checks do not catch every problem.</p>
- <p>Only turn this on if you do not modify 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 to your hosting's cron jobs, for example once a day. Automatic updates do nothing until a cron job calls them.</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 call this URL (keep it secret)</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('Generate a new cron URL? The current URL 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">Generate new URL</button>
- </form>
- <?php elseif (!$updaterEnabled): ?>
- <p class="form-hint">Enable the updater above to use automatic updates.<?= $autoUpdate ? ' Automatic updates are switched on but will not run while the updater is disabled.' : '' ?></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 my approval and may break the 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>This installation (<?= e($currentVersion) ?>) is newer than the published version (<?= 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">The release is downloaded temporarily and compared with your files. Nothing is installed or changed.</p>
- <?php elseif (empty($result['files'])): ?>
- <p class="form-hint">Your files are identical to the release.</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'] . '? A backup of the files being replaced is saved first, and everything is rolled back if a file cannot be written.')) ?>);">
- <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>Overwrite .htaccess (your own rules in 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">Files are backed up to updater-files/backups before being replaced. Your content (media/ and your own files in data/) is never touched; only data/default* files are updated.</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 are identical to yours or have an invalid path.</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 the files from this backup? Files installed by the update will be replaced with 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 permanently?');">
- <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>
-