WebOrbiton
v1.0.0.1

StocketBase

404 lines · 22.4 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/includes/config.php';
  6. require_once __DIR__ . '/includes/database.php';
  7. require_once __DIR__ . '/includes/auth.php';
  8. require_once __DIR__ . '/includes/csrf.php';
  9. require_once __DIR__ . '/includes/updater.php';
  10. ​
  11. Auth::boot();
  12. Auth::requireRoleAtLeast(Auth::ROLE_STORE_OWNER);
  13. ​
  14. $db = Database::site();
  15. $currentVersion = AppVersion::current($db);
  16. $lockedByConfig = Updater::lockedByConfig();
  17. $canInstall = Auth::role() === Auth::ROLE_SUPER_ADMIN;
  18. ​
  19. $flashMessage = null;
  20. $flashType = 'success';
  21. $result = null;
  22. ​
  23. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  24. if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
  25. $flashMessage = 'Your session expired. Please try again.';
  26. $flashType = 'error';
  27. } else {
  28. $action = $_POST['action'] ?? '';
  29. ​
  30. if ($action === 'toggle_updater') {
  31. if ($lockedByConfig) {
  32. $flashMessage = 'Updates are switched off in your server settings.';
  33. $flashType = 'error';
  34. } else {
  35. $turnOn = ($_POST['enabled'] ?? '0') === '1';
  36. Updater::setEnabled($db, $turnOn);
  37. $flashMessage = $turnOn ? 'Updates turned on.' : 'Updates turned off.';
  38. }
  39. }
  40. ​
  41. if ($action === 'save_auto_update' || $action === 'regenerate_cron_token') {
  42. if (!$canInstall) {
  43. $flashMessage = 'Only the Super Admin can change automatic updates.';
  44. $flashType = 'error';
  45. } elseif (!Updater::enabled($db)) {
  46. $flashMessage = 'Turn on updates first.';
  47. $flashType = 'error';
  48. } elseif ($action === 'regenerate_cron_token') {
  49. Updater::regenerateCronToken($db);
  50. $flashMessage = 'New link created. Put it in your cron job, the old one won\'t work anymore.';
  51. } else {
  52. $turnOnAuto = ($_POST['auto_enabled'] ?? '0') === '1';
  53. if ($turnOnAuto && ($_POST['auto_risk_ack'] ?? '') !== '1') {
  54. $flashMessage = 'Please tick the box to confirm you understand the risks first.';
  55. $flashType = 'error';
  56. } else {
  57. Updater::saveAutoSettings($db, $turnOnAuto, ($_POST['auto_htaccess'] ?? '0') === '1');
  58. $flashMessage = $turnOnAuto ? 'Automatic updates are on. Add the cron job below so they actually run.' : 'Automatic updates are off.';
  59. }
  60. }
  61. }
  62. ​
  63. if ($action === 'check_updates' || $action === 'download_release') {
  64. if (!Updater::enabled($db)) {
  65. $flashMessage = 'Updates are turned off.';
  66. $flashType = 'error';
  67. } else {
  68. session_write_close();
  69. set_time_limit(180);
  70. $result = $action === 'download_release' ? Updater::compare($currentVersion) : Updater::check($currentVersion);
  71. if (!$result['ok']) {
  72. $flashMessage = $result['error'];
  73. $flashType = 'error';
  74. $result = null;
  75. }
  76. }
  77. }
  78. ​
  79. if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
  80. if (!$canInstall) {
  81. $flashMessage = 'Only the Super Admin can install updates or use backups.';
  82. $flashType = 'error';
  83. } elseif ($action === 'install_release' && !Updater::enabled($db)) {
  84. $flashMessage = 'Updates are turned off.';
  85. $flashType = 'error';
  86. } else {
  87. session_write_close();
  88. set_time_limit(300);
  89. $backupId = (string) ($_POST['backup_id'] ?? '');
  90. ​
  91. if ($action === 'install_release') {
  92. $outcome = Updater::install($currentVersion, ($_POST['update_htaccess'] ?? '1') === '1');
  93. $flashMessage = $outcome['ok']
  94. ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files changed). We saved a backup first: ' . $outcome['backup'] . '.'
  95. : $outcome['error'];
  96. } elseif ($action === 'restore_backup') {
  97. $outcome = Updater::restore($backupId);
  98. $flashMessage = $outcome['ok']
  99. ? 'Backup restored (' . $outcome['restored'] . ' files). Version is now ' . $outcome['version'] . '.'
  100. : $outcome['error'];
  101. } else {
  102. $outcome = ['ok' => Updater::deleteBackup($backupId)];
  103. $flashMessage = $outcome['ok'] ? 'Backup deleted.' : 'We couldn\'t find that backup.';
  104. }
  105. ​
  106. $flashType = $outcome['ok'] ? 'success' : 'error';
  107. $currentVersion = AppVersion::current($db);
  108. }
  109. }
  110. }
  111. }
  112. ​
  113. Updater::pruneBackups();
  114. $backups = Updater::backups();
  115. ​
  116. $updaterEnabled = Updater::enabled($db);
  117. $autoUpdate = Updater::autoSettings($db);
  118. $cronBaseUrl = rtrim((string) Config::get('APP_URL', ''), '/') . Config::get('APP_BASE_PATH', '') . '/cron-update.php';
  119. $cronHttpUrl = $cronBaseUrl . '?token=' . rawurlencode($autoUpdate['token']);
  120. $cronCliCommand = 'php ' . __DIR__ . DIRECTORY_SEPARATOR . 'cron-update.php';
  121. ​
  122. $statusLabels = ['added' => 'Added', 'modified' => 'Edited', 'deleted' => 'Removed'];
  123. $statusClasses = ['added' => 'published', 'modified' => 'scheduled', 'deleted' => 'rejected'];
  124. ​
  125. $dashActivePage = 'settings';
  126. $dashPageTitle = 'Updater';
  127. ​
  128. require __DIR__ . '/includes/dash-header.php';
  129. ​
  130. ?>
  131. ​
  132. <?php if ($flashMessage !== null): ?>
  133. <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
  134. <?php endif; ?>
  135. ​
  136. <h1 class="dash-title"><?= Icons::icon('refresh', 'icon icon-lg') ?>Updater</h1>
  137. ​
  138. <div class="dash-cards">
  139. <div class="dash-card">
  140. <?= Icons::icon('layers') ?>
  141. <div class="dash-card-value"><?= htmlspecialchars($currentVersion) ?></div>
  142. <div class="dash-card-label">Your version</div>
  143. </div>
  144. <div class="dash-card">
  145. <?= Icons::icon('toggle') ?>
  146. <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'On' : 'Off' ?></span></div>
  147. <div class="dash-card-label">Updates</div>
  148. </div>
  149. </div>
  150. ​
  151. <?php if ($lockedByConfig): ?>
  152. <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>
  153. <?php else: ?>
  154. <div class="publish-actions">
  155. <?php if ($updaterEnabled): ?>
  156. <form method="post">
  157. <?= Csrf::field() ?>
  158. <input type="hidden" name="action" value="check_updates">
  159. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('refresh', 'icon icon-sm') ?>Check for updates</button>
  160. </form>
  161. <?php endif; ?>
  162. <form method="post">
  163. <?= Csrf::field() ?>
  164. <input type="hidden" name="action" value="toggle_updater">
  165. <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
  166. <?php if ($updaterEnabled): ?>
  167. <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Turn off updates</button>
  168. <?php else: ?>
  169. <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Turn on updates</button>
  170. <?php endif; ?>
  171. </form>
  172. </div>
  173. <?php if (!$updaterEnabled): ?>
  174. <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>
  175. <?php endif; ?>
  176. ​
  177. <?php if ($updaterEnabled): ?>
  178. <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates</h2>
  179. ​
  180. <div class="updater-auto-warning" role="alert">
  181. <strong><?= Icons::icon('shield', 'icon icon-sm') ?>Heads up: automatic updates can break your store.</strong>
  182. <ul>
  183. <li>New versions get installed <strong>without anyone checking them first</strong>.</li>
  184. <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>
  185. <li>A bad release could take your store, checkout or dashboard down until someone restores a backup.</li>
  186. <li>We back up before every install and keep backups for 30 days, but someone still has to log in to restore one.</li>
  187. </ul>
  188. <p>Leave this off unless you're OK with that. With manual updates you see every change before it goes in.</p>
  189. </div>
  190. ​
  191. <div class="dash-cards">
  192. <div class="dash-card">
  193. <?= Icons::icon('toggle') ?>
  194. <div class="dash-card-value"><span class="status-pill status-<?= $autoUpdate['enabled'] ? 'rejected' : 'archived' ?>"><?= $autoUpdate['enabled'] ? 'On' : 'Off' ?></span></div>
  195. <div class="dash-card-label">Automatic updates</div>
  196. </div>
  197. <div class="dash-card">
  198. <?= Icons::icon('clock') ?>
  199. <div class="dash-card-value" style="font-size:14px;"><?= $autoUpdate['last_run'] !== null ? htmlspecialchars(substr((string) $autoUpdate['last_run']['at'], 0, 19)) : 'Never' ?></div>
  200. <div class="dash-card-label">Last automatic check</div>
  201. </div>
  202. </div>
  203. ​
  204. <?php if ($autoUpdate['last_run'] !== null): ?>
  205. <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>
  206. <?php endif; ?>
  207. ​
  208. <?php if ($canInstall): ?>
  209. <form method="post" class="settings-section" style="max-width:720px;">
  210. <?= Csrf::field() ?>
  211. <input type="hidden" name="action" value="save_auto_update">
  212. <label><input type="checkbox" name="auto_enabled" value="1" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new versions automatically</label>
  213. <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>
  214. <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>
  215. <button type="submit" class="dash-btn dash-btn-primary" style="margin-top:12px;"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
  216. </form>
  217. ​
  218. <?php if ($autoUpdate['enabled']): ?>
  219. <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Cron job</h2>
  220. <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>
  221. <label>Command (best option)</label>
  222. <input type="text" readonly value="<?= htmlspecialchars($cronCliCommand) ?>" onclick="this.select();">
  223. <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>
  224. <label>Link (if your host can't run commands)</label>
  225. <input type="text" readonly value="<?= htmlspecialchars('wget -q -O - "' . $cronHttpUrl . '"') ?>" onclick="this.select();">
  226. <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Anyone with this link can start an update, so keep it to yourself.</p>
  227. <form method="post" onsubmit="return confirm('Create a new link? The current one will stop working.');">
  228. <?= Csrf::field() ?>
  229. <input type="hidden" name="action" value="regenerate_cron_token">
  230. <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Create a new link</button>
  231. </form>
  232. <?php endif; ?>
  233. <?php else: ?>
  234. <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can change this.</p>
  235. <?php endif; ?>
  236. <?php endif; ?>
  237. <?php endif; ?>
  238. ​
  239. <?php if ($result !== null): ?>
  240. <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>Result</h2>
  241. ​
  242. <?php if ($result['status'] === 'update'): ?>
  243. <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>
  244. <?php elseif ($result['status'] === 'current'): ?>
  245. <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>You're up to date.</div>
  246. <?php else: ?>
  247. <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>
  248. <?php endif; ?>
  249. ​
  250. <?php if (!empty($result['changelog'])): ?>
  251. <h2 class="dash-subtitle"><?= Icons::icon('list', 'icon icon-sm') ?>Changes</h2>
  252. <ul class="updater-changelog">
  253. <?php foreach ($result['changelog'] as $entry): ?>
  254. <li><?= htmlspecialchars($entry) ?></li>
  255. <?php endforeach; ?>
  256. </ul>
  257. <?php endif; ?>
  258. ​
  259. <?php if (!$result['compared']): ?>
  260. <form method="post" class="publish-actions">
  261. <?= Csrf::field() ?>
  262. <input type="hidden" name="action" value="download_release">
  263. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>See what's changed</button>
  264. </form>
  265. <p class="updater-note"><?= Icons::icon('info', 'icon icon-sm') ?>We just compare the new version with your files. Nothing gets installed yet.</p>
  266. <?php elseif (empty($result['files'])): ?>
  267. <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files already match this version.</p>
  268. <?php endif; ?>
  269. ​
  270. <?php
  271. $htaccessCount = 0;
  272. foreach ($result['files'] as $candidate) {
  273. if (Updater::isHtaccess($candidate['path'])) {
  274. $htaccessCount++;
  275. }
  276. }
  277. ?>
  278. ​
  279. <?php if ($result['compared'] && $result['status'] !== 'ahead' && !empty($result['files'])): ?>
  280. <?php if ($canInstall): ?>
  281. <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.');">
  282. <?= Csrf::field() ?>
  283. <input type="hidden" name="action" value="install_release">
  284. <?php if ($htaccessCount > 0): ?>
  285. <input type="hidden" name="update_htaccess" value="0">
  286. <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>
  287. <?php endif; ?>
  288. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?><?= $result['status'] === 'update' ? 'Install update' : 'Match my files' ?></button>
  289. </form>
  290. <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>
  291. <?php else: ?>
  292. <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can install updates.</p>
  293. <?php endif; ?>
  294. <?php endif; ?>
  295. ​
  296. <?php if (!empty($result['files'])): ?>
  297. <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Changed files (<?= count($result['files']) ?>)</h2>
  298. ​
  299. <?php foreach ($result['files'] as $file): ?>
  300. <details class="updater-file">
  301. <summary>
  302. <span class="status-pill status-<?= $statusClasses[$file['status']] ?>"><?= $statusLabels[$file['status']] ?></span>
  303. <span class="updater-file-path"><?= htmlspecialchars($file['path']) ?><?= Updater::isHtaccess($file['path']) ? ' (optional)' : '' ?></span>
  304. <span class="updater-file-stats">
  305. <?php if ($file['added'] > 0): ?><span class="updater-stat-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
  306. <?php if ($file['removed'] > 0): ?><span class="updater-stat-del">&minus;<?= (int) $file['removed'] ?></span><?php endif; ?>
  307. <?php if ($file['moved'] > 0): ?><span class="updater-stat-move">&asymp;<?= (int) intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
  308. </span>
  309. </summary>
  310. ​
  311. <?php if ($file['note'] !== ''): ?>
  312. <p class="updater-note"><?= htmlspecialchars($file['note']) ?></p>
  313. <?php else: ?>
  314. <div class="updater-diff">
  315. <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
  316. <?php if ($hunkIndex > 0): ?>
  317. <div class="updater-gap">&hellip;</div>
  318. <?php endif; ?>
  319. <?php foreach ($hunk as $line): ?>
  320. <?php
  321. $lineClass = match ($line['type']) {
  322. 'add' => 'add',
  323. 'del' => 'del',
  324. 'moved_in', 'moved_out' => 'move',
  325. default => 'eq',
  326. };
  327. $marker = match ($line['type']) {
  328. 'add' => '+',
  329. 'del' => '−',
  330. 'moved_in' => '↓',
  331. 'moved_out' => '↑',
  332. default => ' ',
  333. };
  334. ?>
  335. <div class="updater-line updater-line-<?= $lineClass ?>">
  336. <span class="updater-ln"><?= $line['old'] ?? '' ?></span>
  337. <span class="updater-ln"><?= $line['new'] ?? '' ?></span>
  338. <span class="updater-marker"><?= $marker ?></span>
  339. <span class="updater-code"><?= htmlspecialchars($line['text']) ?></span>
  340. </div>
  341. <?php endforeach; ?>
  342. <?php endforeach; ?>
  343. </div>
  344. <?php endif; ?>
  345. </details>
  346. <?php endforeach; ?>
  347. ​
  348. <?php if ($result['skipped'] > 0): ?>
  349. <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>
  350. <?php endif; ?>
  351. <?php endif; ?>
  352. <?php endif; ?>
  353. ​
  354. <?php if (!empty($backups)): ?>
  355. <h2 class="dash-subtitle"><?= Icons::icon('save', 'icon icon-sm') ?>Backups</h2>
  356. <table class="dash-table">
  357. <thead>
  358. <tr>
  359. <th><?= Icons::icon('clock', 'icon icon-sm') ?>Created</th>
  360. <th><?= Icons::icon('layers', 'icon icon-sm') ?>Version</th>
  361. <th><?= Icons::icon('code', 'icon icon-sm') ?>Files</th>
  362. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  363. <th></th>
  364. </tr>
  365. </thead>
  366. <tbody>
  367. <?php foreach ($backups as $backup): ?>
  368. <tr>
  369. <td data-label="Created"><?= htmlspecialchars(substr($backup['created_at'], 0, 19)) ?></td>
  370. <td data-label="Version"><?= htmlspecialchars($backup['from_version']) ?> &rarr; <?= htmlspecialchars($backup['to_version']) ?></td>
  371. <td data-label="Files"><?= (int) $backup['files'] ?></td>
  372. <td data-label="Status">
  373. <?php if ($backup['restored']): ?>
  374. <span class="status-pill status-scheduled">Restored</span>
  375. <?php elseif ($backup['completed']): ?>
  376. <span class="status-pill status-published">Installed</span>
  377. <?php else: ?>
  378. <span class="status-pill status-rejected">Didn't finish</span>
  379. <?php endif; ?>
  380. </td>
  381. <td class="dash-table-actions">
  382. <?php if ($canInstall): ?>
  383. <form method="post" onsubmit="return confirm('Restore this backup? Files from the update will be swapped back to the saved ones.');">
  384. <?= Csrf::field() ?>
  385. <input type="hidden" name="action" value="restore_backup">
  386. <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
  387. <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
  388. </form>
  389. <form method="post" onsubmit="return confirm('Delete this backup? You can\'t get it back.');">
  390. <?= Csrf::field() ?>
  391. <input type="hidden" name="action" value="delete_backup">
  392. <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
  393. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  394. </form>
  395. <?php endif; ?>
  396. </td>
  397. </tr>
  398. <?php endforeach; ?>
  399. </tbody>
  400. </table>
  401. <?php endif; ?>
  402. ​
  403. <?php require __DIR__ . '/includes/dash-footer.php'; ?>
  404. ​