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