xref: /plugin/sitebackup/admin.php (revision 29ed7b46e047d8ed0dd05f2940a812025dfd12ab)
1*29ed7b46Stracker-user<?php
2*29ed7b46Stracker-user/**
3*29ed7b46Stracker-user * Site Backup admin plugin for DokuWiki.
4*29ed7b46Stracker-user *
5*29ed7b46Stracker-user * Renders a small form letting an admin pick which parts of the wiki to
6*29ed7b46Stracker-user * include in a tar.gz, builds it on the server side, then streams it to
7*29ed7b46Stracker-user * the browser as a download. Uses DokuWiki's bundled splitbrain/php-archive
8*29ed7b46Stracker-user * (no external dependencies).
9*29ed7b46Stracker-user *
10*29ed7b46Stracker-user * Intentionally admin-only (forAdminOnly() = true). The archive can contain
11*29ed7b46Stracker-user * password hashes (conf/users.auth.php), ACLs, and any credentials stored in
12*29ed7b46Stracker-user * conf/local.php (DB, SMTP, etc.), so treat the download as sensitive.
13*29ed7b46Stracker-user */
14*29ed7b46Stracker-user
15*29ed7b46Stracker-useruse dokuwiki\Extension\AdminPlugin;
16*29ed7b46Stracker-useruse dokuwiki\Form\Form;
17*29ed7b46Stracker-useruse splitbrain\PHPArchive\Tar;
18*29ed7b46Stracker-useruse splitbrain\PHPArchive\Archive;
19*29ed7b46Stracker-useruse splitbrain\PHPArchive\ArchiveIOException;
20*29ed7b46Stracker-user
21*29ed7b46Stracker-userclass admin_plugin_sitebackup extends AdminPlugin
22*29ed7b46Stracker-user{
23*29ed7b46Stracker-user    /** @var array list of [absolute path, archive-relative path] of files to include */
24*29ed7b46Stracker-user    protected $fileList = [];
25*29ed7b46Stracker-user
26*29ed7b46Stracker-user    /** @var int total uncompressed size of selected files */
27*29ed7b46Stracker-user    protected $totalBytes = 0;
28*29ed7b46Stracker-user
29*29ed7b46Stracker-user    public function forAdminOnly()
30*29ed7b46Stracker-user    {
31*29ed7b46Stracker-user        return true;
32*29ed7b46Stracker-user    }
33*29ed7b46Stracker-user
34*29ed7b46Stracker-user    public function getMenuSort()
35*29ed7b46Stracker-user    {
36*29ed7b46Stracker-user        return 1000;
37*29ed7b46Stracker-user    }
38*29ed7b46Stracker-user
39*29ed7b46Stracker-user    public function getMenuText($language)
40*29ed7b46Stracker-user    {
41*29ed7b46Stracker-user        return 'Site Backup';
42*29ed7b46Stracker-user    }
43*29ed7b46Stracker-user
44*29ed7b46Stracker-user    /**
45*29ed7b46Stracker-user     * Dispatch based on the submitted action.
46*29ed7b46Stracker-user     * Actions: "preview" (default - show file list + sizes), "download" (stream tar.gz).
47*29ed7b46Stracker-user     */
48*29ed7b46Stracker-user    public function handle()
49*29ed7b46Stracker-user    {
50*29ed7b46Stracker-user        global $INPUT;
51*29ed7b46Stracker-user        if (!$INPUT->has('sitebackup_action')) return;
52*29ed7b46Stracker-user        if (!checkSecurityToken()) return;
53*29ed7b46Stracker-user
54*29ed7b46Stracker-user        $action = $INPUT->str('sitebackup_action');
55*29ed7b46Stracker-user        if ($action !== 'preview' && $action !== 'download') return;
56*29ed7b46Stracker-user
57*29ed7b46Stracker-user        $this->collectFiles();
58*29ed7b46Stracker-user
59*29ed7b46Stracker-user        if ($action === 'download') {
60*29ed7b46Stracker-user            $this->streamArchive();
61*29ed7b46Stracker-user            // streamArchive exits when successful; if it returns, fall through to html()
62*29ed7b46Stracker-user        }
63*29ed7b46Stracker-user        // For 'preview', html() will render the file list + a download button.
64*29ed7b46Stracker-user    }
65*29ed7b46Stracker-user
66*29ed7b46Stracker-user    public function html()
67*29ed7b46Stracker-user    {
68*29ed7b46Stracker-user        global $INPUT;
69*29ed7b46Stracker-user
70*29ed7b46Stracker-user        echo '<h1>Site Backup</h1>';
71*29ed7b46Stracker-user        echo '<p>Select what to include, click <em>Preview</em> to see the file list and total size, '
72*29ed7b46Stracker-user            . 'then <em>Download</em> to get a tar.gz archive.</p>';
73*29ed7b46Stracker-user        echo '<p style="background:#fff3cd;border:1px solid #ffeeba;padding:8px;border-radius:4px;">'
74*29ed7b46Stracker-user            . '<strong>Sensitive content warning.</strong> The archive can contain password hashes '
75*29ed7b46Stracker-user            . '(<code>conf/users.auth.php</code>), ACL rules, and any secrets stored in '
76*29ed7b46Stracker-user            . '<code>conf/local.php</code> (DB, SMTP, API keys). Treat it like a credential.'
77*29ed7b46Stracker-user            . '</p>';
78*29ed7b46Stracker-user
79*29ed7b46Stracker-user        $this->renderForm();
80*29ed7b46Stracker-user
81*29ed7b46Stracker-user        if ($this->fileList) {
82*29ed7b46Stracker-user            $this->renderPreview();
83*29ed7b46Stracker-user        }
84*29ed7b46Stracker-user    }
85*29ed7b46Stracker-user
86*29ed7b46Stracker-user    /* ----------------------------------------------------------------- *
87*29ed7b46Stracker-user     *  Form
88*29ed7b46Stracker-user     * ----------------------------------------------------------------- */
89*29ed7b46Stracker-user
90*29ed7b46Stracker-user    protected function renderForm()
91*29ed7b46Stracker-user    {
92*29ed7b46Stracker-user        global $INPUT;
93*29ed7b46Stracker-user
94*29ed7b46Stracker-user        // Read current selections (defaulting to "everything sensible" on first load).
95*29ed7b46Stracker-user        $hasSubmitted = $INPUT->has('sitebackup_action');
96*29ed7b46Stracker-user        $defaults = [
97*29ed7b46Stracker-user            'pages'       => true,
98*29ed7b46Stracker-user            'media'       => true,
99*29ed7b46Stracker-user            'meta'        => true,
100*29ed7b46Stracker-user            'media_meta'  => true,
101*29ed7b46Stracker-user            'attic'       => false,
102*29ed7b46Stracker-user            'media_attic' => false,
103*29ed7b46Stracker-user            'index'       => false,
104*29ed7b46Stracker-user            'conf'        => true,
105*29ed7b46Stracker-user            'plugins'     => true,
106*29ed7b46Stracker-user            'tpl'         => true,
107*29ed7b46Stracker-user        ];
108*29ed7b46Stracker-user        $sel = [];
109*29ed7b46Stracker-user        foreach ($defaults as $k => $def) {
110*29ed7b46Stracker-user            $sel[$k] = $hasSubmitted ? $INPUT->bool('sb_' . $k, false) : $def;
111*29ed7b46Stracker-user        }
112*29ed7b46Stracker-user
113*29ed7b46Stracker-user        $form = new Form(['method' => 'POST', 'id' => 'sitebackup_form']);
114*29ed7b46Stracker-user        $form->setHiddenField('do', 'admin');
115*29ed7b46Stracker-user        $form->setHiddenField('page', 'sitebackup');
116*29ed7b46Stracker-user
117*29ed7b46Stracker-user        $form->addFieldsetOpen('Wiki content');
118*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_pages',       'Pages (data/pages)',                       $sel['pages']);
119*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_media',       'Media files (data/media)',                 $sel['media']);
120*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_meta',        'Page metadata (data/meta)',                $sel['meta']);
121*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_media_meta',  'Media metadata (data/media_meta)',         $sel['media_meta']);
122*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_attic',       'Page revisions (data/attic) - can be large', $sel['attic']);
123*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_media_attic', 'Media revisions (data/media_attic)',       $sel['media_attic']);
124*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_index',       'Search index (data/index) - rebuildable',  $sel['index']);
125*29ed7b46Stracker-user        $form->addFieldsetClose();
126*29ed7b46Stracker-user
127*29ed7b46Stracker-user        $form->addFieldsetOpen('Configuration & code');
128*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_conf',    'Configuration (conf/) - includes secrets',  $sel['conf']);
129*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_plugins', 'Plugins source (lib/plugins/)',             $sel['plugins']);
130*29ed7b46Stracker-user        $this->addCheckboxRow($form, 'sb_tpl',     'Templates source (lib/tpl/)',               $sel['tpl']);
131*29ed7b46Stracker-user        $form->addFieldsetClose();
132*29ed7b46Stracker-user
133*29ed7b46Stracker-user        $form->addTagOpen('p');
134*29ed7b46Stracker-user        $form->addButton('sitebackup_action', 'Preview')->val('preview');
135*29ed7b46Stracker-user        $form->addHTML(' ');
136*29ed7b46Stracker-user        $form->addButton('sitebackup_action', 'Download tar.gz')->val('download');
137*29ed7b46Stracker-user        $form->addTagClose('p');
138*29ed7b46Stracker-user
139*29ed7b46Stracker-user        echo $form->toHTML();
140*29ed7b46Stracker-user    }
141*29ed7b46Stracker-user
142*29ed7b46Stracker-user    protected function addCheckboxRow(Form $form, $name, $label, $checked)
143*29ed7b46Stracker-user    {
144*29ed7b46Stracker-user        $form->addTagOpen('div')->attr('style', 'margin:4px 0;');
145*29ed7b46Stracker-user        $cb = $form->addCheckbox($name, ' ' . $label);
146*29ed7b46Stracker-user        $cb->val('1');
147*29ed7b46Stracker-user        if ($checked) $cb->attr('checked', 'checked');
148*29ed7b46Stracker-user        $form->addTagClose('div');
149*29ed7b46Stracker-user    }
150*29ed7b46Stracker-user
151*29ed7b46Stracker-user    /* ----------------------------------------------------------------- *
152*29ed7b46Stracker-user     *  File collection
153*29ed7b46Stracker-user     * ----------------------------------------------------------------- */
154*29ed7b46Stracker-user
155*29ed7b46Stracker-user    /**
156*29ed7b46Stracker-user     * Walk every selected root and build $this->fileList + $this->totalBytes.
157*29ed7b46Stracker-user     */
158*29ed7b46Stracker-user    protected function collectFiles()
159*29ed7b46Stracker-user    {
160*29ed7b46Stracker-user        global $INPUT, $conf;
161*29ed7b46Stracker-user
162*29ed7b46Stracker-user        // Map of (form-field => [absolute source path, archive-relative path]).
163*29ed7b46Stracker-user        // Use $conf[...] for data dirs so we handle relocated savedir installs correctly.
164*29ed7b46Stracker-user        $roots = [
165*29ed7b46Stracker-user            'sb_pages'       => [$conf['datadir'],      'data/pages'],
166*29ed7b46Stracker-user            'sb_media'       => [$conf['mediadir'],     'data/media'],
167*29ed7b46Stracker-user            'sb_meta'        => [$conf['metadir'],      'data/meta'],
168*29ed7b46Stracker-user            'sb_media_meta'  => [$conf['mediametadir'], 'data/media_meta'],
169*29ed7b46Stracker-user            'sb_attic'       => [$conf['olddir'],       'data/attic'],
170*29ed7b46Stracker-user            'sb_media_attic' => [$conf['mediaolddir'],  'data/media_attic'],
171*29ed7b46Stracker-user            'sb_index'       => [$conf['indexdir'],     'data/index'],
172*29ed7b46Stracker-user            'sb_conf'        => [rtrim(DOKU_CONF, '/'), 'conf'],
173*29ed7b46Stracker-user            'sb_plugins'     => [rtrim(DOKU_PLUGIN, '/'), 'lib/plugins'],
174*29ed7b46Stracker-user            'sb_tpl'         => [DOKU_INC . 'lib/tpl',  'lib/tpl'],
175*29ed7b46Stracker-user        ];
176*29ed7b46Stracker-user
177*29ed7b46Stracker-user        foreach ($roots as $field => $pair) {
178*29ed7b46Stracker-user            if (!$INPUT->bool($field, false)) continue;
179*29ed7b46Stracker-user            [$srcAbs, $archiveRel] = $pair;
180*29ed7b46Stracker-user            $this->walkInto($srcAbs, $archiveRel);
181*29ed7b46Stracker-user        }
182*29ed7b46Stracker-user    }
183*29ed7b46Stracker-user
184*29ed7b46Stracker-user    /**
185*29ed7b46Stracker-user     * Recursively walk a directory (or single file) and append to $fileList.
186*29ed7b46Stracker-user     */
187*29ed7b46Stracker-user    protected function walkInto($srcAbs, $archiveRel)
188*29ed7b46Stracker-user    {
189*29ed7b46Stracker-user        if (!file_exists($srcAbs)) return;
190*29ed7b46Stracker-user
191*29ed7b46Stracker-user        if (is_file($srcAbs)) {
192*29ed7b46Stracker-user            $this->appendFile($srcAbs, $archiveRel);
193*29ed7b46Stracker-user            return;
194*29ed7b46Stracker-user        }
195*29ed7b46Stracker-user
196*29ed7b46Stracker-user        try {
197*29ed7b46Stracker-user            $it = new RecursiveIteratorIterator(
198*29ed7b46Stracker-user                new RecursiveDirectoryIterator($srcAbs, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS),
199*29ed7b46Stracker-user                RecursiveIteratorIterator::LEAVES_ONLY
200*29ed7b46Stracker-user            );
201*29ed7b46Stracker-user        } catch (Exception $e) {
202*29ed7b46Stracker-user            return;
203*29ed7b46Stracker-user        }
204*29ed7b46Stracker-user
205*29ed7b46Stracker-user        $srcRoot = rtrim($srcAbs, '/');
206*29ed7b46Stracker-user        $rootLen = strlen($srcRoot) + 1;
207*29ed7b46Stracker-user        foreach ($it as $info) {
208*29ed7b46Stracker-user            try {
209*29ed7b46Stracker-user                if (!$info->isFile()) continue;
210*29ed7b46Stracker-user                if (!$info->isReadable()) continue;
211*29ed7b46Stracker-user                $abs = $info->getPathname();
212*29ed7b46Stracker-user                $rel = substr($abs, $rootLen);
213*29ed7b46Stracker-user                // Normalize Windows-style separators just in case.
214*29ed7b46Stracker-user                $rel = str_replace('\\', '/', $rel);
215*29ed7b46Stracker-user
216*29ed7b46Stracker-user                if ($this->isIgnored($rel)) continue;
217*29ed7b46Stracker-user
218*29ed7b46Stracker-user                $this->appendFile($abs, $archiveRel . '/' . $rel);
219*29ed7b46Stracker-user            } catch (Exception $e) {
220*29ed7b46Stracker-user                // Skip unreadable / vanished files silently.
221*29ed7b46Stracker-user                continue;
222*29ed7b46Stracker-user            }
223*29ed7b46Stracker-user        }
224*29ed7b46Stracker-user    }
225*29ed7b46Stracker-user
226*29ed7b46Stracker-user    /**
227*29ed7b46Stracker-user     * Per-tree filename ignores. Cache/lock/tmp/log are noisy and not useful for a restore.
228*29ed7b46Stracker-user     * `_dummy` are placeholder files DokuWiki ships to keep empty dirs in tarballs.
229*29ed7b46Stracker-user     */
230*29ed7b46Stracker-user    protected function isIgnored($relPath)
231*29ed7b46Stracker-user    {
232*29ed7b46Stracker-user        $base = basename($relPath);
233*29ed7b46Stracker-user        if ($base === '_dummy') return true;
234*29ed7b46Stracker-user        if ($base === '.DS_Store') return true;
235*29ed7b46Stracker-user        if ($base === 'Thumbs.db') return true;
236*29ed7b46Stracker-user        // The plugin's own scratch file - shouldn't exist, but belt and suspenders.
237*29ed7b46Stracker-user        if (strpos($base, 'sitebackup_tmp_') === 0) return true;
238*29ed7b46Stracker-user        return false;
239*29ed7b46Stracker-user    }
240*29ed7b46Stracker-user
241*29ed7b46Stracker-user    protected function appendFile($abs, $archiveRel)
242*29ed7b46Stracker-user    {
243*29ed7b46Stracker-user        $size = @filesize($abs);
244*29ed7b46Stracker-user        if ($size === false) $size = 0;
245*29ed7b46Stracker-user        $this->fileList[] = [$abs, $archiveRel, $size];
246*29ed7b46Stracker-user        $this->totalBytes += $size;
247*29ed7b46Stracker-user    }
248*29ed7b46Stracker-user
249*29ed7b46Stracker-user    /* ----------------------------------------------------------------- *
250*29ed7b46Stracker-user     *  Preview
251*29ed7b46Stracker-user     * ----------------------------------------------------------------- */
252*29ed7b46Stracker-user
253*29ed7b46Stracker-user    protected function renderPreview()
254*29ed7b46Stracker-user    {
255*29ed7b46Stracker-user        echo '<h2>Preview</h2>';
256*29ed7b46Stracker-user        echo '<p>' . count($this->fileList) . ' files, '
257*29ed7b46Stracker-user            . hsc($this->humanBytes($this->totalBytes)) . ' uncompressed.</p>';
258*29ed7b46Stracker-user
259*29ed7b46Stracker-user        // Per-top-level summary so the user can see what each section costs.
260*29ed7b46Stracker-user        $perRoot = [];
261*29ed7b46Stracker-user        foreach ($this->fileList as [$abs, $rel, $size]) {
262*29ed7b46Stracker-user            $parts = explode('/', $rel, 4);
263*29ed7b46Stracker-user            $top = isset($parts[1]) ? ($parts[0] . '/' . $parts[1]) : $parts[0];
264*29ed7b46Stracker-user            if (!isset($perRoot[$top])) $perRoot[$top] = ['count' => 0, 'bytes' => 0];
265*29ed7b46Stracker-user            $perRoot[$top]['count']++;
266*29ed7b46Stracker-user            $perRoot[$top]['bytes'] += $size;
267*29ed7b46Stracker-user        }
268*29ed7b46Stracker-user        ksort($perRoot);
269*29ed7b46Stracker-user
270*29ed7b46Stracker-user        echo '<table class="inline"><thead><tr><th>Section</th><th>Files</th><th>Size</th></tr></thead><tbody>';
271*29ed7b46Stracker-user        foreach ($perRoot as $section => $stats) {
272*29ed7b46Stracker-user            echo '<tr><td><code>' . hsc($section) . '</code></td>'
273*29ed7b46Stracker-user                . '<td style="text-align:right;">' . (int)$stats['count'] . '</td>'
274*29ed7b46Stracker-user                . '<td style="text-align:right;">' . hsc($this->humanBytes($stats['bytes'])) . '</td></tr>';
275*29ed7b46Stracker-user        }
276*29ed7b46Stracker-user        echo '</tbody></table>';
277*29ed7b46Stracker-user        echo '<p>Click <em>Download tar.gz</em> above to create and download the archive '
278*29ed7b46Stracker-user            . '(the compressed size will typically be smaller).</p>';
279*29ed7b46Stracker-user    }
280*29ed7b46Stracker-user
281*29ed7b46Stracker-user    protected function humanBytes($bytes)
282*29ed7b46Stracker-user    {
283*29ed7b46Stracker-user        $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
284*29ed7b46Stracker-user        $i = 0;
285*29ed7b46Stracker-user        $n = (float)$bytes;
286*29ed7b46Stracker-user        while ($n >= 1024 && $i < count($units) - 1) {
287*29ed7b46Stracker-user            $n /= 1024;
288*29ed7b46Stracker-user            $i++;
289*29ed7b46Stracker-user        }
290*29ed7b46Stracker-user        return sprintf($i === 0 ? '%d %s' : '%.2f %s', $n, $units[$i]);
291*29ed7b46Stracker-user    }
292*29ed7b46Stracker-user
293*29ed7b46Stracker-user    /* ----------------------------------------------------------------- *
294*29ed7b46Stracker-user     *  Archive creation + streaming
295*29ed7b46Stracker-user     * ----------------------------------------------------------------- */
296*29ed7b46Stracker-user
297*29ed7b46Stracker-user    protected function streamArchive()
298*29ed7b46Stracker-user    {
299*29ed7b46Stracker-user        global $conf;
300*29ed7b46Stracker-user
301*29ed7b46Stracker-user        if (!$this->fileList) {
302*29ed7b46Stracker-user            // Fall through to html() which will just show the form again.
303*29ed7b46Stracker-user            return;
304*29ed7b46Stracker-user        }
305*29ed7b46Stracker-user
306*29ed7b46Stracker-user        @set_time_limit(0);
307*29ed7b46Stracker-user        @ignore_user_abort(true);
308*29ed7b46Stracker-user        // PHP 8.x: gzopen and tar building don't need huge memory; bump modestly just in case.
309*29ed7b46Stracker-user        @ini_set('memory_limit', '256M');
310*29ed7b46Stracker-user
311*29ed7b46Stracker-user        $tmpDir = $conf['tmpdir'];
312*29ed7b46Stracker-user        if (!is_dir($tmpDir) || !is_writable($tmpDir)) {
313*29ed7b46Stracker-user            msg('Site Backup: temp directory is not writable: ' . hsc($tmpDir), -1);
314*29ed7b46Stracker-user            return;
315*29ed7b46Stracker-user        }
316*29ed7b46Stracker-user
317*29ed7b46Stracker-user        // Hostname for filename - sanitize aggressively.
318*29ed7b46Stracker-user        $host = $_SERVER['HTTP_HOST'] ?? 'wiki';
319*29ed7b46Stracker-user        $host = preg_replace('/[^a-zA-Z0-9._-]+/', '_', $host);
320*29ed7b46Stracker-user        $stamp = date('Ymd-His');
321*29ed7b46Stracker-user        $prefix = $host . '-backup-' . $stamp;       // dir name inside the archive
322*29ed7b46Stracker-user        $filename = $prefix . '.tar.gz';             // download filename
323*29ed7b46Stracker-user        $tmpFile = $tmpDir . '/sitebackup_tmp_' . bin2hex(random_bytes(8)) . '.tar.gz';
324*29ed7b46Stracker-user
325*29ed7b46Stracker-user        try {
326*29ed7b46Stracker-user            $tar = new Tar();
327*29ed7b46Stracker-user            $tar->setCompression(6, Archive::COMPRESS_GZIP);
328*29ed7b46Stracker-user            $tar->create($tmpFile);
329*29ed7b46Stracker-user
330*29ed7b46Stracker-user            foreach ($this->fileList as [$abs, $rel, $size]) {
331*29ed7b46Stracker-user                try {
332*29ed7b46Stracker-user                    $tar->addFile($abs, $prefix . '/' . $rel);
333*29ed7b46Stracker-user                } catch (Exception $e) {
334*29ed7b46Stracker-user                    // Skip individual broken files rather than aborting the whole backup.
335*29ed7b46Stracker-user                    continue;
336*29ed7b46Stracker-user                }
337*29ed7b46Stracker-user            }
338*29ed7b46Stracker-user            $tar->close();
339*29ed7b46Stracker-user        } catch (ArchiveIOException $e) {
340*29ed7b46Stracker-user            @unlink($tmpFile);
341*29ed7b46Stracker-user            msg('Site Backup: could not create archive: ' . hsc($e->getMessage()), -1);
342*29ed7b46Stracker-user            return;
343*29ed7b46Stracker-user        }
344*29ed7b46Stracker-user
345*29ed7b46Stracker-user        if (!is_file($tmpFile) || filesize($tmpFile) === 0) {
346*29ed7b46Stracker-user            @unlink($tmpFile);
347*29ed7b46Stracker-user            msg('Site Backup: archive was empty or could not be written.', -1);
348*29ed7b46Stracker-user            return;
349*29ed7b46Stracker-user        }
350*29ed7b46Stracker-user
351*29ed7b46Stracker-user        // Stream out. We bypass DokuWiki's normal output by sending headers and
352*29ed7b46Stracker-user        // exiting after writing the file body.
353*29ed7b46Stracker-user        $size = filesize($tmpFile);
354*29ed7b46Stracker-user
355*29ed7b46Stracker-user        // Clear any output buffering DokuWiki / extensions may have started.
356*29ed7b46Stracker-user        while (ob_get_level() > 0) {
357*29ed7b46Stracker-user            @ob_end_clean();
358*29ed7b46Stracker-user        }
359*29ed7b46Stracker-user
360*29ed7b46Stracker-user        header('Content-Type: application/gzip');
361*29ed7b46Stracker-user        header('Content-Disposition: attachment; filename="' . $filename . '"');
362*29ed7b46Stracker-user        header('Content-Length: ' . $size);
363*29ed7b46Stracker-user        header('Cache-Control: no-store, no-cache, must-revalidate');
364*29ed7b46Stracker-user        header('Pragma: no-cache');
365*29ed7b46Stracker-user
366*29ed7b46Stracker-user        $fp = fopen($tmpFile, 'rb');
367*29ed7b46Stracker-user        if ($fp) {
368*29ed7b46Stracker-user            while (!feof($fp)) {
369*29ed7b46Stracker-user                $chunk = fread($fp, 1024 * 256);
370*29ed7b46Stracker-user                if ($chunk === false) break;
371*29ed7b46Stracker-user                echo $chunk;
372*29ed7b46Stracker-user                @flush();
373*29ed7b46Stracker-user            }
374*29ed7b46Stracker-user            fclose($fp);
375*29ed7b46Stracker-user        }
376*29ed7b46Stracker-user        @unlink($tmpFile);
377*29ed7b46Stracker-user        exit;
378*29ed7b46Stracker-user    }
379*29ed7b46Stracker-user}
380