Failed Conditions
Push — refactorSubscriptions ( 75d664...451969 )
by Michael
03:05
created

inc/common.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

Code
1
<?php
2
/**
3
 * Common DokuWiki functions
4
 *
5
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6
 * @author     Andreas Gohr <[email protected]>
7
 */
8
9
use dokuwiki\Cache\CacheInstructions;
10
use dokuwiki\Cache\CacheRenderer;
11
use dokuwiki\ChangeLog\PageChangeLog;
12
use dokuwiki\Subscriptions\PageSubscriptionSender;
13
use dokuwiki\Subscriptions\SubscriberManager;
14
15
/**
16
 * These constants are used with the recents function
17
 */
18
define('RECENTS_SKIP_DELETED', 2);
19
define('RECENTS_SKIP_MINORS', 4);
20
define('RECENTS_SKIP_SUBSPACES', 8);
21
define('RECENTS_MEDIA_CHANGES', 16);
22
define('RECENTS_MEDIA_PAGES_MIXED', 32);
23
24
/**
25
 * Wrapper around htmlspecialchars()
26
 *
27
 * @author Andreas Gohr <[email protected]>
28
 * @see    htmlspecialchars()
29
 *
30
 * @param string $string the string being converted
31
 * @return string converted string
32
 */
33
function hsc($string) {
34
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
35
}
36
37
/**
38
 * Checks if the given input is blank
39
 *
40
 * This is similar to empty() but will return false for "0".
41
 *
42
 * Please note: when you pass uninitialized variables, they will implicitly be created
43
 * with a NULL value without warning.
44
 *
45
 * To avoid this it's recommended to guard the call with isset like this:
46
 *
47
 * (isset($foo) && !blank($foo))
48
 * (!isset($foo) || blank($foo))
49
 *
50
 * @param $in
51
 * @param bool $trim Consider a string of whitespace to be blank
52
 * @return bool
53
 */
54
function blank(&$in, $trim = false) {
55
    if(is_null($in)) return true;
56
    if(is_array($in)) return empty($in);
57
    if($in === "\0") return true;
58
    if($trim && trim($in) === '') return true;
59
    if(strlen($in) > 0) return false;
60
    return empty($in);
61
}
62
63
/**
64
 * print a newline terminated string
65
 *
66
 * You can give an indention as optional parameter
67
 *
68
 * @author Andreas Gohr <[email protected]>
69
 *
70
 * @param string $string  line of text
71
 * @param int    $indent  number of spaces indention
72
 */
73
function ptln($string, $indent = 0) {
74
    echo str_repeat(' ', $indent)."$string\n";
75
}
76
77
/**
78
 * strips control characters (<32) from the given string
79
 *
80
 * @author Andreas Gohr <[email protected]>
81
 *
82
 * @param string $string being stripped
83
 * @return string
84
 */
85
function stripctl($string) {
86
    return preg_replace('/[\x00-\x1F]+/s', '', $string);
87
}
88
89
/**
90
 * Return a secret token to be used for CSRF attack prevention
91
 *
92
 * @author  Andreas Gohr <[email protected]>
93
 * @link    http://en.wikipedia.org/wiki/Cross-site_request_forgery
94
 * @link    http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
95
 *
96
 * @return  string
97
 */
98
function getSecurityToken() {
99
    /** @var Input $INPUT */
100
    global $INPUT;
101
102
    $user = $INPUT->server->str('REMOTE_USER');
103
    $session = session_id();
104
105
    // CSRF checks are only for logged in users - do not generate for anonymous
106
    if(trim($user) == '' || trim($session) == '') return '';
107
    return \dokuwiki\PassHash::hmac('md5', $session.$user, auth_cookiesalt());
108
}
109
110
/**
111
 * Check the secret CSRF token
112
 *
113
 * @param null|string $token security token or null to read it from request variable
114
 * @return bool success if the token matched
115
 */
116
function checkSecurityToken($token = null) {
117
    /** @var Input $INPUT */
118
    global $INPUT;
119
    if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
120
121
    if(is_null($token)) $token = $INPUT->str('sectok');
122
    if(getSecurityToken() != $token) {
123
        msg('Security Token did not match. Possible CSRF attack.', -1);
124
        return false;
125
    }
126
    return true;
127
}
128
129
/**
130
 * Print a hidden form field with a secret CSRF token
131
 *
132
 * @author  Andreas Gohr <[email protected]>
133
 *
134
 * @param bool $print  if true print the field, otherwise html of the field is returned
135
 * @return string html of hidden form field
136
 */
137
function formSecurityToken($print = true) {
138
    $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
139
    if($print) echo $ret;
140
    return $ret;
141
}
142
143
/**
144
 * Determine basic information for a request of $id
145
 *
146
 * @author Andreas Gohr <[email protected]>
147
 * @author Chris Smith <[email protected]>
148
 *
149
 * @param string $id         pageid
150
 * @param bool   $htmlClient add info about whether is mobile browser
151
 * @return array with info for a request of $id
152
 *
153
 */
154
function basicinfo($id, $htmlClient=true){
155
    global $USERINFO;
156
    /* @var Input $INPUT */
157
    global $INPUT;
158
159
    // set info about manager/admin status.
160
    $info = array();
161
    $info['isadmin']   = false;
162
    $info['ismanager'] = false;
163
    if($INPUT->server->has('REMOTE_USER')) {
164
        $info['userinfo']   = $USERINFO;
165
        $info['perm']       = auth_quickaclcheck($id);
166
        $info['client']     = $INPUT->server->str('REMOTE_USER');
167
168
        if($info['perm'] == AUTH_ADMIN) {
169
            $info['isadmin']   = true;
170
            $info['ismanager'] = true;
171
        } elseif(auth_ismanager()) {
172
            $info['ismanager'] = true;
173
        }
174
175
        // if some outside auth were used only REMOTE_USER is set
176
        if(!$info['userinfo']['name']) {
177
            $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
178
        }
179
180
    } else {
181
        $info['perm']       = auth_aclcheck($id, '', null);
182
        $info['client']     = clientIP(true);
183
    }
184
185
    $info['namespace'] = getNS($id);
186
187
    // mobile detection
188
    if ($htmlClient) {
189
        $info['ismobile'] = clientismobile();
190
    }
191
192
    return $info;
193
 }
194
195
/**
196
 * Return info about the current document as associative
197
 * array.
198
 *
199
 * @author Andreas Gohr <[email protected]>
200
 *
201
 * @return array with info about current document
202
 */
203
function pageinfo() {
204
    global $ID;
205
    global $REV;
206
    global $RANGE;
207
    global $lang;
208
    /* @var Input $INPUT */
209
    global $INPUT;
210
211
    $info = basicinfo($ID);
212
213
    // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
214
    // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
215
    $info['id']  = $ID;
216
    $info['rev'] = $REV;
217
218
    if($INPUT->server->has('REMOTE_USER')) {
219
        $subManager = new SubscriberManager();
220
        $info['subscribed'] = $subManager->userSubscription();
221
    } else {
222
        $info['subscribed'] = false;
223
    }
224
225
    $info['locked']     = checklock($ID);
226
    $info['filepath']   = wikiFN($ID);
227
    $info['exists']     = file_exists($info['filepath']);
228
    $info['currentrev'] = @filemtime($info['filepath']);
229
    if($REV) {
230
        //check if current revision was meant
231
        if($info['exists'] && ($info['currentrev'] == $REV)) {
232
            $REV = '';
233
        } elseif($RANGE) {
234
            //section editing does not work with old revisions!
235
            $REV   = '';
236
            $RANGE = '';
237
            msg($lang['nosecedit'], 0);
238
        } else {
239
            //really use old revision
240
            $info['filepath'] = wikiFN($ID, $REV);
241
            $info['exists']   = file_exists($info['filepath']);
242
        }
243
    }
244
    $info['rev'] = $REV;
245
    if($info['exists']) {
246
        $info['writable'] = (is_writable($info['filepath']) &&
247
            ($info['perm'] >= AUTH_EDIT));
248
    } else {
249
        $info['writable'] = ($info['perm'] >= AUTH_CREATE);
250
    }
251
    $info['editable'] = ($info['writable'] && empty($info['locked']));
252
    $info['lastmod']  = @filemtime($info['filepath']);
253
254
    //load page meta data
255
    $info['meta'] = p_get_metadata($ID);
256
257
    //who's the editor
258
    $pagelog = new PageChangeLog($ID, 1024);
259
    if($REV) {
260
        $revinfo = $pagelog->getRevisionInfo($REV);
261
    } else {
262
        if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
263
            $revinfo = $info['meta']['last_change'];
264
        } else {
265
            $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
266
            // cache most recent changelog line in metadata if missing and still valid
267
            if($revinfo !== false) {
268
                $info['meta']['last_change'] = $revinfo;
269
                p_set_metadata($ID, array('last_change' => $revinfo));
270
            }
271
        }
272
    }
273
    //and check for an external edit
274
    if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
275
        // cached changelog line no longer valid
276
        $revinfo                     = false;
277
        $info['meta']['last_change'] = $revinfo;
278
        p_set_metadata($ID, array('last_change' => $revinfo));
279
    }
280
281
    $info['ip']   = $revinfo['ip'];
282
    $info['user'] = $revinfo['user'];
283
    $info['sum']  = $revinfo['sum'];
284
    // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
285
    // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
286
287
    if($revinfo['user']) {
288
        $info['editor'] = $revinfo['user'];
289
    } else {
290
        $info['editor'] = $revinfo['ip'];
291
    }
292
293
    // draft
294
    $draft = new \dokuwiki\Draft($ID, $info['client']);
295
    if ($draft->isDraftAvailable()) {
296
        $info['draft'] = $draft->getDraftFilename();
297
    }
298
299
    return $info;
300
}
301
302
/**
303
 * Initialize and/or fill global $JSINFO with some basic info to be given to javascript
304
 */
305
function jsinfo() {
306
    global $JSINFO, $ID, $INFO, $ACT;
307
308
    if (!is_array($JSINFO)) {
309
        $JSINFO = [];
310
    }
311
    //export minimal info to JS, plugins can add more
312
    $JSINFO['id']                    = $ID;
313
    $JSINFO['namespace']             = (string) $INFO['namespace'];
314
    $JSINFO['ACT']                   = act_clean($ACT);
315
    $JSINFO['useHeadingNavigation']  = (int) useHeading('navigation');
316
    $JSINFO['useHeadingContent']     = (int) useHeading('content');
317
}
318
319
/**
320
 * Return information about the current media item as an associative array.
321
 *
322
 * @return array with info about current media item
323
 */
324
function mediainfo(){
325
    global $NS;
326
    global $IMG;
327
328
    $info = basicinfo("$NS:*");
329
    $info['image'] = $IMG;
330
331
    return $info;
332
}
333
334
/**
335
 * Build an string of URL parameters
336
 *
337
 * @author Andreas Gohr
338
 *
339
 * @param array  $params    array with key-value pairs
340
 * @param string $sep       series of pairs are separated by this character
341
 * @return string query string
342
 */
343
function buildURLparams($params, $sep = '&amp;') {
344
    $url = '';
345
    $amp = false;
346
    foreach($params as $key => $val) {
347
        if($amp) $url .= $sep;
348
349
        $url .= rawurlencode($key).'=';
350
        $url .= rawurlencode((string) $val);
351
        $amp = true;
352
    }
353
    return $url;
354
}
355
356
/**
357
 * Build an string of html tag attributes
358
 *
359
 * Skips keys starting with '_', values get HTML encoded
360
 *
361
 * @author Andreas Gohr
362
 *
363
 * @param array $params    array with (attribute name-attribute value) pairs
364
 * @param bool  $skipempty skip empty string values?
365
 * @return string
366
 */
367
function buildAttributes($params, $skipempty = false) {
368
    $url   = '';
369
    $white = false;
370
    foreach($params as $key => $val) {
371
        if($key{0} == '_') continue;
372
        if($val === '' && $skipempty) continue;
373
        if($white) $url .= ' ';
374
375
        $url .= $key.'="';
376
        $url .= htmlspecialchars($val);
377
        $url .= '"';
378
        $white = true;
379
    }
380
    return $url;
381
}
382
383
/**
384
 * This builds the breadcrumb trail and returns it as array
385
 *
386
 * @author Andreas Gohr <[email protected]>
387
 *
388
 * @return string[] with the data: array(pageid=>name, ... )
389
 */
390
function breadcrumbs() {
391
    // we prepare the breadcrumbs early for quick session closing
392
    static $crumbs = null;
393
    if($crumbs != null) return $crumbs;
394
395
    global $ID;
396
    global $ACT;
397
    global $conf;
398
399
    //first visit?
400
    $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
401
    //we only save on show and existing visible wiki documents
402
    $file = wikiFN($ID);
403
    if($ACT != 'show' || isHiddenPage($ID) || !file_exists($file)) {
404
        $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
405
        return $crumbs;
406
    }
407
408
    // page names
409
    $name = noNSorNS($ID);
410
    if(useHeading('navigation')) {
411
        // get page title
412
        $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
413
        if($title) {
414
            $name = $title;
415
        }
416
    }
417
418
    //remove ID from array
419
    if(isset($crumbs[$ID])) {
420
        unset($crumbs[$ID]);
421
    }
422
423
    //add to array
424
    $crumbs[$ID] = $name;
425
    //reduce size
426
    while(count($crumbs) > $conf['breadcrumbs']) {
427
        array_shift($crumbs);
428
    }
429
    //save to session
430
    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
431
    return $crumbs;
432
}
433
434
/**
435
 * Filter for page IDs
436
 *
437
 * This is run on a ID before it is outputted somewhere
438
 * currently used to replace the colon with something else
439
 * on Windows (non-IIS) systems and to have proper URL encoding
440
 *
441
 * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and
442
 * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of
443
 * unaffected servers instead of blacklisting affected servers here.
444
 *
445
 * Urlencoding is ommitted when the second parameter is false
446
 *
447
 * @author Andreas Gohr <[email protected]>
448
 *
449
 * @param string $id pageid being filtered
450
 * @param bool   $ue apply urlencoding?
451
 * @return string
452
 */
453
function idfilter($id, $ue = true) {
454
    global $conf;
455
    /* @var Input $INPUT */
456
    global $INPUT;
457
458
    if($conf['useslash'] && $conf['userewrite']) {
459
        $id = strtr($id, ':', '/');
460
    } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
461
        $conf['userewrite'] &&
462
        strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
463
    ) {
464
        $id = strtr($id, ':', ';');
465
    }
466
    if($ue) {
467
        $id = rawurlencode($id);
468
        $id = str_replace('%3A', ':', $id); //keep as colon
469
        $id = str_replace('%3B', ';', $id); //keep as semicolon
470
        $id = str_replace('%2F', '/', $id); //keep as slash
471
    }
472
    return $id;
473
}
474
475
/**
476
 * This builds a link to a wikipage
477
 *
478
 * It handles URL rewriting and adds additional parameters
479
 *
480
 * @author Andreas Gohr <[email protected]>
481
 *
482
 * @param string       $id             page id, defaults to start page
483
 * @param string|array $urlParameters  URL parameters, associative array recommended
484
 * @param bool         $absolute       request an absolute URL instead of relative
485
 * @param string       $separator      parameter separator
486
 * @return string
487
 */
488
function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;') {
489
    global $conf;
490
    if(is_array($urlParameters)) {
491
        if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']);
492
        if(isset($urlParameters['at']) && $conf['date_at_format']) {
493
            $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']);
494
        }
495
        $urlParameters = buildURLparams($urlParameters, $separator);
496
    } else {
497
        $urlParameters = str_replace(',', $separator, $urlParameters);
498
    }
499
    if($id === '') {
500
        $id = $conf['start'];
501
    }
502
    $id = idfilter($id);
503
    if($absolute) {
504
        $xlink = DOKU_URL;
505
    } else {
506
        $xlink = DOKU_BASE;
507
    }
508
509
    if($conf['userewrite'] == 2) {
510
        $xlink .= DOKU_SCRIPT.'/'.$id;
511
        if($urlParameters) $xlink .= '?'.$urlParameters;
512
    } elseif($conf['userewrite']) {
513
        $xlink .= $id;
514
        if($urlParameters) $xlink .= '?'.$urlParameters;
515
    } elseif($id !== '') {
516
        $xlink .= DOKU_SCRIPT.'?id='.$id;
517
        if($urlParameters) $xlink .= $separator.$urlParameters;
518
    } else {
519
        $xlink .= DOKU_SCRIPT;
520
        if($urlParameters) $xlink .= '?'.$urlParameters;
521
    }
522
523
    return $xlink;
524
}
525
526
/**
527
 * This builds a link to an alternate page format
528
 *
529
 * Handles URL rewriting if enabled. Follows the style of wl().
530
 *
531
 * @author Ben Coburn <[email protected]>
532
 * @param string       $id             page id, defaults to start page
533
 * @param string       $format         the export renderer to use
534
 * @param string|array $urlParameters  URL parameters, associative array recommended
535
 * @param bool         $abs            request an absolute URL instead of relative
536
 * @param string       $sep            parameter separator
537
 * @return string
538
 */
539
function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;') {
540
    global $conf;
541
    if(is_array($urlParameters)) {
542
        $urlParameters = buildURLparams($urlParameters, $sep);
543
    } else {
544
        $urlParameters = str_replace(',', $sep, $urlParameters);
545
    }
546
547
    $format = rawurlencode($format);
548
    $id     = idfilter($id);
549
    if($abs) {
550
        $xlink = DOKU_URL;
551
    } else {
552
        $xlink = DOKU_BASE;
553
    }
554
555
    if($conf['userewrite'] == 2) {
556
        $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
557
        if($urlParameters) $xlink .= $sep.$urlParameters;
558
    } elseif($conf['userewrite'] == 1) {
559
        $xlink .= '_export/'.$format.'/'.$id;
560
        if($urlParameters) $xlink .= '?'.$urlParameters;
561
    } else {
562
        $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
563
        if($urlParameters) $xlink .= $sep.$urlParameters;
564
    }
565
566
    return $xlink;
567
}
568
569
/**
570
 * Build a link to a media file
571
 *
572
 * Will return a link to the detail page if $direct is false
573
 *
574
 * The $more parameter should always be given as array, the function then
575
 * will strip default parameters to produce even cleaner URLs
576
 *
577
 * @param string  $id     the media file id or URL
578
 * @param mixed   $more   string or array with additional parameters
579
 * @param bool    $direct link to detail page if false
580
 * @param string  $sep    URL parameter separator
581
 * @param bool    $abs    Create an absolute URL
582
 * @return string
583
 */
584
function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
585
    global $conf;
586
    $isexternalimage = media_isexternal($id);
587
    if(!$isexternalimage) {
588
        $id = cleanID($id);
589
    }
590
591
    if(is_array($more)) {
592
        // add token for resized images
593
        if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){
594
            $more['tok'] = media_get_token($id,$more['w'],$more['h']);
595
        }
596
        // strip defaults for shorter URLs
597
        if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
598
        if(empty($more['w'])) unset($more['w']);
599
        if(empty($more['h'])) unset($more['h']);
600
        if(isset($more['id']) && $direct) unset($more['id']);
601
        if(isset($more['rev']) && !$more['rev']) unset($more['rev']);
602
        $more = buildURLparams($more, $sep);
603
    } else {
604
        $matches = array();
605
        if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){
606
            $resize = array('w'=>0, 'h'=>0);
607
            foreach ($matches as $match){
608
                $resize[$match[1]] = $match[2];
609
            }
610
            $more .= $more === '' ? '' : $sep;
611
            $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']);
612
        }
613
        $more = str_replace('cache=cache', '', $more); //skip default
614
        $more = str_replace(',,', ',', $more);
615
        $more = str_replace(',', $sep, $more);
616
    }
617
618
    if($abs) {
619
        $xlink = DOKU_URL;
620
    } else {
621
        $xlink = DOKU_BASE;
622
    }
623
624
    // external URLs are always direct without rewriting
625
    if($isexternalimage) {
626
        $xlink .= 'lib/exe/fetch.php';
627
        $xlink .= '?'.$more;
628
        $xlink .= $sep.'media='.rawurlencode($id);
629
        return $xlink;
630
    }
631
632
    $id = idfilter($id);
633
634
    // decide on scriptname
635
    if($direct) {
636
        if($conf['userewrite'] == 1) {
637
            $script = '_media';
638
        } else {
639
            $script = 'lib/exe/fetch.php';
640
        }
641
    } else {
642
        if($conf['userewrite'] == 1) {
643
            $script = '_detail';
644
        } else {
645
            $script = 'lib/exe/detail.php';
646
        }
647
    }
648
649
    // build URL based on rewrite mode
650
    if($conf['userewrite']) {
651
        $xlink .= $script.'/'.$id;
652
        if($more) $xlink .= '?'.$more;
653
    } else {
654
        if($more) {
655
            $xlink .= $script.'?'.$more;
656
            $xlink .= $sep.'media='.$id;
657
        } else {
658
            $xlink .= $script.'?media='.$id;
659
        }
660
    }
661
662
    return $xlink;
663
}
664
665
/**
666
 * Returns the URL to the DokuWiki base script
667
 *
668
 * Consider using wl() instead, unless you absoutely need the doku.php endpoint
669
 *
670
 * @author Andreas Gohr <[email protected]>
671
 *
672
 * @return string
673
 */
674
function script() {
675
    return DOKU_BASE.DOKU_SCRIPT;
676
}
677
678
/**
679
 * Spamcheck against wordlist
680
 *
681
 * Checks the wikitext against a list of blocked expressions
682
 * returns true if the text contains any bad words
683
 *
684
 * Triggers COMMON_WORDBLOCK_BLOCKED
685
 *
686
 *  Action Plugins can use this event to inspect the blocked data
687
 *  and gain information about the user who was blocked.
688
 *
689
 *  Event data:
690
 *    data['matches']  - array of matches
691
 *    data['userinfo'] - information about the blocked user
692
 *      [ip]           - ip address
693
 *      [user]         - username (if logged in)
694
 *      [mail]         - mail address (if logged in)
695
 *      [name]         - real name (if logged in)
696
 *
697
 * @author Andreas Gohr <[email protected]>
698
 * @author Michael Klier <[email protected]>
699
 *
700
 * @param  string $text - optional text to check, if not given the globals are used
701
 * @return bool         - true if a spam word was found
702
 */
703
function checkwordblock($text = '') {
704
    global $TEXT;
705
    global $PRE;
706
    global $SUF;
707
    global $SUM;
708
    global $conf;
709
    global $INFO;
710
    /* @var Input $INPUT */
711
    global $INPUT;
712
713
    if(!$conf['usewordblock']) return false;
714
715
    if(!$text) $text = "$PRE $TEXT $SUF $SUM";
716
717
    // we prepare the text a tiny bit to prevent spammers circumventing URL checks
718
    // phpcs:disable Generic.Files.LineLength.TooLong
719
    $text = preg_replace(
720
        '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i',
721
        '\1http://\2 \2\3',
722
        $text
723
    );
724
    // phpcs:enable
725
726
    $wordblocks = getWordblocks();
727
    // how many lines to read at once (to work around some PCRE limits)
728
    if(version_compare(phpversion(), '4.3.0', '<')) {
729
        // old versions of PCRE define a maximum of parenthesises even if no
730
        // backreferences are used - the maximum is 99
731
        // this is very bad performancewise and may even be too high still
732
        $chunksize = 40;
733
    } else {
734
        // read file in chunks of 200 - this should work around the
735
        // MAX_PATTERN_SIZE in modern PCRE
736
        $chunksize = 200;
737
    }
738
    while($blocks = array_splice($wordblocks, 0, $chunksize)) {
739
        $re = array();
740
        // build regexp from blocks
741
        foreach($blocks as $block) {
742
            $block = preg_replace('/#.*$/', '', $block);
743
            $block = trim($block);
744
            if(empty($block)) continue;
745
            $re[] = $block;
746
        }
747
        if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) {
748
            // prepare event data
749
            $data = array();
750
            $data['matches']        = $matches;
751
            $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
752
            if($INPUT->server->str('REMOTE_USER')) {
753
                $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
754
                $data['userinfo']['name'] = $INFO['userinfo']['name'];
755
                $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
756
            }
757
            $callback = function () {
758
                return true;
759
            };
760
            return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
761
        }
762
    }
763
    return false;
764
}
765
766
/**
767
 * Return the IP of the client
768
 *
769
 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
770
 *
771
 * It returns a comma separated list of IPs if the above mentioned
772
 * headers are set. If the single parameter is set, it tries to return
773
 * a routable public address, prefering the ones suplied in the X
774
 * headers
775
 *
776
 * @author Andreas Gohr <[email protected]>
777
 *
778
 * @param  boolean $single If set only a single IP is returned
779
 * @return string
780
 */
781
function clientIP($single = false) {
782
    /* @var Input $INPUT */
783
    global $INPUT;
784
785
    $ip   = array();
786
    $ip[] = $INPUT->server->str('REMOTE_ADDR');
787
    if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
788
        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
789
    }
790
    if($INPUT->server->str('HTTP_X_REAL_IP')) {
791
        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
792
    }
793
794
    // some IPv4/v6 regexps borrowed from Feyd
795
    // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
796
    $dec_octet   = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
797
    $hex_digit   = '[A-Fa-f0-9]';
798
    $h16         = "{$hex_digit}{1,4}";
799
    $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
800
    $ls32        = "(?:$h16:$h16|$IPv4Address)";
801
    $IPv6Address =
802
        "(?:(?:{$IPv4Address})|(?:".
803
            "(?:$h16:){6}$ls32".
804
            "|::(?:$h16:){5}$ls32".
805
            "|(?:$h16)?::(?:$h16:){4}$ls32".
806
            "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32".
807
            "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32".
808
            "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32".
809
            "|(?:(?:$h16:){0,4}$h16)?::$ls32".
810
            "|(?:(?:$h16:){0,5}$h16)?::$h16".
811
            "|(?:(?:$h16:){0,6}$h16)?::".
812
            ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
813
814
    // remove any non-IP stuff
815
    $cnt   = count($ip);
816
    $match = array();
817
    for($i = 0; $i < $cnt; $i++) {
818
        if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) {
819
            $ip[$i] = $match[0];
820
        } else {
821
            $ip[$i] = '';
822
        }
823
        if(empty($ip[$i])) unset($ip[$i]);
824
    }
825
    $ip = array_values(array_unique($ip));
826
    if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
827
828
    if(!$single) return join(',', $ip);
829
830
    // decide which IP to use, trying to avoid local addresses
831
    $ip = array_reverse($ip);
832
    foreach($ip as $i) {
833
        if(preg_match('/^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/', $i)) {
834
            continue;
835
        } else {
836
            return $i;
837
        }
838
    }
839
    // still here? just use the first (last) address
840
    return $ip[0];
841
}
842
843
/**
844
 * Check if the browser is on a mobile device
845
 *
846
 * Adapted from the example code at url below
847
 *
848
 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
849
 *
850
 * @deprecated 2018-04-27 you probably want media queries instead anyway
851
 * @return bool if true, client is mobile browser; otherwise false
852
 */
853
function clientismobile() {
854
    /* @var Input $INPUT */
855
    global $INPUT;
856
857
    if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
858
859
    if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
860
861
    if(!$INPUT->server->has('HTTP_USER_AGENT')) return false;
862
863
    $uamatches = join(
864
        '|',
865
        [
866
            'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv',
867
            'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia',
868
            'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-',
869
            'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx',
870
            'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox',
871
            'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb',
872
            '\d\d\di', 'moto'
873
        ]
874
    );
875
876
    if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
877
878
    return false;
879
}
880
881
/**
882
 * check if a given link is interwiki link
883
 *
884
 * @param string $link the link, e.g. "wiki>page"
885
 * @return bool
886
 */
887
function link_isinterwiki($link){
888
    if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true;
889
    return false;
890
}
891
892
/**
893
 * Convert one or more comma separated IPs to hostnames
894
 *
895
 * If $conf['dnslookups'] is disabled it simply returns the input string
896
 *
897
 * @author Glen Harris <[email protected]>
898
 *
899
 * @param  string $ips comma separated list of IP addresses
900
 * @return string a comma separated list of hostnames
901
 */
902
function gethostsbyaddrs($ips) {
903
    global $conf;
904
    if(!$conf['dnslookups']) return $ips;
905
906
    $hosts = array();
907
    $ips   = explode(',', $ips);
908
909
    if(is_array($ips)) {
910
        foreach($ips as $ip) {
911
            $hosts[] = gethostbyaddr(trim($ip));
912
        }
913
        return join(',', $hosts);
914
    } else {
915
        return gethostbyaddr(trim($ips));
916
    }
917
}
918
919
/**
920
 * Checks if a given page is currently locked.
921
 *
922
 * removes stale lockfiles
923
 *
924
 * @author Andreas Gohr <[email protected]>
925
 *
926
 * @param string $id page id
927
 * @return bool page is locked?
928
 */
929
function checklock($id) {
930
    global $conf;
931
    /* @var Input $INPUT */
932
    global $INPUT;
933
934
    $lock = wikiLockFN($id);
935
936
    //no lockfile
937
    if(!file_exists($lock)) return false;
938
939
    //lockfile expired
940
    if((time() - filemtime($lock)) > $conf['locktime']) {
941
        @unlink($lock);
942
        return false;
943
    }
944
945
    //my own lock
946
    @list($ip, $session) = explode("\n", io_readFile($lock));
947
    if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) {
948
        return false;
949
    }
950
951
    return $ip;
952
}
953
954
/**
955
 * Lock a page for editing
956
 *
957
 * @author Andreas Gohr <[email protected]>
958
 *
959
 * @param string $id page id to lock
960
 */
961
function lock($id) {
962
    global $conf;
963
    /* @var Input $INPUT */
964
    global $INPUT;
965
966
    if($conf['locktime'] == 0) {
967
        return;
968
    }
969
970
    $lock = wikiLockFN($id);
971
    if($INPUT->server->str('REMOTE_USER')) {
972
        io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
973
    } else {
974
        io_saveFile($lock, clientIP()."\n".session_id());
975
    }
976
}
977
978
/**
979
 * Unlock a page if it was locked by the user
980
 *
981
 * @author Andreas Gohr <[email protected]>
982
 *
983
 * @param string $id page id to unlock
984
 * @return bool true if a lock was removed
985
 */
986
function unlock($id) {
987
    /* @var Input $INPUT */
988
    global $INPUT;
989
990
    $lock = wikiLockFN($id);
991
    if(file_exists($lock)) {
992
        @list($ip, $session) = explode("\n", io_readFile($lock));
993
        if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) {
994
            @unlink($lock);
995
            return true;
996
        }
997
    }
998
    return false;
999
}
1000
1001
/**
1002
 * convert line ending to unix format
1003
 *
1004
 * also makes sure the given text is valid UTF-8
1005
 *
1006
 * @see    formText() for 2crlf conversion
1007
 * @author Andreas Gohr <[email protected]>
1008
 *
1009
 * @param string $text
1010
 * @return string
1011
 */
1012
function cleanText($text) {
1013
    $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
1014
1015
    // if the text is not valid UTF-8 we simply assume latin1
1016
    // this won't break any worse than it breaks with the wrong encoding
1017
    // but might actually fix the problem in many cases
1018
    if(!utf8_check($text)) $text = utf8_encode($text);
1019
1020
    return $text;
1021
}
1022
1023
/**
1024
 * Prepares text for print in Webforms by encoding special chars.
1025
 * It also converts line endings to Windows format which is
1026
 * pseudo standard for webforms.
1027
 *
1028
 * @see    cleanText() for 2unix conversion
1029
 * @author Andreas Gohr <[email protected]>
1030
 *
1031
 * @param string $text
1032
 * @return string
1033
 */
1034
function formText($text) {
1035
    $text = str_replace("\012", "\015\012", $text);
1036
    return htmlspecialchars($text);
1037
}
1038
1039
/**
1040
 * Returns the specified local text in raw format
1041
 *
1042
 * @author Andreas Gohr <[email protected]>
1043
 *
1044
 * @param string $id   page id
1045
 * @param string $ext  extension of file being read, default 'txt'
1046
 * @return string
1047
 */
1048
function rawLocale($id, $ext = 'txt') {
1049
    return io_readFile(localeFN($id, $ext));
1050
}
1051
1052
/**
1053
 * Returns the raw WikiText
1054
 *
1055
 * @author Andreas Gohr <[email protected]>
1056
 *
1057
 * @param string $id   page id
1058
 * @param string|int $rev  timestamp when a revision of wikitext is desired
1059
 * @return string
1060
 */
1061
function rawWiki($id, $rev = '') {
1062
    return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1063
}
1064
1065
/**
1066
 * Returns the pagetemplate contents for the ID's namespace
1067
 *
1068
 * @triggers COMMON_PAGETPL_LOAD
1069
 * @author Andreas Gohr <[email protected]>
1070
 *
1071
 * @param string $id the id of the page to be created
1072
 * @return string parsed pagetemplate content
1073
 */
1074
function pageTemplate($id) {
1075
    global $conf;
1076
1077
    if(is_array($id)) $id = $id[0];
1078
1079
    // prepare initial event data
1080
    $data = array(
1081
        'id'        => $id, // the id of the page to be created
1082
        'tpl'       => '', // the text used as template
1083
        'tplfile'   => '', // the file above text was/should be loaded from
1084
        'doreplace' => true // should wildcard replacements be done on the text?
1085
    );
1086
1087
    $evt = new Doku_Event('COMMON_PAGETPL_LOAD', $data);
1088
    if($evt->advise_before(true)) {
1089
        // the before event might have loaded the content already
1090
        if(empty($data['tpl'])) {
1091
            // if the before event did not set a template file, try to find one
1092
            if(empty($data['tplfile'])) {
1093
                $path = dirname(wikiFN($id));
1094
                if(file_exists($path.'/_template.txt')) {
1095
                    $data['tplfile'] = $path.'/_template.txt';
1096
                } else {
1097
                    // search upper namespaces for templates
1098
                    $len = strlen(rtrim($conf['datadir'], '/'));
1099
                    while(strlen($path) >= $len) {
1100
                        if(file_exists($path.'/__template.txt')) {
1101
                            $data['tplfile'] = $path.'/__template.txt';
1102
                            break;
1103
                        }
1104
                        $path = substr($path, 0, strrpos($path, '/'));
1105
                    }
1106
                }
1107
            }
1108
            // load the content
1109
            $data['tpl'] = io_readFile($data['tplfile']);
1110
        }
1111
        if($data['doreplace']) parsePageTemplate($data);
1112
    }
1113
    $evt->advise_after();
1114
    unset($evt);
1115
1116
    return $data['tpl'];
1117
}
1118
1119
/**
1120
 * Performs common page template replacements
1121
 * This works on data from COMMON_PAGETPL_LOAD
1122
 *
1123
 * @author Andreas Gohr <[email protected]>
1124
 *
1125
 * @param array $data array with event data
1126
 * @return string
1127
 */
1128
function parsePageTemplate(&$data) {
1129
    /**
1130
     * @var string $id        the id of the page to be created
1131
     * @var string $tpl       the text used as template
1132
     * @var string $tplfile   the file above text was/should be loaded from
1133
     * @var bool   $doreplace should wildcard replacements be done on the text?
1134
     */
1135
    extract($data);
1136
1137
    global $USERINFO;
1138
    global $conf;
1139
    /* @var Input $INPUT */
1140
    global $INPUT;
1141
1142
    // replace placeholders
1143
    $file = noNS($id);
1144
    $page = strtr($file, $conf['sepchar'], ' ');
1145
1146
    $tpl = str_replace(
1147
        array(
1148
             '@ID@',
1149
             '@NS@',
1150
             '@CURNS@',
1151
             '@FILE@',
1152
             '@!FILE@',
1153
             '@!FILE!@',
1154
             '@PAGE@',
1155
             '@!PAGE@',
1156
             '@!!PAGE@',
1157
             '@!PAGE!@',
1158
             '@USER@',
1159
             '@NAME@',
1160
             '@MAIL@',
1161
             '@DATE@',
1162
        ),
1163
        array(
1164
             $id,
1165
             getNS($id),
1166
             curNS($id),
1167
             $file,
1168
             utf8_ucfirst($file),
1169
             utf8_strtoupper($file),
1170
             $page,
1171
             utf8_ucfirst($page),
1172
             utf8_ucwords($page),
1173
             utf8_strtoupper($page),
1174
             $INPUT->server->str('REMOTE_USER'),
1175
             $USERINFO['name'],
1176
             $USERINFO['mail'],
1177
             $conf['dformat'],
1178
        ), $tpl
1179
    );
1180
1181
    // we need the callback to work around strftime's char limit
1182
    $tpl = preg_replace_callback(
1183
        '/%./',
1184
        function ($m) {
1185
            return strftime($m[0]);
1186
        },
1187
        $tpl
1188
    );
1189
    $data['tpl'] = $tpl;
1190
    return $tpl;
1191
}
1192
1193
/**
1194
 * Returns the raw Wiki Text in three slices.
1195
 *
1196
 * The range parameter needs to have the form "from-to"
1197
 * and gives the range of the section in bytes - no
1198
 * UTF-8 awareness is needed.
1199
 * The returned order is prefix, section and suffix.
1200
 *
1201
 * @author Andreas Gohr <[email protected]>
1202
 *
1203
 * @param string $range in form "from-to"
1204
 * @param string $id    page id
1205
 * @param string $rev   optional, the revision timestamp
1206
 * @return string[] with three slices
1207
 */
1208
function rawWikiSlices($range, $id, $rev = '') {
1209
    $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1210
1211
    // Parse range
1212
    list($from, $to) = explode('-', $range, 2);
1213
    // Make range zero-based, use defaults if marker is missing
1214
    $from = !$from ? 0 : ($from - 1);
1215
    $to   = !$to ? strlen($text) : ($to - 1);
1216
1217
    $slices = array();
1218
    $slices[0] = substr($text, 0, $from);
1219
    $slices[1] = substr($text, $from, $to - $from);
1220
    $slices[2] = substr($text, $to);
1221
    return $slices;
1222
}
1223
1224
/**
1225
 * Joins wiki text slices
1226
 *
1227
 * function to join the text slices.
1228
 * When the pretty parameter is set to true it adds additional empty
1229
 * lines between sections if needed (used on saving).
1230
 *
1231
 * @author Andreas Gohr <[email protected]>
1232
 *
1233
 * @param string $pre   prefix
1234
 * @param string $text  text in the middle
1235
 * @param string $suf   suffix
1236
 * @param bool $pretty add additional empty lines between sections
1237
 * @return string
1238
 */
1239
function con($pre, $text, $suf, $pretty = false) {
1240
    if($pretty) {
1241
        if($pre !== '' && substr($pre, -1) !== "\n" &&
1242
            substr($text, 0, 1) !== "\n"
1243
        ) {
1244
            $pre .= "\n";
1245
        }
1246
        if($suf !== '' && substr($text, -1) !== "\n" &&
1247
            substr($suf, 0, 1) !== "\n"
1248
        ) {
1249
            $text .= "\n";
1250
        }
1251
    }
1252
1253
    return $pre.$text.$suf;
1254
}
1255
1256
/**
1257
 * Checks if the current page version is newer than the last entry in the page's
1258
 * changelog. If so, we assume it has been an external edit and we create an
1259
 * attic copy and add a proper changelog line.
1260
 *
1261
 * This check is only executed when the page is about to be saved again from the
1262
 * wiki, triggered in @see saveWikiText()
1263
 *
1264
 * @param string $id the page ID
1265
 */
1266
function detectExternalEdit($id) {
1267
    global $lang;
1268
1269
    $fileLastMod = wikiFN($id);
1270
    $lastMod     = @filemtime($fileLastMod); // from page
1271
    $pagelog     = new PageChangeLog($id, 1024);
1272
    $lastRev     = $pagelog->getRevisions(-1, 1); // from changelog
1273
    $lastRev     = (int) (empty($lastRev) ? 0 : $lastRev[0]);
1274
1275
    if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
1276
        // add old revision to the attic if missing
1277
        saveOldRevision($id);
1278
        // add a changelog entry if this edit came from outside dokuwiki
1279
        if($lastMod > $lastRev) {
1280
            $fileLastRev = wikiFN($id, $lastRev);
1281
            $revinfo = $pagelog->getRevisionInfo($lastRev);
1282
            if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
1283
                $filesize_old = 0;
1284
            } else {
1285
                $filesize_old = io_getSizeFile($fileLastRev);
1286
            }
1287
            $filesize_new = filesize($fileLastMod);
1288
            $sizechange = $filesize_new - $filesize_old;
1289
1290
            addLogEntry(
1291
                $lastMod,
1292
                $id,
1293
                DOKU_CHANGE_TYPE_EDIT,
1294
                $lang['external_edit'],
1295
                '',
1296
                array('ExternalEdit' => true),
1297
                $sizechange
1298
            );
1299
            // remove soon to be stale instructions
1300
            $cache = new CacheInstructions($id, $fileLastMod);
1301
            $cache->removeCache();
1302
        }
1303
    }
1304
}
1305
1306
/**
1307
 * Saves a wikitext by calling io_writeWikiPage.
1308
 * Also directs changelog and attic updates.
1309
 *
1310
 * @author Andreas Gohr <[email protected]>
1311
 * @author Ben Coburn <[email protected]>
1312
 *
1313
 * @param string $id       page id
1314
 * @param string $text     wikitext being saved
1315
 * @param string $summary  summary of text update
1316
 * @param bool   $minor    mark this saved version as minor update
1317
 */
1318
function saveWikiText($id, $text, $summary, $minor = false) {
1319
    /* Note to developers:
1320
       This code is subtle and delicate. Test the behavior of
1321
       the attic and changelog with dokuwiki and external edits
1322
       after any changes. External edits change the wiki page
1323
       directly without using php or dokuwiki.
1324
     */
1325
    global $conf;
1326
    global $lang;
1327
    global $REV;
1328
    /* @var Input $INPUT */
1329
    global $INPUT;
1330
1331
    // prepare data for event
1332
    $svdta = array();
1333
    $svdta['id']             = $id;
1334
    $svdta['file']           = wikiFN($id);
1335
    $svdta['revertFrom']     = $REV;
1336
    $svdta['oldRevision']    = @filemtime($svdta['file']);
1337
    $svdta['newRevision']    = 0;
1338
    $svdta['newContent']     = $text;
1339
    $svdta['oldContent']     = rawWiki($id);
1340
    $svdta['summary']        = $summary;
1341
    $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
1342
    $svdta['changeInfo']     = '';
1343
    $svdta['changeType']     = DOKU_CHANGE_TYPE_EDIT;
1344
    $svdta['sizechange']     = null;
1345
1346
    // select changelog line type
1347
    if($REV) {
1348
        $svdta['changeType']  = DOKU_CHANGE_TYPE_REVERT;
1349
        $svdta['changeInfo'] = $REV;
1350
    } else if(!file_exists($svdta['file'])) {
1351
        $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
1352
    } else if(trim($text) == '') {
1353
        // empty or whitespace only content deletes
1354
        $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
1355
        // autoset summary on deletion
1356
        if(blank($svdta['summary'])) {
1357
            $svdta['summary'] = $lang['deleted'];
1358
        }
1359
    } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
1360
        //minor edits only for logged in users
1361
        $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
1362
    }
1363
1364
    $event = new Doku_Event('COMMON_WIKIPAGE_SAVE', $svdta);
1365
    if(!$event->advise_before()) return;
1366
1367
    // if the content has not been changed, no save happens (plugins may override this)
1368
    if(!$svdta['contentChanged']) return;
1369
1370
    detectExternalEdit($id);
1371
1372
    if(
1373
        $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
1374
        ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
1375
    ) {
1376
        $filesize_old = 0;
1377
    } else {
1378
        $filesize_old = filesize($svdta['file']);
1379
    }
1380
    if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
1381
        // Send "update" event with empty data, so plugins can react to page deletion
1382
        $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
1383
        trigger_event('IO_WIKIPAGE_WRITE', $data);
1384
        // pre-save deleted revision
1385
        @touch($svdta['file']);
1386
        clearstatcache();
1387
        $svdta['newRevision'] = saveOldRevision($id);
1388
        // remove empty file
1389
        @unlink($svdta['file']);
1390
        $filesize_new = 0;
1391
        // don't remove old meta info as it should be saved, plugins can use
1392
        // IO_WIKIPAGE_WRITE for removing their metadata...
1393
        // purge non-persistant meta data
1394
        p_purge_metadata($id);
1395
        // remove empty namespaces
1396
        io_sweepNS($id, 'datadir');
1397
        io_sweepNS($id, 'mediadir');
1398
    } else {
1399
        // save file (namespace dir is created in io_writeWikiPage)
1400
        io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
1401
        // pre-save the revision, to keep the attic in sync
1402
        $svdta['newRevision'] = saveOldRevision($id);
1403
        $filesize_new = filesize($svdta['file']);
1404
    }
1405
    $svdta['sizechange'] = $filesize_new - $filesize_old;
1406
1407
    $event->advise_after();
1408
1409
    addLogEntry(
1410
        $svdta['newRevision'],
1411
        $svdta['id'],
1412
        $svdta['changeType'],
1413
        $svdta['summary'],
1414
        $svdta['changeInfo'],
1415
        null,
1416
        $svdta['sizechange']
1417
    );
1418
1419
    // send notify mails
1420
    notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor);
1421
    notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor);
1422
1423
    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1424
    io_saveFile($conf['cachedir'].'/purgefile', time());
1425
1426
    // if useheading is enabled, purge the cache of all linking pages
1427
    if(useHeading('content')) {
1428
        $pages = ft_backlinks($id, true);
1429
        foreach($pages as $page) {
1430
            $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
1431
            $cache->removeCache();
1432
        }
1433
    }
1434
}
1435
1436
/**
1437
 * moves the current version to the attic and returns its
1438
 * revision date
1439
 *
1440
 * @author Andreas Gohr <[email protected]>
1441
 *
1442
 * @param string $id page id
1443
 * @return int|string revision timestamp
1444
 */
1445
function saveOldRevision($id) {
1446
    $oldf = wikiFN($id);
1447
    if(!file_exists($oldf)) return '';
1448
    $date = filemtime($oldf);
1449
    $newf = wikiFN($id, $date);
1450
    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1451
    return $date;
1452
}
1453
1454
/**
1455
 * Sends a notify mail on page change or registration
1456
 *
1457
 * @param string     $id       The changed page
1458
 * @param string     $who      Who to notify (admin|subscribers|register)
1459
 * @param int|string $rev Old page revision
1460
 * @param string     $summary  What changed
1461
 * @param boolean    $minor    Is this a minor edit?
1462
 * @param string[]   $replace  Additional string substitutions, @KEY@ to be replaced by value
1463
 * @return bool
1464
 *
1465
 * @author Andreas Gohr <[email protected]>
1466
 */
1467
function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) {
1468
    global $conf;
1469
    /* @var Input $INPUT */
1470
    global $INPUT;
1471
1472
    // decide if there is something to do, eg. whom to mail
1473
    if($who == 'admin') {
1474
        if(empty($conf['notify'])) return false; //notify enabled?
1475
        $tpl = 'mailtext';
1476
        $to  = $conf['notify'];
1477
    } elseif($who == 'subscribers') {
1478
        if(!actionOK('subscribe')) return false; //subscribers enabled?
1479
        if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1480
        $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
1481
        trigger_event(
1482
            'COMMON_NOTIFY_ADDRESSLIST', $data,
1483
            array(new Subscription(), 'notifyaddresses')
0 ignored issues
show
Deprecated Code introduced by
The class Subscription has been deprecated with message: 2019-04-22 Use the classes in the \dokuwiki\Subscriptions namespace instead!

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
1484
        );
1485
        $to = $data['addresslist'];
1486
        if(empty($to)) return false;
1487
        $tpl = 'subscr_single';
1488
    } else {
1489
        return false; //just to be safe
1490
    }
1491
1492
    // prepare content
1493
    $subscription = new PageSubscriptionSender();
1494
    return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary);
1495
}
1496
1497
/**
1498
 * extracts the query from a search engine referrer
1499
 *
1500
 * @author Andreas Gohr <[email protected]>
1501
 * @author Todd Augsburger <[email protected]>
1502
 *
1503
 * @return array|string
1504
 */
1505
function getGoogleQuery() {
1506
    /* @var Input $INPUT */
1507
    global $INPUT;
1508
1509
    if(!$INPUT->server->has('HTTP_REFERER')) {
1510
        return '';
1511
    }
1512
    $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1513
1514
    // only handle common SEs
1515
    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1516
1517
    $query = array();
1518
    // temporary workaround against PHP bug #49733
1519
    // see http://bugs.php.net/bug.php?id=49733
1520
    if(UTF8_MBSTRING) $enc = mb_internal_encoding();
1521
    parse_str($url['query'], $query);
1522
    if(UTF8_MBSTRING) mb_internal_encoding($enc);
1523
1524
    $q = '';
1525
    if(isset($query['q'])){
1526
        $q = $query['q'];
1527
    }elseif(isset($query['p'])){
1528
        $q = $query['p'];
1529
    }elseif(isset($query['query'])){
1530
        $q = $query['query'];
1531
    }
1532
    $q = trim($q);
1533
1534
    if(!$q) return '';
1535
    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1536
    return $q;
1537
}
1538
1539
/**
1540
 * Return the human readable size of a file
1541
 *
1542
 * @param int $size A file size
1543
 * @param int $dec A number of decimal places
1544
 * @return string human readable size
1545
 *
1546
 * @author      Martin Benjamin <[email protected]>
1547
 * @author      Aidan Lister <[email protected]>
1548
 * @version     1.0.0
1549
 */
1550
function filesize_h($size, $dec = 1) {
1551
    $sizes = array('B', 'KB', 'MB', 'GB');
1552
    $count = count($sizes);
1553
    $i     = 0;
1554
1555
    while($size >= 1024 && ($i < $count - 1)) {
1556
        $size /= 1024;
1557
        $i++;
1558
    }
1559
1560
    return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space
1561
}
1562
1563
/**
1564
 * Return the given timestamp as human readable, fuzzy age
1565
 *
1566
 * @author Andreas Gohr <[email protected]>
1567
 *
1568
 * @param int $dt timestamp
1569
 * @return string
1570
 */
1571
function datetime_h($dt) {
1572
    global $lang;
1573
1574
    $ago = time() - $dt;
1575
    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1576
        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1577
    }
1578
    if($ago > 24 * 60 * 60 * 30 * 2) {
1579
        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1580
    }
1581
    if($ago > 24 * 60 * 60 * 7 * 2) {
1582
        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1583
    }
1584
    if($ago > 24 * 60 * 60 * 2) {
1585
        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1586
    }
1587
    if($ago > 60 * 60 * 2) {
1588
        return sprintf($lang['hours'], round($ago / (60 * 60)));
1589
    }
1590
    if($ago > 60 * 2) {
1591
        return sprintf($lang['minutes'], round($ago / (60)));
1592
    }
1593
    return sprintf($lang['seconds'], $ago);
1594
}
1595
1596
/**
1597
 * Wraps around strftime but provides support for fuzzy dates
1598
 *
1599
 * The format default to $conf['dformat']. It is passed to
1600
 * strftime - %f can be used to get the value from datetime_h()
1601
 *
1602
 * @see datetime_h
1603
 * @author Andreas Gohr <[email protected]>
1604
 *
1605
 * @param int|null $dt      timestamp when given, null will take current timestamp
1606
 * @param string   $format  empty default to $conf['dformat'], or provide format as recognized by strftime()
1607
 * @return string
1608
 */
1609
function dformat($dt = null, $format = '') {
1610
    global $conf;
1611
1612
    if(is_null($dt)) $dt = time();
1613
    $dt = (int) $dt;
1614
    if(!$format) $format = $conf['dformat'];
1615
1616
    $format = str_replace('%f', datetime_h($dt), $format);
1617
    return strftime($format, $dt);
1618
}
1619
1620
/**
1621
 * Formats a timestamp as ISO 8601 date
1622
 *
1623
 * @author <ungu at terong dot com>
1624
 * @link http://php.net/manual/en/function.date.php#54072
1625
 *
1626
 * @param int $int_date current date in UNIX timestamp
1627
 * @return string
1628
 */
1629
function date_iso8601($int_date) {
1630
    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1631
    $pre_timezone = date('O', $int_date);
1632
    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1633
    $date_mod .= $time_zone;
1634
    return $date_mod;
1635
}
1636
1637
/**
1638
 * return an obfuscated email address in line with $conf['mailguard'] setting
1639
 *
1640
 * @author Harry Fuecks <[email protected]>
1641
 * @author Christopher Smith <[email protected]>
1642
 *
1643
 * @param string $email email address
1644
 * @return string
1645
 */
1646
function obfuscate($email) {
1647
    global $conf;
1648
1649
    switch($conf['mailguard']) {
1650
        case 'visible' :
1651
            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1652
            return strtr($email, $obfuscate);
1653
1654
        case 'hex' :
1655
            $encode = '';
1656
            $len    = strlen($email);
1657
            for($x = 0; $x < $len; $x++) {
1658
                $encode .= '&#x'.bin2hex($email{$x}).';';
1659
            }
1660
            return $encode;
1661
1662
        case 'none' :
1663
        default :
1664
            return $email;
1665
    }
1666
}
1667
1668
/**
1669
 * Removes quoting backslashes
1670
 *
1671
 * @author Andreas Gohr <[email protected]>
1672
 *
1673
 * @param string $string
1674
 * @param string $char backslashed character
1675
 * @return string
1676
 */
1677
function unslash($string, $char = "'") {
1678
    return str_replace('\\'.$char, $char, $string);
1679
}
1680
1681
/**
1682
 * Convert php.ini shorthands to byte
1683
 *
1684
 * @author <gilthans dot NO dot SPAM at gmail dot com>
1685
 * @link   http://php.net/manual/en/ini.core.php#79564
1686
 *
1687
 * @param string $v shorthands
1688
 * @return int|string
1689
 */
1690
function php_to_byte($v) {
1691
    $l   = substr($v, -1);
1692
    $ret = substr($v, 0, -1);
1693
    switch(strtoupper($l)) {
1694
        /** @noinspection PhpMissingBreakStatementInspection */
1695
        // no-break
1696
        case 'P':
1697
            $ret *= 1024;
1698
        /** @noinspection PhpMissingBreakStatementInspection */
1699
        // no-break
1700
        case 'T':
1701
            $ret *= 1024;
1702
        /** @noinspection PhpMissingBreakStatementInspection */
1703
        // no-break
1704
        case 'G':
1705
            $ret *= 1024;
1706
        /** @noinspection PhpMissingBreakStatementInspection */
1707
        // no-break
1708
        case 'M':
1709
            $ret *= 1024;
1710
        /** @noinspection PhpMissingBreakStatementInspection */
1711
        // no-break
1712
        case 'K':
1713
            $ret *= 1024;
1714
            break;
1715
        default:
1716
            $ret *= 10;
1717
            break;
1718
    }
1719
    return $ret;
1720
}
1721
1722
/**
1723
 * Wrapper around preg_quote adding the default delimiter
1724
 *
1725
 * @param string $string
1726
 * @return string
1727
 */
1728
function preg_quote_cb($string) {
1729
    return preg_quote($string, '/');
1730
}
1731
1732
/**
1733
 * Shorten a given string by removing data from the middle
1734
 *
1735
 * You can give the string in two parts, the first part $keep
1736
 * will never be shortened. The second part $short will be cut
1737
 * in the middle to shorten but only if at least $min chars are
1738
 * left to display it. Otherwise it will be left off.
1739
 *
1740
 * @param string $keep   the part to keep
1741
 * @param string $short  the part to shorten
1742
 * @param int    $max    maximum chars you want for the whole string
1743
 * @param int    $min    minimum number of chars to have left for middle shortening
1744
 * @param string $char   the shortening character to use
1745
 * @return string
1746
 */
1747
function shorten($keep, $short, $max, $min = 9, $char = '…') {
1748
    $max = $max - utf8_strlen($keep);
1749
    if($max < $min) return $keep;
1750
    $len = utf8_strlen($short);
1751
    if($len <= $max) return $keep.$short;
1752
    $half = floor($max / 2);
1753
    return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half);
1754
}
1755
1756
/**
1757
 * Return the users real name or e-mail address for use
1758
 * in page footer and recent changes pages
1759
 *
1760
 * @param string|null $username or null when currently logged-in user should be used
1761
 * @param bool $textonly true returns only plain text, true allows returning html
1762
 * @return string html or plain text(not escaped) of formatted user name
1763
 *
1764
 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1765
 */
1766
function editorinfo($username, $textonly = false) {
1767
    return userlink($username, $textonly);
1768
}
1769
1770
/**
1771
 * Returns users realname w/o link
1772
 *
1773
 * @param string|null $username or null when currently logged-in user should be used
1774
 * @param bool $textonly true returns only plain text, true allows returning html
1775
 * @return string html or plain text(not escaped) of formatted user name
1776
 *
1777
 * @triggers COMMON_USER_LINK
1778
 */
1779
function userlink($username = null, $textonly = false) {
1780
    global $conf, $INFO;
1781
    /** @var DokuWiki_Auth_Plugin $auth */
1782
    global $auth;
1783
    /** @var Input $INPUT */
1784
    global $INPUT;
1785
1786
    // prepare initial event data
1787
    $data = array(
1788
        'username' => $username, // the unique user name
1789
        'name' => '',
1790
        'link' => array( //setting 'link' to false disables linking
1791
                         'target' => '',
1792
                         'pre' => '',
1793
                         'suf' => '',
1794
                         'style' => '',
1795
                         'more' => '',
1796
                         'url' => '',
1797
                         'title' => '',
1798
                         'class' => ''
1799
        ),
1800
        'userlink' => '', // formatted user name as will be returned
1801
        'textonly' => $textonly
1802
    );
1803
    if($username === null) {
1804
        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1805
        if($textonly){
1806
            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1807
        }else {
1808
            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '.
1809
                '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1810
        }
1811
    }
1812
1813
    $evt = new Doku_Event('COMMON_USER_LINK', $data);
1814
    if($evt->advise_before(true)) {
1815
        if(empty($data['name'])) {
1816
            if($auth) $info = $auth->getUserData($username);
1817
            if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1818
                switch($conf['showuseras']) {
1819
                    case 'username':
1820
                    case 'username_link':
1821
                        $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1822
                        break;
1823
                    case 'email':
1824
                    case 'email_link':
1825
                        $data['name'] = obfuscate($info['mail']);
1826
                        break;
1827
                }
1828
            } else {
1829
                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1830
            }
1831
        }
1832
1833
        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1834
        static $xhtml_renderer = null;
1835
1836
        if(!$data['textonly'] && empty($data['link']['url'])) {
1837
1838
            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1839
                if(!isset($info)) {
1840
                    if($auth) $info = $auth->getUserData($username);
1841
                }
1842
                if(isset($info) && $info) {
1843
                    if($conf['showuseras'] == 'email_link') {
1844
                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1845
                    } else {
1846
                        if(is_null($xhtml_renderer)) {
1847
                            $xhtml_renderer = p_get_renderer('xhtml');
1848
                        }
1849
                        if(empty($xhtml_renderer->interwiki)) {
1850
                            $xhtml_renderer->interwiki = getInterwiki();
1851
                        }
1852
                        $shortcut = 'user';
1853
                        $exists = null;
1854
                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1855
                        $data['link']['class'] .= ' interwiki iw_user';
1856
                        if($exists !== null) {
1857
                            if($exists) {
1858
                                $data['link']['class'] .= ' wikilink1';
1859
                            } else {
1860
                                $data['link']['class'] .= ' wikilink2';
1861
                                $data['link']['rel'] = 'nofollow';
1862
                            }
1863
                        }
1864
                    }
1865
                } else {
1866
                    $data['textonly'] = true;
1867
                }
1868
1869
            } else {
1870
                $data['textonly'] = true;
1871
            }
1872
        }
1873
1874
        if($data['textonly']) {
1875
            $data['userlink'] = $data['name'];
1876
        } else {
1877
            $data['link']['name'] = $data['name'];
1878
            if(is_null($xhtml_renderer)) {
1879
                $xhtml_renderer = p_get_renderer('xhtml');
1880
            }
1881
            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1882
        }
1883
    }
1884
    $evt->advise_after();
1885
    unset($evt);
1886
1887
    return $data['userlink'];
1888
}
1889
1890
/**
1891
 * Returns the path to a image file for the currently chosen license.
1892
 * When no image exists, returns an empty string
1893
 *
1894
 * @author Andreas Gohr <[email protected]>
1895
 *
1896
 * @param  string $type - type of image 'badge' or 'button'
1897
 * @return string
1898
 */
1899
function license_img($type) {
1900
    global $license;
1901
    global $conf;
1902
    if(!$conf['license']) return '';
1903
    if(!is_array($license[$conf['license']])) return '';
1904
    $try   = array();
1905
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1906
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1907
    if(substr($conf['license'], 0, 3) == 'cc-') {
1908
        $try[] = 'lib/images/license/'.$type.'/cc.png';
1909
    }
1910
    foreach($try as $src) {
1911
        if(file_exists(DOKU_INC.$src)) return $src;
1912
    }
1913
    return '';
1914
}
1915
1916
/**
1917
 * Checks if the given amount of memory is available
1918
 *
1919
 * If the memory_get_usage() function is not available the
1920
 * function just assumes $bytes of already allocated memory
1921
 *
1922
 * @author Filip Oscadal <[email protected]>
1923
 * @author Andreas Gohr <[email protected]>
1924
 *
1925
 * @param int  $mem    Size of memory you want to allocate in bytes
1926
 * @param int  $bytes  already allocated memory (see above)
1927
 * @return bool
1928
 */
1929
function is_mem_available($mem, $bytes = 1048576) {
1930
    $limit = trim(ini_get('memory_limit'));
1931
    if(empty($limit)) return true; // no limit set!
1932
    if($limit == -1) return true; // unlimited
1933
1934
    // parse limit to bytes
1935
    $limit = php_to_byte($limit);
1936
1937
    // get used memory if possible
1938
    if(function_exists('memory_get_usage')) {
1939
        $used = memory_get_usage();
1940
    } else {
1941
        $used = $bytes;
1942
    }
1943
1944
    if($used + $mem > $limit) {
1945
        return false;
1946
    }
1947
1948
    return true;
1949
}
1950
1951
/**
1952
 * Send a HTTP redirect to the browser
1953
 *
1954
 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1955
 *
1956
 * @link   http://support.microsoft.com/kb/q176113/
1957
 * @author Andreas Gohr <[email protected]>
1958
 *
1959
 * @param string $url url being directed to
1960
 */
1961
function send_redirect($url) {
1962
    $url = stripctl($url); // defend against HTTP Response Splitting
1963
1964
    /* @var Input $INPUT */
1965
    global $INPUT;
1966
1967
    //are there any undisplayed messages? keep them in session for display
1968
    global $MSG;
1969
    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1970
        //reopen session, store data and close session again
1971
        @session_start();
1972
        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1973
    }
1974
1975
    // always close the session
1976
    session_write_close();
1977
1978
    // check if running on IIS < 6 with CGI-PHP
1979
    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1980
        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1981
        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1982
        $matches[1] < 6
1983
    ) {
1984
        header('Refresh: 0;url='.$url);
1985
    } else {
1986
        header('Location: '.$url);
1987
    }
1988
1989
    // no exits during unit tests
1990
    if(defined('DOKU_UNITTEST')) {
1991
        // pass info about the redirect back to the test suite
1992
        $testRequest = TestRequest::getRunning();
1993
        if($testRequest !== null) {
1994
            $testRequest->addData('send_redirect', $url);
1995
        }
1996
        return;
1997
    }
1998
1999
    exit;
2000
}
2001
2002
/**
2003
 * Validate a value using a set of valid values
2004
 *
2005
 * This function checks whether a specified value is set and in the array
2006
 * $valid_values. If not, the function returns a default value or, if no
2007
 * default is specified, throws an exception.
2008
 *
2009
 * @param string $param        The name of the parameter
2010
 * @param array  $valid_values A set of valid values; Optionally a default may
2011
 *                             be marked by the key “default”.
2012
 * @param array  $array        The array containing the value (typically $_POST
2013
 *                             or $_GET)
2014
 * @param string $exc          The text of the raised exception
2015
 *
2016
 * @throws Exception
2017
 * @return mixed
2018
 * @author Adrian Lang <[email protected]>
2019
 */
2020
function valid_input_set($param, $valid_values, $array, $exc = '') {
2021
    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
2022
        return $array[$param];
2023
    } elseif(isset($valid_values['default'])) {
2024
        return $valid_values['default'];
2025
    } else {
2026
        throw new Exception($exc);
2027
    }
2028
}
2029
2030
/**
2031
 * Read a preference from the DokuWiki cookie
2032
 * (remembering both keys & values are urlencoded)
2033
 *
2034
 * @param string $pref     preference key
2035
 * @param mixed  $default  value returned when preference not found
2036
 * @return string preference value
2037
 */
2038
function get_doku_pref($pref, $default) {
2039
    $enc_pref = urlencode($pref);
2040
    if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
2041
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2042
        $cnt   = count($parts);
2043
2044
        // due to #2721 there might be duplicate entries,
2045
        // so we read from the end
2046
        for($i = $cnt-2; $i >= 0; $i -= 2) {
2047
            if($parts[$i] == $enc_pref) {
2048
                return urldecode($parts[$i + 1]);
2049
            }
2050
        }
2051
    }
2052
    return $default;
2053
}
2054
2055
/**
2056
 * Add a preference to the DokuWiki cookie
2057
 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
2058
 * Remove it by setting $val to false
2059
 *
2060
 * @param string $pref  preference key
2061
 * @param string $val   preference value
2062
 */
2063
function set_doku_pref($pref, $val) {
2064
    global $conf;
2065
    $orig = get_doku_pref($pref, false);
2066
    $cookieVal = '';
2067
2068
    if($orig !== false && ($orig !== $val)) {
2069
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2070
        $cnt   = count($parts);
2071
        // urlencode $pref for the comparison
2072
        $enc_pref = rawurlencode($pref);
2073
        $seen = false;
2074
        for ($i = 0; $i < $cnt; $i += 2) {
2075
            if ($parts[$i] == $enc_pref) {
2076
                if (!$seen){
2077
                    if ($val !== false) {
2078
                        $parts[$i + 1] = rawurlencode($val);
2079
                    } else {
2080
                        unset($parts[$i]);
2081
                        unset($parts[$i + 1]);
2082
                    }
2083
                    $seen = true;
2084
                } else {
2085
                    // no break because we want to remove duplicate entries
2086
                    unset($parts[$i]);
2087
                    unset($parts[$i + 1]);
2088
                }
2089
            }
2090
        }
2091
        $cookieVal = implode('#', $parts);
2092
    } else if ($orig === false && $val !== false) {
2093
        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'] . '#' : '') .
2094
            rawurlencode($pref) . '#' . rawurlencode($val);
2095
    }
2096
2097
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
2098
    if(defined('DOKU_UNITTEST')) {
2099
        $_COOKIE['DOKU_PREFS'] = $cookieVal;
2100
    }else{
2101
        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
2102
    }
2103
}
2104
2105
/**
2106
 * Strips source mapping declarations from given text #601
2107
 *
2108
 * @param string &$text reference to the CSS or JavaScript code to clean
2109
 */
2110
function stripsourcemaps(&$text){
2111
    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
2112
}
2113
2114
/**
2115
 * Returns the contents of a given SVG file for embedding
2116
 *
2117
 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through
2118
 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small
2119
 * files are embedded.
2120
 *
2121
 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG!
2122
 *
2123
 * @param string $file full path to the SVG file
2124
 * @param int $maxsize maximum allowed size for the SVG to be embedded
2125
 * @return string|false the SVG content, false if the file couldn't be loaded
2126
 */
2127
function inlineSVG($file, $maxsize = 2048) {
2128
    $file = trim($file);
2129
    if($file === '') return false;
2130
    if(!file_exists($file)) return false;
2131
    if(filesize($file) > $maxsize) return false;
2132
    if(!is_readable($file)) return false;
2133
    $content = file_get_contents($file);
2134
    $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments
2135
    $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header
2136
    $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type
2137
    $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags
2138
    $content = trim($content);
2139
    if(substr($content, 0, 5) !== '<svg ') return false;
2140
    return $content;
2141
}
2142
2143
//Setup VIM: ex: et ts=2 :
2144