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