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