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