1<?php
2
3use splitbrain\phpcli\Exception;
4use dokuwiki\Extension\CLIPlugin;
5use splitbrain\phpcli\Options;
6use splitbrain\phpcli\TableFormatter;
7use splitbrain\phpcli\Colors;
8
9/**
10 * Class cli_plugin_extension
11 *
12 * Command Line component for the extension manager
13 *
14 * @license GPL2
15 * @author Andreas Gohr <andi@splitbrain.org>
16 */
17class cli_plugin_extension extends CLIPlugin
18{
19    /** @inheritdoc */
20    protected function setup(Options $options)
21    {
22        // general setup
23        $options->setHelp(
24            "Manage plugins and templates for this DokuWiki instance\n\n" .
25            "Status codes:\n" .
26            "   i - installed\n" .
27            "   b - bundled with DokuWiki\n" .
28            "   g - installed via git\n" .
29            "   d - disabled\n" .
30            "   u - update available\n"
31        );
32
33        // search
34        $options->registerCommand('search', 'Search for an extension');
35        $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search');
36        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search');
37        $options->registerArgument('query', 'The keyword(s) to search for', true, 'search');
38
39        // list
40        $options->registerCommand('list', 'List installed extensions');
41        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list');
42        $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list');
43
44        // upgrade
45        $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions');
46
47        // install
48        $options->registerCommand('install', 'Install or upgrade extensions');
49        $options->registerArgument(
50            'extensions...',
51            'One or more extensions to install. Either by name or download URL',
52            true,
53            'install'
54        );
55
56        // uninstall
57        $options->registerCommand('uninstall', 'Uninstall a new extension');
58        $options->registerArgument('extensions...', 'One or more extensions to install', true, 'uninstall');
59
60        // enable
61        $options->registerCommand('enable', 'Enable installed extensions');
62        $options->registerArgument('extensions...', 'One or more extensions to enable', true, 'enable');
63
64        // disable
65        $options->registerCommand('disable', 'Disable installed extensions');
66        $options->registerArgument('extensions...', 'One or more extensions to disable', true, 'disable');
67    }
68
69    /** @inheritdoc */
70    protected function main(Options $options)
71    {
72        /** @var helper_plugin_extension_repository $repo */
73        $repo = plugin_load('helper', 'extension_repository');
74        if (!$repo->hasAccess(false)) {
75            $this->warning('Extension Repository API is not accessible, no remote info available!');
76        }
77
78        switch ($options->getCmd()) {
79            case 'list':
80                $ret = $this->cmdList($options->getOpt('verbose'), $options->getOpt('filter', ''));
81                break;
82            case 'search':
83                $ret = $this->cmdSearch(
84                    implode(' ', $options->getArgs()),
85                    $options->getOpt('verbose'),
86                    (int)$options->getOpt('max', 10)
87                );
88                break;
89            case 'install':
90                $ret = $this->cmdInstall($options->getArgs());
91                break;
92            case 'uninstall':
93                $ret = $this->cmdUnInstall($options->getArgs());
94                break;
95            case 'enable':
96                $ret = $this->cmdEnable(true, $options->getArgs());
97                break;
98            case 'disable':
99                $ret = $this->cmdEnable(false, $options->getArgs());
100                break;
101            case 'upgrade':
102                $ret = $this->cmdUpgrade();
103                break;
104            default:
105                echo $options->help();
106                $ret = 0;
107        }
108
109        exit($ret);
110    }
111
112    /**
113     * Upgrade all extensions
114     *
115     * @return int
116     */
117    protected function cmdUpgrade()
118    {
119        /* @var helper_plugin_extension_extension $ext */
120        $ext = $this->loadHelper('extension_extension');
121        $list = $this->getInstalledExtensions();
122
123        $ok = 0;
124        foreach ($list as $extname) {
125            $ext->setExtension($extname);
126            $date = $ext->getInstalledVersion();
127            $avail = $ext->getLastUpdate();
128            if ($avail && $avail > $date && !$ext->isBundled()) {
129                $ok += $this->cmdInstall([$extname]);
130            }
131        }
132
133        return $ok;
134    }
135
136    /**
137     * Enable or disable one or more extensions
138     *
139     * @param bool $set
140     * @param string[] $extensions
141     * @return int
142     */
143    protected function cmdEnable($set, $extensions)
144    {
145        /* @var helper_plugin_extension_extension $ext */
146        $ext = $this->loadHelper('extension_extension');
147
148        $ok = 0;
149        foreach ($extensions as $extname) {
150            $ext->setExtension($extname);
151            if (!$ext->isInstalled()) {
152                $this->error(sprintf('Extension %s is not installed', $ext->getID()));
153                ++$ok;
154                continue;
155            }
156
157            if ($set) {
158                $status = $ext->enable();
159                $msg = 'msg_enabled';
160            } else {
161                $status = $ext->disable();
162                $msg = 'msg_disabled';
163            }
164
165            if ($status !== true) {
166                $this->error($status);
167                ++$ok;
168                continue;
169            } else {
170                $this->success(sprintf($this->getLang($msg), $ext->getID()));
171            }
172        }
173
174        return $ok;
175    }
176
177    /**
178     * Uninstall one or more extensions
179     *
180     * @param string[] $extensions
181     * @return int
182     */
183    protected function cmdUnInstall($extensions)
184    {
185        /* @var helper_plugin_extension_extension $ext */
186        $ext = $this->loadHelper('extension_extension');
187
188        $ok = 0;
189        foreach ($extensions as $extname) {
190            $ext->setExtension($extname);
191            if (!$ext->isInstalled()) {
192                $this->error(sprintf('Extension %s is not installed', $ext->getID()));
193                ++$ok;
194                continue;
195            }
196
197            $status = $ext->uninstall();
198            if ($status) {
199                $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID()));
200            } else {
201                $this->error(sprintf($this->getLang('msg_delete_failed'), hsc($ext->getID())));
202                $ok = 1;
203            }
204        }
205
206        return $ok;
207    }
208
209    /**
210     * Install one or more extensions
211     *
212     * @param string[] $extensions
213     * @return int
214     */
215    protected function cmdInstall($extensions)
216    {
217        /* @var helper_plugin_extension_extension $ext */
218        $ext = $this->loadHelper('extension_extension');
219
220        $ok = 0;
221        foreach ($extensions as $extname) {
222            $installed = [];
223
224            if (preg_match("/^https?:\/\//i", $extname)) {
225                try {
226                    $installed = $ext->installFromURL($extname, true);
227                } catch (Exception $e) {
228                    $this->error($e->getMessage());
229                    ++$ok;
230                }
231            } else {
232                $ext->setExtension($extname);
233
234                if (!$ext->getDownloadURL()) {
235                    ++$ok;
236                    $this->error(
237                        sprintf('Could not find download for %s', $ext->getID())
238                    );
239                    continue;
240                }
241
242                try {
243                    $installed = $ext->installOrUpdate();
244                } catch (Exception $e) {
245                    $this->error($e->getMessage());
246                    ++$ok;
247                }
248            }
249
250            foreach ($installed as $info) {
251                $this->success(
252                    sprintf(
253                        $this->getLang('msg_' . $info['type'] . '_' . $info['action'] . '_success'),
254                        $info['base']
255                    )
256                );
257            }
258        }
259        return $ok;
260    }
261
262    /**
263     * Search for an extension
264     *
265     * @param string $query
266     * @param bool $showdetails
267     * @param int $max
268     * @return int
269     * @throws Exception
270     */
271    protected function cmdSearch($query, $showdetails, $max)
272    {
273        /** @var helper_plugin_extension_repository $repository */
274        $repository = $this->loadHelper('extension_repository');
275        $result = $repository->search($query);
276        if ($max) {
277            $result = array_slice($result, 0, $max);
278        }
279
280        $this->listExtensions($result, $showdetails);
281        return 0;
282    }
283
284    /**
285     * @param bool $showdetails
286     * @param string $filter
287     * @return int
288     * @throws Exception
289     */
290    protected function cmdList($showdetails, $filter)
291    {
292        $list = $this->getInstalledExtensions();
293        $this->listExtensions($list, $showdetails, $filter);
294
295        return 0;
296    }
297
298    /**
299     * Get all installed extensions
300     *
301     * @return array
302     */
303    protected function getInstalledExtensions()
304    {
305        /** @var Doku_Plugin_Controller $plugin_controller */
306        global $plugin_controller;
307        $pluginlist = $plugin_controller->getList('', true);
308        $tpllist = glob(DOKU_INC . 'lib/tpl/*', GLOB_ONLYDIR);
309        $tpllist = array_map(static fn($path) => 'template:' . basename($path), $tpllist);
310
311        $list = array_merge($pluginlist, $tpllist);
312        sort($list);
313        return $list;
314    }
315
316    /**
317     * List the given extensions
318     *
319     * @param string[] $list
320     * @param bool $details display details
321     * @param string $filter filter for this status
322     * @throws Exception
323     */
324    protected function listExtensions($list, $details, $filter = '')
325    {
326        /** @var helper_plugin_extension_extension $ext */
327        $ext = $this->loadHelper('extension_extension');
328        $tr = new TableFormatter($this->colors);
329
330
331        foreach ($list as $name) {
332            $ext->setExtension($name);
333
334            $status = '';
335            if ($ext->isInstalled()) {
336                $date = $ext->getInstalledVersion();
337                $avail = $ext->getLastUpdate();
338                $status = 'i';
339                if ($avail && $avail > $date) {
340                    $vcolor = Colors::C_RED;
341                    $status .= 'u';
342                } else {
343                    $vcolor = Colors::C_GREEN;
344                }
345                if ($ext->isGitControlled()) $status = 'g';
346                if ($ext->isBundled()) $status = 'b';
347                if ($ext->isEnabled()) {
348                    $ecolor = Colors::C_BROWN;
349                } else {
350                    $ecolor = Colors::C_DARKGRAY;
351                    $status .= 'd';
352                }
353            } else {
354                $ecolor = null;
355                $date = $ext->getLastUpdate();
356                $vcolor = null;
357            }
358
359            if ($filter && strpos($status, $filter) === false) {
360                continue;
361            }
362
363            echo $tr->format(
364                [20, 3, 12, '*'],
365                [
366                    $ext->getID(),
367                    $status,
368                    $date,
369                    strip_tags(sprintf(
370                        $this->getLang('extensionby'),
371                        $ext->getDisplayName(),
372                        $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE)
373                    ))
374                ],
375                [
376                    $ecolor,
377                    Colors::C_YELLOW,
378                    $vcolor,
379                    null,
380                ]
381            );
382
383            if (!$details) continue;
384
385            echo $tr->format(
386                [5, '*'],
387                ['', $ext->getDescription()],
388                [null, Colors::C_CYAN]
389            );
390        }
391    }
392}
393