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\Notice; 9use dokuwiki\plugin\extension\Repository; 10use splitbrain\phpcli\Colors; 11use splitbrain\phpcli\Exception; 12use splitbrain\phpcli\Options; 13use splitbrain\phpcli\TableFormatter; 14 15/** 16 * Class cli_plugin_extension 17 * 18 * Command Line component for the extension manager 19 * 20 * @license GPL2 21 * @author Andreas Gohr <andi@splitbrain.org> 22 */ 23class cli_plugin_extension extends CLIPlugin 24{ 25 /** @inheritdoc */ 26 protected function setup(Options $options) 27 { 28 // general setup 29 $options->useCompactHelp(); 30 $options->setHelp( 31 "Manage plugins and templates for this DokuWiki instance\n\n" . 32 "Status codes:\n" . 33 " i - installed " . Notice::symbol(Notice::SECURITY) . " - security issue\n" . 34 " b - bundled with DokuWiki " . Notice::symbol(Notice::ERROR) . " - extension error\n" . 35 " g - installed via git " . Notice::symbol(Notice::WARNING) . " - extension warning\n" . 36 " d - disabled " . Notice::symbol(Notice::INFO) . " - extension info\n" . 37 " u - update available\n" 38 ); 39 40 // search 41 $options->registerCommand('search', 'Search for an extension'); 42 $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search'); 43 $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search'); 44 $options->registerArgument('query', 'The keyword(s) to search for', true, 'search'); 45 46 // list 47 $options->registerCommand('list', 'List installed extensions'); 48 $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list'); 49 $options->registerOption('filter', 'Filter by this status', 'f', 'status', 'list'); 50 51 // upgrade 52 $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions'); 53 $options->registerOption('git-overwrite', 'Do not skip git-controlled extensions', 'g', false, 'upgrade'); 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($e->getMessage()); 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($options->getOpt('git-overwrite', false)); 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($gitOverwrite) 127 { 128 $local = new Local(); 129 $extensions = []; 130 foreach ($local->getExtensions() as $ext) { 131 if ($ext->isGitControlled() && !$gitOverwrite) continue; // skip git controlled extensions 132 if ($ext->isUpdateAvailable()) $extensions[] = $ext->getID(); 133 } 134 return $this->cmdInstall($extensions); 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 $ok = 0; 147 foreach ($extensions as $extname) { 148 $ext = Extension::createFromId($extname); 149 150 try { 151 if ($set) { 152 $ext->enable(); 153 $msg = 'msg_enabled'; 154 } else { 155 $ext->disable(); 156 $msg = 'msg_disabled'; 157 } 158 $this->success(sprintf($this->getLang($msg), $ext->getID())); 159 } catch (ExtensionException $e) { 160 $this->error($e->getMessage()); 161 ++$ok; 162 continue; 163 } 164 } 165 166 return $ok; 167 } 168 169 /** 170 * Uninstall one or more extensions 171 * 172 * @param string[] $extensions 173 * @return int 174 */ 175 protected function cmdUnInstall($extensions) 176 { 177 $installer = new Installer(); 178 179 $ok = 0; 180 foreach ($extensions as $extname) { 181 $ext = Extension::createFromId($extname); 182 183 try { 184 $installer->uninstall($ext); 185 $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID())); 186 } catch (ExtensionException $e) { 187 $this->debug($e->getTraceAsString()); 188 $this->error($e->getMessage()); 189 $ok++; // error code is number of failed uninstalls 190 } 191 } 192 return $ok; 193 } 194 195 /** 196 * Install one or more extensions 197 * 198 * @param string[] $extensions 199 * @return int 200 */ 201 protected function cmdInstall($extensions) 202 { 203 $ok = 0; 204 foreach ($extensions as $extname) { 205 $installer = new Installer(true); 206 207 try { 208 if (preg_match("/^https?:\/\//i", $extname)) { 209 $installer->installFromURL($extname, true); 210 } else { 211 $installer->installFromId($extname); 212 } 213 } catch (ExtensionException $e) { 214 $this->debug($e->getTraceAsString()); 215 $this->error($e->getMessage()); 216 $ok++; // error code is number of failed installs 217 } 218 219 $processed = $installer->getProcessed(); 220 foreach ($processed as $id => $status) { 221 if ($status == Installer::STATUS_INSTALLED) { 222 $this->success(sprintf($this->getLang('msg_install_success'), $id)); 223 } elseif ($status == Installer::STATUS_UPDATED) { 224 $this->success(sprintf($this->getLang('msg_update_success'), $id)); 225 } 226 } 227 } 228 229 return $ok; 230 } 231 232 /** 233 * Search for an extension 234 * 235 * @param string $query 236 * @param bool $showdetails 237 * @param int $max 238 * @return int 239 * @throws Exception 240 */ 241 protected function cmdSearch($query, $showdetails, $max) 242 { 243 $repo = Repository::getInstance(); 244 $result = $repo->searchExtensions($query); 245 if ($max) { 246 $result = array_slice($result, 0, $max); 247 } 248 249 $this->listExtensions($result, $showdetails); 250 return 0; 251 } 252 253 /** 254 * @param bool $showdetails 255 * @param string $filter 256 * @return int 257 * @throws Exception 258 */ 259 protected function cmdList($showdetails, $filter) 260 { 261 $extensions = (new Local())->getExtensions(); 262 // initialize remote data in one go 263 Repository::getInstance()->initExtensions(array_keys($extensions)); 264 265 $this->listExtensions($extensions, $showdetails, $filter); 266 return 0; 267 } 268 269 /** 270 * List the given extensions 271 * 272 * @param Extension[] $list 273 * @param bool $details display details 274 * @param string $filter filter for this status 275 * @throws Exception 276 * @todo break into smaller methods 277 */ 278 protected function listExtensions($list, $details, $filter = '') 279 { 280 $tr = new TableFormatter($this->colors); 281 foreach ($list as $ext) { 282 $status = ''; 283 if ($ext->isInstalled()) { 284 $date = $ext->getInstalledVersion(); 285 $avail = $ext->getLastUpdate(); 286 $status = 'i'; 287 if ($avail && $avail > $date) { 288 $vcolor = Colors::C_RED; 289 $status .= 'u'; 290 } else { 291 $vcolor = Colors::C_GREEN; 292 } 293 if ($ext->isGitControlled()) $status = 'g'; 294 if ($ext->isBundled()) { 295 $status = 'b'; 296 $date = '<bundled>'; 297 $vcolor = null; 298 } 299 if ($ext->isEnabled()) { 300 $ecolor = Colors::C_BROWN; 301 } else { 302 $ecolor = Colors::C_DARKGRAY; 303 $status .= 'd'; 304 } 305 } else { 306 $ecolor = null; 307 $date = $ext->getLastUpdate(); 308 $vcolor = null; 309 } 310 311 if ($filter && strpos($status, $filter) === false) { 312 continue; 313 } 314 315 $notices = Notice::list($ext); 316 if ($notices[Notice::SECURITY]) $status .= Notice::symbol(Notice::SECURITY); 317 if ($notices[Notice::ERROR]) $status .= Notice::symbol(Notice::ERROR); 318 if ($notices[Notice::WARNING]) $status .= Notice::symbol(Notice::WARNING); 319 if ($notices[Notice::INFO]) $status .= Notice::symbol(Notice::INFO); 320 321 echo $tr->format( 322 [20, 5, 12, '*'], 323 [ 324 $ext->getID(), 325 $status, 326 $date, 327 strip_tags(sprintf( 328 $this->getLang('extensionby'), 329 $ext->getDisplayName(), 330 $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE) 331 )) 332 ], 333 [ 334 $ecolor, 335 Colors::C_YELLOW, 336 $vcolor, 337 null, 338 ] 339 ); 340 341 342 if (!$details) continue; 343 344 echo $tr->format( 345 [7, '*'], 346 ['', $ext->getDescription()], 347 [null, Colors::C_CYAN] 348 ); 349 foreach ($notices as $type => $msgs) { 350 if (!$msgs) continue; 351 foreach ($msgs as $msg) { 352 echo $tr->format( 353 [7, '*'], 354 ['', Notice::symbol($type) . ' ' . $msg], 355 [null, Colors::C_LIGHTBLUE] 356 ); 357 } 358 } 359 } 360 } 361} 362