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