xref: /dokuwiki/inc/common.php (revision e8b5a4f91c8a6e230a6cfe13c43dc9ddce31e253)
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 * 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 */
504function 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 */
532function 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 */
597function 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 */
661function 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 */
685function 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 */
709function 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 */
736function 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 */
758function 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 */
776function 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 */
789function 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 */
799function 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 */
808function 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 */
818function 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 */
869function 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 */
934function 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 */
958function 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 */
982function 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 */
1082function 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 */
1105function 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        $df              = new Diff(explode("\n", rawWiki($id, $rev)),
1154                                    explode("\n", rawWiki($id)));
1155        $dformat         = new UnifiedDiffFormatter();
1156        $tdiff           = $dformat->format($df);
1157
1158        $DIFF_INLINESTYLES = true;
1159        $dformat           = new InlineDiffFormatter();
1160        $hdiff             = $dformat->format($df);
1161        $hdiff             = '<table>'.$hdiff.'</table>';
1162        $DIFF_INLINESTYLES = false;
1163    } else {
1164        $subject         = $lang['mail_newpage'].' '.$id;
1165        $trep['OLDPAGE'] = '---';
1166        $tdiff           = rawWiki($id);
1167        $hdiff           = nl2br(hsc($tdiff));
1168    }
1169    $trep['DIFF'] = $tdiff;
1170    $hrep['DIFF'] = $hdiff;
1171
1172    // send mail
1173    $mail = new Mailer();
1174    $mail->to($to);
1175    $mail->bcc($bcc);
1176    $mail->subject($subject);
1177    $mail->setBody($text, $trep, $hrep);
1178    if($who == 'subscribers') {
1179        $mail->setHeader(
1180            'List-Unsubscribe',
1181            '<'.wl($id, array('do'=> 'subscribe'), true, '&').'>',
1182            false
1183        );
1184    }
1185    return $mail->send();
1186}
1187
1188/**
1189 * extracts the query from a search engine referrer
1190 *
1191 * @author Andreas Gohr <andi@splitbrain.org>
1192 * @author Todd Augsburger <todd@rollerorgans.com>
1193 */
1194function getGoogleQuery() {
1195    if(!isset($_SERVER['HTTP_REFERER'])) {
1196        return '';
1197    }
1198    $url = parse_url($_SERVER['HTTP_REFERER']);
1199
1200    $query = array();
1201
1202    // temporary workaround against PHP bug #49733
1203    // see http://bugs.php.net/bug.php?id=49733
1204    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1205    parse_str($url['query'], $query);
1206    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1207
1208    $q = '';
1209    if(isset($query['q']))
1210        $q = $query['q']; // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1211    elseif(isset($query['p']))
1212        $q = $query['p']; // yahoo
1213    elseif(isset($query['query']))
1214        $q = $query['query']; // lycos, netscape, clusty, hotbot
1215    elseif(preg_match("#a9\.com#i", $url['host'])) // a9
1216        $q = urldecode(ltrim($url['path'], '/'));
1217
1218    if($q === '') return '';
1219    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1220    return $q;
1221}
1222
1223/**
1224 * Try to set correct locale
1225 *
1226 * @deprecated No longer used
1227 * @author     Andreas Gohr <andi@splitbrain.org>
1228 */
1229function setCorrectLocale() {
1230    global $conf;
1231    global $lang;
1232
1233    $enc = strtoupper($lang['encoding']);
1234    foreach($lang['locales'] as $loc) {
1235        //try locale
1236        if(@setlocale(LC_ALL, $loc)) return;
1237        //try loceale with encoding
1238        if(@setlocale(LC_ALL, "$loc.$enc")) return;
1239    }
1240    //still here? try to set from environment
1241    @setlocale(LC_ALL, "");
1242}
1243
1244/**
1245 * Return the human readable size of a file
1246 *
1247 * @param       int    $size   A file size
1248 * @param       int    $dec    A number of decimal places
1249 * @author      Martin Benjamin <b.martin@cybernet.ch>
1250 * @author      Aidan Lister <aidan@php.net>
1251 * @version     1.0.0
1252 */
1253function filesize_h($size, $dec = 1) {
1254    $sizes = array('B', 'KB', 'MB', 'GB');
1255    $count = count($sizes);
1256    $i     = 0;
1257
1258    while($size >= 1024 && ($i < $count - 1)) {
1259        $size /= 1024;
1260        $i++;
1261    }
1262
1263    return round($size, $dec).' '.$sizes[$i];
1264}
1265
1266/**
1267 * Return the given timestamp as human readable, fuzzy age
1268 *
1269 * @author Andreas Gohr <gohr@cosmocode.de>
1270 */
1271function datetime_h($dt) {
1272    global $lang;
1273
1274    $ago = time() - $dt;
1275    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1276        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1277    }
1278    if($ago > 24 * 60 * 60 * 30 * 2) {
1279        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1280    }
1281    if($ago > 24 * 60 * 60 * 7 * 2) {
1282        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1283    }
1284    if($ago > 24 * 60 * 60 * 2) {
1285        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1286    }
1287    if($ago > 60 * 60 * 2) {
1288        return sprintf($lang['hours'], round($ago / (60 * 60)));
1289    }
1290    if($ago > 60 * 2) {
1291        return sprintf($lang['minutes'], round($ago / (60)));
1292    }
1293    return sprintf($lang['seconds'], $ago);
1294}
1295
1296/**
1297 * Wraps around strftime but provides support for fuzzy dates
1298 *
1299 * The format default to $conf['dformat']. It is passed to
1300 * strftime - %f can be used to get the value from datetime_h()
1301 *
1302 * @see datetime_h
1303 * @author Andreas Gohr <gohr@cosmocode.de>
1304 */
1305function dformat($dt = null, $format = '') {
1306    global $conf;
1307
1308    if(is_null($dt)) $dt = time();
1309    $dt = (int) $dt;
1310    if(!$format) $format = $conf['dformat'];
1311
1312    $format = str_replace('%f', datetime_h($dt), $format);
1313    return strftime($format, $dt);
1314}
1315
1316/**
1317 * Formats a timestamp as ISO 8601 date
1318 *
1319 * @author <ungu at terong dot com>
1320 * @link http://www.php.net/manual/en/function.date.php#54072
1321 * @param int $int_date: current date in UNIX timestamp
1322 * @return string
1323 */
1324function date_iso8601($int_date) {
1325    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1326    $pre_timezone = date('O', $int_date);
1327    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1328    $date_mod .= $time_zone;
1329    return $date_mod;
1330}
1331
1332/**
1333 * return an obfuscated email address in line with $conf['mailguard'] setting
1334 *
1335 * @author Harry Fuecks <hfuecks@gmail.com>
1336 * @author Christopher Smith <chris@jalakai.co.uk>
1337 */
1338function obfuscate($email) {
1339    global $conf;
1340
1341    switch($conf['mailguard']) {
1342        case 'visible' :
1343            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1344            return strtr($email, $obfuscate);
1345
1346        case 'hex' :
1347            $encode = '';
1348            $len    = strlen($email);
1349            for($x = 0; $x < $len; $x++) {
1350                $encode .= '&#x'.bin2hex($email{$x}).';';
1351            }
1352            return $encode;
1353
1354        case 'none' :
1355        default :
1356            return $email;
1357    }
1358}
1359
1360/**
1361 * Removes quoting backslashes
1362 *
1363 * @author Andreas Gohr <andi@splitbrain.org>
1364 */
1365function unslash($string, $char = "'") {
1366    return str_replace('\\'.$char, $char, $string);
1367}
1368
1369/**
1370 * Convert php.ini shorthands to byte
1371 *
1372 * @author <gilthans dot NO dot SPAM at gmail dot com>
1373 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1374 */
1375function php_to_byte($v) {
1376    $l   = substr($v, -1);
1377    $ret = substr($v, 0, -1);
1378    switch(strtoupper($l)) {
1379        case 'P':
1380            $ret *= 1024;
1381        case 'T':
1382            $ret *= 1024;
1383        case 'G':
1384            $ret *= 1024;
1385        case 'M':
1386            $ret *= 1024;
1387        case 'K':
1388            $ret *= 1024;
1389            break;
1390        default;
1391            $ret *= 10;
1392            break;
1393    }
1394    return $ret;
1395}
1396
1397/**
1398 * Wrapper around preg_quote adding the default delimiter
1399 */
1400function preg_quote_cb($string) {
1401    return preg_quote($string, '/');
1402}
1403
1404/**
1405 * Shorten a given string by removing data from the middle
1406 *
1407 * You can give the string in two parts, the first part $keep
1408 * will never be shortened. The second part $short will be cut
1409 * in the middle to shorten but only if at least $min chars are
1410 * left to display it. Otherwise it will be left off.
1411 *
1412 * @param string $keep   the part to keep
1413 * @param string $short  the part to shorten
1414 * @param int    $max    maximum chars you want for the whole string
1415 * @param int    $min    minimum number of chars to have left for middle shortening
1416 * @param string $char   the shortening character to use
1417 * @return string
1418 */
1419function shorten($keep, $short, $max, $min = 9, $char = '…') {
1420    $max = $max - utf8_strlen($keep);
1421    if($max < $min) return $keep;
1422    $len = utf8_strlen($short);
1423    if($len <= $max) return $keep.$short;
1424    $half = floor($max / 2);
1425    return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half);
1426}
1427
1428/**
1429 * Return the users realname or e-mail address for use
1430 * in page footer and recent changes pages
1431 *
1432 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1433 */
1434function editorinfo($username) {
1435    global $conf;
1436    global $auth;
1437
1438    switch($conf['showuseras']) {
1439        case 'username':
1440        case 'email':
1441        case 'email_link':
1442            if($auth) $info = $auth->getUserData($username);
1443            break;
1444        default:
1445            return hsc($username);
1446    }
1447
1448    if(isset($info) && $info) {
1449        switch($conf['showuseras']) {
1450            case 'username':
1451                return hsc($info['name']);
1452            case 'email':
1453                return obfuscate($info['mail']);
1454            case 'email_link':
1455                $mail = obfuscate($info['mail']);
1456                return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1457            default:
1458                return hsc($username);
1459        }
1460    } else {
1461        return hsc($username);
1462    }
1463}
1464
1465/**
1466 * Returns the path to a image file for the currently chosen license.
1467 * When no image exists, returns an empty string
1468 *
1469 * @author Andreas Gohr <andi@splitbrain.org>
1470 * @param  string $type - type of image 'badge' or 'button'
1471 * @return string
1472 */
1473function license_img($type) {
1474    global $license;
1475    global $conf;
1476    if(!$conf['license']) return '';
1477    if(!is_array($license[$conf['license']])) return '';
1478    $lic   = $license[$conf['license']];
1479    $try   = array();
1480    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1481    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1482    if(substr($conf['license'], 0, 3) == 'cc-') {
1483        $try[] = 'lib/images/license/'.$type.'/cc.png';
1484    }
1485    foreach($try as $src) {
1486        if(@file_exists(DOKU_INC.$src)) return $src;
1487    }
1488    return '';
1489}
1490
1491/**
1492 * Checks if the given amount of memory is available
1493 *
1494 * If the memory_get_usage() function is not available the
1495 * function just assumes $bytes of already allocated memory
1496 *
1497 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1498 * @author Andreas Gohr <andi@splitbrain.org>
1499 *
1500 * @param  int $mem  Size of memory you want to allocate in bytes
1501 * @param int  $bytes
1502 * @internal param int $used already allocated memory (see above)
1503 * @return bool
1504 */
1505function is_mem_available($mem, $bytes = 1048576) {
1506    $limit = trim(ini_get('memory_limit'));
1507    if(empty($limit)) return true; // no limit set!
1508
1509    // parse limit to bytes
1510    $limit = php_to_byte($limit);
1511
1512    // get used memory if possible
1513    if(function_exists('memory_get_usage')) {
1514        $used = memory_get_usage();
1515    } else {
1516        $used = $bytes;
1517    }
1518
1519    if($used + $mem > $limit) {
1520        return false;
1521    }
1522
1523    return true;
1524}
1525
1526/**
1527 * Send a HTTP redirect to the browser
1528 *
1529 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1530 *
1531 * @link   http://support.microsoft.com/kb/q176113/
1532 * @author Andreas Gohr <andi@splitbrain.org>
1533 */
1534function send_redirect($url) {
1535    //are there any undisplayed messages? keep them in session for display
1536    global $MSG;
1537    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1538        //reopen session, store data and close session again
1539        @session_start();
1540        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1541    }
1542
1543    // always close the session
1544    session_write_close();
1545
1546    // work around IE bug
1547    // http://www.ianhoar.com/2008/11/16/internet-explorer-6-and-redirected-anchor-links/
1548    list($url, $hash) = explode('#', $url);
1549    if($hash) {
1550        if(strpos($url, '?')) {
1551            $url = $url.'&#'.$hash;
1552        } else {
1553            $url = $url.'?&#'.$hash;
1554        }
1555    }
1556
1557    // check if running on IIS < 6 with CGI-PHP
1558    if(isset($_SERVER['SERVER_SOFTWARE']) && isset($_SERVER['GATEWAY_INTERFACE']) &&
1559        (strpos($_SERVER['GATEWAY_INTERFACE'], 'CGI') !== false) &&
1560        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($_SERVER['SERVER_SOFTWARE']), $matches)) &&
1561        $matches[1] < 6
1562    ) {
1563        header('Refresh: 0;url='.$url);
1564    } else {
1565        header('Location: '.$url);
1566    }
1567    exit;
1568}
1569
1570/**
1571 * Validate a value using a set of valid values
1572 *
1573 * This function checks whether a specified value is set and in the array
1574 * $valid_values. If not, the function returns a default value or, if no
1575 * default is specified, throws an exception.
1576 *
1577 * @param string $param        The name of the parameter
1578 * @param array  $valid_values A set of valid values; Optionally a default may
1579 *                             be marked by the key “default”.
1580 * @param array  $array        The array containing the value (typically $_POST
1581 *                             or $_GET)
1582 * @param string $exc          The text of the raised exception
1583 *
1584 * @throws Exception
1585 * @return mixed
1586 * @author Adrian Lang <lang@cosmocode.de>
1587 */
1588function valid_input_set($param, $valid_values, $array, $exc = '') {
1589    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1590        return $array[$param];
1591    } elseif(isset($valid_values['default'])) {
1592        return $valid_values['default'];
1593    } else {
1594        throw new Exception($exc);
1595    }
1596}
1597
1598/**
1599 * Read a preference from the DokuWiki cookie
1600 */
1601function get_doku_pref($pref, $default) {
1602    if(strpos($_COOKIE['DOKU_PREFS'], $pref) !== false) {
1603        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1604        $cnt   = count($parts);
1605        for($i = 0; $i < $cnt; $i += 2) {
1606            if($parts[$i] == $pref) {
1607                return $parts[$i + 1];
1608            }
1609        }
1610    }
1611    return $default;
1612}
1613
1614//Setup VIM: ex: et ts=2 :
1615