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
14All 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
43if(!defined('DOKU_INC')) die();
44if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
45require_once(DOKU_PLUGIN.'syntax.php');  // Deprecated in latest Dokuwiki's release, to be removed
46
47/*  2020-08-04 - This is a quick hack to fix compatibility issue with hogfather (see issue #13) :
48 *  It seems that the handler.php file is no more loaded when rendering cached contents, causing a crash.
49 *  This is due to a bad initial conception of this plugin that does not comply to dokuwiki's guidance of handle / render repartition.
50 *
51 *  FIXME : refactor handle / render repartition ; most of the processing should be moved in the handle section.
52 *  /!\ 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...
53 */
54require_once(DOKU_INC.'inc/parser/handler.php');
55
56// str_contains polyfill for PHP versions before PHP8  (by jnv - #26)
57if (!function_exists('str_contains')) {
58    function str_contains(string $haystack, string $needle): bool
59    {
60        return '' === $needle || false !== strpos($haystack, $needle);
61    }
62}
63
64class syntax_plugin_button extends DokuWiki_Syntax_Plugin {
65
66    function getType() { return 'substition'; }
67    function getPType() { return 'normal'; }
68    function getSort() { return 25; }  // Internal link is 300
69//    function connectTo($mode) { $this->Lexer->addSpecialPattern('\[\[{[^}]*}[^\]]*\]\]',$mode,'plugin_button'); }
70    function connectTo($mode) {
71        $this->Lexer->addSpecialPattern('\[\[{conf[^}]*}[^\]]*\]\]',$mode,'plugin_button');
72        $this->Lexer->addEntryPattern('\[\[{[^}]*}[^\]\|]*\|?',$mode,'plugin_button');
73        $this->Lexer->addExitPattern(']]','plugin_button');
74    }
75    function postConnect() { }
76    function getAllowedTypes() { return array('formatting','substition'); }
77
78
79
80    protected $confStyles;
81    protected $styles = array();
82    protected $targets = array();
83    protected function setStyle($name,$value) {
84        global $ID;
85        $this->styles[$ID][$name] = $value;
86    }
87    protected function getStyle($name) {
88        global $ID;
89        return isset($this->styles[$ID][$name]) ? $this->styles[$ID][$name] : $this->getConfStyles($name);
90    }
91    protected function hasStyle($name) {
92        global $ID;
93        return (array_key_exists($ID,$this->styles) && is_array($this->styles[$ID]) && array_key_exists($name,$this->styles[$ID]))
94                    || $this->getConfStyles($name) ? true : false;
95    }
96    protected function getConfStyles($name = null) {
97        if($this->confStyles === null) {
98            $this->confStyles = array();
99
100            $styles = $this->getConf('styles');
101            if(!$styles) return;
102
103            $styles = explode("\n", $styles);
104            if(!is_array($styles)) return;
105
106            foreach ($styles as $style) {
107                $style = trim($style);
108                if(!$style) continue;
109
110		if (str_contains($style, '|')) {
111
112                     $style = explode('|', $style,2);
113                     if(!is_array($style) || !$style[0] || !$style[1]) continue;
114
115                     $this->confStyles[trim($style[0])] = trim($style[1]);
116		}
117            }
118            //dbg($this->confStyles);
119
120        }
121
122        if($name) {
123            if(!isset($this->confStyles[$name])) return false;
124
125            return $this->confStyles[$name];
126        }
127        return $this->confStyles;
128
129    }
130
131
132    protected function setTarget($name,$value) {
133        global $ID;
134        $this->targets[$ID][$name] = $value;
135    }
136    protected function getTarget($name) {
137        global $ID;
138        return $this->targets[$ID][$name];
139    }
140    protected function hasTarget($name) {
141        global $ID;
142        return (array_key_exists($ID,$this->targets) && is_array($this->targets[$ID]) && array_key_exists($name,$this->targets[$ID])) ? true : false;
143    }
144
145    function handle($match, $state, $pos, Doku_Handler $handler)
146    {
147        global $plugin_button_styles;
148        global $plugin_button_target;
149
150        switch ($state) {
151          case DOKU_LEXER_SPECIAL :
152          case DOKU_LEXER_ENTER :
153                $data = '';
154                // Button
155                if (preg_match('/\[\[{ *(?<image>[^}\|]*) *\|?(?<css>[^}]*)}(?<link>[^\]\|]*)\|?(?<title>[^\]]*)/', $match, $matches))
156                {
157                    $data = $matches;
158                }
159                if (is_array($data))
160                {
161                    if ($data['image'] == 'conf.styles')
162                    {
163                        $this->setStyle($data['link'],$data['title']);
164                    }
165                    else if ($data['image'] == 'conf.target')
166                    {
167                        $this->setTarget($data['link'],$data['title']);
168                    }
169                    else
170                    {
171                        $data['target'] = "";
172                        if ($this->hasTarget($data['css']))
173                        {
174                            $data['target'] =  $this->getTarget($data['css']);
175                        }
176                        else if ($this->hasTarget('default'))
177                        {
178                            $data['target'] = $this->getTarget('default');
179                        }
180
181
182                        if ($data['css'] != "" && $this->hasStyle($data['css']))
183                        {
184                            $data['css'] = $this->getStyle($data['css']);
185                        }
186
187                        if ($this->hasStyle('default') && ($data['css'] != 'default'))
188                        {
189                            $data['css'] = $this->getStyle('default') .' ; '. $data['css'];
190                        }
191                    }
192                }
193
194                return array($state, $data);
195
196          case DOKU_LEXER_UNMATCHED :  return array($state, $match);
197          case DOKU_LEXER_EXIT :            return array($state, '');
198        }
199        return array();
200    }
201
202    function render($mode, Doku_Renderer $renderer, $data)
203    {
204        global $plugin_button_styles;
205        global $plugin_button_target;
206        global $ID;
207        global $conf;
208
209        if($mode == 'xhtml'){
210            list($state, $match) = $data;
211            switch ($state) {
212              case DOKU_LEXER_SPECIAL:
213              case DOKU_LEXER_ENTER:
214                if (is_array($match))
215                {
216                    $image = $match['image'];
217                    if (($image != "conf.target") && ($image != "conf.styles"))
218                    {
219                        // Test if internal or external link (from handler.php / internallink)
220                        // 2020-07-09 : added special prefix '!' to allow other URI schemes without '//' in it (ex : apt,...)
221						$force_uri_prefix = "!";  // "/" can be confused with url, "!" not working
222                        if ( (substr($match['link'],0,strlen($force_uri_prefix)) === $force_uri_prefix) || (preg_match('#^mailto:|^([a-z0-9\-\.+]+?)://#i',$match['link'])))
223                        {
224                            // External
225                            $link['url'] = $match['link'];
226							// Strip trailing prefix
227							if (substr($link['url'],0,strlen($force_uri_prefix)) === $force_uri_prefix) { $link['url'] = substr($link['url'], strlen($force_uri_prefix));  }
228							// Check if it is an allowed protocol
229							$link_items=explode(":",$link['url']);
230							// Adds mailto as it is implicitely allowed wih mail syntax.
231							if (! in_array($link_items[0],getSchemes() + array('mailto'))) {
232								$link['url']="Unauthorized URI scheme";
233							}
234                            $link['name'] = $match['title'];
235                            if ($link['name'] == "") $link['name'] = $match['link'];
236                            $link['class'] = 'urlextern';
237                            if(strlen($match['target']) == 0){
238                                $match['target'] = $conf['target']['extern'];
239                            }
240                        }
241                        else
242                        {
243                            // Internal
244                            $link = $this->dokuwiki_get_link($renderer, $match['link'], $match['title']);
245                        }
246                        $target = $match['target'];
247                        if($target) $target = " target ='" .hsc($target). "' ";
248
249                        $link['name'] = str_replace('\\\\','<br />', $link['name']); //textbreak support
250                        if ($image != '')
251                        {
252                            $image = Doku_Handler_Parse_Media("{{".$image."}}");
253                            $image = $this->internalmedia($renderer,$image['src'],null,null,$image['width'],$image['height']);
254                            $image =  "<span class='plugin_button_image'>". $image['name'] ."</span>";
255                        }
256                        $text = "<a ".$target." href='".$link['url']."'><span class='plugin_button' style='".hsc($match['css'])."'>$image<span class='plugin_button_text ${link['class']}'>";
257                        if (substr($match[0],-1) != "|") $text .= $link['name'];
258                        $renderer->doc .= $text;
259						// Update meta data for move
260                       p_set_metadata($ID, array(
261                          'relation'=>array(
262                              'references'=>array(
263                                  $match['link']=>true,
264                              ),
265                              'media'=>array(
266                                  $match['image']=>true,
267                              ),
268                          ),
269                          'plugin_move'=>array(
270                              'pages'=>array(
271                                  $match['link'],
272                              ),
273                              'medias'=>array(
274                                  $match['image'],
275                              ),
276                          ),
277                        ));
278                    }
279                }
280                break;
281
282              case DOKU_LEXER_UNMATCHED :  $renderer->doc .= $renderer->_xmlEntities($match); break;
283              case DOKU_LEXER_EXIT :       $renderer->doc .= "</span></span></a>"; break;
284            }
285            return true;
286        }
287        elseif ($mode=='metadata')
288        {
289            list($state, $match) = $data;
290            switch ($state) {
291              case DOKU_LEXER_SPECIAL:
292              case DOKU_LEXER_ENTER:
293                if (is_array($match))
294                {
295                    /** @var Doku_Renderer_metadata $renderer */
296                    $renderer->internallink($match['link']);
297                    // I am assuming that when processing in handle(), you have stored
298                    // the link destination in $data[0]
299                    return true;
300                }
301                break;
302              case DOKU_LEXER_UNMATCHED :  break;
303              case DOKU_LEXER_EXIT :  break;
304            }
305            return true;
306        }
307        return false;
308    }
309
310    function dokuwiki_get_link(&$xhtml, $id, $name = NULL) {
311        global $ID;
312        $resolveid = $id;    // To prevent resolve_pageid to change $id value
313        resolve_pageid(getNS($ID),$resolveid,$exists); //page file?
314        if($exists) {
315            return $this->internallink($xhtml,$id,$name);
316        }
317        $resolveid = $id;
318        resolve_mediaid(getNS($ID),$resolveid,$exists); //media file?
319        if($exists) {
320            return $this->internalmedia($xhtml,$id,$name);
321        } else {
322            return $this->internallink($xhtml,$id,$name);
323        }
324    }
325
326    // Copied and adapted from inc/parser/xhtml.php, function internallink (see RPHACK)
327    // Should use wl instead (from commons), but this won't do the trick for the name
328    function internallink(&$xhtml, $id, $name = NULL, $search=NULL,$returnonly=false,$linktype='content')
329    {
330        global $conf;
331        global $ID;
332        global $INFO;
333
334
335        $params = '';
336        $parts = explode('?', $id, 2);
337        if (count($parts) === 2) {
338            $id = $parts[0];
339            $params = $parts[1];
340        }
341
342        // For empty $id we need to know the current $ID
343        // We need this check because _simpleTitle needs
344        // correct $id and resolve_pageid() use cleanID($id)
345        // (some things could be lost)
346        if ($id === '') {
347            $id = $ID;
348        }
349
350        // RPHACK for get_link to work with local links '#id'
351        if (substr($id, 0, 1) === '#') {
352            $id = $ID . $id;
353        }
354        // -------
355
356        // default name is based on $id as given
357        $default = $xhtml->_simpleTitle($id);
358
359        // now first resolve and clean up the $id
360        resolve_pageid(getNS($ID),$id,$exists);
361
362        $name = $xhtml->_getLinkTitle($name, $default, $isImage, $id, $linktype);
363        if ( !$isImage ) {
364            if ( $exists ) {
365                $class='wikilink1';
366            } else {
367                $class='wikilink2';
368                $link['rel']='nofollow';
369            }
370        } else {
371            $class='media';
372        }
373
374        //keep hash anchor
375	$hash = NULL;
376        if (str_contains($id, '#')) list($id,$hash) = explode('#',$id,2);
377        if(!empty($hash)) $hash = $xhtml->_headerToLink($hash);
378
379        //prepare for formating
380        $link['target'] = $conf['target']['wiki'];
381        $link['style']  = '';
382        $link['pre']    = '';
383        $link['suf']    = '';
384        // highlight link to current page
385        if ($id == $INFO['id']) {
386            $link['pre']    = '<span class="curid">';
387            $link['suf']    = '</span>';
388        }
389        $link['more']   = '';
390        $link['class']  = $class;
391        $link['url']    = wl($id, $params);
392        $link['name']   = $name;
393        $link['title']  = $id;
394        //add search string
395        if($search){
396            ($conf['userewrite']) ? $link['url'].='?' : $link['url'].='&amp;';
397            if(is_array($search)){
398                $search = array_map('rawurlencode',$search);
399                $link['url'] .= 's[]='.join('&amp;s[]=',$search);
400            }else{
401                $link['url'] .= 's='.rawurlencode($search);
402            }
403        }
404
405        //keep hash
406        if($hash) $link['url'].='#'.$hash;
407
408        return $link;
409        //output formatted
410        //if($returnonly){
411        //    return $this->_formatLink($link);
412        //}else{
413        //    $this->doc .= $this->_formatLink($link);
414        //}
415    }
416
417
418    function internalmedia (&$xhtml, $src, $title=NULL, $align=NULL, $width=NULL,
419            $height=NULL, $cache=NULL, $linking=NULL) {
420        global $ID;
421
422	$hash = NULL;
423        if (str_contains($src, '#')) list($src,$hash) = explode('#',$src,2);
424        resolve_mediaid(getNS($ID),$src, $exists);
425
426        $noLink = false;
427        $render = ($linking == 'linkonly') ? false : true;
428        $link = $xhtml->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
429
430        list($ext,$mime,$dl) = mimetype($src,false);
431        if(substr($mime,0,5) == 'image' && $render){
432            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct'));
433        }elseif($mime == 'application/x-shockwave-flash' && $render){
434            // don't link flash movies
435            $noLink = true;
436        }else{
437            // add file icons
438            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
439            $link['class'] .= ' mediafile mf_'.$class;
440            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true);
441            if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))).')';
442        }
443
444        if($hash) $link['url'] .= '#'.$hash;
445
446        //markup non existing files
447        if (!$exists) {
448            $link['class'] .= ' wikilink2';
449        }
450
451        return $link;
452        //output formatted
453//         if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
454//         else $this->doc .= $this->_formatLink($link);
455    }
456}
457
458?>
459