1<?php 2 3/** 4 * Information and debugging functions 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Andreas Gohr <andi@splitbrain.org> 8 */ 9 10use dokuwiki\Debug\DebugHelper; 11use dokuwiki\Extension\AuthPlugin; 12use dokuwiki\Extension\Event; 13use dokuwiki\HTTP\DokuHTTPClient; 14use dokuwiki\Logger; 15use dokuwiki\Utf8\PhpString; 16 17if (!defined('DOKU_MESSAGEURL')) { 18 if (in_array('ssl', stream_get_transports())) { 19 define('DOKU_MESSAGEURL', 'https://update.dokuwiki.org/check/'); 20 } else { 21 define('DOKU_MESSAGEURL', 'http://update.dokuwiki.org/check/'); 22 } 23} 24 25/** 26 * Check for new messages from upstream 27 * 28 * @author Andreas Gohr <andi@splitbrain.org> 29 */ 30function checkUpdateMessages() 31{ 32 global $conf; 33 global $INFO; 34 global $updateVersion; 35 if (!$conf['updatecheck']) return; 36 if ($conf['useacl'] && !$INFO['ismanager']) return; 37 38 $cf = getCacheName($updateVersion, '.updmsg'); 39 $lm = @filemtime($cf); 40 $is_http = !str_starts_with(DOKU_MESSAGEURL, 'https'); 41 42 // check if new messages needs to be fetched 43 if ($lm < time() - (60 * 60 * 24) || $lm < @filemtime(DOKU_INC . DOKU_SCRIPT)) { 44 @touch($cf); 45 Logger::debug( 46 sprintf( 47 'checkUpdateMessages(): downloading messages to %s%s', 48 $cf, 49 $is_http ? ' (without SSL)' : ' (with SSL)' 50 ) 51 ); 52 $http = new DokuHTTPClient(); 53 $http->timeout = 12; 54 $resp = $http->get(DOKU_MESSAGEURL . $updateVersion); 55 if (is_string($resp) && ($resp == '' || str_ends_with(trim($resp), '%'))) { 56 // basic sanity check that this is either an empty string response (ie "no messages") 57 // or it looks like one of our messages, not WiFi login or other interposed response 58 io_saveFile($cf, $resp); 59 } else { 60 Logger::debug("checkUpdateMessages(): unexpected HTTP response received", $http->error); 61 } 62 } else { 63 Logger::debug("checkUpdateMessages(): messages up to date"); 64 } 65 66 $data = io_readFile($cf); 67 // show messages through the usual message mechanism 68 $msgs = explode("\n%\n", $data); 69 foreach ($msgs as $msg) { 70 if ($msg) msg($msg, 2); 71 } 72} 73 74 75/** 76 * Return DokuWiki's version (split up in date and type) 77 * 78 * @author Andreas Gohr <andi@splitbrain.org> 79 */ 80function getVersionData() 81{ 82 $version = []; 83 //import version string 84 if (file_exists(DOKU_INC . 'VERSION')) { 85 //official release 86 $version['date'] = trim(io_readFile(DOKU_INC . 'VERSION')); 87 $version['type'] = 'Release'; 88 } elseif (is_dir(DOKU_INC . '.git')) { 89 $version['type'] = 'Git'; 90 $version['date'] = 'unknown'; 91 92 // First try to get date and commit hash by calling Git 93 if (function_exists('shell_exec')) { 94 $args = ['git', 'log', '-1', '--pretty=format:%h %cd', '--date=short']; 95 $commitInfo = shell_exec(implode(' ', array_map('escapeshellarg', $args))); 96 if ($commitInfo) { 97 [$version['sha'], $date] = explode(' ', $commitInfo); 98 $version['date'] = hsc($date); 99 return $version; 100 } 101 } 102 103 // we cannot use git on the shell -- let's do it manually! 104 if (file_exists(DOKU_INC . '.git/HEAD')) { 105 $headCommit = trim(file_get_contents(DOKU_INC . '.git/HEAD')); 106 if (str_starts_with($headCommit, 'ref: ')) { 107 // it is something like `ref: refs/heads/master` 108 $headCommit = substr($headCommit, 5); 109 $pathToHead = DOKU_INC . '.git/' . $headCommit; 110 if (file_exists($pathToHead)) { 111 $headCommit = trim(file_get_contents($pathToHead)); 112 } else { 113 $packedRefs = file_get_contents(DOKU_INC . '.git/packed-refs'); 114 if (!preg_match("~([[:xdigit:]]+) $headCommit~", $packedRefs, $matches)) { 115 # ref not found in pack file 116 return $version; 117 } 118 $headCommit = $matches[1]; 119 } 120 } 121 // At this point $headCommit is a SHA 122 $version['sha'] = $headCommit; 123 124 // Get commit date from Git object 125 $subDir = substr($headCommit, 0, 2); 126 $fileName = substr($headCommit, 2); 127 $gitCommitObject = DOKU_INC . ".git/objects/$subDir/$fileName"; 128 if (file_exists($gitCommitObject) && function_exists('zlib_decode')) { 129 $commit = zlib_decode(file_get_contents($gitCommitObject)); 130 $committerLine = explode("\n", $commit)[3]; 131 $committerData = explode(' ', $committerLine); 132 end($committerData); 133 $ts = prev($committerData); 134 if ($ts && $date = date('Y-m-d', $ts)) { 135 $version['date'] = $date; 136 } 137 } 138 } 139 } else { 140 global $updateVersion; 141 $version['date'] = 'update version ' . $updateVersion; 142 $version['type'] = 'snapshot?'; 143 } 144 return $version; 145} 146 147/** 148 * Return DokuWiki's version 149 * 150 * This returns the version in the form "Type Date (SHA)". Where type is either 151 * "Release" or "Git" and date is the date of the release or the date of the 152 * last commit. SHA is the short SHA of the last commit - this is only added on 153 * git checkouts. 154 * 155 * If no version can be determined "snapshot? update version XX" is returned. 156 * Where XX represents the update version number set in doku.php. 157 * 158 * @return string The version string e.g. "Release 2023-04-04a" 159 * @author Anika Henke <anika@selfthinker.org> 160 */ 161function getVersion() 162{ 163 $version = getVersionData(); 164 $sha = empty($version['sha']) ? '' : ' (' . $version['sha'] . ')'; 165 return $version['type'] . ' ' . $version['date'] . $sha; 166} 167 168/** 169 * Get some data about the environment this wiki is running in 170 * 171 * @return array 172 */ 173function getRuntimeVersions() 174{ 175 $data = []; 176 $data['php'] = 'PHP ' . PHP_VERSION; 177 178 $osRelease = getOsRelease(); 179 if (isset($osRelease['PRETTY_NAME'])) { 180 $data['dist'] = $osRelease['PRETTY_NAME']; 181 } 182 183 $data['os'] = php_uname('s') . ' ' . php_uname('r'); 184 $data['sapi'] = PHP_SAPI; 185 186 if (getenv('KUBERNETES_SERVICE_HOST')) { 187 $data['container'] = 'Kubernetes'; 188 } elseif (@file_exists('/.dockerenv')) { 189 $data['container'] = 'Docker'; 190 } 191 192 return $data; 193} 194 195/** 196 * Get informational data about the linux distribution this wiki is running on 197 * 198 * @see https://gist.github.com/natefoo/814c5bf936922dad97ff 199 * @return array an os-release array, might be empty 200 */ 201function getOsRelease() 202{ 203 $reader = fn($file) => @parse_ini_string(preg_replace('/#.*$/m', '', file_get_contents($file))); 204 205 $osRelease = []; 206 if (@file_exists('/etc/os-release')) { 207 // pretty much any common Linux distribution has this 208 $osRelease = $reader('/etc/os-release'); 209 } elseif (@file_exists('/etc/synoinfo.conf') && @file_exists('/etc/VERSION')) { 210 // Synology DSM has its own way 211 $synoInfo = $reader('/etc/synoinfo.conf'); 212 $synoVersion = $reader('/etc/VERSION'); 213 $osRelease['NAME'] = 'Synology DSM'; 214 $osRelease['ID'] = 'synology'; 215 $osRelease['ID_LIKE'] = 'linux'; 216 $osRelease['VERSION_ID'] = $synoVersion['productversion']; 217 $osRelease['VERSION'] = $synoVersion['productversion']; 218 $osRelease['SYNO_MODEL'] = $synoInfo['upnpmodelname']; 219 $osRelease['PRETTY_NAME'] = implode(' ', [$osRelease['NAME'], $osRelease['VERSION'], $osRelease['SYNO_MODEL']]); 220 } 221 return $osRelease; 222} 223 224 225/** 226 * Run a few sanity checks 227 * 228 * @author Andreas Gohr <andi@splitbrain.org> 229 */ 230function check() 231{ 232 global $conf; 233 global $INFO; 234 /* @var Input $INPUT */ 235 global $INPUT; 236 237 if ($INFO['isadmin'] || $INFO['ismanager']) { 238 msg('DokuWiki version: ' . getVersion(), 1); 239 if (version_compare(phpversion(), '8.2.0', '<')) { 240 msg('Your PHP version is too old (' . phpversion() . ' vs. 8.2+ needed)', -1); 241 } else { 242 msg('PHP version ' . phpversion(), 1); 243 } 244 } elseif (version_compare(phpversion(), '8.2.0', '<')) { 245 msg('Your PHP version is too old', -1); 246 } 247 248 $mem = php_to_byte(ini_get('memory_limit')); 249 if ($mem) { 250 if ($mem === -1) { 251 msg('PHP memory is unlimited', 1); 252 } elseif ($mem < 16_777_216) { 253 msg('PHP is limited to less than 16MB RAM (' . filesize_h($mem) . '). 254 Increase memory_limit in php.ini', -1); 255 } elseif ($mem < 20_971_520) { 256 msg('PHP is limited to less than 20MB RAM (' . filesize_h($mem) . '), 257 you might encounter problems with bigger pages. Increase memory_limit in php.ini', -1); 258 } elseif ($mem < 33_554_432) { 259 msg('PHP is limited to less than 32MB RAM (' . filesize_h($mem) . '), 260 but that should be enough in most cases. If not, increase memory_limit in php.ini', 0); 261 } else { 262 msg('More than 32MB RAM (' . filesize_h($mem) . ') available.', 1); 263 } 264 } 265 266 if (is_writable($conf['changelog'])) { 267 msg('Changelog is writable', 1); 268 } elseif (file_exists($conf['changelog'])) { 269 msg('Changelog is not writable', -1); 270 } 271 272 if (isset($conf['changelog_old']) && file_exists($conf['changelog_old'])) { 273 msg('Old changelog exists', 0); 274 } 275 276 if (file_exists($conf['changelog'] . '_failed')) { 277 msg('Importing old changelog failed', -1); 278 } elseif (file_exists($conf['changelog'] . '_importing')) { 279 msg('Importing old changelog now.', 0); 280 } elseif (file_exists($conf['changelog'] . '_import_ok')) { 281 msg('Old changelog imported', 1); 282 if (!plugin_isdisabled('importoldchangelog')) { 283 msg('Importoldchangelog plugin not disabled after import', -1); 284 } 285 } 286 287 if (is_writable(DOKU_CONF)) { 288 msg('conf directory is writable', 1); 289 } else { 290 msg('conf directory is not writable', -1); 291 } 292 293 if ($conf['authtype'] == 'plain') { 294 global $config_cascade; 295 if (is_writable($config_cascade['plainauth.users']['default'])) { 296 msg('conf/users.auth.php is writable', 1); 297 } else { 298 msg('conf/users.auth.php is not writable', 0); 299 } 300 } 301 302 if (function_exists('mb_strpos')) { 303 if (defined('UTF8_NOMBSTRING')) { 304 msg('mb_string extension is available but will not be used', 0); 305 } else { 306 msg('mb_string extension is available and will be used', 1); 307 } 308 } else { 309 msg('mb_string extension not available - PHP only replacements will be used', 0); 310 } 311 312 if (!UTF8_PREGSUPPORT) { 313 msg('PHP is missing UTF-8 support in Perl-Compatible Regular Expressions (PCRE)', -1); 314 } 315 if (!UTF8_PROPERTYSUPPORT) { 316 msg('PHP is missing Unicode properties support in Perl-Compatible Regular Expressions (PCRE)', -1); 317 } 318 319 $loc = setlocale(LC_ALL, 0); 320 if (!$loc) { 321 msg('No valid locale is set for your PHP setup. You should fix this', -1); 322 } elseif (stripos($loc, 'utf') === false) { 323 msg('Your locale <code>' . hsc($loc) . '</code> seems not to be a UTF-8 locale, 324 you should fix this if you encounter problems.', 0); 325 } else { 326 msg('Valid locale ' . hsc($loc) . ' found.', 1); 327 } 328 329 if ($conf['allowdebug']) { 330 msg('Debugging support is enabled. If you don\'t need it you should set $conf[\'allowdebug\'] = 0', -1); 331 } else { 332 msg('Debugging support is disabled', 1); 333 } 334 335 if (!empty($INFO['userinfo']['name'])) { 336 msg(sprintf( 337 "You are currently logged in as %s (%s)", 338 $INPUT->server->str('REMOTE_USER'), 339 $INFO['userinfo']['name'] 340 ), 0); 341 msg('You are part of the groups ' . implode(', ', $INFO['userinfo']['grps']), 0); 342 } else { 343 msg('You are currently not logged in', 0); 344 } 345 346 msg('Your current permission for this page is ' . $INFO['perm'], 0); 347 348 if (file_exists($INFO['filepath']) && is_writable($INFO['filepath'])) { 349 msg('The current page is writable by the webserver', 1); 350 } elseif (!file_exists($INFO['filepath']) && is_writable(dirname($INFO['filepath']))) { 351 msg('The current page can be created by the webserver', 1); 352 } else { 353 msg('The current page is not writable by the webserver', -1); 354 } 355 356 if ($INFO['writable']) { 357 msg('The current page is writable by you', 1); 358 } else { 359 msg('The current page is not writable by you', -1); 360 } 361 362 // Check for corrupted search index 363 $lengths = idx_listIndexLengths(); 364 $index_corrupted = false; 365 foreach ($lengths as $length) { 366 if (count(idx_getIndex('w', $length)) !== count(idx_getIndex('i', $length))) { 367 $index_corrupted = true; 368 break; 369 } 370 } 371 372 foreach (idx_getIndex('metadata', '') as $index) { 373 if (count(idx_getIndex($index . '_w', '')) !== count(idx_getIndex($index . '_i', ''))) { 374 $index_corrupted = true; 375 break; 376 } 377 } 378 379 if ($index_corrupted) { 380 msg( 381 'The search index is corrupted. It might produce wrong results and most 382 probably needs to be rebuilt. See 383 <a href="https://www.dokuwiki.org/faq:searchindex">faq:searchindex</a> 384 for ways to rebuild the search index.', 385 -1 386 ); 387 } elseif (!empty($lengths)) { 388 msg('The search index seems to be working', 1); 389 } else { 390 msg( 391 'The search index is empty. See 392 <a href="https://www.dokuwiki.org/faq:searchindex">faq:searchindex</a> 393 for help on how to fix the search index. If the default indexer 394 isn\'t used or the wiki is actually empty this is normal.' 395 ); 396 } 397 398 // rough time check 399 $http = new DokuHTTPClient(); 400 $http->max_redirect = 0; 401 $http->timeout = 3; 402 $http->sendRequest('https://www.dokuwiki.org', '', 'HEAD'); 403 404 $now = time(); 405 if (isset($http->resp_headers['date'])) { 406 $time = strtotime($http->resp_headers['date']); 407 $diff = $time - $now; 408 409 if (abs($diff) < 4) { 410 msg("Server time seems to be okay. Diff: {$diff}s", 1); 411 } else { 412 msg("Your server's clock seems to be out of sync! 413 Consider configuring a sync with a NTP server. Diff: {$diff}s"); 414 } 415 } 416} 417 418/** 419 * Display a message to the user 420 * 421 * If HTTP headers were not sent yet the message is added 422 * to the global message array else it's printed directly 423 * using html_msgarea() 424 * 425 * Triggers INFOUTIL_MSG_SHOW 426 * 427 * @param string $message 428 * @param int $lvl -1 = error, 0 = info, 1 = success, 2 = notify 429 * @param string $line line number 430 * @param string $file file number 431 * @param int $allow who's allowed to see the message, see MSG_* constants 432 * @see html_msgarea() 433 */ 434function msg($message, $lvl = 0, $line = '', $file = '', $allow = MSG_PUBLIC) 435{ 436 global $MSG, $MSG_shown; 437 static $errors = [ 438 -1 => 'error', 439 0 => 'info', 440 1 => 'success', 441 2 => 'notify', 442 ]; 443 444 $msgdata = [ 445 'msg' => $message, 446 'lvl' => $errors[$lvl], 447 'allow' => $allow, 448 'line' => $line, 449 'file' => $file, 450 ]; 451 452 $evt = new Event('INFOUTIL_MSG_SHOW', $msgdata); 453 if ($evt->advise_before()) { 454 /* Show msg normally - event could suppress message show */ 455 if ($msgdata['line'] || $msgdata['file']) { 456 $basename = PhpString::basename($msgdata['file']); 457 $msgdata['msg'] .= ' [' . $basename . ':' . $msgdata['line'] . ']'; 458 } 459 460 if (!isset($MSG)) $MSG = []; 461 $MSG[] = $msgdata; 462 if (isset($MSG_shown) || headers_sent()) { 463 if (function_exists('html_msgarea')) { 464 html_msgarea(); 465 } else { 466 echo "ERROR(" . $msgdata['lvl'] . ") " . $msgdata['msg'] . "\n"; 467 } 468 unset($GLOBALS['MSG']); 469 } 470 } 471 $evt->advise_after(); 472 unset($evt); 473} 474 475/** 476 * Determine whether the current user is allowed to view the message 477 * in the $msg data structure 478 * 479 * @param array $msg dokuwiki msg structure: 480 * msg => string, the message; 481 * lvl => int, level of the message (see msg() function); 482 * allow => int, flag used to determine who is allowed to see the message, see MSG_* constants 483 * @return bool 484 */ 485function info_msg_allowed($msg) 486{ 487 global $INFO, $auth; 488 489 // is the message public? - everyone and anyone can see it 490 if (empty($msg['allow']) || ($msg['allow'] == MSG_PUBLIC)) return true; 491 492 // restricted msg, but no authentication 493 if (!$auth instanceof AuthPlugin) return false; 494 495 switch ($msg['allow']) { 496 case MSG_USERS_ONLY: 497 return !empty($INFO['userinfo']); 498 499 case MSG_MANAGERS_ONLY: 500 return $INFO['ismanager']; 501 502 case MSG_ADMINS_ONLY: 503 return $INFO['isadmin']; 504 505 default: 506 trigger_error( 507 'invalid msg allow restriction. msg="' . $msg['msg'] . '" allow=' . $msg['allow'] . '"', 508 E_USER_WARNING 509 ); 510 return $INFO['isadmin']; 511 } 512} 513 514/** 515 * print debug messages 516 * 517 * little function to print the content of a var 518 * 519 * @param string $msg 520 * @param bool $hidden 521 * 522 * @author Andreas Gohr <andi@splitbrain.org> 523 */ 524function dbg($msg, $hidden = false) 525{ 526 if ($hidden) { 527 echo "<!--\n"; 528 print_r($msg); 529 echo "\n-->"; 530 } else { 531 echo '<pre class="dbg">'; 532 echo hsc(print_r($msg, true)); 533 echo '</pre>'; 534 } 535} 536 537/** 538 * Print info to debug log file 539 * 540 * @param string $msg 541 * @param string $header 542 * 543 * @author Andreas Gohr <andi@splitbrain.org> 544 * @deprecated 2020-08-13 545 */ 546function dbglog($msg, $header = '') 547{ 548 dbg_deprecated('\\dokuwiki\\Logger'); 549 550 // was the msg as single line string? use it as header 551 if ($header === '' && is_string($msg) && !str_contains($msg, "\n")) { 552 $header = $msg; 553 $msg = ''; 554 } 555 556 Logger::getInstance(Logger::LOG_DEBUG)->log( 557 $header, 558 $msg 559 ); 560} 561 562/** 563 * Log accesses to deprecated fucntions to the debug log 564 * 565 * @param string $alternative The function or method that should be used instead 566 * @triggers INFO_DEPRECATION_LOG 567 */ 568function dbg_deprecated($alternative = '') 569{ 570 DebugHelper::dbgDeprecatedFunction($alternative, 2); 571} 572 573/** 574 * Print a reversed, prettyprinted backtrace 575 * 576 * @author Gary Owen <gary_owen@bigfoot.com> 577 */ 578function dbg_backtrace() 579{ 580 // Get backtrace 581 $backtrace = debug_backtrace(); 582 583 // Unset call to debug_print_backtrace 584 array_shift($backtrace); 585 586 // Iterate backtrace 587 $calls = []; 588 $depth = count($backtrace) - 1; 589 foreach ($backtrace as $i => $call) { 590 if (isset($call['file'])) { 591 $location = $call['file'] . ':' . ($call['line'] ?? '0'); 592 } else { 593 $location = '[anonymous]'; 594 } 595 if (isset($call['class'])) { 596 $function = $call['class'] . $call['type'] . $call['function']; 597 } else { 598 $function = $call['function']; 599 } 600 601 $params = []; 602 if (isset($call['args'])) { 603 foreach ($call['args'] as $arg) { 604 if (is_object($arg)) { 605 $params[] = '[Object ' . $arg::class . ']'; 606 } elseif (is_array($arg)) { 607 $params[] = '[Array]'; 608 } elseif (is_null($arg)) { 609 $params[] = '[NULL]'; 610 } else { 611 $params[] = '"' . $arg . '"'; 612 } 613 } 614 } 615 $params = implode(', ', $params); 616 617 $calls[$depth - $i] = sprintf( 618 '%s(%s) called at %s', 619 $function, 620 str_replace("\n", '\n', $params), 621 $location 622 ); 623 } 624 ksort($calls); 625 626 return implode("\n", $calls); 627} 628 629/** 630 * Remove all data from an array where the key seems to point to sensitive data 631 * 632 * This is used to remove passwords, mail addresses and similar data from the 633 * debug output 634 * 635 * @param array $data 636 * 637 * @author Andreas Gohr <andi@splitbrain.org> 638 */ 639function debug_guard(&$data) 640{ 641 foreach ($data as $key => $value) { 642 if (preg_match('/(notify|pass|auth|secret|ftp|userinfo|token|buid|mail|proxy)/i', $key)) { 643 $data[$key] = '***'; 644 continue; 645 } 646 if (is_array($value)) debug_guard($data[$key]); 647 } 648} 649