WebOrbiton
v3.0.0.1

FlatlyPage

460 lines · 28.0 KB
  1. <?php
  2. require_once __DIR__ . '/../config.php';
  3. require_once __DIR__ . '/sidebar.php';
  4. require_once __DIR__ . '/updater-lib.php';
  5. ​
  6. require_login();
  7. ​
  8. $currentVersion = Updater::currentVersion();
  9. $updaterEnabled = Updater::enabled();
  10. ​
  11. $message = '';
  12. $message_type = 'success';
  13. $result = null;
  14. ​
  15. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  16. if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
  17. $message = 'Invalid request. Please try again.';
  18. $message_type = 'error';
  19. } else {
  20. $action = (string) ($_POST['action'] ?? '');
  21. ​
  22. if ($action === 'toggle_updater') {
  23. $updaterEnabled = ($_POST['enabled'] ?? '0') === '1';
  24. if (Updater::setEnabled($updaterEnabled)) {
  25. $message = $updaterEnabled ? 'Updater enabled.' : 'Updater disabled.';
  26. } else {
  27. $updaterEnabled = !$updaterEnabled;
  28. $message = 'Could not save the updater setting.';
  29. $message_type = 'error';
  30. }
  31. }
  32. ​
  33. if ($action === 'toggle_cleanup') {
  34. $turnOn = ($_POST['enabled'] ?? '0') === '1';
  35. if (Updater::setAutoCleanup($turnOn)) {
  36. $message = $turnOn
  37. ? 'Automatic cleanup enabled.'
  38. : 'Automatic cleanup disabled.';
  39. if ($turnOn) {
  40. $removed = Updater::cleanupBackups();
  41. $message .= $removed > 0 ? ' Removed ' . $removed . ' old backup(s).' : '';
  42. }
  43. } else {
  44. $message = 'Could not save the setting.';
  45. $message_type = 'error';
  46. }
  47. }
  48. ​
  49. if ($action === 'toggle_auto_update') {
  50. $turnOn = ($_POST['enabled'] ?? '0') === '1';
  51. if ($turnOn && !$updaterEnabled) {
  52. $message = 'Enable the updater first.';
  53. $message_type = 'error';
  54. } elseif ($turnOn && ($_POST['accept_risk'] ?? '') !== '1') {
  55. $message = 'Confirm that you understand the risks to turn on automatic updates.';
  56. $message_type = 'error';
  57. } elseif (Updater::setAutoUpdate($turnOn)) {
  58. $message = $turnOn
  59. ? 'Automatic updates enabled. Add the cron job below, otherwise nothing will run.'
  60. : 'Automatic updates disabled.';
  61. } else {
  62. $message = 'Could not save the setting.';
  63. $message_type = 'error';
  64. }
  65. }
  66. ​
  67. if ($action === 'regenerate_cron_token') {
  68. Updater::regenerateCronToken();
  69. $message = 'A new cron URL was generated. Update the URL in your cron job; the old one no longer works.';
  70. }
  71. ​
  72. if ($action === 'check_updates' || $action === 'download_release') {
  73. if (!$updaterEnabled) {
  74. $message = 'The updater is disabled.';
  75. $message_type = 'error';
  76. } else {
  77. session_write_close();
  78. set_time_limit(180);
  79. $result = $action === 'download_release' ? Updater::compare($currentVersion) : Updater::check($currentVersion);
  80. if (!$result['ok']) {
  81. $message = $result['error'];
  82. $message_type = 'error';
  83. $result = null;
  84. }
  85. }
  86. }
  87. ​
  88. if (in_array($action, ['install_release', 'restore_backup', 'delete_backup'], true)) {
  89. if ($action === 'install_release' && !$updaterEnabled) {
  90. $message = 'The updater is disabled.';
  91. $message_type = 'error';
  92. } else {
  93. session_write_close();
  94. set_time_limit(300);
  95. $backupId = (string) ($_POST['backup_id'] ?? '');
  96. ​
  97. if ($action === 'install_release') {
  98. $outcome = Updater::install($currentVersion, ($_POST['overwrite_htaccess'] ?? '') === '1');
  99. $message = $outcome['ok']
  100. ? 'Updated to ' . $outcome['version'] . ' (' . $outcome['installed'] . ' files). A backup was saved as ' . $outcome['backup'] . '.'
  101. : $outcome['error'];
  102. } elseif ($action === 'restore_backup') {
  103. $outcome = Updater::restore($backupId);
  104. $message = $outcome['ok']
  105. ? 'Backup restored (' . $outcome['restored'] . ' files). Version is now ' . $outcome['version'] . '.'
  106. : $outcome['error'];
  107. } else {
  108. $outcome = ['ok' => Updater::deleteBackup($backupId)];
  109. $message = $outcome['ok'] ? 'Backup deleted.' : 'Backup not found.';
  110. }
  111. ​
  112. $message_type = $outcome['ok'] ? 'success' : 'error';
  113. $currentVersion = Updater::currentVersion();
  114. }
  115. }
  116. }
  117. }
  118. ​
  119. $autoUpdate = Updater::autoUpdate();
  120. $lastAutoRun = Updater::lastAutomaticRun();
  121. $autoRunning = $autoUpdate && $updaterEnabled;
  122. $cronUrl = $autoRunning ? SITE_URL . '/cron-update?token=' . Updater::cronToken() : '';
  123. $autoCleanup = Updater::autoCleanup();
  124. if ($autoCleanup) {
  125. Updater::cleanupBackups();
  126. }
  127. $backups = Updater::backups();
  128. $csrf_token = generate_csrf_token();
  129. ​
  130. $statusLabels = ['added' => 'Added', 'modified' => 'Edited'];
  131. ?>
  132. <!DOCTYPE html>
  133. <html lang="en">
  134. ​
  135. <head>
  136. <meta charset="UTF-8">
  137. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  138. <title>Updater - FlatlyPage CMS</title>
  139. <link rel="icon" href="admin.ico?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>" type="image/x-icon">
  140. <?= admin_font_head() ?>
  141. <link rel="stylesheet" href="/css/admin.css?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>">
  142. <script src="/assets/js/admin-theme.js?v=5"></script>
  143. </head>
  144. ​
  145. <body>
  146. <div class="app">
  147. <?php admin_sidebar('updater'); ?>
  148. ​
  149. <main class="main">
  150. <header class="main-header">
  151. <button class="mobile-nav-toggle" onclick="document.querySelector('.sidebar').classList.add('open')">
  152. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
  153. <line x1="3" y1="6" x2="21" y2="6" />
  154. <line x1="3" y1="12" x2="21" y2="12" />
  155. <line x1="3" y1="18" x2="21" y2="18" />
  156. </svg>
  157. </button>
  158. ​
  159. <div class="main-header-inner">
  160. <h1>Updater</h1>
  161. <div class="header-actions">
  162. <a class="btn btn-secondary btn-sm" href="javascript:void(0)" onclick="toggleTheme()" id="theme-toggle-btn">
  163. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16">
  164. <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
  165. </svg>
  166. <span>Theme</span>
  167. </a>
  168. </div>
  169. </div>
  170. </header>
  171. ​
  172. <div class="main-content">
  173. <?php if ($message): ?>
  174. <div class="message <?= e($message_type) ?>">
  175. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  176. <?php if ($message_type === 'success'): ?>
  177. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
  178. <polyline points="22 4 12 14.01 9 11.01" />
  179. <?php else: ?>
  180. <circle cx="12" cy="12" r="10" />
  181. <line x1="12" y1="8" x2="12" y2="12" />
  182. <line x1="12" y1="16" x2="12.01" y2="16" />
  183. <?php endif; ?>
  184. </svg>
  185. <?= e($message) ?>
  186. </div>
  187. <?php endif; ?>
  188. ​
  189. <div class="card" style="margin-bottom: 24px;">
  190. <div class="card-header">
  191. <h3 class="card-title">Status</h3>
  192. </div>
  193. <div class="card-body">
  194. <p>Installed version: <strong><?= e($currentVersion) ?></strong>
  195. <span class="upd-pill <?= $updaterEnabled ? 'upd-pill-added' : 'upd-pill-muted' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></p>
  196. ​
  197. <div class="upd-actions">
  198. <?php if ($updaterEnabled): ?>
  199. <form method="POST">
  200. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  201. <input type="hidden" name="action" value="check_updates">
  202. <button type="submit" class="btn btn-primary">Check for updates</button>
  203. </form>
  204. <?php endif; ?>
  205. <form method="POST">
  206. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  207. <input type="hidden" name="action" value="toggle_updater">
  208. <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
  209. <button type="submit" class="btn <?= $updaterEnabled ? 'btn-danger' : 'btn-secondary' ?>"><?= $updaterEnabled ? 'Disable updater' : 'Enable updater' ?></button>
  210. </form>
  211. </div>
  212. <?php if (!$updaterEnabled): ?>
  213. <p class="form-hint">The updater is off by default. While it is off, this site sends no requests to the update server.</p>
  214. <?php endif; ?>
  215. ​
  216. <form method="POST" class="upd-actions">
  217. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  218. <input type="hidden" name="action" value="toggle_cleanup">
  219. <input type="hidden" name="enabled" value="<?= $autoCleanup ? '0' : '1' ?>">
  220. <button type="submit" class="btn btn-secondary"><?= $autoCleanup ? 'Turn off backup cleanup' : 'Turn on backup cleanup' ?></button>
  221. </form>
  222. <p class="form-hint">Backup cleanup: <?= $autoCleanup ? 'on. Backups older than 30 days are deleted automatically.' : 'off. Turn it on to delete backups older than 30 days automatically.' ?></p>
  223. </div>
  224. </div>
  225. ​
  226. <div class="card" style="margin-bottom: 24px;">
  227. <div class="card-header">
  228. <h3 class="card-title">Automatic updates</h3>
  229. <span class="upd-pill <?= $autoRunning ? 'upd-pill-deleted' : 'upd-pill-muted' ?>"><?= $autoRunning ? 'On' : 'Off' ?></span>
  230. </div>
  231. <div class="card-body">
  232. <div class="upd-warning" role="note">
  233. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
  234. <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
  235. <line x1="12" y1="9" x2="12" y2="13" />
  236. <line x1="12" y1="17" x2="12.01" y2="17" />
  237. </svg>
  238. <div>
  239. <strong>Warning: automatic updates can be dangerous for your site.</strong>
  240. <p>When this is on, a cron job installs every new FlatlyPage release by itself, <strong>without asking anyone and without notifying anyone</strong>. Nobody reviews the changes before they go live.</p>
  241. <ul>
  242. <li>An update can break the site or parts of it.</li>
  243. <li>It can overwrite files you changed yourself (edited templates, custom fixes, your own code in CMS files) and replace or change existing functions and behaviour, silently.</li>
  244. <li>You will only find out when you open this page or notice a problem on the site.</li>
  245. </ul>
  246. <p>Safety nets: the replaced files are backed up before every update, .htaccess is never overwritten, and if the homepage or admin page returns a server error right after the update, the backup is restored automatically. These checks do not catch every problem.</p>
  247. <p>Only turn this on if you do not modify the CMS files and you check your site regularly.</p>
  248. </div>
  249. </div>
  250. ​
  251. <?php if ($autoRunning): ?>
  252. <form method="POST" class="upd-actions">
  253. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  254. <input type="hidden" name="action" value="toggle_auto_update">
  255. <input type="hidden" name="enabled" value="0">
  256. <button type="submit" class="btn btn-danger">Turn off automatic updates</button>
  257. </form>
  258. ​
  259. <h4 class="upd-heading">Cron job</h4>
  260. <p class="form-hint">Add one of these to your hosting's cron jobs, for example once a day. Automatic updates do nothing until a cron job calls them.</p>
  261. <label class="form-label" for="updCronCli">Command (recommended)</label>
  262. <input type="text" id="updCronCli" class="form-input upd-copy" readonly value="<?= e('php ' . BASE_DIR . '/cron-update.php') ?>" onclick="this.select()">
  263. <label class="form-label" for="updCronUrl" style="margin-top: 12px;">Or call this URL (keep it secret)</label>
  264. <input type="text" id="updCronUrl" class="form-input upd-copy" readonly value="<?= e($cronUrl) ?>" onclick="this.select()">
  265. <p class="form-hint">Example: <code>0 4 * * * wget -qO- "<?= e($cronUrl) ?>"</code></p>
  266. <form method="POST" class="upd-actions" onsubmit="return confirm('Generate a new cron URL? The current URL will stop working.');">
  267. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  268. <input type="hidden" name="action" value="regenerate_cron_token">
  269. <button type="submit" class="btn btn-secondary btn-sm">Generate new URL</button>
  270. </form>
  271. <?php elseif (!$updaterEnabled): ?>
  272. <p class="form-hint">Enable the updater above to use automatic updates.<?= $autoUpdate ? ' Automatic updates are switched on but will not run while the updater is disabled.' : '' ?></p>
  273. <?php else: ?>
  274. <form method="POST" class="upd-auto-form">
  275. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  276. <input type="hidden" name="action" value="toggle_auto_update">
  277. <input type="hidden" name="enabled" value="1">
  278. <label class="checkbox-label">
  279. <input type="checkbox" name="accept_risk" value="1" required>
  280. <span>I understand that updates will be installed without my approval and may break the site or overwrite my changes.</span>
  281. </label>
  282. <button type="submit" class="btn btn-secondary">Turn on automatic updates</button>
  283. </form>
  284. <?php endif; ?>
  285. ​
  286. <?php if ($lastAutoRun !== null): ?>
  287. <p class="form-hint upd-last-run">
  288. Last automatic run: <?= e(date('Y-m-d H:i', strtotime($lastAutoRun['at']) ?: time())) ?>
  289. <span class="upd-pill <?= $lastAutoRun['ok'] ? 'upd-pill-added' : 'upd-pill-deleted' ?>"><?= $lastAutoRun['ok'] ? 'OK' : 'Problem' ?></span><br>
  290. <?= e($lastAutoRun['message']) ?>
  291. </p>
  292. <?php endif; ?>
  293. </div>
  294. </div>
  295. ​
  296. <?php if ($result !== null): ?>
  297. <div class="card" style="margin-bottom: 24px;">
  298. <div class="card-header">
  299. <h3 class="card-title">Result</h3>
  300. </div>
  301. <div class="card-body">
  302. <?php if ($result['status'] === 'update'): ?>
  303. <p><strong>Update available: <?= e($result['remote_version']) ?></strong><?= $result['released_at'] !== '' ? ' (' . e($result['released_at']) . ')' : '' ?></p>
  304. <?php elseif ($result['status'] === 'current'): ?>
  305. <p><strong>You are running the latest version.</strong></p>
  306. <?php else: ?>
  307. <p>This installation (<?= e($currentVersion) ?>) is newer than the published version (<?= e($result['remote_version']) ?>).</p>
  308. <?php endif; ?>
  309. ​
  310. <?php if (!empty($result['changelog'])): ?>
  311. <h4 class="upd-heading">Changes</h4>
  312. <ul class="upd-changelog">
  313. <?php foreach ($result['changelog'] as $entry): ?>
  314. <li><?= e($entry) ?></li>
  315. <?php endforeach; ?>
  316. </ul>
  317. <?php endif; ?>
  318. ​
  319. <?php if (!$result['compared']): ?>
  320. <form method="POST" class="upd-actions">
  321. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  322. <input type="hidden" name="action" value="download_release">
  323. <button type="submit" class="btn btn-primary">Download and show changes</button>
  324. </form>
  325. <p class="form-hint">The release is downloaded temporarily and compared with your files. Nothing is installed or changed.</p>
  326. <?php elseif (empty($result['files'])): ?>
  327. <p class="form-hint">Your files are identical to the release.</p>
  328. <?php elseif ($result['status'] !== 'ahead'): ?>
  329. <?php $installLabel = $result['status'] === 'update' ? 'Install version' : 'Sync your files with version'; ?>
  330. <form method="POST" class="upd-actions" onsubmit="return confirm(<?= e(json_encode($installLabel . ' ' . $result['remote_version'] . '? A backup of the files being replaced is saved first, and everything is rolled back if a file cannot be written.')) ?>);">
  331. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  332. <input type="hidden" name="action" value="install_release">
  333. <?php if (in_array('.htaccess', array_column($result['files'], 'path'), true)): ?>
  334. <label class="checkbox-label">
  335. <input type="checkbox" name="overwrite_htaccess" value="1">
  336. <span>Overwrite .htaccess (your own rules in it will be lost)</span>
  337. </label>
  338. <?php endif; ?>
  339. <button type="submit" class="btn btn-primary"><?= $result['status'] === 'update' ? 'Install update' : 'Sync files with release' ?></button>
  340. </form>
  341. <p class="form-hint">Files are backed up to updater-files/backups before being replaced. Your content (media/ and your own files in data/) is never touched; only data/default* files are updated.</p>
  342. <?php endif; ?>
  343. </div>
  344. </div>
  345. ​
  346. <?php if (!empty($result['files'])): ?>
  347. <h3 class="upd-heading">Files (<?= count($result['files']) ?>)</h3>
  348. ​
  349. <?php foreach ($result['files'] as $file): ?>
  350. <details class="upd-file">
  351. <summary>
  352. <span class="upd-pill upd-pill-<?= e($file['status']) ?>"><?= e($statusLabels[$file['status']]) ?></span>
  353. <span class="upd-path"><?= e($file['path']) ?></span>
  354. <span class="upd-stats">
  355. <?php if ($file['added'] > 0): ?><span class="upd-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
  356. <?php if ($file['removed'] > 0): ?><span class="upd-del">&minus;<?= (int) $file['removed'] ?></span><?php endif; ?>
  357. <?php if ($file['moved'] > 0): ?><span class="upd-move">&asymp;<?= intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
  358. </span>
  359. </summary>
  360. ​
  361. <?php if ($file['note'] !== ''): ?>
  362. <p class="form-hint upd-note"><?= e($file['note']) ?></p>
  363. <?php else: ?>
  364. <div class="upd-diff">
  365. <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
  366. <?php if ($hunkIndex > 0): ?>
  367. <div class="upd-gap">&hellip;</div>
  368. <?php endif; ?>
  369. <?php foreach ($hunk as $line): ?>
  370. <?php
  371. $lineClass = match ($line['type']) {
  372. 'add' => 'add',
  373. 'del' => 'del',
  374. 'moved_in', 'moved_out' => 'move',
  375. default => 'eq',
  376. };
  377. $marker = match ($line['type']) {
  378. 'add' => '+',
  379. 'del' => '−',
  380. 'moved_in' => '↓',
  381. 'moved_out' => '↑',
  382. default => ' ',
  383. };
  384. ?>
  385. <div class="upd-line upd-line-<?= $lineClass ?>">
  386. <span class="upd-ln"><?= $line['old'] ?? '' ?></span>
  387. <span class="upd-ln"><?= $line['new'] ?? '' ?></span>
  388. <span class="upd-marker"><?= $marker ?></span>
  389. <span class="upd-code"><?= e($line['text']) ?></span>
  390. </div>
  391. <?php endforeach; ?>
  392. <?php endforeach; ?>
  393. </div>
  394. <?php endif; ?>
  395. </details>
  396. <?php endforeach; ?>
  397. ​
  398. <?php if ($result['skipped'] > 0): ?>
  399. <p class="form-hint"><?= (int) $result['skipped'] ?> files were skipped because they are identical to yours or have an invalid path.</p>
  400. <?php endif; ?>
  401. <?php endif; ?>
  402. <?php endif; ?>
  403. ​
  404. <?php if (!empty($backups)): ?>
  405. <div class="card" style="margin-bottom: 24px;">
  406. <div class="card-header">
  407. <h3 class="card-title">Backups</h3>
  408. </div>
  409. <table class="table">
  410. <thead>
  411. <tr>
  412. <th>Created</th>
  413. <th>Version</th>
  414. <th>Files</th>
  415. <th>Status</th>
  416. <th></th>
  417. </tr>
  418. </thead>
  419. <tbody>
  420. <?php foreach ($backups as $backup): ?>
  421. <tr>
  422. <td><?= e(substr($backup['created_at'], 0, 19)) ?></td>
  423. <td><?= e($backup['from_version']) ?> &rarr; <?= e($backup['to_version']) ?></td>
  424. <td><?= (int) $backup['files'] ?></td>
  425. <td>
  426. <?php if ($backup['restored']): ?>
  427. <span class="upd-pill upd-pill-modified">Restored</span>
  428. <?php elseif ($backup['completed']): ?>
  429. <span class="upd-pill upd-pill-added">Installed</span>
  430. <?php else: ?>
  431. <span class="upd-pill upd-pill-deleted">Not completed</span>
  432. <?php endif; ?>
  433. </td>
  434. <td class="upd-row-actions">
  435. <form method="POST" onsubmit="return confirm('Restore the files from this backup? Files installed by the update will be replaced with the saved versions.');">
  436. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  437. <input type="hidden" name="action" value="restore_backup">
  438. <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
  439. <button type="submit" class="btn btn-secondary btn-sm">Restore</button>
  440. </form>
  441. <form method="POST" onsubmit="return confirm('Delete this backup permanently?');">
  442. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  443. <input type="hidden" name="action" value="delete_backup">
  444. <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
  445. <button type="submit" class="btn btn-danger btn-sm">Delete</button>
  446. </form>
  447. </td>
  448. </tr>
  449. <?php endforeach; ?>
  450. </tbody>
  451. </table>
  452. </div>
  453. <?php endif; ?>
  454. </div>
  455. </main>
  456. </div>
  457. </body>
  458. ​
  459. </html>
  460. ​