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