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