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