Failed Conditions
Push — pr/3115 ( f9aa34 )
by Andreas
07:32 queued 04:11
created

inc/common.php (5 issues)

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

This function 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 function will be removed from the class and what other function to use instead.

Loading history...
1178
             utf8_ucwords(curNS($id)),
0 ignored issues
show
Deprecated Code introduced by
The function utf8_ucwords() has been deprecated with message: 2019-06-09

This function 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 function will be removed from the class and what other function to use instead.

Loading history...
1179
             utf8_strtoupper(curNS($id)),
0 ignored issues
show
Deprecated Code introduced by
The function utf8_strtoupper() has been deprecated with message: 2019-06-09

This function 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 function will be removed from the class and what other function to use instead.

Loading history...
1180
             $file,
1181
             \dokuwiki\Utf8\PhpString::ucfirst($file),
1182
             \dokuwiki\Utf8\PhpString::strtoupper($file),
1183
             $page,
1184
             \dokuwiki\Utf8\PhpString::ucfirst($page),
1185
             \dokuwiki\Utf8\PhpString::ucwords($page),
1186
             \dokuwiki\Utf8\PhpString::strtoupper($page),
1187
             $INPUT->server->str('REMOTE_USER'),
1188
             $USERINFO ? $USERINFO['name'] : '',
1189
             $USERINFO ? $USERINFO['mail'] : '',
1190
             $conf['dformat'],
1191
        ), $tpl
1192
    );
1193
1194
    // we need the callback to work around strftime's char limit
1195
    $tpl = preg_replace_callback(
1196
        '/%./',
1197
        function ($m) {
1198
            return strftime($m[0]);
1199
        },
1200
        $tpl
1201
    );
1202
    $data['tpl'] = $tpl;
1203
    return $tpl;
1204
}
1205
1206
/**
1207
 * Returns the raw Wiki Text in three slices.
1208
 *
1209
 * The range parameter needs to have the form "from-to"
1210
 * and gives the range of the section in bytes - no
1211
 * UTF-8 awareness is needed.
1212
 * The returned order is prefix, section and suffix.
1213
 *
1214
 * @author Andreas Gohr <[email protected]>
1215
 *
1216
 * @param string $range in form "from-to"
1217
 * @param string $id    page id
1218
 * @param string $rev   optional, the revision timestamp
1219
 * @return string[] with three slices
1220
 */
1221
function rawWikiSlices($range, $id, $rev = '') {
1222
    $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1223
1224
    // Parse range
1225
    list($from, $to) = explode('-', $range, 2);
1226
    // Make range zero-based, use defaults if marker is missing
1227
    $from = !$from ? 0 : ($from - 1);
1228
    $to   = !$to ? strlen($text) : ($to - 1);
1229
1230
    $slices = array();
1231
    $slices[0] = substr($text, 0, $from);
1232
    $slices[1] = substr($text, $from, $to - $from);
1233
    $slices[2] = substr($text, $to);
1234
    return $slices;
1235
}
1236
1237
/**
1238
 * Joins wiki text slices
1239
 *
1240
 * function to join the text slices.
1241
 * When the pretty parameter is set to true it adds additional empty
1242
 * lines between sections if needed (used on saving).
1243
 *
1244
 * @author Andreas Gohr <[email protected]>
1245
 *
1246
 * @param string $pre   prefix
1247
 * @param string $text  text in the middle
1248
 * @param string $suf   suffix
1249
 * @param bool $pretty add additional empty lines between sections
1250
 * @return string
1251
 */
1252
function con($pre, $text, $suf, $pretty = false) {
1253
    if($pretty) {
1254
        if($pre !== '' && substr($pre, -1) !== "\n" &&
1255
            substr($text, 0, 1) !== "\n"
1256
        ) {
1257
            $pre .= "\n";
1258
        }
1259
        if($suf !== '' && substr($text, -1) !== "\n" &&
1260
            substr($suf, 0, 1) !== "\n"
1261
        ) {
1262
            $text .= "\n";
1263
        }
1264
    }
1265
1266
    return $pre.$text.$suf;
1267
}
1268
1269
/**
1270
 * Checks if the current page version is newer than the last entry in the page's
1271
 * changelog. If so, we assume it has been an external edit and we create an
1272
 * attic copy and add a proper changelog line.
1273
 *
1274
 * This check is only executed when the page is about to be saved again from the
1275
 * wiki, triggered in @see saveWikiText()
1276
 *
1277
 * @param string $id the page ID
1278
 */
1279
function detectExternalEdit($id) {
1280
    global $lang;
1281
1282
    $fileLastMod = wikiFN($id);
1283
    $lastMod     = @filemtime($fileLastMod); // from page
1284
    $pagelog     = new PageChangeLog($id, 1024);
1285
    $lastRev     = $pagelog->getRevisions(-1, 1); // from changelog
1286
    $lastRev     = (int) (empty($lastRev) ? 0 : $lastRev[0]);
1287
1288
    if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
1289
        // add old revision to the attic if missing
1290
        saveOldRevision($id);
1291
        // add a changelog entry if this edit came from outside dokuwiki
1292
        if($lastMod > $lastRev) {
1293
            $fileLastRev = wikiFN($id, $lastRev);
1294
            $revinfo = $pagelog->getRevisionInfo($lastRev);
1295
            if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
1296
                $filesize_old = 0;
1297
            } else {
1298
                $filesize_old = io_getSizeFile($fileLastRev);
1299
            }
1300
            $filesize_new = filesize($fileLastMod);
1301
            $sizechange = $filesize_new - $filesize_old;
1302
1303
            addLogEntry(
1304
                $lastMod,
1305
                $id,
1306
                DOKU_CHANGE_TYPE_EDIT,
1307
                $lang['external_edit'],
1308
                '',
1309
                array('ExternalEdit' => true),
1310
                $sizechange
1311
            );
1312
            // remove soon to be stale instructions
1313
            $cache = new CacheInstructions($id, $fileLastMod);
1314
            $cache->removeCache();
1315
        }
1316
    }
1317
}
1318
1319
/**
1320
 * Saves a wikitext by calling io_writeWikiPage.
1321
 * Also directs changelog and attic updates.
1322
 *
1323
 * @author Andreas Gohr <[email protected]>
1324
 * @author Ben Coburn <[email protected]>
1325
 *
1326
 * @param string $id       page id
1327
 * @param string $text     wikitext being saved
1328
 * @param string $summary  summary of text update
1329
 * @param bool   $minor    mark this saved version as minor update
1330
 */
1331
function saveWikiText($id, $text, $summary, $minor = false) {
1332
    /* Note to developers:
1333
       This code is subtle and delicate. Test the behavior of
1334
       the attic and changelog with dokuwiki and external edits
1335
       after any changes. External edits change the wiki page
1336
       directly without using php or dokuwiki.
1337
     */
1338
    global $conf;
1339
    global $lang;
1340
    global $REV;
1341
    /* @var Input $INPUT */
1342
    global $INPUT;
1343
1344
    // prepare data for event
1345
    $svdta = array();
1346
    $svdta['id']             = $id;
1347
    $svdta['file']           = wikiFN($id);
1348
    $svdta['revertFrom']     = $REV;
1349
    $svdta['oldRevision']    = @filemtime($svdta['file']);
1350
    $svdta['newRevision']    = 0;
1351
    $svdta['newContent']     = $text;
1352
    $svdta['oldContent']     = rawWiki($id);
1353
    $svdta['summary']        = $summary;
1354
    $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
1355
    $svdta['changeInfo']     = '';
1356
    $svdta['changeType']     = DOKU_CHANGE_TYPE_EDIT;
1357
    $svdta['sizechange']     = null;
1358
1359
    // select changelog line type
1360
    if($REV) {
1361
        $svdta['changeType']  = DOKU_CHANGE_TYPE_REVERT;
1362
        $svdta['changeInfo'] = $REV;
1363
    } else if(!file_exists($svdta['file'])) {
1364
        $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
1365
    } else if(trim($text) == '') {
1366
        // empty or whitespace only content deletes
1367
        $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
1368
        // autoset summary on deletion
1369
        if(blank($svdta['summary'])) {
1370
            $svdta['summary'] = $lang['deleted'];
1371
        }
1372
    } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
1373
        //minor edits only for logged in users
1374
        $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
1375
    }
1376
1377
    $event = new Event('COMMON_WIKIPAGE_SAVE', $svdta);
1378
    if(!$event->advise_before()) return;
1379
1380
    // if the content has not been changed, no save happens (plugins may override this)
1381
    if(!$svdta['contentChanged']) return;
1382
1383
    detectExternalEdit($id);
1384
1385
    if(
1386
        $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
1387
        ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
1388
    ) {
1389
        $filesize_old = 0;
1390
    } else {
1391
        $filesize_old = filesize($svdta['file']);
1392
    }
1393
    if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
1394
        // Send "update" event with empty data, so plugins can react to page deletion
1395
        $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
1396
        Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data);
1397
        // pre-save deleted revision
1398
        @touch($svdta['file']);
1399
        clearstatcache();
1400
        $svdta['newRevision'] = saveOldRevision($id);
1401
        // remove empty file
1402
        @unlink($svdta['file']);
1403
        $filesize_new = 0;
1404
        // don't remove old meta info as it should be saved, plugins can use
1405
        // IO_WIKIPAGE_WRITE for removing their metadata...
1406
        // purge non-persistant meta data
1407
        p_purge_metadata($id);
1408
        // remove empty namespaces
1409
        io_sweepNS($id, 'datadir');
1410
        io_sweepNS($id, 'mediadir');
1411
    } else {
1412
        // save file (namespace dir is created in io_writeWikiPage)
1413
        io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
1414
        // pre-save the revision, to keep the attic in sync
1415
        $svdta['newRevision'] = saveOldRevision($id);
1416
        $filesize_new = filesize($svdta['file']);
1417
    }
1418
    $svdta['sizechange'] = $filesize_new - $filesize_old;
1419
1420
    $event->advise_after();
1421
1422
    addLogEntry(
1423
        $svdta['newRevision'],
1424
        $svdta['id'],
1425
        $svdta['changeType'],
1426
        $svdta['summary'],
1427
        $svdta['changeInfo'],
1428
        null,
1429
        $svdta['sizechange']
1430
    );
1431
1432
    // send notify mails
1433
    notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
1434
    notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
1435
1436
    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1437
    io_saveFile($conf['cachedir'].'/purgefile', time());
1438
1439
    // if useheading is enabled, purge the cache of all linking pages
1440
    if(useHeading('content')) {
1441
        $pages = ft_backlinks($id, true);
1442
        foreach($pages as $page) {
1443
            $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
1444
            $cache->removeCache();
1445
        }
1446
    }
1447
}
1448
1449
/**
1450
 * moves the current version to the attic and returns its
1451
 * revision date
1452
 *
1453
 * @author Andreas Gohr <[email protected]>
1454
 *
1455
 * @param string $id page id
1456
 * @return int|string revision timestamp
1457
 */
1458
function saveOldRevision($id) {
1459
    $oldf = wikiFN($id);
1460
    if(!file_exists($oldf)) return '';
1461
    $date = filemtime($oldf);
1462
    $newf = wikiFN($id, $date);
1463
    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1464
    return $date;
1465
}
1466
1467
/**
1468
 * Sends a notify mail on page change or registration
1469
 *
1470
 * @param string     $id       The changed page
1471
 * @param string     $who      Who to notify (admin|subscribers|register)
1472
 * @param int|string $rev Old page revision
1473
 * @param string     $summary  What changed
1474
 * @param boolean    $minor    Is this a minor edit?
1475
 * @param string[]   $replace  Additional string substitutions, @KEY@ to be replaced by value
1476
 * @param int|string $current_rev  New page revision
1477
 * @return bool
1478
 *
1479
 * @author Andreas Gohr <[email protected]>
1480
 */
1481
function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) {
1482
    global $conf;
1483
    /* @var Input $INPUT */
1484
    global $INPUT;
1485
1486
    // decide if there is something to do, eg. whom to mail
1487
    if($who == 'admin') {
1488
        if(empty($conf['notify'])) return false; //notify enabled?
1489
        $tpl = 'mailtext';
1490
        $to  = $conf['notify'];
1491
    } elseif($who == 'subscribers') {
1492
        if(!actionOK('subscribe')) return false; //subscribers enabled?
1493
        if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1494
        $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
1495
        Event::createAndTrigger(
1496
            'COMMON_NOTIFY_ADDRESSLIST', $data,
1497
            array(new Subscription(), 'notifyaddresses')
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...
1498
        );
1499
        $to = $data['addresslist'];
1500
        if(empty($to)) return false;
1501
        $tpl = 'subscr_single';
1502
    } else {
1503
        return false; //just to be safe
1504
    }
1505
1506
    // prepare content
1507
    $subscription = new PageSubscriptionSender();
1508
    return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev);
1509
}
1510
1511
/**
1512
 * extracts the query from a search engine referrer
1513
 *
1514
 * @author Andreas Gohr <[email protected]>
1515
 * @author Todd Augsburger <[email protected]>
1516
 *
1517
 * @return array|string
1518
 */
1519
function getGoogleQuery() {
1520
    /* @var Input $INPUT */
1521
    global $INPUT;
1522
1523
    if(!$INPUT->server->has('HTTP_REFERER')) {
1524
        return '';
1525
    }
1526
    $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1527
1528
    // only handle common SEs
1529
    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1530
1531
    $query = array();
1532
    parse_str($url['query'], $query);
1533
1534
    $q = '';
1535
    if(isset($query['q'])){
1536
        $q = $query['q'];
1537
    }elseif(isset($query['p'])){
1538
        $q = $query['p'];
1539
    }elseif(isset($query['query'])){
1540
        $q = $query['query'];
1541
    }
1542
    $q = trim($q);
1543
1544
    if(!$q) return '';
1545
    // ignore if query includes a full URL
1546
    if(strpos($q, '//') !== false) return '';
1547
    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1548
    return $q;
1549
}
1550
1551
/**
1552
 * Return the human readable size of a file
1553
 *
1554
 * @param int $size A file size
1555
 * @param int $dec A number of decimal places
1556
 * @return string human readable size
1557
 *
1558
 * @author      Martin Benjamin <[email protected]>
1559
 * @author      Aidan Lister <[email protected]>
1560
 * @version     1.0.0
1561
 */
1562
function filesize_h($size, $dec = 1) {
1563
    $sizes = array('B', 'KB', 'MB', 'GB');
1564
    $count = count($sizes);
1565
    $i     = 0;
1566
1567
    while($size >= 1024 && ($i < $count - 1)) {
1568
        $size /= 1024;
1569
        $i++;
1570
    }
1571
1572
    return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space
1573
}
1574
1575
/**
1576
 * Return the given timestamp as human readable, fuzzy age
1577
 *
1578
 * @author Andreas Gohr <[email protected]>
1579
 *
1580
 * @param int $dt timestamp
1581
 * @return string
1582
 */
1583
function datetime_h($dt) {
1584
    global $lang;
1585
1586
    $ago = time() - $dt;
1587
    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1588
        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1589
    }
1590
    if($ago > 24 * 60 * 60 * 30 * 2) {
1591
        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1592
    }
1593
    if($ago > 24 * 60 * 60 * 7 * 2) {
1594
        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1595
    }
1596
    if($ago > 24 * 60 * 60 * 2) {
1597
        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1598
    }
1599
    if($ago > 60 * 60 * 2) {
1600
        return sprintf($lang['hours'], round($ago / (60 * 60)));
1601
    }
1602
    if($ago > 60 * 2) {
1603
        return sprintf($lang['minutes'], round($ago / (60)));
1604
    }
1605
    return sprintf($lang['seconds'], $ago);
1606
}
1607
1608
/**
1609
 * Wraps around strftime but provides support for fuzzy dates
1610
 *
1611
 * The format default to $conf['dformat']. It is passed to
1612
 * strftime - %f can be used to get the value from datetime_h()
1613
 *
1614
 * @see datetime_h
1615
 * @author Andreas Gohr <[email protected]>
1616
 *
1617
 * @param int|null $dt      timestamp when given, null will take current timestamp
1618
 * @param string   $format  empty default to $conf['dformat'], or provide format as recognized by strftime()
1619
 * @return string
1620
 */
1621
function dformat($dt = null, $format = '') {
1622
    global $conf;
1623
1624
    if(is_null($dt)) $dt = time();
1625
    $dt = (int) $dt;
1626
    if(!$format) $format = $conf['dformat'];
1627
1628
    $format = str_replace('%f', datetime_h($dt), $format);
1629
    return strftime($format, $dt);
1630
}
1631
1632
/**
1633
 * Formats a timestamp as ISO 8601 date
1634
 *
1635
 * @author <ungu at terong dot com>
1636
 * @link http://php.net/manual/en/function.date.php#54072
1637
 *
1638
 * @param int $int_date current date in UNIX timestamp
1639
 * @return string
1640
 */
1641
function date_iso8601($int_date) {
1642
    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1643
    $pre_timezone = date('O', $int_date);
1644
    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1645
    $date_mod .= $time_zone;
1646
    return $date_mod;
1647
}
1648
1649
/**
1650
 * return an obfuscated email address in line with $conf['mailguard'] setting
1651
 *
1652
 * @author Harry Fuecks <[email protected]>
1653
 * @author Christopher Smith <[email protected]>
1654
 *
1655
 * @param string $email email address
1656
 * @return string
1657
 */
1658
function obfuscate($email) {
1659
    global $conf;
1660
1661
    switch($conf['mailguard']) {
1662
        case 'visible' :
1663
            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1664
            return strtr($email, $obfuscate);
1665
1666
        case 'hex' :
1667
            return utf8_tohtml($email, true);
0 ignored issues
show
Deprecated Code introduced by
The function utf8_tohtml() has been deprecated with message: 2019-06-09

This function 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 function will be removed from the class and what other function to use instead.

Loading history...
1668
1669
        case 'none' :
1670
        default :
1671
            return $email;
1672
    }
1673
}
1674
1675
/**
1676
 * Removes quoting backslashes
1677
 *
1678
 * @author Andreas Gohr <[email protected]>
1679
 *
1680
 * @param string $string
1681
 * @param string $char backslashed character
1682
 * @return string
1683
 */
1684
function unslash($string, $char = "'") {
1685
    return str_replace('\\'.$char, $char, $string);
1686
}
1687
1688
/**
1689
 * Convert php.ini shorthands to byte
1690
 *
1691
 * On 32 bit systems values >= 2GB will fail!
1692
 *
1693
 * -1 (infinite size) will be reported as -1
1694
 *
1695
 * @link   https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1696
 * @param string $value PHP size shorthand
1697
 * @return int
1698
 */
1699
function php_to_byte($value) {
1700
    switch (strtoupper(substr($value,-1))) {
1701
        case 'G':
1702
            $ret = intval(substr($value, 0, -1)) * 1024 * 1024 * 1024;
1703
            break;
1704
        case 'M':
1705
            $ret = intval(substr($value, 0, -1)) * 1024 * 1024;
1706
            break;
1707
        case 'K':
1708
            $ret = intval(substr($value, 0, -1)) * 1024;
1709
            break;
1710
        default:
1711
            $ret = intval($value);
1712
            break;
1713
    }
1714
    return $ret;
1715
}
1716
1717
/**
1718
 * Wrapper around preg_quote adding the default delimiter
1719
 *
1720
 * @param string $string
1721
 * @return string
1722
 */
1723
function preg_quote_cb($string) {
1724
    return preg_quote($string, '/');
1725
}
1726
1727
/**
1728
 * Shorten a given string by removing data from the middle
1729
 *
1730
 * You can give the string in two parts, the first part $keep
1731
 * will never be shortened. The second part $short will be cut
1732
 * in the middle to shorten but only if at least $min chars are
1733
 * left to display it. Otherwise it will be left off.
1734
 *
1735
 * @param string $keep   the part to keep
1736
 * @param string $short  the part to shorten
1737
 * @param int    $max    maximum chars you want for the whole string
1738
 * @param int    $min    minimum number of chars to have left for middle shortening
1739
 * @param string $char   the shortening character to use
1740
 * @return string
1741
 */
1742
function shorten($keep, $short, $max, $min = 9, $char = '…') {
1743
    $max = $max - \dokuwiki\Utf8\PhpString::strlen($keep);
1744
    if($max < $min) return $keep;
1745
    $len = \dokuwiki\Utf8\PhpString::strlen($short);
1746
    if($len <= $max) return $keep.$short;
1747
    $half = floor($max / 2);
1748
    return $keep .
1749
        \dokuwiki\Utf8\PhpString::substr($short, 0, $half - 1) .
1750
        $char .
1751
        \dokuwiki\Utf8\PhpString::substr($short, $len - $half);
1752
}
1753
1754
/**
1755
 * Return the users real name or e-mail address for use
1756
 * in page footer and recent changes pages
1757
 *
1758
 * @param string|null $username or null when currently logged-in user should be used
1759
 * @param bool $textonly true returns only plain text, true allows returning html
1760
 * @return string html or plain text(not escaped) of formatted user name
1761
 *
1762
 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1763
 */
1764
function editorinfo($username, $textonly = false) {
1765
    return userlink($username, $textonly);
1766
}
1767
1768
/**
1769
 * Returns users realname w/o link
1770
 *
1771
 * @param string|null $username or null when currently logged-in user should be used
1772
 * @param bool $textonly true returns only plain text, true allows returning html
1773
 * @return string html or plain text(not escaped) of formatted user name
1774
 *
1775
 * @triggers COMMON_USER_LINK
1776
 */
1777
function userlink($username = null, $textonly = false) {
1778
    global $conf, $INFO;
1779
    /** @var AuthPlugin $auth */
1780
    global $auth;
1781
    /** @var Input $INPUT */
1782
    global $INPUT;
1783
1784
    // prepare initial event data
1785
    $data = array(
1786
        'username' => $username, // the unique user name
1787
        'name' => '',
1788
        'link' => array( //setting 'link' to false disables linking
1789
                         'target' => '',
1790
                         'pre' => '',
1791
                         'suf' => '',
1792
                         'style' => '',
1793
                         'more' => '',
1794
                         'url' => '',
1795
                         'title' => '',
1796
                         'class' => ''
1797
        ),
1798
        'userlink' => '', // formatted user name as will be returned
1799
        'textonly' => $textonly
1800
    );
1801
    if($username === null) {
1802
        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1803
        if($textonly){
1804
            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1805
        }else {
1806
            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '.
1807
                '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1808
        }
1809
    }
1810
1811
    $evt = new Event('COMMON_USER_LINK', $data);
1812
    if($evt->advise_before(true)) {
1813
        if(empty($data['name'])) {
1814
            if($auth) $info = $auth->getUserData($username);
1815
            if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1816
                switch($conf['showuseras']) {
1817
                    case 'username':
1818
                    case 'username_link':
1819
                        $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1820
                        break;
1821
                    case 'email':
1822
                    case 'email_link':
1823
                        $data['name'] = obfuscate($info['mail']);
1824
                        break;
1825
                }
1826
            } else {
1827
                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1828
            }
1829
        }
1830
1831
        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1832
        static $xhtml_renderer = null;
1833
1834
        if(!$data['textonly'] && empty($data['link']['url'])) {
1835
1836
            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1837
                if(!isset($info)) {
1838
                    if($auth) $info = $auth->getUserData($username);
1839
                }
1840
                if(isset($info) && $info) {
1841
                    if($conf['showuseras'] == 'email_link') {
1842
                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1843
                    } else {
1844
                        if(is_null($xhtml_renderer)) {
1845
                            $xhtml_renderer = p_get_renderer('xhtml');
1846
                        }
1847
                        if(empty($xhtml_renderer->interwiki)) {
1848
                            $xhtml_renderer->interwiki = getInterwiki();
1849
                        }
1850
                        $shortcut = 'user';
1851
                        $exists = null;
1852
                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1853
                        $data['link']['class'] .= ' interwiki iw_user';
1854
                        if($exists !== null) {
1855
                            if($exists) {
1856
                                $data['link']['class'] .= ' wikilink1';
1857
                            } else {
1858
                                $data['link']['class'] .= ' wikilink2';
1859
                                $data['link']['rel'] = 'nofollow';
1860
                            }
1861
                        }
1862
                    }
1863
                } else {
1864
                    $data['textonly'] = true;
1865
                }
1866
1867
            } else {
1868
                $data['textonly'] = true;
1869
            }
1870
        }
1871
1872
        if($data['textonly']) {
1873
            $data['userlink'] = $data['name'];
1874
        } else {
1875
            $data['link']['name'] = $data['name'];
1876
            if(is_null($xhtml_renderer)) {
1877
                $xhtml_renderer = p_get_renderer('xhtml');
1878
            }
1879
            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1880
        }
1881
    }
1882
    $evt->advise_after();
1883
    unset($evt);
1884
1885
    return $data['userlink'];
1886
}
1887
1888
/**
1889
 * Returns the path to a image file for the currently chosen license.
1890
 * When no image exists, returns an empty string
1891
 *
1892
 * @author Andreas Gohr <[email protected]>
1893
 *
1894
 * @param  string $type - type of image 'badge' or 'button'
1895
 * @return string
1896
 */
1897
function license_img($type) {
1898
    global $license;
1899
    global $conf;
1900
    if(!$conf['license']) return '';
1901
    if(!is_array($license[$conf['license']])) return '';
1902
    $try   = array();
1903
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1904
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1905
    if(substr($conf['license'], 0, 3) == 'cc-') {
1906
        $try[] = 'lib/images/license/'.$type.'/cc.png';
1907
    }
1908
    foreach($try as $src) {
1909
        if(file_exists(DOKU_INC.$src)) return $src;
1910
    }
1911
    return '';
1912
}
1913
1914
/**
1915
 * Checks if the given amount of memory is available
1916
 *
1917
 * If the memory_get_usage() function is not available the
1918
 * function just assumes $bytes of already allocated memory
1919
 *
1920
 * @author Filip Oscadal <[email protected]>
1921
 * @author Andreas Gohr <[email protected]>
1922
 *
1923
 * @param int  $mem    Size of memory you want to allocate in bytes
1924
 * @param int  $bytes  already allocated memory (see above)
1925
 * @return bool
1926
 */
1927
function is_mem_available($mem, $bytes = 1048576) {
1928
    $limit = trim(ini_get('memory_limit'));
1929
    if(empty($limit)) return true; // no limit set!
1930
    if($limit == -1) return true; // unlimited
1931
1932
    // parse limit to bytes
1933
    $limit = php_to_byte($limit);
1934
1935
    // get used memory if possible
1936
    if(function_exists('memory_get_usage')) {
1937
        $used = memory_get_usage();
1938
    } else {
1939
        $used = $bytes;
1940
    }
1941
1942
    if($used + $mem > $limit) {
1943
        return false;
1944
    }
1945
1946
    return true;
1947
}
1948
1949
/**
1950
 * Send a HTTP redirect to the browser
1951
 *
1952
 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1953
 *
1954
 * @link   http://support.microsoft.com/kb/q176113/
1955
 * @author Andreas Gohr <[email protected]>
1956
 *
1957
 * @param string $url url being directed to
1958
 */
1959
function send_redirect($url) {
1960
    $url = stripctl($url); // defend against HTTP Response Splitting
1961
1962
    /* @var Input $INPUT */
1963
    global $INPUT;
1964
1965
    //are there any undisplayed messages? keep them in session for display
1966
    global $MSG;
1967
    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1968
        //reopen session, store data and close session again
1969
        @session_start();
1970
        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1971
    }
1972
1973
    // always close the session
1974
    session_write_close();
1975
1976
    // check if running on IIS < 6 with CGI-PHP
1977
    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1978
        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1979
        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1980
        $matches[1] < 6
1981
    ) {
1982
        header('Refresh: 0;url='.$url);
1983
    } else {
1984
        header('Location: '.$url);
1985
    }
1986
1987
    // no exits during unit tests
1988
    if(defined('DOKU_UNITTEST')) {
1989
        // pass info about the redirect back to the test suite
1990
        $testRequest = TestRequest::getRunning();
1991
        if($testRequest !== null) {
1992
            $testRequest->addData('send_redirect', $url);
1993
        }
1994
        return;
1995
    }
1996
1997
    exit;
1998
}
1999
2000
/**
2001
 * Validate a value using a set of valid values
2002
 *
2003
 * This function checks whether a specified value is set and in the array
2004
 * $valid_values. If not, the function returns a default value or, if no
2005
 * default is specified, throws an exception.
2006
 *
2007
 * @param string $param        The name of the parameter
2008
 * @param array  $valid_values A set of valid values; Optionally a default may
2009
 *                             be marked by the key “default”.
2010
 * @param array  $array        The array containing the value (typically $_POST
2011
 *                             or $_GET)
2012
 * @param string $exc          The text of the raised exception
2013
 *
2014
 * @throws Exception
2015
 * @return mixed
2016
 * @author Adrian Lang <[email protected]>
2017
 */
2018
function valid_input_set($param, $valid_values, $array, $exc = '') {
2019
    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
2020
        return $array[$param];
2021
    } elseif(isset($valid_values['default'])) {
2022
        return $valid_values['default'];
2023
    } else {
2024
        throw new Exception($exc);
2025
    }
2026
}
2027
2028
/**
2029
 * Read a preference from the DokuWiki cookie
2030
 * (remembering both keys & values are urlencoded)
2031
 *
2032
 * @param string $pref     preference key
2033
 * @param mixed  $default  value returned when preference not found
2034
 * @return string preference value
2035
 */
2036
function get_doku_pref($pref, $default) {
2037
    $enc_pref = urlencode($pref);
2038
    if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
2039
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2040
        $cnt   = count($parts);
2041
2042
        // due to #2721 there might be duplicate entries,
2043
        // so we read from the end
2044
        for($i = $cnt-2; $i >= 0; $i -= 2) {
2045
            if($parts[$i] == $enc_pref) {
2046
                return urldecode($parts[$i + 1]);
2047
            }
2048
        }
2049
    }
2050
    return $default;
2051
}
2052
2053
/**
2054
 * Add a preference to the DokuWiki cookie
2055
 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
2056
 * Remove it by setting $val to false
2057
 *
2058
 * @param string $pref  preference key
2059
 * @param string $val   preference value
2060
 */
2061
function set_doku_pref($pref, $val) {
2062
    global $conf;
2063
    $orig = get_doku_pref($pref, false);
2064
    $cookieVal = '';
2065
2066
    if($orig !== false && ($orig !== $val)) {
2067
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2068
        $cnt   = count($parts);
2069
        // urlencode $pref for the comparison
2070
        $enc_pref = rawurlencode($pref);
2071
        $seen = false;
2072
        for ($i = 0; $i < $cnt; $i += 2) {
2073
            if ($parts[$i] == $enc_pref) {
2074
                if (!$seen){
2075
                    if ($val !== false) {
2076
                        $parts[$i + 1] = rawurlencode($val);
2077
                    } else {
2078
                        unset($parts[$i]);
2079
                        unset($parts[$i + 1]);
2080
                    }
2081
                    $seen = true;
2082
                } else {
2083
                    // no break because we want to remove duplicate entries
2084
                    unset($parts[$i]);
2085
                    unset($parts[$i + 1]);
2086
                }
2087
            }
2088
        }
2089
        $cookieVal = implode('#', $parts);
2090
    } else if ($orig === false && $val !== false) {
2091
        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'] . '#' : '') .
2092
            rawurlencode($pref) . '#' . rawurlencode($val);
2093
    }
2094
2095
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
2096
    if(defined('DOKU_UNITTEST')) {
2097
        $_COOKIE['DOKU_PREFS'] = $cookieVal;
2098
    }else{
2099
        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
2100
    }
2101
}
2102
2103
/**
2104
 * Strips source mapping declarations from given text #601
2105
 *
2106
 * @param string &$text reference to the CSS or JavaScript code to clean
2107
 */
2108
function stripsourcemaps(&$text){
2109
    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
2110
}
2111
2112
/**
2113
 * Returns the contents of a given SVG file for embedding
2114
 *
2115
 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through
2116
 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small
2117
 * files are embedded.
2118
 *
2119
 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG!
2120
 *
2121
 * @param string $file full path to the SVG file
2122
 * @param int $maxsize maximum allowed size for the SVG to be embedded
2123
 * @return string|false the SVG content, false if the file couldn't be loaded
2124
 */
2125
function inlineSVG($file, $maxsize = 2048) {
2126
    $file = trim($file);
2127
    if($file === '') return false;
2128
    if(!file_exists($file)) return false;
2129
    if(filesize($file) > $maxsize) return false;
2130
    if(!is_readable($file)) return false;
2131
    $content = file_get_contents($file);
2132
    $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments
2133
    $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header
2134
    $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type
2135
    $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags
2136
    $content = trim($content);
2137
    if(substr($content, 0, 5) !== '<svg ') return false;
2138
    return $content;
2139
}
2140
2141
//Setup VIM: ex: et ts=2 :
2142