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