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