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