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