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