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