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