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