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