Completed
Pull Request — master (#3270)
by
unknown
03:00
created

common.php ➔ clientIP()   B

Complexity

Conditions 10
Paths 96

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 96
nop 1
dl 0
loc 37
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PageChangeLog.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
12
use dokuwiki\Subscriptions\PageSubscriptionSender;
13
use dokuwiki\Subscriptions\SubscriberManager;
14
use dokuwiki\Extension\AuthPlugin;
15
use dokuwiki\Extension\Event;
16
17
/**
18
 * Wrapper around htmlspecialchars()
19
 *
20
 * @author Andreas Gohr <[email protected]>
21
 * @see    htmlspecialchars()
22
 *
23
 * @param string $string the string being converted
24
 * @return string converted string
25
 */
26
function hsc($string) {
27
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
28
}
29
30
/**
31
 * Checks if the given input is blank
32
 *
33
 * This is similar to empty() but will return false for "0".
34
 *
35
 * Please note: when you pass uninitialized variables, they will implicitly be created
36
 * with a NULL value without warning.
37
 *
38
 * To avoid this it's recommended to guard the call with isset like this:
39
 *
40
 * (isset($foo) && !blank($foo))
41
 * (!isset($foo) || blank($foo))
42
 *
43
 * @param $in
44
 * @param bool $trim Consider a string of whitespace to be blank
45
 * @return bool
46
 */
47
function blank(&$in, $trim = false) {
48
    if(is_null($in)) return true;
49
    if(is_array($in)) return empty($in);
50
    if($in === "\0") return true;
51
    if($trim && trim($in) === '') return true;
52
    if(strlen($in) > 0) return false;
53
    return empty($in);
54
}
55
56
/**
57
 * print a newline terminated string
58
 *
59
 * You can give an indention as optional parameter
60
 *
61
 * @author Andreas Gohr <[email protected]>
62
 *
63
 * @param string $string  line of text
64
 * @param int    $indent  number of spaces indention
65
 */
66
function ptln($string, $indent = 0) {
67
    echo str_repeat(' ', $indent)."$string\n";
68
}
69
70
/**
71
 * strips control characters (<32) from the given string
72
 *
73
 * @author Andreas Gohr <[email protected]>
74
 *
75
 * @param string $string being stripped
76
 * @return string
77
 */
78
function stripctl($string) {
79
    return preg_replace('/[\x00-\x1F]+/s', '', $string);
80
}
81
82
/**
83
 * Return a secret token to be used for CSRF attack prevention
84
 *
85
 * @author  Andreas Gohr <[email protected]>
86
 * @link    http://en.wikipedia.org/wiki/Cross-site_request_forgery
87
 * @link    http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
88
 *
89
 * @return  string
90
 */
91
function getSecurityToken() {
92
    /** @var Input $INPUT */
93
    global $INPUT;
94
95
    $user = $INPUT->server->str('REMOTE_USER');
96
    $session = session_id();
97
98
    // CSRF checks are only for logged in users - do not generate for anonymous
99
    if(trim($user) == '' || trim($session) == '') return '';
100
    return \dokuwiki\PassHash::hmac('md5', $session.$user, auth_cookiesalt());
0 ignored issues
show
Bug introduced by
It seems like auth_cookiesalt() can also be of type boolean; however, dokuwiki\PassHash::hmac() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
101
}
102
103
/**
104
 * Check the secret CSRF token
105
 *
106
 * @param null|string $token security token or null to read it from request variable
107
 * @return bool success if the token matched
108
 */
109
function checkSecurityToken($token = null) {
110
    /** @var Input $INPUT */
111
    global $INPUT;
112
    if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
113
114
    if(is_null($token)) $token = $INPUT->str('sectok');
115
    if(getSecurityToken() != $token) {
116
        msg('Security Token did not match. Possible CSRF attack.', -1);
117
        return false;
118
    }
119
    return true;
120
}
121
122
/**
123
 * Print a hidden form field with a secret CSRF token
124
 *
125
 * @author  Andreas Gohr <[email protected]>
126
 *
127
 * @param bool $print  if true print the field, otherwise html of the field is returned
128
 * @return string html of hidden form field
129
 */
130
function formSecurityToken($print = true) {
131
    $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
132
    if($print) echo $ret;
133
    return $ret;
134
}
135
136
/**
137
 * Determine basic information for a request of $id
138
 *
139
 * @author Andreas Gohr <[email protected]>
140
 * @author Chris Smith <[email protected]>
141
 *
142
 * @param string $id         pageid
143
 * @param bool   $htmlClient add info about whether is mobile browser
144
 * @return array with info for a request of $id
145
 *
146
 */
147
function basicinfo($id, $htmlClient=true){
148
    global $USERINFO;
149
    /* @var Input $INPUT */
150
    global $INPUT;
151
152
    // set info about manager/admin status.
153
    $info = array();
154
    $info['isadmin']   = false;
155
    $info['ismanager'] = false;
156
    if($INPUT->server->has('REMOTE_USER')) {
157
        $info['userinfo']   = $USERINFO;
158
        $info['perm']       = auth_quickaclcheck($id);
159
        $info['client']     = $INPUT->server->str('REMOTE_USER');
160
161
        if($info['perm'] == AUTH_ADMIN) {
162
            $info['isadmin']   = true;
163
            $info['ismanager'] = true;
164
        } elseif(auth_ismanager()) {
165
            $info['ismanager'] = true;
166
        }
167
168
        // if some outside auth were used only REMOTE_USER is set
169
        if(!$info['userinfo']['name']) {
170
            $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
171
        }
172
173
    } else {
174
        $info['perm']       = auth_aclcheck($id, '', null);
175
        $info['client']     = clientIP(true);
176
    }
177
178
    $info['namespace'] = getNS($id);
179
180
    // mobile detection
181
    if ($htmlClient) {
182
        $info['ismobile'] = clientismobile();
0 ignored issues
show
Deprecated Code introduced by
The function clientismobile() has been deprecated with message: 2018-04-27 you probably want media queries instead anyway

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...
183
    }
184
185
    return $info;
186
 }
187
188
/**
189
 * Return info about the current document as associative
190
 * array.
191
 *
192
 * @author Andreas Gohr <[email protected]>
193
 *
194
 * @return array with info about current document
195
 */
196
function pageinfo() {
197
    global $ID;
198
    global $REV;
199
    global $RANGE;
200
    global $lang;
201
    /* @var Input $INPUT */
202
    global $INPUT;
203
204
    $info = basicinfo($ID);
205
206
    // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
207
    // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
208
    $info['id']  = $ID;
209
    $info['rev'] = $REV;
210
211
    $subManager = new SubscriberManager();
212
    $info['subscribed'] = $subManager->userSubscription();
213
214
    $info['locked']     = checklock($ID);
215
    $info['filepath']   = wikiFN($ID);
216
    $info['exists']     = file_exists($info['filepath']);
217
    $info['currentrev'] = @filemtime($info['filepath']);
218
    if($REV) {
219
        //check if current revision was meant
220
        if($info['exists'] && ($info['currentrev'] == $REV)) {
221
            $REV = '';
222
        } elseif($RANGE) {
223
            //section editing does not work with old revisions!
224
            $REV   = '';
225
            $RANGE = '';
226
            msg($lang['nosecedit'], 0);
227
        } else {
228
            //really use old revision
229
            $info['filepath'] = wikiFN($ID, $REV);
230
            $info['exists']   = file_exists($info['filepath']);
231
        }
232
    }
233
    $info['rev'] = $REV;
234
    if($info['exists']) {
235
        $info['writable'] = (is_writable($info['filepath']) &&
236
            ($info['perm'] >= AUTH_EDIT));
237
    } else {
238
        $info['writable'] = ($info['perm'] >= AUTH_CREATE);
239
    }
240
    $info['editable'] = ($info['writable'] && empty($info['locked']));
241
    $info['lastmod']  = @filemtime($info['filepath']);
242
243
    //load page meta data
244
    $info['meta'] = p_get_metadata($ID);
245
246
    //who's the editor
247
    $pagelog = new PageChangeLog($ID, 1024);
248
    if($REV) {
249
        $revinfo = $pagelog->getRevisionInfo($REV);
250
    } else {
251
        if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
252
            $revinfo = $info['meta']['last_change'];
253
        } else {
254
            $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
255
            // cache most recent changelog line in metadata if missing and still valid
256
            if($revinfo !== false) {
257
                $info['meta']['last_change'] = $revinfo;
258
                p_set_metadata($ID, array('last_change' => $revinfo));
259
            }
260
        }
261
    }
262
    //and check for an external edit
263
    if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
264
        // cached changelog line no longer valid
265
        $revinfo                     = false;
266
        $info['meta']['last_change'] = $revinfo;
267
        p_set_metadata($ID, array('last_change' => $revinfo));
268
    }
269
270
    if($revinfo !== false){
271
        $info['ip']   = $revinfo['ip'];
272
        $info['user'] = $revinfo['user'];
273
        $info['sum']  = $revinfo['sum'];
274
        // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
275
        // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
276
277
        if($revinfo['user']) {
278
            $info['editor'] = $revinfo['user'];
279
        } else {
280
            $info['editor'] = $revinfo['ip'];
281
        }
282
    }else{
283
        $info['ip']     = null;
284
        $info['user']   = null;
285
        $info['sum']    = null;
286
        $info['editor'] = null;
287
    }
288
289
    // draft
290
    $draft = new \dokuwiki\Draft($ID, $info['client']);
291
    if ($draft->isDraftAvailable()) {
292
        $info['draft'] = $draft->getDraftFilename();
293
    }
294
295
    return $info;
296
}
297
298
/**
299
 * Initialize and/or fill global $JSINFO with some basic info to be given to javascript
300
 */
301
function jsinfo() {
302
    global $JSINFO, $ID, $INFO, $ACT;
303
304
    if (!is_array($JSINFO)) {
305
        $JSINFO = [];
306
    }
307
    //export minimal info to JS, plugins can add more
308
    $JSINFO['id']                    = $ID;
309
    $JSINFO['namespace']             = isset($INFO) ? (string) $INFO['namespace'] : '';
310
    $JSINFO['ACT']                   = act_clean($ACT);
311
    $JSINFO['useHeadingNavigation']  = (int) useHeading('navigation');
312
    $JSINFO['useHeadingContent']     = (int) useHeading('content');
313
}
314
315
/**
316
 * Return information about the current media item as an associative array.
317
 *
318
 * @return array with info about current media item
319
 */
320
function mediainfo(){
321
    global $NS;
322
    global $IMG;
323
324
    $info = basicinfo("$NS:*");
325
    $info['image'] = $IMG;
326
327
    return $info;
328
}
329
330
/**
331
 * Build an string of URL parameters
332
 *
333
 * @author Andreas Gohr
334
 *
335
 * @param array  $params    array with key-value pairs
336
 * @param string $sep       series of pairs are separated by this character
337
 * @return string query string
338
 */
339
function buildURLparams($params, $sep = '&amp;') {
340
    $url = '';
341
    $amp = false;
342
    foreach($params as $key => $val) {
343
        if($amp) $url .= $sep;
344
345
        $url .= rawurlencode($key).'=';
346
        $url .= rawurlencode((string) $val);
347
        $amp = true;
348
    }
349
    return $url;
350
}
351
352
/**
353
 * Build an string of html tag attributes
354
 *
355
 * Skips keys starting with '_', values get HTML encoded
356
 *
357
 * @author Andreas Gohr
358
 *
359
 * @param array $params           array with (attribute name-attribute value) pairs
360
 * @param bool  $skipEmptyStrings skip empty string values?
361
 * @return string
362
 */
363
function buildAttributes($params, $skipEmptyStrings = false) {
364
    $url   = '';
365
    $white = false;
366
    foreach($params as $key => $val) {
367
        if($key[0] == '_') continue;
368
        if($val === '' && $skipEmptyStrings) continue;
369
        if($white) $url .= ' ';
370
371
        $url .= $key.'="';
372
        $url .= htmlspecialchars($val);
373
        $url .= '"';
374
        $white = true;
375
    }
376
    return $url;
377
}
378
379
/**
380
 * This builds the breadcrumb trail and returns it as array
381
 *
382
 * @author Andreas Gohr <[email protected]>
383
 *
384
 * @return string[] with the data: array(pageid=>name, ... )
385
 */
386
function breadcrumbs() {
387
    // we prepare the breadcrumbs early for quick session closing
388
    static $crumbs = null;
389
    if($crumbs != null) return $crumbs;
390
391
    global $ID;
392
    global $ACT;
393
    global $conf;
394
    global $INFO;
395
396
    //first visit?
397
    $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
398
    //we only save on show and existing visible readable wiki documents
399
    $file = wikiFN($ID);
400
    if($ACT != 'show' || $INFO['perm'] < AUTH_READ || isHiddenPage($ID) || !file_exists($file)) {
401
        $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
402
        return $crumbs;
403
    }
404
405
    // page names
406
    $name = noNSorNS($ID);
407
    if(useHeading('navigation')) {
408
        // get page title
409
        $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
410
        if($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
411
            $name = $title;
412
        }
413
    }
414
415
    //remove ID from array
416
    if(isset($crumbs[$ID])) {
417
        unset($crumbs[$ID]);
418
    }
419
420
    //add to array
421
    $crumbs[$ID] = $name;
422
    //reduce size
423
    while(count($crumbs) > $conf['breadcrumbs']) {
424
        array_shift($crumbs);
425
    }
426
    //save to session
427
    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
428
    return $crumbs;
429
}
430
431
/**
432
 * Filter for page IDs
433
 *
434
 * This is run on a ID before it is outputted somewhere
435
 * currently used to replace the colon with something else
436
 * on Windows (non-IIS) systems and to have proper URL encoding
437
 *
438
 * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and
439
 * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of
440
 * unaffected servers instead of blacklisting affected servers here.
441
 *
442
 * Urlencoding is ommitted when the second parameter is false
443
 *
444
 * @author Andreas Gohr <[email protected]>
445
 *
446
 * @param string $id pageid being filtered
447
 * @param bool   $ue apply urlencoding?
448
 * @return string
449
 */
450
function idfilter($id, $ue = true) {
451
    global $conf;
452
    /* @var Input $INPUT */
453
    global $INPUT;
454
455
    if($conf['useslash'] && $conf['userewrite']) {
456
        $id = strtr($id, ':', '/');
457
    } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
458
        $conf['userewrite'] &&
459
        strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
460
    ) {
461
        $id = strtr($id, ':', ';');
462
    }
463
    if($ue) {
464
        $id = rawurlencode($id);
465
        $id = str_replace('%3A', ':', $id); //keep as colon
466
        $id = str_replace('%3B', ';', $id); //keep as semicolon
467
        $id = str_replace('%2F', '/', $id); //keep as slash
468
    }
469
    return $id;
470
}
471
472
/**
473
 * This builds a link to a wikipage
474
 *
475
 * It handles URL rewriting and adds additional parameters
476
 *
477
 * @author Andreas Gohr <[email protected]>
478
 *
479
 * @param string       $id             page id, defaults to start page
480
 * @param string|array $urlParameters  URL parameters, associative array recommended
481
 * @param bool         $absolute       request an absolute URL instead of relative
482
 * @param string       $separator      parameter separator
483
 * @return string
484
 */
485
function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;') {
486
    global $conf;
487
    if(is_array($urlParameters)) {
488
        if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']);
489
        if(isset($urlParameters['at']) && $conf['date_at_format']) {
490
            $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']);
491
        }
492
        $urlParameters = buildURLparams($urlParameters, $separator);
493
    } else {
494
        $urlParameters = str_replace(',', $separator, $urlParameters);
495
    }
496
    if($id === '') {
497
        $id = $conf['start'];
498
    }
499
    $id = idfilter($id);
500
    if($absolute) {
501
        $xlink = DOKU_URL;
502
    } else {
503
        $xlink = DOKU_BASE;
504
    }
505
506
    if($conf['userewrite'] == 2) {
507
        $xlink .= DOKU_SCRIPT.'/'.$id;
508
        if($urlParameters) $xlink .= '?'.$urlParameters;
509
    } elseif($conf['userewrite']) {
510
        $xlink .= $id;
511
        if($urlParameters) $xlink .= '?'.$urlParameters;
512
    } elseif($id !== '') {
513
        $xlink .= DOKU_SCRIPT.'?id='.$id;
514
        if($urlParameters) $xlink .= $separator.$urlParameters;
515
    } else {
516
        $xlink .= DOKU_SCRIPT;
517
        if($urlParameters) $xlink .= '?'.$urlParameters;
518
    }
519
520
    return $xlink;
521
}
522
523
/**
524
 * This builds a link to an alternate page format
525
 *
526
 * Handles URL rewriting if enabled. Follows the style of wl().
527
 *
528
 * @author Ben Coburn <[email protected]>
529
 * @param string       $id             page id, defaults to start page
530
 * @param string       $format         the export renderer to use
531
 * @param string|array $urlParameters  URL parameters, associative array recommended
532
 * @param bool         $abs            request an absolute URL instead of relative
533
 * @param string       $sep            parameter separator
534
 * @return string
535
 */
536
function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;') {
537
    global $conf;
538
    if(is_array($urlParameters)) {
539
        $urlParameters = buildURLparams($urlParameters, $sep);
540
    } else {
541
        $urlParameters = str_replace(',', $sep, $urlParameters);
542
    }
543
544
    $format = rawurlencode($format);
545
    $id     = idfilter($id);
546
    if($abs) {
547
        $xlink = DOKU_URL;
548
    } else {
549
        $xlink = DOKU_BASE;
550
    }
551
552
    if($conf['userewrite'] == 2) {
553
        $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
554
        if($urlParameters) $xlink .= $sep.$urlParameters;
555
    } elseif($conf['userewrite'] == 1) {
556
        $xlink .= '_export/'.$format.'/'.$id;
557
        if($urlParameters) $xlink .= '?'.$urlParameters;
558
    } else {
559
        $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
560
        if($urlParameters) $xlink .= $sep.$urlParameters;
561
    }
562
563
    return $xlink;
564
}
565
566
/**
567
 * Build a link to a media file
568
 *
569
 * Will return a link to the detail page if $direct is false
570
 *
571
 * The $more parameter should always be given as array, the function then
572
 * will strip default parameters to produce even cleaner URLs
573
 *
574
 * @param string  $id     the media file id or URL
575
 * @param mixed   $more   string or array with additional parameters
576
 * @param bool    $direct link to detail page if false
577
 * @param string  $sep    URL parameter separator
578
 * @param bool    $abs    Create an absolute URL
579
 * @return string
580
 */
581
function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
582
    global $conf;
583
    $isexternalimage = media_isexternal($id);
584
    if(!$isexternalimage) {
585
        $id = cleanID($id);
586
    }
587
588
    if(is_array($more)) {
589
        // add token for resized images
590
        if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){
591
            $more['tok'] = media_get_token($id,$more['w'],$more['h']);
592
        }
593
        // strip defaults for shorter URLs
594
        if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
595
        if(empty($more['w'])) unset($more['w']);
596
        if(empty($more['h'])) unset($more['h']);
597
        if(isset($more['id']) && $direct) unset($more['id']);
598
        if(isset($more['rev']) && !$more['rev']) unset($more['rev']);
599
        $more = buildURLparams($more, $sep);
600
    } else {
601
        $matches = array();
602
        if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){
603
            $resize = array('w'=>0, 'h'=>0);
604
            foreach ($matches as $match){
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
605
                $resize[$match[1]] = $match[2];
606
            }
607
            $more .= $more === '' ? '' : $sep;
608
            $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']);
609
        }
610
        $more = str_replace('cache=cache', '', $more); //skip default
611
        $more = str_replace(',,', ',', $more);
612
        $more = str_replace(',', $sep, $more);
613
    }
614
615
    if($abs) {
616
        $xlink = DOKU_URL;
617
    } else {
618
        $xlink = DOKU_BASE;
619
    }
620
621
    // external URLs are always direct without rewriting
622
    if($isexternalimage) {
623
        $xlink .= 'lib/exe/fetch.php';
624
        $xlink .= '?'.$more;
625
        $xlink .= $sep.'media='.rawurlencode($id);
626
        return $xlink;
627
    }
628
629
    $id = idfilter($id);
630
631
    // decide on scriptname
632
    if($direct) {
633
        if($conf['userewrite'] == 1) {
634
            $script = '_media';
635
        } else {
636
            $script = 'lib/exe/fetch.php';
637
        }
638
    } else {
639
        if($conf['userewrite'] == 1) {
640
            $script = '_detail';
641
        } else {
642
            $script = 'lib/exe/detail.php';
643
        }
644
    }
645
646
    // build URL based on rewrite mode
647
    if($conf['userewrite']) {
648
        $xlink .= $script.'/'.$id;
649
        if($more) $xlink .= '?'.$more;
650
    } else {
651
        if($more) {
652
            $xlink .= $script.'?'.$more;
653
            $xlink .= $sep.'media='.$id;
654
        } else {
655
            $xlink .= $script.'?media='.$id;
656
        }
657
    }
658
659
    return $xlink;
660
}
661
662
/**
663
 * Returns the URL to the DokuWiki base script
664
 *
665
 * Consider using wl() instead, unless you absoutely need the doku.php endpoint
666
 *
667
 * @author Andreas Gohr <[email protected]>
668
 *
669
 * @return string
670
 */
671
function script() {
672
    return DOKU_BASE.DOKU_SCRIPT;
673
}
674
675
/**
676
 * Spamcheck against wordlist
677
 *
678
 * Checks the wikitext against a list of blocked expressions
679
 * returns true if the text contains any bad words
680
 *
681
 * Triggers COMMON_WORDBLOCK_BLOCKED
682
 *
683
 *  Action Plugins can use this event to inspect the blocked data
684
 *  and gain information about the user who was blocked.
685
 *
686
 *  Event data:
687
 *    data['matches']  - array of matches
688
 *    data['userinfo'] - information about the blocked user
689
 *      [ip]           - ip address
690
 *      [user]         - username (if logged in)
691
 *      [mail]         - mail address (if logged in)
692
 *      [name]         - real name (if logged in)
693
 *
694
 * @author Andreas Gohr <[email protected]>
695
 * @author Michael Klier <[email protected]>
696
 *
697
 * @param  string $text - optional text to check, if not given the globals are used
698
 * @return bool         - true if a spam word was found
699
 */
700
function checkwordblock($text = '') {
701
    global $TEXT;
702
    global $PRE;
703
    global $SUF;
704
    global $SUM;
705
    global $conf;
706
    global $INFO;
707
    /* @var Input $INPUT */
708
    global $INPUT;
709
710
    if(!$conf['usewordblock']) return false;
711
712
    if(!$text) $text = "$PRE $TEXT $SUF $SUM";
713
714
    // we prepare the text a tiny bit to prevent spammers circumventing URL checks
715
    // phpcs:disable Generic.Files.LineLength.TooLong
716
    $text = preg_replace(
717
        '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i',
718
        '\1http://\2 \2\3',
719
        $text
720
    );
721
    // phpcs:enable
722
723
    $wordblocks = getWordblocks();
724
    // how many lines to read at once (to work around some PCRE limits)
725
    if(version_compare(phpversion(), '4.3.0', '<')) {
726
        // old versions of PCRE define a maximum of parenthesises even if no
727
        // backreferences are used - the maximum is 99
728
        // this is very bad performancewise and may even be too high still
729
        $chunksize = 40;
730
    } else {
731
        // read file in chunks of 200 - this should work around the
732
        // MAX_PATTERN_SIZE in modern PCRE
733
        $chunksize = 200;
734
    }
735
    while($blocks = array_splice($wordblocks, 0, $chunksize)) {
736
        $re = array();
737
        // build regexp from blocks
738
        foreach($blocks as $block) {
739
            $block = preg_replace('/#.*$/', '', $block);
740
            $block = trim($block);
741
            if(empty($block)) continue;
742
            $re[] = $block;
743
        }
744
        if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) {
745
            // prepare event data
746
            $data = array();
747
            $data['matches']        = $matches;
748
            $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
749
            if($INPUT->server->str('REMOTE_USER')) {
750
                $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
751
                $data['userinfo']['name'] = $INFO['userinfo']['name'];
752
                $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
753
            }
754
            $callback = function () {
755
                return true;
756
            };
757
            return Event::createAndTrigger('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
758
        }
759
    }
760
    return false;
761
}
762
763
/**
764
 * Return the IP of the client
765
 *
766
 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
767
 *
768
 * It returns a comma separated list of IPs if the above mentioned
769
 * headers are set. If the single parameter is set, it tries to return
770
 * a routable public address, prefering the ones suplied in the X
771
 * headers
772
 *
773
 * @author Andreas Gohr <[email protected]>
774
 *
775
 * @param  boolean $single If set only a single IP is returned
776
 * @return string
777
 */
778
function clientIP($single = false) {
779
    /* @var Input $INPUT */
780
    global $INPUT, $conf;
781
782
    $ip   = array();
783
    $ip[] = $INPUT->server->str('REMOTE_ADDR');
784
    if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
785
        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
786
    }
787
    if($INPUT->server->str('HTTP_X_REAL_IP')) {
788
        $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
789
    }
790
791
    // remove any non-IP stuff
792
    $cnt   = count($ip);
793
    for($i = 0; $i < $cnt; $i++) {
794
        $ip[$i] = filter_var($ip[$i], FILTER_VALIDATE_IP);
795
        if(empty($ip[$i])) unset($ip[$i]);
796
    }
797
    $ip = array_values(array_unique($ip));
798
    if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
799
800
    if(!$single) return join(',', $ip);
801
802
    // skip trusted local addresses
803
    foreach($ip as $i) {
804
        if(!empty($conf['trustedproxy']) && preg_match('/'.$conf['trustedproxy'].'/', $i)) {
805
            continue;
806
        } else {
807
            return $i;
808
        }
809
    }
810
811
    // still here? just use the last address
812
    // this case all ips in the list are trusted
813
    return $ip[count($ip)-1];
814
}
815
816
/**
817
 * Check if the browser is on a mobile device
818
 *
819
 * Adapted from the example code at url below
820
 *
821
 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
822
 *
823
 * @deprecated 2018-04-27 you probably want media queries instead anyway
824
 * @return bool if true, client is mobile browser; otherwise false
825
 */
826
function clientismobile() {
827
    /* @var Input $INPUT */
828
    global $INPUT;
829
830
    if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
831
832
    if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
833
834
    if(!$INPUT->server->has('HTTP_USER_AGENT')) return false;
835
836
    $uamatches = join(
837
        '|',
838
        [
839
            'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv',
840
            'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia',
841
            'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-',
842
            'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx',
843
            'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox',
844
            'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb',
845
            '\d\d\di', 'moto'
846
        ]
847
    );
848
849
    if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
850
851
    return false;
852
}
853
854
/**
855
 * check if a given link is interwiki link
856
 *
857
 * @param string $link the link, e.g. "wiki>page"
858
 * @return bool
859
 */
860
function link_isinterwiki($link){
861
    if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true;
862
    return false;
863
}
864
865
/**
866
 * Convert one or more comma separated IPs to hostnames
867
 *
868
 * If $conf['dnslookups'] is disabled it simply returns the input string
869
 *
870
 * @author Glen Harris <[email protected]>
871
 *
872
 * @param  string $ips comma separated list of IP addresses
873
 * @return string a comma separated list of hostnames
874
 */
875
function gethostsbyaddrs($ips) {
876
    global $conf;
877
    if(!$conf['dnslookups']) return $ips;
878
879
    $hosts = array();
880
    $ips   = explode(',', $ips);
881
882
    if(is_array($ips)) {
883
        foreach($ips as $ip) {
884
            $hosts[] = gethostbyaddr(trim($ip));
885
        }
886
        return join(',', $hosts);
887
    } else {
888
        return gethostbyaddr(trim($ips));
889
    }
890
}
891
892
/**
893
 * Checks if a given page is currently locked.
894
 *
895
 * removes stale lockfiles
896
 *
897
 * @author Andreas Gohr <[email protected]>
898
 *
899
 * @param string $id page id
900
 * @return bool page is locked?
901
 */
902
function checklock($id) {
903
    global $conf;
904
    /* @var Input $INPUT */
905
    global $INPUT;
906
907
    $lock = wikiLockFN($id);
908
909
    //no lockfile
910
    if(!file_exists($lock)) return false;
911
912
    //lockfile expired
913
    if((time() - filemtime($lock)) > $conf['locktime']) {
914
        @unlink($lock);
915
        return false;
916
    }
917
918
    //my own lock
919
    @list($ip, $session) = explode("\n", io_readFile($lock));
920
    if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) {
921
        return false;
922
    }
923
924
    return $ip;
925
}
926
927
/**
928
 * Lock a page for editing
929
 *
930
 * @author Andreas Gohr <[email protected]>
931
 *
932
 * @param string $id page id to lock
933
 */
934
function lock($id) {
935
    global $conf;
936
    /* @var Input $INPUT */
937
    global $INPUT;
938
939
    if($conf['locktime'] == 0) {
940
        return;
941
    }
942
943
    $lock = wikiLockFN($id);
944
    if($INPUT->server->str('REMOTE_USER')) {
945
        io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
946
    } else {
947
        io_saveFile($lock, clientIP()."\n".session_id());
948
    }
949
}
950
951
/**
952
 * Unlock a page if it was locked by the user
953
 *
954
 * @author Andreas Gohr <[email protected]>
955
 *
956
 * @param string $id page id to unlock
957
 * @return bool true if a lock was removed
958
 */
959
function unlock($id) {
960
    /* @var Input $INPUT */
961
    global $INPUT;
962
963
    $lock = wikiLockFN($id);
964
    if(file_exists($lock)) {
965
        @list($ip, $session) = explode("\n", io_readFile($lock));
966
        if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) {
967
            @unlink($lock);
968
            return true;
969
        }
970
    }
971
    return false;
972
}
973
974
/**
975
 * convert line ending to unix format
976
 *
977
 * also makes sure the given text is valid UTF-8
978
 *
979
 * @see    formText() for 2crlf conversion
980
 * @author Andreas Gohr <[email protected]>
981
 *
982
 * @param string $text
983
 * @return string
984
 */
985
function cleanText($text) {
986
    $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
987
988
    // if the text is not valid UTF-8 we simply assume latin1
989
    // this won't break any worse than it breaks with the wrong encoding
990
    // but might actually fix the problem in many cases
991
    if(!\dokuwiki\Utf8\Clean::isUtf8($text)) $text = utf8_encode($text);
992
993
    return $text;
994
}
995
996
/**
997
 * Prepares text for print in Webforms by encoding special chars.
998
 * It also converts line endings to Windows format which is
999
 * pseudo standard for webforms.
1000
 *
1001
 * @see    cleanText() for 2unix conversion
1002
 * @author Andreas Gohr <[email protected]>
1003
 *
1004
 * @param string $text
1005
 * @return string
1006
 */
1007
function formText($text) {
1008
    $text = str_replace("\012", "\015\012", $text);
1009
    return htmlspecialchars($text);
1010
}
1011
1012
/**
1013
 * Returns the specified local text in raw format
1014
 *
1015
 * @author Andreas Gohr <[email protected]>
1016
 *
1017
 * @param string $id   page id
1018
 * @param string $ext  extension of file being read, default 'txt'
1019
 * @return string
1020
 */
1021
function rawLocale($id, $ext = 'txt') {
1022
    return io_readFile(localeFN($id, $ext));
1023
}
1024
1025
/**
1026
 * Returns the raw WikiText
1027
 *
1028
 * @author Andreas Gohr <[email protected]>
1029
 *
1030
 * @param string $id   page id
1031
 * @param string|int $rev  timestamp when a revision of wikitext is desired
1032
 * @return string
1033
 */
1034
function rawWiki($id, $rev = '') {
1035
    return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1036
}
1037
1038
/**
1039
 * Returns the pagetemplate contents for the ID's namespace
1040
 *
1041
 * @triggers COMMON_PAGETPL_LOAD
1042
 * @author Andreas Gohr <[email protected]>
1043
 *
1044
 * @param string $id the id of the page to be created
1045
 * @return string parsed pagetemplate content
1046
 */
1047
function pageTemplate($id) {
1048
    global $conf;
1049
1050
    if(is_array($id)) $id = $id[0];
1051
1052
    // prepare initial event data
1053
    $data = array(
1054
        'id'        => $id, // the id of the page to be created
1055
        'tpl'       => '', // the text used as template
1056
        'tplfile'   => '', // the file above text was/should be loaded from
1057
        'doreplace' => true // should wildcard replacements be done on the text?
1058
    );
1059
1060
    $evt = new Event('COMMON_PAGETPL_LOAD', $data);
1061
    if($evt->advise_before(true)) {
1062
        // the before event might have loaded the content already
1063
        if(empty($data['tpl'])) {
1064
            // if the before event did not set a template file, try to find one
1065
            if(empty($data['tplfile'])) {
1066
                $path = dirname(wikiFN($id));
1067
                if(file_exists($path.'/_template.txt')) {
1068
                    $data['tplfile'] = $path.'/_template.txt';
1069
                } else {
1070
                    // search upper namespaces for templates
1071
                    $len = strlen(rtrim($conf['datadir'], '/'));
1072
                    while(strlen($path) >= $len) {
1073
                        if(file_exists($path.'/__template.txt')) {
1074
                            $data['tplfile'] = $path.'/__template.txt';
1075
                            break;
1076
                        }
1077
                        $path = substr($path, 0, strrpos($path, '/'));
1078
                    }
1079
                }
1080
            }
1081
            // load the content
1082
            $data['tpl'] = io_readFile($data['tplfile']);
1083
        }
1084
        if($data['doreplace']) parsePageTemplate($data);
1085
    }
1086
    $evt->advise_after();
1087
    unset($evt);
1088
1089
    return $data['tpl'];
1090
}
1091
1092
/**
1093
 * Performs common page template replacements
1094
 * This works on data from COMMON_PAGETPL_LOAD
1095
 *
1096
 * @author Andreas Gohr <[email protected]>
1097
 *
1098
 * @param array $data array with event data
1099
 * @return string
1100
 */
1101
function parsePageTemplate(&$data) {
1102
    /**
1103
     * @var string $id        the id of the page to be created
1104
     * @var string $tpl       the text used as template
1105
     * @var string $tplfile   the file above text was/should be loaded from
1106
     * @var bool   $doreplace should wildcard replacements be done on the text?
1107
     */
1108
    extract($data);
1109
1110
    global $USERINFO;
1111
    global $conf;
1112
    /* @var Input $INPUT */
1113
    global $INPUT;
1114
1115
    // replace placeholders
1116
    $file = noNS($id);
1117
    $page = strtr($file, $conf['sepchar'], ' ');
1118
1119
    $tpl = str_replace(
1120
        array(
1121
             '@ID@',
1122
             '@NS@',
1123
             '@CURNS@',
1124
             '@!CURNS@',
1125
             '@!!CURNS@',
1126
             '@!CURNS!@',
1127
             '@FILE@',
1128
             '@!FILE@',
1129
             '@!FILE!@',
1130
             '@PAGE@',
1131
             '@!PAGE@',
1132
             '@!!PAGE@',
1133
             '@!PAGE!@',
1134
             '@USER@',
1135
             '@NAME@',
1136
             '@MAIL@',
1137
             '@DATE@',
1138
        ),
1139
        array(
1140
             $id,
1141
             getNS($id),
1142
             curNS($id),
1143
             \dokuwiki\Utf8\PhpString::ucfirst(curNS($id)),
1144
             \dokuwiki\Utf8\PhpString::ucwords(curNS($id)),
1145
             \dokuwiki\Utf8\PhpString::strtoupper(curNS($id)),
1146
             $file,
1147
             \dokuwiki\Utf8\PhpString::ucfirst($file),
1148
             \dokuwiki\Utf8\PhpString::strtoupper($file),
1149
             $page,
1150
             \dokuwiki\Utf8\PhpString::ucfirst($page),
1151
             \dokuwiki\Utf8\PhpString::ucwords($page),
1152
             \dokuwiki\Utf8\PhpString::strtoupper($page),
1153
             $INPUT->server->str('REMOTE_USER'),
1154
             $USERINFO ? $USERINFO['name'] : '',
1155
             $USERINFO ? $USERINFO['mail'] : '',
1156
             $conf['dformat'],
1157
        ), $tpl
1158
    );
1159
1160
    // we need the callback to work around strftime's char limit
1161
    $tpl = preg_replace_callback(
1162
        '/%./',
1163
        function ($m) {
1164
            return strftime($m[0]);
1165
        },
1166
        $tpl
1167
    );
1168
    $data['tpl'] = $tpl;
1169
    return $tpl;
1170
}
1171
1172
/**
1173
 * Returns the raw Wiki Text in three slices.
1174
 *
1175
 * The range parameter needs to have the form "from-to"
1176
 * and gives the range of the section in bytes - no
1177
 * UTF-8 awareness is needed.
1178
 * The returned order is prefix, section and suffix.
1179
 *
1180
 * @author Andreas Gohr <[email protected]>
1181
 *
1182
 * @param string $range in form "from-to"
1183
 * @param string $id    page id
1184
 * @param string $rev   optional, the revision timestamp
1185
 * @return string[] with three slices
1186
 */
1187
function rawWikiSlices($range, $id, $rev = '') {
1188
    $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
1189
1190
    // Parse range
1191
    list($from, $to) = explode('-', $range, 2);
1192
    // Make range zero-based, use defaults if marker is missing
1193
    $from = !$from ? 0 : ($from - 1);
1194
    $to   = !$to ? strlen($text) : ($to - 1);
1195
1196
    $slices = array();
1197
    $slices[0] = substr($text, 0, $from);
1198
    $slices[1] = substr($text, $from, $to - $from);
1199
    $slices[2] = substr($text, $to);
1200
    return $slices;
1201
}
1202
1203
/**
1204
 * Joins wiki text slices
1205
 *
1206
 * function to join the text slices.
1207
 * When the pretty parameter is set to true it adds additional empty
1208
 * lines between sections if needed (used on saving).
1209
 *
1210
 * @author Andreas Gohr <[email protected]>
1211
 *
1212
 * @param string $pre   prefix
1213
 * @param string $text  text in the middle
1214
 * @param string $suf   suffix
1215
 * @param bool $pretty add additional empty lines between sections
1216
 * @return string
1217
 */
1218
function con($pre, $text, $suf, $pretty = false) {
1219
    if($pretty) {
1220
        if($pre !== '' && substr($pre, -1) !== "\n" &&
1221
            substr($text, 0, 1) !== "\n"
1222
        ) {
1223
            $pre .= "\n";
1224
        }
1225
        if($suf !== '' && substr($text, -1) !== "\n" &&
1226
            substr($suf, 0, 1) !== "\n"
1227
        ) {
1228
            $text .= "\n";
1229
        }
1230
    }
1231
1232
    return $pre.$text.$suf;
1233
}
1234
1235
/**
1236
 * Checks if the current page version is newer than the last entry in the page's
1237
 * changelog. If so, we assume it has been an external edit and we create an
1238
 * attic copy and add a proper changelog line.
1239
 *
1240
 * This check is only executed when the page is about to be saved again from the
1241
 * wiki, triggered in @see saveWikiText()
1242
 *
1243
 * @param string $id the page ID
1244
 */
1245
function detectExternalEdit($id) {
1246
    global $lang;
1247
1248
    $fileLastMod = wikiFN($id);
1249
    $lastMod     = @filemtime($fileLastMod); // from page
1250
    $pagelog     = new PageChangeLog($id, 1024);
1251
    $lastRev     = $pagelog->getRevisions(-1, 1); // from changelog
1252
    $lastRev     = (int) (empty($lastRev) ? 0 : $lastRev[0]);
1253
1254
    if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
1255
        // add old revision to the attic if missing
1256
        saveOldRevision($id);
1257
        // add a changelog entry if this edit came from outside dokuwiki
1258
        if($lastMod > $lastRev) {
1259
            $fileLastRev = wikiFN($id, $lastRev);
1260
            $revinfo = $pagelog->getRevisionInfo($lastRev);
1261
            if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
1262
                $filesize_old = 0;
1263
            } else {
1264
                $filesize_old = io_getSizeFile($fileLastRev);
1265
            }
1266
            $filesize_new = filesize($fileLastMod);
1267
            $sizechange = $filesize_new - $filesize_old;
1268
1269
            addLogEntry(
1270
                $lastMod,
1271
                $id,
1272
                DOKU_CHANGE_TYPE_EDIT,
1273
                $lang['external_edit'],
1274
                '',
1275
                array('ExternalEdit' => true),
1276
                $sizechange
1277
            );
1278
            // remove soon to be stale instructions
1279
            $cache = new CacheInstructions($id, $fileLastMod);
1280
            $cache->removeCache();
1281
        }
1282
    }
1283
}
1284
1285
/**
1286
 * Saves a wikitext by calling io_writeWikiPage.
1287
 * Also directs changelog and attic updates.
1288
 *
1289
 * @author Andreas Gohr <[email protected]>
1290
 * @author Ben Coburn <[email protected]>
1291
 *
1292
 * @param string $id       page id
1293
 * @param string $text     wikitext being saved
1294
 * @param string $summary  summary of text update
1295
 * @param bool   $minor    mark this saved version as minor update
1296
 */
1297
function saveWikiText($id, $text, $summary, $minor = false) {
1298
    /* Note to developers:
1299
       This code is subtle and delicate. Test the behavior of
1300
       the attic and changelog with dokuwiki and external edits
1301
       after any changes. External edits change the wiki page
1302
       directly without using php or dokuwiki.
1303
     */
1304
    global $conf;
1305
    global $lang;
1306
    global $REV;
1307
    /* @var Input $INPUT */
1308
    global $INPUT;
1309
1310
    // prepare data for event
1311
    $svdta = array();
1312
    $svdta['id']             = $id;
1313
    $svdta['file']           = wikiFN($id);
1314
    $svdta['revertFrom']     = $REV;
1315
    $svdta['oldRevision']    = @filemtime($svdta['file']);
1316
    $svdta['newRevision']    = 0;
1317
    $svdta['newContent']     = $text;
1318
    $svdta['oldContent']     = rawWiki($id);
1319
    $svdta['summary']        = $summary;
1320
    $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
1321
    $svdta['changeInfo']     = '';
1322
    $svdta['changeType']     = DOKU_CHANGE_TYPE_EDIT;
1323
    $svdta['sizechange']     = null;
1324
1325
    // select changelog line type
1326
    if($REV) {
1327
        $svdta['changeType']  = DOKU_CHANGE_TYPE_REVERT;
1328
        $svdta['changeInfo'] = $REV;
1329
    } else if(!file_exists($svdta['file'])) {
1330
        $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
1331
    } else if(trim($text) == '') {
1332
        // empty or whitespace only content deletes
1333
        $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
1334
        // autoset summary on deletion
1335
        if(blank($svdta['summary'])) {
1336
            $svdta['summary'] = $lang['deleted'];
1337
        }
1338
    } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
1339
        //minor edits only for logged in users
1340
        $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
1341
    }
1342
1343
    $event = new Event('COMMON_WIKIPAGE_SAVE', $svdta);
1344
    if(!$event->advise_before()) return;
1345
1346
    // if the content has not been changed, no save happens (plugins may override this)
1347
    if(!$svdta['contentChanged']) return;
1348
1349
    detectExternalEdit($id);
1350
1351
    if(
1352
        $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
1353
        ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
1354
    ) {
1355
        $filesize_old = 0;
1356
    } else {
1357
        $filesize_old = filesize($svdta['file']);
1358
    }
1359
    if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
1360
        // Send "update" event with empty data, so plugins can react to page deletion
1361
        $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
1362
        Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data);
1363
        // pre-save deleted revision
1364
        @touch($svdta['file']);
1365
        clearstatcache();
1366
        $svdta['newRevision'] = saveOldRevision($id);
1367
        // remove empty file
1368
        @unlink($svdta['file']);
1369
        $filesize_new = 0;
1370
        // don't remove old meta info as it should be saved, plugins can use
1371
        // IO_WIKIPAGE_WRITE for removing their metadata...
1372
        // purge non-persistant meta data
1373
        p_purge_metadata($id);
1374
        // remove empty namespaces
1375
        io_sweepNS($id, 'datadir');
1376
        io_sweepNS($id, 'mediadir');
1377
    } else {
1378
        // save file (namespace dir is created in io_writeWikiPage)
1379
        io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
1380
        // pre-save the revision, to keep the attic in sync
1381
        $svdta['newRevision'] = saveOldRevision($id);
1382
        $filesize_new = filesize($svdta['file']);
1383
    }
1384
    $svdta['sizechange'] = $filesize_new - $filesize_old;
1385
1386
    $event->advise_after();
1387
1388
    addLogEntry(
1389
        $svdta['newRevision'],
1390
        $svdta['id'],
1391
        $svdta['changeType'],
1392
        $svdta['summary'],
1393
        $svdta['changeInfo'],
1394
        null,
1395
        $svdta['sizechange']
1396
    );
1397
1398
    // send notify mails
1399
    notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
1400
    notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
1401
1402
    // update the purgefile (timestamp of the last time anything within the wiki was changed)
1403
    io_saveFile($conf['cachedir'].'/purgefile', time());
1404
1405
    // if useheading is enabled, purge the cache of all linking pages
1406
    if(useHeading('content')) {
1407
        $pages = ft_backlinks($id, true);
1408
        foreach($pages as $page) {
1409
            $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
1410
            $cache->removeCache();
1411
        }
1412
    }
1413
}
1414
1415
/**
1416
 * moves the current version to the attic and returns its
1417
 * revision date
1418
 *
1419
 * @author Andreas Gohr <[email protected]>
1420
 *
1421
 * @param string $id page id
1422
 * @return int|string revision timestamp
1423
 */
1424
function saveOldRevision($id) {
1425
    $oldf = wikiFN($id);
1426
    if(!file_exists($oldf)) return '';
1427
    $date = filemtime($oldf);
1428
    $newf = wikiFN($id, $date);
1429
    io_writeWikiPage($newf, rawWiki($id), $id, $date);
1430
    return $date;
1431
}
1432
1433
/**
1434
 * Sends a notify mail on page change or registration
1435
 *
1436
 * @param string     $id       The changed page
1437
 * @param string     $who      Who to notify (admin|subscribers|register)
1438
 * @param int|string $rev Old page revision
1439
 * @param string     $summary  What changed
1440
 * @param boolean    $minor    Is this a minor edit?
1441
 * @param string[]   $replace  Additional string substitutions, @KEY@ to be replaced by value
1442
 * @param int|string $current_rev  New page revision
1443
 * @return bool
1444
 *
1445
 * @author Andreas Gohr <[email protected]>
1446
 */
1447
function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) {
1448
    global $conf;
1449
    /* @var Input $INPUT */
1450
    global $INPUT;
1451
1452
    // decide if there is something to do, eg. whom to mail
1453
    if($who == 'admin') {
1454
        if(empty($conf['notify'])) return false; //notify enabled?
1455
        $tpl = 'mailtext';
1456
        $to  = $conf['notify'];
1457
    } elseif($who == 'subscribers') {
1458
        if(!actionOK('subscribe')) return false; //subscribers enabled?
1459
        if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
1460
        $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
1461
        Event::createAndTrigger(
1462
            'COMMON_NOTIFY_ADDRESSLIST', $data,
1463
            array(new SubscriberManager(), 'notifyAddresses')
1464
        );
1465
        $to = $data['addresslist'];
1466
        if(empty($to)) return false;
1467
        $tpl = 'subscr_single';
1468
    } else {
1469
        return false; //just to be safe
1470
    }
1471
1472
    // prepare content
1473
    $subscription = new PageSubscriptionSender();
1474
    return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev);
1475
}
1476
1477
/**
1478
 * extracts the query from a search engine referrer
1479
 *
1480
 * @author Andreas Gohr <[email protected]>
1481
 * @author Todd Augsburger <[email protected]>
1482
 *
1483
 * @return array|string
1484
 */
1485
function getGoogleQuery() {
1486
    /* @var Input $INPUT */
1487
    global $INPUT;
1488
1489
    if(!$INPUT->server->has('HTTP_REFERER')) {
1490
        return '';
1491
    }
1492
    $url = parse_url($INPUT->server->str('HTTP_REFERER'));
1493
1494
    // only handle common SEs
1495
    if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
1496
1497
    $query = array();
1498
    parse_str($url['query'], $query);
1499
1500
    $q = '';
1501
    if(isset($query['q'])){
1502
        $q = $query['q'];
1503
    }elseif(isset($query['p'])){
1504
        $q = $query['p'];
1505
    }elseif(isset($query['query'])){
1506
        $q = $query['query'];
1507
    }
1508
    $q = trim($q);
1509
1510
    if(!$q) return '';
1511
    // ignore if query includes a full URL
1512
    if(strpos($q, '//') !== false) return '';
1513
    $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
1514
    return $q;
1515
}
1516
1517
/**
1518
 * Return the human readable size of a file
1519
 *
1520
 * @param int $size A file size
1521
 * @param int $dec A number of decimal places
1522
 * @return string human readable size
1523
 *
1524
 * @author      Martin Benjamin <[email protected]>
1525
 * @author      Aidan Lister <[email protected]>
1526
 * @version     1.0.0
1527
 */
1528
function filesize_h($size, $dec = 1) {
1529
    $sizes = array('B', 'KB', 'MB', 'GB');
1530
    $count = count($sizes);
1531
    $i     = 0;
1532
1533
    while($size >= 1024 && ($i < $count - 1)) {
1534
        $size /= 1024;
1535
        $i++;
1536
    }
1537
1538
    return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space
1539
}
1540
1541
/**
1542
 * Return the given timestamp as human readable, fuzzy age
1543
 *
1544
 * @author Andreas Gohr <[email protected]>
1545
 *
1546
 * @param int $dt timestamp
1547
 * @return string
1548
 */
1549
function datetime_h($dt) {
1550
    global $lang;
1551
1552
    $ago = time() - $dt;
1553
    if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
1554
        return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
1555
    }
1556
    if($ago > 24 * 60 * 60 * 30 * 2) {
1557
        return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
1558
    }
1559
    if($ago > 24 * 60 * 60 * 7 * 2) {
1560
        return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
1561
    }
1562
    if($ago > 24 * 60 * 60 * 2) {
1563
        return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
1564
    }
1565
    if($ago > 60 * 60 * 2) {
1566
        return sprintf($lang['hours'], round($ago / (60 * 60)));
1567
    }
1568
    if($ago > 60 * 2) {
1569
        return sprintf($lang['minutes'], round($ago / (60)));
1570
    }
1571
    return sprintf($lang['seconds'], $ago);
1572
}
1573
1574
/**
1575
 * Wraps around strftime but provides support for fuzzy dates
1576
 *
1577
 * The format default to $conf['dformat']. It is passed to
1578
 * strftime - %f can be used to get the value from datetime_h()
1579
 *
1580
 * @see datetime_h
1581
 * @author Andreas Gohr <[email protected]>
1582
 *
1583
 * @param int|null $dt      timestamp when given, null will take current timestamp
1584
 * @param string   $format  empty default to $conf['dformat'], or provide format as recognized by strftime()
1585
 * @return string
1586
 */
1587
function dformat($dt = null, $format = '') {
1588
    global $conf;
1589
1590
    if(is_null($dt)) $dt = time();
1591
    $dt = (int) $dt;
1592
    if(!$format) $format = $conf['dformat'];
1593
1594
    $format = str_replace('%f', datetime_h($dt), $format);
1595
    return strftime($format, $dt);
1596
}
1597
1598
/**
1599
 * Formats a timestamp as ISO 8601 date
1600
 *
1601
 * @author <ungu at terong dot com>
1602
 * @link http://php.net/manual/en/function.date.php#54072
1603
 *
1604
 * @param int $int_date current date in UNIX timestamp
1605
 * @return string
1606
 */
1607
function date_iso8601($int_date) {
1608
    $date_mod     = date('Y-m-d\TH:i:s', $int_date);
1609
    $pre_timezone = date('O', $int_date);
1610
    $time_zone    = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2);
1611
    $date_mod .= $time_zone;
1612
    return $date_mod;
1613
}
1614
1615
/**
1616
 * return an obfuscated email address in line with $conf['mailguard'] setting
1617
 *
1618
 * @author Harry Fuecks <[email protected]>
1619
 * @author Christopher Smith <[email protected]>
1620
 *
1621
 * @param string $email email address
1622
 * @return string
1623
 */
1624
function obfuscate($email) {
1625
    global $conf;
1626
1627
    switch($conf['mailguard']) {
1628
        case 'visible' :
1629
            $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1630
            return strtr($email, $obfuscate);
1631
1632
        case 'hex' :
1633
            return \dokuwiki\Utf8\Conversion::toHtml($email, true);
1634
1635
        case 'none' :
1636
        default :
1637
            return $email;
1638
    }
1639
}
1640
1641
/**
1642
 * Removes quoting backslashes
1643
 *
1644
 * @author Andreas Gohr <[email protected]>
1645
 *
1646
 * @param string $string
1647
 * @param string $char backslashed character
1648
 * @return string
1649
 */
1650
function unslash($string, $char = "'") {
1651
    return str_replace('\\'.$char, $char, $string);
1652
}
1653
1654
/**
1655
 * Convert php.ini shorthands to byte
1656
 *
1657
 * On 32 bit systems values >= 2GB will fail!
1658
 *
1659
 * -1 (infinite size) will be reported as -1
1660
 *
1661
 * @link   https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
1662
 * @param string $value PHP size shorthand
1663
 * @return int
1664
 */
1665
function php_to_byte($value) {
1666
    switch (strtoupper(substr($value,-1))) {
1667
        case 'G':
1668
            $ret = intval(substr($value, 0, -1)) * 1024 * 1024 * 1024;
1669
            break;
1670
        case 'M':
1671
            $ret = intval(substr($value, 0, -1)) * 1024 * 1024;
1672
            break;
1673
        case 'K':
1674
            $ret = intval(substr($value, 0, -1)) * 1024;
1675
            break;
1676
        default:
1677
            $ret = intval($value);
1678
            break;
1679
    }
1680
    return $ret;
1681
}
1682
1683
/**
1684
 * Wrapper around preg_quote adding the default delimiter
1685
 *
1686
 * @param string $string
1687
 * @return string
1688
 */
1689
function preg_quote_cb($string) {
1690
    return preg_quote($string, '/');
1691
}
1692
1693
/**
1694
 * Shorten a given string by removing data from the middle
1695
 *
1696
 * You can give the string in two parts, the first part $keep
1697
 * will never be shortened. The second part $short will be cut
1698
 * in the middle to shorten but only if at least $min chars are
1699
 * left to display it. Otherwise it will be left off.
1700
 *
1701
 * @param string $keep   the part to keep
1702
 * @param string $short  the part to shorten
1703
 * @param int    $max    maximum chars you want for the whole string
1704
 * @param int    $min    minimum number of chars to have left for middle shortening
1705
 * @param string $char   the shortening character to use
1706
 * @return string
1707
 */
1708
function shorten($keep, $short, $max, $min = 9, $char = '…') {
1709
    $max = $max - \dokuwiki\Utf8\PhpString::strlen($keep);
1710
    if($max < $min) return $keep;
1711
    $len = \dokuwiki\Utf8\PhpString::strlen($short);
1712
    if($len <= $max) return $keep.$short;
1713
    $half = floor($max / 2);
1714
    return $keep .
1715
        \dokuwiki\Utf8\PhpString::substr($short, 0, $half - 1) .
1716
        $char .
1717
        \dokuwiki\Utf8\PhpString::substr($short, $len - $half);
1718
}
1719
1720
/**
1721
 * Return the users real name or e-mail address for use
1722
 * in page footer and recent changes pages
1723
 *
1724
 * @param string|null $username or null when currently logged-in user should be used
1725
 * @param bool $textonly true returns only plain text, true allows returning html
1726
 * @return string html or plain text(not escaped) of formatted user name
1727
 *
1728
 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1729
 */
1730
function editorinfo($username, $textonly = false) {
1731
    return userlink($username, $textonly);
1732
}
1733
1734
/**
1735
 * Returns users realname w/o link
1736
 *
1737
 * @param string|null $username or null when currently logged-in user should be used
1738
 * @param bool $textonly true returns only plain text, true allows returning html
1739
 * @return string html or plain text(not escaped) of formatted user name
1740
 *
1741
 * @triggers COMMON_USER_LINK
1742
 */
1743
function userlink($username = null, $textonly = false) {
1744
    global $conf, $INFO;
1745
    /** @var AuthPlugin $auth */
1746
    global $auth;
1747
    /** @var Input $INPUT */
1748
    global $INPUT;
1749
1750
    // prepare initial event data
1751
    $data = array(
1752
        'username' => $username, // the unique user name
1753
        'name' => '',
1754
        'link' => array( //setting 'link' to false disables linking
1755
                         'target' => '',
1756
                         'pre' => '',
1757
                         'suf' => '',
1758
                         'style' => '',
1759
                         'more' => '',
1760
                         'url' => '',
1761
                         'title' => '',
1762
                         'class' => ''
1763
        ),
1764
        'userlink' => '', // formatted user name as will be returned
1765
        'textonly' => $textonly
1766
    );
1767
    if($username === null) {
1768
        $data['username'] = $username = $INPUT->server->str('REMOTE_USER');
1769
        if($textonly){
1770
            $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')';
1771
        }else {
1772
            $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> '.
1773
                '(<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)';
1774
        }
1775
    }
1776
1777
    $evt = new Event('COMMON_USER_LINK', $data);
1778
    if($evt->advise_before(true)) {
1779
        if(empty($data['name'])) {
1780
            if($auth) $info = $auth->getUserData($username);
1781
            if($conf['showuseras'] != 'loginname' && isset($info) && $info) {
1782
                switch($conf['showuseras']) {
1783
                    case 'username':
1784
                    case 'username_link':
1785
                        $data['name'] = $textonly ? $info['name'] : hsc($info['name']);
1786
                        break;
1787
                    case 'email':
1788
                    case 'email_link':
1789
                        $data['name'] = obfuscate($info['mail']);
1790
                        break;
1791
                }
1792
            } else {
1793
                $data['name'] = $textonly ? $data['username'] : hsc($data['username']);
1794
            }
1795
        }
1796
1797
        /** @var Doku_Renderer_xhtml $xhtml_renderer */
1798
        static $xhtml_renderer = null;
1799
1800
        if(!$data['textonly'] && empty($data['link']['url'])) {
1801
1802
            if(in_array($conf['showuseras'], array('email_link', 'username_link'))) {
1803
                if(!isset($info)) {
1804
                    if($auth) $info = $auth->getUserData($username);
1805
                }
1806
                if(isset($info) && $info) {
1807
                    if($conf['showuseras'] == 'email_link') {
1808
                        $data['link']['url'] = 'mailto:' . obfuscate($info['mail']);
1809
                    } else {
1810
                        if(is_null($xhtml_renderer)) {
1811
                            $xhtml_renderer = p_get_renderer('xhtml');
1812
                        }
1813
                        if(empty($xhtml_renderer->interwiki)) {
1814
                            $xhtml_renderer->interwiki = getInterwiki();
1815
                        }
1816
                        $shortcut = 'user';
1817
                        $exists = null;
1818
                        $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists);
1819
                        $data['link']['class'] .= ' interwiki iw_user';
1820
                        if($exists !== null) {
1821
                            if($exists) {
1822
                                $data['link']['class'] .= ' wikilink1';
1823
                            } else {
1824
                                $data['link']['class'] .= ' wikilink2';
1825
                                $data['link']['rel'] = 'nofollow';
1826
                            }
1827
                        }
1828
                    }
1829
                } else {
1830
                    $data['textonly'] = true;
1831
                }
1832
1833
            } else {
1834
                $data['textonly'] = true;
1835
            }
1836
        }
1837
1838
        if($data['textonly']) {
1839
            $data['userlink'] = $data['name'];
1840
        } else {
1841
            $data['link']['name'] = $data['name'];
1842
            if(is_null($xhtml_renderer)) {
1843
                $xhtml_renderer = p_get_renderer('xhtml');
1844
            }
1845
            $data['userlink'] = $xhtml_renderer->_formatLink($data['link']);
1846
        }
1847
    }
1848
    $evt->advise_after();
1849
    unset($evt);
1850
1851
    return $data['userlink'];
1852
}
1853
1854
/**
1855
 * Returns the path to a image file for the currently chosen license.
1856
 * When no image exists, returns an empty string
1857
 *
1858
 * @author Andreas Gohr <[email protected]>
1859
 *
1860
 * @param  string $type - type of image 'badge' or 'button'
1861
 * @return string
1862
 */
1863
function license_img($type) {
1864
    global $license;
1865
    global $conf;
1866
    if(!$conf['license']) return '';
1867
    if(!is_array($license[$conf['license']])) return '';
1868
    $try   = array();
1869
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1870
    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1871
    if(substr($conf['license'], 0, 3) == 'cc-') {
1872
        $try[] = 'lib/images/license/'.$type.'/cc.png';
1873
    }
1874
    foreach($try as $src) {
1875
        if(file_exists(DOKU_INC.$src)) return $src;
1876
    }
1877
    return '';
1878
}
1879
1880
/**
1881
 * Checks if the given amount of memory is available
1882
 *
1883
 * If the memory_get_usage() function is not available the
1884
 * function just assumes $bytes of already allocated memory
1885
 *
1886
 * @author Filip Oscadal <[email protected]>
1887
 * @author Andreas Gohr <[email protected]>
1888
 *
1889
 * @param int  $mem    Size of memory you want to allocate in bytes
1890
 * @param int  $bytes  already allocated memory (see above)
1891
 * @return bool
1892
 */
1893
function is_mem_available($mem, $bytes = 1048576) {
1894
    $limit = trim(ini_get('memory_limit'));
1895
    if(empty($limit)) return true; // no limit set!
1896
    if($limit == -1) return true; // unlimited
1897
1898
    // parse limit to bytes
1899
    $limit = php_to_byte($limit);
1900
1901
    // get used memory if possible
1902
    if(function_exists('memory_get_usage')) {
1903
        $used = memory_get_usage();
1904
    } else {
1905
        $used = $bytes;
1906
    }
1907
1908
    if($used + $mem > $limit) {
1909
        return false;
1910
    }
1911
1912
    return true;
1913
}
1914
1915
/**
1916
 * Send a HTTP redirect to the browser
1917
 *
1918
 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1919
 *
1920
 * @link   http://support.microsoft.com/kb/q176113/
1921
 * @author Andreas Gohr <[email protected]>
1922
 *
1923
 * @param string $url url being directed to
1924
 */
1925
function send_redirect($url) {
1926
    $url = stripctl($url); // defend against HTTP Response Splitting
1927
1928
    /* @var Input $INPUT */
1929
    global $INPUT;
1930
1931
    //are there any undisplayed messages? keep them in session for display
1932
    global $MSG;
1933
    if(isset($MSG) && count($MSG) && !defined('NOSESSION')) {
1934
        //reopen session, store data and close session again
1935
        @session_start();
1936
        $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
1937
    }
1938
1939
    // always close the session
1940
    session_write_close();
1941
1942
    // check if running on IIS < 6 with CGI-PHP
1943
    if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') &&
1944
        (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) &&
1945
        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) &&
1946
        $matches[1] < 6
1947
    ) {
1948
        header('Refresh: 0;url='.$url);
1949
    } else {
1950
        header('Location: '.$url);
1951
    }
1952
1953
    // no exits during unit tests
1954
    if(defined('DOKU_UNITTEST')) {
1955
        // pass info about the redirect back to the test suite
1956
        $testRequest = TestRequest::getRunning();
1957
        if($testRequest !== null) {
1958
            $testRequest->addData('send_redirect', $url);
1959
        }
1960
        return;
1961
    }
1962
1963
    exit;
1964
}
1965
1966
/**
1967
 * Validate a value using a set of valid values
1968
 *
1969
 * This function checks whether a specified value is set and in the array
1970
 * $valid_values. If not, the function returns a default value or, if no
1971
 * default is specified, throws an exception.
1972
 *
1973
 * @param string $param        The name of the parameter
1974
 * @param array  $valid_values A set of valid values; Optionally a default may
1975
 *                             be marked by the key “default”.
1976
 * @param array  $array        The array containing the value (typically $_POST
1977
 *                             or $_GET)
1978
 * @param string $exc          The text of the raised exception
1979
 *
1980
 * @throws Exception
1981
 * @return mixed
1982
 * @author Adrian Lang <[email protected]>
1983
 */
1984
function valid_input_set($param, $valid_values, $array, $exc = '') {
1985
    if(isset($array[$param]) && in_array($array[$param], $valid_values)) {
1986
        return $array[$param];
1987
    } elseif(isset($valid_values['default'])) {
1988
        return $valid_values['default'];
1989
    } else {
1990
        throw new Exception($exc);
1991
    }
1992
}
1993
1994
/**
1995
 * Read a preference from the DokuWiki cookie
1996
 * (remembering both keys & values are urlencoded)
1997
 *
1998
 * @param string $pref     preference key
1999
 * @param mixed  $default  value returned when preference not found
2000
 * @return string preference value
2001
 */
2002
function get_doku_pref($pref, $default) {
2003
    $enc_pref = urlencode($pref);
2004
    if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) {
2005
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2006
        $cnt   = count($parts);
2007
2008
        // due to #2721 there might be duplicate entries,
2009
        // so we read from the end
2010
        for($i = $cnt-2; $i >= 0; $i -= 2) {
2011
            if($parts[$i] == $enc_pref) {
2012
                return urldecode($parts[$i + 1]);
2013
            }
2014
        }
2015
    }
2016
    return $default;
2017
}
2018
2019
/**
2020
 * Add a preference to the DokuWiki cookie
2021
 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded)
2022
 * Remove it by setting $val to false
2023
 *
2024
 * @param string $pref  preference key
2025
 * @param string $val   preference value
2026
 */
2027
function set_doku_pref($pref, $val) {
2028
    global $conf;
2029
    $orig = get_doku_pref($pref, false);
2030
    $cookieVal = '';
2031
2032
    if($orig !== false && ($orig !== $val)) {
2033
        $parts = explode('#', $_COOKIE['DOKU_PREFS']);
2034
        $cnt   = count($parts);
2035
        // urlencode $pref for the comparison
2036
        $enc_pref = rawurlencode($pref);
2037
        $seen = false;
2038
        for ($i = 0; $i < $cnt; $i += 2) {
2039
            if ($parts[$i] == $enc_pref) {
2040
                if (!$seen){
2041
                    if ($val !== false) {
2042
                        $parts[$i + 1] = rawurlencode($val);
2043
                    } else {
2044
                        unset($parts[$i]);
2045
                        unset($parts[$i + 1]);
2046
                    }
2047
                    $seen = true;
2048
                } else {
2049
                    // no break because we want to remove duplicate entries
2050
                    unset($parts[$i]);
2051
                    unset($parts[$i + 1]);
2052
                }
2053
            }
2054
        }
2055
        $cookieVal = implode('#', $parts);
2056
    } else if ($orig === false && $val !== false) {
2057
        $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'] . '#' : '') .
2058
            rawurlencode($pref) . '#' . rawurlencode($val);
2059
    }
2060
2061
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
2062
    if(defined('DOKU_UNITTEST')) {
2063
        $_COOKIE['DOKU_PREFS'] = $cookieVal;
2064
    }else{
2065
        setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
2066
    }
2067
}
2068
2069
/**
2070
 * Strips source mapping declarations from given text #601
2071
 *
2072
 * @param string &$text reference to the CSS or JavaScript code to clean
2073
 */
2074
function stripsourcemaps(&$text){
2075
    $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text);
2076
}
2077
2078
/**
2079
 * Returns the contents of a given SVG file for embedding
2080
 *
2081
 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through
2082
 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small
2083
 * files are embedded.
2084
 *
2085
 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG!
2086
 *
2087
 * @param string $file full path to the SVG file
2088
 * @param int $maxsize maximum allowed size for the SVG to be embedded
2089
 * @return string|false the SVG content, false if the file couldn't be loaded
2090
 */
2091
function inlineSVG($file, $maxsize = 2048) {
2092
    $file = trim($file);
2093
    if($file === '') return false;
2094
    if(!file_exists($file)) return false;
2095
    if(filesize($file) > $maxsize) return false;
2096
    if(!is_readable($file)) return false;
2097
    $content = file_get_contents($file);
2098
    $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments
2099
    $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header
2100
    $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type
2101
    $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags
2102
    $content = trim($content);
2103
    if(substr($content, 0, 5) !== '<svg ') return false;
2104
    return $content;
2105
}
2106
2107
//Setup VIM: ex: et ts=2 :
2108