1<?php 2 3use dokuwiki\Extension\Plugin; 4 5/** 6 * DokuWiki Plugin extension (Helper Component) 7 * 8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9 * @author Michael Hamann <michael@content-space.de> 10 */ 11/** 12 * Class helper_plugin_extension_list takes care of creating a HTML list of extensions 13 */ 14class helper_plugin_extension_list extends Plugin 15{ 16 protected $form = ''; 17 /** @var helper_plugin_extension_gui */ 18 protected $gui; 19 20 /** 21 * Constructor 22 * 23 * loads additional helpers 24 */ 25 public function __construct() 26 { 27 $this->gui = plugin_load('helper', 'extension_gui'); 28 } 29 30 /** 31 * Initialize the extension table form 32 */ 33 public function startForm() 34 { 35 $this->form .= '<ul class="extensionList">'; 36 } 37 38 /** 39 * Build single row of extension table 40 * 41 * @param helper_plugin_extension_extension $extension The extension that shall be added 42 * @param bool $showinfo Show the info area 43 */ 44 public function addRow(helper_plugin_extension_extension $extension, $showinfo = false) 45 { 46 $this->startRow($extension); 47 $this->populateColumn('legend', $this->makeLegend($extension, $showinfo)); 48 $this->populateColumn('actions', $this->makeActions($extension)); 49 $this->endRow(); 50 } 51 52 /** 53 * Adds a header to the form 54 * 55 * @param string $id The id of the header 56 * @param string $header The content of the header 57 * @param int $level The level of the header 58 */ 59 public function addHeader($id, $header, $level = 2) 60 { 61 $this->form .= '<h' . $level . ' id="' . $id . '">' . hsc($header) . '</h' . $level . '>' . DOKU_LF; 62 } 63 64 /** 65 * Adds a paragraph to the form 66 * 67 * @param string $data The content 68 */ 69 public function addParagraph($data) 70 { 71 $this->form .= '<p>' . hsc($data) . '</p>' . DOKU_LF; 72 } 73 74 /** 75 * Add hidden fields to the form with the given data 76 * 77 * @param array $data key-value list of fields and their values to add 78 */ 79 public function addHidden(array $data) 80 { 81 $this->form .= '<div class="no">'; 82 foreach ($data as $key => $value) { 83 $this->form .= '<input type="hidden" name="' . hsc($key) . '" value="' . hsc($value) . '" />'; 84 } 85 $this->form .= '</div>' . DOKU_LF; 86 } 87 88 /** 89 * Add closing tags 90 */ 91 public function endForm() 92 { 93 $this->form .= '</ul>'; 94 } 95 96 /** 97 * Show message when no results are found 98 */ 99 public function nothingFound() 100 { 101 global $lang; 102 $this->form .= '<li class="notfound">' . $lang['nothingfound'] . '</li>'; 103 } 104 105 /** 106 * Print the form 107 * 108 * @param bool $returnonly whether to return html or print 109 */ 110 public function render($returnonly = false) 111 { 112 if ($returnonly) return $this->form; 113 echo $this->form; 114 } 115 116 /** 117 * Start the HTML for the row for the extension 118 * 119 * @param helper_plugin_extension_extension $extension The extension 120 */ 121 private function startRow(helper_plugin_extension_extension $extension) 122 { 123 $this->form .= '<li id="extensionplugin__' . hsc($extension->getID()) . 124 '" class="' . $this->makeClass($extension) . '">'; 125 } 126 127 /** 128 * Add a column with the given class and content 129 * @param string $class The class name 130 * @param string $html The content 131 */ 132 private function populateColumn($class, $html) 133 { 134 $this->form .= '<div class="' . $class . ' col">' . $html . '</div>' . DOKU_LF; 135 } 136 137 /** 138 * End the row 139 */ 140 private function endRow() 141 { 142 $this->form .= '</li>' . DOKU_LF; 143 } 144 145 /** 146 * Generate the link to the plugin homepage 147 * 148 * @param helper_plugin_extension_extension $extension The extension 149 * @return string The HTML code 150 */ 151 public function makeHomepageLink(helper_plugin_extension_extension $extension) 152 { 153 global $conf; 154 $url = $extension->getURL(); 155 if (strtolower(parse_url($url, PHP_URL_HOST)) == 'www.dokuwiki.org') { 156 $linktype = 'interwiki'; 157 } else { 158 $linktype = 'extern'; 159 } 160 $param = [ 161 'href' => $url, 162 'title' => $url, 163 'class' => ($linktype == 'extern') ? 'urlextern' : 'interwiki iw_doku', 164 'target' => $conf['target'][$linktype], 165 'rel' => ($linktype == 'extern') ? 'noopener' : '' 166 ]; 167 if ($linktype == 'extern' && $conf['relnofollow']) { 168 $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']); 169 } 170 $html = ' <a ' . buildAttributes($param, true) . '>' . 171 $this->getLang('homepage_link') . '</a>'; 172 return $html; 173 } 174 175 /** 176 * Generate the class name for the row of the extension 177 * 178 * @param helper_plugin_extension_extension $extension The extension object 179 * @return string The class name 180 */ 181 public function makeClass(helper_plugin_extension_extension $extension) 182 { 183 $class = ($extension->isTemplate()) ? 'template' : 'plugin'; 184 if ($extension->isInstalled()) { 185 $class .= ' installed'; 186 $class .= ($extension->isEnabled()) ? ' enabled' : ' disabled'; 187 if ($extension->updateAvailable()) $class .= ' updatable'; 188 } 189 if (!$extension->canModify()) $class .= ' notselect'; 190 if ($extension->isProtected()) $class .= ' protected'; 191 //if($this->showinfo) $class.= ' showinfo'; 192 return $class; 193 } 194 195 /** 196 * Generate a link to the author of the extension 197 * 198 * @param helper_plugin_extension_extension $extension The extension object 199 * @return string The HTML code of the link 200 */ 201 public function makeAuthor(helper_plugin_extension_extension $extension) 202 { 203 if ($extension->getAuthor()) { 204 $mailid = $extension->getEmailID(); 205 if ($mailid) { 206 $url = $this->gui->tabURL('search', ['q' => 'authorid:' . $mailid]); 207 $html = '<a href="' . $url . '" class="author" title="' . $this->getLang('author_hint') . '" >' . 208 '<img src="//www.gravatar.com/avatar/' . $mailid . 209 '?s=20&d=mm" width="20" height="20" alt="" /> ' . 210 hsc($extension->getAuthor()) . '</a>'; 211 } else { 212 $html = '<span class="author">' . hsc($extension->getAuthor()) . '</span>'; 213 } 214 $html = '<bdi>' . $html . '</bdi>'; 215 } else { 216 $html = '<em class="author">' . $this->getLang('unknown_author') . '</em>' . DOKU_LF; 217 } 218 return $html; 219 } 220 221 /** 222 * Get the link and image tag for the screenshot/thumbnail 223 * 224 * @param helper_plugin_extension_extension $extension The extension object 225 * @return string The HTML code 226 */ 227 public function makeScreenshot(helper_plugin_extension_extension $extension) 228 { 229 $screen = $extension->getScreenshotURL(); 230 $thumb = $extension->getThumbnailURL(); 231 232 if ($screen) { 233 // use protocol independent URLs for images coming from us #595 234 $screen = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $screen); 235 $thumb = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $thumb); 236 237 $title = sprintf($this->getLang('screenshot'), hsc($extension->getDisplayName())); 238 $img = '<a href="' . hsc($screen) . '" target="_blank" class="extension_screenshot">' . 239 '<img alt="' . $title . '" width="120" height="70" src="' . hsc($thumb) . '" />' . 240 '</a>'; 241 } elseif ($extension->isTemplate()) { 242 $img = '<img alt="" width="120" height="70" src="' . DOKU_BASE . 243 'lib/plugins/extension/images/template.png" />'; 244 } else { 245 $img = '<img alt="" width="120" height="70" src="' . DOKU_BASE . 246 'lib/plugins/extension/images/plugin.png" />'; 247 } 248 $html = '<div class="screenshot" >' . $img . '<span></span></div>' . DOKU_LF; 249 return $html; 250 } 251 252 /** 253 * Extension main description 254 * 255 * @param helper_plugin_extension_extension $extension The extension object 256 * @param bool $showinfo Show the info section 257 * @return string The HTML code 258 */ 259 public function makeLegend(helper_plugin_extension_extension $extension, $showinfo = false) 260 { 261 $html = '<div>'; 262 $html .= '<h2>'; 263 $html .= sprintf( 264 $this->getLang('extensionby'), 265 '<bdi>' . hsc($extension->getDisplayName()) . '</bdi>', 266 $this->makeAuthor($extension) 267 ); 268 $html .= '</h2>' . DOKU_LF; 269 270 $html .= $this->makeScreenshot($extension); 271 272 $popularity = $extension->getPopularity(); 273 if ($popularity !== false && !$extension->isBundled()) { 274 $popularityText = sprintf($this->getLang('popularity'), round($popularity * 100, 2)); 275 $html .= '<div class="popularity" title="' . $popularityText . '">' . 276 '<div style="width: ' . ($popularity * 100) . '%;">' . 277 '<span class="a11y">' . $popularityText . '</span>' . 278 '</div></div>' . DOKU_LF; 279 } 280 281 if ($extension->getDescription()) { 282 $html .= '<p><bdi>'; 283 $html .= hsc($extension->getDescription()) . ' '; 284 $html .= '</bdi></p>' . DOKU_LF; 285 } 286 287 $html .= $this->makeLinkbar($extension); 288 289 if ($showinfo) { 290 $url = $this->gui->tabURL(''); 291 $class = 'close'; 292 } else { 293 $url = $this->gui->tabURL('', ['info' => $extension->getID()]); 294 $class = ''; 295 } 296 $html .= ' <a href="' . $url . '#extensionplugin__' . $extension->getID() . 297 '" class="info ' . $class . '" title="' . $this->getLang('btn_info') . 298 '" data-extid="' . $extension->getID() . '">' . $this->getLang('btn_info') . '</a>'; 299 300 if ($showinfo) { 301 $html .= $this->makeInfo($extension); 302 } 303 $html .= $this->makeNoticeArea($extension); 304 $html .= '</div>' . DOKU_LF; 305 return $html; 306 } 307 308 /** 309 * Generate the link bar HTML code 310 * 311 * @param helper_plugin_extension_extension $extension The extension instance 312 * @return string The HTML code 313 */ 314 public function makeLinkbar(helper_plugin_extension_extension $extension) 315 { 316 global $conf; 317 $html = '<div class="linkbar">'; 318 $html .= $this->makeHomepageLink($extension); 319 320 $bugtrackerURL = $extension->getBugtrackerURL(); 321 if ($bugtrackerURL) { 322 if (strtolower(parse_url($bugtrackerURL, PHP_URL_HOST)) == 'www.dokuwiki.org') { 323 $linktype = 'interwiki'; 324 } else { 325 $linktype = 'extern'; 326 } 327 $param = [ 328 'href' => $bugtrackerURL, 329 'title' => $bugtrackerURL, 330 'class' => 'bugs', 331 'target' => $conf['target'][$linktype], 332 'rel' => ($linktype == 'extern') ? 'noopener' : '' 333 ]; 334 if ($conf['relnofollow']) { 335 $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']); 336 } 337 $html .= ' <a ' . buildAttributes($param, true) . '>' . 338 $this->getLang('bugs_features') . '</a>'; 339 } 340 if ($extension->getTags()) { 341 $first = true; 342 $html .= ' <span class="tags">' . $this->getLang('tags') . ' '; 343 foreach ($extension->getTags() as $tag) { 344 if (!$first) { 345 $html .= ', '; 346 } else { 347 $first = false; 348 } 349 $url = $this->gui->tabURL('search', ['q' => 'tag:' . $tag]); 350 $html .= '<bdi><a href="' . $url . '">' . hsc($tag) . '</a></bdi>'; 351 } 352 $html .= '</span>'; 353 } 354 $html .= '</div>' . DOKU_LF; 355 return $html; 356 } 357 358 /** 359 * Notice area 360 * 361 * @param helper_plugin_extension_extension $extension The extension 362 * @return string The HTML code 363 */ 364 public function makeNoticeArea(helper_plugin_extension_extension $extension) 365 { 366 $html = ''; 367 $missing_dependencies = $extension->getMissingDependencies(); 368 if (!empty($missing_dependencies)) { 369 $html .= '<div class="msg error">' . 370 sprintf( 371 $this->getLang('missing_dependency'), 372 '<bdi>' . implode(', ', $missing_dependencies) . '</bdi>' 373 ) . 374 '</div>'; 375 } 376 if ($extension->isInWrongFolder()) { 377 $html .= '<div class="msg error">' . 378 sprintf( 379 $this->getLang('wrong_folder'), 380 '<bdi>' . hsc($extension->getInstallName()) . '</bdi>', 381 '<bdi>' . hsc($extension->getBase()) . '</bdi>' 382 ) . 383 '</div>'; 384 } 385 if (($securityissue = $extension->getSecurityIssue()) !== false) { 386 $html .= '<div class="msg error">' . 387 sprintf($this->getLang('security_issue'), '<bdi>' . hsc($securityissue) . '</bdi>') . 388 '</div>'; 389 } 390 if (($securitywarning = $extension->getSecurityWarning()) !== false) { 391 $html .= '<div class="msg notify">' . 392 sprintf($this->getLang('security_warning'), '<bdi>' . hsc($securitywarning) . '</bdi>') . 393 '</div>'; 394 } 395 if (($updateMessage = $extension->getUpdateMessage()) !== false) { 396 $html .= '<div class="msg notify">' . 397 sprintf($this->getLang('update_message'), '<bdi>' . hsc($updateMessage) . '</bdi>') . 398 '</div>'; 399 } 400 if ($extension->updateAvailable()) { 401 $html .= '<div class="msg notify">' . 402 sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())) . 403 '</div>'; 404 } 405 if ($extension->hasDownloadURLChanged()) { 406 $html .= '<div class="msg notify">' . 407 sprintf( 408 $this->getLang('url_change'), 409 '<bdi>' . hsc($extension->getDownloadURL()) . '</bdi>', 410 '<bdi>' . hsc($extension->getLastDownloadURL()) . '</bdi>' 411 ) . 412 '</div>'; 413 } 414 return $html . DOKU_LF; 415 } 416 417 /** 418 * Create a link from the given URL 419 * 420 * Shortens the URL for display 421 * 422 * @param string $url 423 * @return string HTML link 424 */ 425 public function shortlink($url) 426 { 427 $link = parse_url($url); 428 429 $base = $link['host']; 430 if (!empty($link['port'])) $base .= $base . ':' . $link['port']; 431 $long = $link['path']; 432 if (!empty($link['query'])) $long .= $link['query']; 433 434 $name = shorten($base, $long, 55); 435 436 $html = '<a href="' . hsc($url) . '" class="urlextern">' . hsc($name) . '</a>'; 437 return $html; 438 } 439 440 /** 441 * Plugin/template details 442 * 443 * @param helper_plugin_extension_extension $extension The extension 444 * @return string The HTML code 445 */ 446 public function makeInfo(helper_plugin_extension_extension $extension) 447 { 448 $default = $this->getLang('unknown'); 449 $html = '<dl class="details">'; 450 451 $html .= '<dt>' . $this->getLang('status') . '</dt>'; 452 $html .= '<dd>' . $this->makeStatus($extension) . '</dd>'; 453 454 if ($extension->getDonationURL()) { 455 $html .= '<dt>' . $this->getLang('donate') . '</dt>'; 456 $html .= '<dd>'; 457 $html .= '<a href="' . $extension->getDonationURL() . '" class="donate">' . 458 $this->getLang('donate_action') . '</a>'; 459 $html .= '</dd>'; 460 } 461 462 if (!$extension->isBundled()) { 463 $html .= '<dt>' . $this->getLang('downloadurl') . '</dt>'; 464 $html .= '<dd><bdi>'; 465 $html .= ($extension->getDownloadURL() 466 ? $this->shortlink($extension->getDownloadURL()) 467 : $default); 468 $html .= '</bdi></dd>'; 469 470 $html .= '<dt>' . $this->getLang('repository') . '</dt>'; 471 $html .= '<dd><bdi>'; 472 $html .= ($extension->getSourcerepoURL() 473 ? $this->shortlink($extension->getSourcerepoURL()) 474 : $default); 475 $html .= '</bdi></dd>'; 476 } 477 478 if ($extension->isInstalled()) { 479 if ($extension->getInstalledVersion()) { 480 $html .= '<dt>' . $this->getLang('installed_version') . '</dt>'; 481 $html .= '<dd>'; 482 $html .= hsc($extension->getInstalledVersion()); 483 $html .= '</dd>'; 484 } 485 if (!$extension->isBundled()) { 486 $html .= '<dt>' . $this->getLang('install_date') . '</dt>'; 487 $html .= '<dd>'; 488 $html .= ($extension->getUpdateDate() 489 ? hsc($extension->getUpdateDate()) 490 : $this->getLang('unknown')); 491 $html .= '</dd>'; 492 } 493 } 494 if (!$extension->isInstalled() || $extension->updateAvailable()) { 495 $html .= '<dt>' . $this->getLang('available_version') . '</dt>'; 496 $html .= '<dd>'; 497 $html .= ($extension->getLastUpdate() 498 ? hsc($extension->getLastUpdate()) 499 : $this->getLang('unknown')); 500 $html .= '</dd>'; 501 } 502 503 $html .= '<dt>' . $this->getLang('provides') . '</dt>'; 504 $html .= '<dd><bdi>'; 505 $html .= ($extension->getTypes() 506 ? hsc(implode(', ', $extension->getTypes())) 507 : $default); 508 $html .= '</bdi></dd>'; 509 510 if (!$extension->isBundled() && $extension->getCompatibleVersions()) { 511 $html .= '<dt>' . $this->getLang('compatible') . '</dt>'; 512 $html .= '<dd>'; 513 foreach ($extension->getCompatibleVersions() as $date => $version) { 514 $html .= '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>, '; 515 } 516 $html = rtrim($html, ', '); 517 $html .= '</dd>'; 518 } 519 if ($extension->getDependencies()) { 520 $html .= '<dt>' . $this->getLang('depends') . '</dt>'; 521 $html .= '<dd>'; 522 $html .= $this->makeLinkList($extension->getDependencies()); 523 $html .= '</dd>'; 524 } 525 526 if ($extension->getSimilarExtensions()) { 527 $html .= '<dt>' . $this->getLang('similar') . '</dt>'; 528 $html .= '<dd>'; 529 $html .= $this->makeLinkList($extension->getSimilarExtensions()); 530 $html .= '</dd>'; 531 } 532 533 if ($extension->getConflicts()) { 534 $html .= '<dt>' . $this->getLang('conflicts') . '</dt>'; 535 $html .= '<dd>'; 536 $html .= $this->makeLinkList($extension->getConflicts()); 537 $html .= '</dd>'; 538 } 539 $html .= '</dl>' . DOKU_LF; 540 return $html; 541 } 542 543 /** 544 * Generate a list of links for extensions 545 * 546 * @param array $ext The extensions 547 * @return string The HTML code 548 */ 549 public function makeLinkList($ext) 550 { 551 $html = ''; 552 foreach ($ext as $link) { 553 $html .= '<bdi><a href="' . 554 $this->gui->tabURL('search', ['q' => 'ext:' . $link]) . '">' . 555 hsc($link) . '</a></bdi>, '; 556 } 557 return rtrim($html, ', '); 558 } 559 560 /** 561 * Display the action buttons if they are possible 562 * 563 * @param helper_plugin_extension_extension $extension The extension 564 * @return string The HTML code 565 */ 566 public function makeActions(helper_plugin_extension_extension $extension) 567 { 568 global $conf; 569 $html = ''; 570 $errors = ''; 571 572 if ($extension->isInstalled()) { 573 if (($canmod = $extension->canModify()) === true) { 574 if (!$extension->isProtected()) { 575 $html .= $this->makeAction('uninstall', $extension); 576 } 577 if ($extension->getDownloadURL()) { 578 if ($extension->updateAvailable()) { 579 $html .= $this->makeAction('update', $extension); 580 } else { 581 $html .= $this->makeAction('reinstall', $extension); 582 } 583 } 584 } else { 585 $errors .= '<p class="permerror">' . $this->getLang($canmod) . '</p>'; 586 } 587 if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates 588 if ($extension->isEnabled()) { 589 $html .= $this->makeAction('disable', $extension); 590 } else { 591 $html .= $this->makeAction('enable', $extension); 592 } 593 } 594 if ($extension->isGitControlled()) { 595 $errors .= '<p class="permerror">' . $this->getLang('git') . '</p>'; 596 } 597 if ( 598 $extension->isEnabled() && 599 in_array('Auth', $extension->getTypes()) && 600 $conf['authtype'] != $extension->getID() 601 ) { 602 $errors .= '<p class="permerror">' . $this->getLang('auth') . '</p>'; 603 } 604 } elseif (($canmod = $extension->canModify()) === true) { 605 if ($extension->getDownloadURL()) { 606 $html .= $this->makeAction('install', $extension); 607 } 608 } else { 609 $errors .= '<div class="permerror">' . $this->getLang($canmod) . '</div>'; 610 } 611 612 if (!$extension->isInstalled() && $extension->getDownloadURL()) { 613 $html .= ' <span class="version">' . $this->getLang('available_version') . ' '; 614 $html .= ($extension->getLastUpdate() 615 ? hsc($extension->getLastUpdate()) 616 : $this->getLang('unknown')) . '</span>'; 617 } 618 619 return $html . ' ' . $errors . DOKU_LF; 620 } 621 622 /** 623 * Display an action button for an extension 624 * 625 * @param string $action The action 626 * @param helper_plugin_extension_extension $extension The extension 627 * @return string The HTML code 628 */ 629 public function makeAction($action, $extension) 630 { 631 $title = ''; 632 633 if ($action == 'install' || $action == 'reinstall') { 634 $title = 'title="' . hsc($extension->getDownloadURL()) . '"'; 635 } 636 637 $classes = 'button ' . $action; 638 $name = 'fn[' . $action . '][' . hsc($extension->getID()) . ']'; 639 640 $html = '<button class="' . $classes . '" name="' . $name . '" type="submit" ' . $title . '>' . 641 $this->getLang('btn_' . $action) . '</button> '; 642 return $html; 643 } 644 645 /** 646 * Plugin/template status 647 * 648 * @param helper_plugin_extension_extension $extension The extension 649 * @return string The description of all relevant statusses 650 */ 651 public function makeStatus(helper_plugin_extension_extension $extension) 652 { 653 $status = []; 654 655 if ($extension->isInstalled()) { 656 $status[] = $this->getLang('status_installed'); 657 if ($extension->isProtected()) { 658 $status[] = $this->getLang('status_protected'); 659 } else { 660 $status[] = $extension->isEnabled() 661 ? $this->getLang('status_enabled') 662 : $this->getLang('status_disabled'); 663 } 664 } else { 665 $status[] = $this->getLang('status_not_installed'); 666 } 667 if (!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable'); 668 if ($extension->isBundled()) $status[] = $this->getLang('status_bundled'); 669 $status[] = $extension->isTemplate() 670 ? $this->getLang('status_template') 671 : $this->getLang('status_plugin'); 672 return implode(', ', $status); 673 } 674} 675