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 } else { 207 $info['perm'] = auth_aclcheck($id, '', null); 208 $info['client'] = clientIP(true); 209 } 210 211 $info['namespace'] = getNS($id); 212 213 // mobile detection 214 if ($htmlClient) { 215 $info['ismobile'] = clientismobile(); 216 } 217 218 return $info; 219 } 220 221/** 222 * Return info about the current document as associative 223 * array. 224 * 225 * @author Andreas Gohr <andi@splitbrain.org> 226 * 227 * @return array with info about current document 228 */ 229function pageinfo() 230{ 231 global $ID; 232 global $REV; 233 global $RANGE; 234 global $lang; 235 /* @var Input $INPUT */ 236 global $INPUT; 237 238 $info = basicinfo($ID); 239 240 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml 241 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary 242 $info['id'] = $ID; 243 $info['rev'] = $REV; 244 245 $subManager = new SubscriberManager(); 246 $info['subscribed'] = $subManager->userSubscription(); 247 248 $info['locked'] = checklock($ID); 249 $info['filepath'] = wikiFN($ID); 250 $info['exists'] = file_exists($info['filepath']); 251 $info['currentrev'] = @filemtime($info['filepath']); 252 253 if ($REV) { 254 //check if current revision was meant 255 if ($info['exists'] && ($info['currentrev'] == $REV)) { 256 $REV = ''; 257 } elseif ($RANGE) { 258 //section editing does not work with old revisions! 259 $REV = ''; 260 $RANGE = ''; 261 msg($lang['nosecedit'], 0); 262 } else { 263 //really use old revision 264 $info['filepath'] = wikiFN($ID, $REV); 265 $info['exists'] = file_exists($info['filepath']); 266 } 267 } 268 $info['rev'] = $REV; 269 if ($info['exists']) { 270 $info['writable'] = (is_writable($info['filepath']) && $info['perm'] >= AUTH_EDIT); 271 } else { 272 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 273 } 274 $info['editable'] = ($info['writable'] && empty($info['locked'])); 275 $info['lastmod'] = @filemtime($info['filepath']); 276 277 //load page meta data 278 $info['meta'] = p_get_metadata($ID); 279 280 //who's the editor 281 $pagelog = new PageChangeLog($ID, 1024); 282 if ($REV) { 283 $revinfo = $pagelog->getRevisionInfo($REV); 284 } elseif (!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) { 285 $revinfo = $info['meta']['last_change']; 286 } else { 287 $revinfo = $pagelog->getRevisionInfo($info['lastmod']); 288 // cache most recent changelog line in metadata if missing and still valid 289 if ($revinfo !== false) { 290 $info['meta']['last_change'] = $revinfo; 291 p_set_metadata($ID, ['last_change' => $revinfo]); 292 } 293 } 294 //and check for an external edit 295 if ($revinfo !== false && $revinfo['date'] != $info['lastmod']) { 296 // cached changelog line no longer valid 297 $revinfo = false; 298 $info['meta']['last_change'] = $revinfo; 299 p_set_metadata($ID, ['last_change' => $revinfo]); 300 } 301 302 if ($revinfo !== false) { 303 $info['ip'] = $revinfo['ip']; 304 $info['user'] = $revinfo['user']; 305 $info['sum'] = $revinfo['sum']; 306 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID. 307 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor']. 308 309 $info['editor'] = $revinfo['user'] ?: $revinfo['ip']; 310 } else { 311 $info['ip'] = null; 312 $info['user'] = null; 313 $info['sum'] = null; 314 $info['editor'] = null; 315 } 316 317 // draft 318 $draft = new Draft($ID, $info['client']); 319 if ($draft->isDraftAvailable()) { 320 $info['draft'] = $draft->getDraftFilename(); 321 } 322 323 return $info; 324} 325 326/** 327 * Initialize and/or fill global $JSINFO with some basic info to be given to javascript 328 */ 329function jsinfo() 330{ 331 global $JSINFO, $ID, $INFO, $ACT; 332 333 if (!is_array($JSINFO)) { 334 $JSINFO = []; 335 } 336 //export minimal info to JS, plugins can add more 337 $JSINFO['id'] = $ID; 338 $JSINFO['namespace'] = isset($INFO) ? (string) $INFO['namespace'] : ''; 339 $JSINFO['ACT'] = act_clean($ACT); 340 $JSINFO['useHeadingNavigation'] = (int) useHeading('navigation'); 341 $JSINFO['useHeadingContent'] = (int) useHeading('content'); 342} 343 344/** 345 * Return information about the current media item as an associative array. 346 * 347 * @return array with info about current media item 348 */ 349function mediainfo() 350{ 351 global $NS; 352 global $IMG; 353 354 $info = basicinfo("$NS:*"); 355 $info['image'] = $IMG; 356 357 return $info; 358} 359 360/** 361 * Build an string of URL parameters 362 * 363 * @author Andreas Gohr 364 * 365 * @param array $params array with key-value pairs 366 * @param string $sep series of pairs are separated by this character 367 * @return string query string 368 */ 369function buildURLparams($params, $sep = '&') 370{ 371 $url = ''; 372 $amp = false; 373 foreach ($params as $key => $val) { 374 if ($amp) $url .= $sep; 375 376 $url .= rawurlencode($key).'='; 377 $url .= rawurlencode((string) $val); 378 $amp = true; 379 } 380 return $url; 381} 382 383/** 384 * Build an string of html tag attributes 385 * 386 * Skips keys starting with '_', values get HTML encoded 387 * 388 * @author Andreas Gohr 389 * 390 * @param array $params array with (attribute name-attribute value) pairs 391 * @param bool $skipEmptyStrings skip empty string values? 392 * @return string 393 */ 394function buildAttributes($params, $skipEmptyStrings = false) 395{ 396 $url = ''; 397 $white = false; 398 foreach ($params as $key => $val) { 399 if ($key[0] == '_') continue; 400 if ($val === '' && $skipEmptyStrings) continue; 401 if ($white) $url .= ' '; 402 403 $url .= $key.'="'; 404 $url .= hsc($val); 405 $url .= '"'; 406 $white = true; 407 } 408 return $url; 409} 410 411/** 412 * This builds the breadcrumb trail and returns it as array 413 * 414 * @author Andreas Gohr <andi@splitbrain.org> 415 * 416 * @return string[] with the data: array(pageid=>name, ... ) 417 */ 418function breadcrumbs() 419{ 420 // we prepare the breadcrumbs early for quick session closing 421 static $crumbs = null; 422 if ($crumbs != null) return $crumbs; 423 424 global $ID; 425 global $ACT; 426 global $conf; 427 global $INFO; 428 429 //first visit? 430 $crumbs = $_SESSION[DOKU_COOKIE]['bc'] ?? []; 431 //we only save on show and existing visible readable wiki documents 432 $file = wikiFN($ID); 433 if ($ACT != 'show' || $INFO['perm'] < AUTH_READ || isHiddenPage($ID) || !file_exists($file)) { 434 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 435 return $crumbs; 436 } 437 438 // page names 439 $name = noNSorNS($ID); 440 if (useHeading('navigation')) { 441 // get page title 442 $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE); 443 if ($title) { 444 $name = $title; 445 } 446 } 447 448 //remove ID from array 449 if (isset($crumbs[$ID])) { 450 unset($crumbs[$ID]); 451 } 452 453 //add to array 454 $crumbs[$ID] = $name; 455 //reduce size 456 while (count($crumbs) > $conf['breadcrumbs']) { 457 array_shift($crumbs); 458 } 459 //save to session 460 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 461 return $crumbs; 462} 463 464/** 465 * Filter for page IDs 466 * 467 * This is run on a ID before it is outputted somewhere 468 * currently used to replace the colon with something else 469 * on Windows (non-IIS) systems and to have proper URL encoding 470 * 471 * See discussions at https://github.com/dokuwiki/dokuwiki/pull/84 and 472 * https://github.com/dokuwiki/dokuwiki/pull/173 why we use a whitelist of 473 * unaffected servers instead of blacklisting affected servers here. 474 * 475 * Urlencoding is ommitted when the second parameter is false 476 * 477 * @author Andreas Gohr <andi@splitbrain.org> 478 * 479 * @param string $id pageid being filtered 480 * @param bool $ue apply urlencoding? 481 * @return string 482 */ 483function idfilter($id, $ue = true) 484{ 485 global $conf; 486 /* @var Input $INPUT */ 487 global $INPUT; 488 489 $id = (string) $id; 490 491 if ($conf['useslash'] && $conf['userewrite']) { 492 $id = strtr($id, ':', '/'); 493 } elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 494 $conf['userewrite'] && 495 strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false 496 ) { 497 $id = strtr($id, ':', ';'); 498 } 499 if ($ue) { 500 $id = rawurlencode($id); 501 $id = str_replace('%3A', ':', $id); //keep as colon 502 $id = str_replace('%3B', ';', $id); //keep as semicolon 503 $id = str_replace('%2F', '/', $id); //keep as slash 504 } 505 return $id; 506} 507 508/** 509 * This builds a link to a wikipage 510 * 511 * It handles URL rewriting and adds additional parameters 512 * 513 * @author Andreas Gohr <andi@splitbrain.org> 514 * 515 * @param string $id page id, defaults to start page 516 * @param string|array $urlParameters URL parameters, associative array recommended 517 * @param bool $absolute request an absolute URL instead of relative 518 * @param string $separator parameter separator 519 * @return string 520 */ 521function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&') 522{ 523 global $conf; 524 if (is_array($urlParameters)) { 525 if (isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']); 526 if (isset($urlParameters['at']) && $conf['date_at_format']) { 527 $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']); 528 } 529 $urlParameters = buildURLparams($urlParameters, $separator); 530 } else { 531 $urlParameters = str_replace(',', $separator, $urlParameters); 532 } 533 if ($id === '') { 534 $id = $conf['start']; 535 } 536 $id = idfilter($id); 537 if ($absolute) { 538 $xlink = DOKU_URL; 539 } else { 540 $xlink = DOKU_BASE; 541 } 542 543 if ($conf['userewrite'] == 2) { 544 $xlink .= DOKU_SCRIPT.'/'.$id; 545 if ($urlParameters) $xlink .= '?'.$urlParameters; 546 } elseif ($conf['userewrite']) { 547 $xlink .= $id; 548 if ($urlParameters) $xlink .= '?'.$urlParameters; 549 } elseif ($id !== '') { 550 $xlink .= DOKU_SCRIPT.'?id='.$id; 551 if ($urlParameters) $xlink .= $separator.$urlParameters; 552 } else { 553 $xlink .= DOKU_SCRIPT; 554 if ($urlParameters) $xlink .= '?'.$urlParameters; 555 } 556 557 return $xlink; 558} 559 560/** 561 * This builds a link to an alternate page format 562 * 563 * Handles URL rewriting if enabled. Follows the style of wl(). 564 * 565 * @author Ben Coburn <btcoburn@silicodon.net> 566 * @param string $id page id, defaults to start page 567 * @param string $format the export renderer to use 568 * @param string|array $urlParameters URL parameters, associative array recommended 569 * @param bool $abs request an absolute URL instead of relative 570 * @param string $sep parameter separator 571 * @return string 572 */ 573function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&') 574{ 575 global $conf; 576 if (is_array($urlParameters)) { 577 $urlParameters = buildURLparams($urlParameters, $sep); 578 } else { 579 $urlParameters = str_replace(',', $sep, $urlParameters); 580 } 581 582 $format = rawurlencode($format); 583 $id = idfilter($id); 584 if ($abs) { 585 $xlink = DOKU_URL; 586 } else { 587 $xlink = DOKU_BASE; 588 } 589 590 if ($conf['userewrite'] == 2) { 591 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 592 if ($urlParameters) $xlink .= $sep.$urlParameters; 593 } elseif ($conf['userewrite'] == 1) { 594 $xlink .= '_export/'.$format.'/'.$id; 595 if ($urlParameters) $xlink .= '?'.$urlParameters; 596 } else { 597 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 598 if ($urlParameters) $xlink .= $sep.$urlParameters; 599 } 600 601 return $xlink; 602} 603 604/** 605 * Build a link to a media file 606 * 607 * Will return a link to the detail page if $direct is false 608 * 609 * The $more parameter should always be given as array, the function then 610 * will strip default parameters to produce even cleaner URLs 611 * 612 * @param string $id the media file id or URL 613 * @param mixed $more string or array with additional parameters 614 * @param bool $direct link to detail page if false 615 * @param string $sep URL parameter separator 616 * @param bool $abs Create an absolute URL 617 * @return string 618 */ 619function ml($id = '', $more = '', $direct = true, $sep = '&', $abs = false) 620{ 621 global $conf; 622 $isexternalimage = media_isexternal($id); 623 if (!$isexternalimage) { 624 $id = cleanID($id); 625 } 626 627 if (is_array($more)) { 628 // add token for resized images 629 $w = $more['w'] ?? null; 630 $h = $more['h'] ?? null; 631 if ($w || $h || $isexternalimage) { 632 $more['tok'] = media_get_token($id, $w, $h); 633 } 634 // strip defaults for shorter URLs 635 if (isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']); 636 if (empty($more['w'])) unset($more['w']); 637 if (empty($more['h'])) unset($more['h']); 638 if (isset($more['id']) && $direct) unset($more['id']); 639 if (isset($more['rev']) && !$more['rev']) unset($more['rev']); 640 $more = buildURLparams($more, $sep); 641 } else { 642 $matches = []; 643 if (preg_match_all('/\b(w|h)=(\d*)\b/', $more, $matches, PREG_SET_ORDER) || $isexternalimage) { 644 $resize = ['w'=>0, 'h'=>0]; 645 foreach ($matches as $match) { 646 $resize[$match[1]] = $match[2]; 647 } 648 $more .= $more === '' ? '' : $sep; 649 $more .= 'tok='.media_get_token($id, $resize['w'], $resize['h']); 650 } 651 $more = str_replace('cache=cache', '', $more); //skip default 652 $more = str_replace(',,', ',', $more); 653 $more = str_replace(',', $sep, $more); 654 } 655 656 if ($abs) { 657 $xlink = DOKU_URL; 658 } else { 659 $xlink = DOKU_BASE; 660 } 661 662 // external URLs are always direct without rewriting 663 if ($isexternalimage) { 664 $xlink .= 'lib/exe/fetch.php'; 665 $xlink .= '?'.$more; 666 $xlink .= $sep.'media='.rawurlencode($id); 667 return $xlink; 668 } 669 670 $id = idfilter($id); 671 672 // decide on scriptname 673 if ($direct) { 674 if ($conf['userewrite'] == 1) { 675 $script = '_media'; 676 } else { 677 $script = 'lib/exe/fetch.php'; 678 } 679 } elseif ($conf['userewrite'] == 1) { 680 $script = '_detail'; 681 } else { 682 $script = 'lib/exe/detail.php'; 683 } 684 685 // build URL based on rewrite mode 686 if ($conf['userewrite']) { 687 $xlink .= $script.'/'.$id; 688 if ($more) $xlink .= '?'.$more; 689 } elseif ($more) { 690 $xlink .= $script.'?'.$more; 691 $xlink .= $sep.'media='.$id; 692 } else { 693 $xlink .= $script.'?media='.$id; 694 } 695 696 return $xlink; 697} 698 699/** 700 * Returns the URL to the DokuWiki base script 701 * 702 * Consider using wl() instead, unless you absoutely need the doku.php endpoint 703 * 704 * @author Andreas Gohr <andi@splitbrain.org> 705 * 706 * @return string 707 */ 708function script() 709{ 710 return DOKU_BASE.DOKU_SCRIPT; 711} 712 713/** 714 * Spamcheck against wordlist 715 * 716 * Checks the wikitext against a list of blocked expressions 717 * returns true if the text contains any bad words 718 * 719 * Triggers COMMON_WORDBLOCK_BLOCKED 720 * 721 * Action Plugins can use this event to inspect the blocked data 722 * and gain information about the user who was blocked. 723 * 724 * Event data: 725 * data['matches'] - array of matches 726 * data['userinfo'] - information about the blocked user 727 * [ip] - ip address 728 * [user] - username (if logged in) 729 * [mail] - mail address (if logged in) 730 * [name] - real name (if logged in) 731 * 732 * @author Andreas Gohr <andi@splitbrain.org> 733 * @author Michael Klier <chi@chimeric.de> 734 * 735 * @param string $text - optional text to check, if not given the globals are used 736 * @return bool - true if a spam word was found 737 */ 738function checkwordblock($text = '') 739{ 740 global $TEXT; 741 global $PRE; 742 global $SUF; 743 global $SUM; 744 global $conf; 745 global $INFO; 746 /* @var Input $INPUT */ 747 global $INPUT; 748 749 if (!$conf['usewordblock']) return false; 750 751 if (!$text) $text = "$PRE $TEXT $SUF $SUM"; 752 753 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 754 // phpcs:disable Generic.Files.LineLength.TooLong 755 $text = preg_replace( 756 '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i', 757 '\1http://\2 \2\3', 758 $text 759 ); 760 // phpcs:enable 761 762 $wordblocks = getWordblocks(); 763 // read file in chunks of 200 - this should work around the 764 // MAX_PATTERN_SIZE in modern PCRE 765 $chunksize = 200; 766 767 while ($blocks = array_splice($wordblocks, 0, $chunksize)) { 768 $re = []; 769 // build regexp from blocks 770 foreach ($blocks as $block) { 771 $block = preg_replace('/#.*$/', '', $block); 772 $block = trim($block); 773 if (empty($block)) continue; 774 $re[] = $block; 775 } 776 if (count($re) && preg_match('#('.implode('|', $re).')#si', $text, $matches)) { 777 // prepare event data 778 $data = []; 779 $data['matches'] = $matches; 780 $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR'); 781 if ($INPUT->server->str('REMOTE_USER')) { 782 $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER'); 783 $data['userinfo']['name'] = $INFO['userinfo']['name']; 784 $data['userinfo']['mail'] = $INFO['userinfo']['mail']; 785 } 786 $callback = static fn() => true; 787 return Event::createAndTrigger('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 788 } 789 } 790 return false; 791} 792 793/** 794 * Return the IP of the client 795 * 796 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 797 * 798 * It returns a comma separated list of IPs if the above mentioned 799 * headers are set. If the single parameter is set, it tries to return 800 * a routable public address, prefering the ones suplied in the X 801 * headers 802 * 803 * @author Andreas Gohr <andi@splitbrain.org> 804 * 805 * @param boolean $single If set only a single IP is returned 806 * @return string 807 */ 808function clientIP($single = false) 809{ 810 /* @var Input $INPUT */ 811 global $INPUT, $conf; 812 813 $ip = []; 814 $ip[] = $INPUT->server->str('REMOTE_ADDR'); 815 if ($INPUT->server->str('HTTP_X_FORWARDED_FOR')) { 816 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR')))); 817 } 818 if ($INPUT->server->str('HTTP_X_REAL_IP')) { 819 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP')))); 820 } 821 822 // remove any non-IP stuff 823 $cnt = count($ip); 824 for ($i = 0; $i < $cnt; $i++) { 825 if (filter_var($ip[$i], FILTER_VALIDATE_IP) === false) { 826 unset($ip[$i]); 827 } 828 } 829 $ip = array_values(array_unique($ip)); 830 if ($ip === [] || !$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 831 832 if (!$single) return implode(',', $ip); 833 834 // skip trusted local addresses 835 foreach ($ip as $i) { 836 if (!empty($conf['trustedproxy']) && preg_match('/'.$conf['trustedproxy'].'/', $i)) { 837 continue; 838 } else { 839 return $i; 840 } 841 } 842 843 // still here? just use the last address 844 // this case all ips in the list are trusted 845 return $ip[count($ip)-1]; 846} 847 848/** 849 * Check if the browser is on a mobile device 850 * 851 * Adapted from the example code at url below 852 * 853 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 854 * 855 * @deprecated 2018-04-27 you probably want media queries instead anyway 856 * @return bool if true, client is mobile browser; otherwise false 857 */ 858function clientismobile() 859{ 860 /* @var Input $INPUT */ 861 global $INPUT; 862 863 if ($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true; 864 865 if (preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true; 866 867 if (!$INPUT->server->has('HTTP_USER_AGENT')) return false; 868 869 $uamatches = implode( 870 '|', 871 [ 872 'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv', 873 'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia', 874 'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-', 875 'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx', 876 'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox', 877 'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb', 878 '\d\d\di', 'moto' 879 ] 880 ); 881 882 if (preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true; 883 884 return false; 885} 886 887/** 888 * check if a given link is interwiki link 889 * 890 * @param string $link the link, e.g. "wiki>page" 891 * @return bool 892 */ 893function link_isinterwiki($link) 894{ 895 if (preg_match('/^[a-zA-Z0-9\.]+>/u', $link)) return true; 896 return false; 897} 898 899/** 900 * Convert one or more comma separated IPs to hostnames 901 * 902 * If $conf['dnslookups'] is disabled it simply returns the input string 903 * 904 * @author Glen Harris <astfgl@iamnota.org> 905 * 906 * @param string $ips comma separated list of IP addresses 907 * @return string a comma separated list of hostnames 908 */ 909function gethostsbyaddrs($ips) 910{ 911 global $conf; 912 if (!$conf['dnslookups']) return $ips; 913 914 $hosts = []; 915 $ips = explode(',', $ips); 916 917 if (is_array($ips)) { 918 foreach ($ips as $ip) { 919 $hosts[] = gethostbyaddr(trim($ip)); 920 } 921 return implode(',', $hosts); 922 } else { 923 return gethostbyaddr(trim($ips)); 924 } 925} 926 927/** 928 * Checks if a given page is currently locked. 929 * 930 * removes stale lockfiles 931 * 932 * @author Andreas Gohr <andi@splitbrain.org> 933 * 934 * @param string $id page id 935 * @return bool page is locked? 936 */ 937function checklock($id) 938{ 939 global $conf; 940 /* @var Input $INPUT */ 941 global $INPUT; 942 943 $lock = wikiLockFN($id); 944 945 //no lockfile 946 if (!file_exists($lock)) return false; 947 948 //lockfile expired 949 if ((time() - filemtime($lock)) > $conf['locktime']) { 950 @unlink($lock); 951 return false; 952 } 953 954 //my own lock 955 @[$ip, $session] = explode("\n", io_readFile($lock)); 956 if ($ip == $INPUT->server->str('REMOTE_USER') || (session_id() && $session === session_id())) { 957 return false; 958 } 959 960 return $ip; 961} 962 963/** 964 * Lock a page for editing 965 * 966 * @author Andreas Gohr <andi@splitbrain.org> 967 * 968 * @param string $id page id to lock 969 */ 970function lock($id) 971{ 972 global $conf; 973 /* @var Input $INPUT */ 974 global $INPUT; 975 976 if ($conf['locktime'] == 0) { 977 return; 978 } 979 980 $lock = wikiLockFN($id); 981 if ($INPUT->server->str('REMOTE_USER')) { 982 io_saveFile($lock, $INPUT->server->str('REMOTE_USER')); 983 } else { 984 io_saveFile($lock, clientIP()."\n".session_id()); 985 } 986} 987 988/** 989 * Unlock a page if it was locked by the user 990 * 991 * @author Andreas Gohr <andi@splitbrain.org> 992 * 993 * @param string $id page id to unlock 994 * @return bool true if a lock was removed 995 */ 996function unlock($id) 997{ 998 /* @var Input $INPUT */ 999 global $INPUT; 1000 1001 $lock = wikiLockFN($id); 1002 if (file_exists($lock)) { 1003 @[$ip, $session] = explode("\n", io_readFile($lock)); 1004 if ($ip == $INPUT->server->str('REMOTE_USER') || $session == session_id()) { 1005 @unlink($lock); 1006 return true; 1007 } 1008 } 1009 return false; 1010} 1011 1012/** 1013 * convert line ending to unix format 1014 * 1015 * also makes sure the given text is valid UTF-8 1016 * 1017 * @see formText() for 2crlf conversion 1018 * @author Andreas Gohr <andi@splitbrain.org> 1019 * 1020 * @param string $text 1021 * @return string 1022 */ 1023function cleanText($text) 1024{ 1025 $text = preg_replace("/(\015\012)|(\015)/", "\012", $text); 1026 1027 // if the text is not valid UTF-8 we simply assume latin1 1028 // this won't break any worse than it breaks with the wrong encoding 1029 // but might actually fix the problem in many cases 1030 if (!Clean::isUtf8($text)) $text = utf8_encode($text); 1031 1032 return $text; 1033} 1034 1035/** 1036 * Prepares text for print in Webforms by encoding special chars. 1037 * It also converts line endings to Windows format which is 1038 * pseudo standard for webforms. 1039 * 1040 * @see cleanText() for 2unix conversion 1041 * @author Andreas Gohr <andi@splitbrain.org> 1042 * 1043 * @param string $text 1044 * @return string 1045 */ 1046function formText($text) 1047{ 1048 $text = str_replace("\012", "\015\012", $text ?? ''); 1049 return htmlspecialchars($text); 1050} 1051 1052/** 1053 * Returns the specified local text in raw format 1054 * 1055 * @author Andreas Gohr <andi@splitbrain.org> 1056 * 1057 * @param string $id page id 1058 * @param string $ext extension of file being read, default 'txt' 1059 * @return string 1060 */ 1061function rawLocale($id, $ext = 'txt') 1062{ 1063 return io_readFile(localeFN($id, $ext)); 1064} 1065 1066/** 1067 * Returns the raw WikiText 1068 * 1069 * @author Andreas Gohr <andi@splitbrain.org> 1070 * 1071 * @param string $id page id 1072 * @param string|int $rev timestamp when a revision of wikitext is desired 1073 * @return string 1074 */ 1075function rawWiki($id, $rev = '') 1076{ 1077 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1078} 1079 1080/** 1081 * Returns the pagetemplate contents for the ID's namespace 1082 * 1083 * @triggers COMMON_PAGETPL_LOAD 1084 * @author Andreas Gohr <andi@splitbrain.org> 1085 * 1086 * @param string $id the id of the page to be created 1087 * @return string parsed pagetemplate content 1088 */ 1089function pageTemplate($id) 1090{ 1091 global $conf; 1092 1093 if (is_array($id)) $id = $id[0]; 1094 1095 // prepare initial event data 1096 $data = [ 1097 'id' => $id, // the id of the page to be created 1098 'tpl' => '', // the text used as template 1099 'tplfile' => '', // the file above text was/should be loaded from 1100 'doreplace' => true, 1101 ]; 1102 1103 $evt = new Event('COMMON_PAGETPL_LOAD', $data); 1104 if ($evt->advise_before(true)) { 1105 // the before event might have loaded the content already 1106 if (empty($data['tpl'])) { 1107 // if the before event did not set a template file, try to find one 1108 if (empty($data['tplfile'])) { 1109 $path = dirname(wikiFN($id)); 1110 if (file_exists($path.'/_template.txt')) { 1111 $data['tplfile'] = $path.'/_template.txt'; 1112 } else { 1113 // search upper namespaces for templates 1114 $len = strlen(rtrim($conf['datadir'], '/')); 1115 while (strlen($path) >= $len) { 1116 if (file_exists($path.'/__template.txt')) { 1117 $data['tplfile'] = $path.'/__template.txt'; 1118 break; 1119 } 1120 $path = substr($path, 0, strrpos($path, '/')); 1121 } 1122 } 1123 } 1124 // load the content 1125 $data['tpl'] = io_readFile($data['tplfile']); 1126 } 1127 if ($data['doreplace']) parsePageTemplate($data); 1128 } 1129 $evt->advise_after(); 1130 unset($evt); 1131 1132 return $data['tpl']; 1133} 1134 1135/** 1136 * Performs common page template replacements 1137 * This works on data from COMMON_PAGETPL_LOAD 1138 * 1139 * @author Andreas Gohr <andi@splitbrain.org> 1140 * 1141 * @param array $data array with event data 1142 * @return string 1143 */ 1144function parsePageTemplate(&$data) 1145{ 1146 /** 1147 * @var string $id the id of the page to be created 1148 * @var string $tpl the text used as template 1149 * @var string $tplfile the file above text was/should be loaded from 1150 * @var bool $doreplace should wildcard replacements be done on the text? 1151 */ 1152 extract($data); 1153 1154 global $USERINFO; 1155 global $conf; 1156 /* @var Input $INPUT */ 1157 global $INPUT; 1158 1159 // replace placeholders 1160 $file = noNS($id); 1161 $page = strtr($file, $conf['sepchar'], ' '); 1162 1163 $tpl = str_replace( 1164 [ 1165 '@ID@', 1166 '@NS@', 1167 '@CURNS@', 1168 '@!CURNS@', 1169 '@!!CURNS@', 1170 '@!CURNS!@', 1171 '@FILE@', 1172 '@!FILE@', 1173 '@!FILE!@', 1174 '@PAGE@', 1175 '@!PAGE@', 1176 '@!!PAGE@', 1177 '@!PAGE!@', 1178 '@USER@', 1179 '@NAME@', 1180 '@MAIL@', 1181 '@DATE@' 1182 ], 1183 [ 1184 $id, 1185 getNS($id), 1186 curNS($id), 1187 PhpString::ucfirst(curNS($id)), 1188 PhpString::ucwords(curNS($id)), 1189 PhpString::strtoupper(curNS($id)), 1190 $file, 1191 PhpString::ucfirst($file), 1192 PhpString::strtoupper($file), 1193 $page, 1194 PhpString::ucfirst($page), 1195 PhpString::ucwords($page), 1196 PhpString::strtoupper($page), 1197 $INPUT->server->str('REMOTE_USER'), 1198 $USERINFO ? $USERINFO['name'] : '', 1199 $USERINFO ? $USERINFO['mail'] : '', 1200 $conf['dformat'] 1201 ], 1202 $tpl 1203 ); 1204 1205 // we need the callback to work around strftime's char limit 1206 $tpl = preg_replace_callback( 1207 '/%./', 1208 static fn($m) => dformat(null, $m[0]), 1209 $tpl 1210 ); 1211 $data['tpl'] = $tpl; 1212 return $tpl; 1213} 1214 1215/** 1216 * Returns the raw Wiki Text in three slices. 1217 * 1218 * The range parameter needs to have the form "from-to" 1219 * and gives the range of the section in bytes - no 1220 * UTF-8 awareness is needed. 1221 * The returned order is prefix, section and suffix. 1222 * 1223 * @author Andreas Gohr <andi@splitbrain.org> 1224 * 1225 * @param string $range in form "from-to" 1226 * @param string $id page id 1227 * @param string $rev optional, the revision timestamp 1228 * @return string[] with three slices 1229 */ 1230function rawWikiSlices($range, $id, $rev = '') 1231{ 1232 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1233 1234 // Parse range 1235 [$from, $to] = sexplode('-', $range, 2); 1236 // Make range zero-based, use defaults if marker is missing 1237 $from = $from ? $from - 1 : (0); 1238 $to = $to ? $to - 1 : (strlen($text)); 1239 1240 $slices = []; 1241 $slices[0] = substr($text, 0, $from); 1242 $slices[1] = substr($text, $from, $to - $from); 1243 $slices[2] = substr($text, $to); 1244 return $slices; 1245} 1246 1247/** 1248 * Joins wiki text slices 1249 * 1250 * function to join the text slices. 1251 * When the pretty parameter is set to true it adds additional empty 1252 * lines between sections if needed (used on saving). 1253 * 1254 * @author Andreas Gohr <andi@splitbrain.org> 1255 * 1256 * @param string $pre prefix 1257 * @param string $text text in the middle 1258 * @param string $suf suffix 1259 * @param bool $pretty add additional empty lines between sections 1260 * @return string 1261 */ 1262function con($pre, $text, $suf, $pretty = false) 1263{ 1264 if ($pretty) { 1265 if ($pre !== '' && substr($pre, -1) !== "\n" && 1266 substr($text, 0, 1) !== "\n" 1267 ) { 1268 $pre .= "\n"; 1269 } 1270 if ($suf !== '' && substr($text, -1) !== "\n" && 1271 substr($suf, 0, 1) !== "\n" 1272 ) { 1273 $text .= "\n"; 1274 } 1275 } 1276 1277 return $pre.$text.$suf; 1278} 1279 1280/** 1281 * Checks if the current page version is newer than the last entry in the page's 1282 * changelog. If so, we assume it has been an external edit and we create an 1283 * attic copy and add a proper changelog line. 1284 * 1285 * This check is only executed when the page is about to be saved again from the 1286 * wiki, triggered in @see saveWikiText() 1287 * 1288 * @param string $id the page ID 1289 * @deprecated 2021-11-28 1290 */ 1291function detectExternalEdit($id) 1292{ 1293 dbg_deprecated(PageFile::class .'::detectExternalEdit()'); 1294 (new PageFile($id))->detectExternalEdit(); 1295} 1296 1297/** 1298 * Saves a wikitext by calling io_writeWikiPage. 1299 * Also directs changelog and attic updates. 1300 * 1301 * @author Andreas Gohr <andi@splitbrain.org> 1302 * @author Ben Coburn <btcoburn@silicodon.net> 1303 * 1304 * @param string $id page id 1305 * @param string $text wikitext being saved 1306 * @param string $summary summary of text update 1307 * @param bool $minor mark this saved version as minor update 1308 */ 1309function saveWikiText($id, $text, $summary, $minor = false) 1310{ 1311 1312 // get COMMON_WIKIPAGE_SAVE event data 1313 $data = (new PageFile($id))->saveWikiText($text, $summary, $minor); 1314 if (!$data) return; // save was cancelled (for no changes or by a plugin) 1315 1316 // send notify mails 1317 ['oldRevision' => $rev, 'newRevision' => $new_rev, 'summary' => $summary] = $data; 1318 notify($id, 'admin', $rev, $summary, $minor, $new_rev); 1319 notify($id, 'subscribers', $rev, $summary, $minor, $new_rev); 1320 1321 // if useheading is enabled, purge the cache of all linking pages 1322 if (useHeading('content')) { 1323 $pages = ft_backlinks($id, true); 1324 foreach ($pages as $page) { 1325 $cache = new CacheRenderer($page, wikiFN($page), 'xhtml'); 1326 $cache->removeCache(); 1327 } 1328 } 1329} 1330 1331/** 1332 * moves the current version to the attic and returns its revision date 1333 * 1334 * @author Andreas Gohr <andi@splitbrain.org> 1335 * 1336 * @param string $id page id 1337 * @return int|string revision timestamp 1338 * @deprecated 2021-11-28 1339 */ 1340function saveOldRevision($id) 1341{ 1342 dbg_deprecated(PageFile::class .'::saveOldRevision()'); 1343 return (new PageFile($id))->saveOldRevision(); 1344} 1345 1346/** 1347 * Sends a notify mail on page change or registration 1348 * 1349 * @param string $id The changed page 1350 * @param string $who Who to notify (admin|subscribers|register) 1351 * @param int|string $rev Old page revision 1352 * @param string $summary What changed 1353 * @param boolean $minor Is this a minor edit? 1354 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1355 * @param int|string $current_rev New page revision 1356 * @return bool 1357 * 1358 * @author Andreas Gohr <andi@splitbrain.org> 1359 */ 1360function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = [], $current_rev = false) 1361{ 1362 global $conf; 1363 /* @var Input $INPUT */ 1364 global $INPUT; 1365 1366 // decide if there is something to do, eg. whom to mail 1367 if ($who == 'admin') { 1368 if (empty($conf['notify'])) return false; //notify enabled? 1369 $tpl = 'mailtext'; 1370 $to = $conf['notify']; 1371 } elseif ($who == 'subscribers') { 1372 if (!actionOK('subscribe')) return false; //subscribers enabled? 1373 if ($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1374 $data = ['id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace]; 1375 Event::createAndTrigger( 1376 'COMMON_NOTIFY_ADDRESSLIST', 1377 $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 if (in_array($conf['showuseras'], ['email_link', 'username_link'])) { 1732 if (!isset($info)) { 1733 if ($auth) $info = $auth->getUserData($username); 1734 } 1735 if (isset($info) && $info) { 1736 if ($conf['showuseras'] == 'email_link') { 1737 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1738 } else { 1739 if (is_null($xhtml_renderer)) { 1740 $xhtml_renderer = p_get_renderer('xhtml'); 1741 } 1742 if (empty($xhtml_renderer->interwiki)) { 1743 $xhtml_renderer->interwiki = getInterwiki(); 1744 } 1745 $shortcut = 'user'; 1746 $exists = null; 1747 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1748 $data['link']['class'] .= ' interwiki iw_user'; 1749 if ($exists !== null) { 1750 if ($exists) { 1751 $data['link']['class'] .= ' wikilink1'; 1752 } else { 1753 $data['link']['class'] .= ' wikilink2'; 1754 $data['link']['rel'] = 'nofollow'; 1755 } 1756 } 1757 } 1758 } else { 1759 $data['textonly'] = true; 1760 } 1761 } else { 1762 $data['textonly'] = true; 1763 } 1764 } 1765 1766 if ($data['textonly']) { 1767 $data['userlink'] = $data['name']; 1768 } else { 1769 $data['link']['name'] = $data['name']; 1770 if (is_null($xhtml_renderer)) { 1771 $xhtml_renderer = p_get_renderer('xhtml'); 1772 } 1773 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1774 } 1775 } 1776 $evt->advise_after(); 1777 unset($evt); 1778 1779 return $data['userlink']; 1780} 1781 1782/** 1783 * Returns the path to a image file for the currently chosen license. 1784 * When no image exists, returns an empty string 1785 * 1786 * @author Andreas Gohr <andi@splitbrain.org> 1787 * 1788 * @param string $type - type of image 'badge' or 'button' 1789 * @return string 1790 */ 1791function license_img($type) 1792{ 1793 global $license; 1794 global $conf; 1795 if (!$conf['license']) return ''; 1796 if (!is_array($license[$conf['license']])) return ''; 1797 $try = []; 1798 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1799 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1800 if (substr($conf['license'], 0, 3) == 'cc-') { 1801 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1802 } 1803 foreach ($try as $src) { 1804 if (file_exists(DOKU_INC.$src)) return $src; 1805 } 1806 return ''; 1807} 1808 1809/** 1810 * Checks if the given amount of memory is available 1811 * 1812 * If the memory_get_usage() function is not available the 1813 * function just assumes $bytes of already allocated memory 1814 * 1815 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1816 * @author Andreas Gohr <andi@splitbrain.org> 1817 * 1818 * @param int $mem Size of memory you want to allocate in bytes 1819 * @param int $bytes already allocated memory (see above) 1820 * @return bool 1821 */ 1822function is_mem_available($mem, $bytes = 1_048_576) 1823{ 1824 $limit = trim(ini_get('memory_limit')); 1825 if (empty($limit)) return true; // no limit set! 1826 if ($limit == -1) return true; // unlimited 1827 1828 // parse limit to bytes 1829 $limit = php_to_byte($limit); 1830 1831 // get used memory if possible 1832 if (function_exists('memory_get_usage')) { 1833 $used = memory_get_usage(); 1834 } else { 1835 $used = $bytes; 1836 } 1837 1838 if ($used + $mem > $limit) { 1839 return false; 1840 } 1841 1842 return true; 1843} 1844 1845/** 1846 * Send a HTTP redirect to the browser 1847 * 1848 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1849 * 1850 * @link http://support.microsoft.com/kb/q176113/ 1851 * @author Andreas Gohr <andi@splitbrain.org> 1852 * 1853 * @param string $url url being directed to 1854 */ 1855function send_redirect($url) 1856{ 1857 $url = stripctl($url); // defend against HTTP Response Splitting 1858 1859 /* @var Input $INPUT */ 1860 global $INPUT; 1861 1862 //are there any undisplayed messages? keep them in session for display 1863 global $MSG; 1864 if (isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1865 //reopen session, store data and close session again 1866 @session_start(); 1867 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1868 } 1869 1870 // always close the session 1871 session_write_close(); 1872 1873 // check if running on IIS < 6 with CGI-PHP 1874 if ($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1875 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1876 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1877 $matches[1] < 6 1878 ) { 1879 header('Refresh: 0;url='.$url); 1880 } else { 1881 header('Location: '.$url); 1882 } 1883 1884 // no exits during unit tests 1885 if (defined('DOKU_UNITTEST')) { 1886 // pass info about the redirect back to the test suite 1887 $testRequest = TestRequest::getRunning(); 1888 if ($testRequest !== null) { 1889 $testRequest->addData('send_redirect', $url); 1890 } 1891 return; 1892 } 1893 1894 exit; 1895} 1896 1897/** 1898 * Validate a value using a set of valid values 1899 * 1900 * This function checks whether a specified value is set and in the array 1901 * $valid_values. If not, the function returns a default value or, if no 1902 * default is specified, throws an exception. 1903 * 1904 * @param string $param The name of the parameter 1905 * @param array $valid_values A set of valid values; Optionally a default may 1906 * be marked by the key “default”. 1907 * @param array $array The array containing the value (typically $_POST 1908 * or $_GET) 1909 * @param string $exc The text of the raised exception 1910 * 1911 * @throws Exception 1912 * @return mixed 1913 * @author Adrian Lang <lang@cosmocode.de> 1914 */ 1915function valid_input_set($param, $valid_values, $array, $exc = '') 1916{ 1917 if (isset($array[$param]) && in_array($array[$param], $valid_values)) { 1918 return $array[$param]; 1919 } elseif (isset($valid_values['default'])) { 1920 return $valid_values['default']; 1921 } else { 1922 throw new Exception($exc); 1923 } 1924} 1925 1926/** 1927 * Read a preference from the DokuWiki cookie 1928 * (remembering both keys & values are urlencoded) 1929 * 1930 * @param string $pref preference key 1931 * @param mixed $default value returned when preference not found 1932 * @return string preference value 1933 */ 1934function get_doku_pref($pref, $default) 1935{ 1936 $enc_pref = urlencode($pref); 1937 if (isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 1938 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1939 $cnt = count($parts); 1940 1941 // due to #2721 there might be duplicate entries, 1942 // so we read from the end 1943 for ($i = $cnt-2; $i >= 0; $i -= 2) { 1944 if ($parts[$i] === $enc_pref) { 1945 return urldecode($parts[$i + 1]); 1946 } 1947 } 1948 } 1949 return $default; 1950} 1951 1952/** 1953 * Add a preference to the DokuWiki cookie 1954 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 1955 * Remove it by setting $val to false 1956 * 1957 * @param string $pref preference key 1958 * @param string $val preference value 1959 */ 1960function set_doku_pref($pref, $val) 1961{ 1962 global $conf; 1963 $orig = get_doku_pref($pref, false); 1964 $cookieVal = ''; 1965 1966 if ($orig !== false && ($orig !== $val)) { 1967 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1968 $cnt = count($parts); 1969 // urlencode $pref for the comparison 1970 $enc_pref = rawurlencode($pref); 1971 $seen = false; 1972 for ($i = 0; $i < $cnt; $i += 2) { 1973 if ($parts[$i] === $enc_pref) { 1974 if (!$seen) { 1975 if ($val !== false) { 1976 $parts[$i + 1] = rawurlencode($val ?? ''); 1977 } else { 1978 unset($parts[$i]); 1979 unset($parts[$i + 1]); 1980 } 1981 $seen = true; 1982 } else { 1983 // no break because we want to remove duplicate entries 1984 unset($parts[$i]); 1985 unset($parts[$i + 1]); 1986 } 1987 } 1988 } 1989 $cookieVal = implode('#', $parts); 1990 } elseif ($orig === false && $val !== false) { 1991 $cookieVal = (isset($_COOKIE['DOKU_PREFS']) ? $_COOKIE['DOKU_PREFS'] . '#' : '') . 1992 rawurlencode($pref) . '#' . rawurlencode($val); 1993 } 1994 1995 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 1996 if (defined('DOKU_UNITTEST')) { 1997 $_COOKIE['DOKU_PREFS'] = $cookieVal; 1998 } else { 1999 setcookie('DOKU_PREFS', $cookieVal, [ 2000 'expires' => time() + 365 * 24 * 3600, 2001 'path' => $cookieDir, 2002 'secure' => ($conf['securecookie'] && is_ssl()), 2003 'samesite' => 'Lax' 2004 ]); 2005 } 2006} 2007 2008/** 2009 * Strips source mapping declarations from given text #601 2010 * 2011 * @param string &$text reference to the CSS or JavaScript code to clean 2012 */ 2013function stripsourcemaps(&$text) 2014{ 2015 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 2016} 2017 2018/** 2019 * Returns the contents of a given SVG file for embedding 2020 * 2021 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through 2022 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small 2023 * files are embedded. 2024 * 2025 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG! 2026 * 2027 * @param string $file full path to the SVG file 2028 * @param int $maxsize maximum allowed size for the SVG to be embedded 2029 * @return string|false the SVG content, false if the file couldn't be loaded 2030 */ 2031function inlineSVG($file, $maxsize = 2048) 2032{ 2033 $file = trim($file); 2034 if ($file === '') return false; 2035 if (!file_exists($file)) return false; 2036 if (filesize($file) > $maxsize) return false; 2037 if (!is_readable($file)) return false; 2038 $content = file_get_contents($file); 2039 $content = preg_replace('/<!--.*?(-->)/s', '', $content); // comments 2040 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header 2041 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type 2042 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags 2043 $content = trim($content); 2044 if (substr($content, 0, 5) !== '<svg ') return false; 2045 return $content; 2046} 2047 2048//Setup VIM: ex: et ts=2 : 2049