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