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