1<?php
2/**
3 * Title: Plugins
4 * Icon: fa-plug
5 * Group: Settings
6 * Order: 20
7 * Description: Install, update, enable and disable what is in plugins/.
8 */
9
10/*
11 * How a plugin gets here: you download it and drop the folder into plugins/.
12 * ZnoteX does not fetch anything by itself - installing a plugin means running
13 * someone else's PHP on every page of your site, so putting the file there
14 * stays a deliberate act, not a button.
15 *
16 * From that point this page does the rest:
17 *
18 * Install runs install.sql and records the version (folder -> working)
19 * Update shown when the folder is newer than what was installed
20 * Enable the plugin starts running
21 * Disable it stops, tables untouched
22 */
23
24if (!defined('ACP_ROOT')) {
25 http_response_code(403);
26 die('Direct access denied.');
27}
28
29if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['clear_plugin_repository_cache'])) {
30 if (plugin_repository_clear_cache()) {
31 acp_log('plugins.cache_clear');
32 acp_flash_success(t('acp.plgbr.cache_deleted'));
33 } else {
34 acp_flash_error(t('acp.plgbr.cache_delete_failed', [
35 'path' => '<code>' . h(plugin_repository_cache_path()) . '</code>',
36 ]));
37 }
38
39 acp_redirect('plugins', array('tab' => 'browse', 'refresh' => 1));
40}
41
42if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['repo_install'])) {
43
44 $key = znote_plugin_sanitize((string)$_POST['repo_install']);
45 $overwrite = !empty($_POST['overwrite']);
46 $result = plugin_repository_install($key, $overwrite);
47
48 if ($result === '') {
49 $sqlError = znote_plugin_install($key);
50 if ($sqlError !== '') {
51 acp_flash_error(t('acp.plgbr.install_failed', ['error' => h($sqlError)]));
52 } else {
53 znote_plugin_set_enabled($key, true);
54 acp_log('plugins.repo_install', $key);
55 $m = znote_plugin_manifest($key);
56 acp_flash_success(t('acp.plgbr.installed', [
57 'plugin' => '<strong>' . h($m['name']) . '</strong>',
58 'path' => '<code>plugins/' . h($key) . '/</code>',
59 ]));
60 }
61 } elseif ($result === 'already-installed') {
62 acp_flash_error(t('acp.plgbr.already_installed', ['plugin' => '<strong>' . h($key) . '</strong>']));
63 } else {
64 acp_flash_error(t('acp.plgbr.install_failed', ['error' => h($result)]));
65 }
66
67 acp_redirect('plugins', array('tab' => 'browse'));
68}
69
70if (($_GET['tab'] ?? '') === 'browse') {
71 // "Refresh catalogue" also wipes the local caches - a stale cache is what
72 // usually keeps a just-installed or updated plugin from showing correctly.
73 if (isset($_GET['refresh']) && function_exists('znote_cache_flush')) {
74 znote_cache_flush();
75 acp_log('plugins.catalogue_refresh');
76 }
77 include ACP_ROOT . '/modules/_partials/plugins_browse.php';
78 return;
79}
80
81if ($_SERVER['REQUEST_METHOD'] === 'POST') {
82 $name = znote_plugin_sanitize((string)($_POST['plugin'] ?? ''));
83 $action = (string)($_POST['action'] ?? '');
84 $known = znote_plugins(true);
85
86 if ($name === '' || !isset($known[$name])) {
87 acp_flash_error(t('acp.plg.no_such'));
88 acp_redirect('plugins');
89 }
90
91 $plugin = $known[$name];
92 $label = $plugin['name'];
93
94 switch ($action) {
95
96 case 'install':
97 $error = znote_plugin_install($name);
98
99 if ($error !== '') {
100 acp_flash_error(t('acp.plg.install_failed', ['plugin' => $label, 'error' => $error]));
101 break;
102 }
103
104 // Installing implies wanting it on. Nobody installs a plugin in
105 // order to leave it switched off.
106 znote_plugin_set_enabled($name, true);
107 acp_log('plugin.install', $name, ['version' => $plugin['version']]);
108 acp_flash_success(t('acp.plg.installed', ['plugin' => $label, 'version' => $plugin['version']]));
109 break;
110
111 case 'update':
112 $from = $plugin['installed_version'];
113 $error = znote_plugin_install($name);
114
115 if ($error !== '') {
116 acp_flash_error(t('acp.plg.update_failed', ['plugin' => $label, 'error' => $error]));
117 } else {
118 acp_log('plugin.update', $name, ['from' => $from, 'to' => $plugin['version']]);
119 acp_flash_success(t('acp.plg.updated', ['plugin' => $label, 'from' => $from, 'to' => $plugin['version']]));
120 }
121 break;
122
123 case 'enable':
124 if (!$plugin['compatible']) {
125 acp_flash_error(h(implode(' ', $plugin['compatibility_errors'])));
126 } elseif (znote_plugin_set_enabled($name, true)) {
127 acp_log('plugin.enable', $name);
128 acp_flash_success(t('acp.plg.enabled', ['plugin' => $label]));
129 } else {
130 acp_flash_error(t_default('acp.plg.enable_failed', 'Could not enable {plugin}.', ['plugin' => h($label)]));
131 }
132 break;
133
134 case 'disable':
135 znote_plugin_set_enabled($name, false);
136 acp_log('plugin.disable', $name);
137 acp_flash_info(t('acp.plg.disabled', ['plugin' => $label]));
138 break;
139
140 case 'uninstall':
141 // Forgets the install, keeps the data. Dropping a player's coupons
142 // because someone clicked a button would be the wrong default.
143 znote_plugin_uninstall($name);
144 acp_log('plugin.uninstall', $name);
145 acp_flash_info(t('acp.plg.uninstalled', ['plugin' => $label]));
146 break;
147
148 default:
149 acp_flash_error(t('acp.plg.unknown_action'));
150 }
151
152 acp_redirect('plugins');
153}
154
155$plugins = znote_plugins(true);
156$active = 0;
157$updatable = 0;
158
159foreach ($plugins as $plugin) {
160 if ($plugin['enabled'] && $plugin['installed'] && $plugin['compatible']) {
161 $active++;
162 }
163 if ($plugin['update']) {
164 $updatable++;
165 }
166}
167?>
168
169<?php if (plugin_repository_config()['enabled']): ?>
170 <div class="acp-toolbar">
171 <div></div>
172 <div class="acp-actions is-tight">
173 <a class="acp-btn" href="<?= h(acp_url('plugins', array('tab' => 'browse'))) ?>">
174 <i class="fa fa-cloud-download"></i> <?= t('acp.plg.browse_plugins') ?>
175 </a>
176 </div>
177 </div>
178<?php endif; ?>
179
180<div class="acp-grid">
181 <?php
182 acp_stat(t('acp.plg.stat_in_folder'), count($plugins), 'fa-folder-o', null, 'blue');
183 acp_stat(t('acp.plg.stat_running'), $active, 'fa-check', null, 'green');
184 if ($updatable > 0) {
185 acp_stat(t('acp.plg.stat_updates'), $updatable, 'fa-arrow-up', null, 'amber');
186 }
187 ?>
188</div>
189
190<?php acp_card_open(t('acp.plg.title'), t('acp.plg.sub')); ?>
191
192 <?php if (!$plugins): ?>
193
194 <?php acp_empty(t('acp.plg.empty'), 'fa-plug'); ?>
195
196 <?php else: ?>
197
198 <?php if (count($plugins) > 6): ?>
199 <div class="acp-toolbar">
200 <div style="display:flex;gap:8px;flex:1 1 320px;max-width:460px;">
201 <input class="acp-input" type="search" data-acp-search-input="acpPluginTable"
202 placeholder="<?= h(t_default('acp.plg.search_placeholder', 'Search plugins...')) ?>">
203 </div>
204 <span class="is-muted" data-acp-search-count="acpPluginTable"></span>
205 </div>
206 <?php endif; ?>
207
208 <table class="acp-table" id="acpPluginTable">
209 <thead>
210 <tr>
211 <th><?= t('acp.plg.col_plugin') ?></th>
212 <th><?= t('acp.plg.col_adds') ?></th>
213 <th><?= t('acp.plg.col_version') ?></th>
214 <th><?= t('acp.plg.col_status') ?></th>
215 <th></th>
216 </tr>
217 </thead>
218 <tbody>
219 <?php foreach ($plugins as $name => $plugin):
220
221 $adds = array();
222 if ($plugin['pages']) { $adds[] = t('acp.plg.page_count', ['n' => $plugin['pages']]); }
223 if ($plugin['admin']) { $adds[] = t('acp.plg.admin_page_count', ['n' => $plugin['admin']]); }
224 if ($plugin['sql']) { $adds[] = t('acp.plg.tables'); }
225
226 $running = $plugin['installed'] && $plugin['enabled'] && $plugin['compatible'];
227 ?>
228 <tr data-acp-search="<?= h(strtolower($plugin['name'] . ' ' . $name . ' ' . ($plugin['description'] ?? ''))) ?>">
229 <td>
230 <strong><?= h($plugin['name']) ?></strong>
231 <?php if ($plugin['description'] !== ''): ?>
232 <span class="acp-hint" style="display:block;"><?= h($plugin['description']) ?></span>
233 <?php endif; ?>
234 <span class="acp-hint" style="display:block;">
235 <code><?= h($name) ?></code>
236 <?php if ($plugin['author'] !== ''): ?>· <?= h($plugin['author']) ?><?php endif; ?>
237 <?php if ($plugin['url'] !== ''): ?>
238 · <a href="<?= h($plugin['url']) ?>" target="_blank" rel="noopener noreferrer"><?= t('acp.plg.website') ?></a>
239 <?php endif; ?>
240 </span>
241 <?php if (!$plugin['compatible']): ?>
242 <span class="acp-hint" style="display:block;color:var(--acp-red);">
243 <?= h(implode(' ', $plugin['compatibility_errors'])) ?>
244 </span>
245 <?php endif; ?>
246 </td>
247
248 <td><?= $adds ? h(implode(', ', $adds)) : '<span class="acp-hint">' . t('acp.plg.hooks_only') . '</span>' ?></td>
249
250 <td>
251 <?= $plugin['version'] !== '' ? h($plugin['version']) : '—' ?>
252 <?php if (!$plugin['compatible']): ?>
253 <span class="acp-pill acp-pill--red"><?= h(t_default('acp.plg.incompatible', 'Incompatible')) ?></span>
254 <?php elseif ($plugin['update']): ?>
255 <span class="acp-hint" style="display:block;"><?= t('acp.plg.installed_label', ['version' => h($plugin['installed_version'])]) ?></span>
256 <?php endif; ?>
257 </td>
258
259 <td>
260 <?php if ($plugin['update']): ?>
261 <span class="acp-pill acp-pill--amber"><?= t('acp.plg.update_available') ?></span>
262 <?php elseif (!$plugin['installed']): ?>
263 <span class="acp-pill"><?= t('acp.plg.not_installed') ?></span>
264 <?php elseif ($running): ?>
265 <span class="acp-pill acp-pill--green"><?= t('acp.plg.running_pill') ?></span>
266 <?php else: ?>
267 <span class="acp-pill"><?= t('acp.plg.disabled_pill') ?></span>
268 <?php endif; ?>
269 </td>
270
271 <td style="text-align:right;white-space:nowrap;">
272
273 <?php if ($running && $plugin['pages']): ?>
274 <a class="acp-btn acp-btn--ghost acp-btn--sm"
275 href="<?= h(acp_site(znote_plugin_url($name, $plugin['page_list'][0]))) ?>"
276 target="_blank" rel="noopener"><?= t('acp.plg.view') ?></a>
277 <?php endif; ?>
278
279 <?php if ($running && znote_plugin_settings_has($name)): ?>
280 <a class="acp-btn acp-btn--ghost acp-btn--sm"
281 href="<?= h(acp_url('plugin_settings', array('plugin' => $name))) ?>">
282 <i class="fa fa-sliders"></i> <?= t_default('acp.plg.settings', 'Settings') ?>
283 </a>
284 <?php endif; ?>
285
286 <?php if (!$plugin['installed']): ?>
287
288 <form method="post" style="display:inline;">
289 <?= acp_csrf_field() ?>
290 <input type="hidden" name="plugin" value="<?= h($name) ?>">
291 <input type="hidden" name="action" value="install">
292 <button class="acp-btn acp-btn--green acp-btn--sm" type="submit" <?= $plugin['compatible'] ? '' : 'disabled' ?>>
293 <i class="fa fa-download"></i> <?= t('acp.plg.install') ?>
294 </button>
295 </form>
296
297 <?php else: ?>
298
299 <?php if ($plugin['update']): ?>
300 <form method="post" style="display:inline;">
301 <?= acp_csrf_field() ?>
302 <input type="hidden" name="plugin" value="<?= h($name) ?>">
303 <input type="hidden" name="action" value="update">
304 <button class="acp-btn acp-btn--amber acp-btn--sm" type="submit" <?= $plugin['compatible'] ? '' : 'disabled' ?>>
305 <i class="fa fa-arrow-up"></i> <?= t('acp.plg.update_to', ['version' => h($plugin['version'])]) ?>
306 </button>
307 </form>
308 <?php endif; ?>
309
310 <form method="post" style="display:inline;">
311 <?= acp_csrf_field() ?>
312 <input type="hidden" name="plugin" value="<?= h($name) ?>">
313 <input type="hidden" name="action" value="<?= $plugin['enabled'] ? 'disable' : 'enable' ?>">
314 <button class="acp-btn acp-btn--sm <?= $plugin['enabled'] ? '' : 'acp-btn--green' ?>" type="submit" <?= !$plugin['enabled'] && !$plugin['compatible'] ? 'disabled' : '' ?>>
315 <?= $plugin['enabled'] ? t('acp.plg.disable') : t('acp.plg.enable') ?>
316 </button>
317 </form>
318
319 <form method="post" style="display:inline;"
320 onsubmit="return confirm('<?= h(t('acp.plg.confirm_uninstall', ['plugin' => $plugin['name']])) ?>');">
321 <?= acp_csrf_field() ?>
322 <input type="hidden" name="plugin" value="<?= h($name) ?>">
323 <input type="hidden" name="action" value="uninstall">
324 <button class="acp-btn acp-btn--red acp-btn--sm" type="submit" title="<?= h(t('acp.plg.uninstall')) ?>">
325 <i class="fa fa-times"></i>
326 </button>
327 </form>
328
329 <?php endif; ?>
330
331 </td>
332 </tr>
333 <?php endforeach; ?>
334 </tbody>
335 </table>
336
337 <?php endif; ?>
338
339<?php acp_card_close(); ?>
340
341<?php acp_card_open(t('acp.plg.how_title'), ''); ?>
342 <p>
343 <strong><?= t('acp.plg.how_install') ?></strong>
344 <?= t('acp.plg.how_install_text', [
345 'folder' => '<code>plugins/</code>',
346 'install' => '<em>' . t('acp.plg.install') . '</em>',
347 ]) ?>
348 </p>
349 <p>
350 <strong><?= t('acp.plg.how_update') ?></strong>
351 <?= t('acp.plg.how_update_text', [
352 'json' => '<code>plugin.json</code>',
353 'update' => '<em>Update</em>',
354 ]) ?>
355 </p>
356 <p>
357 <strong><?= t('acp.plg.how_remove') ?></strong>
358 <?= t('acp.plg.how_remove_text', [
359 'disable' => '<em>' . t('acp.plg.disable') . '</em>',
360 'uninstall' => '<em>' . t('acp.plg.uninstall') . '</em>',
361 ]) ?>
362 </p>
363 <p>
364 <?= t('acp.plg.how_write', [
365 'json' => '<code>plugin.json</code>',
366 'readme' => '<code>plugins/README.md</code>',
367 'example' => '<code>plugins/shop_coupons/</code>',
368 ]) ?>
369 </p>
370<?php acp_card_close(); ?>
371