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