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