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