v1.0.0.1
StocketBase
- <?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/updater.php';
-
- Auth::boot();
- Auth::requireRoleAtLeast(Auth::ROLE_STORE_OWNER);
-
- $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 your server settings.';
- $flashType = 'error';
- } else {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- Updater::setEnabled($db, $turnOn);
- $flashMessage = $turnOn ? 'Updates turned on.' : 'Updates turned off.';
- }
- }
-
- if ($action === 'save_auto_update' || $action === 'regenerate_cron_token') {
- if (!$canInstall) {
- $flashMessage = 'Only the Super Admin can change automatic updates.';
- $flashType = 'error';
- } elseif (!Updater::enabled($db)) {
- $flashMessage = 'Turn on updates first.';
- $flashType = 'error';
- } elseif ($action === 'regenerate_cron_token') {
- Updater::regenerateCronToken($db);
- $flashMessage = 'New link created. Put it in your cron job, the old one won\'t work anymore.';
- } else {
- $turnOnAuto = ($_POST['auto_enabled'] ?? '0') === '1';
- if ($turnOnAuto && ($_POST['auto_risk_ack'] ?? '') !== '1') {
- $flashMessage = 'Please tick the box to confirm you understand the risks first.';
- $flashType = 'error';
- } else {
- Updater::saveAutoSettings($db, $turnOnAuto, ($_POST['auto_htaccess'] ?? '0') === '1');
- $flashMessage = $turnOnAuto ? 'Automatic updates are on. Add the cron job below so they actually run.' : 'Automatic updates are off.';
- }
- }
- }
-
- if ($action === 'check_updates' || $action === 'download_release') {
- if (!Updater::enabled($db)) {
- $flashMessage = 'Updates are turned 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 or use backups.';
- $flashType = 'error';
- } elseif ($action === 'install_release' && !Updater::enabled($db)) {
- $flashMessage = 'Updates are turned 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']
- ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files changed). We saved a backup first: ' . $outcome['backup'] . '.'
- : $outcome['error'];
- } elseif ($action === 'restore_backup') {
- $outcome = Updater::restore($backupId);
- $flashMessage = $outcome['ok']
- ? 'Backup restored (' . $outcome['restored'] . ' files). Version is now ' . $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);
- $cronBaseUrl = rtrim((string) Config::get('APP_URL', ''), '/') . Config::get('APP_BASE_PATH', '') . '/cron-update.php';
- $cronHttpUrl = $cronBaseUrl . '?token=' . rawurlencode($autoUpdate['token']);
- $cronCliCommand = 'php ' . __DIR__ . DIRECTORY_SEPARATOR . 'cron-update.php';
-
- $statusLabels = ['added' => 'Added', 'modified' => 'Edited', 'deleted' => 'Removed'];
- $statusClasses = ['added' => 'published', 'modified' => 'scheduled', 'deleted' => 'rejected'];
-
- $dashActivePage = 'settings';
- $dashPageTitle = 'Updater';
-
- 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') ?>Updater</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">Your version</div>
- </div>
- <div class="dash-card">
- <?= Icons::icon('toggle') ?>
- <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'On' : 'Off' ?></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 your server settings (UPDATER_DISABLED=1), so this site never checks for them.</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') ?>Turn off updates</button>
- <?php else: ?>
- <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Turn on updates</button>
- <?php endif; ?>
- </form>
- </div>
- <?php if (!$updaterEnabled): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Updates are off until you turn them on. While they're off, your store never contacts the update server.</p>
- <?php endif; ?>
-
- <?php if ($updaterEnabled): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates</h2>
-
- <div class="updater-auto-warning" role="alert">
- <strong><?= Icons::icon('shield', 'icon icon-sm') ?>Heads up: automatic updates can break your store.</strong>
- <ul>
- <li>New versions get installed <strong>without anyone checking them first</strong>.</li>
- <li>An update can <strong>change or replace features and files</strong>, including your own edits, and <strong>nobody gets asked or warned</strong>.</li>
- <li>A bad release could take your store, checkout or dashboard down until someone restores a backup.</li>
- <li>We back up before every install and keep backups for 30 days, but someone still has to log in to restore one.</li>
- </ul>
- <p>Leave this off unless you're OK with that. With manual updates you see every change before it goes in.</p>
- </div>
-
- <div class="dash-cards">
- <div class="dash-card">
- <?= Icons::icon('toggle') ?>
- <div class="dash-card-value"><span class="status-pill status-<?= $autoUpdate['enabled'] ? 'rejected' : 'archived' ?>"><?= $autoUpdate['enabled'] ? 'On' : 'Off' ?></span></div>
- <div class="dash-card-label">Automatic updates</div>
- </div>
- <div class="dash-card">
- <?= Icons::icon('clock') ?>
- <div class="dash-card-value" style="font-size:14px;"><?= $autoUpdate['last_run'] !== null ? htmlspecialchars(substr((string) $autoUpdate['last_run']['at'], 0, 19)) : 'Never' ?></div>
- <div class="dash-card-label">Last automatic check</div>
- </div>
- </div>
-
- <?php if ($autoUpdate['last_run'] !== null): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Last time (<?= htmlspecialchars((string) $autoUpdate['last_run']['status']) ?>): <?= htmlspecialchars((string) $autoUpdate['last_run']['message']) ?></p>
- <?php endif; ?>
-
- <?php if ($canInstall): ?>
- <form method="post" class="settings-section" style="max-width:720px;">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="save_auto_update">
- <label><input type="checkbox" name="auto_enabled" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new versions automatically</label>
- <label><input type="checkbox" name="auto_htaccess" value="1" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also replace .htaccess files (leave this off if you changed them yourself)</label>
- <label><input type="checkbox" name="auto_risk_ack" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> I understand automatic updates can break my store and change things without telling anyone</label>
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
- </form>
-
- <?php if ($autoUpdate['enabled']): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Cron job</h2>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Add one of these as a cron job in your hosting panel. Once a day is plenty. Nothing happens until it runs.</p>
- <label>Command (best option)</label>
- <input type="text" readonly value="<?= htmlspecialchars($cronCliCommand) ?>" onclick="this.select();">
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Run it as the same user as your website, or the new files may end up with the wrong permissions.</p>
- <label>Link (if your host can't run commands)</label>
- <input type="text" readonly value="<?= htmlspecialchars('wget -q -O - "' . $cronHttpUrl . '"') ?>" onclick="this.select();">
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Anyone with this link can start an update, so keep it to yourself.</p>
- <form method="post" onsubmit="return confirm('Create a new link? The current one will stop working.');">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="regenerate_cron_token">
- <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Create a new link</button>
- </form>
- <?php endif; ?>
- <?php else: ?>
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change this.</p>
- <?php endif; ?>
- <?php endif; ?>
- <?php endif; ?>
-
- <?php if ($result !== null): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>Result</h2>
-
- <?php if ($result['status'] === 'update'): ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>New version available: <?= 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 up to date.</div>
- <?php else: ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>This installation (<?= 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') ?>Changes</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') ?>See what's changed</button>
- </form>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>We just compare the new version with your files. Nothing gets installed yet.</p>
- <?php elseif (empty($result['files'])): ?>
- <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files already match this 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 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 replace .htaccess files (<?= (int) $htaccessCount ?>). Untick if you changed them yourself.</label>
- <?php endif; ?>
- <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?><?= $result['status'] === 'update' ? 'Install update' : 'Match my files' ?></button>
- </form>
- <p class="updater-note"><?= Icons::icon('shield', 'icon icon-sm') ?>We back up your files before replacing anything. Backups are kept for 30 days.</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') ?>Changed 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're already the same or couldn't be used.</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">Didn't finish</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 ones.');">
- <?= 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? You can\'t get it back.');">
- <?= 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'; ?>
-