v2.0.0.1
Publisium
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/activity-log.php';
- require_once __DIR__ . '/includes/updater.php';
-
- Auth::boot();
- Auth::requireRoleAtLeast(Auth::ROLE_EDITOR_IN_CHIEF);
-
- $db = Database::site();
- $currentVersion = AppVersion::current($db);
- $lockedByConfig = Updater::lockedByConfig();
- $canInstall = Auth::role() === Auth::ROLE_SUPER_ADMIN;
-
- $flashMessage = null;
- $flashType = 'success';
- $result = null;
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
- $flashMessage = 'Your session expired. Please try again.';
- $flashType = 'error';
- } else {
- $action = $_POST['action'] ?? '';
-
- if ($action === 'toggle_updater') {
- if ($lockedByConfig) {
- $flashMessage = 'Updates are switched off in the server settings.';
- $flashType = 'error';
- } else {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- Updater::setEnabled($db, $turnOn);
- $flashMessage = $turnOn ? 'Updates switched on.' : 'Updates switched off.';
- }
- }
-
- if ($action === 'save_auto_update') {
- $enableAuto = isset($_POST['auto_enabled']);
- if (!$canInstall) {
- $flashMessage = 'Only the Super Admin can change automatic updates.';
- $flashType = 'error';
- } elseif ($lockedByConfig) {
- $flashMessage = 'Updates are switched off in the server settings.';
- $flashType = 'error';
- } elseif ($enableAuto && !isset($_POST['auto_accept_risk'])) {
- $flashMessage = 'Tick the box to confirm you understand the risks first.';
- $flashType = 'error';
- } else {
- Updater::setAutoSettings($db, $enableAuto, isset($_POST['auto_htaccess']));
- ActivityLog::record($enableAuto ? 'updater.auto_enable' : 'updater.auto_disable', 'updater');
- $flashMessage = $enableAuto ? 'Automatic updates turned on.' : 'Automatic updates turned off.';
- }
- }
-
- if ($action === 'check_updates' || $action === 'download_release') {
- if (!Updater::enabled($db)) {
- $flashMessage = 'Updates are switched off.';
- $flashType = 'error';
- } else {
- session_write_close();
- set_time_limit(180);
- $result = $action === 'download_release' ? Updater::compare($currentVersion) : Updater::check($currentVersion);
- if (!$result['ok']) {
- $flashMessage = $result['error'];
- $flashType = 'error';
- $result = null;
- }
- }
- }
-
- if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
- if (!$canInstall) {
- $flashMessage = 'Only the Super Admin can install updates and manage backups.';
- $flashType = 'error';
- } elseif ($action === 'install_release' && !Updater::enabled($db)) {
- $flashMessage = 'Updates are switched off.';
- $flashType = 'error';
- } else {
- session_write_close();
- set_time_limit(300);
- $backupId = (string) ($_POST['backup_id'] ?? '');
-
- if ($action === 'install_release') {
- $outcome = Updater::install($currentVersion, ($_POST['update_htaccess'] ?? '1') === '1');
- $flashMessage = $outcome['ok']
- ? 'All done, you’re now on ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files updated). We saved a backup called ' . $outcome['backup'] . '.'
- : $outcome['error'];
- } elseif ($action === 'restore_backup') {
- $outcome = Updater::restore($backupId);
- $flashMessage = $outcome['ok']
- ? 'Backup restored (' . $outcome['restored'] . ' files). You’re back on ' . $outcome['version'] . '.'
- : $outcome['error'];
- } else {
- $outcome = ['ok' => Updater::deleteBackup($backupId)];
- $flashMessage = $outcome['ok'] ? 'Backup deleted.' : 'We couldn’t find that backup.';
- }
-
- $flashType = $outcome['ok'] ? 'success' : 'error';
- $currentVersion = AppVersion::current($db);
- }
- }
- }
- }
-
- Updater::pruneBackups();
- $backups = Updater::backups();
-
- $updaterEnabled = Updater::enabled($db);
- $autoUpdate = Updater::autoSettings($db);
- $cronCommand = 'php ' . str_replace('\\', '/', __DIR__) . '/cron-update.php';
-
- $statusLabels = ['added' => 'Added', 'modified' => 'Edited', 'deleted' => 'Removed'];
- $statusClasses = ['added' => 'published', 'modified' => 'scheduled', 'deleted' => 'rejected'];
-
- $dashActivePage = 'settings';
- $dashPageTitle = 'Updates';
-
- require __DIR__ . '/includes/dash-header.php';
-
- ?>
-
- <?php if ($flashMessage !== null): ?>
- <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
- <?php endif; ?>
-
- <h1 class="dash-title"><?= Icons::icon('refresh', 'icon icon-lg') ?>Updates</h1>
-
- <div class="dash-cards">
- <div class="dash-card">
- <?= Icons::icon('layers') ?>
- <div class="dash-card-value"><?= htmlspecialchars($currentVersion) ?></div>
- <div class="dash-card-label">Installed version</div>
- </div>
- <div class="dash-card">
- <?= Icons::icon('toggle') ?>
- <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></div>
- <div class="dash-card-label">Updates</div>
- </div>
- </div>
-
- <?php if ($lockedByConfig): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Updates are switched off in the server settings (UPDATER_DISABLED=1), so the site never contacts the update server.</p>
- <?php else: ?>
- <div class="publish-actions">
- <?php if ($updaterEnabled): ?>
- <form method="post">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="check_updates">
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('refresh', 'icon icon-sm') ?>Check for updates</button>
- </form>
- <?php endif; ?>
- <form method="post">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="toggle_updater">
- <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
- <?php if ($updaterEnabled): ?>
- <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Switch off updates</button>
- <?php else: ?>
- <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Switch on updates</button>
- <?php endif; ?>
- </form>
- </div>
- <?php if (!$updaterEnabled): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Updates are off to start with. While they’re off, the site doesn’t contact the update server at all.</p>
- <?php else: ?>
- <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates</h2>
-
- <div class="updater-warning">
- <?= Icons::icon('shield', 'icon icon-sm') ?>
- <div>
- <strong>Automatic updates can break your site.</strong>
- New versions get installed on a schedule, without anyone checking them first. An update can replace files you changed yourself,
- change or remove things you rely on, or stop the site from working, and nobody is asked first.
- We save a backup before each install that you can restore below, but do check the site after every update.
- </div>
- </div>
-
- <?php if ($canInstall): ?>
- <form method="post" class="updater-auto-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_auto_update">
- <label><input type="checkbox" name="auto_enabled" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new versions automatically on a schedule</label>
- <label><input type="checkbox" name="auto_htaccess" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also replace .htaccess files (leave unticked to keep your own server rules)</label>
- <label><input type="checkbox" name="auto_accept_risk" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> I understand updates will be installed without me checking them, and they may undo my changes or break the site</label>
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
- <?php else: ?>
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change automatic updates. They are currently <?= $autoUpdate['enabled'] ? 'on' : 'off' ?>.</p>
- <?php endif; ?>
-
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Add this command to your server’s scheduled tasks (cron), for example once a day. It only works from the server, not in a browser:</p>
- <pre class="updater-cron"><code><?= htmlspecialchars($cronCommand) ?></code></pre>
-
- <?php if ($autoUpdate['last_run'] !== ''): ?>
- <p class="updater-note"><?= Icons::icon('clock', 'icon icon-sm') ?>Last automatic check: <?= htmlspecialchars($autoUpdate['last_run']) ?>, <?= htmlspecialchars($autoUpdate['last_result']) ?></p>
- <?php elseif ($autoUpdate['enabled']): ?>
- <p class="updater-note"><?= Icons::icon('clock', 'icon icon-sm') ?>The scheduled check hasn’t run yet.</p>
- <?php endif; ?>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if ($result !== null): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>What we found</h2>
-
- <?php if ($result['status'] === 'update'): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>A new version is ready: <?= htmlspecialchars($result['remote_version']) ?><?= $result['released_at'] !== '' ? ' (' . htmlspecialchars($result['released_at']) . ')' : '' ?></div>
- <?php elseif ($result['status'] === 'current'): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>You’re on the latest version.</div>
- <?php else: ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>Your site (<?= htmlspecialchars($currentVersion) ?>) is newer than the latest release (<?= htmlspecialchars($result['remote_version']) ?>).</div>
- <?php endif; ?>
-
- <?php if (!empty($result['changelog'])): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('list', 'icon icon-sm') ?>What’s changed</h2>
- <ul class="updater-changelog">
- <?php foreach ($result['changelog'] as $entry): ?>
- <li><?= htmlspecialchars($entry) ?></li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
-
- <?php if (!$result['compared']): ?>
- <form method="post" class="publish-actions">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="download_release">
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>Show me what’s changed</button>
- </form>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>We download the new version and compare it with your files. Nothing gets installed or changed yet.</p>
- <?php elseif (empty($result['files'])): ?>
- <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files already match the new version.</p>
- <?php endif; ?>
-
- <?php
- $htaccessCount = 0;
- foreach ($result['files'] as $candidate) {
- if (Updater::isHtaccess($candidate['path'])) {
- $htaccessCount++;
- }
- }
- ?>
-
- <?php if ($result['compared'] && $result['status'] !== 'ahead' && !empty($result['files'])): ?>
- <?php if ($canInstall): ?>
- <form method="post" class="publish-actions" onsubmit="return confirm('<?= $result['status'] === 'update' ? 'Install version' : 'Match your files to version' ?> <?= htmlspecialchars($result['remote_version'], ENT_QUOTES) ?>? We back up the files first, and undo everything if something goes wrong.');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="install_release">
- <?php if ($htaccessCount > 0): ?>
- <input type="hidden" name="update_htaccess" value="0">
- <label style="flex-basis:100%;"><input type="checkbox" name="update_htaccess" value="1" checked> Also update .htaccess files (<?= (int) $htaccessCount ?>). Untick to keep your own server rules.</label>
- <?php endif; ?>
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?><?= $result['status'] === 'update' ? 'Install update' : 'Match files to this version' ?></button>
- </form>
- <p class="updater-note"><?= Icons::icon('shield', 'icon icon-sm') ?>Files are backed up before they’re replaced. Backups older than 30 days are cleaned up automatically.</p>
- <?php else: ?>
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can install updates.</p>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if (!empty($result['files'])): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Files (<?= count($result['files']) ?>)</h2>
-
- <?php foreach ($result['files'] as $file): ?>
- <details class="updater-file">
- <summary>
- <span class="status-pill status-<?= $statusClasses[$file['status']] ?>"><?= $statusLabels[$file['status']] ?></span>
- <span class="updater-file-path"><?= htmlspecialchars($file['path']) ?><?= Updater::isHtaccess($file['path']) ? ' (optional)' : '' ?></span>
- <span class="updater-file-stats">
- <?php if ($file['added'] > 0): ?><span class="updater-stat-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
- <?php if ($file['removed'] > 0): ?><span class="updater-stat-del">−<?= (int) $file['removed'] ?></span><?php endif; ?>
- <?php if ($file['moved'] > 0): ?><span class="updater-stat-move">≈<?= (int) intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
- </span>
- </summary>
-
- <?php if ($file['note'] !== ''): ?>
- <p class="updater-note"><?= htmlspecialchars($file['note']) ?></p>
- <?php else: ?>
- <div class="updater-diff">
- <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
- <?php if ($hunkIndex > 0): ?>
- <div class="updater-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="updater-line updater-line-<?= $lineClass ?>">
- <span class="updater-ln"><?= $line['old'] ?? '' ?></span>
- <span class="updater-ln"><?= $line['new'] ?? '' ?></span>
- <span class="updater-marker"><?= $marker ?></span>
- <span class="updater-code"><?= htmlspecialchars($line['text']) ?></span>
- </div>
- <?php endforeach; ?>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
- </details>
- <?php endforeach; ?>
-
- <?php if ($result['skipped'] > 0): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?><?= (int) $result['skipped'] ?> files were skipped because they already match yours or couldn’t be read.</p>
- <?php endif; ?>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if (!empty($backups)): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('save', 'icon icon-sm') ?>Backups</h2>
- <table class="dash-table">
- <thead>
- <tr>
- <th><?= Icons::icon('clock', 'icon icon-sm') ?>Created</th>
- <th><?= Icons::icon('layers', 'icon icon-sm') ?>Version</th>
- <th><?= Icons::icon('code', 'icon icon-sm') ?>Files</th>
- <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ($backups as $backup): ?>
- <tr>
- <td data-label="Created"><?= htmlspecialchars(substr($backup['created_at'], 0, 19)) ?></td>
- <td data-label="Version"><?= htmlspecialchars($backup['from_version']) ?> → <?= htmlspecialchars($backup['to_version']) ?></td>
- <td data-label="Files"><?= (int) $backup['files'] ?></td>
- <td data-label="Status">
- <?php if ($backup['restored']): ?>
- <span class="status-pill status-scheduled">Restored</span>
- <?php elseif ($backup['completed']): ?>
- <span class="status-pill status-published">Installed</span>
- <?php else: ?>
- <span class="status-pill status-rejected">Not completed</span>
- <?php endif; ?>
- </td>
- <td class="dash-table-actions">
- <?php if ($canInstall): ?>
- <form method="post" onsubmit="return confirm('Restore this backup? Files from the update will be swapped back to the saved versions.');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="restore_backup">
- <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
- <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
- </form>
- <form method="post" onsubmit="return confirm('Delete this backup for good?');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="delete_backup">
- <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
- <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
- </form>
- <?php endif; ?>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php endif; ?>
-
- <?php require __DIR__ . '/includes/dash-footer.php'; ?>
-