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