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