xref: /dokuwiki/inc/common.php (revision 253d4b48ec708eb42033862dc15c8576f44a48ed)
1<?php
2/**
3 * Common DokuWiki functions
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9if(!defined('DOKU_INC')) die('meh.');
10
11/**
12 * These constants are used with the recents function
13 */
14define('RECENTS_SKIP_DELETED', 2);
15define('RECENTS_SKIP_MINORS', 4);
16define('RECENTS_SKIP_SUBSPACES', 8);
17define('RECENTS_MEDIA_CHANGES', 16);
18define('RECENTS_MEDIA_PAGES_MIXED', 32);
19
20/**
21 * Wrapper around htmlspecialchars()
22 *
23 * @author Andreas Gohr <andi@splitbrain.org>
24 * @see    htmlspecialchars()
25 *
26 * @param string $string the string being converted
27 * @return string converted string
28 */
29function hsc($string) {
30    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
31}
32
33/**
34 * print a newline terminated string
35 *
36 * You can give an indention as optional parameter
37 *
38 * @author Andreas Gohr <andi@splitbrain.org>
39 *
40 * @param string $string  line of text
41 * @param int    $indent  number of spaces indention
42 */
43function ptln($string, $indent = 0) {
44    echo str_repeat(' ', $indent)."$string\n";
45}
46
47/**
48 * strips control characters (<32) from the given string
49 *
50 * @author Andreas Gohr <andi@splitbrain.org>
51 *
52 * @param string $string being stripped
53 * @return string
54 */
55function stripctl($string) {
56    return preg_replace('/[\x00-\x1F]+/s', '', $string);
57}
58
59/**
60 * Return a secret token to be used for CSRF attack prevention
61 *
62 * @author  Andreas Gohr <andi@splitbrain.org>
63 * @link    http://en.wikipedia.org/wiki/Cross-site_request_forgery
64 * @link    http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
65 *
66 * @return  string
67 */
68function getSecurityToken() {
69    /** @var Input $INPUT */
70    global $INPUT;
71    return PassHash::hmac('md5', session_id().$INPUT->server->str('REMOTE_USER'), auth_cookiesalt());
72}
73
74/**
75 * Check the secret CSRF token
76 *
77 * @param null|string $token security token or null to read it from request variable
78 * @return bool success if the token matched
79 */
80function checkSecurityToken($token = null) {
81    /** @var Input $INPUT */
82    global $INPUT;
83    if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
84
85    if(is_null($token)) $token = $INPUT->str('sectok');
86    if(getSecurityToken() != $token) {
87        msg('Security Token did not match. Possible CSRF attack.', -1);
88        return false;
89    }
90    return true;
91}
92
93/**
94 * Print a hidden form field with a secret CSRF token
95 *
96 * @author  Andreas Gohr <andi@splitbrain.org>
97 *
98 * @param bool $print  if true print the field, otherwise html of the field is returned
99 * @return string html of hidden form field
100 */
101function formSecurityToken($print = true) {
102    $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
103    if($print) echo $ret;
104    return $ret;
105}
106
107/**
108 * Determine basic information for a request of $id
109 *
110 * @author Andreas Gohr <andi@splitbrain.org>
111 * @author Chris Smith <chris@jalakai.co.uk>
112 *
113 * @param string $id         pageid
114 * @param bool   $htmlClient add info about whether is mobile browser
115 * @return array with info for a request of $id
116 *
117 */
118function basicinfo($id, $htmlClient=true){
119    global $USERINFO;
120    /* @var Input $INPUT */
121    global $INPUT;
122
123    // set info about manager/admin status.
124    $info = array();
125    $info['isadmin']   = false;
126    $info['ismanager'] = false;
127    if($INPUT->server->has('REMOTE_USER')) {
128        $info['userinfo']   = $USERINFO;
129        $info['perm']       = auth_quickaclcheck($id);
130        $info['client']     = $INPUT->server->str('REMOTE_USER');
131
132        if($info['perm'] == AUTH_ADMIN) {
133            $info['isadmin']   = true;
134            $info['ismanager'] = true;
135        } elseif(auth_ismanager()) {
136            $info['ismanager'] = true;
137        }
138
139        // if some outside auth were used only REMOTE_USER is set
140        if(!$info['userinfo']['name']) {
141            $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
142        }
143
144    } else {
145        $info['perm']       = auth_aclcheck($id, '', null);
146        $info['client']     = clientIP(true);
147    }
148
149    $info['namespace'] = getNS($id);
150
151    // mobile detection
152    if ($htmlClient) {
153        $info['ismobile'] = clientismobile();
154    }
155
156    return $info;
157 }
158
159/**
160 * Return info about the current document as associative
161 * array.
162 *
163 * @author Andreas Gohr <andi@splitbrain.org>
164 *
165 * @return array with info about current document
166 */
167function pageinfo() {
168    global $ID;
169    global $REV;
170    global $RANGE;
171    global $lang;
172    /* @var Input $INPUT */
173    global $INPUT;
174
175    $info = basicinfo($ID);
176
177    // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
178    // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
179    $info['id']  = $ID;
180    $info['rev'] = $REV;
181
182    if($INPUT->server->has('REMOTE_USER')) {
183        $sub = new Subscription();
184        $info['subscribed'] = $sub->user_subscription();
185    } else {
186        $info['subscribed'] = false;
187    }
188
189    $info['locked']     = checklock($ID);
190    $info['filepath']   = fullpath(wikiFN($ID));
191    $info['exists']     = @file_exists($info['filepath']);
192    $info['currentrev'] = @filemtime($info['filepath']);
193    if($REV) {
194        //check if current revision was meant
195        if($info['exists'] && ($info['currentrev'] == $REV)) {
196            $REV = '';
197        } elseif($RANGE) {
198            //section editing does not work with old revisions!
199            $REV   = '';
200            $RANGE = '';
201            msg($lang['nosecedit'], 0);
202        } else {
203            //really use old revision
204            $info['filepath'] = fullpath(wikiFN($ID, $REV));
205            $info['exists']   = @file_exists($info['filepath']);
206        }
207    }
208    $info['rev'] = $REV;
209    if($info['exists']) {
210        $info['writable'] = (is_writable($info['filepath']) &&
211            ($info['perm'] >= AUTH_EDIT));
212    } else {
213        $info['writable'] = ($info['perm'] >= AUTH_CREATE);
214    }
215    $info['editable'] = ($info['writable'] && empty($info['locked']));
216    $info['lastmod']  = @filemtime($info['filepath']);
217
218    //load page meta data
219    $info['meta'] = p_get_metadata($ID);
220
221    //who's the editor
222    $pagelog = new PageChangeLog($ID, 1024);
223    if($REV) {
224        $revinfo = $pagelog->getRevisionInfo($REV);
225    } else {
226        if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
227            $revinfo = $info['meta']['last_change'];
228        } else {
229            $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
230            // cache most recent changelog line in metadata if missing and still valid
231            if($revinfo !== false) {
232                $info['meta']['last_change'] = $revinfo;
233                p_set_metadata($ID, array('last_change' => $revinfo));
234            }
235        }
236    }
237    //and check for an external edit
238    if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
239        // cached changelog line no longer valid
240        $revinfo                     = false;
241        $info['meta']['last_change'] = $revinfo;
242        p_set_metadata($ID, array('last_change' => $revinfo));
243    }
244
245    $info['ip']   = $revinfo['ip'];
246    $info['user'] = $revinfo['user'];
247    $info['sum']  = $revinfo['sum'];
248    // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
249    // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
250
251    if($revinfo['user']) {
252        $info['editor'] = $revinfo['user'];
253    } else {
254        $info['editor'] = $revinfo['ip'];
255    }
256
257    // draft
258    $draft = getCacheName($info['client'].$ID, '.draft');
259    if(@file_exists($draft)) {
260        if(@filemtime($draft) < @filemtime(wikiFN($ID))) {
261            // remove stale draft
262            @unlink($draft);
263        } else {
264            $info['draft'] = $draft;
265        }
266    }
267
268    return $info;
269}
270
271/**
272 * Return information about the current media item as an associative array.
273 *
274 * @return array with info about current media item
275 */
276function mediainfo(){
277    global $NS;
278    global $IMG;
279
280    $info = basicinfo("$NS:*");
281    $info['image'] = $IMG;
282
283    return $info;
284}
285
286/**
287 * Build an string of URL parameters
288 *
289 * @author Andreas Gohr
290 *
291 * @param array  $params    array with key-value pairs
292 * @param string $sep       series of pairs are separated by this character
293 * @return string query string
294 */
295function buildURLparams($params, $sep = '&amp;') {
296    $url = '';
297    $amp = false;
298    foreach($params as $key => $val) {
299        if($amp) $url .= $sep;
300
301        $url .= rawurlencode($key).'=';
302        $url .= rawurlencode((string) $val);
303        $amp = true;
304    }
305    return $url;
306}
307
308/**
309 * Build an string of html tag attributes
310 *
311 * Skips keys starting with '_', values get HTML encoded
312 *
313 * @author Andreas Gohr
314 *
315 * @param array $params    array with (attribute name-attribute value) pairs
316 * @param bool  $skipempty skip empty string values?
317 * @return string
318 */
319function buildAttributes($params, $skipempty = false) {
320    $url   = '';
321    $white = false;
322    foreach($params as $key => $val) {
323        if($key{0} == '_') continue;
324        if($val === '' && $skipempty) continue;
325        if($white) $url .= ' ';
326
327        $url .= $key.'="';
328        $url .= htmlspecialchars($val);
329        $url .= '"';
330        $white = true;
331    }
332    return $url;
333}
334
335/**
336 * This builds the breadcrumb trail and returns it as array
337 *
338 * @author Andreas Gohr <andi@splitbrain.org>
339 *
340 * @return string[string] with the data: array(pageid=>name, ... )
341 */
342function breadcrumbs() {
343    // we prepare the breadcrumbs early for quick session closing
344    static $crumbs = null;
345    if($crumbs != null) return $crumbs;
346
347    global $ID;
348    global $ACT;
349    global $conf;
350
351    //first visit?
352    $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
353    //we only save on show and existing wiki documents
354    $file = wikiFN($ID);
355    if($ACT != 'show' || !@file_exists($file)) {
356        $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
357        return $crumbs;
358    }
359
360    // page names
361    $name = noNSorNS($ID);
362    if(useHeading('navigation')) {
363        // get page title
364        $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
365        if($title) {
366            $name = $title;
367        }
368    }
369
370    //remove ID from array
371    if(isset($crumbs[$ID])) {
372        unset($crumbs[$ID]);
373    }
374
375    //add to array
376    $crumbs[$ID] = $name;
377    //reduce size
378    while(count($crumbs) > $conf['breadcrumbs']) {
379        array_shift($crumbs);
380    }
381    //save to session
382    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
383    return $crumbs;
384}
385
386/**
387 * Filter for page IDs
388 *
389 * This is run on a ID before it is outputted somewhere
390 * currently used to replace the colon with something else
391 * on Windows (non-IIS) systems and to have proper URL encoding
392 *
393 * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and
394 * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of
395 * unaffected servers instead of blacklisting affected servers here.
396 *
397 * Urlencoding is ommitted when the second parameter is false
398 *
399 * @author Andreas Gohr <andi@splitbrain.org>
400 *
401 * @param string $id pageid being filtered
402 * @param bool   $ue apply urlencoding?
403 * @return string
404 */
405function idfilter($id, $ue = true) {
406    global $conf;
407    /* @var Input $INPUT */
408    global $INPUT;
409
410    if($conf['useslash'] && $conf['userewrite']) {
411        $id = strtr($id, ':', '/');
412    } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
413        $conf['userewrite'] &&
414        strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
415    ) {
416        $id = strtr($id, ':', ';');
417    }
418    if($ue) {
419        $id = rawurlencode($id);
420        $id = str_replace('%3A', ':', $id); //keep as colon
421        $id = str_replace('%3B', ';', $id); //keep as semicolon
422        $id = str_replace('%2F', '/', $id); //keep as slash
423    }
424    return $id;
425}
426
427/**
428 * This builds a link to a wikipage
429 *
430 * It handles URL rewriting and adds additional parameters
431 *
432 * @author Andreas Gohr <andi@splitbrain.org>
433 *
434 * @param string       $id             page id, defaults to start page
435 * @param string|array $urlParameters  URL parameters, associative array recommended
436 * @param bool         $absolute       request an absolute URL instead of relative
437 * @param string       $separator      parameter separator
438 * @return string
439 */
440function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;') {
441    global $conf;
442    if(is_array($urlParameters)) {
443        $urlParameters = buildURLparams($urlParameters, $separator);
444    } else {
445        $urlParameters = str_replace(',', $separator, $urlParameters);
446    }
447    if($id === '') {
448        $id = $conf['start'];
449    }
450    $id = idfilter($id);
451    if($absolute) {
452        $xlink = DOKU_URL;
453    } else {
454        $xlink = DOKU_BASE;
455    }
456
457    if($conf['userewrite'] == 2) {
458        $xlink .= DOKU_SCRIPT.'/'.$id;
459        if($urlParameters) $xlink .= '?'.$urlParameters;
460    } elseif($conf['userewrite']) {
461        $xlink .= $id;
462        if($urlParameters) $xlink .= '?'.$urlParameters;
463    } elseif($id) {
464        $xlink .= DOKU_SCRIPT.'?id='.$id;
465        if($urlParameters) $xlink .= $separator.$urlParameters;
466    } else {
467        $xlink .= DOKU_SCRIPT;
468        if($urlParameters) $xlink .= '?'.$urlParameters;
469    }
470
471    return $xlink;
472}
473
474/**
475 * This builds a link to an alternate page format
476 *
477 * Handles URL rewriting if enabled. Follows the style of wl().
478 *
479 * @author Ben Coburn <btcoburn@silicodon.net>
480 * @param string       $id             page id, defaults to start page
481 * @param string       $format         the export renderer to use
482 * @param string|array $urlParameters  URL parameters, associative array recommended
483 * @param bool         $abs            request an absolute URL instead of relative
484 * @param string       $sep            parameter separator
485 * @return string
486 */
487function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;') {
488    global $conf;
489    if(is_array($urlParameters)) {
490        $urlParameters = buildURLparams($urlParameters, $sep);
491    } else {
492        $urlParameters = str_replace(',', $sep, $urlParameters);
493    }
494
495    $format = rawurlencode($format);
496    $id     = idfilter($id);
497    if($abs) {
498        $xlink = DOKU_URL;
499    } else {
500        $xlink = DOKU_BASE;
501    }
502
503    if($conf['userewrite'] == 2) {
504        $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
505        if($urlParameters) $xlink .= $sep.$urlParameters;
506    } elseif($conf['userewrite'] == 1) {
507        $xlink .= '_export/'.$format.'/'.$id;
508        if($urlParameters) $xlink .= '?'.$urlParameters;
509    } else {
510        $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
511        if($urlParameters) $xlink .= $sep.$urlParameters;
512    }
513
514    return $xlink;
515}
516
517/**
518 * Build a link to a media file
519 *
520 * Will return a link to the detail page if $direct is false
521 *
522 * The $more parameter should always be given as array, the function then
523 * will strip default parameters to produce even cleaner URLs
524 *
525 * @param string  $id     the media file id or URL
526 * @param mixed   $more   string or array with additional parameters
527 * @param bool    $direct link to detail page if false
528 * @param string  $sep    URL parameter separator
529 * @param bool    $abs    Create an absolute URL
530 * @return string
531 */
532function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
533    global $conf;
534    $isexternalimage = media_isexternal($id);
535    if(!$isexternalimage) {
536        $id = cleanID($id);
537    }
538
539    if(is_array($more)) {
540        // add token for resized images
541        if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){
542            $more['tok'] = media_get_token($id,$more['w'],$more['h']);
543        }
544        // strip defaults for shorter URLs
545        if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
546        if(empty($more['w'])) unset($more['w']);
547        if(empty($more['h'])) unset($more['h']);
548        if(isset($more['id']) && $direct) unset($more['id']);
549        $more = buildURLparams($more, $sep);
550    } else {
551        $matches = array();
552        if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){
553            $resize = array('w'=>0, 'h'=>0);
554            foreach ($matches as $match){
555                $resize[$match[1]] = $match[2];
556            }
557            $more .= $more === '' ? '' : $sep;
558            $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']);
559        }
560        $more = str_replace('cache=cache', '', $more); //skip default
561        $more = str_replace(',,', ',', $more);
562        $more = str_replace(',', $sep, $more);
563    }
564
565    if($abs) {
566        $xlink = DOKU_URL;
567    } else {
568        $xlink = DOKU_BASE;
569    }
570
571    // external URLs are always direct without rewriting
572    if($isexternalimage) {
573        $xlink .= 'lib/exe/fetch.php';
574        $xlink .= '?'.$more;
575        $xlink .= $sep.'media='.rawurlencode($id);
576        return $xlink;
577    }
578
579    $id = idfilter($id);
580
581    // decide on scriptname
582    if($direct) {
583        if($conf['userewrite'] == 1) {
584            $script = '_media';
585        } else {
586            $script = 'lib/exe/fetch.php';
587        }
588    } else {
589        if($conf['userewrite'] == 1) {
590            $script = '_detail';
591        } else {
592            $script = 'lib/exe/detail.php';
593        }
594    }
595
596    // build URL based on rewrite mode
597    if($conf['userewrite']) {
598        $xlink .= $script.'/'.$id;
599        if($more) $xlink .= '?'.$more;
600    } else {
601        if($more) {
602            $xlink .= $script.'?'.$more;
603            $xlink .= $sep.'media='.$id;
604        } else {
605            $xlink .= $script.'?media='.$id;
606        }
607    }
608
609    return $xlink;
610}
611
612/**
613 * Returns the URL to the DokuWiki base script
614 *
615 * Consider using wl() instead, unless you absoutely need the doku.php endpoint
616 *
617 * @author Andreas Gohr <andi@splitbrain.org>
618 *
619 * @return string
620 */
621function script() {
622    return DOKU_BASE.DOKU_SCRIPT;
623}
624
625/**
626 * Spamcheck against wordlist
627 *
628 * Checks the wikitext against a list of blocked expressions
629 * returns true if the text contains any bad words
630 *
631 * Triggers COMMON_WORDBLOCK_BLOCKED
632 *
633 *  Action Plugins can use this event to inspect the blocked data
634 *  and gain information about the user who was blocked.
635 *
636 *  Event data:
637 *    data['matches']  - array of matches
638 *    data['userinfo'] - information about the blocked user
639 *      [ip]           - ip address
640 *      [user]         - username (if logged in)
641 *      [mail]         - mail address (if logged in)
642 *      [name]         - real name (if logged in)
643 *
644 * @author Andreas Gohr <andi@splitbrain.org>
645 * @author Michael Klier <chi@chimeric.de>
646 *
647 * @param  string $text - optional text to check, if not given the globals are used
648 * @return bool         - true if a spam word was found
649 */
650function checkwordblock($text = '') {
651    global $TEXT;
652    global $PRE;
653    global $SUF;
654    global $SUM;
655    global $conf;
656    global $INFO;
657    /* @var Input $INPUT */
658    global $INPUT;
659
660    if(!$conf['usewordblock']) return false;
661
662    if(!$text) $text = "$PRE $TEXT $SUF $SUM";
663
664    // we prepare the text a tiny bit to prevent spammers circumventing URL checks
665    $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i', '\1http://\2 \2\3', $text);
666
667    $wordblocks = getWordblocks();
668    // how many lines to read at once (to work around some PCRE limits)
669    if(version_compare(phpversion(), '4.3.0', '<')) {
670        // old versions of PCRE define a maximum of parenthesises even if no
671        // backreferences are used - the maximum is 99
672        // this is very bad performancewise and may even be too high still
673        $chunksize = 40;
674    } else {
675        // read file in chunks of 200 - this should work around the
676        // MAX_PATTERN_SIZE in modern PCRE
677        $chunksize = 200;
678    }
679    while($blocks = array_splice($wordblocks, 0, $chunksize)) {
680        $re = array();
681        // build regexp from blocks
682        foreach($blocks as $block) {
683            $block = preg_replace('/#.*$/', '', $block);
684            $block = trim($block);
685            if(empty($block)) continue;
686            $re[] = $block;
687        }
688        if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) {
689            // prepare event data
690            $data = array();
691            $data['matches']        = $matches;
692            $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
693            if($INPUT->server->str('REMOTE_USER')) {
694                $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
695                $data['userinfo']['name'] = $INFO['userinfo']['name'];
696                $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
697            }
698            $callback = create_function('', 'return true;');
699            return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
700        }
701    }
702    return false;
703}
704
705/**
706 * Return the IP of the client
707 *
708 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
709 *
710 * It returns a comma separated list of IPs if the above mentioned
711 * headers are set. If the single parameter is set, it tries to return
712 * a routable public address, prefering the ones suplied in the X
713 * headers
714 *
715 * @author Andreas Gohr <andi@splitbrain.org>
716 *
717 * @param  boolean $single If set only a single IP is returned
718 * @return string
719 */
720function clientIP($single = false) {
721    /* @var Input $INPUT */
722    global $INPUT;
723
724    $ip   = array();
725    $ip[] = $INPUT->server->str('REMOTE_ADDR');
726    if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
727        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
728    }
729    if($INPUT->server->str('HTTP_X_REAL_IP')) {
730        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
731    }
732
733    // some IPv4/v6 regexps borrowed from Feyd
734    // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
735    $dec_octet   = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
736    $hex_digit   = '[A-Fa-f0-9]';
737    $h16         = "{$hex_digit}{1,4}";
738    $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
739    $ls32        = "(?:$h16:$h16|$IPv4Address)";
740    $IPv6Address =
741        "(?:(?:{$IPv4Address})|(?:".
742            "(?:$h16:){6}$ls32".
743            "|::(?:$h16:){5}$ls32".
744            "|(?:$h16)?::(?:$h16:){4}$ls32".
745            "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32".
746            "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32".
747            "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32".
748            "|(?:(?:$h16:){0,4}$h16)?::$ls32".
749            "|(?:(?:$h16:){0,5}$h16)?::$h16".
750            "|(?:(?:$h16:){0,6}$h16)?::".
751            ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
752
753    // remove any non-IP stuff
754    $cnt   = count($ip);
755    $match = array();
756    for($i = 0; $i < $cnt; $i++) {
757        if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) {
758            $ip[$i] = $match[0];
759        } else {
760            $ip[$i] = '';
761        }
762        if(empty($ip[$i])) unset($ip[$i]);
763    }
764    $ip = array_values(array_unique($ip));
765    if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
766
767    if(!$single) return join(',', $ip);
768
769    // decide which IP to use, trying to avoid local addresses
770    $ip = array_reverse($ip);
771    foreach($ip as $i) {
772        if(preg_match('/^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/', $i)) {
773            continue;
774        } else {
775            return $i;
776        }
777    }
778    // still here? just use the first (last) address
779    return $ip[0];
780}
781
782/**
783 * Check if the browser is on a mobile device
784 *
785 * Adapted from the example code at url below
786 *
787 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
788 *
789 * @return bool if true, client is mobile browser; otherwise false
790 */
791function clientismobile() {
792    /* @var Input $INPUT */
793    global $INPUT;
794
795    if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
796
797    if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
798
799    if(!$INPUT->server->has('HTTP_USER_AGENT')) return false;
800
801    $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto';
802
803    if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
804
805    return false;
806}
807
808/**
809 * Convert one or more comma separated IPs to hostnames
810 *
811 * If $conf['dnslookups'] is disabled it simply returns the input string
812 *
813 * @author Glen Harris <astfgl@iamnota.org>
814 *
815 * @param  string $ips comma separated list of IP addresses
816 * @return string a comma separated list of hostnames
817 */
818function gethostsbyaddrs($ips) {
819    global $conf;
820    if(!$conf['dnslookups']) return $ips;
821
822    $hosts = array();
823    $ips   = explode(',', $ips);
824
825    if(is_array($ips)) {
826        foreach($ips as $ip) {
827            $hosts[] = gethostbyaddr(trim($ip));
828        }
829        return join(',', $hosts);
830    } else {
831        return gethostbyaddr(trim($ips));
832    }
833}
834
835/**
836 * Checks if a given page is currently locked.
837 *
838 * removes stale lockfiles
839 *
840 * @author Andreas Gohr <andi@splitbrain.org>
841 *
842 * @param string $id page id
843 * @return bool page is locked?
844 */
845function checklock($id) {
846    global $conf;
847    /* @var Input $INPUT */
848    global $INPUT;
849
850    $lock = wikiLockFN($id);
851
852    //no lockfile
853    if(!@file_exists($lock)) return false;
854
855    //lockfile expired
856    if((time() - filemtime($lock)) > $conf['locktime']) {
857        @unlink($lock);
858        return false;
859    }
860
861    //my own lock
862    @list($ip, $session) = explode("\n", io_readFile($lock));
863    if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) {
864        return false;
865    }
866
867    return $ip;
868}
869
870/**
871 * Lock a page for editing
872 *
873 * @author Andreas Gohr <andi@splitbrain.org>
874 *
875 * @param string $id page id to lock
876 */
877function lock($id) {
878    global $conf;
879    /* @var Input $INPUT */
880    global $INPUT;
881
882    if($conf['locktime'] == 0) {
883        return;
884    }
885
886    $lock = wikiLockFN($id);
887    if($INPUT->server->str('REMOTE_USER')) {
888        io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
889    } else {
890        io_saveFile($lock, clientIP()."\n".session_id());
891    }
892}
893
894/**
895 * Unlock a page if it was locked by the user
896 *
897 * @author Andreas Gohr <andi@splitbrain.org>
898 *
899 * @param string $id page id to unlock
900 * @return bool true if a lock was removed
901 */
902function unlock($id) {
903    /* @var Input $INPUT */
904    global $INPUT;
905
906    $lock = wikiLockFN($id);
907    if(@file_exists($lock)) {
908        @list($ip, $session) = explode("\n", io_readFile($lock));
909        if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) {
910            @unlink($lock);
911            return true;
912        }
913    }
914    return false;
915}
916
917/**
918 * convert line ending to unix format
919 *
920 * also makes sure the given text is valid UTF-8
921 *
922 * @see    formText() for 2crlf conversion
923 * @author Andreas Gohr <andi@splitbrain.org>
924 *
925 * @param string $text
926 * @return string
927 */
928function cleanText($text) {
929    $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
930
931    // if the text is not valid UTF-8 we simply assume latin1
932    // this won't break any worse than it breaks with the wrong encoding
933    // but might actually fix the problem in many cases
934    if(!utf8_check($text)) $text = utf8_encode($text);
935
936    return $text;
937}
938
939/**
940 * Prepares text for print in Webforms by encoding special chars.
941 * It also converts line endings to Windows format which is
942 * pseudo standard for webforms.
943 *
944 * @see    cleanText() for 2unix conversion
945 * @author Andreas Gohr <andi@splitbrain.org>
946 *
947 * @param string $text
948 * @return string
949 */
950function formText($text) {
951    $text = str_replace("\012", "\015\012", $text);
952    return htmlspecialchars($text);
953}
954
955/**
956 * Returns the specified local text in raw format
957 *
958 * @author Andreas Gohr <andi@splitbrain.org>
959 *
960 * @param string $id   page id
961 * @param string $ext  extension of file being read, default 'txt'
962 * @return string
963 */
964function rawLocale($id, $ext = 'txt') {
965    return io_readFile(localeFN($id, $ext));
966}
967
968/**
969 * Returns the raw WikiText
970 *
971 * @author Andreas Gohr <andi@splitbrain.org>
972 *
973 * @param string $id   page id
974 * @param string|int $rev  timestamp when a revision of wikitext is desired
975 * @return string
976 */
977function rawWiki($id, $rev = '') {
978    return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
979}
980
981/**
982 * Returns the pagetemplate contents for the ID's namespace
983 *
984 * @triggers COMMON_PAGETPL_LOAD
985 * @author Andreas Gohr <andi@splitbrain.org>
986 *
987 * @param string $id the id of the page to be created
988 * @return string parsed pagetemplate content
989 */
990function pageTemplate($id) {
991    global $conf;
992
993    if(is_array($id)) $id = $id[0];
994
995    // prepare initial event data
996    $data = array(
997        'id'        => $id, // the id of the page to be created
998        'tpl'       => '', // the text used as template
999        'tplfile'   => '', // the file above text was/should be loaded from
1000        'doreplace' => true // should wildcard replacements be done on the text?
1001    );
1002
1003    $evt = new Doku_Event('COMMON_PAGETPL_LOAD', $data);
1004    if($evt->advise_before(true)) {
1005        // the before event might have loaded the content already
1006        if(empty($data['tpl'])) {
1007            // if the before event did not set a template file, try to find one
1008            if(empty($data['tplfile'])) {
1009                $path = dirname(wikiFN($id));
1010                if(@file_exists($path.'/_template.txt')) {
1011                    $data['tplfile'] = $path.'/_template.txt';
1012                } else {
1013                    // search upper namespaces for templates
1014                    $len = strlen(rtrim($conf['datadir'], '/'));
1015                    while(strlen($path) >= $len) {
1016                        if(@file_exists($path.'/__template.txt')) {
1017                            $data['tplfile'] = $path.'/__template.txt';
1018                            break;
1019                        }
1020                        $path = substr($path, 0, strrpos($path, '/'));
1021                    }
1022                }
1023            }
1024            // load the content
1025            $data['tpl'] = io_readFile($data['tplfile']);
1026        }
1027        if($data['doreplace']) parsePageTemplate($data);
1028    }
1029    $evt->advise_after();
1030    unset($evt);
1031
1032    return $data['tpl'];
1033}
1034
1035/**
1036 * Performs common page template replacements
1037 * This works on data from COMMON_PAGETPL_LOAD
1038 *
1039 * @author Andreas Gohr <andi@splitbrain.org>
1040 *
1041 * @param array $data array with event data
1042 * @return string
1043 */
1044function parsePageTemplate(&$data) {
1045    /**
1046     * @var string $id        the id of the page to be created
1047     * @var string $tpl       the text used as template
1048     * @var string $tplfile   the file above text was/should be loaded from
1049     * @var bool   $doreplace should wildcard replacements be done on the text?
1050     */
1051    extract($data);
1052
1053    global $USERINFO;
1054    global $conf;
1055    /* @var Input $INPUT */
1056    global $INPUT;
1057
1058    // replace placeholders
1059    $file = noNS($id);
1060    $page = strtr($file, $conf['sepchar'], ' ');
1061
1062    $tpl = str_replace(
1063        array(
1064             '@ID@',
1065             '@NS@',
1066             '@FILE@',
1067             '@!FILE@',
1068             '@!FILE!@',
1069             '@PAGE@',
1070             '@!PAGE@',
1071             '@!!PAGE@',
1072             '@!PAGE!@',
1073             '@USER@',
1074             '@NAME@',
1075             '@MAIL@',
1076             '@DATE@',
1077        ),
1078        array(
1079             $id,
1080             getNS($id),
1081             $file,
1082             utf8_ucfirst($file),
1083             utf8_strtoupper($file),
1084             $page,
1085             utf8_ucfirst($page),
1086             utf8_ucwords($page),
1087             utf8_strtoupper($page),
1088             $INPUT->server->str('REMOTE_USER'),
1089             $USERINFO['name'],
1090             $USERINFO['mail'],
1091             $conf['dformat'],
1092        ), $tpl
1093    );
1094
1095    // we need the callback to work around strftime's char limit
1096    $tpl         = preg_replace_callback('/%./', create_function('$m', 'return strftime($m[0]);'), $tpl);
1097    $data['tpl'] = $tpl;
1098    return $tpl;
1099}
1100
1101/**
1102 * Returns the raw Wiki Text in three slices.
1103 *
1104 * The range parameter needs to have the form "from-to"
1105 * and gives the range of the section in bytes - no
1106 * UTF-8 awareness is needed.
1107 * The returned order is prefix, section and suffix.
1108 *
1109 * @author Andreas Gohr <andi@splitbrain.org>
1110 *
1111 * @param string $range in form "from-to"
1112 * @param string $id    page id
1113 * @param string $rev   optional, the revision timestamp
1114 * @return string[] with three slices
1115 */
1116function rawWikiSlices($range, $id, $rev = '') {
1117    $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1118
1119    // Parse range
1120    list($from, $to) = explode('-', $range, 2);
1121    // Make range zero-based, use defaults if marker is missing
1122    $from = !$from ? 0 : ($from - 1);
1123    $to   = !$to ? strlen($text) : ($to - 1);
1124
1125    $slices = array();
1126    $slices[0] = substr($text, 0, $from);
1127    $slices[1] = substr($text, $from, $to - $from);
1128    $slices[2] = substr($text, $to);
1129    return $slices;
1130}
1131
1132/**
1133 * Joins wiki text slices
1134 *
1135 * function to join the text slices.
1136 * When the pretty parameter is set to true it adds additional empty
1137 * lines between sections if needed (used on saving).
1138 *
1139 * @author Andreas Gohr <andi@splitbrain.org>
1140 *
1141 * @param string $pre   prefix
1142 * @param string $text  text in the middle
1143 * @param string $suf   suffix
1144 * @param bool $pretty add additional empty lines between sections
1145 * @return string
1146 */
1147function con($pre, $text, $suf, $pretty = false) {
1148    if($pretty) {
1149        if($pre !== '' && substr($pre, -1) !== "\n" &&
1150            substr($text, 0, 1) !== "\n"
1151        ) {
1152            $pre .= "\n";
1153        }
1154        if($suf !== '' && substr($text, -1) !== "\n" &&
1155            substr($suf, 0, 1) !== "\n"
1156        ) {
1157            $text .= "\n";
1158        }
1159    }
1160
1161    return $pre.$text.$suf;
1162}
1163
1164/**
1165 * Saves a wikitext by calling io_writeWikiPage.
1166 * Also directs changelog and attic updates.
1167 *
1168 * @author Andreas Gohr <andi@splitbrain.org>
1169 * @author Ben Coburn <btcoburn@silicodon.net>
1170 *
1171 * @param string $id       page id
1172 * @param string $text     wikitext being saved
1173 * @param string $summary  summary of text update
1174 * @param bool   $minor    mark this saved version as minor update
1175 */
1176function saveWikiText($id, $text, $summary, $minor = false) {
1177    /* Note to developers:
1178       This code is subtle and delicate. Test the behavior of
1179       the attic and changelog with dokuwiki and external edits
1180       after any changes. External edits change the wiki page
1181       directly without using php or dokuwiki.
1182     */
1183    global $conf;
1184    global $lang;
1185    global $REV;
1186    /* @var Input $INPUT */
1187    global $INPUT;
1188
1189    // ignore if no changes were made
1190    if($text == rawWiki($id, '')) {
1191        return;
1192    }
1193
1194    $file        = wikiFN($id);
1195    $old         = @filemtime($file); // from page
1196    $wasRemoved  = (trim($text) == ''); // check for empty or whitespace only
1197    $wasCreated  = !@file_exists($file);
1198    $wasReverted = ($REV == true);
1199    $pagelog     = new PageChangeLog($id, 1024);
1200    $newRev      = false;
1201    $oldRev      = $pagelog->getRevisions(-1, 1); // from changelog
1202    $oldRev      = (int) (empty($oldRev) ? 0 : $oldRev[0]);
1203    if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old >= $oldRev) {
1204        // add old revision to the attic if missing
1205        saveOldRevision($id);
1206        // add a changelog entry if this edit came from outside dokuwiki
1207        if($old > $oldRev) {
1208            addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true));
1209            // remove soon to be stale instructions
1210            $cache = new cache_instructions($id, $file);
1211            $cache->removeCache();
1212        }
1213    }
1214
1215    if($wasRemoved) {
1216        // Send "update" event with empty data, so plugins can react to page deletion
1217        $data = array(array($file, '', false), getNS($id), noNS($id), false);
1218        trigger_event('IO_WIKIPAGE_WRITE', $data);
1219        // pre-save deleted revision
1220        @touch($file);
1221        clearstatcache();
1222        $newRev = saveOldRevision($id);
1223        // remove empty file
1224        @unlink($file);
1225        // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata...
1226        // purge non-persistant meta data
1227        p_purge_metadata($id);
1228        $del = true;
1229        // autoset summary on deletion
1230        if(empty($summary)) $summary = $lang['deleted'];
1231        // remove empty namespaces
1232        io_sweepNS($id, 'datadir');
1233        io_sweepNS($id, 'mediadir');
1234    } else {
1235        // save file (namespace dir is created in io_writeWikiPage)
1236        io_writeWikiPage($file, $text, $id);
1237        // pre-save the revision, to keep the attic in sync
1238        $newRev = saveOldRevision($id);
1239        $del    = false;
1240    }
1241
1242    // select changelog line type
1243    $extra = '';
1244    $type  = DOKU_CHANGE_TYPE_EDIT;
1245    if($wasReverted) {
1246        $type  = DOKU_CHANGE_TYPE_REVERT;
1247        $extra = $REV;
1248    } else if($wasCreated) {
1249        $type = DOKU_CHANGE_TYPE_CREATE;
1250    } else if($wasRemoved) {
1251        $type = DOKU_CHANGE_TYPE_DELETE;
1252    } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
1253        $type = DOKU_CHANGE_TYPE_MINOR_EDIT;
1254    } //minor edits only for logged in users
1255
1256    addLogEntry($newRev, $id, $type, $summary, $extra);
1257    // send notify mails
1258    notify($id, 'admin', $old, $summary, $minor);
1259    notify($id, 'subscribers', $old, $summary, $minor);
1260
1261    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1262    io_saveFile($conf['cachedir'].'/purgefile', time());
1263
1264    // if useheading is enabled, purge the cache of all linking pages
1265    if(useHeading('content')) {
1266        $pages = ft_backlinks($id, true);
1267        foreach($pages as $page) {
1268            $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
1269            $cache->removeCache();
1270        }
1271    }
1272}
1273
1274/**
1275 * moves the current version to the attic and returns its
1276 * revision date
1277 *
1278 * @author Andreas Gohr <andi@splitbrain.org>
1279 *
1280 * @param string $id page id
1281 * @return int|string revision timestamp
1282 */
1283function saveOldRevision($id) {
1284    $oldf = wikiFN($id);
1285    if(!@file_exists($oldf)) return '';
1286    $date = filemtime($oldf);
1287    $newf = wikiFN($id, $date);
1288    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1289    return $date;
1290}
1291
1292/**
1293 * Sends a notify mail on page change or registration
1294 *
1295 * @param string     $id       The changed page
1296 * @param string     $who      Who to notify (admin|subscribers|register)
1297 * @param int|string $rev Old page revision
1298 * @param string     $summary  What changed
1299 * @param boolean    $minor    Is this a minor edit?
1300 * @param string[]   $replace  Additional string substitutions, @KEY@ to be replaced by value
1301 * @return bool
1302 *
1303 * @author Andreas Gohr <andi@splitbrain.org>
1304 */
1305function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) {
1306    global $conf;
1307    /* @var Input $INPUT */
1308    global $INPUT;
1309
1310    // decide if there is something to do, eg. whom to mail
1311    if($who == 'admin') {
1312        if(empty($conf['notify'])) return false; //notify enabled?
1313        $tpl = 'mailtext';
1314        $to  = $conf['notify'];
1315    } elseif($who == 'subscribers') {
1316        if(!actionOK('subscribe')) return false; //subscribers enabled?
1317        if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1318        $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
1319        trigger_event(
1320            'COMMON_NOTIFY_ADDRESSLIST', $data,
1321            array(new Subscription(), 'notifyaddresses')
1322        );
1323        $to = $data['addresslist'];
1324        if(empty($to)) return false;
1325        $tpl = 'subscr_single';
1326    } else {
1327        return false; //just to be safe
1328    }
1329
1330    // prepare content
1331    $subscription = new Subscription();
1332    return $subscription->send_diff($to, $tpl, $id, $rev, $summary);
1333}
1334
1335/**
1336 * extracts the query from a search engine referrer
1337 *
1338 * @author Andreas Gohr <andi@splitbrain.org>
1339 * @author Todd Augsburger <todd@rollerorgans.com>
1340 *
1341 * @return array|string
1342 */
1343function getGoogleQuery() {
1344    /* @var Input $INPUT */
1345    global $INPUT;
1346
1347    if(!$INPUT->server->has('HTTP_REFERER')) {
1348        return '';
1349    }
1350    $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1351
1352    // only handle common SEs
1353    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1354
1355    $query = array();
1356    // temporary workaround against PHP bug #49733
1357    // see http://bugs.php.net/bug.php?id=49733
1358    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1359    parse_str($url['query'], $query);
1360    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1361
1362    $q = '';
1363    if(isset($query['q'])){
1364        $q = $query['q'];
1365    }elseif(isset($query['p'])){
1366        $q = $query['p'];
1367    }elseif(isset($query['query'])){
1368        $q = $query['query'];
1369    }
1370    $q = trim($q);
1371
1372    if(!$q) return '';
1373    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1374    return $q;
1375}
1376
1377/**
1378 * Return the human readable size of a file
1379 *
1380 * @param int $size A file size
1381 * @param int $dec A number of decimal places
1382 * @return string human readable size
1383 *
1384 * @author      Martin Benjamin <b.martin@cybernet.ch>
1385 * @author      Aidan Lister <aidan@php.net>
1386 * @version     1.0.0
1387 */
1388function filesize_h($size, $dec = 1) {
1389    $sizes = array('B', 'KB', 'MB', 'GB');
1390    $count = count($sizes);
1391    $i     = 0;
1392
1393    while($size >= 1024 && ($i < $count - 1)) {
1394        $size /= 1024;
1395        $i++;
1396    }
1397
1398    return round($size, $dec).' '.$sizes[$i];
1399}
1400
1401/**
1402 * Return the given timestamp as human readable, fuzzy age
1403 *
1404 * @author Andreas Gohr <gohr@cosmocode.de>
1405 *
1406 * @param int $dt timestamp
1407 * @return string
1408 */
1409function datetime_h($dt) {
1410    global $lang;
1411
1412    $ago = time() - $dt;
1413    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1414        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1415    }
1416    if($ago > 24 * 60 * 60 * 30 * 2) {
1417        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1418    }
1419    if($ago > 24 * 60 * 60 * 7 * 2) {
1420        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1421    }
1422    if($ago > 24 * 60 * 60 * 2) {
1423        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1424    }
1425    if($ago > 60 * 60 * 2) {
1426        return sprintf($lang['hours'], round($ago / (60 * 60)));
1427    }
1428    if($ago > 60 * 2) {
1429        return sprintf($lang['minutes'], round($ago / (60)));
1430    }
1431    return sprintf($lang['seconds'], $ago);
1432}
1433
1434/**
1435 * Wraps around strftime but provides support for fuzzy dates
1436 *
1437 * The format default to $conf['dformat']. It is passed to
1438 * strftime - %f can be used to get the value from datetime_h()
1439 *
1440 * @see datetime_h
1441 * @author Andreas Gohr <gohr@cosmocode.de>
1442 *
1443 * @param int|null $dt      timestamp when given, null will take current timestamp
1444 * @param string   $format  empty default to $conf['dformat'], or provide format as recognized by strftime()
1445 * @return string
1446 */
1447function dformat($dt = null, $format = '') {
1448    global $conf;
1449
1450    if(is_null($dt)) $dt = time();
1451    $dt = (int) $dt;
1452    if(!$format) $format = $conf['dformat'];
1453
1454    $format = str_replace('%f', datetime_h($dt), $format);
1455    return strftime($format, $dt);
1456}
1457
1458/**
1459 * Formats a timestamp as ISO 8601 date
1460 *
1461 * @author <ungu at terong dot com>
1462 * @link http://www.php.net/manual/en/function.date.php#54072
1463 *
1464 * @param int $int_date: current date in UNIX timestamp
1465 * @return string
1466 */
1467function date_iso8601($int_date) {
1468    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1469    $pre_timezone = date('O', $int_date);
1470    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1471    $date_mod .= $time_zone;
1472    return $date_mod;
1473}
1474
1475/**
1476 * return an obfuscated email address in line with $conf['mailguard'] setting
1477 *
1478 * @author Harry Fuecks <hfuecks@gmail.com>
1479 * @author Christopher Smith <chris@jalakai.co.uk>
1480 *
1481 * @param string $email email address
1482 * @return string
1483 */
1484function obfuscate($email) {
1485    global $conf;
1486
1487    switch($conf['mailguard']) {
1488        case 'visible' :
1489            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1490            return strtr($email, $obfuscate);
1491
1492        case 'hex' :
1493            $encode = '';
1494            $len    = strlen($email);
1495            for($x = 0; $x < $len; $x++) {
1496                $encode .= '&#x'.bin2hex($email{$x}).';';
1497            }
1498            return $encode;
1499
1500        case 'none' :
1501        default :
1502            return $email;
1503    }
1504}
1505
1506/**
1507 * Removes quoting backslashes
1508 *
1509 * @author Andreas Gohr <andi@splitbrain.org>
1510 *
1511 * @param string $string
1512 * @param string $char backslashed character
1513 * @return string
1514 */
1515function unslash($string, $char = "'") {
1516    return str_replace('\\'.$char, $char, $string);
1517}
1518
1519/**
1520 * Convert php.ini shorthands to byte
1521 *
1522 * @author <gilthans dot NO dot SPAM at gmail dot com>
1523 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1524 *
1525 * @param string $v shorthands
1526 * @return int|string
1527 */
1528function php_to_byte($v) {
1529    $l   = substr($v, -1);
1530    $ret = substr($v, 0, -1);
1531    switch(strtoupper($l)) {
1532        /** @noinspection PhpMissingBreakStatementInspection */
1533        case 'P':
1534            $ret *= 1024;
1535        /** @noinspection PhpMissingBreakStatementInspection */
1536        case 'T':
1537            $ret *= 1024;
1538        /** @noinspection PhpMissingBreakStatementInspection */
1539        case 'G':
1540            $ret *= 1024;
1541        /** @noinspection PhpMissingBreakStatementInspection */
1542        case 'M':
1543            $ret *= 1024;
1544        /** @noinspection PhpMissingBreakStatementInspection */
1545        case 'K':
1546            $ret *= 1024;
1547            break;
1548        default;
1549            $ret *= 10;
1550            break;
1551    }
1552    return $ret;
1553}
1554
1555/**
1556 * Wrapper around preg_quote adding the default delimiter
1557 *
1558 * @param string $string
1559 * @return string
1560 */
1561function preg_quote_cb($string) {
1562    return preg_quote($string, '/');
1563}
1564
1565/**
1566 * Shorten a given string by removing data from the middle
1567 *
1568 * You can give the string in two parts, the first part $keep
1569 * will never be shortened. The second part $short will be cut
1570 * in the middle to shorten but only if at least $min chars are
1571 * left to display it. Otherwise it will be left off.
1572 *
1573 * @param string $keep   the part to keep
1574 * @param string $short  the part to shorten
1575 * @param int    $max    maximum chars you want for the whole string
1576 * @param int    $min    minimum number of chars to have left for middle shortening
1577 * @param string $char   the shortening character to use
1578 * @return string
1579 */
1580function shorten($keep, $short, $max, $min = 9, $char = '…') {
1581    $max = $max - utf8_strlen($keep);
1582    if($max < $min) return $keep;
1583    $len = utf8_strlen($short);
1584    if($len <= $max) return $keep.$short;
1585    $half = floor($max / 2);
1586    return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half);
1587}
1588
1589/**
1590 * Return the users real name or e-mail address for use
1591 * in page footer and recent changes pages
1592 *
1593 * @param string|null $username or null when currently logged-in user should be used
1594 * @param bool $textonly true returns only plain text, true allows returning html
1595 * @return string html or plain text(not escaped) of formatted user name
1596 *
1597 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1598 */
1599function editorinfo($username, $textonly = false) {
1600    return userlink($username, $textonly);
1601}
1602
1603/**
1604 * Returns users realname w/o link
1605 *
1606 * @param string|null $username or null when currently logged-in user should be used
1607 * @param bool $textonly true returns only plain text, true allows returning html
1608 * @return string html or plain text(not escaped) of formatted user name
1609 *
1610 * @triggers COMMON_USER_LINK
1611 */
1612function userlink($username = null, $textonly = false) {
1613    global $conf, $INFO;
1614    /** @var DokuWiki_Auth_Plugin $auth */
1615    global $auth;
1616    /** @var Input $INPUT */
1617    global $INPUT;
1618
1619    // prepare initial event data
1620    $data = array(
1621        'username' => $username, // the unique user name
1622        'name' => '',
1623        'link' => array( //setting 'link' to false disables linking
1624                         'target' => '',
1625                         'pre' => '',
1626                         'suf' => '',
1627                         'style' => '',
1628                         'more' => '',
1629                         'url' => '',
1630                         'title' => '',
1631                         'class' => ''
1632        ),
1633        'userlink' => '', // formatted user name as will be returned
1634        'textonly' => $textonly
1635    );
1636    if($username === null) {
1637        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1638        if($textonly){
1639            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1640        }else {
1641            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1642        }
1643    }
1644
1645    $evt = new Doku_Event('COMMON_USER_LINK', $data);
1646    if($evt->advise_before(true)) {
1647        if(empty($data['name'])) {
1648            if($auth) $info = $auth->getUserData($username);
1649            if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1650                switch($conf['showuseras']) {
1651                    case 'username':
1652                    case 'username_link':
1653                        $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1654                        break;
1655                    case 'email':
1656                    case 'email_link':
1657                        $data['name'] = obfuscate($info['mail']);
1658                        break;
1659                }
1660            } else {
1661                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1662            }
1663        }
1664
1665        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1666        static $xhtml_renderer = null;
1667
1668        if(!$data['textonly'] && empty($data['link']['url'])) {
1669
1670            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1671                if(!isset($info)) {
1672                    if($auth) $info = $auth->getUserData($username);
1673                }
1674                if(isset($info) && $info) {
1675                    if($conf['showuseras'] == 'email_link') {
1676                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1677                    } else {
1678                        if(is_null($xhtml_renderer)) {
1679                            $xhtml_renderer = p_get_renderer('xhtml');
1680                        }
1681                        if(empty($xhtml_renderer->interwiki)) {
1682                            $xhtml_renderer->interwiki = getInterwiki();
1683                        }
1684                        $shortcut = 'user';
1685                        $exists = null;
1686                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1687                        $data['link']['class'] .= ' interwiki iw_user';
1688                        if($exists !== null) {
1689                            if($exists) {
1690                                $data['link']['class'] .= ' wikilink1';
1691                            } else {
1692                                $data['link']['class'] .= ' wikilink2';
1693                                $data['link']['rel'] = 'nofollow';
1694                            }
1695                        }
1696                    }
1697                } else {
1698                    $data['textonly'] = true;
1699                }
1700
1701            } else {
1702                $data['textonly'] = true;
1703            }
1704        }
1705
1706        if($data['textonly']) {
1707            $data['userlink'] = $data['name'];
1708        } else {
1709            $data['link']['name'] = $data['name'];
1710            if(is_null($xhtml_renderer)) {
1711                $xhtml_renderer = p_get_renderer('xhtml');
1712            }
1713            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1714        }
1715    }
1716    $evt->advise_after();
1717    unset($evt);
1718
1719    return $data['userlink'];
1720}
1721
1722/**
1723 * Returns the path to a image file for the currently chosen license.
1724 * When no image exists, returns an empty string
1725 *
1726 * @author Andreas Gohr <andi@splitbrain.org>
1727 *
1728 * @param  string $type - type of image 'badge' or 'button'
1729 * @return string
1730 */
1731function license_img($type) {
1732    global $license;
1733    global $conf;
1734    if(!$conf['license']) return '';
1735    if(!is_array($license[$conf['license']])) return '';
1736    $try   = array();
1737    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1738    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1739    if(substr($conf['license'], 0, 3) == 'cc-') {
1740        $try[] = 'lib/images/license/'.$type.'/cc.png';
1741    }
1742    foreach($try as $src) {
1743        if(@file_exists(DOKU_INC.$src)) return $src;
1744    }
1745    return '';
1746}
1747
1748/**
1749 * Checks if the given amount of memory is available
1750 *
1751 * If the memory_get_usage() function is not available the
1752 * function just assumes $bytes of already allocated memory
1753 *
1754 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1755 * @author Andreas Gohr <andi@splitbrain.org>
1756 *
1757 * @param int  $mem    Size of memory you want to allocate in bytes
1758 * @param int  $bytes  already allocated memory (see above)
1759 * @return bool
1760 */
1761function is_mem_available($mem, $bytes = 1048576) {
1762    $limit = trim(ini_get('memory_limit'));
1763    if(empty($limit)) return true; // no limit set!
1764
1765    // parse limit to bytes
1766    $limit = php_to_byte($limit);
1767
1768    // get used memory if possible
1769    if(function_exists('memory_get_usage')) {
1770        $used = memory_get_usage();
1771    } else {
1772        $used = $bytes;
1773    }
1774
1775    if($used + $mem > $limit) {
1776        return false;
1777    }
1778
1779    return true;
1780}
1781
1782/**
1783 * Send a HTTP redirect to the browser
1784 *
1785 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1786 *
1787 * @link   http://support.microsoft.com/kb/q176113/
1788 * @author Andreas Gohr <andi@splitbrain.org>
1789 *
1790 * @param string $url url being directed to
1791 */
1792function send_redirect($url) {
1793    /* @var Input $INPUT */
1794    global $INPUT;
1795
1796    //are there any undisplayed messages? keep them in session for display
1797    global $MSG;
1798    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1799        //reopen session, store data and close session again
1800        @session_start();
1801        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1802    }
1803
1804    // always close the session
1805    session_write_close();
1806
1807    // work around IE bug
1808    // http://www.ianhoar.com/2008/11/16/internet-explorer-6-and-redirected-anchor-links/
1809    @list($url, $hash) = explode('#', $url);
1810    if($hash) {
1811        if(strpos($url, '?')) {
1812            $url = $url.'&#'.$hash;
1813        } else {
1814            $url = $url.'?&#'.$hash;
1815        }
1816    }
1817
1818    // check if running on IIS < 6 with CGI-PHP
1819    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1820        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1821        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1822        $matches[1] < 6
1823    ) {
1824        header('Refresh: 0;url='.$url);
1825    } else {
1826        header('Location: '.$url);
1827    }
1828    exit;
1829}
1830
1831/**
1832 * Validate a value using a set of valid values
1833 *
1834 * This function checks whether a specified value is set and in the array
1835 * $valid_values. If not, the function returns a default value or, if no
1836 * default is specified, throws an exception.
1837 *
1838 * @param string $param        The name of the parameter
1839 * @param array  $valid_values A set of valid values; Optionally a default may
1840 *                             be marked by the key “default”.
1841 * @param array  $array        The array containing the value (typically $_POST
1842 *                             or $_GET)
1843 * @param string $exc          The text of the raised exception
1844 *
1845 * @throws Exception
1846 * @return mixed
1847 * @author Adrian Lang <lang@cosmocode.de>
1848 */
1849function valid_input_set($param, $valid_values, $array, $exc = '') {
1850    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1851        return $array[$param];
1852    } elseif(isset($valid_values['default'])) {
1853        return $valid_values['default'];
1854    } else {
1855        throw new Exception($exc);
1856    }
1857}
1858
1859/**
1860 * Read a preference from the DokuWiki cookie
1861 * (remembering both keys & values are urlencoded)
1862 *
1863 * @param string $pref     preference key
1864 * @param mixed  $default  value returned when preference not found
1865 * @return string preference value
1866 */
1867function get_doku_pref($pref, $default) {
1868    $enc_pref = urlencode($pref);
1869    if(strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
1870        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1871        $cnt   = count($parts);
1872        for($i = 0; $i < $cnt; $i += 2) {
1873            if($parts[$i] == $enc_pref) {
1874                return urldecode($parts[$i + 1]);
1875            }
1876        }
1877    }
1878    return $default;
1879}
1880
1881/**
1882 * Add a preference to the DokuWiki cookie
1883 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
1884 *
1885 * @param string $pref  preference key
1886 * @param string $val   preference value
1887 */
1888function set_doku_pref($pref, $val) {
1889    global $conf;
1890    $orig = get_doku_pref($pref, false);
1891    $cookieVal = '';
1892
1893    if($orig && ($orig != $val)) {
1894        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1895        $cnt   = count($parts);
1896        // urlencode $pref for the comparison
1897        $enc_pref = rawurlencode($pref);
1898        for($i = 0; $i < $cnt; $i += 2) {
1899            if($parts[$i] == $enc_pref) {
1900                $parts[$i + 1] = rawurlencode($val);
1901                break;
1902            }
1903        }
1904        $cookieVal = implode('#', $parts);
1905    } else if (!$orig) {
1906        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val);
1907    }
1908
1909    if (!empty($cookieVal)) {
1910        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1911        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
1912    }
1913}
1914
1915/**
1916 * Strips source mapping declarations from given text #601
1917 *
1918 * @param string &$text reference to the CSS or JavaScript code to clean
1919 */
1920function stripsourcemaps(&$text){
1921    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
1922}
1923
1924//Setup VIM: ex: et ts=2 :
1925