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