true, 'ignore_symlinks' => true, 'sophisticated' => false, 'theme' => false, 'install_path' => '', 'exclude_checks' => [self::EXISTS, self::NOT_FOUND, self::IS_LINK] ]; /** * Installs a given package to a given destination. * * @param string $package The local path to the ZIP package * @param string $destination The local path to the Grav Instance * @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter'] * * @return boolean True if everything went fine, False otherwise. */ public static function install($package, $destination, $options = []) { $destination = rtrim($destination, DS); $options = array_merge(self::$options, $options); $install_path = rtrim($destination . DS . ltrim($options['install_path'], DS), DS); if (!self::isGravInstance($destination) || !self::isValidDestination($install_path, $options['exclude_checks'])) { return false; } if (self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] || self::lastErrorCode() == self::EXISTS && !$options['overwrite']) { return false; } // Pre install checks static::flightProcessing('pre_install', $install_path); $zip = new \ZipArchive(); $archive = $zip->open($package); $tmp = CACHE_DIR . 'tmp/Grav-' . uniqid(); if ($archive !== true) { self::$error = self::ZIP_OPEN_ERROR; return false; } Folder::mkdir($tmp); $unzip = $zip->extractTo($tmp); if (!$unzip) { self::$error = self::ZIP_EXTRACT_ERROR; $zip->close(); Folder::delete($tmp); return false; } if (!$options['sophisticated']) { if ($options['theme']) { self::copyInstall($zip, $install_path, $tmp); } else { self::moveInstall($zip, $install_path, $tmp); } } else { self::sophisticatedInstall($zip, $install_path, $tmp); } Folder::delete($tmp); $zip->close(); // Post install checks static::flightProcessing('post_install', $install_path); self::$error = self::OK; return true; } protected static function flightProcessing($state, $install_path) { $blueprints_path = $install_path . DS . 'blueprints.yaml'; if (file_exists($blueprints_path)) { $package_yaml = Yaml::parse(file_get_contents($blueprints_path)); if (isset($package_yaml['install'][$state]['create'])) { foreach ((array) $package_yaml['install']['pre_install']['create'] as $file) { Folder::mkdir($install_path . '/' . ltrim($file, '/')); } } if (isset($package_yaml['install'][$state]['remove'])) { foreach ((array) $package_yaml['install']['pre_install']['remove'] as $file) { Folder::delete($install_path . '/' . ltrim($file, '/')); } } } } public static function moveInstall(\ZipArchive $zip, $install_path, $tmp) { $container = $zip->getNameIndex(0); if (file_exists($install_path)) { Folder::delete($install_path); } Folder::move($tmp . DS . $container, $install_path); return true; } public static function copyInstall(\ZipArchive $zip, $install_path, $tmp) { $firstDir = $zip->getNameIndex(0); if (empty($firstDir)) { throw new \RuntimeException("Directory $firstDir is missing"); } else { $tmp = realpath($tmp . DS . $firstDir); Folder::rcopy($tmp, $install_path); } return true; } public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp) { for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) { $filename = $zip->getNameIndex($i); $fileinfo = pathinfo($filename); $depth = count(explode(DS, rtrim($filename, '/'))); if ($depth > 2) { continue; } $path = $install_path . DS . $fileinfo['basename']; if (is_link($path)) { continue; } else { if (is_dir($path)) { Folder::delete($path); Folder::move($tmp . DS . $filename, $path); if ($fileinfo['basename'] == 'bin') { foreach (glob($path . DS . '*') as $file) { @chmod($file, 0755); } } } else { @unlink($path); @copy($tmp . DS . $filename, $path); } } } return true; } /** * Uninstalls one or more given package * * @param string $path The slug of the package(s) * @param array $options Options to use for uninstalling * * @return boolean True if everything went fine, False otherwise. */ public static function uninstall($path, $options = []) { $options = array_merge(self::$options, $options); if (!self::isValidDestination($path, $options['exclude_checks']) ) { return false; } return Folder::delete($path); } /** * Runs a set of checks on the destination and sets the Error if any * * @param string $destination The directory to run validations at * @param array $exclude An array of constants to exclude from the validation * * @return boolean True if validation passed. False otherwise */ public static function isValidDestination($destination, $exclude = []) { self::$error = 0; self::$target = $destination; if (is_link($destination)) { self::$error = self::IS_LINK; } elseif (file_exists($destination)) { self::$error = self::EXISTS; } elseif (!file_exists($destination)) { self::$error = self::NOT_FOUND; } elseif (!is_dir($destination)) { self::$error = self::NOT_DIRECTORY; } if (count($exclude) && in_array(self::$error, $exclude)) { return true; } return !(self::$error); } /** * Validates if the given path is a Grav Instance * * @param string $target The local path to the Grav Instance * * @return boolean True if is a Grav Instance. False otherwise */ public static function isGravInstance($target) { self::$error = 0; self::$target = $target; if ( !file_exists($target . DS . 'index.php') || !file_exists($target . DS . 'bin') || !file_exists($target . DS . 'user') || !file_exists($target . DS . 'system' . DS . 'config' . DS . 'system.yaml') ) { self::$error = self::NOT_GRAV_ROOT; } return !self::$error; } /** * Returns the last error occurred in a string message format * @return string The message of the last error */ public static function lastErrorMsg() { $msg = 'Unknown Error'; switch (self::$error) { case 0: $msg = 'No Error'; break; case self::EXISTS: $msg = 'The target path "' . self::$target . '" already exists'; break; case self::IS_LINK: $msg = 'The target path "' . self::$target . '" is a symbolic link'; break; case self::NOT_FOUND: $msg = 'The target path "' . self::$target . '" does not appear to exist'; break; case self::NOT_DIRECTORY: $msg = 'The target path "' . self::$target . '" does not appear to be a folder'; break; case self::NOT_GRAV_ROOT: $msg = 'The target path "' . self::$target . '" does not appear to be a Grav instance'; break; case self::ZIP_OPEN_ERROR: $msg = 'Unable to open the package file'; break; case self::ZIP_EXTRACT_ERROR: $msg = 'An error occurred while extracting the package'; break; default: return 'Unknown error'; break; } return $msg; } /** * Returns the last error code of the occurred error * @return integer The code of the last error */ public static function lastErrorCode() { return self::$error; } /** * Allows to manually set an error * @param $error the Error code */ public static function setError($error) { self::$error = $error; } }