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