xref: /dokuwiki/lib/plugins/extension/GuiExtension.php (revision 981e70cabab67fd2c1ea32d1f7d1ee51fe3e16a6)
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&amp;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