1<?php
2
3/**
4 * File IO functions
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Andreas Gohr <andi@splitbrain.org>
8 */
9
10use dokuwiki\Logger;
11use dokuwiki\Utf8\PhpString;
12use dokuwiki\HTTP\DokuHTTPClient;
13use dokuwiki\Extension\Event;
14
15/**
16 * Removes empty directories
17 *
18 * Sends IO_NAMESPACE_DELETED events for 'pages' and 'media' namespaces.
19 * Event data:
20 * $data[0]    ns: The colon separated namespace path minus the trailing page name.
21 * $data[1]    ns_type: 'pages' or 'media' namespace tree.
22 *
23 * @param string $id - a pageid, the namespace of that id will be tried to deleted
24 * @param string $basedir - the config name of the type to delete (datadir or mediadir usally)
25 * @return bool - true if at least one namespace was deleted
26 *
27 * @author  Andreas Gohr <andi@splitbrain.org>
28 * @author Ben Coburn <btcoburn@silicodon.net>
29 */
30function io_sweepNS($id, $basedir = 'datadir')
31{
32    global $conf;
33    $types = ['datadir' => 'pages', 'mediadir' => 'media'];
34    $ns_type = ($types[$basedir] ?? false);
35
36    $delone = false;
37
38    //scan all namespaces
39    while (($id = getNS($id)) !== false) {
40        $dir = $conf[$basedir] . '/' . utf8_encodeFN(str_replace(':', '/', $id));
41
42        //try to delete dir else return
43        if (@rmdir($dir)) {
44            if ($ns_type !== false) {
45                $data = [$id, $ns_type];
46                $delone = true; // we deleted at least one dir
47                Event::createAndTrigger('IO_NAMESPACE_DELETED', $data);
48            }
49        } else {
50            return $delone;
51        }
52    }
53    return $delone;
54}
55
56/**
57 * Used to read in a DokuWiki page from file, and send IO_WIKIPAGE_READ events.
58 *
59 * Generates the action event which delegates to io_readFile().
60 * Action plugins are allowed to modify the page content in transit.
61 * The file path should not be changed.
62 *
63 * Event data:
64 * $data[0]    The raw arguments for io_readFile as an array.
65 * $data[1]    ns: The colon separated namespace path minus the trailing page name. (false if root ns)
66 * $data[2]    page_name: The wiki page name.
67 * $data[3]    rev: The page revision, false for current wiki pages.
68 *
69 * @param string $file filename
70 * @param string $id page id
71 * @param bool|int|string $rev revision timestamp
72 * @return string
73 *
74 * @author Ben Coburn <btcoburn@silicodon.net>
75 */
76function io_readWikiPage($file, $id, $rev = false)
77{
78    if (empty($rev)) {
79        $rev = false;
80    }
81    $data = [[$file, true], getNS($id), noNS($id), $rev];
82    return Event::createAndTrigger('IO_WIKIPAGE_READ', $data, '_io_readWikiPage_action', false);
83}
84
85/**
86 * Callback adapter for io_readFile().
87 *
88 * @param array $data event data
89 * @return string
90 *
91 * @author Ben Coburn <btcoburn@silicodon.net>
92 */
93function _io_readWikiPage_action($data)
94{
95    if (is_array($data) && is_array($data[0]) && count($data[0]) === 2) {
96        return io_readFile(...$data[0]);
97    } else {
98        return ''; //callback error
99    }
100}
101
102/**
103 * Returns content of $file as cleaned string.
104 *
105 * Uses gzip if extension is .gz
106 *
107 * If you want to use the returned value in unserialize
108 * be sure to set $clean to false!
109 *
110 *
111 * @param string $file filename
112 * @param bool $clean
113 * @return string|bool the file contents or false on error
114 *
115 * @author  Andreas Gohr <andi@splitbrain.org>
116 */
117function io_readFile($file, $clean = true)
118{
119    $ret = '';
120    if (file_exists($file)) {
121        if (str_ends_with($file, '.gz')) {
122            if (!DOKU_HAS_GZIP) return false;
123            $ret = gzfile($file);
124            if (is_array($ret)) {
125                $ret = implode('', $ret);
126            }
127        } elseif (str_ends_with($file, '.bz2')) {
128            if (!DOKU_HAS_BZIP) return false;
129            $ret = bzfile($file);
130        } else {
131            $ret = file_get_contents($file);
132        }
133    }
134    if ($ret === null) return false;
135    if ($ret !== false && $clean) {
136        return cleanText($ret);
137    } else {
138        return $ret;
139    }
140}
141
142/**
143 * Returns the content of a .bz2 compressed file as string
144 *
145 * @param string $file filename
146 * @param bool $array return array of lines
147 * @return string|array|bool content or false on error
148 *
149 * @author marcel senf <marcel@rucksackreinigung.de>
150 * @author  Andreas Gohr <andi@splitbrain.org>
151 */
152function bzfile($file, $array = false)
153{
154    $bz = bzopen($file, "r");
155    if ($bz === false) return false;
156
157    if ($array) {
158        $lines = [];
159    }
160    $str = '';
161    while (!feof($bz)) {
162        //8192 seems to be the maximum buffersize?
163        $buffer = bzread($bz, 8192);
164        if (($buffer === false) || (bzerrno($bz) !== 0)) {
165            return false;
166        }
167        $str .= $buffer;
168        if ($array) {
169            $pos = strpos($str, "\n");
170            while ($pos !== false) {
171                $lines[] = substr($str, 0, $pos + 1);
172                $str = substr($str, $pos + 1);
173                $pos = strpos($str, "\n");
174            }
175        }
176    }
177    bzclose($bz);
178    if ($array) {
179        if ($str !== '') {
180            $lines[] = $str;
181        }
182        return $lines;
183    }
184    return $str;
185}
186
187/**
188 * Used to write out a DokuWiki page to file, and send IO_WIKIPAGE_WRITE events.
189 *
190 * This generates an action event and delegates to io_saveFile().
191 * Action plugins are allowed to modify the page content in transit.
192 * The file path should not be changed.
193 * (The append parameter is set to false.)
194 *
195 * Event data:
196 * $data[0]    The raw arguments for io_saveFile as an array.
197 * $data[1]    ns: The colon separated namespace path minus the trailing page name. (false if root ns)
198 * $data[2]    page_name: The wiki page name.
199 * $data[3]    rev: The page revision, false for current wiki pages.
200 *
201 * @param string $file filename
202 * @param string $content
203 * @param string $id page id
204 * @param int|bool|string $rev timestamp of revision
205 * @return bool
206 *
207 * @author Ben Coburn <btcoburn@silicodon.net>
208 */
209function io_writeWikiPage($file, $content, $id, $rev = false)
210{
211    if (empty($rev)) {
212        $rev = false;
213    }
214    if ($rev === false) {
215        io_createNamespace($id); // create namespaces as needed
216    }
217    $data = [[$file, $content, false], getNS($id), noNS($id), $rev];
218    return Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data, '_io_writeWikiPage_action', false);
219}
220
221/**
222 * Callback adapter for io_saveFile().
223 *
224 * @param array $data event data
225 * @return bool
226 *
227 * @author Ben Coburn <btcoburn@silicodon.net>
228 */
229function _io_writeWikiPage_action($data)
230{
231    if (is_array($data) && is_array($data[0]) && count($data[0]) === 3) {
232        $ok = io_saveFile(...$data[0]);
233        // for attic files make sure the file has the mtime of the revision
234        if ($ok && is_int($data[3]) && $data[3] > 0) {
235            @touch($data[0][0], $data[3]);
236        }
237        return $ok;
238    } else {
239        return false; //callback error
240    }
241}
242
243/**
244 * Internal function to save contents to a file.
245 *
246 * @param string $file filename path to file
247 * @param string $content
248 * @param bool $append
249 * @return bool true on success, otherwise false
250 *
251 * @author  Andreas Gohr <andi@splitbrain.org>
252 */
253function _io_saveFile($file, $content, $append)
254{
255    global $conf;
256    $mode = ($append) ? 'ab' : 'wb';
257    $fileexists = file_exists($file);
258
259    if (str_ends_with($file, '.gz')) {
260        if (!DOKU_HAS_GZIP) return false;
261        $fh = @gzopen($file, $mode . '9');
262        if (!$fh) return false;
263        gzwrite($fh, $content);
264        gzclose($fh);
265    } elseif (str_ends_with($file, '.bz2')) {
266        if (!DOKU_HAS_BZIP) return false;
267        if ($append) {
268            $bzcontent = bzfile($file);
269            if ($bzcontent === false) return false;
270            $content = $bzcontent . $content;
271        }
272        $fh = @bzopen($file, 'w');
273        if (!$fh) return false;
274        bzwrite($fh, $content);
275        bzclose($fh);
276    } else {
277        $fh = @fopen($file, $mode);
278        if (!$fh) return false;
279        fwrite($fh, $content);
280        fclose($fh);
281    }
282
283    if (!$fileexists && $conf['fperm']) {
284        chmod($file, $conf['fperm']);
285    }
286    return true;
287}
288
289/**
290 * Saves $content to $file.
291 *
292 * If the third parameter is set to true the given content
293 * will be appended.
294 *
295 * Uses gzip if extension is .gz
296 * and bz2 if extension is .bz2
297 *
298 * @param string $file filename path to file
299 * @param string $content
300 * @param bool $append
301 * @return bool true on success, otherwise false
302 *
303 * @author  Andreas Gohr <andi@splitbrain.org>
304 */
305function io_saveFile($file, $content, $append = false)
306{
307    io_makeFileDir($file);
308    io_lock($file);
309    if (!_io_saveFile($file, $content, $append)) {
310        msg("Writing $file failed", -1);
311        io_unlock($file);
312        return false;
313    }
314    io_unlock($file);
315    return true;
316}
317
318/**
319 * Replace one or more occurrences of a line in a file.
320 *
321 * The default, when $maxlines is 0 is to delete all matching lines then append a single line.
322 * A regex that matches any part of the line will remove the entire line in this mode.
323 * Captures in $newline are not available.
324 *
325 * Otherwise each line is matched and replaced individually, up to the first $maxlines lines
326 * or all lines if $maxlines is -1. If $regex is true then captures can be used in $newline.
327 *
328 * Be sure to include the trailing newline in $oldline when replacing entire lines.
329 *
330 * Uses gzip if extension is .gz
331 * and bz2 if extension is .bz2
332 *
333 * @param string $file filename
334 * @param string $oldline exact linematch to remove
335 * @param string $newline new line to insert
336 * @param bool $regex use regexp?
337 * @param int $maxlines number of occurrences of the line to replace
338 * @return bool true on success
339 *
340 * @author Steven Danz <steven-danz@kc.rr.com>
341 * @author Christopher Smith <chris@jalakai.co.uk>
342 * @author Patrick Brown <ptbrown@whoopdedo.org>
343 */
344function io_replaceInFile($file, $oldline, $newline, $regex = false, $maxlines = 0)
345{
346    if ((string)$oldline === '') {
347        Logger::error('io_replaceInFile() $oldline parameter cannot be empty');
348        return false;
349    }
350
351    if (!file_exists($file)) return true;
352
353    io_lock($file);
354
355    // load into array
356    if (str_ends_with($file, '.gz')) {
357        if (!DOKU_HAS_GZIP) return false;
358        $lines = gzfile($file);
359    } elseif (str_ends_with($file, '.bz2')) {
360        if (!DOKU_HAS_BZIP) return false;
361        $lines = bzfile($file, true);
362    } else {
363        $lines = file($file);
364    }
365
366    // make non-regexes into regexes
367    $pattern = $regex ? $oldline : '/^' . preg_quote($oldline, '/') . '$/';
368    $replace = $regex ? $newline : addcslashes($newline, '\$');
369
370    // remove matching lines
371    if ($maxlines > 0) {
372        $count = 0;
373        $matched = 0;
374        foreach ($lines as $i => $line) {
375            if ($count >= $maxlines) break;
376            // $matched will be set to 0|1 depending on whether pattern is matched and line replaced
377            $lines[$i] = preg_replace($pattern, $replace, $line, -1, $matched);
378            if ($matched) {
379                $count++;
380            }
381        }
382    } elseif ($maxlines == 0) {
383        $lines = preg_grep($pattern, $lines, PREG_GREP_INVERT);
384        if ((string)$newline !== '') {
385            $lines[] = $newline;
386        }
387    } else {
388        $lines = preg_replace($pattern, $replace, $lines);
389    }
390
391    if (count($lines)) {
392        if (!_io_saveFile($file, implode('', $lines), false)) {
393            msg("Removing content from $file failed", -1);
394            io_unlock($file);
395            return false;
396        }
397    } else {
398        @unlink($file);
399    }
400
401    io_unlock($file);
402    return true;
403}
404
405/**
406 * Delete lines that match $badline from $file.
407 *
408 * Be sure to include the trailing newline in $badline
409 *
410 * @param string $file filename
411 * @param string $badline exact linematch to remove
412 * @param bool $regex use regexp?
413 * @return bool true on success
414 *
415 * @author Patrick Brown <ptbrown@whoopdedo.org>
416 */
417function io_deleteFromFile($file, $badline, $regex = false)
418{
419    return io_replaceInFile($file, $badline, '', $regex, 0);
420}
421
422/**
423 * Tries to lock a file
424 *
425 * Locking is only done for io_savefile and uses directories
426 * inside $conf['lockdir']
427 *
428 * It waits maximal 3 seconds for the lock, after this time
429 * the lock is assumed to be stale and the function goes on
430 *
431 * @param string $file filename
432 *
433 * @author Andreas Gohr <andi@splitbrain.org>
434 */
435function io_lock($file)
436{
437    global $conf;
438
439    $lockDir = $conf['lockdir'] . '/' . md5($file);
440    @ignore_user_abort(1);
441
442    $timeStart = time();
443    do {
444        //waited longer than 3 seconds? -> stale lock
445        if ((time() - $timeStart) > 3) break;
446        $locked = @mkdir($lockDir);
447        if ($locked) {
448            if ($conf['dperm']) {
449                chmod($lockDir, $conf['dperm']);
450            }
451            break;
452        }
453        usleep(50);
454    } while ($locked === false);
455}
456
457/**
458 * Unlocks a file
459 *
460 * @param string $file filename
461 *
462 * @author Andreas Gohr <andi@splitbrain.org>
463 */
464function io_unlock($file)
465{
466    global $conf;
467
468    $lockDir = $conf['lockdir'] . '/' . md5($file);
469    @rmdir($lockDir);
470    @ignore_user_abort(0);
471}
472
473/**
474 * Create missing namespace directories and send the IO_NAMESPACE_CREATED events
475 * in the order of directory creation. (Parent directories first.)
476 *
477 * Event data:
478 * $data[0]    ns: The colon separated namespace path minus the trailing page name.
479 * $data[1]    ns_type: 'pages' or 'media' namespace tree.
480 *
481 * @param string $id page id
482 * @param string $ns_type 'pages' or 'media'
483 *
484 * @author Ben Coburn <btcoburn@silicodon.net>
485 */
486function io_createNamespace($id, $ns_type = 'pages')
487{
488    // verify ns_type
489    $types = ['pages' => 'wikiFN', 'media' => 'mediaFN'];
490    if (!isset($types[$ns_type])) {
491        trigger_error('Bad $ns_type parameter for io_createNamespace().');
492        return;
493    }
494    // make event list
495    $missing = [];
496    $ns_stack = explode(':', $id);
497    $ns = $id;
498    $tmp = dirname($file = call_user_func($types[$ns_type], $ns));
499    while (!@is_dir($tmp) && !(file_exists($tmp) && !is_dir($tmp))) {
500        array_pop($ns_stack);
501        $ns = implode(':', $ns_stack);
502        if (strlen($ns) == 0) {
503            break;
504        }
505        $missing[] = $ns;
506        $tmp = dirname(call_user_func($types[$ns_type], $ns));
507    }
508    // make directories
509    io_makeFileDir($file);
510    // send the events
511    $missing = array_reverse($missing); // inside out
512    foreach ($missing as $ns) {
513        $data = [$ns, $ns_type];
514        Event::createAndTrigger('IO_NAMESPACE_CREATED', $data);
515    }
516}
517
518/**
519 * Create the directory needed for the given file
520 *
521 * @param string $file file name
522 *
523 * @author  Andreas Gohr <andi@splitbrain.org>
524 */
525function io_makeFileDir($file)
526{
527    $dir = dirname($file);
528    if (!@is_dir($dir)) {
529        if (!io_mkdir_p($dir)) {
530            msg("Creating directory $dir failed", -1);
531        }
532    }
533}
534
535/**
536 * Creates a directory hierachy.
537 *
538 * @param string $target filename
539 * @return bool
540 *
541 * @link    http://php.net/manual/en/function.mkdir.php
542 * @author  <saint@corenova.com>
543 * @author  Andreas Gohr <andi@splitbrain.org>
544 */
545function io_mkdir_p($target)
546{
547    global $conf;
548    if (@is_dir($target) || empty($target)) return true; // best case check first
549    if (file_exists($target) && !is_dir($target)) return false;
550    //recursion
551    if (io_mkdir_p(substr($target, 0, strrpos($target, '/')))) {
552        $ret = @mkdir($target); // crawl back up & create dir tree
553        if ($ret && !empty($conf['dperm'])) {
554            chmod($target, $conf['dperm']);
555        }
556        return $ret;
557    }
558    return false;
559}
560
561/**
562 * Recursively delete a directory
563 *
564 * @param string $path
565 * @param bool $removefiles defaults to false which will delete empty directories only
566 * @return bool
567 *
568 * @author Andreas Gohr <andi@splitbrain.org>
569 */
570function io_rmdir($path, $removefiles = false)
571{
572    if (!is_string($path) || $path == "") return false;
573    if (!file_exists($path)) return true; // it's already gone or was never there, count as success
574
575    if (is_dir($path) && !is_link($path)) {
576        $dirs = [];
577        $files = [];
578        if (!$dh = @opendir($path)) return false;
579        while (false !== ($f = readdir($dh))) {
580            if ($f == '..' || $f == '.') continue;
581
582            // collect dirs and files first
583            if (is_dir("$path/$f") && !is_link("$path/$f")) {
584                $dirs[] = "$path/$f";
585            } elseif ($removefiles) {
586                $files[] = "$path/$f";
587            } else {
588                return false; // abort when non empty
589            }
590        }
591        closedir($dh);
592        // now traverse into  directories first
593        foreach ($dirs as $dir) {
594            if (!io_rmdir($dir, $removefiles)) return false; // abort on any error
595        }
596        // now delete files
597        foreach ($files as $file) {
598            if (!@unlink($file)) return false; //abort on any error
599        }
600        // remove self
601        return @rmdir($path);
602    } elseif ($removefiles) {
603        return @unlink($path);
604    }
605    return false;
606}
607
608/**
609 * Creates a unique temporary directory and returns
610 * its path.
611 *
612 * @return false|string path to new directory or false
613 * @throws Exception
614 *
615 * @author Michael Klier <chi@chimeric.de>
616 */
617function io_mktmpdir()
618{
619    global $conf;
620
621    $base = $conf['tmpdir'];
622    $dir = md5(uniqid(random_int(0, mt_getrandmax()), true));
623    $tmpdir = $base . '/' . $dir;
624
625    if (io_mkdir_p($tmpdir)) {
626        return $tmpdir;
627    } else {
628        return false;
629    }
630}
631
632/**
633 * downloads a file from the net and saves it
634 *
635 * if $useAttachment is false,
636 * - $file is the full filename to save the file, incl. path
637 * - if successful will return true, false otherwise
638 *
639 * if $useAttachment is true,
640 * - $file is the directory where the file should be saved
641 * - if successful will return the name used for the saved file, false otherwise
642 *
643 * @param string $url url to download
644 * @param string $file path to file or directory where to save
645 * @param bool $useAttachment true: try to use name of download, uses otherwise $defaultName
646 *                            false: uses $file as path to file
647 * @param string $defaultName fallback for if using $useAttachment
648 * @param int $maxSize maximum file size
649 * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
650 *
651 * @author Andreas Gohr <andi@splitbrain.org>
652 * @author Chris Smith <chris@jalakai.co.uk>
653 */
654function io_download($url, $file, $useAttachment = false, $defaultName = '', $maxSize = 2_097_152)
655{
656    global $conf;
657    $http = new DokuHTTPClient();
658    $http->max_bodysize = $maxSize;
659    $http->timeout = 25; //max. 25 sec
660    $http->keep_alive = false; // we do single ops here, no need for keep-alive
661
662    $data = $http->get($url);
663    if (!$data) return false;
664
665    $name = '';
666    if ($useAttachment) {
667        if (isset($http->resp_headers['content-disposition'])) {
668            $content_disposition = $http->resp_headers['content-disposition'];
669            $match = [];
670            if (
671                is_string($content_disposition) &&
672                preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)
673            ) {
674                $name = PhpString::basename($match[1]);
675            }
676        }
677
678        if (!$name) {
679            if (!$defaultName) return false;
680            $name = $defaultName;
681        }
682
683        $file .= $name;
684    }
685
686    $fileexists = file_exists($file);
687    $fp = @fopen($file, "w");
688    if (!$fp) return false;
689    fwrite($fp, $data);
690    fclose($fp);
691    if (!$fileexists && $conf['fperm']) {
692        chmod($file, $conf['fperm']);
693    }
694    if ($useAttachment) return $name;
695    return true;
696}
697
698/**
699 * Windows compatible rename
700 *
701 * rename() can not overwrite existing files on Windows
702 * this function will use copy/unlink instead
703 *
704 * @param string $from
705 * @param string $to
706 * @return bool succes or fail
707 */
708function io_rename($from, $to)
709{
710    global $conf;
711    if (!@rename($from, $to)) {
712        if (@copy($from, $to)) {
713            if ($conf['fperm']) {
714                chmod($to, $conf['fperm']);
715            }
716            @unlink($from);
717            return true;
718        }
719        return false;
720    }
721    return true;
722}
723
724/**
725 * Runs an external command with input and output pipes.
726 * Returns the exit code from the process.
727 *
728 * @param string $cmd
729 * @param string $input input pipe
730 * @param string $output output pipe
731 * @return int exit code from process
732 *
733 * @author Tom N Harris <tnharris@whoopdedo.org>
734 */
735function io_exec($cmd, $input, &$output)
736{
737    $descspec = [
738        0 => ["pipe", "r"],
739        1 => ["pipe", "w"],
740        2 => ["pipe", "w"]
741    ];
742    $ph = proc_open($cmd, $descspec, $pipes);
743    if (!$ph) return -1;
744    fclose($pipes[2]); // ignore stderr
745    fwrite($pipes[0], $input);
746    fclose($pipes[0]);
747    $output = stream_get_contents($pipes[1]);
748    fclose($pipes[1]);
749    return proc_close($ph);
750}
751
752/**
753 * Search a file for matching lines
754 *
755 * This is probably not faster than file()+preg_grep() but less
756 * memory intensive because not the whole file needs to be loaded
757 * at once.
758 *
759 * @param string $file The file to search
760 * @param string $pattern PCRE pattern
761 * @param int $max How many lines to return (0 for all)
762 * @param bool $backref When true returns array with backreferences instead of lines
763 * @return array matching lines or backref, false on error
764 *
765 * @author Andreas Gohr <andi@splitbrain.org>
766 */
767function io_grep($file, $pattern, $max = 0, $backref = false)
768{
769    $fh = @fopen($file, 'r');
770    if (!$fh) return false;
771    $matches = [];
772
773    $cnt = 0;
774    $line = '';
775    while (!feof($fh)) {
776        $line .= fgets($fh, 4096);  // read full line
777        if (!str_ends_with($line, "\n")) continue;
778
779        // check if line matches
780        if (preg_match($pattern, $line, $match)) {
781            if ($backref) {
782                $matches[] = $match;
783            } else {
784                $matches[] = $line;
785            }
786            $cnt++;
787        }
788        if ($max && $max == $cnt) break;
789        $line = '';
790    }
791    fclose($fh);
792    return $matches;
793}
794
795
796/**
797 * Get size of contents of a file, for a compressed file the uncompressed size
798 * Warning: reading uncompressed size of content of bz-files requires uncompressing
799 *
800 * @param string $file filename path to file
801 * @return int size of file
802 *
803 * @author  Gerrit Uitslag <klapinklapin@gmail.com>
804 */
805function io_getSizeFile($file)
806{
807    if (!file_exists($file)) return 0;
808
809    if (str_ends_with($file, '.gz')) {
810        $fp = @fopen($file, "rb");
811        if ($fp === false) return 0;
812        fseek($fp, -4, SEEK_END);
813        $buffer = fread($fp, 4);
814        fclose($fp);
815        $array = unpack("V", $buffer);
816        $uncompressedsize = end($array);
817    } elseif (str_ends_with($file, '.bz2')) {
818        if (!DOKU_HAS_BZIP) return 0;
819        $bz = bzopen($file, "r");
820        if ($bz === false) return 0;
821        $uncompressedsize = 0;
822        while (!feof($bz)) {
823            //8192 seems to be the maximum buffersize?
824            $buffer = bzread($bz, 8192);
825            if (($buffer === false) || (bzerrno($bz) !== 0)) {
826                return 0;
827            }
828            $uncompressedsize += strlen($buffer);
829        }
830    } else {
831        $uncompressedsize = filesize($file);
832    }
833
834    return $uncompressedsize;
835}
836