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