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