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