xref: /dokuwiki/lib/plugins/extension/Installer.php (revision cf2dcf1b9ac1f331d4667a6c82d326f1a3e5d4c7)
1*cf2dcf1bSAndreas Gohr<?php
2*cf2dcf1bSAndreas Gohr
3*cf2dcf1bSAndreas Gohrnamespace dokuwiki\plugin\extension;
4*cf2dcf1bSAndreas Gohr
5*cf2dcf1bSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient;
6*cf2dcf1bSAndreas Gohruse dokuwiki\Utf8\PhpString;
7*cf2dcf1bSAndreas Gohruse RecursiveDirectoryIterator;
8*cf2dcf1bSAndreas Gohruse RecursiveIteratorIterator;
9*cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveCorruptedException;
10*cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveIllegalCompressionException;
11*cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveIOException;
12*cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\Tar;
13*cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\Zip;
14*cf2dcf1bSAndreas Gohr
15*cf2dcf1bSAndreas Gohr/**
16*cf2dcf1bSAndreas Gohr * Install and deinstall extensions
17*cf2dcf1bSAndreas Gohr *
18*cf2dcf1bSAndreas Gohr * This manages all the file operations and downloads needed to install an extension.
19*cf2dcf1bSAndreas Gohr */
20*cf2dcf1bSAndreas Gohrclass Installer
21*cf2dcf1bSAndreas Gohr{
22*cf2dcf1bSAndreas Gohr    /** @var string[] a list of temporary directories used during this installation */
23*cf2dcf1bSAndreas Gohr    protected array $temporary = [];
24*cf2dcf1bSAndreas Gohr
25*cf2dcf1bSAndreas Gohr    /** @var bool if changes have been made that require a cache purge */
26*cf2dcf1bSAndreas Gohr    protected $isDirty = false;
27*cf2dcf1bSAndreas Gohr
28*cf2dcf1bSAndreas Gohr    /** @var bool Replace existing files? */
29*cf2dcf1bSAndreas Gohr    protected $overwrite = false;
30*cf2dcf1bSAndreas Gohr
31*cf2dcf1bSAndreas Gohr    /** @var string The last used URL to install an extension */
32*cf2dcf1bSAndreas Gohr    protected $sourceUrl = '';
33*cf2dcf1bSAndreas Gohr
34*cf2dcf1bSAndreas Gohr    /**
35*cf2dcf1bSAndreas Gohr     * Initialize a new extension installer
36*cf2dcf1bSAndreas Gohr     *
37*cf2dcf1bSAndreas Gohr     * @param bool $overwrite
38*cf2dcf1bSAndreas Gohr     */
39*cf2dcf1bSAndreas Gohr    public function __construct($overwrite = false)
40*cf2dcf1bSAndreas Gohr    {
41*cf2dcf1bSAndreas Gohr        $this->overwrite = $overwrite;
42*cf2dcf1bSAndreas Gohr    }
43*cf2dcf1bSAndreas Gohr
44*cf2dcf1bSAndreas Gohr    /**
45*cf2dcf1bSAndreas Gohr     * Destructor
46*cf2dcf1bSAndreas Gohr     *
47*cf2dcf1bSAndreas Gohr     * deletes any dangling temporary directories
48*cf2dcf1bSAndreas Gohr     */
49*cf2dcf1bSAndreas Gohr    public function __destruct()
50*cf2dcf1bSAndreas Gohr    {
51*cf2dcf1bSAndreas Gohr        $this->cleanUp();
52*cf2dcf1bSAndreas Gohr    }
53*cf2dcf1bSAndreas Gohr
54*cf2dcf1bSAndreas Gohr    /**
55*cf2dcf1bSAndreas Gohr     * Install extensions from a given URL
56*cf2dcf1bSAndreas Gohr     *
57*cf2dcf1bSAndreas Gohr     * @param string $url the URL to the archive
58*cf2dcf1bSAndreas Gohr     * @param null $base the base directory name to use
59*cf2dcf1bSAndreas Gohr     * @throws Exception
60*cf2dcf1bSAndreas Gohr     */
61*cf2dcf1bSAndreas Gohr    public function installFromUrl($url, $base = null)
62*cf2dcf1bSAndreas Gohr    {
63*cf2dcf1bSAndreas Gohr        $this->sourceUrl = $url;
64*cf2dcf1bSAndreas Gohr        $archive = $this->downloadArchive($url);
65*cf2dcf1bSAndreas Gohr        $this->installFromArchive(
66*cf2dcf1bSAndreas Gohr            $archive,
67*cf2dcf1bSAndreas Gohr            $base
68*cf2dcf1bSAndreas Gohr        );
69*cf2dcf1bSAndreas Gohr    }
70*cf2dcf1bSAndreas Gohr
71*cf2dcf1bSAndreas Gohr    /**
72*cf2dcf1bSAndreas Gohr     * Install extensions from a user upload
73*cf2dcf1bSAndreas Gohr     *
74*cf2dcf1bSAndreas Gohr     * @param string $field name of the upload file
75*cf2dcf1bSAndreas Gohr     * @throws Exception
76*cf2dcf1bSAndreas Gohr     */
77*cf2dcf1bSAndreas Gohr    public function installFromUpload($field)
78*cf2dcf1bSAndreas Gohr    {
79*cf2dcf1bSAndreas Gohr        $this->sourceUrl = '';
80*cf2dcf1bSAndreas Gohr        if ($_FILES[$field]['error']) {
81*cf2dcf1bSAndreas Gohr            throw new Exception('msg_upload_failed', [$_FILES[$field]['error']]);
82*cf2dcf1bSAndreas Gohr        }
83*cf2dcf1bSAndreas Gohr
84*cf2dcf1bSAndreas Gohr        $tmp = $this->mkTmpDir();
85*cf2dcf1bSAndreas Gohr        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
86*cf2dcf1bSAndreas Gohr            throw new Exception('msg_upload_failed', ['move failed']);
87*cf2dcf1bSAndreas Gohr        }
88*cf2dcf1bSAndreas Gohr        $this->installFromArchive(
89*cf2dcf1bSAndreas Gohr            "$tmp/upload.archive",
90*cf2dcf1bSAndreas Gohr            $this->fileToBase($_FILES[$field]['name']),
91*cf2dcf1bSAndreas Gohr        );
92*cf2dcf1bSAndreas Gohr    }
93*cf2dcf1bSAndreas Gohr
94*cf2dcf1bSAndreas Gohr    /**
95*cf2dcf1bSAndreas Gohr     * Install extensions from an archive
96*cf2dcf1bSAndreas Gohr     *
97*cf2dcf1bSAndreas Gohr     * The archive is extracted to a temporary directory and then the contained extensions are installed.
98*cf2dcf1bSAndreas Gohr     * This is is the ultimate installation procedure and all other install methods will end up here.
99*cf2dcf1bSAndreas Gohr     *
100*cf2dcf1bSAndreas Gohr     * @param string $archive the path to the archive
101*cf2dcf1bSAndreas Gohr     * @param string $base the base directory name to use
102*cf2dcf1bSAndreas Gohr     * @throws Exception
103*cf2dcf1bSAndreas Gohr     */
104*cf2dcf1bSAndreas Gohr    public function installFromArchive($archive, $base = null)
105*cf2dcf1bSAndreas Gohr    {
106*cf2dcf1bSAndreas Gohr        if ($base === null) $base = $this->fileToBase($archive);
107*cf2dcf1bSAndreas Gohr        $target = $this->mkTmpDir() . '/' . $base;
108*cf2dcf1bSAndreas Gohr        $this->extractArchive($archive, $target);
109*cf2dcf1bSAndreas Gohr        $extensions = $this->findExtensions($target, $base);
110*cf2dcf1bSAndreas Gohr        foreach ($extensions as $extension) {
111*cf2dcf1bSAndreas Gohr            if ($extension->isInstalled() && !$this->overwrite) {
112*cf2dcf1bSAndreas Gohr                // FIXME remember skipped extensions
113*cf2dcf1bSAndreas Gohr                continue;
114*cf2dcf1bSAndreas Gohr            }
115*cf2dcf1bSAndreas Gohr
116*cf2dcf1bSAndreas Gohr            $this->dircopy(
117*cf2dcf1bSAndreas Gohr                $extension->getCurrentDir(),
118*cf2dcf1bSAndreas Gohr                $extension->getInstallDir()
119*cf2dcf1bSAndreas Gohr            );
120*cf2dcf1bSAndreas Gohr            $this->isDirty = true;
121*cf2dcf1bSAndreas Gohr            $extension->updateManagerInfo($this->sourceUrl);
122*cf2dcf1bSAndreas Gohr            $this->removeDeletedFiles($extension);
123*cf2dcf1bSAndreas Gohr
124*cf2dcf1bSAndreas Gohr            // FIXME remember installed extensions and if it was an update or new install
125*cf2dcf1bSAndreas Gohr            // FIXME queue dependencies for installation
126*cf2dcf1bSAndreas Gohr        }
127*cf2dcf1bSAndreas Gohr
128*cf2dcf1bSAndreas Gohr        // FIXME process dependency queue
129*cf2dcf1bSAndreas Gohr
130*cf2dcf1bSAndreas Gohr        $this->cleanUp();
131*cf2dcf1bSAndreas Gohr    }
132*cf2dcf1bSAndreas Gohr
133*cf2dcf1bSAndreas Gohr    /**
134*cf2dcf1bSAndreas Gohr     * Uninstall an extension
135*cf2dcf1bSAndreas Gohr     *
136*cf2dcf1bSAndreas Gohr     * @param Extension $extension
137*cf2dcf1bSAndreas Gohr     * @throws Exception
138*cf2dcf1bSAndreas Gohr     */
139*cf2dcf1bSAndreas Gohr    public function uninstall(Extension $extension)
140*cf2dcf1bSAndreas Gohr    {
141*cf2dcf1bSAndreas Gohr        // FIXME check if dependencies are still needed
142*cf2dcf1bSAndreas Gohr
143*cf2dcf1bSAndreas Gohr        if($extension->isProtected()) {
144*cf2dcf1bSAndreas Gohr            throw new Exception('error_uninstall_protected', [$extension->getId()]);
145*cf2dcf1bSAndreas Gohr        }
146*cf2dcf1bSAndreas Gohr
147*cf2dcf1bSAndreas Gohr        if (!io_rmdir($extension->getInstallDir(), true)) {
148*cf2dcf1bSAndreas Gohr            throw new Exception('msg_delete_failed', [$extension->getId()]);
149*cf2dcf1bSAndreas Gohr        }
150*cf2dcf1bSAndreas Gohr        self::purgeCache();
151*cf2dcf1bSAndreas Gohr    }
152*cf2dcf1bSAndreas Gohr
153*cf2dcf1bSAndreas Gohr    /**
154*cf2dcf1bSAndreas Gohr     * Download an archive to a protected path
155*cf2dcf1bSAndreas Gohr     *
156*cf2dcf1bSAndreas Gohr     * @param string $url The url to get the archive from
157*cf2dcf1bSAndreas Gohr     * @return string The path where the archive was saved
158*cf2dcf1bSAndreas Gohr     * @throws Exception
159*cf2dcf1bSAndreas Gohr     */
160*cf2dcf1bSAndreas Gohr    public function downloadArchive($url)
161*cf2dcf1bSAndreas Gohr    {
162*cf2dcf1bSAndreas Gohr        // check the url
163*cf2dcf1bSAndreas Gohr        if (!preg_match('/https?:\/\//i', $url)) {
164*cf2dcf1bSAndreas Gohr            throw new Exception('error_badurl');
165*cf2dcf1bSAndreas Gohr        }
166*cf2dcf1bSAndreas Gohr
167*cf2dcf1bSAndreas Gohr        // try to get the file from the path (used as plugin name fallback)
168*cf2dcf1bSAndreas Gohr        $file = parse_url($url, PHP_URL_PATH);
169*cf2dcf1bSAndreas Gohr        $file = $file ? PhpString::basename($file) : md5($url);
170*cf2dcf1bSAndreas Gohr
171*cf2dcf1bSAndreas Gohr        // download
172*cf2dcf1bSAndreas Gohr        $http = new DokuHTTPClient();
173*cf2dcf1bSAndreas Gohr        $http->max_bodysize = 0;
174*cf2dcf1bSAndreas Gohr        $http->timeout = 25; //max. 25 sec
175*cf2dcf1bSAndreas Gohr        $http->keep_alive = false; // we do single ops here, no need for keep-alive
176*cf2dcf1bSAndreas Gohr        $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
177*cf2dcf1bSAndreas Gohr
178*cf2dcf1bSAndreas Gohr        $data = $http->get($url);
179*cf2dcf1bSAndreas Gohr        if ($data === false) throw new Exception('error_download', [$url, $http->error, $http->status]);
180*cf2dcf1bSAndreas Gohr
181*cf2dcf1bSAndreas Gohr        // get filename from headers
182*cf2dcf1bSAndreas Gohr        if (preg_match(
183*cf2dcf1bSAndreas Gohr            '/attachment;\s*filename\s*=\s*"([^"]*)"/i',
184*cf2dcf1bSAndreas Gohr            (string)($http->resp_headers['content-disposition'] ?? ''),
185*cf2dcf1bSAndreas Gohr            $match
186*cf2dcf1bSAndreas Gohr        )) {
187*cf2dcf1bSAndreas Gohr            $file = PhpString::basename($match[1]);
188*cf2dcf1bSAndreas Gohr        }
189*cf2dcf1bSAndreas Gohr
190*cf2dcf1bSAndreas Gohr        // clean up filename
191*cf2dcf1bSAndreas Gohr        $file = $this->fileToBase($file);
192*cf2dcf1bSAndreas Gohr
193*cf2dcf1bSAndreas Gohr        // create tmp directory for download
194*cf2dcf1bSAndreas Gohr        $tmp = $this->mkTmpDir();
195*cf2dcf1bSAndreas Gohr
196*cf2dcf1bSAndreas Gohr        // save the file
197*cf2dcf1bSAndreas Gohr        if (@file_put_contents("$tmp/$file", $data) === false) {
198*cf2dcf1bSAndreas Gohr            throw new Exception('error_save');
199*cf2dcf1bSAndreas Gohr        }
200*cf2dcf1bSAndreas Gohr
201*cf2dcf1bSAndreas Gohr        return "$tmp/$file";
202*cf2dcf1bSAndreas Gohr    }
203*cf2dcf1bSAndreas Gohr
204*cf2dcf1bSAndreas Gohr
205*cf2dcf1bSAndreas Gohr    /**
206*cf2dcf1bSAndreas Gohr     * Delete outdated files
207*cf2dcf1bSAndreas Gohr     */
208*cf2dcf1bSAndreas Gohr    public function removeDeletedFiles(Extension $extension)
209*cf2dcf1bSAndreas Gohr    {
210*cf2dcf1bSAndreas Gohr        $extensiondir = $extension->getInstallDir();
211*cf2dcf1bSAndreas Gohr        $definitionfile = $extensiondir . '/deleted.files';
212*cf2dcf1bSAndreas Gohr        if (!file_exists($definitionfile)) return;
213*cf2dcf1bSAndreas Gohr
214*cf2dcf1bSAndreas Gohr        $list = file($definitionfile);
215*cf2dcf1bSAndreas Gohr        foreach ($list as $line) {
216*cf2dcf1bSAndreas Gohr            $line = trim(preg_replace('/#.*$/', '', $line));
217*cf2dcf1bSAndreas Gohr            $line = str_replace('..', '', $line); // do not run out of the extension directory
218*cf2dcf1bSAndreas Gohr            if (!$line) continue;
219*cf2dcf1bSAndreas Gohr
220*cf2dcf1bSAndreas Gohr            $file = $extensiondir . '/' . $line;
221*cf2dcf1bSAndreas Gohr            if (!file_exists($file)) continue;
222*cf2dcf1bSAndreas Gohr
223*cf2dcf1bSAndreas Gohr            io_rmdir($file, true);
224*cf2dcf1bSAndreas Gohr        }
225*cf2dcf1bSAndreas Gohr    }
226*cf2dcf1bSAndreas Gohr
227*cf2dcf1bSAndreas Gohr    public static function purgeCache()
228*cf2dcf1bSAndreas Gohr    {
229*cf2dcf1bSAndreas Gohr        // expire dokuwiki caches
230*cf2dcf1bSAndreas Gohr        // touching local.php expires wiki page, JS and CSS caches
231*cf2dcf1bSAndreas Gohr        global $config_cascade;
232*cf2dcf1bSAndreas Gohr        @touch(reset($config_cascade['main']['local']));
233*cf2dcf1bSAndreas Gohr
234*cf2dcf1bSAndreas Gohr        if (function_exists('opcache_reset')) {
235*cf2dcf1bSAndreas Gohr            opcache_reset();
236*cf2dcf1bSAndreas Gohr        }
237*cf2dcf1bSAndreas Gohr    }
238*cf2dcf1bSAndreas Gohr
239*cf2dcf1bSAndreas Gohr    /**
240*cf2dcf1bSAndreas Gohr     * Get a base name from an archive name (we don't trust)
241*cf2dcf1bSAndreas Gohr     *
242*cf2dcf1bSAndreas Gohr     * @param string $file
243*cf2dcf1bSAndreas Gohr     * @return string
244*cf2dcf1bSAndreas Gohr     */
245*cf2dcf1bSAndreas Gohr    protected function fileToBase($file)
246*cf2dcf1bSAndreas Gohr    {
247*cf2dcf1bSAndreas Gohr        $base = PhpString::basename($file);
248*cf2dcf1bSAndreas Gohr        $base = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $base);
249*cf2dcf1bSAndreas Gohr        return preg_replace('/\W+/', '', $base);
250*cf2dcf1bSAndreas Gohr    }
251*cf2dcf1bSAndreas Gohr
252*cf2dcf1bSAndreas Gohr    /**
253*cf2dcf1bSAndreas Gohr     * Returns a temporary directory
254*cf2dcf1bSAndreas Gohr     *
255*cf2dcf1bSAndreas Gohr     * The directory is registered for cleanup when the class is destroyed
256*cf2dcf1bSAndreas Gohr     *
257*cf2dcf1bSAndreas Gohr     * @return string
258*cf2dcf1bSAndreas Gohr     * @throws Exception
259*cf2dcf1bSAndreas Gohr     */
260*cf2dcf1bSAndreas Gohr    protected function mkTmpDir()
261*cf2dcf1bSAndreas Gohr    {
262*cf2dcf1bSAndreas Gohr        try {
263*cf2dcf1bSAndreas Gohr            $dir = io_mktmpdir();
264*cf2dcf1bSAndreas Gohr        } catch (\Exception $e) {
265*cf2dcf1bSAndreas Gohr            throw new Exception('error_dircreate', [], $e);
266*cf2dcf1bSAndreas Gohr        }
267*cf2dcf1bSAndreas Gohr        if (!$dir) throw new Exception('error_dircreate');
268*cf2dcf1bSAndreas Gohr        $this->temporary[] = $dir;
269*cf2dcf1bSAndreas Gohr        return $dir;
270*cf2dcf1bSAndreas Gohr    }
271*cf2dcf1bSAndreas Gohr
272*cf2dcf1bSAndreas Gohr    /**
273*cf2dcf1bSAndreas Gohr     * Find all extensions in a given directory
274*cf2dcf1bSAndreas Gohr     *
275*cf2dcf1bSAndreas Gohr     * This allows us to install extensions from archives that contain multiple extensions and
276*cf2dcf1bSAndreas Gohr     * also caters for the fact that archives may or may not contain subdirectories for the extension(s).
277*cf2dcf1bSAndreas Gohr     *
278*cf2dcf1bSAndreas Gohr     * @param string $dir
279*cf2dcf1bSAndreas Gohr     * @return Extension[]
280*cf2dcf1bSAndreas Gohr     */
281*cf2dcf1bSAndreas Gohr    protected function findExtensions($dir, $base = null)
282*cf2dcf1bSAndreas Gohr    {
283*cf2dcf1bSAndreas Gohr        // first check for plugin.info.txt or template.info.txt
284*cf2dcf1bSAndreas Gohr        $extensions = [];
285*cf2dcf1bSAndreas Gohr        $iterator = new RecursiveDirectoryIterator($dir);
286*cf2dcf1bSAndreas Gohr        foreach (new RecursiveIteratorIterator($iterator) as $file) {
287*cf2dcf1bSAndreas Gohr            if (
288*cf2dcf1bSAndreas Gohr                $file->getFilename() === 'plugin.info.txt' ||
289*cf2dcf1bSAndreas Gohr                $file->getFilename() === 'template.info.txt'
290*cf2dcf1bSAndreas Gohr            ) {
291*cf2dcf1bSAndreas Gohr                $extensions = Extension::createFromDirectory($file->getPath());
292*cf2dcf1bSAndreas Gohr            }
293*cf2dcf1bSAndreas Gohr        }
294*cf2dcf1bSAndreas Gohr        if ($extensions) return $extensions;
295*cf2dcf1bSAndreas Gohr
296*cf2dcf1bSAndreas Gohr        // still nothing? we assume this to be a single extension that is either
297*cf2dcf1bSAndreas Gohr        // directly in the given directory or in single subdirectory
298*cf2dcf1bSAndreas Gohr        $base = $base ?? PhpString::basename($dir);
299*cf2dcf1bSAndreas Gohr        $files = glob($dir . '/*');
300*cf2dcf1bSAndreas Gohr        if (count($files) === 1 && is_dir($files[0])) {
301*cf2dcf1bSAndreas Gohr            $dir = $files[0];
302*cf2dcf1bSAndreas Gohr        }
303*cf2dcf1bSAndreas Gohr        return [Extension::createFromDirectory($dir, null, $base)];
304*cf2dcf1bSAndreas Gohr    }
305*cf2dcf1bSAndreas Gohr
306*cf2dcf1bSAndreas Gohr    /**
307*cf2dcf1bSAndreas Gohr     * Extract the given archive to the given target directory
308*cf2dcf1bSAndreas Gohr     *
309*cf2dcf1bSAndreas Gohr     * Auto-guesses the archive type
310*cf2dcf1bSAndreas Gohr     * @throws Exception
311*cf2dcf1bSAndreas Gohr     */
312*cf2dcf1bSAndreas Gohr    protected function extractArchive($archive, $target)
313*cf2dcf1bSAndreas Gohr    {
314*cf2dcf1bSAndreas Gohr        $fh = fopen($archive, 'rb');
315*cf2dcf1bSAndreas Gohr        if (!$fh) throw new Exception('error_archive_read', [$archive]);
316*cf2dcf1bSAndreas Gohr        $magic = fread($fh, 5);
317*cf2dcf1bSAndreas Gohr        fclose($fh);
318*cf2dcf1bSAndreas Gohr
319*cf2dcf1bSAndreas Gohr        if (strpos($magic, "\x50\x4b\x03\x04") === 0) {
320*cf2dcf1bSAndreas Gohr            $archiver = new Zip();
321*cf2dcf1bSAndreas Gohr        } else {
322*cf2dcf1bSAndreas Gohr            $archiver = new Tar();
323*cf2dcf1bSAndreas Gohr        }
324*cf2dcf1bSAndreas Gohr        try {
325*cf2dcf1bSAndreas Gohr            $archiver->open($archive);
326*cf2dcf1bSAndreas Gohr            $archiver->extract($target);
327*cf2dcf1bSAndreas Gohr        } catch (ArchiveIOException|ArchiveCorruptedException|ArchiveIllegalCompressionException $e) {
328*cf2dcf1bSAndreas Gohr            throw new Exception('error_archive_extract', [$archive, $e->getMessage()], $e);
329*cf2dcf1bSAndreas Gohr        }
330*cf2dcf1bSAndreas Gohr    }
331*cf2dcf1bSAndreas Gohr
332*cf2dcf1bSAndreas Gohr    /**
333*cf2dcf1bSAndreas Gohr     * Copy with recursive sub-directory support
334*cf2dcf1bSAndreas Gohr     *
335*cf2dcf1bSAndreas Gohr     * @param string $src filename path to file
336*cf2dcf1bSAndreas Gohr     * @param string $dst filename path to file
337*cf2dcf1bSAndreas Gohr     * @throws Exception
338*cf2dcf1bSAndreas Gohr     */
339*cf2dcf1bSAndreas Gohr    protected function dircopy($src, $dst)
340*cf2dcf1bSAndreas Gohr    {
341*cf2dcf1bSAndreas Gohr        global $conf;
342*cf2dcf1bSAndreas Gohr
343*cf2dcf1bSAndreas Gohr        if (is_dir($src)) {
344*cf2dcf1bSAndreas Gohr            if (!$dh = @opendir($src)) {
345*cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_read', [$src]);
346*cf2dcf1bSAndreas Gohr            }
347*cf2dcf1bSAndreas Gohr
348*cf2dcf1bSAndreas Gohr            if (io_mkdir_p($dst)) {
349*cf2dcf1bSAndreas Gohr                while (false !== ($f = readdir($dh))) {
350*cf2dcf1bSAndreas Gohr                    if ($f == '..' || $f == '.') continue;
351*cf2dcf1bSAndreas Gohr                    $this->dircopy("$src/$f", "$dst/$f");
352*cf2dcf1bSAndreas Gohr                }
353*cf2dcf1bSAndreas Gohr            } else {
354*cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_mkdir', [$dst]);
355*cf2dcf1bSAndreas Gohr            }
356*cf2dcf1bSAndreas Gohr
357*cf2dcf1bSAndreas Gohr            closedir($dh);
358*cf2dcf1bSAndreas Gohr        } else {
359*cf2dcf1bSAndreas Gohr            $existed = file_exists($dst);
360*cf2dcf1bSAndreas Gohr
361*cf2dcf1bSAndreas Gohr            if (!@copy($src, $dst)) {
362*cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_copy', [$src, $dst]);
363*cf2dcf1bSAndreas Gohr            }
364*cf2dcf1bSAndreas Gohr            if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']);
365*cf2dcf1bSAndreas Gohr            @touch($dst, filemtime($src));
366*cf2dcf1bSAndreas Gohr        }
367*cf2dcf1bSAndreas Gohr    }
368*cf2dcf1bSAndreas Gohr
369*cf2dcf1bSAndreas Gohr    /**
370*cf2dcf1bSAndreas Gohr     * Clean up all temporary directories and reset caches
371*cf2dcf1bSAndreas Gohr     */
372*cf2dcf1bSAndreas Gohr    protected function cleanUp()
373*cf2dcf1bSAndreas Gohr    {
374*cf2dcf1bSAndreas Gohr        foreach ($this->temporary as $dir) {
375*cf2dcf1bSAndreas Gohr            io_rmdir($dir, true);
376*cf2dcf1bSAndreas Gohr        }
377*cf2dcf1bSAndreas Gohr        $this->temporary = [];
378*cf2dcf1bSAndreas Gohr
379*cf2dcf1bSAndreas Gohr        if ($this->isDirty) {
380*cf2dcf1bSAndreas Gohr            self::purgeCache();
381*cf2dcf1bSAndreas Gohr            $this->isDirty = false;
382*cf2dcf1bSAndreas Gohr        }
383*cf2dcf1bSAndreas Gohr    }
384*cf2dcf1bSAndreas Gohr}
385