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