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