1<?php 2 3use dokuwiki\Extension\CLIPlugin; 4use dokuwiki\plugin\extension\Exception as ExtensionException; 5use dokuwiki\plugin\extension\Extension; 6use dokuwiki\plugin\extension\Local; 7use dokuwiki\plugin\extension\Repository; 8use splitbrain\phpcli\Colors; 9use splitbrain\phpcli\Exception; 10use splitbrain\phpcli\Options; 11use splitbrain\phpcli\TableFormatter; 12 13/** 14 * Class cli_plugin_extension 15 * 16 * Command Line component for the extension manager 17 * 18 * @license GPL2 19 * @author Andreas Gohr <andi@splitbrain.org> 20 */ 21class cli_plugin_extension extends CLIPlugin 22{ 23 /** @inheritdoc */ 24 protected function setup(Options $options) 25 { 26 // general setup 27 $options->useCompactHelp(); 28 $options->setHelp( 29 "Manage plugins and templates for this DokuWiki instance\n\n" . 30 "Status codes:\n" . 31 " i - installed\n" . 32 " b - bundled with DokuWiki\n" . 33 " g - installed via git\n" . 34 " d - disabled\n" . 35 " u - update available\n" . 36 " ☠ - security issue\n" . 37 " ⚠ - security warning\n" . 38 " ▽ - update message\n" 39 ); 40 41 // search 42 $options->registerCommand('search', 'Search for an extension'); 43 $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search'); 44 $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search'); 45 $options->registerArgument('query', 'The keyword(s) to search for', true, 'search'); 46 47 // list 48 $options->registerCommand('list', 'List installed extensions'); 49 $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list'); 50 $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list'); 51 52 // upgrade 53 $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions'); 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('Extension Repository API is not accessible, no remote info available!'); 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(); 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() 127 { 128 /* @var helper_plugin_extension_extension $ext */ 129 $ext = $this->loadHelper('extension_extension'); 130 $list = $this->getInstalledExtensions(); 131 132 $ok = 0; 133 foreach ($list as $extname) { 134 $ext->setExtension($extname); 135 $date = $ext->getInstalledVersion(); 136 $avail = $ext->getLastUpdate(); 137 if ($avail && $avail > $date && !$ext->isBundled()) { 138 $ok += $this->cmdInstall([$extname]); 139 } 140 } 141 142 return $ok; 143 } 144 145 /** 146 * Enable or disable one or more extensions 147 * 148 * @param bool $set 149 * @param string[] $extensions 150 * @return int 151 */ 152 protected function cmdEnable($set, $extensions) 153 { 154 /* @var helper_plugin_extension_extension $ext */ 155 $ext = $this->loadHelper('extension_extension'); 156 157 $ok = 0; 158 foreach ($extensions as $extname) { 159 $ext->setExtension($extname); 160 if (!$ext->isInstalled()) { 161 $this->error(sprintf('Extension %s is not installed', $ext->getID())); 162 ++$ok; 163 continue; 164 } 165 166 if ($set) { 167 $status = $ext->enable(); 168 $msg = 'msg_enabled'; 169 } else { 170 $status = $ext->disable(); 171 $msg = 'msg_disabled'; 172 } 173 174 if ($status !== true) { 175 $this->error($status); 176 ++$ok; 177 continue; 178 } else { 179 $this->success(sprintf($this->getLang($msg), $ext->getID())); 180 } 181 } 182 183 return $ok; 184 } 185 186 /** 187 * Uninstall one or more extensions 188 * 189 * @param string[] $extensions 190 * @return int 191 */ 192 protected function cmdUnInstall($extensions) 193 { 194 /* @var helper_plugin_extension_extension $ext */ 195 $ext = $this->loadHelper('extension_extension'); 196 197 $ok = 0; 198 foreach ($extensions as $extname) { 199 $ext->setExtension($extname); 200 if (!$ext->isInstalled()) { 201 $this->error(sprintf('Extension %s is not installed', $ext->getID())); 202 ++$ok; 203 continue; 204 } 205 206 $status = $ext->uninstall(); 207 if ($status) { 208 $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID())); 209 } else { 210 $this->error(sprintf($this->getLang('msg_delete_failed'), hsc($ext->getID()))); 211 $ok = 1; 212 } 213 } 214 215 return $ok; 216 } 217 218 /** 219 * Install one or more extensions 220 * 221 * @param string[] $extensions 222 * @return int 223 */ 224 protected function cmdInstall($extensions) 225 { 226 227 $ok = 0; 228 foreach ($extensions as $extname) { 229 $installed = []; 230 231 if (preg_match("/^https?:\/\//i", $extname)) { 232 try { 233 $installed = $ext->installFromURL($extname, true); 234 } catch (Exception $e) { 235 $this->error($e->getMessage()); 236 ++$ok; 237 } 238 } else { 239 $ext->setExtension($extname); 240 241 if (!$ext->getDownloadURL()) { 242 ++$ok; 243 $this->error( 244 sprintf('Could not find download for %s', $ext->getID()) 245 ); 246 continue; 247 } 248 249 try { 250 $installed = $ext->installOrUpdate(); 251 } catch (Exception $e) { 252 $this->error($e->getMessage()); 253 ++$ok; 254 } 255 } 256 257 foreach ($installed as $info) { 258 $this->success( 259 sprintf( 260 $this->getLang('msg_' . $info['type'] . '_' . $info['action'] . '_success'), 261 $info['base'] 262 ) 263 ); 264 } 265 } 266 return $ok; 267 } 268 269 /** 270 * Search for an extension 271 * 272 * @param string $query 273 * @param bool $showdetails 274 * @param int $max 275 * @return int 276 * @throws Exception 277 */ 278 protected function cmdSearch($query, $showdetails, $max) 279 { 280 $repo = Repository::getInstance(); 281 $result = $repo->searchExtensions($query); 282 if ($max) { 283 $result = array_slice($result, 0, $max); 284 } 285 286 $this->listExtensions($result, $showdetails); 287 return 0; 288 } 289 290 /** 291 * @param bool $showdetails 292 * @param string $filter 293 * @return int 294 * @throws Exception 295 */ 296 protected function cmdList($showdetails, $filter) 297 { 298 $this->listExtensions((new Local())->getExtensions(), $showdetails, $filter); 299 return 0; 300 } 301 302 /** 303 * List the given extensions 304 * 305 * @param Extension[] $list 306 * @param bool $details display details 307 * @param string $filter filter for this status 308 * @throws Exception 309 */ 310 protected function listExtensions($list, $details, $filter = '') 311 { 312 $tr = new TableFormatter($this->colors); 313 foreach ($list as $ext) { 314 315 $status = ''; 316 if ($ext->isInstalled()) { 317 $date = $ext->getInstalledVersion(); 318 $avail = $ext->getLastUpdate(); 319 $status = 'i'; 320 if ($avail && $avail > $date) { 321 $vcolor = Colors::C_RED; 322 $status .= 'u'; 323 } else { 324 $vcolor = Colors::C_GREEN; 325 } 326 if ($ext->isGitControlled()) $status = 'g'; 327 if ($ext->isBundled()) { 328 $status = 'b'; 329 $date = '<bundled>'; 330 $vcolor = null; 331 } 332 if ($ext->isEnabled()) { 333 $ecolor = Colors::C_BROWN; 334 } else { 335 $ecolor = Colors::C_DARKGRAY; 336 $status .= 'd'; 337 } 338 } else { 339 $ecolor = null; 340 $date = $ext->getLastUpdate(); 341 $vcolor = null; 342 } 343 344 if ($filter && strpos($status, $filter) === false) { 345 continue; 346 } 347 348 349 if ($ext->getSecurityIssue()) $status .= '☠'; 350 if ($ext->getSecurityWarning()) $status .= '⚠'; 351 if ($ext->getUpdateMessage()) $status .= '▽'; 352 353 echo $tr->format( 354 [20, 3, 12, '*'], 355 [ 356 $ext->getID(), 357 $status, 358 $date, 359 strip_tags(sprintf( 360 $this->getLang('extensionby'), 361 $ext->getDisplayName(), 362 $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE) 363 )) 364 ], 365 [ 366 $ecolor, 367 Colors::C_YELLOW, 368 $vcolor, 369 null, 370 ] 371 ); 372 373 374 if (!$details) continue; 375 376 echo $tr->format( 377 [5, '*'], 378 ['', $ext->getDescription()], 379 [null, Colors::C_CYAN] 380 ); 381 if ($ext->getSecurityWarning()) { 382 echo $tr->format( 383 [5, '*'], 384 ['', '⚠ ' . $ext->getSecurityWarning()], 385 [null, Colors::C_YELLOW] 386 ); 387 } 388 if ($ext->getSecurityIssue()) { 389 echo $tr->format( 390 [5, '*'], 391 ['', '☠ ' . $ext->getSecurityIssue()], 392 [null, Colors::C_LIGHTRED] 393 ); 394 } 395 if ($ext->getUpdateMessage()) { 396 echo $tr->format( 397 [5, '*'], 398 ['', '▽ ' . $ext->getUpdateMessage()], 399 [null, Colors::C_LIGHTBLUE] 400 ); 401 } 402 } 403 } 404} 405