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