1<?php
2
3if (!defined('ACP_ROOT')) {
4 http_response_code(403);
5 die('Direct access denied.');
6}
7
8$latest = null;
9$preflight = null;
10$action = (string)($_POST['action'] ?? '');
11
12if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($action, array('check_start', 'check_step', 'install_start', 'install_step'), true)) {
13 header('Content-Type: application/json');
14
15 if (!acp_verify_csrf()) {
16 http_response_code(400);
17 echo json_encode(array('ok' => false, 'error' => 'Your session expired. Reload the page and try again.'));
18 exit;
19 }
20
21 if ($action === 'check_start') {
22 $latest = znote_update_latest(true);
23 $result = znote_update_check_start($latest);
24 } elseif ($action === 'check_step') {
25 $result = znote_update_check_step();
26 } elseif ($action === 'install_start') {
27 $latest = znote_update_latest(true);
28 $result = znote_update_install_start($latest);
29 } else {
30 $result = znote_update_install_step();
31 }
32
33 if (in_array($action, array('check_start', 'check_step'), true) && ($result['phase'] ?? '') === 'done') {
34 acp_log('update.check', (string)($result['manifest']['version'] ?? ''), array('passed' => $result['ok']));
35 }
36 if (($result['phase'] ?? '') === 'done' && $action === 'install_step') {
37 acp_log('update.install', (string)$result['version'], array('backup' => $result['backup']));
38 } elseif (empty($result['ok']) && $action === 'install_step') {
39 acp_log('update.install_failed', '', array('error' => $result['error'] ?? ''));
40 }
41
42 echo json_encode($result);
43 exit;
44}
45
46if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'rollback') {
47 $result = znote_update_rollback_latest();
48 if ($result['ok']) {
49 acp_log('update.rollback', (string)$result['version']);
50 acp_flash_success('ZnoteX core files were restored to version ' . h($result['version']) . '. Additive database migrations were kept.');
51 } else {
52 acp_log('update.rollback_failed', '', array('error' => $result['error']));
53 acp_flash_error(h($result['error']));
54 }
55 acp_redirect('update');
56}
57
58if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($action, array('check', 'install'), true)) {
59 @set_time_limit(0);
60 @ini_set('max_execution_time', '0');
61
62 $latest = znote_update_latest(true);
63 $preflight = znote_update_preflight($latest);
64 acp_log('update.check', (string)($latest['manifest']['version'] ?? ''), array('passed' => $preflight['ok']));
65
66 if ($action === 'install') {
67 if (!$preflight['ok']) {
68 acp_flash_error('The update was not started because at least one check failed.');
69 } else {
70 $result = znote_update_install($latest);
71 if ($result['ok']) {
72 acp_log('update.install', (string)$result['version'], array('backup' => $result['backup']));
73 acp_flash_success('ZnoteX was updated successfully to version ' . h($result['version']) . '.');
74 acp_redirect('update');
75 } else {
76 acp_log('update.install_failed', (string)($latest['manifest']['version'] ?? ''), array('error' => $result['error']));
77 acp_flash_error(h($result['error']));
78 }
79 }
80 }
81}
82
83if ($latest === null) {
84 $latest = znote_update_latest(false);
85}
86
87$manifest = !empty($latest['ok']) ? $latest['manifest'] : array();
88$backups = znote_update_backups();
89$textValue = static function ($value): string {
90 if (is_string($value)) {
91 return $value;
92 }
93 if (!is_array($value)) {
94 return '';
95 }
96 $language = strtolower((string)($GLOBALS['config']['language'] ?? 'en'));
97 return (string)($value[$language] ?? $value[substr($language, 0, 2)] ?? $value['en'] ?? reset($value));
98};
99?>
100
101<div class="acp-grid">
102 <?php
103 acp_stat('Installed version', znote_update_current_version(), 'fa-code-fork', null, 'blue');
104 acp_stat('Latest stable', !empty($latest['ok']) ? (string)$manifest['version'] : 'Unavailable', 'fa-cloud-download', null, !empty($latest['available']) ? 'amber' : 'green');
105 acp_stat('Update status', !empty($latest['available']) ? 'Available' : (!empty($latest['ok']) ? 'Up to date' : 'Check failed'), !empty($latest['available']) ? 'fa-arrow-up' : 'fa-check', null, !empty($latest['ok']) ? 'green' : 'red');
106 ?>
107</div>
108
109<?php acp_card_open('ZnoteX core update', 'Stable releases are downloaded from the official GitHub repository and accepted only after signature and integrity verification.'); ?>
110 <?php if (empty($latest['ok'])): ?>
111 <div style="padding:14px;border:1px solid var(--acp-red);border-radius:6px;color:var(--acp-red);">
112 <i class="fa fa-times-circle"></i> <?= h($latest['error'] ?? 'The release could not be checked.') ?>
113 </div>
114 <?php elseif (!empty($manifest)): ?>
115 <h3 style="margin-top:0;"><?= h($textValue($manifest['title'] ?? ('ZnoteX ' . $manifest['version']))) ?></h3>
116 <p><?= nl2br(h($textValue($manifest['description'] ?? ($manifest['summary'] ?? '')))) ?></p>
117 <?php $highlights = $manifest['highlights'] ?? array(); ?>
118 <?php if (is_array($highlights) && $highlights !== array()): ?>
119 <ul>
120 <?php foreach ($highlights as $highlight): ?><li><?= h($textValue($highlight)) ?></li><?php endforeach; ?>
121 </ul>
122 <?php endif; ?>
123 <?php if (empty($latest['available'])): ?>
124 <p style="color:var(--acp-green);"><i class="fa fa-check-circle"></i> This installation already uses the latest stable release.</p>
125 <?php endif; ?>
126 <?php endif; ?>
127
128 <div id="acpCheckIdle" style="margin-top:18px;">
129 <button class="acp-btn" type="button" id="acpCheckBtn" data-csrf="<?= h(acp_csrf()) ?>"><i class="fa fa-refresh"></i> Run full pre-installation check</button>
130 <noscript>
131 <form method="post" style="margin-top:12px;">
132 <?= acp_csrf_field() ?>
133 <input type="hidden" name="action" value="check">
134 <button class="acp-btn" type="submit"><i class="fa fa-refresh"></i> Run check without a progress bar (JavaScript is off)</button>
135 </form>
136 </noscript>
137 </div>
138 <div id="acpCheckProgress" hidden style="margin-top:18px;">
139 <div class="acp-progress-bar"><div class="acp-progress-fill" id="acpCheckFill"></div></div>
140 <p><span id="acpCheckPct">0%</span> - <span id="acpCheckPhase">Starting...</span></p>
141 <div class="acp-progress-log" id="acpCheckLog"></div>
142 </div>
143<?php acp_card_close(); ?>
144
145<div id="acpPreflightCard" <?= $preflight === null ? 'hidden' : '' ?>>
146 <?php acp_card_open('Pre-installation check', $preflight !== null && $preflight['ok'] ? 'Every check passed. The update can be installed.' : 'Installation is blocked until every failed check is resolved.'); ?>
147 <div id="acpPreflightCardBody">
148 <?php if ($preflight !== null): ?>
149 <table class="acp-table">
150 <thead><tr><th style="width:52px;">Status</th><th>Check</th><th>Result</th></tr></thead>
151 <tbody>
152 <?php foreach ($preflight['checks'] as $check): ?>
153 <tr>
154 <td style="font-size:20px;color:<?= $check['ok'] ? 'var(--acp-green)' : 'var(--acp-red)' ?>;"><i class="fa <?= $check['ok'] ? 'fa-check-circle' : 'fa-times-circle' ?>"></i></td>
155 <td><strong><?= h($check['label']) ?></strong></td>
156 <td><?= h($check['detail']) ?></td>
157 </tr>
158 <?php endforeach; ?>
159 </tbody>
160 </table>
161 <div id="acpInstallIdle" style="margin-top:18px;">
162 <button class="acp-btn <?= $preflight['ok'] ? 'acp-btn--green' : 'acp-btn--ghost' ?>" type="button" id="acpInstallBtn" data-csrf="<?= h(acp_csrf()) ?>" <?= $preflight['ok'] ? '' : 'disabled' ?>><i class="fa fa-cloud-download"></i> Install version <?= h((string)($manifest['version'] ?? '')) ?></button>
163 <noscript>
164 <form method="post" style="margin-top:12px;">
165 <?= acp_csrf_field() ?>
166 <input type="hidden" name="action" value="install">
167 <button class="acp-btn acp-btn--green" type="submit" <?= $preflight['ok'] ? '' : 'disabled' ?>><i class="fa fa-cloud-download"></i> Install without a progress bar (JavaScript is off)</button>
168 </form>
169 </noscript>
170 </div>
171 <div id="acpInstallProgress" hidden style="margin-top:18px;">
172 <div class="acp-progress-bar"><div class="acp-progress-fill" id="acpInstallFill"></div></div>
173 <p><span id="acpInstallPct">0%</span> - <span id="acpInstallPhase">Starting...</span></p>
174 <div class="acp-progress-log" id="acpInstallLog"></div>
175 </div>
176 <?php endif; ?>
177 </div>
178 <?php acp_card_close(); ?>
179</div>
180
181<script>
182(function () {
183 var checkBtn = document.getElementById('acpCheckBtn');
184 if (!checkBtn) return;
185
186 var idle = document.getElementById('acpCheckIdle');
187 var box = document.getElementById('acpCheckProgress');
188 var fill = document.getElementById('acpCheckFill');
189 var pct = document.getElementById('acpCheckPct');
190 var phaseEl = document.getElementById('acpCheckPhase');
191 var log = document.getElementById('acpCheckLog');
192 var csrf = checkBtn.getAttribute('data-csrf');
193 var preflightCard = document.getElementById('acpPreflightCard');
194 var preflightBody = document.getElementById('acpPreflightCardBody');
195
196 var checkPhaseLabels = { extract: 'Extracting package', localmods: 'Checking for local modifications', done: 'Done' };
197 var installPhaseLabels = { backup: 'Backing up current files', migrate: 'Applying database migrations', copy: 'Installing new files', done: 'Done' };
198
199 function addLog(target, text) {
200 var line = document.createElement('div');
201 line.textContent = text;
202 target.appendChild(line);
203 target.scrollTop = target.scrollHeight;
204 }
205
206 function post(action) {
207 var body = new URLSearchParams();
208 body.set('action', action);
209 body.set('csrf_token', csrf);
210 return fetch(window.location.href, { method: 'POST', body: body }).then(function (r) { return r.json(); });
211 }
212
213 function escapeHtml(s) {
214 var d = document.createElement('div');
215 d.textContent = String(s == null ? '' : s);
216 return d.innerHTML;
217 }
218
219 function renderPreflight(result) {
220 var rows = result.checks.map(function (check) {
221 var color = check.ok ? 'var(--acp-green)' : 'var(--acp-red)';
222 var icon = check.ok ? 'fa-check-circle' : 'fa-times-circle';
223 return '<tr><td style="font-size:20px;color:' + color + ';"><i class="fa ' + icon + '"></i></td>'
224 + '<td><strong>' + escapeHtml(check.label) + '</strong></td>'
225 + '<td>' + escapeHtml(check.detail) + '</td></tr>';
226 }).join('');
227
228 var version = (result.manifest && result.manifest.version) ? result.manifest.version : '';
229
230 preflightBody.innerHTML =
231 '<table class="acp-table"><thead><tr><th style="width:52px;">Status</th><th>Check</th><th>Result</th></tr></thead><tbody>' + rows + '</tbody></table>'
232 + '<div id="acpInstallIdle" style="margin-top:18px;">'
233 + '<button class="acp-btn ' + (result.ok ? 'acp-btn--green' : 'acp-btn--ghost') + '" type="button" id="acpInstallBtn" data-csrf="' + escapeHtml(csrf) + '"' + (result.ok ? '' : ' disabled') + '><i class="fa fa-cloud-download"></i> Install version ' + escapeHtml(version) + '</button>'
234 + '</div>'
235 + '<div id="acpInstallProgress" hidden style="margin-top:18px;">'
236 + '<div class="acp-progress-bar"><div class="acp-progress-fill" id="acpInstallFill"></div></div>'
237 + '<p><span id="acpInstallPct">0%</span> - <span id="acpInstallPhase">Starting...</span></p>'
238 + '<div class="acp-progress-log" id="acpInstallLog"></div>'
239 + '</div>';
240
241 preflightCard.hidden = false;
242 bindInstall();
243 }
244
245 function bindInstall() {
246 var btn = document.getElementById('acpInstallBtn');
247 if (!btn) return;
248
249 var iidle = document.getElementById('acpInstallIdle');
250 var ibox = document.getElementById('acpInstallProgress');
251 var ifill = document.getElementById('acpInstallFill');
252 var ipct = document.getElementById('acpInstallPct');
253 var iphase = document.getElementById('acpInstallPhase');
254 var ilog = document.getElementById('acpInstallLog');
255
256 function setProgress(cursor, total, phase) {
257 var percent = total > 0 ? Math.round((cursor / total) * 100) : (phase === 'migrate' ? 50 : 0);
258 ifill.style.width = percent + '%';
259 ipct.textContent = percent + '%';
260 iphase.textContent = installPhaseLabels[phase] || phase;
261 }
262
263 function step() {
264 post('install_step').then(function (result) {
265 if (!result.ok) {
266 addLog(ilog, 'Failed: ' + result.error);
267 iphase.textContent = 'Failed';
268 return;
269 }
270 if (result.message) addLog(ilog, result.message);
271 setProgress(result.cursor || 0, result.total || 0, result.phase);
272 if (result.phase === 'done') {
273 addLog(ilog, 'Reloading...');
274 setTimeout(function () { window.location.reload(); }, 800);
275 return;
276 }
277 step();
278 }).catch(function (e) {
279 addLog(ilog, 'Network error: ' + e.message);
280 });
281 }
282
283 btn.addEventListener('click', function () {
284 iidle.hidden = true;
285 ibox.hidden = false;
286 addLog(ilog, 'Starting install...');
287 post('install_start').then(function (result) {
288 if (!result.ok) {
289 addLog(ilog, 'Failed: ' + result.error);
290 iphase.textContent = 'Failed';
291 return;
292 }
293 setProgress(0, result.total || 0, 'backup');
294 step();
295 }).catch(function (e) {
296 addLog(ilog, 'Network error: ' + e.message);
297 });
298 });
299 }
300
301 function setCheckProgress(cursor, total, phase) {
302 var percent = total > 0 ? Math.round((cursor / total) * 100) : 0;
303 fill.style.width = percent + '%';
304 pct.textContent = percent + '%';
305 phaseEl.textContent = checkPhaseLabels[phase] || phase;
306 }
307
308 function step() {
309 post('check_step').then(function (result) {
310 if (!result.ok && result.phase !== 'done') {
311 addLog(log, 'Failed: ' + result.error);
312 phaseEl.textContent = 'Failed';
313 return;
314 }
315 if (result.message) addLog(log, result.message);
316 if (result.phase === 'done') {
317 setCheckProgress(1, 1, 'done');
318 renderPreflight(result);
319 return;
320 }
321 setCheckProgress(result.cursor || 0, result.total || 0, result.phase);
322 step();
323 }).catch(function (e) {
324 addLog(log, 'Network error: ' + e.message);
325 });
326 }
327
328 checkBtn.addEventListener('click', function () {
329 idle.hidden = true;
330 box.hidden = false;
331 log.innerHTML = '';
332 addLog(log, 'Starting check...');
333 post('check_start').then(function (result) {
334 if (result.phase === 'done') {
335 renderPreflight(result);
336 idle.hidden = false;
337 box.hidden = true;
338 return;
339 }
340 setCheckProgress(0, result.total || 0, result.phase);
341 step();
342 }).catch(function (e) {
343 addLog(log, 'Network error: ' + e.message);
344 });
345 });
346
347 if (document.getElementById('acpInstallBtn')) {
348 bindInstall();
349 }
350})();
351</script>
352
353<?php acp_card_open('Recovery', 'Each installation creates a complete backup of every managed file before changing the site.'); ?>
354 <?php if ($backups === array()): ?>
355 <p>No update backup is available yet.</p>
356 <?php else: ?>
357 <p>Latest backup: <strong><?= h((string)$backups[0]['journal']['version']) ?></strong> from <?= h((string)$backups[0]['journal']['created_at']) ?>.</p>
358 <form method="post" onsubmit="return confirm('Restore the latest core file backup?');">
359 <?= acp_csrf_field() ?>
360 <input type="hidden" name="action" value="rollback">
361 <button class="acp-btn acp-btn--amber" type="submit"><i class="fa fa-undo"></i> Restore latest file backup</button>
362 </form>
363 <?php endif; ?>
364<?php acp_card_close(); ?>
365