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