1<?php 2/** 3 * Common DokuWiki functions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9/** 10 * These constants are used with the recents function 11 */ 12define('RECENTS_SKIP_DELETED', 2); 13define('RECENTS_SKIP_MINORS', 4); 14define('RECENTS_SKIP_SUBSPACES', 8); 15define('RECENTS_MEDIA_CHANGES', 16); 16define('RECENTS_MEDIA_PAGES_MIXED', 32); 17 18/** 19 * Wrapper around htmlspecialchars() 20 * 21 * @author Andreas Gohr <andi@splitbrain.org> 22 * @see htmlspecialchars() 23 * 24 * @param string $string the string being converted 25 * @return string converted string 26 */ 27function hsc($string) { 28 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 29} 30 31/** 32 * Checks if the given input is blank 33 * 34 * This is similar to empty() but will return false for "0". 35 * 36 * Please note: when you pass uninitialized variables, they will implicitly be created 37 * with a NULL value without warning. 38 * 39 * To avoid this it's recommended to guard the call with isset like this: 40 * 41 * (isset($foo) && !blank($foo)) 42 * (!isset($foo) || blank($foo)) 43 * 44 * @param $in 45 * @param bool $trim Consider a string of whitespace to be blank 46 * @return bool 47 */ 48function blank(&$in, $trim = false) { 49 if(is_null($in)) return true; 50 if(is_array($in)) return empty($in); 51 if($in === "\0") return true; 52 if($trim && trim($in) === '') return true; 53 if(strlen($in) > 0) return false; 54 return empty($in); 55} 56 57/** 58 * print a newline terminated string 59 * 60 * You can give an indention as optional parameter 61 * 62 * @author Andreas Gohr <andi@splitbrain.org> 63 * 64 * @param string $string line of text 65 * @param int $indent number of spaces indention 66 */ 67function ptln($string, $indent = 0) { 68 echo str_repeat(' ', $indent)."$string\n"; 69} 70 71/** 72 * strips control characters (<32) from the given string 73 * 74 * @author Andreas Gohr <andi@splitbrain.org> 75 * 76 * @param string $string being stripped 77 * @return string 78 */ 79function stripctl($string) { 80 return preg_replace('/[\x00-\x1F]+/s', '', $string); 81} 82 83/** 84 * Return a secret token to be used for CSRF attack prevention 85 * 86 * @author Andreas Gohr <andi@splitbrain.org> 87 * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery 88 * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html 89 * 90 * @return string 91 */ 92function getSecurityToken() { 93 /** @var Input $INPUT */ 94 global $INPUT; 95 96 $user = $INPUT->server->str('REMOTE_USER'); 97 $session = session_id(); 98 99 // CSRF checks are only for logged in users - do not generate for anonymous 100 if(trim($user) == '' || trim($session) == '') return ''; 101 return PassHash::hmac('md5', $session.$user, auth_cookiesalt()); 102} 103 104/** 105 * Check the secret CSRF token 106 * 107 * @param null|string $token security token or null to read it from request variable 108 * @return bool success if the token matched 109 */ 110function checkSecurityToken($token = null) { 111 /** @var Input $INPUT */ 112 global $INPUT; 113 if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check 114 115 if(is_null($token)) $token = $INPUT->str('sectok'); 116 if(getSecurityToken() != $token) { 117 msg('Security Token did not match. Possible CSRF attack.', -1); 118 return false; 119 } 120 return true; 121} 122 123/** 124 * Print a hidden form field with a secret CSRF token 125 * 126 * @author Andreas Gohr <andi@splitbrain.org> 127 * 128 * @param bool $print if true print the field, otherwise html of the field is returned 129 * @return string html of hidden form field 130 */ 131function formSecurityToken($print = true) { 132 $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n"; 133 if($print) echo $ret; 134 return $ret; 135} 136 137/** 138 * Determine basic information for a request of $id 139 * 140 * @author Andreas Gohr <andi@splitbrain.org> 141 * @author Chris Smith <chris@jalakai.co.uk> 142 * 143 * @param string $id pageid 144 * @param bool $htmlClient add info about whether is mobile browser 145 * @return array with info for a request of $id 146 * 147 */ 148function basicinfo($id, $htmlClient=true){ 149 global $USERINFO; 150 /* @var Input $INPUT */ 151 global $INPUT; 152 153 // set info about manager/admin status. 154 $info = array(); 155 $info['isadmin'] = false; 156 $info['ismanager'] = false; 157 if($INPUT->server->has('REMOTE_USER')) { 158 $info['userinfo'] = $USERINFO; 159 $info['perm'] = auth_quickaclcheck($id); 160 $info['client'] = $INPUT->server->str('REMOTE_USER'); 161 162 if($info['perm'] == AUTH_ADMIN) { 163 $info['isadmin'] = true; 164 $info['ismanager'] = true; 165 } elseif(auth_ismanager()) { 166 $info['ismanager'] = true; 167 } 168 169 // if some outside auth were used only REMOTE_USER is set 170 if(!$info['userinfo']['name']) { 171 $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER'); 172 } 173 174 } else { 175 $info['perm'] = auth_aclcheck($id, '', null); 176 $info['client'] = clientIP(true); 177 } 178 179 $info['namespace'] = getNS($id); 180 181 // mobile detection 182 if ($htmlClient) { 183 $info['ismobile'] = clientismobile(); 184 } 185 186 return $info; 187 } 188 189/** 190 * Return info about the current document as associative 191 * array. 192 * 193 * @author Andreas Gohr <andi@splitbrain.org> 194 * 195 * @return array with info about current document 196 */ 197function pageinfo() { 198 global $ID; 199 global $REV; 200 global $RANGE; 201 global $lang; 202 /* @var Input $INPUT */ 203 global $INPUT; 204 205 $info = basicinfo($ID); 206 207 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml 208 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary 209 $info['id'] = $ID; 210 $info['rev'] = $REV; 211 212 if($INPUT->server->has('REMOTE_USER')) { 213 $sub = new Subscription(); 214 $info['subscribed'] = $sub->user_subscription(); 215 } else { 216 $info['subscribed'] = false; 217 } 218 219 $info['locked'] = checklock($ID); 220 $info['filepath'] = wikiFN($ID); 221 $info['exists'] = file_exists($info['filepath']); 222 $info['currentrev'] = @filemtime($info['filepath']); 223 if($REV) { 224 //check if current revision was meant 225 if($info['exists'] && ($info['currentrev'] == $REV)) { 226 $REV = ''; 227 } elseif($RANGE) { 228 //section editing does not work with old revisions! 229 $REV = ''; 230 $RANGE = ''; 231 msg($lang['nosecedit'], 0); 232 } else { 233 //really use old revision 234 $info['filepath'] = wikiFN($ID, $REV); 235 $info['exists'] = file_exists($info['filepath']); 236 } 237 } 238 $info['rev'] = $REV; 239 if($info['exists']) { 240 $info['writable'] = (is_writable($info['filepath']) && 241 ($info['perm'] >= AUTH_EDIT)); 242 } else { 243 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 244 } 245 $info['editable'] = ($info['writable'] && empty($info['locked'])); 246 $info['lastmod'] = @filemtime($info['filepath']); 247 248 //load page meta data 249 $info['meta'] = p_get_metadata($ID); 250 251 //who's the editor 252 $pagelog = new PageChangeLog($ID, 1024); 253 if($REV) { 254 $revinfo = $pagelog->getRevisionInfo($REV); 255 } else { 256 if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) { 257 $revinfo = $info['meta']['last_change']; 258 } else { 259 $revinfo = $pagelog->getRevisionInfo($info['lastmod']); 260 // cache most recent changelog line in metadata if missing and still valid 261 if($revinfo !== false) { 262 $info['meta']['last_change'] = $revinfo; 263 p_set_metadata($ID, array('last_change' => $revinfo)); 264 } 265 } 266 } 267 //and check for an external edit 268 if($revinfo !== false && $revinfo['date'] != $info['lastmod']) { 269 // cached changelog line no longer valid 270 $revinfo = false; 271 $info['meta']['last_change'] = $revinfo; 272 p_set_metadata($ID, array('last_change' => $revinfo)); 273 } 274 275 $info['ip'] = $revinfo['ip']; 276 $info['user'] = $revinfo['user']; 277 $info['sum'] = $revinfo['sum']; 278 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID. 279 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor']. 280 281 if($revinfo['user']) { 282 $info['editor'] = $revinfo['user']; 283 } else { 284 $info['editor'] = $revinfo['ip']; 285 } 286 287 // draft 288 $draft = getCacheName($info['client'].$ID, '.draft'); 289 if(file_exists($draft)) { 290 if(@filemtime($draft) < @filemtime(wikiFN($ID))) { 291 // remove stale draft 292 @unlink($draft); 293 } else { 294 $info['draft'] = $draft; 295 } 296 } 297 298 return $info; 299} 300 301/** 302 * Initialize and/or fill global $JSINFO with some basic info to be given to javascript 303 */ 304function jsinfo() { 305 global $JSINFO, $ID, $INFO, $ACT; 306 307 if (!is_array($JSINFO)) { 308 $JSINFO = []; 309 } 310 //export minimal info to JS, plugins can add more 311 $JSINFO['id'] = $ID; 312 $JSINFO['namespace'] = (string) $INFO['namespace']; 313 $JSINFO['ACT'] = act_clean($ACT); 314 $JSINFO['useHeadingNavigation'] = (int) useHeading('navigation'); 315 $JSINFO['useHeadingContent'] = (int) useHeading('content'); 316} 317 318/** 319 * Return information about the current media item as an associative array. 320 * 321 * @return array with info about current media item 322 */ 323function mediainfo(){ 324 global $NS; 325 global $IMG; 326 327 $info = basicinfo("$NS:*"); 328 $info['image'] = $IMG; 329 330 return $info; 331} 332 333/** 334 * Build an string of URL parameters 335 * 336 * @author Andreas Gohr 337 * 338 * @param array $params array with key-value pairs 339 * @param string $sep series of pairs are separated by this character 340 * @return string query string 341 */ 342function buildURLparams($params, $sep = '&') { 343 $url = ''; 344 $amp = false; 345 foreach($params as $key => $val) { 346 if($amp) $url .= $sep; 347 348 $url .= rawurlencode($key).'='; 349 $url .= rawurlencode((string) $val); 350 $amp = true; 351 } 352 return $url; 353} 354 355/** 356 * Build an string of html tag attributes 357 * 358 * Skips keys starting with '_', values get HTML encoded 359 * 360 * @author Andreas Gohr 361 * 362 * @param array $params array with (attribute name-attribute value) pairs 363 * @param bool $skipempty skip empty string values? 364 * @return string 365 */ 366function buildAttributes($params, $skipempty = false) { 367 $url = ''; 368 $white = false; 369 foreach($params as $key => $val) { 370 if($key{0} == '_') continue; 371 if($val === '' && $skipempty) continue; 372 if($white) $url .= ' '; 373 374 $url .= $key.'="'; 375 $url .= htmlspecialchars($val); 376 $url .= '"'; 377 $white = true; 378 } 379 return $url; 380} 381 382/** 383 * This builds the breadcrumb trail and returns it as array 384 * 385 * @author Andreas Gohr <andi@splitbrain.org> 386 * 387 * @return string[] with the data: array(pageid=>name, ... ) 388 */ 389function breadcrumbs() { 390 // we prepare the breadcrumbs early for quick session closing 391 static $crumbs = null; 392 if($crumbs != null) return $crumbs; 393 394 global $ID; 395 global $ACT; 396 global $conf; 397 398 //first visit? 399 $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array(); 400 //we only save on show and existing visible wiki documents 401 $file = wikiFN($ID); 402 if($ACT != 'show' || isHiddenPage($ID) || !file_exists($file)) { 403 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 404 return $crumbs; 405 } 406 407 // page names 408 $name = noNSorNS($ID); 409 if(useHeading('navigation')) { 410 // get page title 411 $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE); 412 if($title) { 413 $name = $title; 414 } 415 } 416 417 //remove ID from array 418 if(isset($crumbs[$ID])) { 419 unset($crumbs[$ID]); 420 } 421 422 //add to array 423 $crumbs[$ID] = $name; 424 //reduce size 425 while(count($crumbs) > $conf['breadcrumbs']) { 426 array_shift($crumbs); 427 } 428 //save to session 429 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 430 return $crumbs; 431} 432 433/** 434 * Filter for page IDs 435 * 436 * This is run on a ID before it is outputted somewhere 437 * currently used to replace the colon with something else 438 * on Windows (non-IIS) systems and to have proper URL encoding 439 * 440 * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and 441 * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of 442 * unaffected servers instead of blacklisting affected servers here. 443 * 444 * Urlencoding is ommitted when the second parameter is false 445 * 446 * @author Andreas Gohr <andi@splitbrain.org> 447 * 448 * @param string $id pageid being filtered 449 * @param bool $ue apply urlencoding? 450 * @return string 451 */ 452function idfilter($id, $ue = true) { 453 global $conf; 454 /* @var Input $INPUT */ 455 global $INPUT; 456 457 if($conf['useslash'] && $conf['userewrite']) { 458 $id = strtr($id, ':', '/'); 459 } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 460 $conf['userewrite'] && 461 strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false 462 ) { 463 $id = strtr($id, ':', ';'); 464 } 465 if($ue) { 466 $id = rawurlencode($id); 467 $id = str_replace('%3A', ':', $id); //keep as colon 468 $id = str_replace('%3B', ';', $id); //keep as semicolon 469 $id = str_replace('%2F', '/', $id); //keep as slash 470 } 471 return $id; 472} 473 474/** 475 * This builds a link to a wikipage 476 * 477 * It handles URL rewriting and adds additional parameters 478 * 479 * @author Andreas Gohr <andi@splitbrain.org> 480 * 481 * @param string $id page id, defaults to start page 482 * @param string|array $urlParameters URL parameters, associative array recommended 483 * @param bool $absolute request an absolute URL instead of relative 484 * @param string $separator parameter separator 485 * @return string 486 */ 487function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&') { 488 global $conf; 489 if(is_array($urlParameters)) { 490 if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']); 491 if(isset($urlParameters['at']) && $conf['date_at_format']) $urlParameters['at'] = date($conf['date_at_format'],$urlParameters['at']); 492 $urlParameters = buildURLparams($urlParameters, $separator); 493 } else { 494 $urlParameters = str_replace(',', $separator, $urlParameters); 495 } 496 if($id === '') { 497 $id = $conf['start']; 498 } 499 $id = idfilter($id); 500 if($absolute) { 501 $xlink = DOKU_URL; 502 } else { 503 $xlink = DOKU_BASE; 504 } 505 506 if($conf['userewrite'] == 2) { 507 $xlink .= DOKU_SCRIPT.'/'.$id; 508 if($urlParameters) $xlink .= '?'.$urlParameters; 509 } elseif($conf['userewrite']) { 510 $xlink .= $id; 511 if($urlParameters) $xlink .= '?'.$urlParameters; 512 } elseif($id) { 513 $xlink .= DOKU_SCRIPT.'?id='.$id; 514 if($urlParameters) $xlink .= $separator.$urlParameters; 515 } else { 516 $xlink .= DOKU_SCRIPT; 517 if($urlParameters) $xlink .= '?'.$urlParameters; 518 } 519 520 return $xlink; 521} 522 523/** 524 * This builds a link to an alternate page format 525 * 526 * Handles URL rewriting if enabled. Follows the style of wl(). 527 * 528 * @author Ben Coburn <btcoburn@silicodon.net> 529 * @param string $id page id, defaults to start page 530 * @param string $format the export renderer to use 531 * @param string|array $urlParameters URL parameters, associative array recommended 532 * @param bool $abs request an absolute URL instead of relative 533 * @param string $sep parameter separator 534 * @return string 535 */ 536function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&') { 537 global $conf; 538 if(is_array($urlParameters)) { 539 $urlParameters = buildURLparams($urlParameters, $sep); 540 } else { 541 $urlParameters = str_replace(',', $sep, $urlParameters); 542 } 543 544 $format = rawurlencode($format); 545 $id = idfilter($id); 546 if($abs) { 547 $xlink = DOKU_URL; 548 } else { 549 $xlink = DOKU_BASE; 550 } 551 552 if($conf['userewrite'] == 2) { 553 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 554 if($urlParameters) $xlink .= $sep.$urlParameters; 555 } elseif($conf['userewrite'] == 1) { 556 $xlink .= '_export/'.$format.'/'.$id; 557 if($urlParameters) $xlink .= '?'.$urlParameters; 558 } else { 559 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 560 if($urlParameters) $xlink .= $sep.$urlParameters; 561 } 562 563 return $xlink; 564} 565 566/** 567 * Build a link to a media file 568 * 569 * Will return a link to the detail page if $direct is false 570 * 571 * The $more parameter should always be given as array, the function then 572 * will strip default parameters to produce even cleaner URLs 573 * 574 * @param string $id the media file id or URL 575 * @param mixed $more string or array with additional parameters 576 * @param bool $direct link to detail page if false 577 * @param string $sep URL parameter separator 578 * @param bool $abs Create an absolute URL 579 * @return string 580 */ 581function ml($id = '', $more = '', $direct = true, $sep = '&', $abs = false) { 582 global $conf; 583 $isexternalimage = media_isexternal($id); 584 if(!$isexternalimage) { 585 $id = cleanID($id); 586 } 587 588 if(is_array($more)) { 589 // add token for resized images 590 if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){ 591 $more['tok'] = media_get_token($id,$more['w'],$more['h']); 592 } 593 // strip defaults for shorter URLs 594 if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']); 595 if(empty($more['w'])) unset($more['w']); 596 if(empty($more['h'])) unset($more['h']); 597 if(isset($more['id']) && $direct) unset($more['id']); 598 if(isset($more['rev']) && !$more['rev']) unset($more['rev']); 599 $more = buildURLparams($more, $sep); 600 } else { 601 $matches = array(); 602 if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){ 603 $resize = array('w'=>0, 'h'=>0); 604 foreach ($matches as $match){ 605 $resize[$match[1]] = $match[2]; 606 } 607 $more .= $more === '' ? '' : $sep; 608 $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']); 609 } 610 $more = str_replace('cache=cache', '', $more); //skip default 611 $more = str_replace(',,', ',', $more); 612 $more = str_replace(',', $sep, $more); 613 } 614 615 if($abs) { 616 $xlink = DOKU_URL; 617 } else { 618 $xlink = DOKU_BASE; 619 } 620 621 // external URLs are always direct without rewriting 622 if($isexternalimage) { 623 $xlink .= 'lib/exe/fetch.php'; 624 $xlink .= '?'.$more; 625 $xlink .= $sep.'media='.rawurlencode($id); 626 return $xlink; 627 } 628 629 $id = idfilter($id); 630 631 // decide on scriptname 632 if($direct) { 633 if($conf['userewrite'] == 1) { 634 $script = '_media'; 635 } else { 636 $script = 'lib/exe/fetch.php'; 637 } 638 } else { 639 if($conf['userewrite'] == 1) { 640 $script = '_detail'; 641 } else { 642 $script = 'lib/exe/detail.php'; 643 } 644 } 645 646 // build URL based on rewrite mode 647 if($conf['userewrite']) { 648 $xlink .= $script.'/'.$id; 649 if($more) $xlink .= '?'.$more; 650 } else { 651 if($more) { 652 $xlink .= $script.'?'.$more; 653 $xlink .= $sep.'media='.$id; 654 } else { 655 $xlink .= $script.'?media='.$id; 656 } 657 } 658 659 return $xlink; 660} 661 662/** 663 * Returns the URL to the DokuWiki base script 664 * 665 * Consider using wl() instead, unless you absoutely need the doku.php endpoint 666 * 667 * @author Andreas Gohr <andi@splitbrain.org> 668 * 669 * @return string 670 */ 671function script() { 672 return DOKU_BASE.DOKU_SCRIPT; 673} 674 675/** 676 * Spamcheck against wordlist 677 * 678 * Checks the wikitext against a list of blocked expressions 679 * returns true if the text contains any bad words 680 * 681 * Triggers COMMON_WORDBLOCK_BLOCKED 682 * 683 * Action Plugins can use this event to inspect the blocked data 684 * and gain information about the user who was blocked. 685 * 686 * Event data: 687 * data['matches'] - array of matches 688 * data['userinfo'] - information about the blocked user 689 * [ip] - ip address 690 * [user] - username (if logged in) 691 * [mail] - mail address (if logged in) 692 * [name] - real name (if logged in) 693 * 694 * @author Andreas Gohr <andi@splitbrain.org> 695 * @author Michael Klier <chi@chimeric.de> 696 * 697 * @param string $text - optional text to check, if not given the globals are used 698 * @return bool - true if a spam word was found 699 */ 700function checkwordblock($text = '') { 701 global $TEXT; 702 global $PRE; 703 global $SUF; 704 global $SUM; 705 global $conf; 706 global $INFO; 707 /* @var Input $INPUT */ 708 global $INPUT; 709 710 if(!$conf['usewordblock']) return false; 711 712 if(!$text) $text = "$PRE $TEXT $SUF $SUM"; 713 714 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 715 $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i', '\1http://\2 \2\3', $text); 716 717 $wordblocks = getWordblocks(); 718 // how many lines to read at once (to work around some PCRE limits) 719 if(version_compare(phpversion(), '4.3.0', '<')) { 720 // old versions of PCRE define a maximum of parenthesises even if no 721 // backreferences are used - the maximum is 99 722 // this is very bad performancewise and may even be too high still 723 $chunksize = 40; 724 } else { 725 // read file in chunks of 200 - this should work around the 726 // MAX_PATTERN_SIZE in modern PCRE 727 $chunksize = 200; 728 } 729 while($blocks = array_splice($wordblocks, 0, $chunksize)) { 730 $re = array(); 731 // build regexp from blocks 732 foreach($blocks as $block) { 733 $block = preg_replace('/#.*$/', '', $block); 734 $block = trim($block); 735 if(empty($block)) continue; 736 $re[] = $block; 737 } 738 if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) { 739 // prepare event data 740 $data = array(); 741 $data['matches'] = $matches; 742 $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR'); 743 if($INPUT->server->str('REMOTE_USER')) { 744 $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER'); 745 $data['userinfo']['name'] = $INFO['userinfo']['name']; 746 $data['userinfo']['mail'] = $INFO['userinfo']['mail']; 747 } 748 $callback = function () { 749 return true; 750 }; 751 return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 752 } 753 } 754 return false; 755} 756 757/** 758 * Return the IP of the client 759 * 760 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 761 * 762 * It returns a comma separated list of IPs if the above mentioned 763 * headers are set. If the single parameter is set, it tries to return 764 * a routable public address, prefering the ones suplied in the X 765 * headers 766 * 767 * @author Andreas Gohr <andi@splitbrain.org> 768 * 769 * @param boolean $single If set only a single IP is returned 770 * @return string 771 */ 772function clientIP($single = false) { 773 /* @var Input $INPUT */ 774 global $INPUT; 775 776 $ip = array(); 777 $ip[] = $INPUT->server->str('REMOTE_ADDR'); 778 if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) { 779 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR')))); 780 } 781 if($INPUT->server->str('HTTP_X_REAL_IP')) { 782 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP')))); 783 } 784 785 // some IPv4/v6 regexps borrowed from Feyd 786 // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479 787 $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])'; 788 $hex_digit = '[A-Fa-f0-9]'; 789 $h16 = "{$hex_digit}{1,4}"; 790 $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet"; 791 $ls32 = "(?:$h16:$h16|$IPv4Address)"; 792 $IPv6Address = 793 "(?:(?:{$IPv4Address})|(?:". 794 "(?:$h16:){6}$ls32". 795 "|::(?:$h16:){5}$ls32". 796 "|(?:$h16)?::(?:$h16:){4}$ls32". 797 "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32". 798 "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32". 799 "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32". 800 "|(?:(?:$h16:){0,4}$h16)?::$ls32". 801 "|(?:(?:$h16:){0,5}$h16)?::$h16". 802 "|(?:(?:$h16:){0,6}$h16)?::". 803 ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)"; 804 805 // remove any non-IP stuff 806 $cnt = count($ip); 807 $match = array(); 808 for($i = 0; $i < $cnt; $i++) { 809 if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) { 810 $ip[$i] = $match[0]; 811 } else { 812 $ip[$i] = ''; 813 } 814 if(empty($ip[$i])) unset($ip[$i]); 815 } 816 $ip = array_values(array_unique($ip)); 817 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 818 819 if(!$single) return join(',', $ip); 820 821 // decide which IP to use, trying to avoid local addresses 822 $ip = array_reverse($ip); 823 foreach($ip as $i) { 824 if(preg_match('/^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/', $i)) { 825 continue; 826 } else { 827 return $i; 828 } 829 } 830 // still here? just use the first (last) address 831 return $ip[0]; 832} 833 834/** 835 * Check if the browser is on a mobile device 836 * 837 * Adapted from the example code at url below 838 * 839 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 840 * 841 * @return bool if true, client is mobile browser; otherwise false 842 */ 843function clientismobile() { 844 /* @var Input $INPUT */ 845 global $INPUT; 846 847 if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true; 848 849 if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true; 850 851 if(!$INPUT->server->has('HTTP_USER_AGENT')) return false; 852 853 $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto'; 854 855 if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true; 856 857 return false; 858} 859 860/** 861 * check if a given link is interwiki link 862 * 863 * @param string $link the link, e.g. "wiki>page" 864 * @return bool 865 */ 866function link_isinterwiki($link){ 867 if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true; 868 return false; 869} 870 871/** 872 * Convert one or more comma separated IPs to hostnames 873 * 874 * If $conf['dnslookups'] is disabled it simply returns the input string 875 * 876 * @author Glen Harris <astfgl@iamnota.org> 877 * 878 * @param string $ips comma separated list of IP addresses 879 * @return string a comma separated list of hostnames 880 */ 881function gethostsbyaddrs($ips) { 882 global $conf; 883 if(!$conf['dnslookups']) return $ips; 884 885 $hosts = array(); 886 $ips = explode(',', $ips); 887 888 if(is_array($ips)) { 889 foreach($ips as $ip) { 890 $hosts[] = gethostbyaddr(trim($ip)); 891 } 892 return join(',', $hosts); 893 } else { 894 return gethostbyaddr(trim($ips)); 895 } 896} 897 898/** 899 * Checks if a given page is currently locked. 900 * 901 * removes stale lockfiles 902 * 903 * @author Andreas Gohr <andi@splitbrain.org> 904 * 905 * @param string $id page id 906 * @return bool page is locked? 907 */ 908function checklock($id) { 909 global $conf; 910 /* @var Input $INPUT */ 911 global $INPUT; 912 913 $lock = wikiLockFN($id); 914 915 //no lockfile 916 if(!file_exists($lock)) return false; 917 918 //lockfile expired 919 if((time() - filemtime($lock)) > $conf['locktime']) { 920 @unlink($lock); 921 return false; 922 } 923 924 //my own lock 925 @list($ip, $session) = explode("\n", io_readFile($lock)); 926 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) { 927 return false; 928 } 929 930 return $ip; 931} 932 933/** 934 * Lock a page for editing 935 * 936 * @author Andreas Gohr <andi@splitbrain.org> 937 * 938 * @param string $id page id to lock 939 */ 940function lock($id) { 941 global $conf; 942 /* @var Input $INPUT */ 943 global $INPUT; 944 945 if($conf['locktime'] == 0) { 946 return; 947 } 948 949 $lock = wikiLockFN($id); 950 if($INPUT->server->str('REMOTE_USER')) { 951 io_saveFile($lock, $INPUT->server->str('REMOTE_USER')); 952 } else { 953 io_saveFile($lock, clientIP()."\n".session_id()); 954 } 955} 956 957/** 958 * Unlock a page if it was locked by the user 959 * 960 * @author Andreas Gohr <andi@splitbrain.org> 961 * 962 * @param string $id page id to unlock 963 * @return bool true if a lock was removed 964 */ 965function unlock($id) { 966 /* @var Input $INPUT */ 967 global $INPUT; 968 969 $lock = wikiLockFN($id); 970 if(file_exists($lock)) { 971 @list($ip, $session) = explode("\n", io_readFile($lock)); 972 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) { 973 @unlink($lock); 974 return true; 975 } 976 } 977 return false; 978} 979 980/** 981 * convert line ending to unix format 982 * 983 * also makes sure the given text is valid UTF-8 984 * 985 * @see formText() for 2crlf conversion 986 * @author Andreas Gohr <andi@splitbrain.org> 987 * 988 * @param string $text 989 * @return string 990 */ 991function cleanText($text) { 992 $text = preg_replace("/(\015\012)|(\015)/", "\012", $text); 993 994 // if the text is not valid UTF-8 we simply assume latin1 995 // this won't break any worse than it breaks with the wrong encoding 996 // but might actually fix the problem in many cases 997 if(!utf8_check($text)) $text = utf8_encode($text); 998 999 return $text; 1000} 1001 1002/** 1003 * Prepares text for print in Webforms by encoding special chars. 1004 * It also converts line endings to Windows format which is 1005 * pseudo standard for webforms. 1006 * 1007 * @see cleanText() for 2unix conversion 1008 * @author Andreas Gohr <andi@splitbrain.org> 1009 * 1010 * @param string $text 1011 * @return string 1012 */ 1013function formText($text) { 1014 $text = str_replace("\012", "\015\012", $text); 1015 return htmlspecialchars($text); 1016} 1017 1018/** 1019 * Returns the specified local text in raw format 1020 * 1021 * @author Andreas Gohr <andi@splitbrain.org> 1022 * 1023 * @param string $id page id 1024 * @param string $ext extension of file being read, default 'txt' 1025 * @return string 1026 */ 1027function rawLocale($id, $ext = 'txt') { 1028 return io_readFile(localeFN($id, $ext)); 1029} 1030 1031/** 1032 * Returns the raw WikiText 1033 * 1034 * @author Andreas Gohr <andi@splitbrain.org> 1035 * 1036 * @param string $id page id 1037 * @param string|int $rev timestamp when a revision of wikitext is desired 1038 * @return string 1039 */ 1040function rawWiki($id, $rev = '') { 1041 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1042} 1043 1044/** 1045 * Returns the pagetemplate contents for the ID's namespace 1046 * 1047 * @triggers COMMON_PAGETPL_LOAD 1048 * @author Andreas Gohr <andi@splitbrain.org> 1049 * 1050 * @param string $id the id of the page to be created 1051 * @return string parsed pagetemplate content 1052 */ 1053function pageTemplate($id) { 1054 global $conf; 1055 1056 if(is_array($id)) $id = $id[0]; 1057 1058 // prepare initial event data 1059 $data = array( 1060 'id' => $id, // the id of the page to be created 1061 'tpl' => '', // the text used as template 1062 'tplfile' => '', // the file above text was/should be loaded from 1063 'doreplace' => true // should wildcard replacements be done on the text? 1064 ); 1065 1066 $evt = new Doku_Event('COMMON_PAGETPL_LOAD', $data); 1067 if($evt->advise_before(true)) { 1068 // the before event might have loaded the content already 1069 if(empty($data['tpl'])) { 1070 // if the before event did not set a template file, try to find one 1071 if(empty($data['tplfile'])) { 1072 $path = dirname(wikiFN($id)); 1073 if(file_exists($path.'/_template.txt')) { 1074 $data['tplfile'] = $path.'/_template.txt'; 1075 } else { 1076 // search upper namespaces for templates 1077 $len = strlen(rtrim($conf['datadir'], '/')); 1078 while(strlen($path) >= $len) { 1079 if(file_exists($path.'/__template.txt')) { 1080 $data['tplfile'] = $path.'/__template.txt'; 1081 break; 1082 } 1083 $path = substr($path, 0, strrpos($path, '/')); 1084 } 1085 } 1086 } 1087 // load the content 1088 $data['tpl'] = io_readFile($data['tplfile']); 1089 } 1090 if($data['doreplace']) parsePageTemplate($data); 1091 } 1092 $evt->advise_after(); 1093 unset($evt); 1094 1095 return $data['tpl']; 1096} 1097 1098/** 1099 * Performs common page template replacements 1100 * This works on data from COMMON_PAGETPL_LOAD 1101 * 1102 * @author Andreas Gohr <andi@splitbrain.org> 1103 * 1104 * @param array $data array with event data 1105 * @return string 1106 */ 1107function parsePageTemplate(&$data) { 1108 /** 1109 * @var string $id the id of the page to be created 1110 * @var string $tpl the text used as template 1111 * @var string $tplfile the file above text was/should be loaded from 1112 * @var bool $doreplace should wildcard replacements be done on the text? 1113 */ 1114 extract($data); 1115 1116 global $USERINFO; 1117 global $conf; 1118 /* @var Input $INPUT */ 1119 global $INPUT; 1120 1121 // replace placeholders 1122 $file = noNS($id); 1123 $page = strtr($file, $conf['sepchar'], ' '); 1124 1125 $tpl = str_replace( 1126 array( 1127 '@ID@', 1128 '@NS@', 1129 '@CURNS@', 1130 '@FILE@', 1131 '@!FILE@', 1132 '@!FILE!@', 1133 '@PAGE@', 1134 '@!PAGE@', 1135 '@!!PAGE@', 1136 '@!PAGE!@', 1137 '@USER@', 1138 '@NAME@', 1139 '@MAIL@', 1140 '@DATE@', 1141 ), 1142 array( 1143 $id, 1144 getNS($id), 1145 curNS($id), 1146 $file, 1147 utf8_ucfirst($file), 1148 utf8_strtoupper($file), 1149 $page, 1150 utf8_ucfirst($page), 1151 utf8_ucwords($page), 1152 utf8_strtoupper($page), 1153 $INPUT->server->str('REMOTE_USER'), 1154 $USERINFO['name'], 1155 $USERINFO['mail'], 1156 $conf['dformat'], 1157 ), $tpl 1158 ); 1159 1160 // we need the callback to work around strftime's char limit 1161 $tpl = preg_replace_callback( 1162 '/%./', 1163 function ($m) { 1164 return strftime($m[0]); 1165 }, 1166 $tpl 1167 ); 1168 $data['tpl'] = $tpl; 1169 return $tpl; 1170} 1171 1172/** 1173 * Returns the raw Wiki Text in three slices. 1174 * 1175 * The range parameter needs to have the form "from-to" 1176 * and gives the range of the section in bytes - no 1177 * UTF-8 awareness is needed. 1178 * The returned order is prefix, section and suffix. 1179 * 1180 * @author Andreas Gohr <andi@splitbrain.org> 1181 * 1182 * @param string $range in form "from-to" 1183 * @param string $id page id 1184 * @param string $rev optional, the revision timestamp 1185 * @return string[] with three slices 1186 */ 1187function rawWikiSlices($range, $id, $rev = '') { 1188 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1189 1190 // Parse range 1191 list($from, $to) = explode('-', $range, 2); 1192 // Make range zero-based, use defaults if marker is missing 1193 $from = !$from ? 0 : ($from - 1); 1194 $to = !$to ? strlen($text) : ($to - 1); 1195 1196 $slices = array(); 1197 $slices[0] = substr($text, 0, $from); 1198 $slices[1] = substr($text, $from, $to - $from); 1199 $slices[2] = substr($text, $to); 1200 return $slices; 1201} 1202 1203/** 1204 * Joins wiki text slices 1205 * 1206 * function to join the text slices. 1207 * When the pretty parameter is set to true it adds additional empty 1208 * lines between sections if needed (used on saving). 1209 * 1210 * @author Andreas Gohr <andi@splitbrain.org> 1211 * 1212 * @param string $pre prefix 1213 * @param string $text text in the middle 1214 * @param string $suf suffix 1215 * @param bool $pretty add additional empty lines between sections 1216 * @return string 1217 */ 1218function con($pre, $text, $suf, $pretty = false) { 1219 if($pretty) { 1220 if($pre !== '' && substr($pre, -1) !== "\n" && 1221 substr($text, 0, 1) !== "\n" 1222 ) { 1223 $pre .= "\n"; 1224 } 1225 if($suf !== '' && substr($text, -1) !== "\n" && 1226 substr($suf, 0, 1) !== "\n" 1227 ) { 1228 $text .= "\n"; 1229 } 1230 } 1231 1232 return $pre.$text.$suf; 1233} 1234 1235/** 1236 * Checks if the current page version is newer than the last entry in the page's 1237 * changelog. If so, we assume it has been an external edit and we create an 1238 * attic copy and add a proper changelog line. 1239 * 1240 * This check is only executed when the page is about to be saved again from the 1241 * wiki, triggered in @see saveWikiText() 1242 * 1243 * @param string $id the page ID 1244 */ 1245function detectExternalEdit($id) { 1246 global $lang; 1247 1248 $fileLastMod = wikiFN($id); 1249 $lastMod = @filemtime($fileLastMod); // from page 1250 $pagelog = new PageChangeLog($id, 1024); 1251 $lastRev = $pagelog->getRevisions(-1, 1); // from changelog 1252 $lastRev = (int) (empty($lastRev) ? 0 : $lastRev[0]); 1253 1254 if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) { 1255 // add old revision to the attic if missing 1256 saveOldRevision($id); 1257 // add a changelog entry if this edit came from outside dokuwiki 1258 if($lastMod > $lastRev) { 1259 $fileLastRev = wikiFN($id, $lastRev); 1260 $revinfo = $pagelog->getRevisionInfo($lastRev); 1261 if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) { 1262 $filesize_old = 0; 1263 } else { 1264 $filesize_old = io_getSizeFile($fileLastRev); 1265 } 1266 $filesize_new = filesize($fileLastMod); 1267 $sizechange = $filesize_new - $filesize_old; 1268 1269 addLogEntry($lastMod, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true), $sizechange); 1270 // remove soon to be stale instructions 1271 $cache = new cache_instructions($id, $fileLastMod); 1272 $cache->removeCache(); 1273 } 1274 } 1275} 1276 1277/** 1278 * Saves a wikitext by calling io_writeWikiPage. 1279 * Also directs changelog and attic updates. 1280 * 1281 * @author Andreas Gohr <andi@splitbrain.org> 1282 * @author Ben Coburn <btcoburn@silicodon.net> 1283 * 1284 * @param string $id page id 1285 * @param string $text wikitext being saved 1286 * @param string $summary summary of text update 1287 * @param bool $minor mark this saved version as minor update 1288 */ 1289function saveWikiText($id, $text, $summary, $minor = false) { 1290 /* Note to developers: 1291 This code is subtle and delicate. Test the behavior of 1292 the attic and changelog with dokuwiki and external edits 1293 after any changes. External edits change the wiki page 1294 directly without using php or dokuwiki. 1295 */ 1296 global $conf; 1297 global $lang; 1298 global $REV; 1299 /* @var Input $INPUT */ 1300 global $INPUT; 1301 1302 // prepare data for event 1303 $svdta = array(); 1304 $svdta['id'] = $id; 1305 $svdta['file'] = wikiFN($id); 1306 $svdta['revertFrom'] = $REV; 1307 $svdta['oldRevision'] = @filemtime($svdta['file']); 1308 $svdta['newRevision'] = 0; 1309 $svdta['newContent'] = $text; 1310 $svdta['oldContent'] = rawWiki($id); 1311 $svdta['summary'] = $summary; 1312 $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']); 1313 $svdta['changeInfo'] = ''; 1314 $svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT; 1315 $svdta['sizechange'] = null; 1316 1317 // select changelog line type 1318 if($REV) { 1319 $svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT; 1320 $svdta['changeInfo'] = $REV; 1321 } else if(!file_exists($svdta['file'])) { 1322 $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE; 1323 } else if(trim($text) == '') { 1324 // empty or whitespace only content deletes 1325 $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE; 1326 // autoset summary on deletion 1327 if(blank($svdta['summary'])) { 1328 $svdta['summary'] = $lang['deleted']; 1329 } 1330 } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) { 1331 //minor edits only for logged in users 1332 $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT; 1333 } 1334 1335 $event = new Doku_Event('COMMON_WIKIPAGE_SAVE', $svdta); 1336 if(!$event->advise_before()) return; 1337 1338 // if the content has not been changed, no save happens (plugins may override this) 1339 if(!$svdta['contentChanged']) return; 1340 1341 detectExternalEdit($id); 1342 1343 if( 1344 $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE || 1345 ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file'])) 1346 ) { 1347 $filesize_old = 0; 1348 } else { 1349 $filesize_old = filesize($svdta['file']); 1350 } 1351 if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 1352 // Send "update" event with empty data, so plugins can react to page deletion 1353 $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false); 1354 trigger_event('IO_WIKIPAGE_WRITE', $data); 1355 // pre-save deleted revision 1356 @touch($svdta['file']); 1357 clearstatcache(); 1358 $svdta['newRevision'] = saveOldRevision($id); 1359 // remove empty file 1360 @unlink($svdta['file']); 1361 $filesize_new = 0; 1362 // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata... 1363 // purge non-persistant meta data 1364 p_purge_metadata($id); 1365 // remove empty namespaces 1366 io_sweepNS($id, 'datadir'); 1367 io_sweepNS($id, 'mediadir'); 1368 } else { 1369 // save file (namespace dir is created in io_writeWikiPage) 1370 io_writeWikiPage($svdta['file'], $svdta['newContent'], $id); 1371 // pre-save the revision, to keep the attic in sync 1372 $svdta['newRevision'] = saveOldRevision($id); 1373 $filesize_new = filesize($svdta['file']); 1374 } 1375 $svdta['sizechange'] = $filesize_new - $filesize_old; 1376 1377 $event->advise_after(); 1378 1379 addLogEntry($svdta['newRevision'], $svdta['id'], $svdta['changeType'], $svdta['summary'], $svdta['changeInfo'], null, $svdta['sizechange']); 1380 1381 // send notify mails 1382 notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor); 1383 notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor); 1384 1385 // update the purgefile (timestamp of the last time anything within the wiki was changed) 1386 io_saveFile($conf['cachedir'].'/purgefile', time()); 1387 1388 // if useheading is enabled, purge the cache of all linking pages 1389 if(useHeading('content')) { 1390 $pages = ft_backlinks($id, true); 1391 foreach($pages as $page) { 1392 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 1393 $cache->removeCache(); 1394 } 1395 } 1396} 1397 1398/** 1399 * moves the current version to the attic and returns its 1400 * revision date 1401 * 1402 * @author Andreas Gohr <andi@splitbrain.org> 1403 * 1404 * @param string $id page id 1405 * @return int|string revision timestamp 1406 */ 1407function saveOldRevision($id) { 1408 $oldf = wikiFN($id); 1409 if(!file_exists($oldf)) return ''; 1410 $date = filemtime($oldf); 1411 $newf = wikiFN($id, $date); 1412 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1413 return $date; 1414} 1415 1416/** 1417 * Sends a notify mail on page change or registration 1418 * 1419 * @param string $id The changed page 1420 * @param string $who Who to notify (admin|subscribers|register) 1421 * @param int|string $rev Old page revision 1422 * @param string $summary What changed 1423 * @param boolean $minor Is this a minor edit? 1424 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1425 * @return bool 1426 * 1427 * @author Andreas Gohr <andi@splitbrain.org> 1428 */ 1429function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) { 1430 global $conf; 1431 /* @var Input $INPUT */ 1432 global $INPUT; 1433 1434 // decide if there is something to do, eg. whom to mail 1435 if($who == 'admin') { 1436 if(empty($conf['notify'])) return false; //notify enabled? 1437 $tpl = 'mailtext'; 1438 $to = $conf['notify']; 1439 } elseif($who == 'subscribers') { 1440 if(!actionOK('subscribe')) return false; //subscribers enabled? 1441 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1442 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1443 trigger_event( 1444 'COMMON_NOTIFY_ADDRESSLIST', $data, 1445 array(new Subscription(), 'notifyaddresses') 1446 ); 1447 $to = $data['addresslist']; 1448 if(empty($to)) return false; 1449 $tpl = 'subscr_single'; 1450 } else { 1451 return false; //just to be safe 1452 } 1453 1454 // prepare content 1455 $subscription = new Subscription(); 1456 return $subscription->send_diff($to, $tpl, $id, $rev, $summary); 1457} 1458 1459/** 1460 * extracts the query from a search engine referrer 1461 * 1462 * @author Andreas Gohr <andi@splitbrain.org> 1463 * @author Todd Augsburger <todd@rollerorgans.com> 1464 * 1465 * @return array|string 1466 */ 1467function getGoogleQuery() { 1468 /* @var Input $INPUT */ 1469 global $INPUT; 1470 1471 if(!$INPUT->server->has('HTTP_REFERER')) { 1472 return ''; 1473 } 1474 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1475 1476 // only handle common SEs 1477 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1478 1479 $query = array(); 1480 // temporary workaround against PHP bug #49733 1481 // see http://bugs.php.net/bug.php?id=49733 1482 if(UTF8_MBSTRING) $enc = mb_internal_encoding(); 1483 parse_str($url['query'], $query); 1484 if(UTF8_MBSTRING) mb_internal_encoding($enc); 1485 1486 $q = ''; 1487 if(isset($query['q'])){ 1488 $q = $query['q']; 1489 }elseif(isset($query['p'])){ 1490 $q = $query['p']; 1491 }elseif(isset($query['query'])){ 1492 $q = $query['query']; 1493 } 1494 $q = trim($q); 1495 1496 if(!$q) return ''; 1497 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1498 return $q; 1499} 1500 1501/** 1502 * Return the human readable size of a file 1503 * 1504 * @param int $size A file size 1505 * @param int $dec A number of decimal places 1506 * @return string human readable size 1507 * 1508 * @author Martin Benjamin <b.martin@cybernet.ch> 1509 * @author Aidan Lister <aidan@php.net> 1510 * @version 1.0.0 1511 */ 1512function filesize_h($size, $dec = 1) { 1513 $sizes = array('B', 'KB', 'MB', 'GB'); 1514 $count = count($sizes); 1515 $i = 0; 1516 1517 while($size >= 1024 && ($i < $count - 1)) { 1518 $size /= 1024; 1519 $i++; 1520 } 1521 1522 return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space 1523} 1524 1525/** 1526 * Return the given timestamp as human readable, fuzzy age 1527 * 1528 * @author Andreas Gohr <gohr@cosmocode.de> 1529 * 1530 * @param int $dt timestamp 1531 * @return string 1532 */ 1533function datetime_h($dt) { 1534 global $lang; 1535 1536 $ago = time() - $dt; 1537 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1538 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1539 } 1540 if($ago > 24 * 60 * 60 * 30 * 2) { 1541 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1542 } 1543 if($ago > 24 * 60 * 60 * 7 * 2) { 1544 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1545 } 1546 if($ago > 24 * 60 * 60 * 2) { 1547 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1548 } 1549 if($ago > 60 * 60 * 2) { 1550 return sprintf($lang['hours'], round($ago / (60 * 60))); 1551 } 1552 if($ago > 60 * 2) { 1553 return sprintf($lang['minutes'], round($ago / (60))); 1554 } 1555 return sprintf($lang['seconds'], $ago); 1556} 1557 1558/** 1559 * Wraps around strftime but provides support for fuzzy dates 1560 * 1561 * The format default to $conf['dformat']. It is passed to 1562 * strftime - %f can be used to get the value from datetime_h() 1563 * 1564 * @see datetime_h 1565 * @author Andreas Gohr <gohr@cosmocode.de> 1566 * 1567 * @param int|null $dt timestamp when given, null will take current timestamp 1568 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1569 * @return string 1570 */ 1571function dformat($dt = null, $format = '') { 1572 global $conf; 1573 1574 if(is_null($dt)) $dt = time(); 1575 $dt = (int) $dt; 1576 if(!$format) $format = $conf['dformat']; 1577 1578 $format = str_replace('%f', datetime_h($dt), $format); 1579 return strftime($format, $dt); 1580} 1581 1582/** 1583 * Formats a timestamp as ISO 8601 date 1584 * 1585 * @author <ungu at terong dot com> 1586 * @link http://php.net/manual/en/function.date.php#54072 1587 * 1588 * @param int $int_date current date in UNIX timestamp 1589 * @return string 1590 */ 1591function date_iso8601($int_date) { 1592 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1593 $pre_timezone = date('O', $int_date); 1594 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1595 $date_mod .= $time_zone; 1596 return $date_mod; 1597} 1598 1599/** 1600 * return an obfuscated email address in line with $conf['mailguard'] setting 1601 * 1602 * @author Harry Fuecks <hfuecks@gmail.com> 1603 * @author Christopher Smith <chris@jalakai.co.uk> 1604 * 1605 * @param string $email email address 1606 * @return string 1607 */ 1608function obfuscate($email) { 1609 global $conf; 1610 1611 switch($conf['mailguard']) { 1612 case 'visible' : 1613 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1614 return strtr($email, $obfuscate); 1615 1616 case 'hex' : 1617 $encode = ''; 1618 $len = strlen($email); 1619 for($x = 0; $x < $len; $x++) { 1620 $encode .= '&#x'.bin2hex($email{$x}).';'; 1621 } 1622 return $encode; 1623 1624 case 'none' : 1625 default : 1626 return $email; 1627 } 1628} 1629 1630/** 1631 * Removes quoting backslashes 1632 * 1633 * @author Andreas Gohr <andi@splitbrain.org> 1634 * 1635 * @param string $string 1636 * @param string $char backslashed character 1637 * @return string 1638 */ 1639function unslash($string, $char = "'") { 1640 return str_replace('\\'.$char, $char, $string); 1641} 1642 1643/** 1644 * Convert php.ini shorthands to byte 1645 * 1646 * @author <gilthans dot NO dot SPAM at gmail dot com> 1647 * @link http://php.net/manual/en/ini.core.php#79564 1648 * 1649 * @param string $v shorthands 1650 * @return int|string 1651 */ 1652function php_to_byte($v) { 1653 $l = substr($v, -1); 1654 $ret = substr($v, 0, -1); 1655 switch(strtoupper($l)) { 1656 /** @noinspection PhpMissingBreakStatementInspection */ 1657 case 'P': 1658 $ret *= 1024; 1659 /** @noinspection PhpMissingBreakStatementInspection */ 1660 case 'T': 1661 $ret *= 1024; 1662 /** @noinspection PhpMissingBreakStatementInspection */ 1663 case 'G': 1664 $ret *= 1024; 1665 /** @noinspection PhpMissingBreakStatementInspection */ 1666 case 'M': 1667 $ret *= 1024; 1668 /** @noinspection PhpMissingBreakStatementInspection */ 1669 case 'K': 1670 $ret *= 1024; 1671 break; 1672 default; 1673 $ret *= 10; 1674 break; 1675 } 1676 return $ret; 1677} 1678 1679/** 1680 * Wrapper around preg_quote adding the default delimiter 1681 * 1682 * @param string $string 1683 * @return string 1684 */ 1685function preg_quote_cb($string) { 1686 return preg_quote($string, '/'); 1687} 1688 1689/** 1690 * Shorten a given string by removing data from the middle 1691 * 1692 * You can give the string in two parts, the first part $keep 1693 * will never be shortened. The second part $short will be cut 1694 * in the middle to shorten but only if at least $min chars are 1695 * left to display it. Otherwise it will be left off. 1696 * 1697 * @param string $keep the part to keep 1698 * @param string $short the part to shorten 1699 * @param int $max maximum chars you want for the whole string 1700 * @param int $min minimum number of chars to have left for middle shortening 1701 * @param string $char the shortening character to use 1702 * @return string 1703 */ 1704function shorten($keep, $short, $max, $min = 9, $char = '…') { 1705 $max = $max - utf8_strlen($keep); 1706 if($max < $min) return $keep; 1707 $len = utf8_strlen($short); 1708 if($len <= $max) return $keep.$short; 1709 $half = floor($max / 2); 1710 return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half); 1711} 1712 1713/** 1714 * Return the users real name or e-mail address for use 1715 * in page footer and recent changes pages 1716 * 1717 * @param string|null $username or null when currently logged-in user should be used 1718 * @param bool $textonly true returns only plain text, true allows returning html 1719 * @return string html or plain text(not escaped) of formatted user name 1720 * 1721 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1722 */ 1723function editorinfo($username, $textonly = false) { 1724 return userlink($username, $textonly); 1725} 1726 1727/** 1728 * Returns users realname w/o link 1729 * 1730 * @param string|null $username or null when currently logged-in user should be used 1731 * @param bool $textonly true returns only plain text, true allows returning html 1732 * @return string html or plain text(not escaped) of formatted user name 1733 * 1734 * @triggers COMMON_USER_LINK 1735 */ 1736function userlink($username = null, $textonly = false) { 1737 global $conf, $INFO; 1738 /** @var DokuWiki_Auth_Plugin $auth */ 1739 global $auth; 1740 /** @var Input $INPUT */ 1741 global $INPUT; 1742 1743 // prepare initial event data 1744 $data = array( 1745 'username' => $username, // the unique user name 1746 'name' => '', 1747 'link' => array( //setting 'link' to false disables linking 1748 'target' => '', 1749 'pre' => '', 1750 'suf' => '', 1751 'style' => '', 1752 'more' => '', 1753 'url' => '', 1754 'title' => '', 1755 'class' => '' 1756 ), 1757 'userlink' => '', // formatted user name as will be returned 1758 'textonly' => $textonly 1759 ); 1760 if($username === null) { 1761 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1762 if($textonly){ 1763 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1764 }else { 1765 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1766 } 1767 } 1768 1769 $evt = new Doku_Event('COMMON_USER_LINK', $data); 1770 if($evt->advise_before(true)) { 1771 if(empty($data['name'])) { 1772 if($auth) $info = $auth->getUserData($username); 1773 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1774 switch($conf['showuseras']) { 1775 case 'username': 1776 case 'username_link': 1777 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1778 break; 1779 case 'email': 1780 case 'email_link': 1781 $data['name'] = obfuscate($info['mail']); 1782 break; 1783 } 1784 } else { 1785 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1786 } 1787 } 1788 1789 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1790 static $xhtml_renderer = null; 1791 1792 if(!$data['textonly'] && empty($data['link']['url'])) { 1793 1794 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1795 if(!isset($info)) { 1796 if($auth) $info = $auth->getUserData($username); 1797 } 1798 if(isset($info) && $info) { 1799 if($conf['showuseras'] == 'email_link') { 1800 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1801 } else { 1802 if(is_null($xhtml_renderer)) { 1803 $xhtml_renderer = p_get_renderer('xhtml'); 1804 } 1805 if(empty($xhtml_renderer->interwiki)) { 1806 $xhtml_renderer->interwiki = getInterwiki(); 1807 } 1808 $shortcut = 'user'; 1809 $exists = null; 1810 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1811 $data['link']['class'] .= ' interwiki iw_user'; 1812 if($exists !== null) { 1813 if($exists) { 1814 $data['link']['class'] .= ' wikilink1'; 1815 } else { 1816 $data['link']['class'] .= ' wikilink2'; 1817 $data['link']['rel'] = 'nofollow'; 1818 } 1819 } 1820 } 1821 } else { 1822 $data['textonly'] = true; 1823 } 1824 1825 } else { 1826 $data['textonly'] = true; 1827 } 1828 } 1829 1830 if($data['textonly']) { 1831 $data['userlink'] = $data['name']; 1832 } else { 1833 $data['link']['name'] = $data['name']; 1834 if(is_null($xhtml_renderer)) { 1835 $xhtml_renderer = p_get_renderer('xhtml'); 1836 } 1837 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1838 } 1839 } 1840 $evt->advise_after(); 1841 unset($evt); 1842 1843 return $data['userlink']; 1844} 1845 1846/** 1847 * Returns the path to a image file for the currently chosen license. 1848 * When no image exists, returns an empty string 1849 * 1850 * @author Andreas Gohr <andi@splitbrain.org> 1851 * 1852 * @param string $type - type of image 'badge' or 'button' 1853 * @return string 1854 */ 1855function license_img($type) { 1856 global $license; 1857 global $conf; 1858 if(!$conf['license']) return ''; 1859 if(!is_array($license[$conf['license']])) return ''; 1860 $try = array(); 1861 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1862 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1863 if(substr($conf['license'], 0, 3) == 'cc-') { 1864 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1865 } 1866 foreach($try as $src) { 1867 if(file_exists(DOKU_INC.$src)) return $src; 1868 } 1869 return ''; 1870} 1871 1872/** 1873 * Checks if the given amount of memory is available 1874 * 1875 * If the memory_get_usage() function is not available the 1876 * function just assumes $bytes of already allocated memory 1877 * 1878 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1879 * @author Andreas Gohr <andi@splitbrain.org> 1880 * 1881 * @param int $mem Size of memory you want to allocate in bytes 1882 * @param int $bytes already allocated memory (see above) 1883 * @return bool 1884 */ 1885function is_mem_available($mem, $bytes = 1048576) { 1886 $limit = trim(ini_get('memory_limit')); 1887 if(empty($limit)) return true; // no limit set! 1888 1889 // parse limit to bytes 1890 $limit = php_to_byte($limit); 1891 1892 // get used memory if possible 1893 if(function_exists('memory_get_usage')) { 1894 $used = memory_get_usage(); 1895 } else { 1896 $used = $bytes; 1897 } 1898 1899 if($used + $mem > $limit) { 1900 return false; 1901 } 1902 1903 return true; 1904} 1905 1906/** 1907 * Send a HTTP redirect to the browser 1908 * 1909 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1910 * 1911 * @link http://support.microsoft.com/kb/q176113/ 1912 * @author Andreas Gohr <andi@splitbrain.org> 1913 * 1914 * @param string $url url being directed to 1915 */ 1916function send_redirect($url) { 1917 $url = stripctl($url); // defend against HTTP Response Splitting 1918 1919 /* @var Input $INPUT */ 1920 global $INPUT; 1921 1922 //are there any undisplayed messages? keep them in session for display 1923 global $MSG; 1924 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1925 //reopen session, store data and close session again 1926 @session_start(); 1927 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1928 } 1929 1930 // always close the session 1931 session_write_close(); 1932 1933 // check if running on IIS < 6 with CGI-PHP 1934 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1935 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1936 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1937 $matches[1] < 6 1938 ) { 1939 header('Refresh: 0;url='.$url); 1940 } else { 1941 header('Location: '.$url); 1942 } 1943 1944 // no exits during unit tests 1945 if(defined('DOKU_UNITTEST')) { 1946 // pass info about the redirect back to the test suite 1947 $testRequest = TestRequest::getRunning(); 1948 if($testRequest !== null) { 1949 $testRequest->addData('send_redirect', $url); 1950 } 1951 return; 1952 } 1953 1954 exit; 1955} 1956 1957/** 1958 * Validate a value using a set of valid values 1959 * 1960 * This function checks whether a specified value is set and in the array 1961 * $valid_values. If not, the function returns a default value or, if no 1962 * default is specified, throws an exception. 1963 * 1964 * @param string $param The name of the parameter 1965 * @param array $valid_values A set of valid values; Optionally a default may 1966 * be marked by the key “default”. 1967 * @param array $array The array containing the value (typically $_POST 1968 * or $_GET) 1969 * @param string $exc The text of the raised exception 1970 * 1971 * @throws Exception 1972 * @return mixed 1973 * @author Adrian Lang <lang@cosmocode.de> 1974 */ 1975function valid_input_set($param, $valid_values, $array, $exc = '') { 1976 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 1977 return $array[$param]; 1978 } elseif(isset($valid_values['default'])) { 1979 return $valid_values['default']; 1980 } else { 1981 throw new Exception($exc); 1982 } 1983} 1984 1985/** 1986 * Read a preference from the DokuWiki cookie 1987 * (remembering both keys & values are urlencoded) 1988 * 1989 * @param string $pref preference key 1990 * @param mixed $default value returned when preference not found 1991 * @return string preference value 1992 */ 1993function get_doku_pref($pref, $default) { 1994 $enc_pref = urlencode($pref); 1995 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 1996 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1997 $cnt = count($parts); 1998 for($i = 0; $i < $cnt; $i += 2) { 1999 if($parts[$i] == $enc_pref) { 2000 return urldecode($parts[$i + 1]); 2001 } 2002 } 2003 } 2004 return $default; 2005} 2006 2007/** 2008 * Add a preference to the DokuWiki cookie 2009 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 2010 * Remove it by setting $val to false 2011 * 2012 * @param string $pref preference key 2013 * @param string $val preference value 2014 */ 2015function set_doku_pref($pref, $val) { 2016 global $conf; 2017 $orig = get_doku_pref($pref, false); 2018 $cookieVal = ''; 2019 2020 if($orig && ($orig != $val)) { 2021 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2022 $cnt = count($parts); 2023 // urlencode $pref for the comparison 2024 $enc_pref = rawurlencode($pref); 2025 for($i = 0; $i < $cnt; $i += 2) { 2026 if($parts[$i] == $enc_pref) { 2027 if ($val !== false) { 2028 $parts[$i + 1] = rawurlencode($val); 2029 } else { 2030 unset($parts[$i]); 2031 unset($parts[$i + 1]); 2032 } 2033 break; 2034 } 2035 } 2036 $cookieVal = implode('#', $parts); 2037 } else if (!$orig && $val !== false) { 2038 $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val); 2039 } 2040 2041 if (!empty($cookieVal)) { 2042 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 2043 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 2044 } 2045} 2046 2047/** 2048 * Strips source mapping declarations from given text #601 2049 * 2050 * @param string &$text reference to the CSS or JavaScript code to clean 2051 */ 2052function stripsourcemaps(&$text){ 2053 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 2054} 2055 2056/** 2057 * Returns the contents of a given SVG file for embedding 2058 * 2059 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through 2060 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small 2061 * files are embedded. 2062 * 2063 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG! 2064 * 2065 * @param string $file full path to the SVG file 2066 * @param int $maxsize maximum allowed size for the SVG to be embedded 2067 * @return string|false the SVG content, false if the file couldn't be loaded 2068 */ 2069function inlineSVG($file, $maxsize = 2048) { 2070 $file = trim($file); 2071 if($file === '') return false; 2072 if(!file_exists($file)) return false; 2073 if(filesize($file) > $maxsize) return false; 2074 if(!is_readable($file)) return false; 2075 $content = file_get_contents($file); 2076 $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments 2077 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header 2078 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type 2079 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags 2080 $content = trim($content); 2081 if(substr($content, 0, 5) !== '<svg ') return false; 2082 return $content; 2083} 2084 2085//Setup VIM: ex: et ts=2 : 2086