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'].='&'; 397 if(is_array($search)){ 398 $search = array_map('rawurlencode',$search); 399 $link['url'] .= 's[]='.join('&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