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 (($updateMessage = $extension->getUpdateMessage()) !== false) {
396            $html .=  '<div class="msg notify">' .
397                sprintf($this->getLang('update_message'), '<bdi>' . hsc($updateMessage) . '</bdi>') .
398                '</div>';
399        }
400        if ($extension->updateAvailable()) {
401            $html .=  '<div class="msg notify">' .
402                sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())) .
403                '</div>';
404        }
405        if ($extension->hasDownloadURLChanged()) {
406            $html .= '<div class="msg notify">' .
407                sprintf(
408                    $this->getLang('url_change'),
409                    '<bdi>' . hsc($extension->getDownloadURL()) . '</bdi>',
410                    '<bdi>' . hsc($extension->getLastDownloadURL()) . '</bdi>'
411                ) .
412                '</div>';
413        }
414        return $html . DOKU_LF;
415    }
416
417    /**
418     * Create a link from the given URL
419     *
420     * Shortens the URL for display
421     *
422     * @param string $url
423     * @return string  HTML link
424     */
425    public function shortlink($url)
426    {
427        $link = parse_url($url);
428
429        $base = $link['host'];
430        if (!empty($link['port'])) $base .= $base . ':' . $link['port'];
431        $long = $link['path'];
432        if (!empty($link['query'])) $long .= $link['query'];
433
434        $name = shorten($base, $long, 55);
435
436        $html = '<a href="' . hsc($url) . '" class="urlextern">' . hsc($name) . '</a>';
437        return $html;
438    }
439
440    /**
441     * Plugin/template details
442     *
443     * @param helper_plugin_extension_extension $extension The extension
444     * @return string The HTML code
445     */
446    public function makeInfo(helper_plugin_extension_extension $extension)
447    {
448        $default = $this->getLang('unknown');
449        $html = '<dl class="details">';
450
451        $html .= '<dt>' . $this->getLang('status') . '</dt>';
452        $html .= '<dd>' . $this->makeStatus($extension) . '</dd>';
453
454        if ($extension->getDonationURL()) {
455            $html .= '<dt>' . $this->getLang('donate') . '</dt>';
456            $html .= '<dd>';
457            $html .= '<a href="' . $extension->getDonationURL() . '" class="donate">' .
458                $this->getLang('donate_action') . '</a>';
459            $html .= '</dd>';
460        }
461
462        if (!$extension->isBundled()) {
463            $html .= '<dt>' . $this->getLang('downloadurl') . '</dt>';
464            $html .= '<dd><bdi>';
465            $html .= ($extension->getDownloadURL()
466                ? $this->shortlink($extension->getDownloadURL())
467                : $default);
468            $html .= '</bdi></dd>';
469
470            $html .= '<dt>' . $this->getLang('repository') . '</dt>';
471            $html .= '<dd><bdi>';
472            $html .= ($extension->getSourcerepoURL()
473                ? $this->shortlink($extension->getSourcerepoURL())
474                : $default);
475            $html .= '</bdi></dd>';
476        }
477
478        if ($extension->isInstalled()) {
479            if ($extension->getInstalledVersion()) {
480                $html .= '<dt>' . $this->getLang('installed_version') . '</dt>';
481                $html .= '<dd>';
482                $html .= hsc($extension->getInstalledVersion());
483                $html .= '</dd>';
484            }
485            if (!$extension->isBundled()) {
486                $html .= '<dt>' . $this->getLang('install_date') . '</dt>';
487                $html .= '<dd>';
488                $html .= ($extension->getUpdateDate()
489                    ? hsc($extension->getUpdateDate())
490                    : $this->getLang('unknown'));
491                $html .= '</dd>';
492            }
493        }
494        if (!$extension->isInstalled() || $extension->updateAvailable()) {
495            $html .= '<dt>' . $this->getLang('available_version') . '</dt>';
496            $html .= '<dd>';
497            $html .= ($extension->getLastUpdate()
498                ? hsc($extension->getLastUpdate())
499                : $this->getLang('unknown'));
500            $html .= '</dd>';
501        }
502
503        $html .= '<dt>' . $this->getLang('provides') . '</dt>';
504        $html .= '<dd><bdi>';
505        $html .= ($extension->getTypes()
506            ? hsc(implode(', ', $extension->getTypes()))
507            : $default);
508        $html .= '</bdi></dd>';
509
510        if (!$extension->isBundled() && $extension->getCompatibleVersions()) {
511            $html .= '<dt>' . $this->getLang('compatible') . '</dt>';
512            $html .= '<dd>';
513            foreach ($extension->getCompatibleVersions() as $date => $version) {
514                $html .= '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>, ';
515            }
516            $html = rtrim($html, ', ');
517            $html .= '</dd>';
518        }
519        if ($extension->getDependencies()) {
520            $html .= '<dt>' . $this->getLang('depends') . '</dt>';
521            $html .= '<dd>';
522            $html .= $this->makeLinkList($extension->getDependencies());
523            $html .= '</dd>';
524        }
525
526        if ($extension->getSimilarExtensions()) {
527            $html .= '<dt>' . $this->getLang('similar') . '</dt>';
528            $html .= '<dd>';
529            $html .= $this->makeLinkList($extension->getSimilarExtensions());
530            $html .= '</dd>';
531        }
532
533        if ($extension->getConflicts()) {
534            $html .= '<dt>' . $this->getLang('conflicts') . '</dt>';
535            $html .= '<dd>';
536            $html .= $this->makeLinkList($extension->getConflicts());
537            $html .= '</dd>';
538        }
539        $html .= '</dl>' . DOKU_LF;
540        return $html;
541    }
542
543    /**
544     * Generate a list of links for extensions
545     *
546     * @param array $ext The extensions
547     * @return string The HTML code
548     */
549    public function makeLinkList($ext)
550    {
551        $html = '';
552        foreach ($ext as $link) {
553            $html .= '<bdi><a href="' .
554                $this->gui->tabURL('search', ['q' => 'ext:' . $link]) . '">' .
555                hsc($link) . '</a></bdi>, ';
556        }
557        return rtrim($html, ', ');
558    }
559
560    /**
561     * Display the action buttons if they are possible
562     *
563     * @param helper_plugin_extension_extension $extension The extension
564     * @return string The HTML code
565     */
566    public function makeActions(helper_plugin_extension_extension $extension)
567    {
568        global $conf;
569        $html   = '';
570        $errors = '';
571
572        if ($extension->isInstalled()) {
573            if (($canmod = $extension->canModify()) === true) {
574                if (!$extension->isProtected()) {
575                    $html .= $this->makeAction('uninstall', $extension);
576                }
577                if ($extension->getDownloadURL()) {
578                    if ($extension->updateAvailable()) {
579                        $html .= $this->makeAction('update', $extension);
580                    } else {
581                        $html .= $this->makeAction('reinstall', $extension);
582                    }
583                }
584            } else {
585                $errors .= '<p class="permerror">' . $this->getLang($canmod) . '</p>';
586            }
587            if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates
588                if ($extension->isEnabled()) {
589                    $html .= $this->makeAction('disable', $extension);
590                } else {
591                    $html .= $this->makeAction('enable', $extension);
592                }
593            }
594            if ($extension->isGitControlled()) {
595                $errors .= '<p class="permerror">' . $this->getLang('git') . '</p>';
596            }
597            if (
598                $extension->isEnabled() &&
599                in_array('Auth', $extension->getTypes()) &&
600                $conf['authtype'] != $extension->getID()
601            ) {
602                $errors .= '<p class="permerror">' . $this->getLang('auth') . '</p>';
603            }
604        } elseif (($canmod = $extension->canModify()) === true) {
605            if ($extension->getDownloadURL()) {
606                $html .= $this->makeAction('install', $extension);
607            }
608        } else {
609            $errors .= '<div class="permerror">' . $this->getLang($canmod) . '</div>';
610        }
611
612        if (!$extension->isInstalled() && $extension->getDownloadURL()) {
613            $html .= ' <span class="version">' . $this->getLang('available_version') . ' ';
614            $html .= ($extension->getLastUpdate()
615                    ? hsc($extension->getLastUpdate())
616                    : $this->getLang('unknown')) . '</span>';
617        }
618
619        return $html . ' ' . $errors . DOKU_LF;
620    }
621
622    /**
623     * Display an action button for an extension
624     *
625     * @param string                            $action    The action
626     * @param helper_plugin_extension_extension $extension The extension
627     * @return string The HTML code
628     */
629    public function makeAction($action, $extension)
630    {
631        $title = '';
632
633        if ($action == 'install' || $action == 'reinstall') {
634            $title = 'title="' . hsc($extension->getDownloadURL()) . '"';
635        }
636
637        $classes = 'button ' . $action;
638        $name    = 'fn[' . $action . '][' . hsc($extension->getID()) . ']';
639
640        $html = '<button class="' . $classes . '" name="' . $name . '" type="submit" ' . $title . '>' .
641            $this->getLang('btn_' . $action) . '</button> ';
642        return $html;
643    }
644
645    /**
646     * Plugin/template status
647     *
648     * @param helper_plugin_extension_extension $extension The extension
649     * @return string The description of all relevant statusses
650     */
651    public function makeStatus(helper_plugin_extension_extension $extension)
652    {
653        $status = [];
654
655        if ($extension->isInstalled()) {
656            $status[] = $this->getLang('status_installed');
657            if ($extension->isProtected()) {
658                $status[] = $this->getLang('status_protected');
659            } else {
660                $status[] = $extension->isEnabled()
661                    ? $this->getLang('status_enabled')
662                    : $this->getLang('status_disabled');
663            }
664        } else {
665            $status[] = $this->getLang('status_not_installed');
666        }
667        if (!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable');
668        if ($extension->isBundled()) $status[] = $this->getLang('status_bundled');
669        $status[] = $extension->isTemplate()
670            ? $this->getLang('status_template')
671            : $this->getLang('status_plugin');
672        return implode(', ', $status);
673    }
674}
675