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