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