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