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