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