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