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