xref: /dokuwiki/inc/common.php (revision b9b1fa3cc0cbb48e6e911f62909871bb8c72c71d)
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 * @param string|bool $username or false when currently logged-in user should be used
1463 * @param bool $textonly true returns only plain text, true allows returning html
1464 * @return string html or plain text(not escaped) of formatted user name
1465 *
1466 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1467 */
1468function editorinfo($username, $textonly = false) {
1469    return userlink($username, $textonly);
1470}
1471
1472/**
1473 * Returns users realname w/o link
1474 *
1475 * @param string|bool $username or false when currently logged-in user should be used
1476 * @param bool $textonly true returns only plain text, true allows returning html
1477 * @return string html or plain text(not escaped) of formatted user name
1478 *
1479 * @triggers COMMON_USER_LINK
1480 */
1481function userlink($username = null, $textonly = false) {
1482    global $conf, $INFO;
1483    /** @var DokuWiki_Auth_Plugin $auth */
1484    global $auth;
1485    /** @var Input $INPUT */
1486    global $INPUT;
1487
1488    // prepare initial event data
1489    $data = array(
1490        'username' => $username, // the unique user name
1491        'name' => '',
1492        'link' => array( //setting 'link' to false disables linking
1493                         'target' => '',
1494                         'pre' => '',
1495                         'suf' => '',
1496                         'style' => '',
1497                         'more' => '',
1498                         'url' => '',
1499                         'title' => '',
1500                         'class' => ''
1501        ),
1502        'userlink' => '', // formatted user name as will be returned
1503        'textonly' => $textonly
1504    );
1505    if($username === null) {
1506        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1507        if($textonly){
1508            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1509        }else {
1510            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1511        }
1512    }
1513
1514    $evt = new Doku_Event('COMMON_USER_LINK', $data);
1515    if($evt->advise_before(true)) {
1516        if(empty($data['name'])) {
1517            if($conf['showuseras'] == 'loginname') {
1518                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1519            } else {
1520                if($auth) $info = $auth->getUserData($username);
1521                if(isset($info) && $info) {
1522                    switch($conf['showuseras']) {
1523                        case 'username':
1524                        case 'username_link':
1525                            $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1526                            break;
1527                        case 'email':
1528                        case 'email_link':
1529                            $data['name'] = obfuscate($info['mail']);
1530                            break;
1531                    }
1532                }
1533            }
1534        }
1535
1536        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1537        static $xhtml_renderer = null;
1538
1539        if(!$data['textonly'] && empty($data['link']['url'])) {
1540
1541            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1542                if(!isset($info)) {
1543                    if($auth) $info = $auth->getUserData($username);
1544                }
1545                if(isset($info) && $info) {
1546                    if($conf['showuseras'] == 'email_link') {
1547                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1548                    } else {
1549                        if(is_null($xhtml_renderer)) {
1550                            $xhtml_renderer = p_get_renderer('xhtml');
1551                        }
1552                        if(empty($xhtml_renderer->interwiki)) {
1553                            $xhtml_renderer->interwiki = getInterwiki();
1554                        }
1555                        $shortcut = 'user';
1556                        $exists = null;
1557                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1558                        $data['link']['class'] .= ' interwiki iw_user';
1559                        if($exists !== null) {
1560                            if($exists) {
1561                                $data['link']['class'] .= ' wikilink1';
1562                            } else {
1563                                $data['link']['class'] .= ' wikilink2';
1564                                $data['link']['rel'] = 'nofollow';
1565                            }
1566                        }
1567                    }
1568                } else {
1569                    $data['textonly'] = true;
1570                }
1571
1572            } else {
1573                $data['textonly'] = true;
1574            }
1575        }
1576
1577        if($data['textonly']) {
1578            $data['userlink'] = $data['name'];
1579        } else {
1580            $data['link']['name'] = $data['name'];
1581            if(is_null($xhtml_renderer)) {
1582                $xhtml_renderer = p_get_renderer('xhtml');
1583            }
1584            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1585        }
1586    }
1587    $evt->advise_after();
1588    unset($evt);
1589
1590    return $data['userlink'];
1591}
1592
1593/**
1594 * Returns the path to a image file for the currently chosen license.
1595 * When no image exists, returns an empty string
1596 *
1597 * @author Andreas Gohr <andi@splitbrain.org>
1598 * @param  string $type - type of image 'badge' or 'button'
1599 * @return string
1600 */
1601function license_img($type) {
1602    global $license;
1603    global $conf;
1604    if(!$conf['license']) return '';
1605    if(!is_array($license[$conf['license']])) return '';
1606    $lic   = $license[$conf['license']];
1607    $try   = array();
1608    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1609    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1610    if(substr($conf['license'], 0, 3) == 'cc-') {
1611        $try[] = 'lib/images/license/'.$type.'/cc.png';
1612    }
1613    foreach($try as $src) {
1614        if(@file_exists(DOKU_INC.$src)) return $src;
1615    }
1616    return '';
1617}
1618
1619/**
1620 * Checks if the given amount of memory is available
1621 *
1622 * If the memory_get_usage() function is not available the
1623 * function just assumes $bytes of already allocated memory
1624 *
1625 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1626 * @author Andreas Gohr <andi@splitbrain.org>
1627 *
1628 * @param  int $mem  Size of memory you want to allocate in bytes
1629 * @param int  $bytes
1630 * @internal param int $used already allocated memory (see above)
1631 * @return bool
1632 */
1633function is_mem_available($mem, $bytes = 1048576) {
1634    $limit = trim(ini_get('memory_limit'));
1635    if(empty($limit)) return true; // no limit set!
1636
1637    // parse limit to bytes
1638    $limit = php_to_byte($limit);
1639
1640    // get used memory if possible
1641    if(function_exists('memory_get_usage')) {
1642        $used = memory_get_usage();
1643    } else {
1644        $used = $bytes;
1645    }
1646
1647    if($used + $mem > $limit) {
1648        return false;
1649    }
1650
1651    return true;
1652}
1653
1654/**
1655 * Send a HTTP redirect to the browser
1656 *
1657 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1658 *
1659 * @link   http://support.microsoft.com/kb/q176113/
1660 * @author Andreas Gohr <andi@splitbrain.org>
1661 */
1662function send_redirect($url) {
1663    /* @var Input $INPUT */
1664    global $INPUT;
1665
1666    //are there any undisplayed messages? keep them in session for display
1667    global $MSG;
1668    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1669        //reopen session, store data and close session again
1670        @session_start();
1671        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1672    }
1673
1674    // always close the session
1675    session_write_close();
1676
1677    // work around IE bug
1678    // http://www.ianhoar.com/2008/11/16/internet-explorer-6-and-redirected-anchor-links/
1679    @list($url, $hash) = explode('#', $url);
1680    if($hash) {
1681        if(strpos($url, '?')) {
1682            $url = $url.'&#'.$hash;
1683        } else {
1684            $url = $url.'?&#'.$hash;
1685        }
1686    }
1687
1688    // check if running on IIS < 6 with CGI-PHP
1689    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1690        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1691        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1692        $matches[1] < 6
1693    ) {
1694        header('Refresh: 0;url='.$url);
1695    } else {
1696        header('Location: '.$url);
1697    }
1698    exit;
1699}
1700
1701/**
1702 * Validate a value using a set of valid values
1703 *
1704 * This function checks whether a specified value is set and in the array
1705 * $valid_values. If not, the function returns a default value or, if no
1706 * default is specified, throws an exception.
1707 *
1708 * @param string $param        The name of the parameter
1709 * @param array  $valid_values A set of valid values; Optionally a default may
1710 *                             be marked by the key “default”.
1711 * @param array  $array        The array containing the value (typically $_POST
1712 *                             or $_GET)
1713 * @param string $exc          The text of the raised exception
1714 *
1715 * @throws Exception
1716 * @return mixed
1717 * @author Adrian Lang <lang@cosmocode.de>
1718 */
1719function valid_input_set($param, $valid_values, $array, $exc = '') {
1720    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1721        return $array[$param];
1722    } elseif(isset($valid_values['default'])) {
1723        return $valid_values['default'];
1724    } else {
1725        throw new Exception($exc);
1726    }
1727}
1728
1729/**
1730 * Read a preference from the DokuWiki cookie
1731 * (remembering both keys & values are urlencoded)
1732 */
1733function get_doku_pref($pref, $default) {
1734    $enc_pref = urlencode($pref);
1735    if(strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
1736        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1737        $cnt   = count($parts);
1738        for($i = 0; $i < $cnt; $i += 2) {
1739            if($parts[$i] == $enc_pref) {
1740                return urldecode($parts[$i + 1]);
1741            }
1742        }
1743    }
1744    return $default;
1745}
1746
1747/**
1748 * Add a preference to the DokuWiki cookie
1749 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
1750 */
1751function set_doku_pref($pref, $val) {
1752    global $conf;
1753    $orig = get_doku_pref($pref, false);
1754    $cookieVal = '';
1755
1756    if($orig && ($orig != $val)) {
1757        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1758        $cnt   = count($parts);
1759        // urlencode $pref for the comparison
1760        $enc_pref = rawurlencode($pref);
1761        for($i = 0; $i < $cnt; $i += 2) {
1762            if($parts[$i] == $enc_pref) {
1763                $parts[$i + 1] = rawurlencode($val);
1764                break;
1765            }
1766        }
1767        $cookieVal = implode('#', $parts);
1768    } else if (!$orig) {
1769        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val);
1770    }
1771
1772    if (!empty($cookieVal)) {
1773        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1774        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
1775    }
1776}
1777
1778/**
1779 * Strips source mapping declarations from given text #601
1780 *
1781 * @param &string $text reference to the CSS or JavaScript code to clean
1782 */
1783function stripsourcemaps(&$text){
1784    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
1785}
1786
1787//Setup VIM: ex: et ts=2 :
1788