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