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 /** 65160d3688SAndreas Gohr * Install an extension by ID 6625d28a01SAndreas Gohr * 67160d3688SAndreas 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 */ 75160d3688SAndreas Gohr public function installFromId($id, $skipInstalled = false) 76160d3688SAndreas Gohr { 7725d28a01SAndreas Gohr $extension = Extension::createFromId($id); 7825d28a01SAndreas Gohr if ($skipInstalled && $extension->isInstalled()) return; 79160d3688SAndreas Gohr $this->installExtension($extension); 80160d3688SAndreas Gohr } 81160d3688SAndreas Gohr 82160d3688SAndreas Gohr /** 83160d3688SAndreas Gohr * Install an extension 84160d3688SAndreas Gohr * 85160d3688SAndreas Gohr * This will simply call installFromUrl() with the URL from the extension 86160d3688SAndreas Gohr * 87160d3688SAndreas Gohr * @param Extension $extension 88160d3688SAndreas Gohr * @throws Exception 89160d3688SAndreas Gohr */ 90160d3688SAndreas Gohr public function installExtension(Extension $extension) 91160d3688SAndreas 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 167b2a05b76SAndreas Gohr // check PHP requirements 168*4fd6a1d7SAndreas Gohr self::ensurePhpCompatibility($extension); 16925d28a01SAndreas Gohr 17025d28a01SAndreas Gohr // install dependencies first 17125d28a01SAndreas Gohr foreach ($extension->getDependencyList() as $id) { 17225d28a01SAndreas Gohr if (isset($this->processed[$id])) continue; 17325d28a01SAndreas Gohr if ($id == $extension->getId()) continue; // avoid circular dependencies 17425d28a01SAndreas Gohr $this->installFromId($id, true); 17525d28a01SAndreas Gohr } 17625d28a01SAndreas Gohr 17725d28a01SAndreas Gohr // now install the extension 178*4fd6a1d7SAndreas Gohr self::ensurePermissions($extension); 179cf2dcf1bSAndreas Gohr $this->dircopy( 180cf2dcf1bSAndreas Gohr $extension->getCurrentDir(), 181cf2dcf1bSAndreas Gohr $extension->getInstallDir() 182cf2dcf1bSAndreas Gohr ); 183cf2dcf1bSAndreas Gohr $this->isDirty = true; 1847c9966a5SAndreas Gohr $extension->getManager()->storeUpdate($this->sourceUrl); 185cf2dcf1bSAndreas Gohr $this->removeDeletedFiles($extension); 18625d28a01SAndreas Gohr $this->processed[$extension->getId()] = $status; 187cf2dcf1bSAndreas Gohr } 188cf2dcf1bSAndreas Gohr 189cf2dcf1bSAndreas Gohr $this->cleanUp(); 190cf2dcf1bSAndreas Gohr } 191cf2dcf1bSAndreas Gohr 192cf2dcf1bSAndreas Gohr /** 193cf2dcf1bSAndreas Gohr * Uninstall an extension 194cf2dcf1bSAndreas Gohr * 195cf2dcf1bSAndreas Gohr * @param Extension $extension 196cf2dcf1bSAndreas Gohr * @throws Exception 197cf2dcf1bSAndreas Gohr */ 198cf2dcf1bSAndreas Gohr public function uninstall(Extension $extension) 199cf2dcf1bSAndreas Gohr { 200cf2dcf1bSAndreas Gohr // FIXME check if dependencies are still needed 201cf2dcf1bSAndreas Gohr 202160d3688SAndreas Gohr if (!$extension->isInstalled()) { 203160d3688SAndreas Gohr throw new Exception('error_notinstalled', [$extension->getId()]); 204160d3688SAndreas Gohr } 205160d3688SAndreas Gohr 206cf2dcf1bSAndreas Gohr if ($extension->isProtected()) { 207cf2dcf1bSAndreas Gohr throw new Exception('error_uninstall_protected', [$extension->getId()]); 208cf2dcf1bSAndreas Gohr } 209cf2dcf1bSAndreas Gohr 210*4fd6a1d7SAndreas Gohr self::ensurePermissions($extension); 211*4fd6a1d7SAndreas Gohr 212cf2dcf1bSAndreas Gohr if (!io_rmdir($extension->getInstallDir(), true)) { 213cf2dcf1bSAndreas Gohr throw new Exception('msg_delete_failed', [$extension->getId()]); 214cf2dcf1bSAndreas Gohr } 215cf2dcf1bSAndreas Gohr self::purgeCache(); 216cf2dcf1bSAndreas Gohr } 217cf2dcf1bSAndreas Gohr 218cf2dcf1bSAndreas Gohr /** 219cf2dcf1bSAndreas Gohr * Download an archive to a protected path 220cf2dcf1bSAndreas Gohr * 221cf2dcf1bSAndreas Gohr * @param string $url The url to get the archive from 222cf2dcf1bSAndreas Gohr * @return string The path where the archive was saved 223cf2dcf1bSAndreas Gohr * @throws Exception 224cf2dcf1bSAndreas Gohr */ 225cf2dcf1bSAndreas Gohr public function downloadArchive($url) 226cf2dcf1bSAndreas Gohr { 227cf2dcf1bSAndreas Gohr // check the url 228cf2dcf1bSAndreas Gohr if (!preg_match('/https?:\/\//i', $url)) { 229cf2dcf1bSAndreas Gohr throw new Exception('error_badurl'); 230cf2dcf1bSAndreas Gohr } 231cf2dcf1bSAndreas Gohr 232cf2dcf1bSAndreas Gohr // try to get the file from the path (used as plugin name fallback) 233cf2dcf1bSAndreas Gohr $file = parse_url($url, PHP_URL_PATH); 234cf2dcf1bSAndreas Gohr $file = $file ? PhpString::basename($file) : md5($url); 235cf2dcf1bSAndreas Gohr 236cf2dcf1bSAndreas Gohr // download 237cf2dcf1bSAndreas Gohr $http = new DokuHTTPClient(); 238cf2dcf1bSAndreas Gohr $http->max_bodysize = 0; 239cf2dcf1bSAndreas Gohr $http->timeout = 25; //max. 25 sec 240cf2dcf1bSAndreas Gohr $http->keep_alive = false; // we do single ops here, no need for keep-alive 241cf2dcf1bSAndreas Gohr $http->agent = 'DokuWiki HTTP Client (Extension Manager)'; 242cf2dcf1bSAndreas Gohr 243cf2dcf1bSAndreas Gohr $data = $http->get($url); 244cf2dcf1bSAndreas Gohr if ($data === false) throw new Exception('error_download', [$url, $http->error, $http->status]); 245cf2dcf1bSAndreas Gohr 246cf2dcf1bSAndreas Gohr // get filename from headers 247cf2dcf1bSAndreas Gohr if (preg_match( 248cf2dcf1bSAndreas Gohr '/attachment;\s*filename\s*=\s*"([^"]*)"/i', 249cf2dcf1bSAndreas Gohr (string)($http->resp_headers['content-disposition'] ?? ''), 250cf2dcf1bSAndreas Gohr $match 251cf2dcf1bSAndreas Gohr )) { 252cf2dcf1bSAndreas Gohr $file = PhpString::basename($match[1]); 253cf2dcf1bSAndreas Gohr } 254cf2dcf1bSAndreas Gohr 255cf2dcf1bSAndreas Gohr // clean up filename 256cf2dcf1bSAndreas Gohr $file = $this->fileToBase($file); 257cf2dcf1bSAndreas Gohr 258cf2dcf1bSAndreas Gohr // create tmp directory for download 259cf2dcf1bSAndreas Gohr $tmp = $this->mkTmpDir(); 260cf2dcf1bSAndreas Gohr 261cf2dcf1bSAndreas Gohr // save the file 262cf2dcf1bSAndreas Gohr if (@file_put_contents("$tmp/$file", $data) === false) { 263cf2dcf1bSAndreas Gohr throw new Exception('error_save'); 264cf2dcf1bSAndreas Gohr } 265cf2dcf1bSAndreas Gohr 266cf2dcf1bSAndreas Gohr return "$tmp/$file"; 267cf2dcf1bSAndreas Gohr } 268cf2dcf1bSAndreas Gohr 269cf2dcf1bSAndreas Gohr 270cf2dcf1bSAndreas Gohr /** 271cf2dcf1bSAndreas Gohr * Delete outdated files 272cf2dcf1bSAndreas Gohr */ 273cf2dcf1bSAndreas Gohr public function removeDeletedFiles(Extension $extension) 274cf2dcf1bSAndreas Gohr { 275cf2dcf1bSAndreas Gohr $extensiondir = $extension->getInstallDir(); 276cf2dcf1bSAndreas Gohr $definitionfile = $extensiondir . '/deleted.files'; 277cf2dcf1bSAndreas Gohr if (!file_exists($definitionfile)) return; 278cf2dcf1bSAndreas Gohr 279cf2dcf1bSAndreas Gohr $list = file($definitionfile); 280cf2dcf1bSAndreas Gohr foreach ($list as $line) { 281cf2dcf1bSAndreas Gohr $line = trim(preg_replace('/#.*$/', '', $line)); 282cf2dcf1bSAndreas Gohr $line = str_replace('..', '', $line); // do not run out of the extension directory 283cf2dcf1bSAndreas Gohr if (!$line) continue; 284cf2dcf1bSAndreas Gohr 285cf2dcf1bSAndreas Gohr $file = $extensiondir . '/' . $line; 286cf2dcf1bSAndreas Gohr if (!file_exists($file)) continue; 287cf2dcf1bSAndreas Gohr 288cf2dcf1bSAndreas Gohr io_rmdir($file, true); 289cf2dcf1bSAndreas Gohr } 290cf2dcf1bSAndreas Gohr } 291cf2dcf1bSAndreas Gohr 29225d28a01SAndreas Gohr /** 29325d28a01SAndreas Gohr * Purge all caches 29425d28a01SAndreas Gohr */ 295cf2dcf1bSAndreas Gohr public static function purgeCache() 296cf2dcf1bSAndreas Gohr { 297cf2dcf1bSAndreas Gohr // expire dokuwiki caches 298cf2dcf1bSAndreas Gohr // touching local.php expires wiki page, JS and CSS caches 299cf2dcf1bSAndreas Gohr global $config_cascade; 300cf2dcf1bSAndreas Gohr @touch(reset($config_cascade['main']['local'])); 301cf2dcf1bSAndreas Gohr 302cf2dcf1bSAndreas Gohr if (function_exists('opcache_reset')) { 303cf2dcf1bSAndreas Gohr opcache_reset(); 304cf2dcf1bSAndreas Gohr } 305cf2dcf1bSAndreas Gohr } 306cf2dcf1bSAndreas Gohr 307cf2dcf1bSAndreas Gohr /** 308160d3688SAndreas Gohr * Get the list of processed extensions and their status during an installation run 30925d28a01SAndreas Gohr * 31025d28a01SAndreas Gohr * @return array id => status 31125d28a01SAndreas Gohr */ 31225d28a01SAndreas Gohr public function getProcessed() 31325d28a01SAndreas Gohr { 31425d28a01SAndreas Gohr return $this->processed; 31525d28a01SAndreas Gohr } 31625d28a01SAndreas Gohr 317b2a05b76SAndreas Gohr 318b2a05b76SAndreas Gohr /** 319b2a05b76SAndreas Gohr * Ensure that the given extension is compatible with the current PHP version 320b2a05b76SAndreas Gohr * 321b2a05b76SAndreas Gohr * Throws an exception if the extension is not compatible 322b2a05b76SAndreas Gohr * 323b2a05b76SAndreas Gohr * @param Extension $extension 324b2a05b76SAndreas Gohr * @throws Exception 325b2a05b76SAndreas Gohr */ 326*4fd6a1d7SAndreas Gohr public static function ensurePhpCompatibility(Extension $extension) 327b2a05b76SAndreas Gohr { 328b2a05b76SAndreas Gohr $min = $extension->getMinimumPHPVersion(); 329b2a05b76SAndreas Gohr if ($min && version_compare(PHP_VERSION, $min, '<')) { 330b2a05b76SAndreas Gohr throw new Exception('error_minphp', [$extension->getId(), $min, PHP_VERSION]); 331b2a05b76SAndreas Gohr } 332b2a05b76SAndreas Gohr 333b2a05b76SAndreas Gohr $max = $extension->getMaximumPHPVersion(); 334b2a05b76SAndreas Gohr if ($max && version_compare(PHP_VERSION, $max, '>')) { 335b2a05b76SAndreas Gohr throw new Exception('error_maxphp', [$extension->getId(), $max, PHP_VERSION]); 336b2a05b76SAndreas Gohr } 337b2a05b76SAndreas Gohr } 338b2a05b76SAndreas Gohr 339*4fd6a1d7SAndreas Gohr /** 340*4fd6a1d7SAndreas Gohr * Ensure the file permissions are correct before attempting to install 341*4fd6a1d7SAndreas Gohr * 342*4fd6a1d7SAndreas Gohr * @throws Exception if the permissions are not correct 343*4fd6a1d7SAndreas Gohr */ 344*4fd6a1d7SAndreas Gohr public static function ensurePermissions(Extension $extension) 345*4fd6a1d7SAndreas Gohr { 346*4fd6a1d7SAndreas Gohr $target = $extension->getInstallDir(); 347*4fd6a1d7SAndreas Gohr 348*4fd6a1d7SAndreas Gohr // updates 349*4fd6a1d7SAndreas Gohr if (file_exists($target)) { 350*4fd6a1d7SAndreas Gohr if (!is_writable($target)) throw new Exception('noperms'); 351*4fd6a1d7SAndreas Gohr return; 352*4fd6a1d7SAndreas Gohr } 353*4fd6a1d7SAndreas Gohr 354*4fd6a1d7SAndreas Gohr // new installs 355*4fd6a1d7SAndreas Gohr $target = dirname($target); 356*4fd6a1d7SAndreas Gohr if (!is_writable($target)) { 357*4fd6a1d7SAndreas Gohr if ($extension->isTemplate()) throw new Exception('notplperms'); 358*4fd6a1d7SAndreas Gohr throw new Exception('nopluginperms'); 359*4fd6a1d7SAndreas Gohr } 360*4fd6a1d7SAndreas Gohr } 361b2a05b76SAndreas Gohr 36225d28a01SAndreas Gohr /** 363cf2dcf1bSAndreas Gohr * Get a base name from an archive name (we don't trust) 364cf2dcf1bSAndreas Gohr * 365cf2dcf1bSAndreas Gohr * @param string $file 366cf2dcf1bSAndreas Gohr * @return string 367cf2dcf1bSAndreas Gohr */ 368cf2dcf1bSAndreas Gohr protected function fileToBase($file) 369cf2dcf1bSAndreas Gohr { 370cf2dcf1bSAndreas Gohr $base = PhpString::basename($file); 371cf2dcf1bSAndreas Gohr $base = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $base); 372cf2dcf1bSAndreas Gohr return preg_replace('/\W+/', '', $base); 373cf2dcf1bSAndreas Gohr } 374cf2dcf1bSAndreas Gohr 375cf2dcf1bSAndreas Gohr /** 376cf2dcf1bSAndreas Gohr * Returns a temporary directory 377cf2dcf1bSAndreas Gohr * 378cf2dcf1bSAndreas Gohr * The directory is registered for cleanup when the class is destroyed 379cf2dcf1bSAndreas Gohr * 380cf2dcf1bSAndreas Gohr * @return string 381cf2dcf1bSAndreas Gohr * @throws Exception 382cf2dcf1bSAndreas Gohr */ 383cf2dcf1bSAndreas Gohr protected function mkTmpDir() 384cf2dcf1bSAndreas Gohr { 385cf2dcf1bSAndreas Gohr try { 386cf2dcf1bSAndreas Gohr $dir = io_mktmpdir(); 387cf2dcf1bSAndreas Gohr } catch (\Exception $e) { 388cf2dcf1bSAndreas Gohr throw new Exception('error_dircreate', [], $e); 389cf2dcf1bSAndreas Gohr } 390cf2dcf1bSAndreas Gohr if (!$dir) throw new Exception('error_dircreate'); 391cf2dcf1bSAndreas Gohr $this->temporary[] = $dir; 392cf2dcf1bSAndreas Gohr return $dir; 393cf2dcf1bSAndreas Gohr } 394cf2dcf1bSAndreas Gohr 395cf2dcf1bSAndreas Gohr /** 396cf2dcf1bSAndreas Gohr * Find all extensions in a given directory 397cf2dcf1bSAndreas Gohr * 398cf2dcf1bSAndreas Gohr * This allows us to install extensions from archives that contain multiple extensions and 399cf2dcf1bSAndreas Gohr * also caters for the fact that archives may or may not contain subdirectories for the extension(s). 400cf2dcf1bSAndreas Gohr * 401cf2dcf1bSAndreas Gohr * @param string $dir 402cf2dcf1bSAndreas Gohr * @return Extension[] 403cf2dcf1bSAndreas Gohr */ 404cf2dcf1bSAndreas Gohr protected function findExtensions($dir, $base = null) 405cf2dcf1bSAndreas Gohr { 406cf2dcf1bSAndreas Gohr // first check for plugin.info.txt or template.info.txt 407cf2dcf1bSAndreas Gohr $extensions = []; 408cf2dcf1bSAndreas Gohr $iterator = new RecursiveDirectoryIterator($dir); 409cf2dcf1bSAndreas Gohr foreach (new RecursiveIteratorIterator($iterator) as $file) { 410cf2dcf1bSAndreas Gohr if ( 411cf2dcf1bSAndreas Gohr $file->getFilename() === 'plugin.info.txt' || 412cf2dcf1bSAndreas Gohr $file->getFilename() === 'template.info.txt' 413cf2dcf1bSAndreas Gohr ) { 41425d28a01SAndreas Gohr $extensions[] = Extension::createFromDirectory($file->getPath()); 415cf2dcf1bSAndreas Gohr } 416cf2dcf1bSAndreas Gohr } 417cf2dcf1bSAndreas Gohr if ($extensions) return $extensions; 418cf2dcf1bSAndreas Gohr 419cf2dcf1bSAndreas Gohr // still nothing? we assume this to be a single extension that is either 420cf2dcf1bSAndreas Gohr // directly in the given directory or in single subdirectory 421cf2dcf1bSAndreas Gohr $base = $base ?? PhpString::basename($dir); 422cf2dcf1bSAndreas Gohr $files = glob($dir . '/*'); 423cf2dcf1bSAndreas Gohr if (count($files) === 1 && is_dir($files[0])) { 424cf2dcf1bSAndreas Gohr $dir = $files[0]; 425cf2dcf1bSAndreas Gohr } 426cf2dcf1bSAndreas Gohr return [Extension::createFromDirectory($dir, null, $base)]; 427cf2dcf1bSAndreas Gohr } 428cf2dcf1bSAndreas Gohr 429cf2dcf1bSAndreas Gohr /** 430cf2dcf1bSAndreas Gohr * Extract the given archive to the given target directory 431cf2dcf1bSAndreas Gohr * 432cf2dcf1bSAndreas Gohr * Auto-guesses the archive type 433cf2dcf1bSAndreas Gohr * @throws Exception 434cf2dcf1bSAndreas Gohr */ 435cf2dcf1bSAndreas Gohr protected function extractArchive($archive, $target) 436cf2dcf1bSAndreas Gohr { 437cf2dcf1bSAndreas Gohr $fh = fopen($archive, 'rb'); 438cf2dcf1bSAndreas Gohr if (!$fh) throw new Exception('error_archive_read', [$archive]); 439cf2dcf1bSAndreas Gohr $magic = fread($fh, 5); 440cf2dcf1bSAndreas Gohr fclose($fh); 441cf2dcf1bSAndreas Gohr 442cf2dcf1bSAndreas Gohr if (strpos($magic, "\x50\x4b\x03\x04") === 0) { 443cf2dcf1bSAndreas Gohr $archiver = new Zip(); 444cf2dcf1bSAndreas Gohr } else { 445cf2dcf1bSAndreas Gohr $archiver = new Tar(); 446cf2dcf1bSAndreas Gohr } 447cf2dcf1bSAndreas Gohr try { 448cf2dcf1bSAndreas Gohr $archiver->open($archive); 449cf2dcf1bSAndreas Gohr $archiver->extract($target); 450cf2dcf1bSAndreas Gohr } catch (ArchiveIOException|ArchiveCorruptedException|ArchiveIllegalCompressionException $e) { 451cf2dcf1bSAndreas Gohr throw new Exception('error_archive_extract', [$archive, $e->getMessage()], $e); 452cf2dcf1bSAndreas Gohr } 453cf2dcf1bSAndreas Gohr } 454cf2dcf1bSAndreas Gohr 455cf2dcf1bSAndreas Gohr /** 456cf2dcf1bSAndreas Gohr * Copy with recursive sub-directory support 457cf2dcf1bSAndreas Gohr * 458cf2dcf1bSAndreas Gohr * @param string $src filename path to file 459cf2dcf1bSAndreas Gohr * @param string $dst filename path to file 460cf2dcf1bSAndreas Gohr * @throws Exception 461cf2dcf1bSAndreas Gohr */ 462cf2dcf1bSAndreas Gohr protected function dircopy($src, $dst) 463cf2dcf1bSAndreas Gohr { 464cf2dcf1bSAndreas Gohr global $conf; 465cf2dcf1bSAndreas Gohr 466cf2dcf1bSAndreas Gohr if (is_dir($src)) { 467cf2dcf1bSAndreas Gohr if (!$dh = @opendir($src)) { 468cf2dcf1bSAndreas Gohr throw new Exception('error_copy_read', [$src]); 469cf2dcf1bSAndreas Gohr } 470cf2dcf1bSAndreas Gohr 471cf2dcf1bSAndreas Gohr if (io_mkdir_p($dst)) { 472cf2dcf1bSAndreas Gohr while (false !== ($f = readdir($dh))) { 473cf2dcf1bSAndreas Gohr if ($f == '..' || $f == '.') continue; 474cf2dcf1bSAndreas Gohr $this->dircopy("$src/$f", "$dst/$f"); 475cf2dcf1bSAndreas Gohr } 476cf2dcf1bSAndreas Gohr } else { 477cf2dcf1bSAndreas Gohr throw new Exception('error_copy_mkdir', [$dst]); 478cf2dcf1bSAndreas Gohr } 479cf2dcf1bSAndreas Gohr 480cf2dcf1bSAndreas Gohr closedir($dh); 481cf2dcf1bSAndreas Gohr } else { 482cf2dcf1bSAndreas Gohr $existed = file_exists($dst); 483cf2dcf1bSAndreas Gohr 484cf2dcf1bSAndreas Gohr if (!@copy($src, $dst)) { 485cf2dcf1bSAndreas Gohr throw new Exception('error_copy_copy', [$src, $dst]); 486cf2dcf1bSAndreas Gohr } 487cf2dcf1bSAndreas Gohr if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']); 488cf2dcf1bSAndreas Gohr @touch($dst, filemtime($src)); 489cf2dcf1bSAndreas Gohr } 490cf2dcf1bSAndreas Gohr } 491cf2dcf1bSAndreas Gohr 492cf2dcf1bSAndreas Gohr /** 49325d28a01SAndreas Gohr * Reset caches if needed 494cf2dcf1bSAndreas Gohr */ 495cf2dcf1bSAndreas Gohr protected function cleanUp() 496cf2dcf1bSAndreas Gohr { 497cf2dcf1bSAndreas Gohr if ($this->isDirty) { 498cf2dcf1bSAndreas Gohr self::purgeCache(); 499cf2dcf1bSAndreas Gohr $this->isDirty = false; 500cf2dcf1bSAndreas Gohr } 501cf2dcf1bSAndreas Gohr } 502cf2dcf1bSAndreas Gohr} 503