xref: /dokuwiki/inc/common.php (revision 7800517deb6ab16f8211f73adc961a5bcb1ea328)
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    $white = false;
246    foreach($params as $key => $val){
247        if($key{0} == '_') continue;
248        if($val === '' && $skipempty) continue;
249        if($white) $url .= ' ';
250
251        $url .= $key.'="';
252        $url .= htmlspecialchars ($val);
253        $url .= '"';
254        $white = true;
255    }
256    return $url;
257}
258
259
260/**
261 * This builds the breadcrumb trail and returns it as array
262 *
263 * @author Andreas Gohr <andi@splitbrain.org>
264 */
265function breadcrumbs(){
266    // we prepare the breadcrumbs early for quick session closing
267    static $crumbs = null;
268    if($crumbs != null) return $crumbs;
269
270    global $ID;
271    global $ACT;
272    global $conf;
273
274    //first visit?
275    $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
276    //we only save on show and existing wiki documents
277    $file = wikiFN($ID);
278    if($ACT != 'show' || !@file_exists($file)){
279        $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
280        return $crumbs;
281    }
282
283    // page names
284    $name = noNSorNS($ID);
285    if (useHeading('navigation')) {
286        // get page title
287        $title = p_get_first_heading($ID,METADATA_RENDER_USING_SIMPLE_CACHE);
288        if ($title) {
289            $name = $title;
290        }
291    }
292
293    //remove ID from array
294    if (isset($crumbs[$ID])) {
295        unset($crumbs[$ID]);
296    }
297
298    //add to array
299    $crumbs[$ID] = $name;
300    //reduce size
301    while(count($crumbs) > $conf['breadcrumbs']){
302        array_shift($crumbs);
303    }
304    //save to session
305    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
306    return $crumbs;
307}
308
309/**
310 * Filter for page IDs
311 *
312 * This is run on a ID before it is outputted somewhere
313 * currently used to replace the colon with something else
314 * on Windows 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('/^(::1|[fF][eE]80:|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_PAGETPL_LOAD
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    // prepare initial event data
816    $data = array(
817        'id'        => $id,   // the id of the page to be created
818        'tpl'       => '',    // the text used as template
819        'tplfile'   => '',    // the file above text was/should be loaded from
820        'doreplace' => true   // should wildcard replacements be done on the text?
821    );
822
823    $evt = new Doku_Event('COMMON_PAGETPL_LOAD',$data);
824    if($evt->advise_before(true)){
825        // the before event might have loaded the content already
826        if(empty($data['tpl'])){
827            // if the before event did not set a template file, try to find one
828            if(empty($data['tplfile'])){
829                $path = dirname(wikiFN($id));
830                $tpl = '';
831                if(@file_exists($path.'/_template.txt')){
832                    $data['tplfile'] = $path.'/_template.txt';
833                }else{
834                    // search upper namespaces for templates
835                    $len = strlen(rtrim($conf['datadir'],'/'));
836                    while (strlen($path) >= $len){
837                        if(@file_exists($path.'/__template.txt')){
838                            $data['tplfile'] = $path.'/__template.txt';
839                            break;
840                        }
841                        $path = substr($path, 0, strrpos($path, '/'));
842                    }
843                }
844            }
845            // load the content
846            $data['tpl'] = io_readFile($data['tplfile']);
847        }
848        if($data['doreplace']) parsePageTemplate($data);
849    }
850    $evt->advise_after();
851    unset($evt);
852
853    return $data['tpl'];
854}
855
856/**
857 * Performs common page template replacements
858 * This works on data from COMMON_PAGETPL_LOAD
859 *
860 * @author Andreas Gohr <andi@splitbrain.org>
861 */
862function parsePageTemplate(&$data) {
863    extract($data);
864
865    global $USERINFO;
866    global $conf;
867
868    // replace placeholders
869    $file = noNS($id);
870    $page = strtr($file, $conf['sepchar'], ' ');
871
872    $tpl = str_replace(array(
873                '@ID@',
874                '@NS@',
875                '@FILE@',
876                '@!FILE@',
877                '@!FILE!@',
878                '@PAGE@',
879                '@!PAGE@',
880                '@!!PAGE@',
881                '@!PAGE!@',
882                '@USER@',
883                '@NAME@',
884                '@MAIL@',
885                '@DATE@',
886                ),
887            array(
888                $id,
889                getNS($id),
890                $file,
891                utf8_ucfirst($file),
892                utf8_strtoupper($file),
893                $page,
894                utf8_ucfirst($page),
895                utf8_ucwords($page),
896                utf8_strtoupper($page),
897                $_SERVER['REMOTE_USER'],
898                $USERINFO['name'],
899                $USERINFO['mail'],
900                $conf['dformat'],
901                ), $tpl);
902
903    // we need the callback to work around strftime's char limit
904    $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
905    $data['tpl'] = $tpl;
906    return $tpl;
907}
908
909/**
910 * Returns the raw Wiki Text in three slices.
911 *
912 * The range parameter needs to have the form "from-to"
913 * and gives the range of the section in bytes - no
914 * UTF-8 awareness is needed.
915 * The returned order is prefix, section and suffix.
916 *
917 * @author Andreas Gohr <andi@splitbrain.org>
918 */
919function rawWikiSlices($range,$id,$rev=''){
920    $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
921
922    // Parse range
923    list($from,$to) = explode('-',$range,2);
924    // Make range zero-based, use defaults if marker is missing
925    $from = !$from ? 0 : ($from - 1);
926    $to   = !$to ? strlen($text) : ($to - 1);
927
928    $slices[0] = substr($text, 0, $from);
929    $slices[1] = substr($text, $from, $to-$from);
930    $slices[2] = substr($text, $to);
931    return $slices;
932}
933
934/**
935 * Joins wiki text slices
936 *
937 * function to join the text slices.
938 * When the pretty parameter is set to true it adds additional empty
939 * lines between sections if needed (used on saving).
940 *
941 * @author Andreas Gohr <andi@splitbrain.org>
942 */
943function con($pre,$text,$suf,$pretty=false){
944    if($pretty){
945        if ($pre !== '' && substr($pre, -1) !== "\n" &&
946            substr($text, 0, 1) !== "\n") {
947            $pre .= "\n";
948        }
949        if ($suf !== '' && substr($text, -1) !== "\n" &&
950            substr($suf, 0, 1) !== "\n") {
951            $text .= "\n";
952        }
953    }
954
955    return $pre.$text.$suf;
956}
957
958/**
959 * Saves a wikitext by calling io_writeWikiPage.
960 * Also directs changelog and attic updates.
961 *
962 * @author Andreas Gohr <andi@splitbrain.org>
963 * @author Ben Coburn <btcoburn@silicodon.net>
964 */
965function saveWikiText($id,$text,$summary,$minor=false){
966    /* Note to developers:
967       This code is subtle and delicate. Test the behavior of
968       the attic and changelog with dokuwiki and external edits
969       after any changes. External edits change the wiki page
970       directly without using php or dokuwiki.
971     */
972    global $conf;
973    global $lang;
974    global $REV;
975    // ignore if no changes were made
976    if($text == rawWiki($id,'')){
977        return;
978    }
979
980    $file = wikiFN($id);
981    $old = @filemtime($file); // from page
982    $wasRemoved = empty($text);
983    $wasCreated = !@file_exists($file);
984    $wasReverted = ($REV==true);
985    $newRev = false;
986    $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
987    $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
988    if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
989        // add old revision to the attic if missing
990        saveOldRevision($id);
991        // add a changelog entry if this edit came from outside dokuwiki
992        if ($old>$oldRev) {
993            addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
994            // remove soon to be stale instructions
995            $cache = new cache_instructions($id, $file);
996            $cache->removeCache();
997        }
998    }
999
1000    if ($wasRemoved){
1001        // Send "update" event with empty data, so plugins can react to page deletion
1002        $data = array(array($file, '', false), getNS($id), noNS($id), false);
1003        trigger_event('IO_WIKIPAGE_WRITE', $data);
1004        // pre-save deleted revision
1005        @touch($file);
1006        clearstatcache();
1007        $newRev = saveOldRevision($id);
1008        // remove empty file
1009        @unlink($file);
1010        // remove old meta info...
1011        $mfiles = metaFiles($id);
1012        $changelog = metaFN($id, '.changes');
1013        $metadata  = metaFN($id, '.meta');
1014        $subscribers = metaFN($id, '.mlist');
1015        foreach ($mfiles as $mfile) {
1016            // but keep per-page changelog to preserve page history, keep subscriber list and keep meta data
1017            if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata && $mfile!==$subscribers) { @unlink($mfile); }
1018        }
1019        // purge 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
1097    // decide if there is something to do
1098    if($who == 'admin'){
1099        if(empty($conf['notify'])) return; //notify enabled?
1100        $text = rawLocale('mailtext');
1101        $to   = $conf['notify'];
1102        $bcc  = '';
1103    }elseif($who == 'subscribers'){
1104        if(!$conf['subscribers']) return; //subscribers enabled?
1105        if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
1106        $data = array('id' => $id, 'addresslist' => '', 'self' => false);
1107        trigger_event('COMMON_NOTIFY_ADDRESSLIST', $data,
1108                      'subscription_addresslist');
1109        $bcc = $data['addresslist'];
1110        if(empty($bcc)) return;
1111        $to   = '';
1112        $text = rawLocale('subscr_single');
1113    }elseif($who == 'register'){
1114        if(empty($conf['registernotify'])) return;
1115        $text = rawLocale('registermail');
1116        $to   = $conf['registernotify'];
1117        $bcc  = '';
1118    }else{
1119        return; //just to be safe
1120    }
1121
1122    $ip   = clientIP();
1123    $text = str_replace('@DATE@',dformat(),$text);
1124    $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
1125    $text = str_replace('@IPADDRESS@',$ip,$text);
1126    $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
1127    $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
1128    $text = str_replace('@PAGE@',$id,$text);
1129    $text = str_replace('@TITLE@',$conf['title'],$text);
1130    $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
1131    $text = str_replace('@SUMMARY@',$summary,$text);
1132    $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
1133    $text = str_replace('@NAME@',$INFO['userinfo']['name'],$text);
1134    $text = str_replace('@MAIL@',$INFO['userinfo']['mail'],$text);
1135
1136    foreach ($replace as $key => $substitution) {
1137        $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
1138    }
1139
1140    if($who == 'register'){
1141        $subject = $lang['mail_new_user'].' '.$summary;
1142    }elseif($rev){
1143        $subject = $lang['mail_changed'].' '.$id;
1144        $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
1145        $df  = new Diff(explode("\n",rawWiki($id,$rev)),
1146                        explode("\n",rawWiki($id)));
1147        $dformat = new UnifiedDiffFormatter();
1148        $diff    = $dformat->format($df);
1149    }else{
1150        $subject=$lang['mail_newpage'].' '.$id;
1151        $text = str_replace('@OLDPAGE@','none',$text);
1152        $diff = rawWiki($id);
1153    }
1154    $text = str_replace('@DIFF@',$diff,$text);
1155    if(empty($conf['mailprefix'])) {
1156        if(utf8_strlen($conf['title']) < 20) {
1157            $subject = '['.$conf['title'].'] '.$subject;
1158        }else{
1159            $subject = '['.utf8_substr($conf['title'], 0, 20).'...] '.$subject;
1160        }
1161    }else{
1162        $subject = '['.$conf['mailprefix'].'] '.$subject;
1163    }
1164    mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc);
1165}
1166
1167/**
1168 * extracts the query from a search engine referrer
1169 *
1170 * @author Andreas Gohr <andi@splitbrain.org>
1171 * @author Todd Augsburger <todd@rollerorgans.com>
1172 */
1173function getGoogleQuery(){
1174    if (!isset($_SERVER['HTTP_REFERER'])) {
1175        return '';
1176    }
1177    $url = parse_url($_SERVER['HTTP_REFERER']);
1178
1179    $query = array();
1180
1181    // temporary workaround against PHP bug #49733
1182    // see http://bugs.php.net/bug.php?id=49733
1183    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1184    parse_str($url['query'],$query);
1185    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1186
1187    $q = '';
1188    if(isset($query['q']))
1189        $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1190    elseif(isset($query['p']))
1191        $q = $query['p'];        // yahoo
1192    elseif(isset($query['query']))
1193        $q = $query['query'];    // lycos, netscape, clusty, hotbot
1194    elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1195        $q = urldecode(ltrim($url['path'],'/'));
1196
1197    if($q === '') return '';
1198    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1199    return $q;
1200}
1201
1202/**
1203 * Try to set correct locale
1204 *
1205 * @deprecated No longer used
1206 * @author     Andreas Gohr <andi@splitbrain.org>
1207 */
1208function setCorrectLocale(){
1209    global $conf;
1210    global $lang;
1211
1212    $enc = strtoupper($lang['encoding']);
1213    foreach ($lang['locales'] as $loc){
1214        //try locale
1215        if(@setlocale(LC_ALL,$loc)) return;
1216        //try loceale with encoding
1217        if(@setlocale(LC_ALL,"$loc.$enc")) return;
1218    }
1219    //still here? try to set from environment
1220    @setlocale(LC_ALL,"");
1221}
1222
1223/**
1224 * Return the human readable size of a file
1225 *
1226 * @param       int    $size   A file size
1227 * @param       int    $dec    A number of decimal places
1228 * @author      Martin Benjamin <b.martin@cybernet.ch>
1229 * @author      Aidan Lister <aidan@php.net>
1230 * @version     1.0.0
1231 */
1232function filesize_h($size, $dec = 1){
1233    $sizes = array('B', 'KB', 'MB', 'GB');
1234    $count = count($sizes);
1235    $i = 0;
1236
1237    while ($size >= 1024 && ($i < $count - 1)) {
1238        $size /= 1024;
1239        $i++;
1240    }
1241
1242    return round($size, $dec) . ' ' . $sizes[$i];
1243}
1244
1245/**
1246 * Return the given timestamp as human readable, fuzzy age
1247 *
1248 * @author Andreas Gohr <gohr@cosmocode.de>
1249 */
1250function datetime_h($dt){
1251    global $lang;
1252
1253    $ago = time() - $dt;
1254    if($ago > 24*60*60*30*12*2){
1255        return sprintf($lang['years'], round($ago/(24*60*60*30*12)));
1256    }
1257    if($ago > 24*60*60*30*2){
1258        return sprintf($lang['months'], round($ago/(24*60*60*30)));
1259    }
1260    if($ago > 24*60*60*7*2){
1261        return sprintf($lang['weeks'], round($ago/(24*60*60*7)));
1262    }
1263    if($ago > 24*60*60*2){
1264        return sprintf($lang['days'], round($ago/(24*60*60)));
1265    }
1266    if($ago > 60*60*2){
1267        return sprintf($lang['hours'], round($ago/(60*60)));
1268    }
1269    if($ago > 60*2){
1270        return sprintf($lang['minutes'], round($ago/(60)));
1271    }
1272    return sprintf($lang['seconds'], $ago);
1273}
1274
1275/**
1276 * Wraps around strftime but provides support for fuzzy dates
1277 *
1278 * The format default to $conf['dformat']. It is passed to
1279 * strftime - %f can be used to get the value from datetime_h()
1280 *
1281 * @see datetime_h
1282 * @author Andreas Gohr <gohr@cosmocode.de>
1283 */
1284function dformat($dt=null,$format=''){
1285    global $conf;
1286
1287    if(is_null($dt)) $dt = time();
1288    $dt = (int) $dt;
1289    if(!$format) $format = $conf['dformat'];
1290
1291    $format = str_replace('%f',datetime_h($dt),$format);
1292    return strftime($format,$dt);
1293}
1294
1295/**
1296 * Formats a timestamp as ISO 8601 date
1297 *
1298 * @author <ungu at terong dot com>
1299 * @link http://www.php.net/manual/en/function.date.php#54072
1300 */
1301function date_iso8601($int_date) {
1302   //$int_date: current date in UNIX timestamp
1303   $date_mod = date('Y-m-d\TH:i:s', $int_date);
1304   $pre_timezone = date('O', $int_date);
1305   $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1306   $date_mod .= $time_zone;
1307   return $date_mod;
1308}
1309
1310/**
1311 * return an obfuscated email address in line with $conf['mailguard'] setting
1312 *
1313 * @author Harry Fuecks <hfuecks@gmail.com>
1314 * @author Christopher Smith <chris@jalakai.co.uk>
1315 */
1316function obfuscate($email) {
1317    global $conf;
1318
1319    switch ($conf['mailguard']) {
1320        case 'visible' :
1321            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1322            return strtr($email, $obfuscate);
1323
1324        case 'hex' :
1325            $encode = '';
1326            $len = strlen($email);
1327            for ($x=0; $x < $len; $x++){
1328                $encode .= '&#x' . bin2hex($email{$x}).';';
1329            }
1330            return $encode;
1331
1332        case 'none' :
1333        default :
1334            return $email;
1335    }
1336}
1337
1338/**
1339 * Removes quoting backslashes
1340 *
1341 * @author Andreas Gohr <andi@splitbrain.org>
1342 */
1343function unslash($string,$char="'"){
1344    return str_replace('\\'.$char,$char,$string);
1345}
1346
1347/**
1348 * Convert php.ini shorthands to byte
1349 *
1350 * @author <gilthans dot NO dot SPAM at gmail dot com>
1351 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1352 */
1353function php_to_byte($v){
1354    $l = substr($v, -1);
1355    $ret = substr($v, 0, -1);
1356    switch(strtoupper($l)){
1357        case 'P':
1358            $ret *= 1024;
1359        case 'T':
1360            $ret *= 1024;
1361        case 'G':
1362            $ret *= 1024;
1363        case 'M':
1364            $ret *= 1024;
1365        case 'K':
1366            $ret *= 1024;
1367        break;
1368        default;
1369            $ret *= 10;
1370        break;
1371    }
1372    return $ret;
1373}
1374
1375/**
1376 * Wrapper around preg_quote adding the default delimiter
1377 */
1378function preg_quote_cb($string){
1379    return preg_quote($string,'/');
1380}
1381
1382/**
1383 * Shorten a given string by removing data from the middle
1384 *
1385 * You can give the string in two parts, the first part $keep
1386 * will never be shortened. The second part $short will be cut
1387 * in the middle to shorten but only if at least $min chars are
1388 * left to display it. Otherwise it will be left off.
1389 *
1390 * @param string $keep   the part to keep
1391 * @param string $short  the part to shorten
1392 * @param int    $max    maximum chars you want for the whole string
1393 * @param int    $min    minimum number of chars to have left for middle shortening
1394 * @param string $char   the shortening character to use
1395 */
1396function shorten($keep,$short,$max,$min=9,$char='…'){
1397    $max = $max - utf8_strlen($keep);
1398    if($max < $min) return $keep;
1399    $len = utf8_strlen($short);
1400    if($len <= $max) return $keep.$short;
1401    $half = floor($max/2);
1402    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1403}
1404
1405/**
1406 * Return the users realname or e-mail address for use
1407 * in page footer and recent changes pages
1408 *
1409 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1410 */
1411function editorinfo($username){
1412    global $conf;
1413    global $auth;
1414
1415    switch($conf['showuseras']){
1416        case 'username':
1417        case 'email':
1418        case 'email_link':
1419            if($auth) $info = $auth->getUserData($username);
1420            break;
1421        default:
1422            return hsc($username);
1423    }
1424
1425    if(isset($info) && $info) {
1426        switch($conf['showuseras']){
1427            case 'username':
1428                return hsc($info['name']);
1429            case 'email':
1430                return obfuscate($info['mail']);
1431            case 'email_link':
1432                $mail=obfuscate($info['mail']);
1433                return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1434            default:
1435                return hsc($username);
1436        }
1437    } else {
1438        return hsc($username);
1439    }
1440}
1441
1442/**
1443 * Returns the path to a image file for the currently chosen license.
1444 * When no image exists, returns an empty string
1445 *
1446 * @author Andreas Gohr <andi@splitbrain.org>
1447 * @param  string $type - type of image 'badge' or 'button'
1448 */
1449function license_img($type){
1450    global $license;
1451    global $conf;
1452    if(!$conf['license']) return '';
1453    if(!is_array($license[$conf['license']])) return '';
1454    $lic = $license[$conf['license']];
1455    $try = array();
1456    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1457    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1458    if(substr($conf['license'],0,3) == 'cc-'){
1459        $try[] = 'lib/images/license/'.$type.'/cc.png';
1460    }
1461    foreach($try as $src){
1462        if(@file_exists(DOKU_INC.$src)) return $src;
1463    }
1464    return '';
1465}
1466
1467/**
1468 * Checks if the given amount of memory is available
1469 *
1470 * If the memory_get_usage() function is not available the
1471 * function just assumes $bytes of already allocated memory
1472 *
1473 * @param  int $mem  Size of memory you want to allocate in bytes
1474 * @param  int $used already allocated memory (see above)
1475 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1476 * @author Andreas Gohr <andi@splitbrain.org>
1477 */
1478function is_mem_available($mem,$bytes=1048576){
1479    $limit = trim(ini_get('memory_limit'));
1480    if(empty($limit)) return true; // no limit set!
1481
1482    // parse limit to bytes
1483    $limit = php_to_byte($limit);
1484
1485    // get used memory if possible
1486    if(function_exists('memory_get_usage')){
1487        $used = memory_get_usage();
1488    }else{
1489        $used = $bytes;
1490    }
1491
1492    if($used+$mem > $limit){
1493        return false;
1494    }
1495
1496    return true;
1497}
1498
1499/**
1500 * Send a HTTP redirect to the browser
1501 *
1502 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1503 *
1504 * @link   http://support.microsoft.com/kb/q176113/
1505 * @author Andreas Gohr <andi@splitbrain.org>
1506 */
1507function send_redirect($url){
1508    //are there any undisplayed messages? keep them in session for display
1509    global $MSG;
1510    if (isset($MSG) && count($MSG) && !defined('NOSESSION')){
1511        //reopen session, store data and close session again
1512        @session_start();
1513        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1514    }
1515
1516    // always close the session
1517    session_write_close();
1518
1519    // work around IE bug
1520    // http://www.ianhoar.com/2008/11/16/internet-explorer-6-and-redirected-anchor-links/
1521    list($url,$hash) = explode('#',$url);
1522    if($hash){
1523        if(strpos($url,'?')){
1524            $url = $url.'&#'.$hash;
1525        }else{
1526            $url = $url.'?&#'.$hash;
1527        }
1528    }
1529
1530    // check if running on IIS < 6 with CGI-PHP
1531    if( isset($_SERVER['SERVER_SOFTWARE']) && isset($_SERVER['GATEWAY_INTERFACE']) &&
1532        (strpos($_SERVER['GATEWAY_INTERFACE'],'CGI') !== false) &&
1533        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($_SERVER['SERVER_SOFTWARE']), $matches)) &&
1534        $matches[1] < 6 ){
1535        header('Refresh: 0;url='.$url);
1536    }else{
1537        header('Location: '.$url);
1538    }
1539    exit;
1540}
1541
1542/**
1543 * Validate a value using a set of valid values
1544 *
1545 * This function checks whether a specified value is set and in the array
1546 * $valid_values. If not, the function returns a default value or, if no
1547 * default is specified, throws an exception.
1548 *
1549 * @param string $param        The name of the parameter
1550 * @param array  $valid_values A set of valid values; Optionally a default may
1551 *                             be marked by the key “default”.
1552 * @param array  $array        The array containing the value (typically $_POST
1553 *                             or $_GET)
1554 * @param string $exc          The text of the raised exception
1555 *
1556 * @author Adrian Lang <lang@cosmocode.de>
1557 */
1558function valid_input_set($param, $valid_values, $array, $exc = '') {
1559    if (isset($array[$param]) && in_array($array[$param], $valid_values)) {
1560        return $array[$param];
1561    } elseif (isset($valid_values['default'])) {
1562        return $valid_values['default'];
1563    } else {
1564        throw new Exception($exc);
1565    }
1566}
1567
1568//Setup VIM: ex: et ts=2 :
1569