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