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