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