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