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