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