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