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