WebOrbiton
v3.0.0.0

FlatlyPage

461 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. <meta name="generator" content="FlatlyPage CMS">
  140. <link rel="icon" href="admin.ico?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>" type="image/x-icon">
  141. <?= admin_font_head() ?>
  142. <link rel="stylesheet" href="/css/admin.css?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>">
  143. <script src="/assets/js/admin-theme.js?v=5"></script>
  144. </head>
  145. ​
  146. <body>
  147. <div class="app">
  148. <?php admin_sidebar('updater'); ?>
  149. ​
  150. <main class="main">
  151. <header class="main-header">
  152. <button class="mobile-nav-toggle" onclick="document.querySelector('.sidebar').classList.add('open')">
  153. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
  154. <line x1="3" y1="6" x2="21" y2="6" />
  155. <line x1="3" y1="12" x2="21" y2="12" />
  156. <line x1="3" y1="18" x2="21" y2="18" />
  157. </svg>
  158. </button>
  159. ​
  160. <div class="main-header-inner">
  161. <h1>Updater</h1>
  162. <div class="header-actions">
  163. <a class="btn btn-secondary btn-sm" href="javascript:void(0)" onclick="toggleTheme()" id="theme-toggle-btn">
  164. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16">
  165. <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
  166. </svg>
  167. <span>Theme</span>
  168. </a>
  169. </div>
  170. </div>
  171. </header>
  172. ​
  173. <div class="main-content">
  174. <?php if ($message): ?>
  175. <div class="message <?= e($message_type) ?>">
  176. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  177. <?php if ($message_type === 'success'): ?>
  178. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
  179. <polyline points="22 4 12 14.01 9 11.01" />
  180. <?php else: ?>
  181. <circle cx="12" cy="12" r="10" />
  182. <line x1="12" y1="8" x2="12" y2="12" />
  183. <line x1="12" y1="16" x2="12.01" y2="16" />
  184. <?php endif; ?>
  185. </svg>
  186. <?= e($message) ?>
  187. </div>
  188. <?php endif; ?>
  189. ​
  190. <div class="card" style="margin-bottom: 24px;">
  191. <div class="card-header">
  192. <h3 class="card-title">Status</h3>
  193. </div>
  194. <div class="card-body">
  195. <p>Installed version: <strong><?= e($currentVersion) ?></strong>
  196. <span class="upd-pill <?= $updaterEnabled ? 'upd-pill-added' : 'upd-pill-muted' ?>"><?= $updaterEnabled ? 'Enabled' : 'Disabled' ?></span></p>
  197. ​
  198. <div class="upd-actions">
  199. <?php if ($updaterEnabled): ?>
  200. <form method="POST">
  201. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  202. <input type="hidden" name="action" value="check_updates">
  203. <button type="submit" class="btn btn-primary">Check for updates</button>
  204. </form>
  205. <?php endif; ?>
  206. <form method="POST">
  207. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  208. <input type="hidden" name="action" value="toggle_updater">
  209. <input type="hidden" name="enabled" value="<?= $updaterEnabled ? '0' : '1' ?>">
  210. <button type="submit" class="btn <?= $updaterEnabled ? 'btn-danger' : 'btn-secondary' ?>"><?= $updaterEnabled ? 'Disable updater' : 'Enable updater' ?></button>
  211. </form>
  212. </div>
  213. <?php if (!$updaterEnabled): ?>
  214. <p class="form-hint">The updater is off by default. While it is off, this site sends no requests to the update server.</p>
  215. <?php endif; ?>
  216. ​
  217. <form method="POST" class="upd-actions">
  218. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  219. <input type="hidden" name="action" value="toggle_cleanup">
  220. <input type="hidden" name="enabled" value="<?= $autoCleanup ? '0' : '1' ?>">
  221. <button type="submit" class="btn btn-secondary"><?= $autoCleanup ? 'Turn off backup cleanup' : 'Turn on backup cleanup' ?></button>
  222. </form>
  223. <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>
  224. </div>
  225. </div>
  226. ​
  227. <div class="card" style="margin-bottom: 24px;">
  228. <div class="card-header">
  229. <h3 class="card-title">Automatic updates</h3>
  230. <span class="upd-pill <?= $autoRunning ? 'upd-pill-deleted' : 'upd-pill-muted' ?>"><?= $autoRunning ? 'On' : 'Off' ?></span>
  231. </div>
  232. <div class="card-body">
  233. <div class="upd-warning" role="note">
  234. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
  235. <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" />
  236. <line x1="12" y1="9" x2="12" y2="13" />
  237. <line x1="12" y1="17" x2="12.01" y2="17" />
  238. </svg>
  239. <div>
  240. <strong>Warning: automatic updates can be dangerous for your site.</strong>
  241. <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>
  242. <ul>
  243. <li>An update can break the site or parts of it.</li>
  244. <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>
  245. <li>You will only find out when you open this page or notice a problem on the site.</li>
  246. </ul>
  247. <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>
  248. <p>Only turn this on if you do not modify the CMS files and you check your site regularly.</p>
  249. </div>
  250. </div>
  251. ​
  252. <?php if ($autoRunning): ?>
  253. <form method="POST" class="upd-actions">
  254. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  255. <input type="hidden" name="action" value="toggle_auto_update">
  256. <input type="hidden" name="enabled" value="0">
  257. <button type="submit" class="btn btn-danger">Turn off automatic updates</button>
  258. </form>
  259. ​
  260. <h4 class="upd-heading">Cron job</h4>
  261. <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>
  262. <label class="form-label" for="updCronCli">Command (recommended)</label>
  263. <input type="text" id="updCronCli" class="form-input upd-copy" readonly value="<?= e('php ' . BASE_DIR . '/cron-update.php') ?>" onclick="this.select()">
  264. <label class="form-label" for="updCronUrl" style="margin-top: 12px;">Or call this URL (keep it secret)</label>
  265. <input type="text" id="updCronUrl" class="form-input upd-copy" readonly value="<?= e($cronUrl) ?>" onclick="this.select()">
  266. <p class="form-hint">Example: <code>0 4 * * * wget -qO- "<?= e($cronUrl) ?>"</code></p>
  267. <form method="POST" class="upd-actions" onsubmit="return confirm('Generate a new cron URL? The current URL will stop working.');">
  268. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  269. <input type="hidden" name="action" value="regenerate_cron_token">
  270. <button type="submit" class="btn btn-secondary btn-sm">Generate new URL</button>
  271. </form>
  272. <?php elseif (!$updaterEnabled): ?>
  273. <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>
  274. <?php else: ?>
  275. <form method="POST" class="upd-auto-form">
  276. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  277. <input type="hidden" name="action" value="toggle_auto_update">
  278. <input type="hidden" name="enabled" value="1">
  279. <label class="checkbox-label">
  280. <input type="checkbox" name="accept_risk" value="1" required>
  281. <span>I understand that updates will be installed without my approval and may break the site or overwrite my changes.</span>
  282. </label>
  283. <button type="submit" class="btn btn-secondary">Turn on automatic updates</button>
  284. </form>
  285. <?php endif; ?>
  286. ​
  287. <?php if ($lastAutoRun !== null): ?>
  288. <p class="form-hint upd-last-run">
  289. Last automatic run: <?= e(date('Y-m-d H:i', strtotime($lastAutoRun['at']) ?: time())) ?>
  290. <span class="upd-pill <?= $lastAutoRun['ok'] ? 'upd-pill-added' : 'upd-pill-deleted' ?>"><?= $lastAutoRun['ok'] ? 'OK' : 'Problem' ?></span><br>
  291. <?= e($lastAutoRun['message']) ?>
  292. </p>
  293. <?php endif; ?>
  294. </div>
  295. </div>
  296. ​
  297. <?php if ($result !== null): ?>
  298. <div class="card" style="margin-bottom: 24px;">
  299. <div class="card-header">
  300. <h3 class="card-title">Result</h3>
  301. </div>
  302. <div class="card-body">
  303. <?php if ($result['status'] === 'update'): ?>
  304. <p><strong>Update available: <?= e($result['remote_version']) ?></strong><?= $result['released_at'] !== '' ? ' (' . e($result['released_at']) . ')' : '' ?></p>
  305. <?php elseif ($result['status'] === 'current'): ?>
  306. <p><strong>You are running the latest version.</strong></p>
  307. <?php else: ?>
  308. <p>This installation (<?= e($currentVersion) ?>) is newer than the published version (<?= e($result['remote_version']) ?>).</p>
  309. <?php endif; ?>
  310. ​
  311. <?php if (!empty($result['changelog'])): ?>
  312. <h4 class="upd-heading">Changes</h4>
  313. <ul class="upd-changelog">
  314. <?php foreach ($result['changelog'] as $entry): ?>
  315. <li><?= e($entry) ?></li>
  316. <?php endforeach; ?>
  317. </ul>
  318. <?php endif; ?>
  319. ​
  320. <?php if (!$result['compared']): ?>
  321. <form method="POST" class="upd-actions">
  322. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  323. <input type="hidden" name="action" value="download_release">
  324. <button type="submit" class="btn btn-primary">Download and show changes</button>
  325. </form>
  326. <p class="form-hint">The release is downloaded temporarily and compared with your files. Nothing is installed or changed.</p>
  327. <?php elseif (empty($result['files'])): ?>
  328. <p class="form-hint">Your files are identical to the release.</p>
  329. <?php elseif ($result['status'] !== 'ahead'): ?>
  330. <?php $installLabel = $result['status'] === 'update' ? 'Install version' : 'Sync your files with version'; ?>
  331. <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.')) ?>);">
  332. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  333. <input type="hidden" name="action" value="install_release">
  334. <?php if (in_array('.htaccess', array_column($result['files'], 'path'), true)): ?>
  335. <label class="checkbox-label">
  336. <input type="checkbox" name="overwrite_htaccess" value="1">
  337. <span>Overwrite .htaccess (your own rules in it will be lost)</span>
  338. </label>
  339. <?php endif; ?>
  340. <button type="submit" class="btn btn-primary"><?= $result['status'] === 'update' ? 'Install update' : 'Sync files with release' ?></button>
  341. </form>
  342. <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>
  343. <?php endif; ?>
  344. </div>
  345. </div>
  346. ​
  347. <?php if (!empty($result['files'])): ?>
  348. <h3 class="upd-heading">Files (<?= count($result['files']) ?>)</h3>
  349. ​
  350. <?php foreach ($result['files'] as $file): ?>
  351. <details class="upd-file">
  352. <summary>
  353. <span class="upd-pill upd-pill-<?= e($file['status']) ?>"><?= e($statusLabels[$file['status']]) ?></span>
  354. <span class="upd-path"><?= e($file['path']) ?></span>
  355. <span class="upd-stats">
  356. <?php if ($file['added'] > 0): ?><span class="upd-add">+<?= (int) $file['added'] ?></span><?php endif; ?>
  357. <?php if ($file['removed'] > 0): ?><span class="upd-del">&minus;<?= (int) $file['removed'] ?></span><?php endif; ?>
  358. <?php if ($file['moved'] > 0): ?><span class="upd-move">&asymp;<?= intdiv($file['moved'], 2) ?> moved</span><?php endif; ?>
  359. </span>
  360. </summary>
  361. ​
  362. <?php if ($file['note'] !== ''): ?>
  363. <p class="form-hint upd-note"><?= e($file['note']) ?></p>
  364. <?php else: ?>
  365. <div class="upd-diff">
  366. <?php foreach (Updater::hunks($file['ops']) as $hunkIndex => $hunk): ?>
  367. <?php if ($hunkIndex > 0): ?>
  368. <div class="upd-gap">&hellip;</div>
  369. <?php endif; ?>
  370. <?php foreach ($hunk as $line): ?>
  371. <?php
  372. $lineClass = match ($line['type']) {
  373. 'add' => 'add',
  374. 'del' => 'del',
  375. 'moved_in', 'moved_out' => 'move',
  376. default => 'eq',
  377. };
  378. $marker = match ($line['type']) {
  379. 'add' => '+',
  380. 'del' => '−',
  381. 'moved_in' => '↓',
  382. 'moved_out' => '↑',
  383. default => ' ',
  384. };
  385. ?>
  386. <div class="upd-line upd-line-<?= $lineClass ?>">
  387. <span class="upd-ln"><?= $line['old'] ?? '' ?></span>
  388. <span class="upd-ln"><?= $line['new'] ?? '' ?></span>
  389. <span class="upd-marker"><?= $marker ?></span>
  390. <span class="upd-code"><?= e($line['text']) ?></span>
  391. </div>
  392. <?php endforeach; ?>
  393. <?php endforeach; ?>
  394. </div>
  395. <?php endif; ?>
  396. </details>
  397. <?php endforeach; ?>
  398. ​
  399. <?php if ($result['skipped'] > 0): ?>
  400. <p class="form-hint"><?= (int) $result['skipped'] ?> files were skipped because they are identical to yours or have an invalid path.</p>
  401. <?php endif; ?>
  402. <?php endif; ?>
  403. <?php endif; ?>
  404. ​
  405. <?php if (!empty($backups)): ?>
  406. <div class="card" style="margin-bottom: 24px;">
  407. <div class="card-header">
  408. <h3 class="card-title">Backups</h3>
  409. </div>
  410. <table class="table">
  411. <thead>
  412. <tr>
  413. <th>Created</th>
  414. <th>Version</th>
  415. <th>Files</th>
  416. <th>Status</th>
  417. <th></th>
  418. </tr>
  419. </thead>
  420. <tbody>
  421. <?php foreach ($backups as $backup): ?>
  422. <tr>
  423. <td><?= e(substr($backup['created_at'], 0, 19)) ?></td>
  424. <td><?= e($backup['from_version']) ?> &rarr; <?= e($backup['to_version']) ?></td>
  425. <td><?= (int) $backup['files'] ?></td>
  426. <td>
  427. <?php if ($backup['restored']): ?>
  428. <span class="upd-pill upd-pill-modified">Restored</span>
  429. <?php elseif ($backup['completed']): ?>
  430. <span class="upd-pill upd-pill-added">Installed</span>
  431. <?php else: ?>
  432. <span class="upd-pill upd-pill-deleted">Not completed</span>
  433. <?php endif; ?>
  434. </td>
  435. <td class="upd-row-actions">
  436. <form method="POST" onsubmit="return confirm('Restore the files from this backup? Files installed by the update will be replaced with the saved versions.');">
  437. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  438. <input type="hidden" name="action" value="restore_backup">
  439. <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
  440. <button type="submit" class="btn btn-secondary btn-sm">Restore</button>
  441. </form>
  442. <form method="POST" onsubmit="return confirm('Delete this backup permanently?');">
  443. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  444. <input type="hidden" name="action" value="delete_backup">
  445. <input type="hidden" name="backup_id" value="<?= e($backup['id']) ?>">
  446. <button type="submit" class="btn btn-danger btn-sm">Delete</button>
  447. </form>
  448. </td>
  449. </tr>
  450. <?php endforeach; ?>
  451. </tbody>
  452. </table>
  453. </div>
  454. <?php endif; ?>
  455. </div>
  456. </main>
  457. </div>
  458. </body>
  459. ​
  460. </html>
  461. ​