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