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