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