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