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