xref: /dokuwiki/lib/plugins/extension/Installer.php (revision 160d3688fe2b70ebe88d464cba3417440127a155)
1cf2dcf1bSAndreas Gohr<?php
2cf2dcf1bSAndreas Gohr
3cf2dcf1bSAndreas Gohrnamespace dokuwiki\plugin\extension;
4cf2dcf1bSAndreas Gohr
5cf2dcf1bSAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient;
6cf2dcf1bSAndreas Gohruse dokuwiki\Utf8\PhpString;
7cf2dcf1bSAndreas Gohruse RecursiveDirectoryIterator;
8cf2dcf1bSAndreas Gohruse RecursiveIteratorIterator;
9cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveCorruptedException;
10cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveIllegalCompressionException;
11cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\ArchiveIOException;
12cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\Tar;
13cf2dcf1bSAndreas Gohruse splitbrain\PHPArchive\Zip;
14cf2dcf1bSAndreas Gohr
15cf2dcf1bSAndreas Gohr/**
16cf2dcf1bSAndreas Gohr * Install and deinstall extensions
17cf2dcf1bSAndreas Gohr *
18cf2dcf1bSAndreas Gohr * This manages all the file operations and downloads needed to install an extension.
19cf2dcf1bSAndreas Gohr */
20cf2dcf1bSAndreas Gohrclass Installer
21cf2dcf1bSAndreas Gohr{
22cf2dcf1bSAndreas Gohr    /** @var string[] a list of temporary directories used during this installation */
23cf2dcf1bSAndreas Gohr    protected array $temporary = [];
24cf2dcf1bSAndreas Gohr
25cf2dcf1bSAndreas Gohr    /** @var bool if changes have been made that require a cache purge */
26cf2dcf1bSAndreas Gohr    protected $isDirty = false;
27cf2dcf1bSAndreas Gohr
28cf2dcf1bSAndreas Gohr    /** @var bool Replace existing files? */
29cf2dcf1bSAndreas Gohr    protected $overwrite = false;
30cf2dcf1bSAndreas Gohr
31cf2dcf1bSAndreas Gohr    /** @var string The last used URL to install an extension */
32cf2dcf1bSAndreas Gohr    protected $sourceUrl = '';
33cf2dcf1bSAndreas Gohr
3425d28a01SAndreas Gohr    protected $processed = [];
3525d28a01SAndreas Gohr
3625d28a01SAndreas Gohr    public const STATUS_SKIPPED = 'skipped';
3725d28a01SAndreas Gohr    public const STATUS_UPDATED = 'updated';
3825d28a01SAndreas Gohr    public const STATUS_INSTALLED = 'installed';
3925d28a01SAndreas Gohr
4025d28a01SAndreas Gohr
41cf2dcf1bSAndreas Gohr    /**
42cf2dcf1bSAndreas Gohr     * Initialize a new extension installer
43cf2dcf1bSAndreas Gohr     *
44cf2dcf1bSAndreas Gohr     * @param bool $overwrite
45cf2dcf1bSAndreas Gohr     */
46cf2dcf1bSAndreas Gohr    public function __construct($overwrite = false)
47cf2dcf1bSAndreas Gohr    {
48cf2dcf1bSAndreas Gohr        $this->overwrite = $overwrite;
49cf2dcf1bSAndreas Gohr    }
50cf2dcf1bSAndreas Gohr
51cf2dcf1bSAndreas Gohr    /**
52cf2dcf1bSAndreas Gohr     * Destructor
53cf2dcf1bSAndreas Gohr     *
54cf2dcf1bSAndreas Gohr     * deletes any dangling temporary directories
55cf2dcf1bSAndreas Gohr     */
56cf2dcf1bSAndreas Gohr    public function __destruct()
57cf2dcf1bSAndreas Gohr    {
5825d28a01SAndreas Gohr        foreach ($this->temporary as $dir) {
5925d28a01SAndreas Gohr            io_rmdir($dir, true);
6025d28a01SAndreas Gohr        }
61cf2dcf1bSAndreas Gohr        $this->cleanUp();
62cf2dcf1bSAndreas Gohr    }
63cf2dcf1bSAndreas Gohr
64cf2dcf1bSAndreas Gohr    /**
65*160d3688SAndreas Gohr     * Install an extension by ID
6625d28a01SAndreas Gohr     *
67*160d3688SAndreas Gohr     * This will simply call installExtension after constructing an extension from the ID
6825d28a01SAndreas Gohr     *
6925d28a01SAndreas Gohr     * The $skipInstalled parameter should only be used when installing dependencies
7025d28a01SAndreas Gohr     *
7125d28a01SAndreas Gohr     * @param string $id the extension ID
7225d28a01SAndreas Gohr     * @param bool $skipInstalled Ignore the overwrite setting and skip installed extensions
7325d28a01SAndreas Gohr     * @throws Exception
7425d28a01SAndreas Gohr     */
75*160d3688SAndreas Gohr    public function installFromId($id, $skipInstalled = false)
76*160d3688SAndreas Gohr    {
7725d28a01SAndreas Gohr        $extension = Extension::createFromId($id);
7825d28a01SAndreas Gohr        if ($skipInstalled && $extension->isInstalled()) return;
79*160d3688SAndreas Gohr        $this->installExtension($extension);
80*160d3688SAndreas Gohr    }
81*160d3688SAndreas Gohr
82*160d3688SAndreas Gohr    /**
83*160d3688SAndreas Gohr     * Install an extension
84*160d3688SAndreas Gohr     *
85*160d3688SAndreas Gohr     * This will simply call installFromUrl() with the URL from the extension
86*160d3688SAndreas Gohr     *
87*160d3688SAndreas Gohr     * @param Extension $extension
88*160d3688SAndreas Gohr     * @throws Exception
89*160d3688SAndreas Gohr     */
90*160d3688SAndreas Gohr    public function installExtension(Extension $extension)
91*160d3688SAndreas Gohr    {
9225d28a01SAndreas Gohr        $url = $extension->getDownloadURL();
9325d28a01SAndreas Gohr        if (!$url) {
9425d28a01SAndreas Gohr            throw new Exception('error_nourl', [$extension->getId()]);
9525d28a01SAndreas Gohr        }
9625d28a01SAndreas Gohr        $this->installFromUrl($url);
9725d28a01SAndreas Gohr    }
9825d28a01SAndreas Gohr
9925d28a01SAndreas Gohr    /**
100cf2dcf1bSAndreas Gohr     * Install extensions from a given URL
101cf2dcf1bSAndreas Gohr     *
102cf2dcf1bSAndreas Gohr     * @param string $url the URL to the archive
103cf2dcf1bSAndreas Gohr     * @param null $base the base directory name to use
104cf2dcf1bSAndreas Gohr     * @throws Exception
105cf2dcf1bSAndreas Gohr     */
106cf2dcf1bSAndreas Gohr    public function installFromUrl($url, $base = null)
107cf2dcf1bSAndreas Gohr    {
108cf2dcf1bSAndreas Gohr        $this->sourceUrl = $url;
109cf2dcf1bSAndreas Gohr        $archive = $this->downloadArchive($url);
110cf2dcf1bSAndreas Gohr        $this->installFromArchive(
111cf2dcf1bSAndreas Gohr            $archive,
112cf2dcf1bSAndreas Gohr            $base
113cf2dcf1bSAndreas Gohr        );
114cf2dcf1bSAndreas Gohr    }
115cf2dcf1bSAndreas Gohr
116cf2dcf1bSAndreas Gohr    /**
117cf2dcf1bSAndreas Gohr     * Install extensions from a user upload
118cf2dcf1bSAndreas Gohr     *
119cf2dcf1bSAndreas Gohr     * @param string $field name of the upload file
120cf2dcf1bSAndreas Gohr     * @throws Exception
121cf2dcf1bSAndreas Gohr     */
122cf2dcf1bSAndreas Gohr    public function installFromUpload($field)
123cf2dcf1bSAndreas Gohr    {
124cf2dcf1bSAndreas Gohr        $this->sourceUrl = '';
125cf2dcf1bSAndreas Gohr        if ($_FILES[$field]['error']) {
126cf2dcf1bSAndreas Gohr            throw new Exception('msg_upload_failed', [$_FILES[$field]['error']]);
127cf2dcf1bSAndreas Gohr        }
128cf2dcf1bSAndreas Gohr
129cf2dcf1bSAndreas Gohr        $tmp = $this->mkTmpDir();
130cf2dcf1bSAndreas Gohr        if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
131cf2dcf1bSAndreas Gohr            throw new Exception('msg_upload_failed', ['move failed']);
132cf2dcf1bSAndreas Gohr        }
133cf2dcf1bSAndreas Gohr        $this->installFromArchive(
134cf2dcf1bSAndreas Gohr            "$tmp/upload.archive",
135cf2dcf1bSAndreas Gohr            $this->fileToBase($_FILES[$field]['name']),
136cf2dcf1bSAndreas Gohr        );
137cf2dcf1bSAndreas Gohr    }
138cf2dcf1bSAndreas Gohr
139cf2dcf1bSAndreas Gohr    /**
140cf2dcf1bSAndreas Gohr     * Install extensions from an archive
141cf2dcf1bSAndreas Gohr     *
142cf2dcf1bSAndreas Gohr     * The archive is extracted to a temporary directory and then the contained extensions are installed.
143cf2dcf1bSAndreas Gohr     * This is is the ultimate installation procedure and all other install methods will end up here.
144cf2dcf1bSAndreas Gohr     *
145cf2dcf1bSAndreas Gohr     * @param string $archive the path to the archive
146cf2dcf1bSAndreas Gohr     * @param string $base the base directory name to use
147cf2dcf1bSAndreas Gohr     * @throws Exception
148cf2dcf1bSAndreas Gohr     */
149cf2dcf1bSAndreas Gohr    public function installFromArchive($archive, $base = null)
150cf2dcf1bSAndreas Gohr    {
151cf2dcf1bSAndreas Gohr        if ($base === null) $base = $this->fileToBase($archive);
152cf2dcf1bSAndreas Gohr        $target = $this->mkTmpDir() . '/' . $base;
153cf2dcf1bSAndreas Gohr        $this->extractArchive($archive, $target);
154cf2dcf1bSAndreas Gohr        $extensions = $this->findExtensions($target, $base);
155cf2dcf1bSAndreas Gohr        foreach ($extensions as $extension) {
15625d28a01SAndreas Gohr            // check installation status
15725d28a01SAndreas Gohr            if ($extension->isInstalled()) {
15825d28a01SAndreas Gohr                if (!$this->overwrite) {
15925d28a01SAndreas Gohr                    $this->processed[$extension->getId()] = self::STATUS_SKIPPED;
160cf2dcf1bSAndreas Gohr                    continue;
161cf2dcf1bSAndreas Gohr                }
16225d28a01SAndreas Gohr                $status = self::STATUS_UPDATED;
16325d28a01SAndreas Gohr            } else {
16425d28a01SAndreas Gohr                $status = self::STATUS_INSTALLED;
16525d28a01SAndreas Gohr            }
166cf2dcf1bSAndreas Gohr
16725d28a01SAndreas Gohr            // FIXME check PHP requirements
16825d28a01SAndreas Gohr
16925d28a01SAndreas Gohr            // install dependencies first
17025d28a01SAndreas Gohr            foreach ($extension->getDependencyList() as $id) {
17125d28a01SAndreas Gohr                if (isset($this->processed[$id])) continue;
17225d28a01SAndreas Gohr                if ($id == $extension->getId()) continue; // avoid circular dependencies
17325d28a01SAndreas Gohr                $this->installFromId($id, true);
17425d28a01SAndreas Gohr            }
17525d28a01SAndreas Gohr
17625d28a01SAndreas Gohr            // now install the extension
177cf2dcf1bSAndreas Gohr            $this->dircopy(
178cf2dcf1bSAndreas Gohr                $extension->getCurrentDir(),
179cf2dcf1bSAndreas Gohr                $extension->getInstallDir()
180cf2dcf1bSAndreas Gohr            );
181cf2dcf1bSAndreas Gohr            $this->isDirty = true;
1827c9966a5SAndreas Gohr            $extension->getManager()->storeUpdate($this->sourceUrl);
183cf2dcf1bSAndreas Gohr            $this->removeDeletedFiles($extension);
18425d28a01SAndreas Gohr            $this->processed[$extension->getId()] = $status;
185cf2dcf1bSAndreas Gohr        }
186cf2dcf1bSAndreas Gohr
187cf2dcf1bSAndreas Gohr        $this->cleanUp();
188cf2dcf1bSAndreas Gohr    }
189cf2dcf1bSAndreas Gohr
190cf2dcf1bSAndreas Gohr    /**
191cf2dcf1bSAndreas Gohr     * Uninstall an extension
192cf2dcf1bSAndreas Gohr     *
193cf2dcf1bSAndreas Gohr     * @param Extension $extension
194cf2dcf1bSAndreas Gohr     * @throws Exception
195cf2dcf1bSAndreas Gohr     */
196cf2dcf1bSAndreas Gohr    public function uninstall(Extension $extension)
197cf2dcf1bSAndreas Gohr    {
198cf2dcf1bSAndreas Gohr        // FIXME check if dependencies are still needed
199cf2dcf1bSAndreas Gohr
200*160d3688SAndreas Gohr        if (!$extension->isInstalled()) {
201*160d3688SAndreas Gohr            throw new Exception('error_notinstalled', [$extension->getId()]);
202*160d3688SAndreas Gohr        }
203*160d3688SAndreas Gohr
204cf2dcf1bSAndreas Gohr        if ($extension->isProtected()) {
205cf2dcf1bSAndreas Gohr            throw new Exception('error_uninstall_protected', [$extension->getId()]);
206cf2dcf1bSAndreas Gohr        }
207cf2dcf1bSAndreas Gohr
208cf2dcf1bSAndreas Gohr        if (!io_rmdir($extension->getInstallDir(), true)) {
209cf2dcf1bSAndreas Gohr            throw new Exception('msg_delete_failed', [$extension->getId()]);
210cf2dcf1bSAndreas Gohr        }
211cf2dcf1bSAndreas Gohr        self::purgeCache();
212cf2dcf1bSAndreas Gohr    }
213cf2dcf1bSAndreas Gohr
214cf2dcf1bSAndreas Gohr    /**
215cf2dcf1bSAndreas Gohr     * Download an archive to a protected path
216cf2dcf1bSAndreas Gohr     *
217cf2dcf1bSAndreas Gohr     * @param string $url The url to get the archive from
218cf2dcf1bSAndreas Gohr     * @return string The path where the archive was saved
219cf2dcf1bSAndreas Gohr     * @throws Exception
220cf2dcf1bSAndreas Gohr     */
221cf2dcf1bSAndreas Gohr    public function downloadArchive($url)
222cf2dcf1bSAndreas Gohr    {
223cf2dcf1bSAndreas Gohr        // check the url
224cf2dcf1bSAndreas Gohr        if (!preg_match('/https?:\/\//i', $url)) {
225cf2dcf1bSAndreas Gohr            throw new Exception('error_badurl');
226cf2dcf1bSAndreas Gohr        }
227cf2dcf1bSAndreas Gohr
228cf2dcf1bSAndreas Gohr        // try to get the file from the path (used as plugin name fallback)
229cf2dcf1bSAndreas Gohr        $file = parse_url($url, PHP_URL_PATH);
230cf2dcf1bSAndreas Gohr        $file = $file ? PhpString::basename($file) : md5($url);
231cf2dcf1bSAndreas Gohr
232cf2dcf1bSAndreas Gohr        // download
233cf2dcf1bSAndreas Gohr        $http = new DokuHTTPClient();
234cf2dcf1bSAndreas Gohr        $http->max_bodysize = 0;
235cf2dcf1bSAndreas Gohr        $http->timeout = 25; //max. 25 sec
236cf2dcf1bSAndreas Gohr        $http->keep_alive = false; // we do single ops here, no need for keep-alive
237cf2dcf1bSAndreas Gohr        $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
238cf2dcf1bSAndreas Gohr
239cf2dcf1bSAndreas Gohr        $data = $http->get($url);
240cf2dcf1bSAndreas Gohr        if ($data === false) throw new Exception('error_download', [$url, $http->error, $http->status]);
241cf2dcf1bSAndreas Gohr
242cf2dcf1bSAndreas Gohr        // get filename from headers
243cf2dcf1bSAndreas Gohr        if (preg_match(
244cf2dcf1bSAndreas Gohr            '/attachment;\s*filename\s*=\s*"([^"]*)"/i',
245cf2dcf1bSAndreas Gohr            (string)($http->resp_headers['content-disposition'] ?? ''),
246cf2dcf1bSAndreas Gohr            $match
247cf2dcf1bSAndreas Gohr        )) {
248cf2dcf1bSAndreas Gohr            $file = PhpString::basename($match[1]);
249cf2dcf1bSAndreas Gohr        }
250cf2dcf1bSAndreas Gohr
251cf2dcf1bSAndreas Gohr        // clean up filename
252cf2dcf1bSAndreas Gohr        $file = $this->fileToBase($file);
253cf2dcf1bSAndreas Gohr
254cf2dcf1bSAndreas Gohr        // create tmp directory for download
255cf2dcf1bSAndreas Gohr        $tmp = $this->mkTmpDir();
256cf2dcf1bSAndreas Gohr
257cf2dcf1bSAndreas Gohr        // save the file
258cf2dcf1bSAndreas Gohr        if (@file_put_contents("$tmp/$file", $data) === false) {
259cf2dcf1bSAndreas Gohr            throw new Exception('error_save');
260cf2dcf1bSAndreas Gohr        }
261cf2dcf1bSAndreas Gohr
262cf2dcf1bSAndreas Gohr        return "$tmp/$file";
263cf2dcf1bSAndreas Gohr    }
264cf2dcf1bSAndreas Gohr
265cf2dcf1bSAndreas Gohr
266cf2dcf1bSAndreas Gohr    /**
267cf2dcf1bSAndreas Gohr     * Delete outdated files
268cf2dcf1bSAndreas Gohr     */
269cf2dcf1bSAndreas Gohr    public function removeDeletedFiles(Extension $extension)
270cf2dcf1bSAndreas Gohr    {
271cf2dcf1bSAndreas Gohr        $extensiondir = $extension->getInstallDir();
272cf2dcf1bSAndreas Gohr        $definitionfile = $extensiondir . '/deleted.files';
273cf2dcf1bSAndreas Gohr        if (!file_exists($definitionfile)) return;
274cf2dcf1bSAndreas Gohr
275cf2dcf1bSAndreas Gohr        $list = file($definitionfile);
276cf2dcf1bSAndreas Gohr        foreach ($list as $line) {
277cf2dcf1bSAndreas Gohr            $line = trim(preg_replace('/#.*$/', '', $line));
278cf2dcf1bSAndreas Gohr            $line = str_replace('..', '', $line); // do not run out of the extension directory
279cf2dcf1bSAndreas Gohr            if (!$line) continue;
280cf2dcf1bSAndreas Gohr
281cf2dcf1bSAndreas Gohr            $file = $extensiondir . '/' . $line;
282cf2dcf1bSAndreas Gohr            if (!file_exists($file)) continue;
283cf2dcf1bSAndreas Gohr
284cf2dcf1bSAndreas Gohr            io_rmdir($file, true);
285cf2dcf1bSAndreas Gohr        }
286cf2dcf1bSAndreas Gohr    }
287cf2dcf1bSAndreas Gohr
28825d28a01SAndreas Gohr    /**
28925d28a01SAndreas Gohr     * Purge all caches
29025d28a01SAndreas Gohr     */
291cf2dcf1bSAndreas Gohr    public static function purgeCache()
292cf2dcf1bSAndreas Gohr    {
293cf2dcf1bSAndreas Gohr        // expire dokuwiki caches
294cf2dcf1bSAndreas Gohr        // touching local.php expires wiki page, JS and CSS caches
295cf2dcf1bSAndreas Gohr        global $config_cascade;
296cf2dcf1bSAndreas Gohr        @touch(reset($config_cascade['main']['local']));
297cf2dcf1bSAndreas Gohr
298cf2dcf1bSAndreas Gohr        if (function_exists('opcache_reset')) {
299cf2dcf1bSAndreas Gohr            opcache_reset();
300cf2dcf1bSAndreas Gohr        }
301cf2dcf1bSAndreas Gohr    }
302cf2dcf1bSAndreas Gohr
303cf2dcf1bSAndreas Gohr    /**
304*160d3688SAndreas Gohr     * Get the list of processed extensions and their status during an installation run
30525d28a01SAndreas Gohr     *
30625d28a01SAndreas Gohr     * @return array id => status
30725d28a01SAndreas Gohr     */
30825d28a01SAndreas Gohr    public function getProcessed()
30925d28a01SAndreas Gohr    {
31025d28a01SAndreas Gohr        return $this->processed;
31125d28a01SAndreas Gohr    }
31225d28a01SAndreas Gohr
31325d28a01SAndreas Gohr    /**
314cf2dcf1bSAndreas Gohr     * Get a base name from an archive name (we don't trust)
315cf2dcf1bSAndreas Gohr     *
316cf2dcf1bSAndreas Gohr     * @param string $file
317cf2dcf1bSAndreas Gohr     * @return string
318cf2dcf1bSAndreas Gohr     */
319cf2dcf1bSAndreas Gohr    protected function fileToBase($file)
320cf2dcf1bSAndreas Gohr    {
321cf2dcf1bSAndreas Gohr        $base = PhpString::basename($file);
322cf2dcf1bSAndreas Gohr        $base = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $base);
323cf2dcf1bSAndreas Gohr        return preg_replace('/\W+/', '', $base);
324cf2dcf1bSAndreas Gohr    }
325cf2dcf1bSAndreas Gohr
326cf2dcf1bSAndreas Gohr    /**
327cf2dcf1bSAndreas Gohr     * Returns a temporary directory
328cf2dcf1bSAndreas Gohr     *
329cf2dcf1bSAndreas Gohr     * The directory is registered for cleanup when the class is destroyed
330cf2dcf1bSAndreas Gohr     *
331cf2dcf1bSAndreas Gohr     * @return string
332cf2dcf1bSAndreas Gohr     * @throws Exception
333cf2dcf1bSAndreas Gohr     */
334cf2dcf1bSAndreas Gohr    protected function mkTmpDir()
335cf2dcf1bSAndreas Gohr    {
336cf2dcf1bSAndreas Gohr        try {
337cf2dcf1bSAndreas Gohr            $dir = io_mktmpdir();
338cf2dcf1bSAndreas Gohr        } catch (\Exception $e) {
339cf2dcf1bSAndreas Gohr            throw new Exception('error_dircreate', [], $e);
340cf2dcf1bSAndreas Gohr        }
341cf2dcf1bSAndreas Gohr        if (!$dir) throw new Exception('error_dircreate');
342cf2dcf1bSAndreas Gohr        $this->temporary[] = $dir;
343cf2dcf1bSAndreas Gohr        return $dir;
344cf2dcf1bSAndreas Gohr    }
345cf2dcf1bSAndreas Gohr
346cf2dcf1bSAndreas Gohr    /**
347cf2dcf1bSAndreas Gohr     * Find all extensions in a given directory
348cf2dcf1bSAndreas Gohr     *
349cf2dcf1bSAndreas Gohr     * This allows us to install extensions from archives that contain multiple extensions and
350cf2dcf1bSAndreas Gohr     * also caters for the fact that archives may or may not contain subdirectories for the extension(s).
351cf2dcf1bSAndreas Gohr     *
352cf2dcf1bSAndreas Gohr     * @param string $dir
353cf2dcf1bSAndreas Gohr     * @return Extension[]
354cf2dcf1bSAndreas Gohr     */
355cf2dcf1bSAndreas Gohr    protected function findExtensions($dir, $base = null)
356cf2dcf1bSAndreas Gohr    {
357cf2dcf1bSAndreas Gohr        // first check for plugin.info.txt or template.info.txt
358cf2dcf1bSAndreas Gohr        $extensions = [];
359cf2dcf1bSAndreas Gohr        $iterator = new RecursiveDirectoryIterator($dir);
360cf2dcf1bSAndreas Gohr        foreach (new RecursiveIteratorIterator($iterator) as $file) {
361cf2dcf1bSAndreas Gohr            if (
362cf2dcf1bSAndreas Gohr                $file->getFilename() === 'plugin.info.txt' ||
363cf2dcf1bSAndreas Gohr                $file->getFilename() === 'template.info.txt'
364cf2dcf1bSAndreas Gohr            ) {
36525d28a01SAndreas Gohr                $extensions[] = Extension::createFromDirectory($file->getPath());
366cf2dcf1bSAndreas Gohr            }
367cf2dcf1bSAndreas Gohr        }
368cf2dcf1bSAndreas Gohr        if ($extensions) return $extensions;
369cf2dcf1bSAndreas Gohr
370cf2dcf1bSAndreas Gohr        // still nothing? we assume this to be a single extension that is either
371cf2dcf1bSAndreas Gohr        // directly in the given directory or in single subdirectory
372cf2dcf1bSAndreas Gohr        $base = $base ?? PhpString::basename($dir);
373cf2dcf1bSAndreas Gohr        $files = glob($dir . '/*');
374cf2dcf1bSAndreas Gohr        if (count($files) === 1 && is_dir($files[0])) {
375cf2dcf1bSAndreas Gohr            $dir = $files[0];
376cf2dcf1bSAndreas Gohr        }
377cf2dcf1bSAndreas Gohr        return [Extension::createFromDirectory($dir, null, $base)];
378cf2dcf1bSAndreas Gohr    }
379cf2dcf1bSAndreas Gohr
380cf2dcf1bSAndreas Gohr    /**
381cf2dcf1bSAndreas Gohr     * Extract the given archive to the given target directory
382cf2dcf1bSAndreas Gohr     *
383cf2dcf1bSAndreas Gohr     * Auto-guesses the archive type
384cf2dcf1bSAndreas Gohr     * @throws Exception
385cf2dcf1bSAndreas Gohr     */
386cf2dcf1bSAndreas Gohr    protected function extractArchive($archive, $target)
387cf2dcf1bSAndreas Gohr    {
388cf2dcf1bSAndreas Gohr        $fh = fopen($archive, 'rb');
389cf2dcf1bSAndreas Gohr        if (!$fh) throw new Exception('error_archive_read', [$archive]);
390cf2dcf1bSAndreas Gohr        $magic = fread($fh, 5);
391cf2dcf1bSAndreas Gohr        fclose($fh);
392cf2dcf1bSAndreas Gohr
393cf2dcf1bSAndreas Gohr        if (strpos($magic, "\x50\x4b\x03\x04") === 0) {
394cf2dcf1bSAndreas Gohr            $archiver = new Zip();
395cf2dcf1bSAndreas Gohr        } else {
396cf2dcf1bSAndreas Gohr            $archiver = new Tar();
397cf2dcf1bSAndreas Gohr        }
398cf2dcf1bSAndreas Gohr        try {
399cf2dcf1bSAndreas Gohr            $archiver->open($archive);
400cf2dcf1bSAndreas Gohr            $archiver->extract($target);
401cf2dcf1bSAndreas Gohr        } catch (ArchiveIOException|ArchiveCorruptedException|ArchiveIllegalCompressionException $e) {
402cf2dcf1bSAndreas Gohr            throw new Exception('error_archive_extract', [$archive, $e->getMessage()], $e);
403cf2dcf1bSAndreas Gohr        }
404cf2dcf1bSAndreas Gohr    }
405cf2dcf1bSAndreas Gohr
406cf2dcf1bSAndreas Gohr    /**
407cf2dcf1bSAndreas Gohr     * Copy with recursive sub-directory support
408cf2dcf1bSAndreas Gohr     *
409cf2dcf1bSAndreas Gohr     * @param string $src filename path to file
410cf2dcf1bSAndreas Gohr     * @param string $dst filename path to file
411cf2dcf1bSAndreas Gohr     * @throws Exception
412cf2dcf1bSAndreas Gohr     */
413cf2dcf1bSAndreas Gohr    protected function dircopy($src, $dst)
414cf2dcf1bSAndreas Gohr    {
415cf2dcf1bSAndreas Gohr        global $conf;
416cf2dcf1bSAndreas Gohr
417cf2dcf1bSAndreas Gohr        if (is_dir($src)) {
418cf2dcf1bSAndreas Gohr            if (!$dh = @opendir($src)) {
419cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_read', [$src]);
420cf2dcf1bSAndreas Gohr            }
421cf2dcf1bSAndreas Gohr
422cf2dcf1bSAndreas Gohr            if (io_mkdir_p($dst)) {
423cf2dcf1bSAndreas Gohr                while (false !== ($f = readdir($dh))) {
424cf2dcf1bSAndreas Gohr                    if ($f == '..' || $f == '.') continue;
425cf2dcf1bSAndreas Gohr                    $this->dircopy("$src/$f", "$dst/$f");
426cf2dcf1bSAndreas Gohr                }
427cf2dcf1bSAndreas Gohr            } else {
428cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_mkdir', [$dst]);
429cf2dcf1bSAndreas Gohr            }
430cf2dcf1bSAndreas Gohr
431cf2dcf1bSAndreas Gohr            closedir($dh);
432cf2dcf1bSAndreas Gohr        } else {
433cf2dcf1bSAndreas Gohr            $existed = file_exists($dst);
434cf2dcf1bSAndreas Gohr
435cf2dcf1bSAndreas Gohr            if (!@copy($src, $dst)) {
436cf2dcf1bSAndreas Gohr                throw new Exception('error_copy_copy', [$src, $dst]);
437cf2dcf1bSAndreas Gohr            }
438cf2dcf1bSAndreas Gohr            if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']);
439cf2dcf1bSAndreas Gohr            @touch($dst, filemtime($src));
440cf2dcf1bSAndreas Gohr        }
441cf2dcf1bSAndreas Gohr    }
442cf2dcf1bSAndreas Gohr
443cf2dcf1bSAndreas Gohr    /**
44425d28a01SAndreas Gohr     * Reset caches if needed
445cf2dcf1bSAndreas Gohr     */
446cf2dcf1bSAndreas Gohr    protected function cleanUp()
447cf2dcf1bSAndreas Gohr    {
448cf2dcf1bSAndreas Gohr        if ($this->isDirty) {
449cf2dcf1bSAndreas Gohr            self::purgeCache();
450cf2dcf1bSAndreas Gohr            $this->isDirty = false;
451cf2dcf1bSAndreas Gohr        }
452cf2dcf1bSAndreas Gohr    }
453cf2dcf1bSAndreas Gohr}
454