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