1<?php
2/**
3 * Plugin Button : Add button with image support syntax for links
4 *
5 * To be run with Dokuwiki only
6
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Rémi Peyronnet  <remi+xslt@via.ecp.fr>
10
11 Full Syntax :
12     [[{namespace:image|extra css}wiki page|Title of the link]]
13
14 All fields optional, minimal syntax:
15    [[{}Simple button]]
16
17 Configuration :
18    [[{conf.styles}style|css]]
19    [[{conf.target}style|target]]
20
21 19/05/2013 : Initial release
22 20/04/2014 : Added target support (feature request from Andrew St Hilaire)
23 07/06/2014 : Added dokuwiki formatting support in title section (not working in wiki page section) (feature request from Willi Lethert)
24 30/08/2014 : Added toolbar button (contribution from Xavier Decuyper) and fixed local anchor (bug reported by Andreas Kuzma)
25 06/09/2014 : Refactored to add backlinks support (feature request from Schümmer Hans-Jürgen)
26 28/04/2015 : Refactored global config handling, add internal media link support, add escaping of userinput (contribution from Peter Stumm   https://github.com/lisps/plugin-button)
27 05/08/2015 : Merged lisps default style option and added french translation
28 12/09/2015 : Fixed PHP error
29 30/04/2020 : Fixed spaces in image field
30 04/08/2020 : Quick hack to add compatibility with hogfather
31 07/02/2022 : Added Português do Brasil translation (PR by mac-sousa)
32 26/11/2022 : Fixed PHP8.1 warnings
33 13/12/2022 : Fixed PHP7 with str_contains polyfill
34
35 @author ThisNameIsNotAllowed
36 17/11/2016 : Added generation of metadata
37 18/11/2016 : Added default target for external links
38
39 @author lisps
40 05/03/2017 : Merged lisps move compatibility fixes
41
42 @author nerun
43 18/08/2023 : Fixed deprecation warnings in PHP 8+  (#33)
44
45 Knwon bugs:
46 - interwiki syntax not supported
47 - handle / render repartition is not optimal regarding cachind, most of the processing should be moved (#14)
48*/
49
50
51use dokuwiki\File\PageResolver;
52use dokuwiki\File\MediaResolver;
53
54if (!defined('DOKU_INC'))
55    die();
56if (!defined('DOKU_PLUGIN'))
57    define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
58
59/*  2020-08-04 - This is a quick hack to fix compatibility issue with hogfather (see issue #13) :
60 *  It seems that the handler.php file is no more loaded when rendering cached contents, causing a crash.
61 *  This is due to a bad initial conception of this plugin that does not comply to dokuwiki's guidance of handle / render repartition.
62 *
63 *  FIXME : refactor handle / render repartition ; most of the processing should be moved in the handle section.
64 *  /!\ to be able to do that (and thus, modify the cached content structure) need to find a way to clear the cache while upgrading the plugin...
65 */
66require_once(DOKU_INC . 'inc/parser/handler.php');
67
68// str_contains polyfill for PHP versions before PHP8  (by jnv - #26)
69if (!function_exists('str_contains')) {
70    function str_contains(string $haystack, string $needle): bool
71    {
72        return '' === $needle || false !== strpos($haystack, $needle);
73    }
74}
75
76class syntax_plugin_button extends DokuWiki_Syntax_Plugin
77{
78
79    function getType()
80    {
81        return 'substition';
82    }
83    function getPType()
84    {
85        return 'normal';
86    }
87    function getSort()
88    {
89        return 25;
90    } // Internal link is 300
91
92    function connectTo($mode)
93    {
94        $this->Lexer->addSpecialPattern('\[\[{conf[^}]*}[^\]]*\]\]', $mode, 'plugin_button');
95        $this->Lexer->addEntryPattern('\[\[{[^}]*}[^\]\|]*\|?', $mode, 'plugin_button');
96        $this->Lexer->addExitPattern(']]', 'plugin_button');
97    }
98    function postConnect()
99    {
100    }
101    function getAllowedTypes()
102    {
103        return array('formatting', 'substition');
104    }
105
106
107
108    protected $confStyles;
109    protected $styles = array();
110    protected $targets = array();
111    protected function setStyle($name, $value)
112    {
113        global $ID;
114        $this->styles[$ID][$name] = $value;
115    }
116    protected function getStyle($name)
117    {
118        global $ID;
119        return isset($this->styles[$ID][$name]) ? $this->styles[$ID][$name] : $this->getConfStyles($name);
120    }
121    protected function hasStyle($name)
122    {
123        global $ID;
124        return (array_key_exists($ID, $this->styles) && is_array($this->styles[$ID]) && array_key_exists($name, $this->styles[$ID]))
125            || $this->getConfStyles($name) ? true : false;
126    }
127    protected function getConfStyles($name = null)
128    {
129        if ($this->confStyles === null) {
130            $this->confStyles = array();
131
132            $styles = $this->getConf('styles');
133            if (!$styles)
134                return;
135
136            $styles = explode("\n", $styles);
137            if (!is_array($styles))
138                return;
139
140            foreach ($styles as $style) {
141                $style = trim($style);
142                if (!$style)
143                    continue;
144
145                if (str_contains($style, '|')) {
146
147                    $style = explode('|', $style, 2);
148                    if (!is_array($style) || !$style[0] || !$style[1])
149                        continue;
150
151                    $this->confStyles[trim($style[0])] = trim($style[1]);
152                }
153            }
154            //dbg($this->confStyles);
155
156        }
157
158        if ($name) {
159            if (!isset($this->confStyles[$name]))
160                return false;
161
162            return $this->confStyles[$name];
163        }
164        return $this->confStyles;
165
166    }
167
168
169    protected function setTarget($name, $value)
170    {
171        global $ID;
172        $this->targets[$ID][$name] = $value;
173    }
174    protected function getTarget($name)
175    {
176        global $ID;
177        return $this->targets[$ID][$name];
178    }
179    protected function hasTarget($name)
180    {
181        global $ID;
182        return (array_key_exists($ID, $this->targets) && is_array($this->targets[$ID]) && array_key_exists($name, $this->targets[$ID])) ? true : false;
183    }
184
185    function handle($match, $state, $pos, Doku_Handler $handler)
186    {
187        global $plugin_button_styles;
188        global $plugin_button_target;
189
190        switch ($state) {
191            case DOKU_LEXER_SPECIAL:
192            case DOKU_LEXER_ENTER:
193                $data = '';
194                // Button
195                if (preg_match('/\[\[{ *(?<image>[^}\|]*) *\|?(?<css>[^}]*)}(?<link>[^\]\|]*)\|?(?<title>[^\]]*)/', $match, $matches)) {
196                    $data = $matches;
197                }
198                if (is_array($data)) {
199                    if ($data['image'] == 'conf.styles') {
200                        $this->setStyle($data['link'], $data['title']);
201                    } else if ($data['image'] == 'conf.target') {
202                        $this->setTarget($data['link'], $data['title']);
203                    } else {
204                        $data['target'] = "";
205                        if ($this->hasTarget($data['css'])) {
206                            $data['target'] = $this->getTarget($data['css']);
207                        } else if ($this->hasTarget('default')) {
208                            $data['target'] = $this->getTarget('default');
209                        }
210
211
212                        if ($data['css'] != "" && $this->hasStyle($data['css'])) {
213                            $data['css'] = $this->getStyle($data['css']);
214                        }
215
216                        if ($this->hasStyle('default') && ($data['css'] != 'default')) {
217                            $data['css'] = $this->getStyle('default') . ' ; ' . $data['css'];
218                        }
219                    }
220                }
221
222                return array($state, $data);
223
224            case DOKU_LEXER_UNMATCHED:
225                return array($state, $match);
226            case DOKU_LEXER_EXIT:
227                return array($state, '');
228        }
229        return array();
230    }
231
232    function render($mode, Doku_Renderer $renderer, $data)
233    {
234        global $plugin_button_styles;
235        global $plugin_button_target;
236        global $ID;
237        global $conf;
238
239        if ($mode == 'xhtml') {
240            list($state, $match) = $data;
241            switch ($state) {
242                case DOKU_LEXER_SPECIAL:
243                case DOKU_LEXER_ENTER:
244                    if (is_array($match)) {
245                        $image = $match['image'];
246                        if (($image != "conf.target") && ($image != "conf.styles")) {
247                            // Test if internal or external link (from handler.php / internallink)
248                            // 2020-07-09 : added special prefix '!' to allow other URI schemes without '//' in it (ex : apt,...)
249                            $force_uri_prefix = "!"; // "/" can be confused with url, "!" not working
250                            if ((substr($match['link'], 0, strlen($force_uri_prefix)) === $force_uri_prefix) || (preg_match('#^mailto:|^([a-z0-9\-\.+]+?)://#i', $match['link']))) {
251                                // External
252                                $link['url'] = $match['link'];
253                                // Strip trailing prefix
254                                if (substr($link['url'], 0, strlen($force_uri_prefix)) === $force_uri_prefix) {
255                                    $link['url'] = substr($link['url'], strlen($force_uri_prefix));
256                                }
257                                // Check if it is an allowed protocol
258                                $link_items = explode(":", $link['url']);
259                                // Adds mailto as it is implicitely allowed wih mail syntax.
260                                if (!in_array($link_items[0], getSchemes() + array('mailto'))) {
261                                    $link['url'] = "Unauthorized URI scheme";
262                                }
263                                $link['name'] = $match['title'];
264                                if ($link['name'] == "")
265                                    $link['name'] = $match['link'];
266                                $link['class'] = 'urlextern';
267                                if (strlen($match['target']) == 0) {
268                                    $match['target'] = $conf['target']['extern'];
269                                }
270                            } else {
271                                // Internal
272                                $link = $this->dokuwiki_get_link($renderer, $match['link'], $match['title']);
273                            }
274                            $target = $match['target'];
275                            if ($target)
276                                $target = " target ='" . hsc($target) . "' ";
277
278                            $link['name'] = str_replace('\\\\', '<br />', $link['name']); //textbreak support
279                            if ($image != '') {
280                                $image = Doku_Handler_Parse_Media("{{" . $image . "}}");
281                                $image = $this->internalmedia($renderer, $image['src'], null, null, $image['width'], $image['height']);
282                                $image = "<span class='plugin_button_image'>" . $image['name'] . "</span>";
283                            }
284                            $text = "<a " . $target . " href='" . $link['url'] . "'><span class='plugin_button' style='" . hsc($match['css']) . "'>$image<span class='plugin_button_text ${link['class']}'>";
285                            if (substr($match[0], -1) != "|")
286                                $text .= $link['name'];
287                            $renderer->doc .= $text;
288                            // Update meta data for move
289                            p_set_metadata(
290                                $ID,
291                                array(
292                                    'relation' => array(
293                                        'references' => array(
294                                            $match['link'] => true,
295                                        ),
296                                        'media' => array(
297                                            $match['image'] => true,
298                                        ),
299                                    ),
300                                    'plugin_move' => array(
301                                        'pages' => array(
302                                            $match['link'],
303                                        ),
304                                        'medias' => array(
305                                            $match['image'],
306                                        ),
307                                    ),
308                                )
309                            );
310                        }
311                    }
312                    break;
313
314                case DOKU_LEXER_UNMATCHED:
315                    $renderer->doc .= $renderer->_xmlEntities($match);
316                    break;
317                case DOKU_LEXER_EXIT:
318                    $renderer->doc .= "</span></span></a>";
319                    break;
320            }
321            return true;
322        } elseif ($mode == 'metadata') {
323            list($state, $match) = $data;
324            switch ($state) {
325                case DOKU_LEXER_SPECIAL:
326                case DOKU_LEXER_ENTER:
327                    if (is_array($match)) {
328                        /** @var Doku_Renderer_metadata $renderer */
329                        $renderer->internallink($match['link']);
330                        // I am assuming that when processing in handle(), you have stored
331                        // the link destination in $data[0]
332                        return true;
333                    }
334                    break;
335                case DOKU_LEXER_UNMATCHED:
336                    break;
337                case DOKU_LEXER_EXIT:
338                    break;
339            }
340            return true;
341        }
342        return false;
343    }
344
345    function dokuwiki_get_link(&$xhtml, $id, $name = NULL)
346    {
347        global $ID;
348        $resolveid = $id; // To prevent resolve_pageid to change $id value
349        $resolveid = (new PageResolver($ID))->resolveId($resolveid);
350        $exists = page_exists($resolveid);
351        if ($exists) {
352            return $this->internallink($xhtml, $id, $name);
353        }
354        $resolveid = $id;
355        $resolveid = (new MediaResolver($ID))->resolveId($resolveid);
356        $exists = media_exists($resolveid);
357        if ($exists) {
358            return $this->internalmedia($xhtml, $id, $name);
359        } else {
360            return $this->internallink($xhtml, $id, $name);
361        }
362    }
363
364    // Copied and adapted from inc/parser/xhtml.php, function internallink (see RPHACK)
365    // Should use wl instead (from commons), but this won't do the trick for the name
366    function internallink(&$xhtml, $id, $name = NULL, $search = NULL, $returnonly = false, $linktype = 'content')
367    {
368        global $conf;
369        global $ID;
370        global $INFO;
371
372
373        $params = '';
374        $parts = explode('?', $id, 2);
375        if (count($parts) === 2) {
376            $id = $parts[0];
377            $params = $parts[1];
378        }
379
380        // For empty $id we need to know the current $ID
381        // We need this check because _simpleTitle needs
382        // correct $id and resolve_pageid() use cleanID($id)
383        // (some things could be lost)
384        if ($id === '') {
385            $id = $ID;
386        }
387
388        // RPHACK for get_link to work with local links '#id'
389        if (substr($id, 0, 1) === '#') {
390            $id = $ID . $id;
391        }
392        // -------
393
394        // default name is based on $id as given
395        $default = $xhtml->_simpleTitle($id);
396
397        // now first resolve and clean up the $id
398        $id = (new PageResolver($ID))->resolveId($id);
399        $exists = page_exists($id);
400
401        $name = $xhtml->_getLinkTitle($name, $default, $isImage, $id, $linktype);
402        if (!$isImage) {
403            if ($exists) {
404                $class = 'wikilink1';
405            } else {
406                $class = 'wikilink2';
407                $link['rel'] = 'nofollow';
408            }
409        } else {
410            $class = 'media';
411        }
412
413        //keep hash anchor
414        $hash = NULL;
415        if (str_contains($id, '#'))
416            list($id, $hash) = explode('#', $id, 2);
417        if (!empty($hash))
418            $hash = $xhtml->_headerToLink($hash);
419
420        //prepare for formating
421        $link['target'] = $conf['target']['wiki'];
422        $link['style'] = '';
423        $link['pre'] = '';
424        $link['suf'] = '';
425        // highlight link to current page
426        if ($id == $INFO['id']) {
427            $link['pre'] = '<span class="curid">';
428            $link['suf'] = '</span>';
429        }
430        $link['more'] = '';
431        $link['class'] = $class;
432        $link['url'] = wl($id, $params);
433        $link['name'] = $name;
434        $link['title'] = $id;
435        //add search string
436        if ($search) {
437            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
438            if (is_array($search)) {
439                $search = array_map('rawurlencode', $search);
440                $link['url'] .= 's[]=' . join('&amp;s[]=', $search);
441            } else {
442                $link['url'] .= 's=' . rawurlencode($search);
443            }
444        }
445
446        //keep hash
447        if ($hash)
448            $link['url'] .= '#' . $hash;
449
450        return $link;
451        //output formatted
452        //if($returnonly){
453        //    return $this->_formatLink($link);
454        //}else{
455        //    $this->doc .= $this->_formatLink($link);
456        //}
457    }
458
459
460    function internalmedia(
461        &$xhtml,
462        $src,
463        $title = NULL,
464        $align = NULL,
465        $width = NULL,
466        $height = NULL,
467        $cache = NULL,
468        $linking = NULL
469    ) {
470        global $ID;
471
472        $hash = NULL;
473        if (str_contains($src, '#'))
474            list($src, $hash) = explode('#', $src, 2);
475        $src = (new MediaResolver($ID))->resolveId($src);
476        $exists = media_exists($src);
477
478        $noLink = false;
479        $render = ($linking == 'linkonly') ? false : true;
480        $link = $xhtml->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
481
482        list($ext, $mime, $dl) = mimetype($src, false);
483        if (substr($mime, 0, 5) == 'image' && $render) {
484            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache), ($linking == 'direct'));
485        } elseif ($mime == 'application/x-shockwave-flash' && $render) {
486            // don't link flash movies
487            $noLink = true;
488        } else {
489            // add file icons
490            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
491            $link['class'] .= ' mediafile mf_' . $class;
492            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache), true);
493            if ($exists)
494                $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))) . ')';
495        }
496
497        if ($hash)
498            $link['url'] .= '#' . $hash;
499
500        //markup non existing files
501        if (!$exists) {
502            $link['class'] .= ' wikilink2';
503        }
504
505        return $link;
506        //output formatted
507        //if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
508        //else $this->doc .= $this->_formatLink($link);
509    }
510}
511
512?>