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