xref: /dokuwiki/inc/common.php (revision f523c9718baf12a5bc99e2285bc0666796ab2a97)
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    if($REV) {
162        //check if current revision was meant
163        if($info['exists'] && (@filemtime($info['filepath']) == $REV)) {
164            $REV = '';
165        } elseif($RANGE) {
166            //section editing does not work with old revisions!
167            $REV   = '';
168            $RANGE = '';
169            msg($lang['nosecedit'], 0);
170        } else {
171            //really use old revision
172            $info['filepath'] = fullpath(wikiFN($ID, $REV));
173            $info['exists']   = @file_exists($info['filepath']);
174        }
175    }
176    $info['rev'] = $REV;
177    if($info['exists']) {
178        $info['writable'] = (is_writable($info['filepath']) &&
179            ($info['perm'] >= AUTH_EDIT));
180    } else {
181        $info['writable'] = ($info['perm'] >= AUTH_CREATE);
182    }
183    $info['editable'] = ($info['writable'] && empty($info['locked']));
184    $info['lastmod']  = @filemtime($info['filepath']);
185
186    //load page meta data
187    $info['meta'] = p_get_metadata($ID);
188
189    //who's the editor
190    $pagelog = new PageRevisionLog($ID, 1024);
191    if($REV) {
192        $revinfo = $pagelog->getRevisionInfo($REV);
193    } else {
194        if(is_array($info['meta']['last_change'])) {
195            $revinfo = $info['meta']['last_change'];
196        } else {
197            $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
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    $pagelog     = new PageRevisionLog($id, 1024);
1064    $newRev      = false;
1065    $oldRev      = $pagelog->getRevisions(-1, 1); // from changelog
1066    $oldRev      = (int) (empty($oldRev) ? 0 : $oldRev[0]);
1067    if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old >= $oldRev) {
1068        // add old revision to the attic if missing
1069        saveOldRevision($id);
1070        // add a changelog entry if this edit came from outside dokuwiki
1071        if($old > $oldRev) {
1072            addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true));
1073            // remove soon to be stale instructions
1074            $cache = new cache_instructions($id, $file);
1075            $cache->removeCache();
1076        }
1077    }
1078
1079    if($wasRemoved) {
1080        // Send "update" event with empty data, so plugins can react to page deletion
1081        $data = array(array($file, '', false), getNS($id), noNS($id), false);
1082        trigger_event('IO_WIKIPAGE_WRITE', $data);
1083        // pre-save deleted revision
1084        @touch($file);
1085        clearstatcache();
1086        $newRev = saveOldRevision($id);
1087        // remove empty file
1088        @unlink($file);
1089        // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata...
1090        // purge non-persistant meta data
1091        p_purge_metadata($id);
1092        $del = true;
1093        // autoset summary on deletion
1094        if(empty($summary)) $summary = $lang['deleted'];
1095        // remove empty namespaces
1096        io_sweepNS($id, 'datadir');
1097        io_sweepNS($id, 'mediadir');
1098    } else {
1099        // save file (namespace dir is created in io_writeWikiPage)
1100        io_writeWikiPage($file, $text, $id);
1101        // pre-save the revision, to keep the attic in sync
1102        $newRev = saveOldRevision($id);
1103        $del    = false;
1104    }
1105
1106    // select changelog line type
1107    $extra = '';
1108    $type  = DOKU_CHANGE_TYPE_EDIT;
1109    if($wasReverted) {
1110        $type  = DOKU_CHANGE_TYPE_REVERT;
1111        $extra = $REV;
1112    } else if($wasCreated) {
1113        $type = DOKU_CHANGE_TYPE_CREATE;
1114    } else if($wasRemoved) {
1115        $type = DOKU_CHANGE_TYPE_DELETE;
1116    } else if($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) {
1117        $type = DOKU_CHANGE_TYPE_MINOR_EDIT;
1118    } //minor edits only for logged in users
1119
1120    addLogEntry($newRev, $id, $type, $summary, $extra);
1121    // send notify mails
1122    notify($id, 'admin', $old, $summary, $minor);
1123    notify($id, 'subscribers', $old, $summary, $minor);
1124
1125    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1126    io_saveFile($conf['cachedir'].'/purgefile', time());
1127
1128    // if useheading is enabled, purge the cache of all linking pages
1129    if(useHeading('content')) {
1130        $pages = ft_backlinks($id, true);
1131        foreach($pages as $page) {
1132            $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
1133            $cache->removeCache();
1134        }
1135    }
1136}
1137
1138/**
1139 * moves the current version to the attic and returns its
1140 * revision date
1141 *
1142 * @author Andreas Gohr <andi@splitbrain.org>
1143 */
1144function saveOldRevision($id) {
1145    global $conf;
1146    $oldf = wikiFN($id);
1147    if(!@file_exists($oldf)) return '';
1148    $date = filemtime($oldf);
1149    $newf = wikiFN($id, $date);
1150    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1151    return $date;
1152}
1153
1154/**
1155 * Sends a notify mail on page change or registration
1156 *
1157 * @param string     $id       The changed page
1158 * @param string     $who      Who to notify (admin|subscribers|register)
1159 * @param int|string $rev Old page revision
1160 * @param string     $summary  What changed
1161 * @param boolean    $minor    Is this a minor edit?
1162 * @param array      $replace  Additional string substitutions, @KEY@ to be replaced by value
1163 *
1164 * @return bool
1165 * @author Andreas Gohr <andi@splitbrain.org>
1166 */
1167function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) {
1168    global $conf;
1169
1170    // decide if there is something to do, eg. whom to mail
1171    if($who == 'admin') {
1172        if(empty($conf['notify'])) return false; //notify enabled?
1173        $tpl = 'mailtext';
1174        $to  = $conf['notify'];
1175    } elseif($who == 'subscribers') {
1176        if(!actionOK('subscribe')) return false; //subscribers enabled?
1177        if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return false; //skip minors
1178        $data = array('id' => $id, 'addresslist' => '', 'self' => false);
1179        trigger_event(
1180            'COMMON_NOTIFY_ADDRESSLIST', $data,
1181            array(new Subscription(), 'notifyaddresses')
1182        );
1183        $to = $data['addresslist'];
1184        if(empty($to)) return false;
1185        $tpl = 'subscr_single';
1186    } else {
1187        return false; //just to be safe
1188    }
1189
1190    // prepare content
1191    $subscription = new Subscription();
1192    return $subscription->send_diff($to, $tpl, $id, $rev, $summary);
1193}
1194
1195/**
1196 * extracts the query from a search engine referrer
1197 *
1198 * @author Andreas Gohr <andi@splitbrain.org>
1199 * @author Todd Augsburger <todd@rollerorgans.com>
1200 */
1201function getGoogleQuery() {
1202    if(!isset($_SERVER['HTTP_REFERER'])) {
1203        return '';
1204    }
1205    $url = parse_url($_SERVER['HTTP_REFERER']);
1206
1207    // only handle common SEs
1208    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1209
1210    $query = array();
1211    // temporary workaround against PHP bug #49733
1212    // see http://bugs.php.net/bug.php?id=49733
1213    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1214    parse_str($url['query'], $query);
1215    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1216
1217    $q = '';
1218    if(isset($query['q'])){
1219        $q = $query['q'];
1220    }elseif(isset($query['p'])){
1221        $q = $query['p'];
1222    }elseif(isset($query['query'])){
1223        $q = $query['query'];
1224    }
1225    $q = trim($q);
1226
1227    if(!$q) return '';
1228    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1229    return $q;
1230}
1231
1232/**
1233 * Return the human readable size of a file
1234 *
1235 * @param       int    $size   A file size
1236 * @param       int    $dec    A number of decimal places
1237 * @author      Martin Benjamin <b.martin@cybernet.ch>
1238 * @author      Aidan Lister <aidan@php.net>
1239 * @version     1.0.0
1240 */
1241function filesize_h($size, $dec = 1) {
1242    $sizes = array('B', 'KB', 'MB', 'GB');
1243    $count = count($sizes);
1244    $i     = 0;
1245
1246    while($size >= 1024 && ($i < $count - 1)) {
1247        $size /= 1024;
1248        $i++;
1249    }
1250
1251    return round($size, $dec).' '.$sizes[$i];
1252}
1253
1254/**
1255 * Return the given timestamp as human readable, fuzzy age
1256 *
1257 * @author Andreas Gohr <gohr@cosmocode.de>
1258 */
1259function datetime_h($dt) {
1260    global $lang;
1261
1262    $ago = time() - $dt;
1263    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1264        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1265    }
1266    if($ago > 24 * 60 * 60 * 30 * 2) {
1267        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1268    }
1269    if($ago > 24 * 60 * 60 * 7 * 2) {
1270        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1271    }
1272    if($ago > 24 * 60 * 60 * 2) {
1273        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1274    }
1275    if($ago > 60 * 60 * 2) {
1276        return sprintf($lang['hours'], round($ago / (60 * 60)));
1277    }
1278    if($ago > 60 * 2) {
1279        return sprintf($lang['minutes'], round($ago / (60)));
1280    }
1281    return sprintf($lang['seconds'], $ago);
1282}
1283
1284/**
1285 * Wraps around strftime but provides support for fuzzy dates
1286 *
1287 * The format default to $conf['dformat']. It is passed to
1288 * strftime - %f can be used to get the value from datetime_h()
1289 *
1290 * @see datetime_h
1291 * @author Andreas Gohr <gohr@cosmocode.de>
1292 */
1293function dformat($dt = null, $format = '') {
1294    global $conf;
1295
1296    if(is_null($dt)) $dt = time();
1297    $dt = (int) $dt;
1298    if(!$format) $format = $conf['dformat'];
1299
1300    $format = str_replace('%f', datetime_h($dt), $format);
1301    return strftime($format, $dt);
1302}
1303
1304/**
1305 * Formats a timestamp as ISO 8601 date
1306 *
1307 * @author <ungu at terong dot com>
1308 * @link http://www.php.net/manual/en/function.date.php#54072
1309 * @param int $int_date: current date in UNIX timestamp
1310 * @return string
1311 */
1312function date_iso8601($int_date) {
1313    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1314    $pre_timezone = date('O', $int_date);
1315    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1316    $date_mod .= $time_zone;
1317    return $date_mod;
1318}
1319
1320/**
1321 * return an obfuscated email address in line with $conf['mailguard'] setting
1322 *
1323 * @author Harry Fuecks <hfuecks@gmail.com>
1324 * @author Christopher Smith <chris@jalakai.co.uk>
1325 */
1326function obfuscate($email) {
1327    global $conf;
1328
1329    switch($conf['mailguard']) {
1330        case 'visible' :
1331            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1332            return strtr($email, $obfuscate);
1333
1334        case 'hex' :
1335            $encode = '';
1336            $len    = strlen($email);
1337            for($x = 0; $x < $len; $x++) {
1338                $encode .= '&#x'.bin2hex($email{$x}).';';
1339            }
1340            return $encode;
1341
1342        case 'none' :
1343        default :
1344            return $email;
1345    }
1346}
1347
1348/**
1349 * Removes quoting backslashes
1350 *
1351 * @author Andreas Gohr <andi@splitbrain.org>
1352 */
1353function unslash($string, $char = "'") {
1354    return str_replace('\\'.$char, $char, $string);
1355}
1356
1357/**
1358 * Convert php.ini shorthands to byte
1359 *
1360 * @author <gilthans dot NO dot SPAM at gmail dot com>
1361 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1362 */
1363function php_to_byte($v) {
1364    $l   = substr($v, -1);
1365    $ret = substr($v, 0, -1);
1366    switch(strtoupper($l)) {
1367        case 'P':
1368            $ret *= 1024;
1369        case 'T':
1370            $ret *= 1024;
1371        case 'G':
1372            $ret *= 1024;
1373        case 'M':
1374            $ret *= 1024;
1375        case 'K':
1376            $ret *= 1024;
1377            break;
1378        default;
1379            $ret *= 10;
1380            break;
1381    }
1382    return $ret;
1383}
1384
1385/**
1386 * Wrapper around preg_quote adding the default delimiter
1387 */
1388function preg_quote_cb($string) {
1389    return preg_quote($string, '/');
1390}
1391
1392/**
1393 * Shorten a given string by removing data from the middle
1394 *
1395 * You can give the string in two parts, the first part $keep
1396 * will never be shortened. The second part $short will be cut
1397 * in the middle to shorten but only if at least $min chars are
1398 * left to display it. Otherwise it will be left off.
1399 *
1400 * @param string $keep   the part to keep
1401 * @param string $short  the part to shorten
1402 * @param int    $max    maximum chars you want for the whole string
1403 * @param int    $min    minimum number of chars to have left for middle shortening
1404 * @param string $char   the shortening character to use
1405 * @return string
1406 */
1407function shorten($keep, $short, $max, $min = 9, $char = '…') {
1408    $max = $max - utf8_strlen($keep);
1409    if($max < $min) return $keep;
1410    $len = utf8_strlen($short);
1411    if($len <= $max) return $keep.$short;
1412    $half = floor($max / 2);
1413    return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half);
1414}
1415
1416/**
1417 * Return the users realname or e-mail address for use
1418 * in page footer and recent changes pages
1419 *
1420 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1421 */
1422function editorinfo($username) {
1423    global $conf;
1424    global $auth;
1425
1426    switch($conf['showuseras']) {
1427        case 'username':
1428        case 'email':
1429        case 'email_link':
1430            if($auth) $info = $auth->getUserData($username);
1431            break;
1432        default:
1433            return hsc($username);
1434    }
1435
1436    if(isset($info) && $info) {
1437        switch($conf['showuseras']) {
1438            case 'username':
1439                return hsc($info['name']);
1440            case 'email':
1441                return obfuscate($info['mail']);
1442            case 'email_link':
1443                $mail = obfuscate($info['mail']);
1444                return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1445            default:
1446                return hsc($username);
1447        }
1448    } else {
1449        return hsc($username);
1450    }
1451}
1452
1453/**
1454 * Returns the path to a image file for the currently chosen license.
1455 * When no image exists, returns an empty string
1456 *
1457 * @author Andreas Gohr <andi@splitbrain.org>
1458 * @param  string $type - type of image 'badge' or 'button'
1459 * @return string
1460 */
1461function license_img($type) {
1462    global $license;
1463    global $conf;
1464    if(!$conf['license']) return '';
1465    if(!is_array($license[$conf['license']])) return '';
1466    $lic   = $license[$conf['license']];
1467    $try   = array();
1468    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1469    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1470    if(substr($conf['license'], 0, 3) == 'cc-') {
1471        $try[] = 'lib/images/license/'.$type.'/cc.png';
1472    }
1473    foreach($try as $src) {
1474        if(@file_exists(DOKU_INC.$src)) return $src;
1475    }
1476    return '';
1477}
1478
1479/**
1480 * Checks if the given amount of memory is available
1481 *
1482 * If the memory_get_usage() function is not available the
1483 * function just assumes $bytes of already allocated memory
1484 *
1485 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1486 * @author Andreas Gohr <andi@splitbrain.org>
1487 *
1488 * @param  int $mem  Size of memory you want to allocate in bytes
1489 * @param int  $bytes
1490 * @internal param int $used already allocated memory (see above)
1491 * @return bool
1492 */
1493function is_mem_available($mem, $bytes = 1048576) {
1494    $limit = trim(ini_get('memory_limit'));
1495    if(empty($limit)) return true; // no limit set!
1496
1497    // parse limit to bytes
1498    $limit = php_to_byte($limit);
1499
1500    // get used memory if possible
1501    if(function_exists('memory_get_usage')) {
1502        $used = memory_get_usage();
1503    } else {
1504        $used = $bytes;
1505    }
1506
1507    if($used + $mem > $limit) {
1508        return false;
1509    }
1510
1511    return true;
1512}
1513
1514/**
1515 * Send a HTTP redirect to the browser
1516 *
1517 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1518 *
1519 * @link   http://support.microsoft.com/kb/q176113/
1520 * @author Andreas Gohr <andi@splitbrain.org>
1521 */
1522function send_redirect($url) {
1523    //are there any undisplayed messages? keep them in session for display
1524    global $MSG;
1525    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1526        //reopen session, store data and close session again
1527        @session_start();
1528        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1529    }
1530
1531    // always close the session
1532    session_write_close();
1533
1534    // work around IE bug
1535    // http://www.ianhoar.com/2008/11/16/internet-explorer-6-and-redirected-anchor-links/
1536    list($url, $hash) = explode('#', $url);
1537    if($hash) {
1538        if(strpos($url, '?')) {
1539            $url = $url.'&#'.$hash;
1540        } else {
1541            $url = $url.'?&#'.$hash;
1542        }
1543    }
1544
1545    // check if running on IIS < 6 with CGI-PHP
1546    if(isset($_SERVER['SERVER_SOFTWARE']) && isset($_SERVER['GATEWAY_INTERFACE']) &&
1547        (strpos($_SERVER['GATEWAY_INTERFACE'], 'CGI') !== false) &&
1548        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($_SERVER['SERVER_SOFTWARE']), $matches)) &&
1549        $matches[1] < 6
1550    ) {
1551        header('Refresh: 0;url='.$url);
1552    } else {
1553        header('Location: '.$url);
1554    }
1555    exit;
1556}
1557
1558/**
1559 * Validate a value using a set of valid values
1560 *
1561 * This function checks whether a specified value is set and in the array
1562 * $valid_values. If not, the function returns a default value or, if no
1563 * default is specified, throws an exception.
1564 *
1565 * @param string $param        The name of the parameter
1566 * @param array  $valid_values A set of valid values; Optionally a default may
1567 *                             be marked by the key “default”.
1568 * @param array  $array        The array containing the value (typically $_POST
1569 *                             or $_GET)
1570 * @param string $exc          The text of the raised exception
1571 *
1572 * @throws Exception
1573 * @return mixed
1574 * @author Adrian Lang <lang@cosmocode.de>
1575 */
1576function valid_input_set($param, $valid_values, $array, $exc = '') {
1577    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1578        return $array[$param];
1579    } elseif(isset($valid_values['default'])) {
1580        return $valid_values['default'];
1581    } else {
1582        throw new Exception($exc);
1583    }
1584}
1585
1586/**
1587 * Read a preference from the DokuWiki cookie
1588 * (remembering both keys & values are urlencoded)
1589 */
1590function get_doku_pref($pref, $default) {
1591    $enc_pref = urlencode($pref);
1592    if(strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
1593        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1594        $cnt   = count($parts);
1595        for($i = 0; $i < $cnt; $i += 2) {
1596            if($parts[$i] == $enc_pref) {
1597                return urldecode($parts[$i + 1]);
1598            }
1599        }
1600    }
1601    return $default;
1602}
1603
1604/**
1605 * Add a preference to the DokuWiki cookie
1606 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
1607 */
1608function set_doku_pref($pref, $val) {
1609    global $conf;
1610    $orig = get_doku_pref($pref, false);
1611    $cookieVal = '';
1612
1613    if($orig && ($orig != $val)) {
1614        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1615        $cnt   = count($parts);
1616        // urlencode $pref for the comparison
1617        $enc_pref = rawurlencode($pref);
1618        for($i = 0; $i < $cnt; $i += 2) {
1619            if($parts[$i] == $enc_pref) {
1620                $parts[$i + 1] = rawurlencode($val);
1621                break;
1622            }
1623        }
1624        $cookieVal = implode('#', $parts);
1625    } else if (!$orig) {
1626        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val);
1627    }
1628
1629    if (!empty($cookieVal)) {
1630        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1631        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
1632    }
1633}
1634
1635//Setup VIM: ex: et ts=2 :
1636