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