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