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