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