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