1 <?php
2 
3 use dokuwiki\Extension\CLIPlugin;
4 use dokuwiki\plugin\extension\Exception as ExtensionException;
5 use dokuwiki\plugin\extension\Extension;
6 use dokuwiki\plugin\extension\Installer;
7 use dokuwiki\plugin\extension\Local;
8 use dokuwiki\plugin\extension\Notice;
9 use dokuwiki\plugin\extension\Repository;
10 use splitbrain\phpcli\Colors;
11 use splitbrain\phpcli\Exception;
12 use splitbrain\phpcli\Options;
13 use 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  */
23 class 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