WebOrbiton
v1.0.0.8

Publisium

372 lines · 20.1 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/activity-log.php';
  10. require_once __DIR__ . '/includes/updater.php';
  11. ​
  12. Auth::boot();
  13. Auth::requireRoleAtLeast(Auth::ROLE_EDITOR_IN_CHIEF);
  14. ​
  15. $db = Database::site();
  16. $currentVersion = AppVersion::current($db);
  17. $lockedByConfig = Updater::lockedByConfig();
  18. $canInstall = Auth::role() === Auth::ROLE_SUPER_ADMIN;
  19. ​
  20. $flashMessage = null;
  21. $flashType = 'success';
  22. $result = null;
  23. ​
  24. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  25. if (!Csrf::verify($_POST['csrf_token'] ?? null)) {
  26. $flashMessage = 'Your session expired. Please try again.';
  27. $flashType = 'error';
  28. } else {
  29. $action = $_POST['action'] ?? '';
  30. ​
  31. if ($action === 'toggle_updater') {
  32. if ($lockedByConfig) {
  33. $flashMessage = 'Updates are switched off in the server settings.';
  34. $flashType = 'error';
  35. } else {
  36. $turnOn = ($_POST['enabled'] ?? '0') === '1';
  37. Updater::setEnabled($db, $turnOn);
  38. $flashMessage = $turnOn ? 'Updates switched on.' : 'Updates switched off.';
  39. }
  40. }
  41. ​
  42. if ($action === 'save_auto_update') {
  43. $enableAuto = isset($_POST['auto_enabled']);
  44. if (!$canInstall) {
  45. $flashMessage = 'Only the Super Admin can change automatic updates.';
  46. $flashType = 'error';
  47. } elseif ($lockedByConfig) {
  48. $flashMessage = 'Updates are switched off in the server settings.';
  49. $flashType = 'error';
  50. } elseif ($enableAuto && !isset($_POST['auto_accept_risk'])) {
  51. $flashMessage = 'Tick the box to confirm you understand the risks first.';
  52. $flashType = 'error';
  53. } else {
  54. Updater::setAutoSettings($db, $enableAuto, isset($_POST['auto_htaccess']));
  55. ActivityLog::record($enableAuto ? 'updater.auto_enable' : 'updater.auto_disable', 'updater');
  56. $flashMessage = $enableAuto ? 'Automatic updates turned on.' : 'Automatic updates turned off.';
  57. }
  58. }
  59. ​
  60. if ($action === 'check_updates' || $action === 'download_release') {
  61. if (!Updater::enabled($db)) {
  62. $flashMessage = 'Updates are switched off.';
  63. $flashType = 'error';
  64. } else {
  65. session_write_close();
  66. set_time_limit(180);
  67. $result = $action === 'download_release' ? Updater::compare($currentVersion) : Updater::check($currentVersion);
  68. if (!$result['ok']) {
  69. $flashMessage = $result['error'];
  70. $flashType = 'error';
  71. $result = null;
  72. }
  73. }
  74. }
  75. ​
  76. if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
  77. if (!$canInstall) {
  78. $flashMessage = 'Only the Super Admin can install updates and manage backups.';
  79. $flashType = 'error';
  80. } elseif ($action === 'install_release' && !Updater::enabled($db)) {
  81. $flashMessage = 'Updates are switched off.';
  82. $flashType = 'error';
  83. } else {
  84. session_write_close();
  85. set_time_limit(300);
  86. $backupId = (string) ($_POST['backup_id'] ?? '');
  87. ​
  88. if ($action === 'install_release') {
  89. $outcome = Updater::install($currentVersion, ($_POST['update_htaccess'] ?? '1') === '1');
  90. $flashMessage = $outcome['ok']
  91. ? 'All done, you’re now on ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files updated). We saved a backup called ' . $outcome['backup'] . '.'
  92. : $outcome['error'];
  93. } elseif ($action === 'restore_backup') {
  94. $outcome = Updater::restore($backupId);
  95. $flashMessage = $outcome['ok']
  96. ? 'Backup restored (' . $outcome['restored'] . ' files). You’re back on ' . $outcome['version'] . '.'
  97. : $outcome['error'];
  98. } else {
  99. $outcome = ['ok' => Updater::deleteBackup($backupId)];
  100. $flashMessage = $outcome['ok'] ? 'Backup deleted.' : 'We couldn’t find that backup.';
  101. }
  102. ​
  103. $flashType = $outcome['ok'] ? 'success' : 'error';
  104. $currentVersion = AppVersion::current($db);
  105. }
  106. }
  107. }
  108. }
  109. ​
  110. Updater::pruneBackups();
  111. $backups = Updater::backups();
  112. ​
  113. $updaterEnabled = Updater::enabled($db);
  114. $autoUpdate = Updater::autoSettings($db);
  115. $cronCommand = 'php ' . str_replace('\\', '/', __DIR__) . '/cron-update.php';
  116. ​
  117. $statusLabels = ['added' => 'Added', 'modified' => 'Edited', 'deleted' => 'Removed'];
  118. $statusClasses = ['added' => 'published', 'modified' => 'scheduled', 'deleted' => 'rejected'];
  119. ​
  120. $dashActivePage = 'settings';
  121. $dashPageTitle = 'Updates';
  122. ​
  123. require __DIR__ . '/includes/dash-header.php';
  124. ​
  125. ?>
  126. ​
  127. <?php if ($flashMessage !== null): ?>
  128. <div class="dash-flash dash-flash-<?= htmlspecialchars($flashType) ?>"><?= Icons::icon($flashType === 'error' ? 'x' : 'check', 'icon icon-sm') ?><?= htmlspecialchars($flashMessage) ?></div>
  129. <?php endif; ?>
  130. ​
  131. <h1 class="dash-title"><?= Icons::icon('refresh', 'icon icon-lg') ?>Updates</h1>
  132. ​
  133. <div class="dash-cards">
  134. <div class="dash-card">
  135. <?= Icons::icon('layers') ?>
  136. <div class="dash-card-value"><?= htmlspecialchars($currentVersion) ?></div>
  137. <div class="dash-card-label">Installed version</div>
  138. </div>
  139. <div class="dash-card">
  140. <?= Icons::icon('toggle') ?>
  141. <div class="dash-card-value"><span class="status-pill status-<?= $updaterEnabled ? 'published' : 'archived' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></div>
  142. <div class="dash-card-label">Updates</div>
  143. </div>
  144. </div>
  145. ​
  146. <?php if ($lockedByConfig): ?>
  147. <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>
  148. <?php else: ?>
  149. <div class="publish-actions">
  150. <?php if ($updaterEnabled): ?>
  151. <form method="post">
  152. <?= Csrf::field() ?>
  153. <input type="hidden" name="action" value="check_updates">
  154. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('refresh', 'icon icon-sm') ?>Check for updates</button>
  155. </form>
  156. <?php endif; ?>
  157. <form method="post">
  158. <?= Csrf::field() ?>
  159. <input type="hidden" name="action" value="toggle_updater">
  160. <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
  161. <?php if ($updaterEnabled): ?>
  162. <button type="submit" class="dash-btn dash-btn-danger"><?= Icons::icon('x', 'icon icon-sm') ?>Switch off updates</button>
  163. <?php else: ?>
  164. <button type="submit" class="dash-btn"><?= Icons::icon('check', 'icon icon-sm') ?>Switch on updates</button>
  165. <?php endif; ?>
  166. </form>
  167. </div>
  168. <?php if (!$updaterEnabled): ?>
  169. <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>
  170. <?php else: ?>
  171. <h2 class="dash-subtitle"><?= Icons::icon('clock', 'icon icon-sm') ?>Automatic updates</h2>
  172. ​
  173. <div class="updater-warning">
  174. <?= Icons::icon('shield', 'icon icon-sm') ?>
  175. <div>
  176. <strong>Automatic updates can break your site.</strong>
  177. New versions get installed on a schedule, without anyone checking them first. An update can replace files you changed yourself,
  178. change or remove things you rely on, or stop the site from working, and nobody is asked first.
  179. We save a backup before each install that you can restore below, but do check the site after every update.
  180. </div>
  181. </div>
  182. ​
  183. <?php if ($canInstall): ?>
  184. <form method="post" class="updater-auto-form">
  185. <?= Csrf::field() ?>
  186. <input type="hidden" name="action" value="save_auto_update">
  187. <label><input type="checkbox" name="auto_enabled" <?= $autoUpdate['enabled'] ? 'checked' : '' ?>> Install new versions automatically on a schedule</label>
  188. <label><input type="checkbox" name="auto_htaccess" <?= $autoUpdate['htaccess'] ? 'checked' : '' ?>> Also replace .htaccess files (leave unticked to keep your own server rules)</label>
  189. <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>
  190. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('check', 'icon icon-sm') ?>Save</button>
  191. </form>
  192. <?php else: ?>
  193. <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>
  194. <?php endif; ?>
  195. ​
  196. <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>
  197. <pre class="updater-cron"><code><?= htmlspecialchars($cronCommand) ?></code></pre>
  198. ​
  199. <?php if ($autoUpdate['last_run'] !== ''): ?>
  200. <p class="updater-note"><?= Icons::icon('clock', 'icon icon-sm') ?>Last automatic check: <?= htmlspecialchars($autoUpdate['last_run']) ?>, <?= htmlspecialchars($autoUpdate['last_result']) ?></p>
  201. <?php elseif ($autoUpdate['enabled']): ?>
  202. <p class="updater-note"><?= Icons::icon('clock', 'icon icon-sm') ?>The scheduled check hasn’t run yet.</p>
  203. <?php endif; ?>
  204. <?php endif; ?>
  205. <?php endif; ?>
  206. ​
  207. <?php if ($result !== null): ?>
  208. <h2 class="dash-subtitle"><?= Icons::icon('download', 'icon icon-sm') ?>What we found</h2>
  209. ​
  210. <?php if ($result['status'] === 'update'): ?>
  211. <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>
  212. <?php elseif ($result['status'] === 'current'): ?>
  213. <div class="dash-flash dash-flash-success"><?= Icons::icon('check', 'icon icon-sm') ?>You’re on the latest version.</div>
  214. <?php else: ?>
  215. <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>
  216. <?php endif; ?>
  217. ​
  218. <?php if (!empty($result['changelog'])): ?>
  219. <h2 class="dash-subtitle"><?= Icons::icon('list', 'icon icon-sm') ?>What’s changed</h2>
  220. <ul class="updater-changelog">
  221. <?php foreach ($result['changelog'] as $entry): ?>
  222. <li><?= htmlspecialchars($entry) ?></li>
  223. <?php endforeach; ?>
  224. </ul>
  225. <?php endif; ?>
  226. ​
  227. <?php if (!$result['compared']): ?>
  228. <form method="post" class="publish-actions">
  229. <?= Csrf::field() ?>
  230. <input type="hidden" name="action" value="download_release">
  231. <button type="submit" class="dash-btn dash-btn-primary"><?= Icons::icon('download', 'icon icon-sm') ?>Show me what’s changed</button>
  232. </form>
  233. <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>
  234. <?php elseif (empty($result['files'])): ?>
  235. <p class="updater-note"><?= Icons::icon('check', 'icon icon-sm') ?>Your files already match the new version.</p>
  236. <?php endif; ?>
  237. ​
  238. <?php
  239. $htaccessCount = 0;
  240. foreach ($result['files'] as $candidate) {
  241. if (Updater::isHtaccess($candidate['path'])) {
  242. $htaccessCount++;
  243. }
  244. }
  245. ?>
  246. ​
  247. <?php if ($result['compared'] && $result['status'] !== 'ahead' && !empty($result['files'])): ?>
  248. <?php if ($canInstall): ?>
  249. <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.');">
  250. <?= Csrf::field() ?>
  251. <input type="hidden" name="action" value="install_release">
  252. <?php if ($htaccessCount > 0): ?>
  253. <input type="hidden" name="update_htaccess" value="0">
  254. <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>
  255. <?php endif; ?>
  256. <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>
  257. </form>
  258. <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>
  259. <?php else: ?>
  260. <p class="updater-note"><?= Icons::icon('lock', 'icon icon-sm') ?>Only the Super Admin can install updates.</p>
  261. <?php endif; ?>
  262. <?php endif; ?>
  263. ​
  264. <?php if (!empty($result['files'])): ?>
  265. <h2 class="dash-subtitle"><?= Icons::icon('code', 'icon icon-sm') ?>Files (<?= count($result['files']) ?>)</h2>
  266. ​
  267. <?php foreach ($result['files'] as $file): ?>
  268. <details class="updater-file">
  269. <summary>
  270. <span class="status-pill status-<?= $statusClasses[$file['status']] ?>"><?= $statusLabels[$file['status']] ?></span>
  271. <span class="updater-file-path"><?= htmlspecialchars($file['path']) ?><?= Updater::isHtaccess($file['path']) ? ' (optional)' : '' ?></span>
  272. <span class="updater-file-stats">
  273. <?php if ($file['added'] > 0): ?><span class="updater-stat-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
  274. <?php if ($file['removed'] > 0): ?><span class="updater-stat-del">&minus;<?= (int) $file['removed'] ?></span><?php endif; ?>
  275. <?php if ($file['moved'] > 0): ?><span class="updater-stat-move">&asymp;<?= (int) intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
  276. </span>
  277. </summary>
  278. ​
  279. <?php if ($file['note'] !== ''): ?>
  280. <p class="updater-note"><?= htmlspecialchars($file['note']) ?></p>
  281. <?php else: ?>
  282. <div class="updater-diff">
  283. <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
  284. <?php if ($hunkIndex > 0): ?>
  285. <div class="updater-gap">&hellip;</div>
  286. <?php endif; ?>
  287. <?php foreach ($hunk as $line): ?>
  288. <?php
  289. $lineClass = match ($line['type']) {
  290. 'add' => 'add',
  291. 'del' => 'del',
  292. 'moved_in', 'moved_out' => 'move',
  293. default => 'eq',
  294. };
  295. $marker = match ($line['type']) {
  296. 'add' => '+',
  297. 'del' => '−',
  298. 'moved_in' => '↓',
  299. 'moved_out' => '↑',
  300. default => ' ',
  301. };
  302. ?>
  303. <div class="updater-line updater-line-<?= $lineClass ?>">
  304. <span class="updater-ln"><?= $line['old'] ?? '' ?></span>
  305. <span class="updater-ln"><?= $line['new'] ?? '' ?></span>
  306. <span class="updater-marker"><?= $marker ?></span>
  307. <span class="updater-code"><?= htmlspecialchars($line['text']) ?></span>
  308. </div>
  309. <?php endforeach; ?>
  310. <?php endforeach; ?>
  311. </div>
  312. <?php endif; ?>
  313. </details>
  314. <?php endforeach; ?>
  315. ​
  316. <?php if ($result['skipped'] > 0): ?>
  317. <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>
  318. <?php endif; ?>
  319. <?php endif; ?>
  320. <?php endif; ?>
  321. ​
  322. <?php if (!empty($backups)): ?>
  323. <h2 class="dash-subtitle"><?= Icons::icon('save', 'icon icon-sm') ?>Backups</h2>
  324. <table class="dash-table">
  325. <thead>
  326. <tr>
  327. <th><?= Icons::icon('clock', 'icon icon-sm') ?>Created</th>
  328. <th><?= Icons::icon('layers', 'icon icon-sm') ?>Version</th>
  329. <th><?= Icons::icon('code', 'icon icon-sm') ?>Files</th>
  330. <th><?= Icons::icon('flag', 'icon icon-sm') ?>Status</th>
  331. <th></th>
  332. </tr>
  333. </thead>
  334. <tbody>
  335. <?php foreach ($backups as $backup): ?>
  336. <tr>
  337. <td data-label="Created"><?= htmlspecialchars(substr($backup['created_at'], 0, 19)) ?></td>
  338. <td data-label="Version"><?= htmlspecialchars($backup['from_version']) ?> &rarr; <?= htmlspecialchars($backup['to_version']) ?></td>
  339. <td data-label="Files"><?= (int) $backup['files'] ?></td>
  340. <td data-label="Status">
  341. <?php if ($backup['restored']): ?>
  342. <span class="status-pill status-scheduled">Restored</span>
  343. <?php elseif ($backup['completed']): ?>
  344. <span class="status-pill status-published">Installed</span>
  345. <?php else: ?>
  346. <span class="status-pill status-rejected">Not completed</span>
  347. <?php endif; ?>
  348. </td>
  349. <td class="dash-table-actions">
  350. <?php if ($canInstall): ?>
  351. <form method="post" onsubmit="return confirm('Restore this backup? Files from the update will be swapped back to the saved versions.');">
  352. <?= Csrf::field() ?>
  353. <input type="hidden" name="action" value="restore_backup">
  354. <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
  355. <button type="submit" class="dash-btn-small"><?= Icons::icon('refresh', 'icon icon-sm') ?>Restore</button>
  356. </form>
  357. <form method="post" onsubmit="return confirm('Delete this backup for good?');">
  358. <?= Csrf::field() ?>
  359. <input type="hidden" name="action" value="delete_backup">
  360. <input type="hidden" name="backup_id" value="<?= htmlspecialchars($backup['id']) ?>">
  361. <button type="submit" class="dash-btn-small dash-btn-danger"><?= Icons::icon('trash', 'icon icon-sm') ?>Delete</button>
  362. </form>
  363. <?php endif; ?>
  364. </td>
  365. </tr>
  366. <?php endforeach; ?>
  367. </tbody>
  368. </table>
  369. <?php endif; ?>
  370. ​
  371. <?php require __DIR__ . '/includes/dash-footer.php'; ?>
  372. ​