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