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