1<?php 2 3namespace dokuwiki\plugin\extension; 4 5class GuiExtension extends Gui 6{ 7 const THUMB_WIDTH = 120; 8 const THUMB_HEIGHT = 70; 9 10 11 protected Extension $extension; 12 13 public function __construct(Extension $extension) 14 { 15 parent::__construct(); 16 $this->extension = $extension; 17 } 18 19 20 public function render() 21 { 22 23 $classes = $this->getClasses(); 24 25 $html = "<section class=\"$classes\">"; 26 $html .= $this->thumbnail(); 27 $html .= $this->popularity(); 28 $html .= $this->info(); 29 $html .= $this->notices(); 30 $html .= $this->mainLinks(); 31 $html .= $this->details(); 32 $html .= $this->actions(); 33 34 35 $html .= '</section>'; 36 37 return $html; 38 } 39 40 // region sections 41 42 /** 43 * Get the link and image tag for the screenshot/thumbnail 44 * 45 * @return string The HTML code 46 */ 47 protected function thumbnail() 48 { 49 $screen = $this->extension->getScreenshotURL(); 50 $thumb = $this->extension->getThumbnailURL(); 51 52 $link = []; 53 $img = [ 54 'width' => self::THUMB_WIDTH, 55 'height' => self::THUMB_HEIGHT, 56 'alt' => '', 57 ]; 58 59 if ($screen) { 60 $link = [ 61 'href' => $screen, 62 'target' => '_blank', 63 'class' => 'extension_screenshot', 64 'title' => sprintf($this->getLang('screenshot'), $this->extension->getDisplayName()) 65 ]; 66 67 $img['src'] = $thumb; 68 $img['alt'] = $link['title']; 69 } elseif ($this->extension->isTemplate()) { 70 $img['src'] = DOKU_BASE . 'lib/plugins/extension/images/template.png'; 71 } else { 72 $img['src'] = DOKU_BASE . 'lib/plugins/extension/images/plugin.png'; 73 } 74 75 $html = '<div class="screenshot">'; 76 if ($link) $html .= '<a ' . buildAttributes($link) . '>'; 77 $html .= '<img ' . buildAttributes($img) . ' />'; 78 if ($link) $html .= '</a>'; 79 $html .= '</div>'; 80 81 return $html; 82 83 } 84 85 /** 86 * The main information about the extension 87 * 88 * @return string 89 */ 90 protected function info() 91 { 92 $html = '<h2>'; 93 $html .= '<bdi>' . hsc($this->extension->getDisplayName()) . '</bdi>'; 94 if($this->extension->isBundled()) { 95 $html .= ' <span class="version">' . hsc('<'.$this->getLang('status_bundled').'>') . '</span>'; 96 } elseif($this->extension->getInstalledVersion()) { 97 $html .= ' <span class="version">' . hsc($this->extension->getInstalledVersion()) . '</span>'; 98 } 99 $html .= '</h2>'; 100 101 $html .= $this->author(); 102 103 $html .= '<p>' . hsc($this->extension->getDescription()) . '</p>'; 104 105 return $html; 106 } 107 108 /** 109 * Display the available notices for the extension 110 * 111 * @return string 112 */ 113 protected function notices() 114 { 115 $notices = Notice::list($this->extension); 116 117 $html = ''; 118 foreach ($notices as $type => $messages) { 119 foreach ($messages as $message) { 120 $message = hsc($message); 121 $message = preg_replace('/`([^`]+)`/', '<bdi>$1</bdi>', $message); 122 $html .= '<div class="msg ' . $type . '">' . $message . '</div>'; 123 } 124 } 125 return $html; 126 } 127 128 /** 129 * Generate the link bar HTML code 130 * 131 * @return string The HTML code 132 */ 133 public function mainLinks() 134 { 135 $html = '<div class="linkbar">'; 136 137 138 $homepage = $this->extension->getURL(); 139 if ($homepage) { 140 $params = $this->prepareLinkAttributes($homepage, 'homepage'); 141 $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('homepage_link') . '</a>'; 142 } 143 144 $bugtracker = $this->extension->getBugtrackerURL(); 145 if ($bugtracker) { 146 $params = $this->prepareLinkAttributes($bugtracker, 'bugs'); 147 $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('bugs_features') . '</a>'; 148 } 149 150 if ($this->extension->getDonationURL()) { 151 $params = $this->prepareLinkAttributes($this->extension->getDonationURL(), 'donate'); 152 $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('donate_action') . '</a>'; 153 } 154 155 156 157 $html .= '</div>'; 158 159 return $html; 160 } 161 162 /** 163 * Create the details section 164 * 165 * @return string 166 */ 167 protected function details() 168 { 169 $html = '<details>'; 170 $html .= '<summary>' . 'FIXME label' . '</summary>'; 171 172 173 $default = $this->getLang('unknown'); 174 $list = []; 175 176 if (!$this->extension->isBundled()) { 177 $list['downloadurl'] = $this->shortlink($this->extension->getDownloadURL(), 'download', $default); 178 $list['repository'] = $this->shortlink($this->extension->getSourcerepoURL(), 'repo', $default); 179 } 180 181 if ($this->extension->isInstalled()) { 182 if ($this->extension->isBundled()) { 183 $list['installed_version'] = $this->getLang('status_bundled'); 184 } else { 185 if ($this->extension->getInstalledVersion()) { 186 $list['installed_version'] = hsc($this->extension->getInstalledVersion()); 187 } 188 if (!$this->extension->isBundled()) { 189 $updateDate = $this->extension->getManager()->getLastUpdate(); 190 $list['install_date'] = $updateDate ? hsc($updateDate) : $default; 191 } 192 } 193 } 194 195 if (!$this->extension->isInstalled() || $this->extension->isUpdateAvailable()) { 196 $list['available_version'] = $this->extension->getLastUpdate() 197 ? hsc($this->extension->getLastUpdate()) 198 : $default; 199 } 200 201 202 if (!$this->extension->isBundled() && $this->extension->getCompatibleVersions()) { 203 $list['compatible'] = join(', ', array_map( 204 function ($date, $version) { 205 return '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>'; 206 }, 207 array_keys($this->extension->getCompatibleVersions()), 208 array_values($this->extension->getCompatibleVersions()) 209 )); 210 } 211 212 $tags = $this->extension->getTags(); 213 if ($tags) { 214 $list['tags'] = join(', ', array_map(function ($tag) { 215 $url = $this->tabURL('search', ['q' => 'tag:' . $tag]); 216 return '<bdi><a href="' . $url . '">' . hsc($tag) . '</a></bdi>'; 217 }, $tags)); 218 } 219 220 if ($this->extension->getDependencyList()) { 221 $list['depends'] = $this->linkExtensions($this->extension->getDependencyList()); 222 } 223 224 if ($this->extension->getSimilarList()) { 225 $list['similar'] = $this->linkExtensions($this->extension->getSimilarList()); 226 } 227 228 if ($this->extension->getConflictList()) { 229 $list['conflicts'] = $this->linkExtensions($this->extension->getConflictList()); 230 } 231 232 foreach ($list as $key => $value) { 233 $html .= '<dt>' . $this->getLang($key) . '</dt>'; 234 $html .= '<dd>' . $value . '</dd>'; 235 } 236 237 $html .= '</details>'; 238 return $html; 239 } 240 241 /** 242 * Generate a link to the author of the extension 243 * 244 * @return string The HTML code of the link 245 */ 246 protected function author() 247 { 248 if (!$this->extension->getAuthor()) { 249 return '<em class="author">' . $this->getLang('unknown_author') . '</em>'; 250 } 251 252 $mailid = $this->extension->getEmailID(); 253 if ($mailid) { 254 $url = $this->tabURL('search', ['q' => 'authorid:' . $mailid]); 255 $html = '<a href="' . $url . '" class="author" title="' . $this->getLang('author_hint') . '" >' . 256 '<img src="//www.gravatar.com/avatar/' . $mailid . 257 '?s=60&d=mm" width="20" height="20" alt="" /> ' . 258 hsc($this->extension->getAuthor()) . '</a>'; 259 } else { 260 $html = '<span class="author">' . hsc($this->extension->getAuthor()) . '</span>'; 261 } 262 return '<bdi>' . $html . '</bdi>'; 263 } 264 265 /** 266 * The popularity bar 267 * 268 * @return string 269 */ 270 protected function popularity() 271 { 272 $popularity = $this->extension->getPopularity(); 273 if (!$popularity) return ''; 274 if ($this->extension->isBundled()) return ''; 275 276 $popularityText = sprintf($this->getLang('popularity'), round($popularity * 100, 2)); 277 return '<div class="popularity" title="' . $popularityText . '">' . 278 '<div style="width: ' . ($popularity * 100) . '%;">' . 279 '<span class="a11y">' . $popularityText . '</span>' . 280 '</div></div>'; 281 282 } 283 284 protected function actions() 285 { 286 global $conf; 287 288 $html = ''; 289 $actions = []; 290 $errors = []; 291 292 // show the available version if there is one 293 if ($this->extension->getDownloadURL() && $this->extension->getLastUpdate()) { 294 $html .= ' <span class="version">' . $this->getLang('available_version') . ' ' . 295 hsc($this->extension->getLastUpdate()) . '</span>'; 296 } 297 298 // gather available actions and possible errors to show 299 try { 300 Installer::ensurePermissions($this->extension); 301 302 if ($this->extension->isInstalled()) { 303 304 if (!$this->extension->isProtected()) $actions[] = 'uninstall'; 305 if ($this->extension->getDownloadURL()) { 306 $actions[] = $this->extension->isUpdateAvailable() ? 'update' : 'reinstall'; 307 308 if ($this->extension->isGitControlled()) { 309 $errors[] = $this->getLang('git'); 310 } 311 } 312 313 if (!$this->extension->isProtected() && !$this->extension->isTemplate()) { // no enable/disable for templates 314 $actions[] = $this->extension->isEnabled() ? 'disable' : 'enable'; 315 316 if ( 317 $this->extension->isEnabled() && 318 in_array('Auth', $this->extension->getComponentTypes()) && 319 $conf['authtype'] != $this->extension->getID() 320 ) { 321 $errors[] = $this->getLang('auth'); 322 } 323 } 324 } else { 325 if ($this->extension->getDownloadURL()) { 326 $actions[] = 'install'; 327 } 328 } 329 } catch (\Exception $e) { 330 $errors[] = $e->getMessage(); 331 } 332 333 foreach ($actions as $action) { 334 $html .= '<button name="fn[' . $action . '][' . $this->extension->getID() . ']" class="button" type="submit">' . 335 $this->getLang('btn_' . $action) . '</button>'; 336 } 337 338 foreach ($errors as $error) { 339 $html .= '<div class="msg error">' . hsc($error) . '</div>'; 340 } 341 342 return $html; 343 } 344 345 346 // endregion 347 // region utility functions 348 349 /** 350 * Create the classes representing the state of the extension 351 * 352 * @return string 353 */ 354 protected function getClasses() 355 { 356 $classes = ['extension', $this->extension->getType()]; 357 if ($this->extension->isInstalled()) $classes[] = 'installed'; 358 if ($this->extension->isUpdateAvailable()) $classes[] = 'update'; 359 $classes[] = $this->extension->isEnabled() ? 'enabled' : 'disabled'; 360 return implode(' ', $classes); 361 } 362 363 /** 364 * Create an attributes array for a link 365 * 366 * Handles interwiki links to dokuwiki.org 367 * 368 * @param string $url The URL to link to 369 * @param string $class Additional classes to add 370 * @return array 371 */ 372 protected function prepareLinkAttributes($url, $class) 373 { 374 global $conf; 375 376 $attributes = [ 377 'href' => $url, 378 'class' => 'urlextern', 379 'target' => $conf['target']['extern'], 380 'rel' => 'noopener', 381 'title' => $url, 382 ]; 383 384 if ($conf['relnofollow']) { 385 $attributes['rel'] .= ' ugc nofollow'; 386 } 387 388 if (preg_match('/^https?:\/\/(www\.)?dokuwiki\.org\//i', $url)) { 389 $attributes['class'] = 'interwiki iw_doku'; 390 $attributes['target'] = $conf['target']['interwiki']; 391 $attributes['rel'] = ''; 392 } 393 394 $attributes['class'] .= ' ' . $class; 395 return $attributes; 396 } 397 398 /** 399 * Create a link from the given URL 400 * 401 * Shortens the URL for display 402 * 403 * @param string $url 404 * @param string $class Additional classes to add 405 * @param string $fallback If URL is empty return this fallback 406 * @return string HTML link 407 */ 408 protected function shortlink($url, $class, $fallback = '') 409 { 410 if (!$url) return hsc($fallback); 411 412 $link = parse_url($url); 413 $base = $link['host']; 414 if (!empty($link['port'])) $base .= $base . ':' . $link['port']; 415 $long = $link['path']; 416 if (!empty($link['query'])) $long .= $link['query']; 417 418 $name = shorten($base, $long, 55); 419 420 $params = $this->prepareLinkAttributes($url, $class); 421 $html = '<a ' . buildAttributes($params, true) . '>' . hsc($name) . '</a>'; 422 return $html; 423 } 424 425 /** 426 * Generate a list of links for extensions 427 * 428 * Links to the search tab with the extension name 429 * 430 * @param array $extensions The extension names 431 * @return string The HTML code 432 */ 433 public function linkExtensions($extensions) 434 { 435 $html = ''; 436 foreach ($extensions as $link) { 437 $html .= '<bdi><a href="' . 438 $this->tabURL('search', ['q' => 'ext:' . $link]) . '">' . 439 hsc($link) . '</a></bdi>, '; 440 } 441 return rtrim($html, ', '); 442 } 443 444 // endregion 445 446 447 /** 448 * Extension main description 449 * 450 * @return string The HTML code 451 */ 452 public function makeLegend() 453 { 454 $html = '<div>'; 455 $html .= '<h2>'; 456 $html .= sprintf( 457 $this->getLang('extensionby'), 458 '<bdi>' . hsc($this->extension->getDisplayName()) . '</bdi>', 459 $this->author() 460 ); 461 $html .= '</h2>' . DOKU_LF; 462 463 $html .= $this->makeScreenshot(); 464 465 $popularity = $this->extension->getPopularity(); 466 if ($popularity !== false && !$this->extension->isBundled()) { 467 $popularityText = sprintf($this->getLang('popularity'), round($popularity * 100, 2)); 468 $html .= '<div class="popularity" title="' . $popularityText . '">' . 469 '<div style="width: ' . ($popularity * 100) . '%;">' . 470 '<span class="a11y">' . $popularityText . '</span>' . 471 '</div></div>' . DOKU_LF; 472 } 473 474 if ($this->extension->getDescription()) { 475 $html .= '<p><bdi>'; 476 $html .= hsc($this->extension->getDescription()) . ' '; 477 $html .= '</bdi></p>' . DOKU_LF; 478 } 479 480 $html .= $this->makeLinkbar(); 481 $html .= $this->makeInfo(); 482 $html .= $this->makeNoticeArea(); 483 $html .= '</div>' . DOKU_LF; 484 return $html; 485 } 486 487 488 /** 489 * Plugin/template details 490 * 491 * @return string The HTML code 492 */ 493 public 494 function makeInfo() 495 { 496 $default = $this->getLang('unknown'); 497 498 499 $list = []; 500 501 $list['status'] = $this->makeStatus(); 502 503 504 if ($this->extension->getDonationURL()) { 505 $list['donate'] = '<a href="' . $this->extension->getDonationURL() . '" class="donate">' . 506 $this->getLang('donate_action') . '</a>'; 507 } 508 509 if (!$this->extension->isBundled()) { 510 $list['downloadurl'] = $this->shortlink($this->extension->getDownloadURL(), $default); 511 $list['repository'] = $this->shortlink($this->extension->getSourcerepoURL(), $default); 512 } 513 514 if ($this->extension->isInstalled()) { 515 if ($this->extension->getInstalledVersion()) { 516 $list['installed_version'] = hsc($this->extension->getInstalledVersion()); 517 } 518 if (!$this->extension->isBundled()) { 519 $updateDate = $this->extension->getManager()->getLastUpdate(); 520 $list['install_date'] = $updateDate ? hsc($updateDate) : $default; 521 } 522 } 523 524 if (!$this->extension->isInstalled() || $this->extension->isUpdateAvailable()) { 525 $list['available_version'] = $this->extension->getLastUpdate() 526 ? hsc($this->extension->getLastUpdate()) 527 : $default; 528 } 529 530 531 if (!$this->extension->isBundled() && $this->extension->getCompatibleVersions()) { 532 $html .= '<dt>' . $this->getLang('compatible') . '</dt>'; 533 $html .= '<dd>'; 534 foreach ($this->extension->getCompatibleVersions() as $date => $version) { 535 $html .= '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>, '; 536 } 537 $html = rtrim($html, ', '); 538 $html .= '</dd>'; 539 } 540 if ($this->extension->getDependencyList()) { 541 $html .= '<dt>' . $this->getLang('depends') . '</dt>'; 542 $html .= '<dd>'; 543 $html .= $this->makeLinkList($extension->getDependencies()); 544 $html .= '</dd>'; 545 } 546 547 if ($this->extension->getSimilarExtensions()) { 548 $html .= '<dt>' . $this->getLang('similar') . '</dt>'; 549 $html .= '<dd>'; 550 $html .= $this->makeLinkList($extension->getSimilarExtensions()); 551 $html .= '</dd>'; 552 } 553 554 if ($this->extension->getConflicts()) { 555 $html .= '<dt>' . $this->getLang('conflicts') . '</dt>'; 556 $html .= '<dd>'; 557 $html .= $this->makeLinkList($extension->getConflicts()); 558 $html .= '</dd>'; 559 } 560 $html .= '</dl>' . DOKU_LF; 561 return $html; 562 } 563 564 565} 566