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::symbol(Notice::SECURITY) . " - security issue\n" .
34            "   b - bundled with DokuWiki        " . Notice::symbol(Notice::ERROR) . " - extension error\n" .
35            "   g - installed via git            " . Notice::symbol(Notice::WARNING) . " - extension warning\n" .
36            "   d - disabled                     " . Notice::symbol(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        $extensions = (new Local())->getExtensions();
262        // initialize remote data in one go
263        Repository::getInstance()->initExtensions(array_keys($extensions));
264
265        $this->listExtensions($extensions, $showdetails, $filter);
266        return 0;
267    }
268
269    /**
270     * List the given extensions
271     *
272     * @param Extension[] $list
273     * @param bool $details display details
274     * @param string $filter filter for this status
275     * @throws Exception
276     * @todo break into smaller methods
277     */
278    protected function listExtensions($list, $details, $filter = '')
279    {
280        $tr = new TableFormatter($this->colors);
281        foreach ($list as $ext) {
282            $status = '';
283            if ($ext->isInstalled()) {
284                $date = $ext->getInstalledVersion();
285                $avail = $ext->getLastUpdate();
286                $status = 'i';
287                if ($avail && $avail > $date) {
288                    $vcolor = Colors::C_RED;
289                    $status .= 'u';
290                } else {
291                    $vcolor = Colors::C_GREEN;
292                }
293                if ($ext->isGitControlled()) $status = 'g';
294                if ($ext->isBundled()) {
295                    $status = 'b';
296                    $date = '<bundled>';
297                    $vcolor = null;
298                }
299                if ($ext->isEnabled()) {
300                    $ecolor = Colors::C_BROWN;
301                } else {
302                    $ecolor = Colors::C_DARKGRAY;
303                    $status .= 'd';
304                }
305            } else {
306                $ecolor = null;
307                $date = $ext->getLastUpdate();
308                $vcolor = null;
309            }
310
311            if ($filter && strpos($status, $filter) === false) {
312                continue;
313            }
314
315            $notices = Notice::list($ext);
316            if ($notices[Notice::SECURITY]) $status .= Notice::symbol(Notice::SECURITY);
317            if ($notices[Notice::ERROR]) $status .= Notice::symbol(Notice::ERROR);
318            if ($notices[Notice::WARNING]) $status .= Notice::symbol(Notice::WARNING);
319            if ($notices[Notice::INFO]) $status .= Notice::symbol(Notice::INFO);
320
321            echo $tr->format(
322                [20, 5, 12, '*'],
323                [
324                    $ext->getID(),
325                    $status,
326                    $date,
327                    strip_tags(sprintf(
328                        $this->getLang('extensionby'),
329                        $ext->getDisplayName(),
330                        $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE)
331                    ))
332                ],
333                [
334                    $ecolor,
335                    Colors::C_YELLOW,
336                    $vcolor,
337                    null,
338                ]
339            );
340
341
342            if (!$details) continue;
343
344            echo $tr->format(
345                [7, '*'],
346                ['', $ext->getDescription()],
347                [null, Colors::C_CYAN]
348            );
349            foreach ($notices as $type => $msgs) {
350                if (!$msgs) continue;
351                foreach ($msgs as $msg) {
352                    echo $tr->format(
353                        [7, '*'],
354                        ['', Notice::symbol($type) . ' ' . $msg],
355                        [null, Colors::C_LIGHTBLUE]
356                    );
357                }
358            }
359        }
360    }
361}
362