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