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