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