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