1<?php
2
3use dokuwiki\Extension\Plugin;
4
5/**
6 * DokuWiki Plugin extension (Helper Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  Michael Hamann <michael@content-space.de>
10 */
11/**
12 * Class helper_plugin_extension_list takes care of creating a HTML list of extensions
13 */
14class helper_plugin_extension_list extends Plugin
15{
16    protected $form = '';
17    /** @var  helper_plugin_extension_gui */
18    protected $gui;
19
20    /**
21     * Constructor
22     *
23     * loads additional helpers
24     */
25    public function __construct()
26    {
27        $this->gui = plugin_load('helper', 'extension_gui');
28    }
29
30    /**
31     * Initialize the extension table form
32     */
33    public function startForm()
34    {
35        $this->form .= '<ul class="extensionList">';
36    }
37
38    /**
39     * Build single row of extension table
40     *
41     * @param helper_plugin_extension_extension  $extension The extension that shall be added
42     * @param bool                               $showinfo  Show the info area
43     */
44    public function addRow(helper_plugin_extension_extension $extension, $showinfo = false)
45    {
46        $this->startRow($extension);
47        $this->populateColumn('legend', $this->makeLegend($extension, $showinfo));
48        $this->populateColumn('actions', $this->makeActions($extension));
49        $this->endRow();
50    }
51
52    /**
53     * Adds a header to the form
54     *
55     * @param string $id     The id of the header
56     * @param string $header The content of the header
57     * @param int    $level  The level of the header
58     */
59    public function addHeader($id, $header, $level = 2)
60    {
61        $this->form .= '<h' . $level . ' id="' . $id . '">' . hsc($header) . '</h' . $level . '>' . DOKU_LF;
62    }
63
64    /**
65     * Adds a paragraph to the form
66     *
67     * @param string $data The content
68     */
69    public function addParagraph($data)
70    {
71        $this->form .= '<p>' . hsc($data) . '</p>' . DOKU_LF;
72    }
73
74    /**
75     * Add hidden fields to the form with the given data
76     *
77     * @param array $data key-value list of fields and their values to add
78     */
79    public function addHidden(array $data)
80    {
81        $this->form .= '<div class="no">';
82        foreach ($data as $key => $value) {
83            $this->form .= '<input type="hidden" name="' . hsc($key) . '" value="' . hsc($value) . '" />';
84        }
85        $this->form .= '</div>' . DOKU_LF;
86    }
87
88    /**
89     * Add closing tags
90     */
91    public function endForm()
92    {
93        $this->form .= '</ul>';
94    }
95
96    /**
97     * Show message when no results are found
98     */
99    public function nothingFound()
100    {
101        global $lang;
102        $this->form .= '<li class="notfound">' . $lang['nothingfound'] . '</li>';
103    }
104
105    /**
106     * Print the form
107     *
108     * @param bool $returnonly whether to return html or print
109     */
110    public function render($returnonly = false)
111    {
112        if ($returnonly) return $this->form;
113        echo $this->form;
114    }
115
116    /**
117     * Start the HTML for the row for the extension
118     *
119     * @param helper_plugin_extension_extension $extension The extension
120     */
121    private function startRow(helper_plugin_extension_extension $extension)
122    {
123        $this->form .= '<li id="extensionplugin__' . hsc($extension->getID()) .
124            '" class="' . $this->makeClass($extension) . '">';
125    }
126
127    /**
128     * Add a column with the given class and content
129     * @param string $class The class name
130     * @param string $html  The content
131     */
132    private function populateColumn($class, $html)
133    {
134        $this->form .= '<div class="' . $class . ' col">' . $html . '</div>' . DOKU_LF;
135    }
136
137    /**
138     * End the row
139     */
140    private function endRow()
141    {
142        $this->form .= '</li>' . DOKU_LF;
143    }
144
145    /**
146     * Generate the link to the plugin homepage
147     *
148     * @param helper_plugin_extension_extension $extension The extension
149     * @return string The HTML code
150     */
151    public function makeHomepageLink(helper_plugin_extension_extension $extension)
152    {
153        global $conf;
154        $url = $extension->getURL();
155        if (strtolower(parse_url($url, PHP_URL_HOST)) == 'www.dokuwiki.org') {
156            $linktype = 'interwiki';
157        } else {
158            $linktype = 'extern';
159        }
160        $param = [
161            'href'   => $url,
162            'title'  => $url,
163            'class'  => ($linktype == 'extern') ? 'urlextern' : 'interwiki iw_doku',
164            'target' => $conf['target'][$linktype],
165            'rel'    => ($linktype == 'extern') ? 'noopener' : ''
166        ];
167        if ($linktype == 'extern' && $conf['relnofollow']) {
168            $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']);
169        }
170        $html = ' <a ' . buildAttributes($param, true) . '>' .
171            $this->getLang('homepage_link') . '</a>';
172        return $html;
173    }
174
175    /**
176     * Generate the class name for the row of the extension
177     *
178     * @param helper_plugin_extension_extension $extension The extension object
179     * @return string The class name
180     */
181    public function makeClass(helper_plugin_extension_extension $extension)
182    {
183        $class = ($extension->isTemplate()) ? 'template' : 'plugin';
184        if ($extension->isInstalled()) {
185            $class .= ' installed';
186            $class .= ($extension->isEnabled()) ? ' enabled' : ' disabled';
187            if ($extension->updateAvailable()) $class .= ' updatable';
188        }
189        if (!$extension->canModify()) $class .= ' notselect';
190        if ($extension->isProtected()) $class .=  ' protected';
191        //if($this->showinfo) $class.= ' showinfo';
192        return $class;
193    }
194
195    /**
196     * Generate a link to the author of the extension
197     *
198     * @param helper_plugin_extension_extension $extension The extension object
199     * @return string The HTML code of the link
200     */
201    public function makeAuthor(helper_plugin_extension_extension $extension)
202    {
203        if ($extension->getAuthor()) {
204            $mailid = $extension->getEmailID();
205            if ($mailid) {
206                $url = $this->gui->tabURL('search', ['q' => 'authorid:' . $mailid]);
207                $html = '<a href="' . $url . '" class="author" title="' . $this->getLang('author_hint') . '" >' .
208                    '<img src="//www.gravatar.com/avatar/' . $mailid .
209                    '?s=20&amp;d=mm" width="20" height="20" alt="" /> ' .
210                    hsc($extension->getAuthor()) . '</a>';
211            } else {
212                $html = '<span class="author">' . hsc($extension->getAuthor()) . '</span>';
213            }
214            $html = '<bdi>' . $html . '</bdi>';
215        } else {
216            $html = '<em class="author">' . $this->getLang('unknown_author') . '</em>' . DOKU_LF;
217        }
218        return $html;
219    }
220
221    /**
222     * Get the link and image tag for the screenshot/thumbnail
223     *
224     * @param helper_plugin_extension_extension $extension The extension object
225     * @return string The HTML code
226     */
227    public function makeScreenshot(helper_plugin_extension_extension $extension)
228    {
229        $screen = $extension->getScreenshotURL();
230        $thumb = $extension->getThumbnailURL();
231
232        if ($screen) {
233            // use protocol independent URLs for images coming from us #595
234            $screen = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $screen);
235            $thumb = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $thumb);
236
237            $title = sprintf($this->getLang('screenshot'), hsc($extension->getDisplayName()));
238            $img = '<a href="' . hsc($screen) . '" target="_blank" class="extension_screenshot">' .
239                '<img alt="' . $title . '" width="120" height="70" src="' . hsc($thumb) . '" />' .
240                '</a>';
241        } elseif ($extension->isTemplate()) {
242            $img = '<img alt="" width="120" height="70" src="' . DOKU_BASE .
243                'lib/plugins/extension/images/template.png" />';
244        } else {
245            $img = '<img alt="" width="120" height="70" src="' . DOKU_BASE .
246                'lib/plugins/extension/images/plugin.png" />';
247        }
248        $html = '<div class="screenshot" >' . $img . '<span></span></div>' . DOKU_LF;
249        return $html;
250    }
251
252    /**
253     * Extension main description
254     *
255     * @param helper_plugin_extension_extension $extension The extension object
256     * @param bool                              $showinfo  Show the info section
257     * @return string The HTML code
258     */
259    public function makeLegend(helper_plugin_extension_extension $extension, $showinfo = false)
260    {
261        $html  = '<div>';
262        $html .= '<h2>';
263        $html .= sprintf(
264            $this->getLang('extensionby'),
265            '<bdi>' . hsc($extension->getDisplayName()) . '</bdi>',
266            $this->makeAuthor($extension)
267        );
268        $html .= '</h2>' . DOKU_LF;
269
270        $html .= $this->makeScreenshot($extension);
271
272        $popularity = $extension->getPopularity();
273        if ($popularity !== false && !$extension->isBundled()) {
274            $popularityText = sprintf($this->getLang('popularity'), round($popularity * 100, 2));
275            $html .= '<div class="popularity" title="' . $popularityText . '">' .
276                '<div style="width: ' . ($popularity * 100) . '%;">' .
277                '<span class="a11y">' . $popularityText . '</span>' .
278                '</div></div>' . DOKU_LF;
279        }
280
281        if ($extension->getDescription()) {
282            $html .= '<p><bdi>';
283            $html .=  hsc($extension->getDescription()) . ' ';
284            $html .= '</bdi></p>' . DOKU_LF;
285        }
286
287        $html .= $this->makeLinkbar($extension);
288
289        if ($showinfo) {
290            $url = $this->gui->tabURL('');
291            $class = 'close';
292        } else {
293            $url = $this->gui->tabURL('', ['info' => $extension->getID()]);
294            $class = '';
295        }
296        $html .= ' <a href="' . $url . '#extensionplugin__' . $extension->getID() .
297            '" class="info ' . $class . '" title="' . $this->getLang('btn_info') .
298            '" data-extid="' . $extension->getID() . '">' . $this->getLang('btn_info') . '</a>';
299
300        if ($showinfo) {
301            $html .= $this->makeInfo($extension);
302        }
303        $html .= $this->makeNoticeArea($extension);
304        $html .= '</div>' . DOKU_LF;
305        return $html;
306    }
307
308    /**
309     * Generate the link bar HTML code
310     *
311     * @param helper_plugin_extension_extension $extension The extension instance
312     * @return string The HTML code
313     */
314    public function makeLinkbar(helper_plugin_extension_extension $extension)
315    {
316        global $conf;
317        $html  = '<div class="linkbar">';
318        $html .= $this->makeHomepageLink($extension);
319
320        $bugtrackerURL = $extension->getBugtrackerURL();
321        if ($bugtrackerURL) {
322            if (strtolower(parse_url($bugtrackerURL, PHP_URL_HOST)) == 'www.dokuwiki.org') {
323                $linktype = 'interwiki';
324            } else {
325                $linktype = 'extern';
326            }
327            $param = [
328                'href'   => $bugtrackerURL,
329                'title'  => $bugtrackerURL,
330                'class'  => 'bugs',
331                'target' => $conf['target'][$linktype],
332                'rel'    => ($linktype == 'extern') ? 'noopener' : ''
333            ];
334            if ($conf['relnofollow']) {
335                $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']);
336            }
337            $html .= ' <a ' . buildAttributes($param, true) . '>' .
338                  $this->getLang('bugs_features') . '</a>';
339        }
340        if ($extension->getTags()) {
341            $first = true;
342            $html .= ' <span class="tags">' . $this->getLang('tags') . ' ';
343            foreach ($extension->getTags() as $tag) {
344                if (!$first) {
345                    $html .= ', ';
346                } else {
347                    $first = false;
348                }
349                $url = $this->gui->tabURL('search', ['q' => 'tag:' . $tag]);
350                $html .= '<bdi><a href="' . $url . '">' . hsc($tag) . '</a></bdi>';
351            }
352            $html .= '</span>';
353        }
354        $html .= '</div>' . DOKU_LF;
355        return $html;
356    }
357
358    /**
359     * Notice area
360     *
361     * @param helper_plugin_extension_extension $extension The extension
362     * @return string The HTML code
363     */
364    public function makeNoticeArea(helper_plugin_extension_extension $extension)
365    {
366        $html = '';
367        $missing_dependencies = $extension->getMissingDependencies();
368        if (!empty($missing_dependencies)) {
369            $html .= '<div class="msg error">' .
370                sprintf(
371                    $this->getLang('missing_dependency'),
372                    '<bdi>' . implode(', ', $missing_dependencies) . '</bdi>'
373                ) .
374                '</div>';
375        }
376        if ($extension->isInWrongFolder()) {
377            $html .= '<div class="msg error">' .
378                sprintf(
379                    $this->getLang('wrong_folder'),
380                    '<bdi>' . hsc($extension->getInstallName()) . '</bdi>',
381                    '<bdi>' . hsc($extension->getBase()) . '</bdi>'
382                ) .
383                '</div>';
384        }
385        if (($securityissue = $extension->getSecurityIssue()) !== false) {
386            $html .= '<div class="msg error">' .
387                sprintf($this->getLang('security_issue'), '<bdi>' . hsc($securityissue) . '</bdi>') .
388                '</div>';
389        }
390        if (($securitywarning = $extension->getSecurityWarning()) !== false) {
391            $html .= '<div class="msg notify">' .
392                sprintf($this->getLang('security_warning'), '<bdi>' . hsc($securitywarning) . '</bdi>') .
393                '</div>';
394        }
395        if ($extension->updateAvailable()) {
396            $html .=  '<div class="msg notify">' .
397                sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())) .
398                '</div>';
399        }
400        if ($extension->hasDownloadURLChanged()) {
401            $html .= '<div class="msg notify">' .
402                sprintf(
403                    $this->getLang('url_change'),
404                    '<bdi>' . hsc($extension->getDownloadURL()) . '</bdi>',
405                    '<bdi>' . hsc($extension->getLastDownloadURL()) . '</bdi>'
406                ) .
407                '</div>';
408        }
409        return $html . DOKU_LF;
410    }
411
412    /**
413     * Create a link from the given URL
414     *
415     * Shortens the URL for display
416     *
417     * @param string $url
418     * @return string  HTML link
419     */
420    public function shortlink($url)
421    {
422        $link = parse_url($url);
423
424        $base = $link['host'];
425        if (!empty($link['port'])) $base .= $base . ':' . $link['port'];
426        $long = $link['path'];
427        if (!empty($link['query'])) $long .= $link['query'];
428
429        $name = shorten($base, $long, 55);
430
431        $html = '<a href="' . hsc($url) . '" class="urlextern">' . hsc($name) . '</a>';
432        return $html;
433    }
434
435    /**
436     * Plugin/template details
437     *
438     * @param helper_plugin_extension_extension $extension The extension
439     * @return string The HTML code
440     */
441    public function makeInfo(helper_plugin_extension_extension $extension)
442    {
443        $default = $this->getLang('unknown');
444        $html = '<dl class="details">';
445
446        $html .= '<dt>' . $this->getLang('status') . '</dt>';
447        $html .= '<dd>' . $this->makeStatus($extension) . '</dd>';
448
449        if ($extension->getDonationURL()) {
450            $html .= '<dt>' . $this->getLang('donate') . '</dt>';
451            $html .= '<dd>';
452            $html .= '<a href="' . $extension->getDonationURL() . '" class="donate">' .
453                $this->getLang('donate_action') . '</a>';
454            $html .= '</dd>';
455        }
456
457        if (!$extension->isBundled()) {
458            $html .= '<dt>' . $this->getLang('downloadurl') . '</dt>';
459            $html .= '<dd><bdi>';
460            $html .= ($extension->getDownloadURL()
461                ? $this->shortlink($extension->getDownloadURL())
462                : $default);
463            $html .= '</bdi></dd>';
464
465            $html .= '<dt>' . $this->getLang('repository') . '</dt>';
466            $html .= '<dd><bdi>';
467            $html .= ($extension->getSourcerepoURL()
468                ? $this->shortlink($extension->getSourcerepoURL())
469                : $default);
470            $html .= '</bdi></dd>';
471        }
472
473        if ($extension->isInstalled()) {
474            if ($extension->getInstalledVersion()) {
475                $html .= '<dt>' . $this->getLang('installed_version') . '</dt>';
476                $html .= '<dd>';
477                $html .= hsc($extension->getInstalledVersion());
478                $html .= '</dd>';
479            }
480            if (!$extension->isBundled()) {
481                $html .= '<dt>' . $this->getLang('install_date') . '</dt>';
482                $html .= '<dd>';
483                $html .= ($extension->getUpdateDate()
484                    ? hsc($extension->getUpdateDate())
485                    : $this->getLang('unknown'));
486                $html .= '</dd>';
487            }
488        }
489        if (!$extension->isInstalled() || $extension->updateAvailable()) {
490            $html .= '<dt>' . $this->getLang('available_version') . '</dt>';
491            $html .= '<dd>';
492            $html .= ($extension->getLastUpdate()
493                ? hsc($extension->getLastUpdate())
494                : $this->getLang('unknown'));
495            $html .= '</dd>';
496        }
497
498        $html .= '<dt>' . $this->getLang('provides') . '</dt>';
499        $html .= '<dd><bdi>';
500        $html .= ($extension->getTypes()
501            ? hsc(implode(', ', $extension->getTypes()))
502            : $default);
503        $html .= '</bdi></dd>';
504
505        if (!$extension->isBundled() && $extension->getCompatibleVersions()) {
506            $html .= '<dt>' . $this->getLang('compatible') . '</dt>';
507            $html .= '<dd>';
508            foreach ($extension->getCompatibleVersions() as $date => $version) {
509                $html .= '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>, ';
510            }
511            $html = rtrim($html, ', ');
512            $html .= '</dd>';
513        }
514        if ($extension->getDependencies()) {
515            $html .= '<dt>' . $this->getLang('depends') . '</dt>';
516            $html .= '<dd>';
517            $html .= $this->makeLinkList($extension->getDependencies());
518            $html .= '</dd>';
519        }
520
521        if ($extension->getSimilarExtensions()) {
522            $html .= '<dt>' . $this->getLang('similar') . '</dt>';
523            $html .= '<dd>';
524            $html .= $this->makeLinkList($extension->getSimilarExtensions());
525            $html .= '</dd>';
526        }
527
528        if ($extension->getConflicts()) {
529            $html .= '<dt>' . $this->getLang('conflicts') . '</dt>';
530            $html .= '<dd>';
531            $html .= $this->makeLinkList($extension->getConflicts());
532            $html .= '</dd>';
533        }
534        $html .= '</dl>' . DOKU_LF;
535        return $html;
536    }
537
538    /**
539     * Generate a list of links for extensions
540     *
541     * @param array $ext The extensions
542     * @return string The HTML code
543     */
544    public function makeLinkList($ext)
545    {
546        $html = '';
547        foreach ($ext as $link) {
548            $html .= '<bdi><a href="' .
549                $this->gui->tabURL('search', ['q' => 'ext:' . $link]) . '">' .
550                hsc($link) . '</a></bdi>, ';
551        }
552        return rtrim($html, ', ');
553    }
554
555    /**
556     * Display the action buttons if they are possible
557     *
558     * @param helper_plugin_extension_extension $extension The extension
559     * @return string The HTML code
560     */
561    public function makeActions(helper_plugin_extension_extension $extension)
562    {
563        global $conf;
564        $html   = '';
565        $errors = '';
566
567        if ($extension->isInstalled()) {
568            if (($canmod = $extension->canModify()) === true) {
569                if (!$extension->isProtected()) {
570                    $html .= $this->makeAction('uninstall', $extension);
571                }
572                if ($extension->getDownloadURL()) {
573                    if ($extension->updateAvailable()) {
574                        $html .= $this->makeAction('update', $extension);
575                    } else {
576                        $html .= $this->makeAction('reinstall', $extension);
577                    }
578                }
579            } else {
580                $errors .= '<p class="permerror">' . $this->getLang($canmod) . '</p>';
581            }
582            if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates
583                if ($extension->isEnabled()) {
584                    $html .= $this->makeAction('disable', $extension);
585                } else {
586                    $html .= $this->makeAction('enable', $extension);
587                }
588            }
589            if ($extension->isGitControlled()) {
590                $errors .= '<p class="permerror">' . $this->getLang('git') . '</p>';
591            }
592            if (
593                $extension->isEnabled() &&
594                in_array('Auth', $extension->getTypes()) &&
595                $conf['authtype'] != $extension->getID()
596            ) {
597                $errors .= '<p class="permerror">' . $this->getLang('auth') . '</p>';
598            }
599        } elseif (($canmod = $extension->canModify()) === true) {
600            if ($extension->getDownloadURL()) {
601                $html .= $this->makeAction('install', $extension);
602            }
603        } else {
604            $errors .= '<div class="permerror">' . $this->getLang($canmod) . '</div>';
605        }
606
607        if (!$extension->isInstalled() && $extension->getDownloadURL()) {
608            $html .= ' <span class="version">' . $this->getLang('available_version') . ' ';
609            $html .= ($extension->getLastUpdate()
610                    ? hsc($extension->getLastUpdate())
611                    : $this->getLang('unknown')) . '</span>';
612        }
613
614        return $html . ' ' . $errors . DOKU_LF;
615    }
616
617    /**
618     * Display an action button for an extension
619     *
620     * @param string                            $action    The action
621     * @param helper_plugin_extension_extension $extension The extension
622     * @return string The HTML code
623     */
624    public function makeAction($action, $extension)
625    {
626        $title = '';
627
628        if ($action == 'install' || $action == 'reinstall') {
629            $title = 'title="' . hsc($extension->getDownloadURL()) . '"';
630        }
631
632        $classes = 'button ' . $action;
633        $name    = 'fn[' . $action . '][' . hsc($extension->getID()) . ']';
634
635        $html = '<button class="' . $classes . '" name="' . $name . '" type="submit" ' . $title . '>' .
636            $this->getLang('btn_' . $action) . '</button> ';
637        return $html;
638    }
639
640    /**
641     * Plugin/template status
642     *
643     * @param helper_plugin_extension_extension $extension The extension
644     * @return string The description of all relevant statusses
645     */
646    public function makeStatus(helper_plugin_extension_extension $extension)
647    {
648        $status = [];
649
650        if ($extension->isInstalled()) {
651            $status[] = $this->getLang('status_installed');
652            if ($extension->isProtected()) {
653                $status[] = $this->getLang('status_protected');
654            } else {
655                $status[] = $extension->isEnabled()
656                    ? $this->getLang('status_enabled')
657                    : $this->getLang('status_disabled');
658            }
659        } else {
660            $status[] = $this->getLang('status_not_installed');
661        }
662        if (!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable');
663        if ($extension->isBundled()) $status[] = $this->getLang('status_bundled');
664        $status[] = $extension->isTemplate()
665            ? $this->getLang('status_template')
666            : $this->getLang('status_plugin');
667        return implode(', ', $status);
668    }
669}
670