v1.0.0.0
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 = 'Security check failed, please try again.';
- $flashType = 'error';
- } else {
- $action = $_POST['action'] ?? '';
-
- if ($action === 'toggle_updater') {
- if ($lockedByConfig) {
- $flashMessage = 'The updater is disabled in the server configuration.';
- $flashType = 'error';
- } else {
- $turnOn = ($_POST['enabled'] ?? '0') === '1';
- Updater::setEnabled($db, $turnOn);
- $flashMessage = $turnOn ? 'Updater enabled.' : 'Updater disabled.';
- }
- }
-
- if ($action === 'save_auto_update' || $action === 'regenerate_cron_token') {
- if (!$canInstall) {
- $flashMessage = 'Only the Super Admin can manage automatic updates.';
- $flashType = 'error';
- } elseif (!Updater::enabled($db)) {
- $flashMessage = 'Turn on the updater first.';
- $flashType = 'error';
- } elseif ($action === 'regenerate_cron_token') {
- Updater::regenerateCronToken($db);
- $flashMessage = 'A new cron token was generated. Update the URL in your cron job — the old one no longer works.';
- } else {
- $turnOnAuto = ($_POST['auto_enabled'] ?? '0') === '1';
- if ($turnOnAuto && ($_POST['auto_risk_ack'] ?? '') !== '1') {
- $flashMessage = 'To turn on automatic updates, confirm that you understand the risks.';
- $flashType = 'error';
- } else {
- Updater::saveAutoSettings($db, $turnOnAuto, ($_POST['auto_htaccess'] ?? '0') === '1');
- $flashMessage = $turnOnAuto ? 'Automatic updates turned on. Add the cron job below to run them.' : 'Automatic updates turned off.';
- }
- }
- }
-
- if ($action === 'check_updates' || $action === 'download_release') {
- if (!Updater::enabled($db)) {
- $flashMessage = 'The updater is disabled.';
- $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 manage backups.';
- $flashType = 'error';
- } elseif ($action === 'install_release' && !Updater::enabled($db)) {
- $flashMessage = 'The updater is disabled.';
- $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). A backup was saved as ' . $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.' : 'Backup not found.';
- }
-
- $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">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">Updater status</div>
- </div>
- </div>
-
- <?php if ($lockedByConfig): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The updater is turned off in the server configuration (UPDATER_DISABLED=1) and sends no requests.</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') ?>Disable updater</button>
- <?php else: ?>
- <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Enable updater</button>
- <?php endif; ?>
- </form>
- </div>
- <?php if (!$updaterEnabled): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The updater is off by default. While it is off, this site sends no requests to the update server.</p>
- <?php endif; ?>
-
- <?php if ($updaterEnabled): ?>
- <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates (cron)</h2>
-
- <div class="updater-auto-warning" role="alert">
- <strong><?= Icons::icon('shield', 'icon icon-sm') ?>Warning: automatic updates can be dangerous for your store.</strong>
- <ul>
- <li>New versions are downloaded and installed <strong>without any human review or confirmation</strong>.</li>
- <li>An update can <strong>overwrite or change features, templates and files</strong> — including your own edits to StocketBase files — and <strong>nobody will be asked or warned first</strong>.</li>
- <li>A broken or incompatible release can take the storefront, checkout or dashboard offline until someone restores a backup.</li>
- <li>Every automatic install makes a backup (kept for 30 days) that can be restored below, but restoring requires a person to log in.</li>
- </ul>
- <p>Leave this off unless you accept these risks. Manual updates above let you review every change before installing.</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 cron run</div>
- </div>
- </div>
-
- <?php if ($autoUpdate['last_run'] !== null): ?>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>Last result (<?= 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 releases automatically when the cron job runs</label>
- <label><input type="checkbox" name="auto_htaccess" value="1" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also overwrite .htaccess files during automatic updates (leave unchecked to keep your own server rules)</label>
- <label><input type="checkbox" name="auto_risk_ack" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> I understand that automatic updates can break the store and overwrite features without anyone being informed</label>
- <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save automatic update settings</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 to your hosting's cron jobs (once a day is enough). Nothing happens until the cron job runs.</p>
- <label>Command line (recommended)</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 system user as the web server, otherwise updated files may end up with the wrong owner.</p>
- <label>URL (for hosts without command-line cron)</label>
- <input type="text" readonly value="<?= htmlspecialchars('wget -q -O - "' . $cronHttpUrl . '"') ?>" onclick="this.select();">
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>The token in this URL lets anyone who knows it start an update. Keep it private.</p>
- <form method="post" onsubmit="return confirm('Generate a new token? The current cron URL 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') ?>Generate new token</button>
- </form>
- <?php endif; ?>
- <?php else: ?>
- <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change automatic updates.</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') ?>Update 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 are running the latest version.</div>
- <?php else: ?>
- <div class="dash-flash dash-flash-success"><?= Icons::icon('info', 'icon icon-sm') ?>This installation (<?= htmlspecialchars($currentVersion) ?>) is newer than the published version (<?= 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') ?>Download and show changes</button>
- </form>
- <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>The release is downloaded temporarily and compared with your files. Nothing is installed or changed.</p>
- <?php elseif (empty($result['files'])): ?>
- <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files are identical to the release.</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' : 'Sync your files with version' ?> <?= htmlspecialchars($result['remote_version'], ENT_QUOTES) ?>? A backup of the files being replaced is saved first, and everything is rolled back if a file cannot be written.');">
- <?= 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 ?>). Uncheck 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' : 'Sync files with release' ?></button>
- </form>
- <p class="updater-note"><?= Icons::icon('shield', 'icon icon-sm') ?>Files are backed up to updater-files/backups before being replaced. Backups older than 30 days are deleted 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 are identical to yours or have an invalid path.</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 the files from this backup? Files installed by the update will be replaced with 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 permanently?');">
- <?= 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'; ?>
-