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