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