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