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