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 $tentative = true; 1355 DETERMINE_CHANGE_TYPE: { 1356 if ($data['revertFrom']) { 1357 // new text may differ from exact revert revision 1358 $data['changeType'] = DOKU_CHANGE_TYPE_REVERT; 1359 $data['changeInfo'] = $REV; 1360 } elseif (trim($data['newContent']) == '') { 1361 // empty or whitespace only content deletes 1362 $data['changeType'] = DOKU_CHANGE_TYPE_DELETE; 1363 } elseif (!file_exists($pagefile)) { 1364 $data['changeType'] = DOKU_CHANGE_TYPE_CREATE; 1365 } else { 1366 // minor edits allowable only for logged in users 1367 $is_minor_change = ($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')); 1368 $data['changeType'] = $is_minor_change 1369 ? DOKU_CHANGE_TYPE_MINOR_EDIT 1370 : DOKU_CHANGE_TYPE_EDIT; 1371 } 1372 if (!$tentative) goto MAIN; 1373 /* FIXME: reluctantly use of goto statement to avoid declare new function in this file. 1374 nice to have a dedicated class that implements saveWikiText(), as well as 1375 determineChangeType(), saveOldRevision(), detectExternalEdit(), ... 1376 */ 1377 } 1378 1379 $event = new Event('COMMON_WIKIPAGE_SAVE', $data); 1380 if (!$event->advise_before()) return; 1381 1382 // if the content has not been changed, no save happens (plugins may override this) 1383 if (!$data['contentChanged']) return; 1384 1385 // Confirm again both event data and pagefile that may altered by event handlers 1386 // 1387 // Event handlers may also modify the pagefile as well as oldRevision of event data. 1388 // For example, we can imagine an action plugin that provides alternative approach for 1389 // handling external edits in changelog; merging early external edits into one normal 1390 // edit entry instead of separating two entries of external and normal edits. 1391 // This will be achievable if the pagefile could be restored to the last revision 1392 // during $event->advise_before() using attic data. 1393 // 1394 $tentative = false; 1395 goto DETERMINE_CHANGE_TYPE; 1396 MAIN: 1397 // Check whether the pagefile has modified during $event->advise_before() 1398 clearstatcache(false, $pagefile); 1399 $fileRev = @filemtime($pagefile); 1400 if ($fileRev === $currentRevision) { 1401 // pagefile has not touched by plugin 1402 // add a potential external edit entry to changelog and store it into attic 1403 detectExternalEdit($id); 1404 $filesize_old = $currentSize; 1405 } else { 1406 // pagefile has modified by plugin that must be responsible for changelog 1407 $filesize_old = ( 1408 $data['changeType'] == DOKU_CHANGE_TYPE_CREATE || ( 1409 $data['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($pagefile)) 1410 ) ? 0 : filesize($pagefile); 1411 } 1412 1413 // make change to the current file 1414 if ($data['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 1415 // nothing to do when the file has already deleted 1416 if (!file_exists($pagefile)) return; 1417 // autoset summary on deletion 1418 if (blank($data['summary'])) { 1419 $data['summary'] = $lang['deleted']; 1420 } 1421 // Send "update" event with empty data, so plugins can react to page deletion 1422 $ioData = array([$pagefile, '', false], getNS($id), noNS($id), false); 1423 Event::createAndTrigger('IO_WIKIPAGE_WRITE', $ioData); 1424 // pre-save deleted revision 1425 @touch($pagefile); 1426 clearstatcache(); 1427 $data['newRevision'] = saveOldRevision($id); 1428 // remove empty file 1429 @unlink($pagefile); 1430 $filesize_new = 0; 1431 // don't remove old meta info as it should be saved, plugins can use 1432 // IO_WIKIPAGE_WRITE for removing their metadata... 1433 // purge non-persistant meta data 1434 p_purge_metadata($id); 1435 // remove empty namespaces 1436 io_sweepNS($id, 'datadir'); 1437 io_sweepNS($id, 'mediadir'); 1438 } else { 1439 // save file (namespace dir is created in io_writeWikiPage) 1440 io_writeWikiPage($pagefile, $data['newContent'], $id); 1441 // pre-save the revision, to keep the attic in sync 1442 $data['newRevision'] = saveOldRevision($id); 1443 $filesize_new = filesize($pagefile); 1444 } 1445 $data['sizechange'] = $filesize_new - $filesize_old; 1446 1447 $event->advise_after(); 1448 1449 // adds an entry to the changelog and saves the metadata for the page 1450 addLogEntry( 1451 $data['newRevision'], 1452 $id, 1453 $data['changeType'], 1454 $data['summary'], 1455 $data['changeInfo'], 1456 null, 1457 $data['sizechange'] 1458 ); 1459 1460 // send notify mails 1461 notify($id, 'admin', $data['oldRevision'], $data['summary'], $minor, $data['newRevision']); 1462 notify($id, 'subscribers', $data['oldRevision'], $data['summary'], $minor, $data['newRevision']); 1463 1464 // update the purgefile (timestamp of the last time anything within the wiki was changed) 1465 io_saveFile($conf['cachedir'].'/purgefile', time()); 1466 1467 // if useheading is enabled, purge the cache of all linking pages 1468 if (useHeading('content')) { 1469 $pages = ft_backlinks($id, true); 1470 foreach ($pages as $page) { 1471 $cache = new CacheRenderer($page, wikiFN($page), 'xhtml'); 1472 $cache->removeCache(); 1473 } 1474 } 1475} 1476 1477/** 1478 * moves the current version to the attic and returns its revision date 1479 * 1480 * @author Andreas Gohr <andi@splitbrain.org> 1481 * 1482 * @param string $id page id 1483 * @return int|string revision timestamp 1484 */ 1485function saveOldRevision($id) { 1486 $oldfile = wikiFN($id); 1487 if (!file_exists($oldfile)) return ''; 1488 $date = filemtime($oldfile); 1489 $newfile = wikiFN($id, $date); 1490 io_writeWikiPage($newfile, rawWiki($id), $id, $date); 1491 return $date; 1492} 1493 1494/** 1495 * Sends a notify mail on page change or registration 1496 * 1497 * @param string $id The changed page 1498 * @param string $who Who to notify (admin|subscribers|register) 1499 * @param int|string $rev Old page revision 1500 * @param string $summary What changed 1501 * @param boolean $minor Is this a minor edit? 1502 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1503 * @param int|string $current_rev New page revision 1504 * @return bool 1505 * 1506 * @author Andreas Gohr <andi@splitbrain.org> 1507 */ 1508function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) { 1509 global $conf; 1510 /* @var Input $INPUT */ 1511 global $INPUT; 1512 1513 // decide if there is something to do, eg. whom to mail 1514 if($who == 'admin') { 1515 if(empty($conf['notify'])) return false; //notify enabled? 1516 $tpl = 'mailtext'; 1517 $to = $conf['notify']; 1518 } elseif($who == 'subscribers') { 1519 if(!actionOK('subscribe')) return false; //subscribers enabled? 1520 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1521 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1522 Event::createAndTrigger( 1523 'COMMON_NOTIFY_ADDRESSLIST', $data, 1524 array(new SubscriberManager(), 'notifyAddresses') 1525 ); 1526 $to = $data['addresslist']; 1527 if(empty($to)) return false; 1528 $tpl = 'subscr_single'; 1529 } else { 1530 return false; //just to be safe 1531 } 1532 1533 // prepare content 1534 $subscription = new PageSubscriptionSender(); 1535 return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev); 1536} 1537 1538/** 1539 * extracts the query from a search engine referrer 1540 * 1541 * @author Andreas Gohr <andi@splitbrain.org> 1542 * @author Todd Augsburger <todd@rollerorgans.com> 1543 * 1544 * @return array|string 1545 */ 1546function getGoogleQuery() { 1547 /* @var Input $INPUT */ 1548 global $INPUT; 1549 1550 if(!$INPUT->server->has('HTTP_REFERER')) { 1551 return ''; 1552 } 1553 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1554 1555 // only handle common SEs 1556 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1557 1558 $query = array(); 1559 parse_str($url['query'], $query); 1560 1561 $q = ''; 1562 if(isset($query['q'])){ 1563 $q = $query['q']; 1564 }elseif(isset($query['p'])){ 1565 $q = $query['p']; 1566 }elseif(isset($query['query'])){ 1567 $q = $query['query']; 1568 } 1569 $q = trim($q); 1570 1571 if(!$q) return ''; 1572 // ignore if query includes a full URL 1573 if(strpos($q, '//') !== false) return ''; 1574 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1575 return $q; 1576} 1577 1578/** 1579 * Return the human readable size of a file 1580 * 1581 * @param int $size A file size 1582 * @param int $dec A number of decimal places 1583 * @return string human readable size 1584 * 1585 * @author Martin Benjamin <b.martin@cybernet.ch> 1586 * @author Aidan Lister <aidan@php.net> 1587 * @version 1.0.0 1588 */ 1589function filesize_h($size, $dec = 1) { 1590 $sizes = array('B', 'KB', 'MB', 'GB'); 1591 $count = count($sizes); 1592 $i = 0; 1593 1594 while($size >= 1024 && ($i < $count - 1)) { 1595 $size /= 1024; 1596 $i++; 1597 } 1598 1599 return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space 1600} 1601 1602/** 1603 * Return the given timestamp as human readable, fuzzy age 1604 * 1605 * @author Andreas Gohr <gohr@cosmocode.de> 1606 * 1607 * @param int $dt timestamp 1608 * @return string 1609 */ 1610function datetime_h($dt) { 1611 global $lang; 1612 1613 $ago = time() - $dt; 1614 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1615 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1616 } 1617 if($ago > 24 * 60 * 60 * 30 * 2) { 1618 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1619 } 1620 if($ago > 24 * 60 * 60 * 7 * 2) { 1621 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1622 } 1623 if($ago > 24 * 60 * 60 * 2) { 1624 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1625 } 1626 if($ago > 60 * 60 * 2) { 1627 return sprintf($lang['hours'], round($ago / (60 * 60))); 1628 } 1629 if($ago > 60 * 2) { 1630 return sprintf($lang['minutes'], round($ago / (60))); 1631 } 1632 return sprintf($lang['seconds'], $ago); 1633} 1634 1635/** 1636 * Wraps around strftime but provides support for fuzzy dates 1637 * 1638 * The format default to $conf['dformat']. It is passed to 1639 * strftime - %f can be used to get the value from datetime_h() 1640 * 1641 * @see datetime_h 1642 * @author Andreas Gohr <gohr@cosmocode.de> 1643 * 1644 * @param int|null $dt timestamp when given, null will take current timestamp 1645 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1646 * @return string 1647 */ 1648function dformat($dt = null, $format = '') { 1649 global $conf; 1650 1651 if(is_null($dt)) $dt = time(); 1652 $dt = (int) $dt; 1653 if(!$format) $format = $conf['dformat']; 1654 1655 $format = str_replace('%f', datetime_h($dt), $format); 1656 return strftime($format, $dt); 1657} 1658 1659/** 1660 * Formats a timestamp as ISO 8601 date 1661 * 1662 * @author <ungu at terong dot com> 1663 * @link http://php.net/manual/en/function.date.php#54072 1664 * 1665 * @param int $int_date current date in UNIX timestamp 1666 * @return string 1667 */ 1668function date_iso8601($int_date) { 1669 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1670 $pre_timezone = date('O', $int_date); 1671 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1672 $date_mod .= $time_zone; 1673 return $date_mod; 1674} 1675 1676/** 1677 * return an obfuscated email address in line with $conf['mailguard'] setting 1678 * 1679 * @author Harry Fuecks <hfuecks@gmail.com> 1680 * @author Christopher Smith <chris@jalakai.co.uk> 1681 * 1682 * @param string $email email address 1683 * @return string 1684 */ 1685function obfuscate($email) { 1686 global $conf; 1687 1688 switch($conf['mailguard']) { 1689 case 'visible' : 1690 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1691 return strtr($email, $obfuscate); 1692 1693 case 'hex' : 1694 return \dokuwiki\Utf8\Conversion::toHtml($email, true); 1695 1696 case 'none' : 1697 default : 1698 return $email; 1699 } 1700} 1701 1702/** 1703 * Removes quoting backslashes 1704 * 1705 * @author Andreas Gohr <andi@splitbrain.org> 1706 * 1707 * @param string $string 1708 * @param string $char backslashed character 1709 * @return string 1710 */ 1711function unslash($string, $char = "'") { 1712 return str_replace('\\'.$char, $char, $string); 1713} 1714 1715/** 1716 * Convert php.ini shorthands to byte 1717 * 1718 * On 32 bit systems values >= 2GB will fail! 1719 * 1720 * -1 (infinite size) will be reported as -1 1721 * 1722 * @link https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes 1723 * @param string $value PHP size shorthand 1724 * @return int 1725 */ 1726function php_to_byte($value) { 1727 switch (strtoupper(substr($value,-1))) { 1728 case 'G': 1729 $ret = intval(substr($value, 0, -1)) * 1024 * 1024 * 1024; 1730 break; 1731 case 'M': 1732 $ret = intval(substr($value, 0, -1)) * 1024 * 1024; 1733 break; 1734 case 'K': 1735 $ret = intval(substr($value, 0, -1)) * 1024; 1736 break; 1737 default: 1738 $ret = intval($value); 1739 break; 1740 } 1741 return $ret; 1742} 1743 1744/** 1745 * Wrapper around preg_quote adding the default delimiter 1746 * 1747 * @param string $string 1748 * @return string 1749 */ 1750function preg_quote_cb($string) { 1751 return preg_quote($string, '/'); 1752} 1753 1754/** 1755 * Shorten a given string by removing data from the middle 1756 * 1757 * You can give the string in two parts, the first part $keep 1758 * will never be shortened. The second part $short will be cut 1759 * in the middle to shorten but only if at least $min chars are 1760 * left to display it. Otherwise it will be left off. 1761 * 1762 * @param string $keep the part to keep 1763 * @param string $short the part to shorten 1764 * @param int $max maximum chars you want for the whole string 1765 * @param int $min minimum number of chars to have left for middle shortening 1766 * @param string $char the shortening character to use 1767 * @return string 1768 */ 1769function shorten($keep, $short, $max, $min = 9, $char = '…') { 1770 $max = $max - \dokuwiki\Utf8\PhpString::strlen($keep); 1771 if($max < $min) return $keep; 1772 $len = \dokuwiki\Utf8\PhpString::strlen($short); 1773 if($len <= $max) return $keep.$short; 1774 $half = floor($max / 2); 1775 return $keep . 1776 \dokuwiki\Utf8\PhpString::substr($short, 0, $half - 1) . 1777 $char . 1778 \dokuwiki\Utf8\PhpString::substr($short, $len - $half); 1779} 1780 1781/** 1782 * Return the users real name or e-mail address for use 1783 * in page footer and recent changes pages 1784 * 1785 * @param string|null $username or null when currently logged-in user should be used 1786 * @param bool $textonly true returns only plain text, true allows returning html 1787 * @return string html or plain text(not escaped) of formatted user name 1788 * 1789 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1790 */ 1791function editorinfo($username, $textonly = false) { 1792 return userlink($username, $textonly); 1793} 1794 1795/** 1796 * Returns users realname w/o link 1797 * 1798 * @param string|null $username or null when currently logged-in user should be used 1799 * @param bool $textonly true returns only plain text, true allows returning html 1800 * @return string html or plain text(not escaped) of formatted user name 1801 * 1802 * @triggers COMMON_USER_LINK 1803 */ 1804function userlink($username = null, $textonly = false) { 1805 global $conf, $INFO; 1806 /** @var AuthPlugin $auth */ 1807 global $auth; 1808 /** @var Input $INPUT */ 1809 global $INPUT; 1810 1811 // prepare initial event data 1812 $data = array( 1813 'username' => $username, // the unique user name 1814 'name' => '', 1815 'link' => array( //setting 'link' to false disables linking 1816 'target' => '', 1817 'pre' => '', 1818 'suf' => '', 1819 'style' => '', 1820 'more' => '', 1821 'url' => '', 1822 'title' => '', 1823 'class' => '' 1824 ), 1825 'userlink' => '', // formatted user name as will be returned 1826 'textonly' => $textonly 1827 ); 1828 if($username === null) { 1829 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1830 if($textonly){ 1831 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1832 }else { 1833 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '. 1834 '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1835 } 1836 } 1837 1838 $evt = new Event('COMMON_USER_LINK', $data); 1839 if($evt->advise_before(true)) { 1840 if(empty($data['name'])) { 1841 if($auth) $info = $auth->getUserData($username); 1842 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1843 switch($conf['showuseras']) { 1844 case 'username': 1845 case 'username_link': 1846 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1847 break; 1848 case 'email': 1849 case 'email_link': 1850 $data['name'] = obfuscate($info['mail']); 1851 break; 1852 } 1853 } else { 1854 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1855 } 1856 } 1857 1858 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1859 static $xhtml_renderer = null; 1860 1861 if(!$data['textonly'] && empty($data['link']['url'])) { 1862 1863 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1864 if(!isset($info)) { 1865 if($auth) $info = $auth->getUserData($username); 1866 } 1867 if(isset($info) && $info) { 1868 if($conf['showuseras'] == 'email_link') { 1869 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1870 } else { 1871 if(is_null($xhtml_renderer)) { 1872 $xhtml_renderer = p_get_renderer('xhtml'); 1873 } 1874 if(empty($xhtml_renderer->interwiki)) { 1875 $xhtml_renderer->interwiki = getInterwiki(); 1876 } 1877 $shortcut = 'user'; 1878 $exists = null; 1879 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1880 $data['link']['class'] .= ' interwiki iw_user'; 1881 if($exists !== null) { 1882 if($exists) { 1883 $data['link']['class'] .= ' wikilink1'; 1884 } else { 1885 $data['link']['class'] .= ' wikilink2'; 1886 $data['link']['rel'] = 'nofollow'; 1887 } 1888 } 1889 } 1890 } else { 1891 $data['textonly'] = true; 1892 } 1893 1894 } else { 1895 $data['textonly'] = true; 1896 } 1897 } 1898 1899 if($data['textonly']) { 1900 $data['userlink'] = $data['name']; 1901 } else { 1902 $data['link']['name'] = $data['name']; 1903 if(is_null($xhtml_renderer)) { 1904 $xhtml_renderer = p_get_renderer('xhtml'); 1905 } 1906 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1907 } 1908 } 1909 $evt->advise_after(); 1910 unset($evt); 1911 1912 return $data['userlink']; 1913} 1914 1915/** 1916 * Returns the path to a image file for the currently chosen license. 1917 * When no image exists, returns an empty string 1918 * 1919 * @author Andreas Gohr <andi@splitbrain.org> 1920 * 1921 * @param string $type - type of image 'badge' or 'button' 1922 * @return string 1923 */ 1924function license_img($type) { 1925 global $license; 1926 global $conf; 1927 if(!$conf['license']) return ''; 1928 if(!is_array($license[$conf['license']])) return ''; 1929 $try = array(); 1930 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1931 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1932 if(substr($conf['license'], 0, 3) == 'cc-') { 1933 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1934 } 1935 foreach($try as $src) { 1936 if(file_exists(DOKU_INC.$src)) return $src; 1937 } 1938 return ''; 1939} 1940 1941/** 1942 * Checks if the given amount of memory is available 1943 * 1944 * If the memory_get_usage() function is not available the 1945 * function just assumes $bytes of already allocated memory 1946 * 1947 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1948 * @author Andreas Gohr <andi@splitbrain.org> 1949 * 1950 * @param int $mem Size of memory you want to allocate in bytes 1951 * @param int $bytes already allocated memory (see above) 1952 * @return bool 1953 */ 1954function is_mem_available($mem, $bytes = 1048576) { 1955 $limit = trim(ini_get('memory_limit')); 1956 if(empty($limit)) return true; // no limit set! 1957 if($limit == -1) return true; // unlimited 1958 1959 // parse limit to bytes 1960 $limit = php_to_byte($limit); 1961 1962 // get used memory if possible 1963 if(function_exists('memory_get_usage')) { 1964 $used = memory_get_usage(); 1965 } else { 1966 $used = $bytes; 1967 } 1968 1969 if($used + $mem > $limit) { 1970 return false; 1971 } 1972 1973 return true; 1974} 1975 1976/** 1977 * Send a HTTP redirect to the browser 1978 * 1979 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1980 * 1981 * @link http://support.microsoft.com/kb/q176113/ 1982 * @author Andreas Gohr <andi@splitbrain.org> 1983 * 1984 * @param string $url url being directed to 1985 */ 1986function send_redirect($url) { 1987 $url = stripctl($url); // defend against HTTP Response Splitting 1988 1989 /* @var Input $INPUT */ 1990 global $INPUT; 1991 1992 //are there any undisplayed messages? keep them in session for display 1993 global $MSG; 1994 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1995 //reopen session, store data and close session again 1996 @session_start(); 1997 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1998 } 1999 2000 // always close the session 2001 session_write_close(); 2002 2003 // check if running on IIS < 6 with CGI-PHP 2004 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 2005 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 2006 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 2007 $matches[1] < 6 2008 ) { 2009 header('Refresh: 0;url='.$url); 2010 } else { 2011 header('Location: '.$url); 2012 } 2013 2014 // no exits during unit tests 2015 if(defined('DOKU_UNITTEST')) { 2016 // pass info about the redirect back to the test suite 2017 $testRequest = TestRequest::getRunning(); 2018 if($testRequest !== null) { 2019 $testRequest->addData('send_redirect', $url); 2020 } 2021 return; 2022 } 2023 2024 exit; 2025} 2026 2027/** 2028 * Validate a value using a set of valid values 2029 * 2030 * This function checks whether a specified value is set and in the array 2031 * $valid_values. If not, the function returns a default value or, if no 2032 * default is specified, throws an exception. 2033 * 2034 * @param string $param The name of the parameter 2035 * @param array $valid_values A set of valid values; Optionally a default may 2036 * be marked by the key “default”. 2037 * @param array $array The array containing the value (typically $_POST 2038 * or $_GET) 2039 * @param string $exc The text of the raised exception 2040 * 2041 * @throws Exception 2042 * @return mixed 2043 * @author Adrian Lang <lang@cosmocode.de> 2044 */ 2045function valid_input_set($param, $valid_values, $array, $exc = '') { 2046 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 2047 return $array[$param]; 2048 } elseif(isset($valid_values['default'])) { 2049 return $valid_values['default']; 2050 } else { 2051 throw new Exception($exc); 2052 } 2053} 2054 2055/** 2056 * Read a preference from the DokuWiki cookie 2057 * (remembering both keys & values are urlencoded) 2058 * 2059 * @param string $pref preference key 2060 * @param mixed $default value returned when preference not found 2061 * @return string preference value 2062 */ 2063function get_doku_pref($pref, $default) { 2064 $enc_pref = urlencode($pref); 2065 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 2066 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2067 $cnt = count($parts); 2068 2069 // due to #2721 there might be duplicate entries, 2070 // so we read from the end 2071 for($i = $cnt-2; $i >= 0; $i -= 2) { 2072 if($parts[$i] == $enc_pref) { 2073 return urldecode($parts[$i + 1]); 2074 } 2075 } 2076 } 2077 return $default; 2078} 2079 2080/** 2081 * Add a preference to the DokuWiki cookie 2082 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 2083 * Remove it by setting $val to false 2084 * 2085 * @param string $pref preference key 2086 * @param string $val preference value 2087 */ 2088function set_doku_pref($pref, $val) { 2089 global $conf; 2090 $orig = get_doku_pref($pref, false); 2091 $cookieVal = ''; 2092 2093 if($orig !== false && ($orig !== $val)) { 2094 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2095 $cnt = count($parts); 2096 // urlencode $pref for the comparison 2097 $enc_pref = rawurlencode($pref); 2098 $seen = false; 2099 for ($i = 0; $i < $cnt; $i += 2) { 2100 if ($parts[$i] == $enc_pref) { 2101 if (!$seen){ 2102 if ($val !== false) { 2103 $parts[$i + 1] = rawurlencode($val); 2104 } else { 2105 unset($parts[$i]); 2106 unset($parts[$i + 1]); 2107 } 2108 $seen = true; 2109 } else { 2110 // no break because we want to remove duplicate entries 2111 unset($parts[$i]); 2112 unset($parts[$i + 1]); 2113 } 2114 } 2115 } 2116 $cookieVal = implode('#', $parts); 2117 } else if ($orig === false && $val !== false) { 2118 $cookieVal = (isset($_COOKIE['DOKU_PREFS']) ? $_COOKIE['DOKU_PREFS'] . '#' : '') . 2119 rawurlencode($pref) . '#' . rawurlencode($val); 2120 } 2121 2122 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 2123 if(defined('DOKU_UNITTEST')) { 2124 $_COOKIE['DOKU_PREFS'] = $cookieVal; 2125 }else{ 2126 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 2127 } 2128} 2129 2130/** 2131 * Strips source mapping declarations from given text #601 2132 * 2133 * @param string &$text reference to the CSS or JavaScript code to clean 2134 */ 2135function stripsourcemaps(&$text){ 2136 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 2137} 2138 2139/** 2140 * Returns the contents of a given SVG file for embedding 2141 * 2142 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through 2143 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small 2144 * files are embedded. 2145 * 2146 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG! 2147 * 2148 * @param string $file full path to the SVG file 2149 * @param int $maxsize maximum allowed size for the SVG to be embedded 2150 * @return string|false the SVG content, false if the file couldn't be loaded 2151 */ 2152function inlineSVG($file, $maxsize = 2048) { 2153 $file = trim($file); 2154 if($file === '') return false; 2155 if(!file_exists($file)) return false; 2156 if(filesize($file) > $maxsize) return false; 2157 if(!is_readable($file)) return false; 2158 $content = file_get_contents($file); 2159 $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments 2160 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header 2161 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type 2162 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags 2163 $content = trim($content); 2164 if(substr($content, 0, 5) !== '<svg ') return false; 2165 return $content; 2166} 2167 2168//Setup VIM: ex: et ts=2 : 2169