xref: /dokuwiki/inc/common.php (revision ec98a822181a2d5942150824e8a7b4a420e71602)
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 * Saves a wikitext by calling io_writeWikiPage.
1188 * Also directs changelog and attic updates.
1189 *
1190 * @author Andreas Gohr <andi@splitbrain.org>
1191 * @author Ben Coburn <btcoburn@silicodon.net>
1192 *
1193 * @param string $id       page id
1194 * @param string $text     wikitext being saved
1195 * @param string $summary  summary of text update
1196 * @param bool   $minor    mark this saved version as minor update
1197 */
1198function saveWikiText($id, $text, $summary, $minor = false) {
1199    /* Note to developers:
1200       This code is subtle and delicate. Test the behavior of
1201       the attic and changelog with dokuwiki and external edits
1202       after any changes. External edits change the wiki page
1203       directly without using php or dokuwiki.
1204     */
1205    global $conf;
1206    global $lang;
1207    global $REV;
1208    /* @var Input $INPUT */
1209    global $INPUT;
1210
1211    // ignore if no changes were made
1212    if($text == rawWiki($id, '')) {
1213        return;
1214    }
1215
1216    $file        = wikiFN($id);
1217    $old         = @filemtime($file); // from page
1218    $wasRemoved  = (trim($text) == ''); // check for empty or whitespace only
1219    $wasCreated  = !file_exists($file);
1220    $wasReverted = ($REV == true);
1221    $pagelog     = new PageChangeLog($id, 1024);
1222    $newRev      = false;
1223    $oldRev      = $pagelog->getRevisions(-1, 1); // from changelog
1224    $oldRev      = (int) (empty($oldRev) ? 0 : $oldRev[0]);
1225    if(!file_exists(wikiFN($id, $old)) && file_exists($file) && $old >= $oldRev) {
1226        // add old revision to the attic if missing
1227        saveOldRevision($id);
1228        // add a changelog entry if this edit came from outside dokuwiki
1229        if($old > $oldRev) {
1230            addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true));
1231            // remove soon to be stale instructions
1232            $cache = new cache_instructions($id, $file);
1233            $cache->removeCache();
1234        }
1235    }
1236
1237    if($wasRemoved) {
1238        // Send "update" event with empty data, so plugins can react to page deletion
1239        $data = array(array($file, '', false), getNS($id), noNS($id), false);
1240        trigger_event('IO_WIKIPAGE_WRITE', $data);
1241        // pre-save deleted revision
1242        @touch($file);
1243        clearstatcache();
1244        $newRev = saveOldRevision($id);
1245        // remove empty file
1246        @unlink($file);
1247        // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata...
1248        // purge non-persistant meta data
1249        p_purge_metadata($id);
1250        $del = true;
1251        // autoset summary on deletion
1252        if(empty($summary)) $summary = $lang['deleted'];
1253        // remove empty namespaces
1254        io_sweepNS($id, 'datadir');
1255        io_sweepNS($id, 'mediadir');
1256    } else {
1257        // save file (namespace dir is created in io_writeWikiPage)
1258        io_writeWikiPage($file, $text, $id);
1259        // pre-save the revision, to keep the attic in sync
1260        $newRev = saveOldRevision($id);
1261        $del    = false;
1262    }
1263
1264    // select changelog line type
1265    $extra = '';
1266    $type  = DOKU_CHANGE_TYPE_EDIT;
1267    if($wasReverted) {
1268        $type  = DOKU_CHANGE_TYPE_REVERT;
1269        $extra = $REV;
1270    } else if($wasCreated) {
1271        $type = DOKU_CHANGE_TYPE_CREATE;
1272    } else if($wasRemoved) {
1273        $type = DOKU_CHANGE_TYPE_DELETE;
1274    } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
1275        $type = DOKU_CHANGE_TYPE_MINOR_EDIT;
1276    } //minor edits only for logged in users
1277
1278    addLogEntry($newRev, $id, $type, $summary, $extra);
1279    // send notify mails
1280    notify($id, 'admin', $old, $summary, $minor);
1281    notify($id, 'subscribers', $old, $summary, $minor);
1282
1283    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1284    io_saveFile($conf['cachedir'].'/purgefile', time());
1285
1286    // if useheading is enabled, purge the cache of all linking pages
1287    if(useHeading('content')) {
1288        $pages = ft_backlinks($id, true);
1289        foreach($pages as $page) {
1290            $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
1291            $cache->removeCache();
1292        }
1293    }
1294}
1295
1296/**
1297 * moves the current version to the attic and returns its
1298 * revision date
1299 *
1300 * @author Andreas Gohr <andi@splitbrain.org>
1301 *
1302 * @param string $id page id
1303 * @return int|string revision timestamp
1304 */
1305function saveOldRevision($id) {
1306    $oldf = wikiFN($id);
1307    if(!file_exists($oldf)) return '';
1308    $date = filemtime($oldf);
1309    $newf = wikiFN($id, $date);
1310    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1311    return $date;
1312}
1313
1314/**
1315 * Sends a notify mail on page change or registration
1316 *
1317 * @param string     $id       The changed page
1318 * @param string     $who      Who to notify (admin|subscribers|register)
1319 * @param int|string $rev Old page revision
1320 * @param string     $summary  What changed
1321 * @param boolean    $minor    Is this a minor edit?
1322 * @param string[]   $replace  Additional string substitutions, @KEY@ to be replaced by value
1323 * @return bool
1324 *
1325 * @author Andreas Gohr <andi@splitbrain.org>
1326 */
1327function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) {
1328    global $conf;
1329    /* @var Input $INPUT */
1330    global $INPUT;
1331
1332    // decide if there is something to do, eg. whom to mail
1333    if($who == 'admin') {
1334        if(empty($conf['notify'])) return false; //notify enabled?
1335        $tpl = 'mailtext';
1336        $to  = $conf['notify'];
1337    } elseif($who == 'subscribers') {
1338        if(!actionOK('subscribe')) return false; //subscribers enabled?
1339        if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1340        $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
1341        trigger_event(
1342            'COMMON_NOTIFY_ADDRESSLIST', $data,
1343            array(new Subscription(), 'notifyaddresses')
1344        );
1345        $to = $data['addresslist'];
1346        if(empty($to)) return false;
1347        $tpl = 'subscr_single';
1348    } else {
1349        return false; //just to be safe
1350    }
1351
1352    // prepare content
1353    $subscription = new Subscription();
1354    return $subscription->send_diff($to, $tpl, $id, $rev, $summary);
1355}
1356
1357/**
1358 * extracts the query from a search engine referrer
1359 *
1360 * @author Andreas Gohr <andi@splitbrain.org>
1361 * @author Todd Augsburger <todd@rollerorgans.com>
1362 *
1363 * @return array|string
1364 */
1365function getGoogleQuery() {
1366    /* @var Input $INPUT */
1367    global $INPUT;
1368
1369    if(!$INPUT->server->has('HTTP_REFERER')) {
1370        return '';
1371    }
1372    $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1373
1374    // only handle common SEs
1375    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1376
1377    $query = array();
1378    // temporary workaround against PHP bug #49733
1379    // see http://bugs.php.net/bug.php?id=49733
1380    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1381    parse_str($url['query'], $query);
1382    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1383
1384    $q = '';
1385    if(isset($query['q'])){
1386        $q = $query['q'];
1387    }elseif(isset($query['p'])){
1388        $q = $query['p'];
1389    }elseif(isset($query['query'])){
1390        $q = $query['query'];
1391    }
1392    $q = trim($q);
1393
1394    if(!$q) return '';
1395    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1396    return $q;
1397}
1398
1399/**
1400 * Return the human readable size of a file
1401 *
1402 * @param int $size A file size
1403 * @param int $dec A number of decimal places
1404 * @return string human readable size
1405 *
1406 * @author      Martin Benjamin <b.martin@cybernet.ch>
1407 * @author      Aidan Lister <aidan@php.net>
1408 * @version     1.0.0
1409 */
1410function filesize_h($size, $dec = 1) {
1411    $sizes = array('B', 'KB', 'MB', 'GB');
1412    $count = count($sizes);
1413    $i     = 0;
1414
1415    while($size >= 1024 && ($i < $count - 1)) {
1416        $size /= 1024;
1417        $i++;
1418    }
1419
1420    return round($size, $dec).' '.$sizes[$i];
1421}
1422
1423/**
1424 * Return the given timestamp as human readable, fuzzy age
1425 *
1426 * @author Andreas Gohr <gohr@cosmocode.de>
1427 *
1428 * @param int $dt timestamp
1429 * @return string
1430 */
1431function datetime_h($dt) {
1432    global $lang;
1433
1434    $ago = time() - $dt;
1435    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1436        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1437    }
1438    if($ago > 24 * 60 * 60 * 30 * 2) {
1439        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1440    }
1441    if($ago > 24 * 60 * 60 * 7 * 2) {
1442        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1443    }
1444    if($ago > 24 * 60 * 60 * 2) {
1445        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1446    }
1447    if($ago > 60 * 60 * 2) {
1448        return sprintf($lang['hours'], round($ago / (60 * 60)));
1449    }
1450    if($ago > 60 * 2) {
1451        return sprintf($lang['minutes'], round($ago / (60)));
1452    }
1453    return sprintf($lang['seconds'], $ago);
1454}
1455
1456/**
1457 * Wraps around strftime but provides support for fuzzy dates
1458 *
1459 * The format default to $conf['dformat']. It is passed to
1460 * strftime - %f can be used to get the value from datetime_h()
1461 *
1462 * @see datetime_h
1463 * @author Andreas Gohr <gohr@cosmocode.de>
1464 *
1465 * @param int|null $dt      timestamp when given, null will take current timestamp
1466 * @param string   $format  empty default to $conf['dformat'], or provide format as recognized by strftime()
1467 * @return string
1468 */
1469function dformat($dt = null, $format = '') {
1470    global $conf;
1471
1472    if(is_null($dt)) $dt = time();
1473    $dt = (int) $dt;
1474    if(!$format) $format = $conf['dformat'];
1475
1476    $format = str_replace('%f', datetime_h($dt), $format);
1477    return strftime($format, $dt);
1478}
1479
1480/**
1481 * Formats a timestamp as ISO 8601 date
1482 *
1483 * @author <ungu at terong dot com>
1484 * @link http://www.php.net/manual/en/function.date.php#54072
1485 *
1486 * @param int $int_date current date in UNIX timestamp
1487 * @return string
1488 */
1489function date_iso8601($int_date) {
1490    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1491    $pre_timezone = date('O', $int_date);
1492    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1493    $date_mod .= $time_zone;
1494    return $date_mod;
1495}
1496
1497/**
1498 * return an obfuscated email address in line with $conf['mailguard'] setting
1499 *
1500 * @author Harry Fuecks <hfuecks@gmail.com>
1501 * @author Christopher Smith <chris@jalakai.co.uk>
1502 *
1503 * @param string $email email address
1504 * @return string
1505 */
1506function obfuscate($email) {
1507    global $conf;
1508
1509    switch($conf['mailguard']) {
1510        case 'visible' :
1511            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1512            return strtr($email, $obfuscate);
1513
1514        case 'hex' :
1515            $encode = '';
1516            $len    = strlen($email);
1517            for($x = 0; $x < $len; $x++) {
1518                $encode .= '&#x'.bin2hex($email{$x}).';';
1519            }
1520            return $encode;
1521
1522        case 'none' :
1523        default :
1524            return $email;
1525    }
1526}
1527
1528/**
1529 * Removes quoting backslashes
1530 *
1531 * @author Andreas Gohr <andi@splitbrain.org>
1532 *
1533 * @param string $string
1534 * @param string $char backslashed character
1535 * @return string
1536 */
1537function unslash($string, $char = "'") {
1538    return str_replace('\\'.$char, $char, $string);
1539}
1540
1541/**
1542 * Convert php.ini shorthands to byte
1543 *
1544 * @author <gilthans dot NO dot SPAM at gmail dot com>
1545 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1546 *
1547 * @param string $v shorthands
1548 * @return int|string
1549 */
1550function php_to_byte($v) {
1551    $l   = substr($v, -1);
1552    $ret = substr($v, 0, -1);
1553    switch(strtoupper($l)) {
1554        /** @noinspection PhpMissingBreakStatementInspection */
1555        case 'P':
1556            $ret *= 1024;
1557        /** @noinspection PhpMissingBreakStatementInspection */
1558        case 'T':
1559            $ret *= 1024;
1560        /** @noinspection PhpMissingBreakStatementInspection */
1561        case 'G':
1562            $ret *= 1024;
1563        /** @noinspection PhpMissingBreakStatementInspection */
1564        case 'M':
1565            $ret *= 1024;
1566        /** @noinspection PhpMissingBreakStatementInspection */
1567        case 'K':
1568            $ret *= 1024;
1569            break;
1570        default;
1571            $ret *= 10;
1572            break;
1573    }
1574    return $ret;
1575}
1576
1577/**
1578 * Wrapper around preg_quote adding the default delimiter
1579 *
1580 * @param string $string
1581 * @return string
1582 */
1583function preg_quote_cb($string) {
1584    return preg_quote($string, '/');
1585}
1586
1587/**
1588 * Shorten a given string by removing data from the middle
1589 *
1590 * You can give the string in two parts, the first part $keep
1591 * will never be shortened. The second part $short will be cut
1592 * in the middle to shorten but only if at least $min chars are
1593 * left to display it. Otherwise it will be left off.
1594 *
1595 * @param string $keep   the part to keep
1596 * @param string $short  the part to shorten
1597 * @param int    $max    maximum chars you want for the whole string
1598 * @param int    $min    minimum number of chars to have left for middle shortening
1599 * @param string $char   the shortening character to use
1600 * @return string
1601 */
1602function shorten($keep, $short, $max, $min = 9, $char = '…') {
1603    $max = $max - utf8_strlen($keep);
1604    if($max < $min) return $keep;
1605    $len = utf8_strlen($short);
1606    if($len <= $max) return $keep.$short;
1607    $half = floor($max / 2);
1608    return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half);
1609}
1610
1611/**
1612 * Return the users real name or e-mail address for use
1613 * in page footer and recent changes pages
1614 *
1615 * @param string|null $username or null when currently logged-in user should be used
1616 * @param bool $textonly true returns only plain text, true allows returning html
1617 * @return string html or plain text(not escaped) of formatted user name
1618 *
1619 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1620 */
1621function editorinfo($username, $textonly = false) {
1622    return userlink($username, $textonly);
1623}
1624
1625/**
1626 * Returns users realname w/o link
1627 *
1628 * @param string|null $username or null when currently logged-in user should be used
1629 * @param bool $textonly true returns only plain text, true allows returning html
1630 * @return string html or plain text(not escaped) of formatted user name
1631 *
1632 * @triggers COMMON_USER_LINK
1633 */
1634function userlink($username = null, $textonly = false) {
1635    global $conf, $INFO;
1636    /** @var DokuWiki_Auth_Plugin $auth */
1637    global $auth;
1638    /** @var Input $INPUT */
1639    global $INPUT;
1640
1641    // prepare initial event data
1642    $data = array(
1643        'username' => $username, // the unique user name
1644        'name' => '',
1645        'link' => array( //setting 'link' to false disables linking
1646                         'target' => '',
1647                         'pre' => '',
1648                         'suf' => '',
1649                         'style' => '',
1650                         'more' => '',
1651                         'url' => '',
1652                         'title' => '',
1653                         'class' => ''
1654        ),
1655        'userlink' => '', // formatted user name as will be returned
1656        'textonly' => $textonly
1657    );
1658    if($username === null) {
1659        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1660        if($textonly){
1661            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1662        }else {
1663            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1664        }
1665    }
1666
1667    $evt = new Doku_Event('COMMON_USER_LINK', $data);
1668    if($evt->advise_before(true)) {
1669        if(empty($data['name'])) {
1670            if($auth) $info = $auth->getUserData($username);
1671            if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1672                switch($conf['showuseras']) {
1673                    case 'username':
1674                    case 'username_link':
1675                        $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1676                        break;
1677                    case 'email':
1678                    case 'email_link':
1679                        $data['name'] = obfuscate($info['mail']);
1680                        break;
1681                }
1682            } else {
1683                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1684            }
1685        }
1686
1687        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1688        static $xhtml_renderer = null;
1689
1690        if(!$data['textonly'] && empty($data['link']['url'])) {
1691
1692            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1693                if(!isset($info)) {
1694                    if($auth) $info = $auth->getUserData($username);
1695                }
1696                if(isset($info) && $info) {
1697                    if($conf['showuseras'] == 'email_link') {
1698                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1699                    } else {
1700                        if(is_null($xhtml_renderer)) {
1701                            $xhtml_renderer = p_get_renderer('xhtml');
1702                        }
1703                        if(empty($xhtml_renderer->interwiki)) {
1704                            $xhtml_renderer->interwiki = getInterwiki();
1705                        }
1706                        $shortcut = 'user';
1707                        $exists = null;
1708                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1709                        $data['link']['class'] .= ' interwiki iw_user';
1710                        if($exists !== null) {
1711                            if($exists) {
1712                                $data['link']['class'] .= ' wikilink1';
1713                            } else {
1714                                $data['link']['class'] .= ' wikilink2';
1715                                $data['link']['rel'] = 'nofollow';
1716                            }
1717                        }
1718                    }
1719                } else {
1720                    $data['textonly'] = true;
1721                }
1722
1723            } else {
1724                $data['textonly'] = true;
1725            }
1726        }
1727
1728        if($data['textonly']) {
1729            $data['userlink'] = $data['name'];
1730        } else {
1731            $data['link']['name'] = $data['name'];
1732            if(is_null($xhtml_renderer)) {
1733                $xhtml_renderer = p_get_renderer('xhtml');
1734            }
1735            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1736        }
1737    }
1738    $evt->advise_after();
1739    unset($evt);
1740
1741    return $data['userlink'];
1742}
1743
1744/**
1745 * Returns the path to a image file for the currently chosen license.
1746 * When no image exists, returns an empty string
1747 *
1748 * @author Andreas Gohr <andi@splitbrain.org>
1749 *
1750 * @param  string $type - type of image 'badge' or 'button'
1751 * @return string
1752 */
1753function license_img($type) {
1754    global $license;
1755    global $conf;
1756    if(!$conf['license']) return '';
1757    if(!is_array($license[$conf['license']])) return '';
1758    $try   = array();
1759    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1760    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1761    if(substr($conf['license'], 0, 3) == 'cc-') {
1762        $try[] = 'lib/images/license/'.$type.'/cc.png';
1763    }
1764    foreach($try as $src) {
1765        if(file_exists(DOKU_INC.$src)) return $src;
1766    }
1767    return '';
1768}
1769
1770/**
1771 * Checks if the given amount of memory is available
1772 *
1773 * If the memory_get_usage() function is not available the
1774 * function just assumes $bytes of already allocated memory
1775 *
1776 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1777 * @author Andreas Gohr <andi@splitbrain.org>
1778 *
1779 * @param int  $mem    Size of memory you want to allocate in bytes
1780 * @param int  $bytes  already allocated memory (see above)
1781 * @return bool
1782 */
1783function is_mem_available($mem, $bytes = 1048576) {
1784    $limit = trim(ini_get('memory_limit'));
1785    if(empty($limit)) return true; // no limit set!
1786
1787    // parse limit to bytes
1788    $limit = php_to_byte($limit);
1789
1790    // get used memory if possible
1791    if(function_exists('memory_get_usage')) {
1792        $used = memory_get_usage();
1793    } else {
1794        $used = $bytes;
1795    }
1796
1797    if($used + $mem > $limit) {
1798        return false;
1799    }
1800
1801    return true;
1802}
1803
1804/**
1805 * Send a HTTP redirect to the browser
1806 *
1807 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1808 *
1809 * @link   http://support.microsoft.com/kb/q176113/
1810 * @author Andreas Gohr <andi@splitbrain.org>
1811 *
1812 * @param string $url url being directed to
1813 */
1814function send_redirect($url) {
1815    /* @var Input $INPUT */
1816    global $INPUT;
1817
1818    //are there any undisplayed messages? keep them in session for display
1819    global $MSG;
1820    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1821        //reopen session, store data and close session again
1822        @session_start();
1823        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1824    }
1825
1826    // always close the session
1827    session_write_close();
1828
1829    // check if running on IIS < 6 with CGI-PHP
1830    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1831        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1832        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1833        $matches[1] < 6
1834    ) {
1835        header('Refresh: 0;url='.$url);
1836    } else {
1837        header('Location: '.$url);
1838    }
1839
1840    if(defined('DOKU_UNITTEST')) return; // no exits during unit tests
1841    exit;
1842}
1843
1844/**
1845 * Validate a value using a set of valid values
1846 *
1847 * This function checks whether a specified value is set and in the array
1848 * $valid_values. If not, the function returns a default value or, if no
1849 * default is specified, throws an exception.
1850 *
1851 * @param string $param        The name of the parameter
1852 * @param array  $valid_values A set of valid values; Optionally a default may
1853 *                             be marked by the key “default”.
1854 * @param array  $array        The array containing the value (typically $_POST
1855 *                             or $_GET)
1856 * @param string $exc          The text of the raised exception
1857 *
1858 * @throws Exception
1859 * @return mixed
1860 * @author Adrian Lang <lang@cosmocode.de>
1861 */
1862function valid_input_set($param, $valid_values, $array, $exc = '') {
1863    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1864        return $array[$param];
1865    } elseif(isset($valid_values['default'])) {
1866        return $valid_values['default'];
1867    } else {
1868        throw new Exception($exc);
1869    }
1870}
1871
1872/**
1873 * Read a preference from the DokuWiki cookie
1874 * (remembering both keys & values are urlencoded)
1875 *
1876 * @param string $pref     preference key
1877 * @param mixed  $default  value returned when preference not found
1878 * @return string preference value
1879 */
1880function get_doku_pref($pref, $default) {
1881    $enc_pref = urlencode($pref);
1882    if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
1883        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1884        $cnt   = count($parts);
1885        for($i = 0; $i < $cnt; $i += 2) {
1886            if($parts[$i] == $enc_pref) {
1887                return urldecode($parts[$i + 1]);
1888            }
1889        }
1890    }
1891    return $default;
1892}
1893
1894/**
1895 * Add a preference to the DokuWiki cookie
1896 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
1897 * Remove it by setting $val to false
1898 *
1899 * @param string $pref  preference key
1900 * @param string $val   preference value
1901 */
1902function set_doku_pref($pref, $val) {
1903    global $conf;
1904    $orig = get_doku_pref($pref, false);
1905    $cookieVal = '';
1906
1907    if($orig && ($orig != $val)) {
1908        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
1909        $cnt   = count($parts);
1910        // urlencode $pref for the comparison
1911        $enc_pref = rawurlencode($pref);
1912        for($i = 0; $i < $cnt; $i += 2) {
1913            if($parts[$i] == $enc_pref) {
1914                if ($val !== false) {
1915                    $parts[$i + 1] = rawurlencode($val);
1916                } else {
1917                    unset($parts[$i]);
1918                    unset($parts[$i + 1]);
1919                }
1920                break;
1921            }
1922        }
1923        $cookieVal = implode('#', $parts);
1924    } else if (!$orig && $val !== false) {
1925        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val);
1926    }
1927
1928    if (!empty($cookieVal)) {
1929        $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1930        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
1931    }
1932}
1933
1934/**
1935 * Strips source mapping declarations from given text #601
1936 *
1937 * @param string &$text reference to the CSS or JavaScript code to clean
1938 */
1939function stripsourcemaps(&$text){
1940    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
1941}
1942
1943//Setup VIM: ex: et ts=2 :
1944