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