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