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