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