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