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