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