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