xref: /dokuwiki/inc/infoutils.php (revision 257be26ca11cc3e4cd657879dce08ee32d8767dd)
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="http://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="http://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('http://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 * @see    html_msgarea()
362 * @param string $message
363 * @param int    $lvl   -1 = error, 0 = info, 1 = success, 2 = notify
364 * @param string $line  line number
365 * @param string $file  file number
366 * @param int    $allow who's allowed to see the message, see MSG_* constants
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 * Determine whether the current user is allowed to view the message
410 * in the $msg data structure
411 *
412 * @param  $msg   array    dokuwiki msg structure
413 *                         msg   => string, the message
414 *                         lvl   => int, level of the message (see msg() function)
415 *                         allow => int, flag used to determine who is allowed to see the message
416 *                                       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 * @author Andreas Gohr <andi@splitbrain.org>
454 *
455 * @param string $msg
456 * @param bool $hidden
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 * @author Andreas Gohr <andi@splitbrain.org>
475 * @deprecated 2020-08-13
476 * @param string $msg
477 * @param string $header
478 */
479function dbglog($msg, $header = '')
480{
481    dbg_deprecated('\\dokuwiki\\Logger');
482
483    // was the msg as single line string? use it as header
484    if ($header === '' && is_string($msg) && strpos($msg, "\n") === false) {
485        $header = $msg;
486        $msg = '';
487    }
488
489    Logger::getInstance(Logger::LOG_DEBUG)->log(
490        $header,
491        $msg
492    );
493}
494
495/**
496 * Log accesses to deprecated fucntions to the debug log
497 *
498 * @param string $alternative The function or method that should be used instead
499 * @triggers INFO_DEPRECATION_LOG
500 */
501function dbg_deprecated($alternative = '')
502{
503    DebugHelper::dbgDeprecatedFunction($alternative, 2);
504}
505
506/**
507 * Print a reversed, prettyprinted backtrace
508 *
509 * @author Gary Owen <gary_owen@bigfoot.com>
510 */
511function dbg_backtrace()
512{
513    // Get backtrace
514    $backtrace = debug_backtrace();
515
516    // Unset call to debug_print_backtrace
517    array_shift($backtrace);
518
519    // Iterate backtrace
520    $calls = [];
521    $depth = count($backtrace) - 1;
522    foreach ($backtrace as $i => $call) {
523        $location = $call['file'] . ':' . $call['line'];
524        $function = (isset($call['class'])) ?
525            $call['class'] . $call['type'] . $call['function'] : $call['function'];
526
527        $params = [];
528        if (isset($call['args'])) {
529            foreach ($call['args'] as $arg) {
530                if (is_object($arg)) {
531                    $params[] = '[Object ' . get_class($arg) . ']';
532                } elseif (is_array($arg)) {
533                    $params[] = '[Array]';
534                } elseif (is_null($arg)) {
535                    $params[] = '[NULL]';
536                } else {
537                    $params[] = '"' . $arg . '"';
538                }
539            }
540        }
541        $params = implode(', ', $params);
542
543        $calls[$depth - $i] = sprintf(
544            '%s(%s) called at %s',
545            $function,
546            str_replace("\n", '\n', $params),
547            $location
548        );
549    }
550    ksort($calls);
551
552    return implode("\n", $calls);
553}
554
555/**
556 * Remove all data from an array where the key seems to point to sensitive data
557 *
558 * This is used to remove passwords, mail addresses and similar data from the
559 * debug output
560 *
561 * @author Andreas Gohr <andi@splitbrain.org>
562 *
563 * @param array $data
564 */
565function debug_guard(&$data)
566{
567    foreach ($data as $key => $value) {
568        if (preg_match('/(notify|pass|auth|secret|ftp|userinfo|token|buid|mail|proxy)/i', $key)) {
569            $data[$key] = '***';
570            continue;
571        }
572        if (is_array($value)) debug_guard($data[$key]);
573    }
574}
575