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; 16use dokuwiki\Search\MetadataIndex; 17 18/** 19 * These constants are used with the recents function 20 */ 21define('RECENTS_SKIP_DELETED', 2); 22define('RECENTS_SKIP_MINORS', 4); 23define('RECENTS_SKIP_SUBSPACES', 8); 24define('RECENTS_MEDIA_CHANGES', 16); 25define('RECENTS_MEDIA_PAGES_MIXED', 32); 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 } elseif (!file_exists($svdta['file'])) { 1369 $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE; 1370 } elseif (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 } elseif ($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 $MetadataIndex = MetadataIndex::getInstance(); 1447 $pages = $MetadataIndex->backlinks($id, true); 1448 foreach ($pages as $page) { 1449 $cache = new CacheRenderer($page, wikiFN($page), 'xhtml'); 1450 $cache->removeCache(); 1451 } 1452 } 1453} 1454 1455/** 1456 * moves the current version to the attic and returns its 1457 * revision date 1458 * 1459 * @author Andreas Gohr <andi@splitbrain.org> 1460 * 1461 * @param string $id page id 1462 * @return int|string revision timestamp 1463 */ 1464function saveOldRevision($id) { 1465 $oldf = wikiFN($id); 1466 if(!file_exists($oldf)) return ''; 1467 $date = filemtime($oldf); 1468 $newf = wikiFN($id, $date); 1469 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1470 return $date; 1471} 1472 1473/** 1474 * Sends a notify mail on page change or registration 1475 * 1476 * @param string $id The changed page 1477 * @param string $who Who to notify (admin|subscribers|register) 1478 * @param int|string $rev Old page revision 1479 * @param string $summary What changed 1480 * @param boolean $minor Is this a minor edit? 1481 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1482 * @param int|string $current_rev New page revision 1483 * @return bool 1484 * 1485 * @author Andreas Gohr <andi@splitbrain.org> 1486 */ 1487function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) { 1488 global $conf; 1489 /* @var Input $INPUT */ 1490 global $INPUT; 1491 1492 // decide if there is something to do, eg. whom to mail 1493 if($who == 'admin') { 1494 if(empty($conf['notify'])) return false; //notify enabled? 1495 $tpl = 'mailtext'; 1496 $to = $conf['notify']; 1497 } elseif($who == 'subscribers') { 1498 if(!actionOK('subscribe')) return false; //subscribers enabled? 1499 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1500 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1501 Event::createAndTrigger( 1502 'COMMON_NOTIFY_ADDRESSLIST', $data, 1503 array(new Subscription(), 'notifyaddresses') 1504 ); 1505 $to = $data['addresslist']; 1506 if(empty($to)) return false; 1507 $tpl = 'subscr_single'; 1508 } else { 1509 return false; //just to be safe 1510 } 1511 1512 // prepare content 1513 $subscription = new PageSubscriptionSender(); 1514 return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev); 1515} 1516 1517/** 1518 * extracts the query from a search engine referrer 1519 * 1520 * @author Andreas Gohr <andi@splitbrain.org> 1521 * @author Todd Augsburger <todd@rollerorgans.com> 1522 * 1523 * @return array|string 1524 */ 1525function getGoogleQuery() { 1526 /* @var Input $INPUT */ 1527 global $INPUT; 1528 1529 if(!$INPUT->server->has('HTTP_REFERER')) { 1530 return ''; 1531 } 1532 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1533 1534 // only handle common SEs 1535 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1536 1537 $query = array(); 1538 parse_str($url['query'], $query); 1539 1540 $q = ''; 1541 if(isset($query['q'])){ 1542 $q = $query['q']; 1543 }elseif(isset($query['p'])){ 1544 $q = $query['p']; 1545 }elseif(isset($query['query'])){ 1546 $q = $query['query']; 1547 } 1548 $q = trim($q); 1549 1550 if(!$q) return ''; 1551 // ignore if query includes a full URL 1552 if(strpos($q, '//') !== false) return ''; 1553 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1554 return $q; 1555} 1556 1557/** 1558 * Return the human readable size of a file 1559 * 1560 * @param int $size A file size 1561 * @param int $dec A number of decimal places 1562 * @return string human readable size 1563 * 1564 * @author Martin Benjamin <b.martin@cybernet.ch> 1565 * @author Aidan Lister <aidan@php.net> 1566 * @version 1.0.0 1567 */ 1568function filesize_h($size, $dec = 1) { 1569 $sizes = array('B', 'KB', 'MB', 'GB'); 1570 $count = count($sizes); 1571 $i = 0; 1572 1573 while($size >= 1024 && ($i < $count - 1)) { 1574 $size /= 1024; 1575 $i++; 1576 } 1577 1578 return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space 1579} 1580 1581/** 1582 * Return the given timestamp as human readable, fuzzy age 1583 * 1584 * @author Andreas Gohr <gohr@cosmocode.de> 1585 * 1586 * @param int $dt timestamp 1587 * @return string 1588 */ 1589function datetime_h($dt) { 1590 global $lang; 1591 1592 $ago = time() - $dt; 1593 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1594 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1595 } 1596 if($ago > 24 * 60 * 60 * 30 * 2) { 1597 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1598 } 1599 if($ago > 24 * 60 * 60 * 7 * 2) { 1600 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1601 } 1602 if($ago > 24 * 60 * 60 * 2) { 1603 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1604 } 1605 if($ago > 60 * 60 * 2) { 1606 return sprintf($lang['hours'], round($ago / (60 * 60))); 1607 } 1608 if($ago > 60 * 2) { 1609 return sprintf($lang['minutes'], round($ago / (60))); 1610 } 1611 return sprintf($lang['seconds'], $ago); 1612} 1613 1614/** 1615 * Wraps around strftime but provides support for fuzzy dates 1616 * 1617 * The format default to $conf['dformat']. It is passed to 1618 * strftime - %f can be used to get the value from datetime_h() 1619 * 1620 * @see datetime_h 1621 * @author Andreas Gohr <gohr@cosmocode.de> 1622 * 1623 * @param int|null $dt timestamp when given, null will take current timestamp 1624 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1625 * @return string 1626 */ 1627function dformat($dt = null, $format = '') { 1628 global $conf; 1629 1630 if(is_null($dt)) $dt = time(); 1631 $dt = (int) $dt; 1632 if(!$format) $format = $conf['dformat']; 1633 1634 $format = str_replace('%f', datetime_h($dt), $format); 1635 return strftime($format, $dt); 1636} 1637 1638/** 1639 * Formats a timestamp as ISO 8601 date 1640 * 1641 * @author <ungu at terong dot com> 1642 * @link http://php.net/manual/en/function.date.php#54072 1643 * 1644 * @param int $int_date current date in UNIX timestamp 1645 * @return string 1646 */ 1647function date_iso8601($int_date) { 1648 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1649 $pre_timezone = date('O', $int_date); 1650 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1651 $date_mod .= $time_zone; 1652 return $date_mod; 1653} 1654 1655/** 1656 * return an obfuscated email address in line with $conf['mailguard'] setting 1657 * 1658 * @author Harry Fuecks <hfuecks@gmail.com> 1659 * @author Christopher Smith <chris@jalakai.co.uk> 1660 * 1661 * @param string $email email address 1662 * @return string 1663 */ 1664function obfuscate($email) { 1665 global $conf; 1666 1667 switch($conf['mailguard']) { 1668 case 'visible' : 1669 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1670 return strtr($email, $obfuscate); 1671 1672 case 'hex' : 1673 return utf8_tohtml($email, true); 1674 1675 case 'none' : 1676 default : 1677 return $email; 1678 } 1679} 1680 1681/** 1682 * Removes quoting backslashes 1683 * 1684 * @author Andreas Gohr <andi@splitbrain.org> 1685 * 1686 * @param string $string 1687 * @param string $char backslashed character 1688 * @return string 1689 */ 1690function unslash($string, $char = "'") { 1691 return str_replace('\\'.$char, $char, $string); 1692} 1693 1694/** 1695 * Convert php.ini shorthands to byte 1696 * 1697 * On 32 bit systems values >= 2GB will fail! 1698 * 1699 * -1 (infinite size) will be reported as -1 1700 * 1701 * @link https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes 1702 * @param string $value PHP size shorthand 1703 * @return int 1704 */ 1705function php_to_byte($value) { 1706 switch (strtoupper(substr($value,-1))) { 1707 case 'G': 1708 $ret = intval(substr($value, 0, -1)) * 1024 * 1024 * 1024; 1709 break; 1710 case 'M': 1711 $ret = intval(substr($value, 0, -1)) * 1024 * 1024; 1712 break; 1713 case 'K': 1714 $ret = intval(substr($value, 0, -1)) * 1024; 1715 break; 1716 default: 1717 $ret = intval($value); 1718 break; 1719 } 1720 return $ret; 1721} 1722 1723/** 1724 * Wrapper around preg_quote adding the default delimiter 1725 * 1726 * @param string $string 1727 * @return string 1728 */ 1729function preg_quote_cb($string) { 1730 return preg_quote($string, '/'); 1731} 1732 1733/** 1734 * Shorten a given string by removing data from the middle 1735 * 1736 * You can give the string in two parts, the first part $keep 1737 * will never be shortened. The second part $short will be cut 1738 * in the middle to shorten but only if at least $min chars are 1739 * left to display it. Otherwise it will be left off. 1740 * 1741 * @param string $keep the part to keep 1742 * @param string $short the part to shorten 1743 * @param int $max maximum chars you want for the whole string 1744 * @param int $min minimum number of chars to have left for middle shortening 1745 * @param string $char the shortening character to use 1746 * @return string 1747 */ 1748function shorten($keep, $short, $max, $min = 9, $char = '…') { 1749 $max = $max - \dokuwiki\Utf8\PhpString::strlen($keep); 1750 if($max < $min) return $keep; 1751 $len = \dokuwiki\Utf8\PhpString::strlen($short); 1752 if($len <= $max) return $keep.$short; 1753 $half = floor($max / 2); 1754 return $keep . 1755 \dokuwiki\Utf8\PhpString::substr($short, 0, $half - 1) . 1756 $char . 1757 \dokuwiki\Utf8\PhpString::substr($short, $len - $half); 1758} 1759 1760/** 1761 * Return the users real name or e-mail address for use 1762 * in page footer and recent changes pages 1763 * 1764 * @param string|null $username or null when currently logged-in user should be used 1765 * @param bool $textonly true returns only plain text, true allows returning html 1766 * @return string html or plain text(not escaped) of formatted user name 1767 * 1768 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1769 */ 1770function editorinfo($username, $textonly = false) { 1771 return userlink($username, $textonly); 1772} 1773 1774/** 1775 * Returns users realname w/o link 1776 * 1777 * @param string|null $username or null when currently logged-in user should be used 1778 * @param bool $textonly true returns only plain text, true allows returning html 1779 * @return string html or plain text(not escaped) of formatted user name 1780 * 1781 * @triggers COMMON_USER_LINK 1782 */ 1783function userlink($username = null, $textonly = false) { 1784 global $conf, $INFO; 1785 /** @var AuthPlugin $auth */ 1786 global $auth; 1787 /** @var Input $INPUT */ 1788 global $INPUT; 1789 1790 // prepare initial event data 1791 $data = array( 1792 'username' => $username, // the unique user name 1793 'name' => '', 1794 'link' => array( //setting 'link' to false disables linking 1795 'target' => '', 1796 'pre' => '', 1797 'suf' => '', 1798 'style' => '', 1799 'more' => '', 1800 'url' => '', 1801 'title' => '', 1802 'class' => '' 1803 ), 1804 'userlink' => '', // formatted user name as will be returned 1805 'textonly' => $textonly 1806 ); 1807 if($username === null) { 1808 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1809 if($textonly){ 1810 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1811 }else { 1812 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '. 1813 '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1814 } 1815 } 1816 1817 $evt = new Event('COMMON_USER_LINK', $data); 1818 if($evt->advise_before(true)) { 1819 if(empty($data['name'])) { 1820 if($auth) $info = $auth->getUserData($username); 1821 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1822 switch($conf['showuseras']) { 1823 case 'username': 1824 case 'username_link': 1825 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1826 break; 1827 case 'email': 1828 case 'email_link': 1829 $data['name'] = obfuscate($info['mail']); 1830 break; 1831 } 1832 } else { 1833 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1834 } 1835 } 1836 1837 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1838 static $xhtml_renderer = null; 1839 1840 if(!$data['textonly'] && empty($data['link']['url'])) { 1841 1842 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1843 if(!isset($info)) { 1844 if($auth) $info = $auth->getUserData($username); 1845 } 1846 if(isset($info) && $info) { 1847 if($conf['showuseras'] == 'email_link') { 1848 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1849 } else { 1850 if(is_null($xhtml_renderer)) { 1851 $xhtml_renderer = p_get_renderer('xhtml'); 1852 } 1853 if(empty($xhtml_renderer->interwiki)) { 1854 $xhtml_renderer->interwiki = getInterwiki(); 1855 } 1856 $shortcut = 'user'; 1857 $exists = null; 1858 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1859 $data['link']['class'] .= ' interwiki iw_user'; 1860 if($exists !== null) { 1861 if($exists) { 1862 $data['link']['class'] .= ' wikilink1'; 1863 } else { 1864 $data['link']['class'] .= ' wikilink2'; 1865 $data['link']['rel'] = 'nofollow'; 1866 } 1867 } 1868 } 1869 } else { 1870 $data['textonly'] = true; 1871 } 1872 1873 } else { 1874 $data['textonly'] = true; 1875 } 1876 } 1877 1878 if($data['textonly']) { 1879 $data['userlink'] = $data['name']; 1880 } else { 1881 $data['link']['name'] = $data['name']; 1882 if(is_null($xhtml_renderer)) { 1883 $xhtml_renderer = p_get_renderer('xhtml'); 1884 } 1885 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1886 } 1887 } 1888 $evt->advise_after(); 1889 unset($evt); 1890 1891 return $data['userlink']; 1892} 1893 1894/** 1895 * Returns the path to a image file for the currently chosen license. 1896 * When no image exists, returns an empty string 1897 * 1898 * @author Andreas Gohr <andi@splitbrain.org> 1899 * 1900 * @param string $type - type of image 'badge' or 'button' 1901 * @return string 1902 */ 1903function license_img($type) { 1904 global $license; 1905 global $conf; 1906 if(!$conf['license']) return ''; 1907 if(!is_array($license[$conf['license']])) return ''; 1908 $try = array(); 1909 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1910 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1911 if(substr($conf['license'], 0, 3) == 'cc-') { 1912 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1913 } 1914 foreach($try as $src) { 1915 if(file_exists(DOKU_INC.$src)) return $src; 1916 } 1917 return ''; 1918} 1919 1920/** 1921 * Checks if the given amount of memory is available 1922 * 1923 * If the memory_get_usage() function is not available the 1924 * function just assumes $bytes of already allocated memory 1925 * 1926 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1927 * @author Andreas Gohr <andi@splitbrain.org> 1928 * 1929 * @param int $mem Size of memory you want to allocate in bytes 1930 * @param int $bytes already allocated memory (see above) 1931 * @return bool 1932 */ 1933function is_mem_available($mem, $bytes = 1048576) { 1934 $limit = trim(ini_get('memory_limit')); 1935 if(empty($limit)) return true; // no limit set! 1936 if($limit == -1) return true; // unlimited 1937 1938 // parse limit to bytes 1939 $limit = php_to_byte($limit); 1940 1941 // get used memory if possible 1942 if(function_exists('memory_get_usage')) { 1943 $used = memory_get_usage(); 1944 } else { 1945 $used = $bytes; 1946 } 1947 1948 if($used + $mem > $limit) { 1949 return false; 1950 } 1951 1952 return true; 1953} 1954 1955/** 1956 * Send a HTTP redirect to the browser 1957 * 1958 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1959 * 1960 * @link http://support.microsoft.com/kb/q176113/ 1961 * @author Andreas Gohr <andi@splitbrain.org> 1962 * 1963 * @param string $url url being directed to 1964 */ 1965function send_redirect($url) { 1966 $url = stripctl($url); // defend against HTTP Response Splitting 1967 1968 /* @var Input $INPUT */ 1969 global $INPUT; 1970 1971 //are there any undisplayed messages? keep them in session for display 1972 global $MSG; 1973 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1974 //reopen session, store data and close session again 1975 @session_start(); 1976 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1977 } 1978 1979 // always close the session 1980 session_write_close(); 1981 1982 // check if running on IIS < 6 with CGI-PHP 1983 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1984 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1985 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1986 $matches[1] < 6 1987 ) { 1988 header('Refresh: 0;url='.$url); 1989 } else { 1990 header('Location: '.$url); 1991 } 1992 1993 // no exits during unit tests 1994 if(defined('DOKU_UNITTEST')) { 1995 // pass info about the redirect back to the test suite 1996 $testRequest = TestRequest::getRunning(); 1997 if($testRequest !== null) { 1998 $testRequest->addData('send_redirect', $url); 1999 } 2000 return; 2001 } 2002 2003 exit; 2004} 2005 2006/** 2007 * Validate a value using a set of valid values 2008 * 2009 * This function checks whether a specified value is set and in the array 2010 * $valid_values. If not, the function returns a default value or, if no 2011 * default is specified, throws an exception. 2012 * 2013 * @param string $param The name of the parameter 2014 * @param array $valid_values A set of valid values; Optionally a default may 2015 * be marked by the key “default”. 2016 * @param array $array The array containing the value (typically $_POST 2017 * or $_GET) 2018 * @param string $exc The text of the raised exception 2019 * 2020 * @throws Exception 2021 * @return mixed 2022 * @author Adrian Lang <lang@cosmocode.de> 2023 */ 2024function valid_input_set($param, $valid_values, $array, $exc = '') { 2025 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 2026 return $array[$param]; 2027 } elseif(isset($valid_values['default'])) { 2028 return $valid_values['default']; 2029 } else { 2030 throw new Exception($exc); 2031 } 2032} 2033 2034/** 2035 * Read a preference from the DokuWiki cookie 2036 * (remembering both keys & values are urlencoded) 2037 * 2038 * @param string $pref preference key 2039 * @param mixed $default value returned when preference not found 2040 * @return string preference value 2041 */ 2042function get_doku_pref($pref, $default) { 2043 $enc_pref = urlencode($pref); 2044 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 2045 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2046 $cnt = count($parts); 2047 2048 // due to #2721 there might be duplicate entries, 2049 // so we read from the end 2050 for($i = $cnt-2; $i >= 0; $i -= 2) { 2051 if($parts[$i] == $enc_pref) { 2052 return urldecode($parts[$i + 1]); 2053 } 2054 } 2055 } 2056 return $default; 2057} 2058 2059/** 2060 * Add a preference to the DokuWiki cookie 2061 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 2062 * Remove it by setting $val to false 2063 * 2064 * @param string $pref preference key 2065 * @param string $val preference value 2066 */ 2067function set_doku_pref($pref, $val) { 2068 global $conf; 2069 $orig = get_doku_pref($pref, false); 2070 $cookieVal = ''; 2071 2072 if($orig !== false && ($orig !== $val)) { 2073 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2074 $cnt = count($parts); 2075 // urlencode $pref for the comparison 2076 $enc_pref = rawurlencode($pref); 2077 $seen = false; 2078 for ($i = 0; $i < $cnt; $i += 2) { 2079 if ($parts[$i] == $enc_pref) { 2080 if (!$seen){ 2081 if ($val !== false) { 2082 $parts[$i + 1] = rawurlencode($val); 2083 } else { 2084 unset($parts[$i]); 2085 unset($parts[$i + 1]); 2086 } 2087 $seen = true; 2088 } else { 2089 // no break because we want to remove duplicate entries 2090 unset($parts[$i]); 2091 unset($parts[$i + 1]); 2092 } 2093 } 2094 } 2095 $cookieVal = implode('#', $parts); 2096 } else if ($orig === false && $val !== false) { 2097 $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'] . '#' : '') . 2098 rawurlencode($pref) . '#' . rawurlencode($val); 2099 } 2100 2101 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 2102 if(defined('DOKU_UNITTEST')) { 2103 $_COOKIE['DOKU_PREFS'] = $cookieVal; 2104 }else{ 2105 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 2106 } 2107} 2108 2109/** 2110 * Strips source mapping declarations from given text #601 2111 * 2112 * @param string &$text reference to the CSS or JavaScript code to clean 2113 */ 2114function stripsourcemaps(&$text){ 2115 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 2116} 2117 2118/** 2119 * Returns the contents of a given SVG file for embedding 2120 * 2121 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through 2122 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small 2123 * files are embedded. 2124 * 2125 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG! 2126 * 2127 * @param string $file full path to the SVG file 2128 * @param int $maxsize maximum allowed size for the SVG to be embedded 2129 * @return string|false the SVG content, false if the file couldn't be loaded 2130 */ 2131function inlineSVG($file, $maxsize = 2048) { 2132 $file = trim($file); 2133 if($file === '') return false; 2134 if(!file_exists($file)) return false; 2135 if(filesize($file) > $maxsize) return false; 2136 if(!is_readable($file)) return false; 2137 $content = file_get_contents($file); 2138 $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments 2139 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header 2140 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type 2141 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags 2142 $content = trim($content); 2143 if(substr($content, 0, 5) !== '<svg ') return false; 2144 return $content; 2145} 2146 2147//Setup VIM: ex: et ts=2 : 2148