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