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