diff --git a/bin/gpm b/bin/gpm index fa5910192..d4932a20e 100755 --- a/bin/gpm +++ b/bin/gpm @@ -67,6 +67,7 @@ $app->addCommands(array( new \Grav\Console\Gpm\UninstallCommand(), new \Grav\Console\Gpm\UpdateCommand(), new \Grav\Console\Gpm\SelfupgradeCommand(), + new \Grav\Console\Gpm\DirectInstallCommand(), )); $app->run(); diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index c7170e973..ed1fca484 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -179,7 +179,7 @@ abstract class Folder /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; if ($recursive) { - $flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + $flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS; if ($locator->isStream($path)) { $directory = $locator->getRecursiveIterator($path, $flags); @@ -403,10 +403,7 @@ abstract class Folder // If the destination directory does not exist create it if (!is_dir($dest)) { - if (!mkdir($dest)) { - // If the destination directory could not be created stop processing - return false; - } + Folder::mkdir($dest); } // Open the source directory to read in files diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index b038cd5c0..add625167 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -29,6 +29,8 @@ class Installer const ZIP_OPEN_ERROR = 32; /** @const Error while trying to extract the ZIP package */ const ZIP_EXTRACT_ERROR = 64; + /** @const Invalid source file */ + const INVALID_SOURCE = 128; /** * Destination folder on which validation checks are applied @@ -62,13 +64,13 @@ class Installer /** * Installs a given package to a given destination. * - * @param string $package The local path to the ZIP package + * @param string $zip the local path to 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. + * @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter'] + * @param string $extracted The local path to the extacted ZIP package + * @return bool True if everything went fine, False otherwise. */ - public static function install($package, $destination, $options = []) + public static function install($zip, $destination, $options = [], $extracted = null) { $destination = rtrim($destination, DS); $options = array_merge(self::$options, $options); @@ -86,34 +88,26 @@ class Installer return false; } - $zip = new \ZipArchive(); - $archive = $zip->open($package); + // Create a tmp location $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); $tmp = $tmp_dir . '/Grav-' . uniqid(); - if ($archive !== true) { - self::$error = self::ZIP_OPEN_ERROR; + if (!$extracted) { + $extracted = self::unZip($zip, $tmp); + if (!$extracted) { + Folder::delete($tmp); + return false; + } + } + if (!file_exists($extracted)) { + self::$error = self::INVALID_SOURCE; return false; } - Folder::mkdir($tmp); - - $unzip = $zip->extractTo($tmp); - - if (!$unzip) { - self::$error = self::ZIP_EXTRACT_ERROR; - $zip->close(); - Folder::delete($tmp); - - return false; - } - - $package_folder_name = $zip->getNameIndex(0); - $installer_file_folder = $tmp . '/' . $package_folder_name; $is_install = true; - $installer = self::loadInstaller($installer_file_folder, $is_install); + $installer = self::loadInstaller($extracted, $is_install); if (isset($options['is_update']) && $options['is_update'] === true) { $method = 'preUpdate'; @@ -135,16 +129,15 @@ class Installer if (!$options['sophisticated']) { if ($options['theme']) { - self::copyInstall($zip, $install_path, $tmp); + self::copyInstall($extracted, $install_path); } else { - self::moveInstall($zip, $install_path, $tmp); + self::moveInstall($extracted, $install_path); } } else { - self::sophisticatedInstall($zip, $install_path, $tmp); + self::sophisticatedInstall($extracted, $install_path); } Folder::delete($tmp); - $zip->close(); if (isset($options['is_update']) && $options['is_update'] === true) { $method = 'postUpdate'; @@ -163,6 +156,43 @@ class Installer } + /** + * Unzip a file to somewhere + * + * @param $zip_file + * @param $destination + * @return bool|string + */ + public static function unZip($zip_file, $destination) + { + $zip = new \ZipArchive(); + $archive = $zip->open($zip_file); + + if ($archive === true) { + Folder::mkdir($destination); + + $unzip = $zip->extractTo($destination); + + + if (!$unzip) { + self::$error = self::ZIP_EXTRACT_ERROR; + Folder::delete($destination); + $zip->close(); + return false; + } + + $package_folder_name = $zip->getNameIndex(0); + $zip->close(); + $extracted_folder = $destination . '/' . $package_folder_name; + + return $extracted_folder; + } + + self::$error = self::ZIP_EXTRACT_ERROR; + return false; + } + + /** * Instantiates and returns the package installer class * @@ -211,80 +241,67 @@ class Installer } /** - * @param \ZipArchive $zip + * @param $source_path * @param $install_path - * @param $tmp * * @return bool */ - public static function moveInstall(\ZipArchive $zip, $install_path, $tmp) + public static function moveInstall($source_path, $install_path) { - $container = $zip->getNameIndex(0); if (file_exists($install_path)) { Folder::delete($install_path); } - Folder::move($tmp . DS . $container, $install_path); + Folder::move($source_path, $install_path); return true; } /** - * @param \ZipArchive $zip + * @param $source_path * @param $install_path - * @param $tmp * * @return bool */ - public static function copyInstall(\ZipArchive $zip, $install_path, $tmp) + public static function copyInstall($source_path, $install_path) { - $firstDir = $zip->getNameIndex(0); - if (empty($firstDir)) { - throw new \RuntimeException("Directory $firstDir is missing"); + if (empty($source_path)) { + throw new \RuntimeException("Directory $source_path is missing"); } else { - $tmp = realpath($tmp . DS . $firstDir); - Folder::rcopy($tmp, $install_path); + Folder::rcopy($source_path, $install_path); } return true; } /** - * @param \ZipArchive $zip + * @param $source_path * @param $install_path - * @param $tmp * * @return bool */ - public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp) + public static function sophisticatedInstall($source_path, $install_path) { - for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) { - $filename = $zip->getNameIndex($i); - $fileinfo = pathinfo($filename); - $depth = count(explode(DS, rtrim($filename, '/'))); + foreach (new \DirectoryIterator($source_path) as $file) { - if ($depth > 2) { + if ($file->isLink() || $file->isDot()) { continue; } - $path = $install_path . DS . $fileinfo['basename']; + $path = $install_path . DS . $file->getBasename(); - if (is_link($path)) { - continue; - } else { - if (is_dir($path)) { - Folder::delete($path); - Folder::move($tmp . DS . $filename, $path); + if ($file->isDir()) { + Folder::delete($path); + Folder::move($file->getPathname(), $path); - if ($fileinfo['basename'] == 'bin') { - foreach (glob($path . DS . '*') as $file) { - @chmod($file, 0755); - } + if ($file->getBasename() == 'bin') { + foreach (glob($path . DS . '*') as $bin_file) { + @chmod($bin_file, 0755); } - } else { - @unlink($path); - @copy($tmp . DS . $filename, $path); } + } else { + @unlink($path); + @copy($file->getPathname(), $path); } } diff --git a/system/src/Grav/Console/Gpm/DirectInstallCommand.php b/system/src/Grav/Console/Gpm/DirectInstallCommand.php new file mode 100644 index 000000000..68cbec98a --- /dev/null +++ b/system/src/Grav/Console/Gpm/DirectInstallCommand.php @@ -0,0 +1,364 @@ +setName("direct-install") + ->setAliases(['directinstall']) + ->addArgument( + 'package-file', + InputArgument::REQUIRED, + 'The local location or remote URL to an installable package file' + ) + ->setDescription("Installs Grav, plugin, or theme directly from a file or a URL") + ->setHelp('The direct-install command installs Grav, plugin, or theme directly from a file or a URL'); + } + + /** + * @return int|null|void + */ + protected function serve() + { + $package_file = $this->input->getArgument('package-file'); + + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Are you sure you want to direct-install '.$package_file.' [y|N] ', false); + + $answer = $helper->ask($this->input, $this->output, $question); + + if (!$answer) { + $this->output->writeln("exiting..."); + $this->output->writeln(''); + exit; + } + + $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); + $tmp_zip = $tmp_dir . '/Grav-' . uniqid(); + + $this->output->writeln(""); + $this->output->writeln("Preparing to install " . $package_file . ""); + + + if ($this->isRemote($package_file)) { + $zip = $this->downloadPackage($package_file, $tmp_zip); + } else { + $zip = $this->copyPackage($package_file, $tmp_zip); + } + + if (file_exists($zip)) { + $tmp_source = $tmp_dir . '/Grav-' . uniqid(); + + $this->output->write(" |- Extracting package... "); + $extracted = Installer::unZip($zip, $tmp_source); + + if (!$extracted) { + $this->output->write("\x0D"); + $this->output->writeln(" |- Extracting package... failed"); + exit; + } + + $this->output->write("\x0D"); + $this->output->writeln(" |- Extracting package... ok"); + + $type = $this->getPackageType($extracted); + + if (!$type) { + $this->output->writeln(" '- ERROR: Not a valid Grav package"); + $this->output->writeln(''); + exit; + } + + $blueprint = $this->getBlueprints($extracted); + if ($blueprint) { + if (isset($blueprint['dependencies'])) { + $depencencies = []; + foreach ($blueprint['dependencies'] as $dependency) { + if (is_array($dependency) && isset($dependency['name'])) { + $depencencies[] = $dependency['name']; + } else { + $depencencies[] = $dependency; + } + } + $this->output->writeln(" |- Dependencies found... [" . implode(',', $depencencies) . "]"); + + + + $question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false); + $answer = $helper->ask($this->input, $this->output, $question); + + if (!$answer) { + $this->output->writeln("exiting..."); + $this->output->writeln(''); + exit; + } + } + } + + if ($type == 'grav') { + + $this->output->write(" |- Checking destination... "); + Installer::isValidDestination(GRAV_ROOT . '/system'); + if (Installer::IS_LINK === Installer::lastErrorCode()) { + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... symbolic link"); + $this->output->writeln(" '- ERROR: symlinks found... " . GRAV_ROOT.""); + $this->output->writeln(''); + exit; + } + + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... ok"); + + $this->output->write(" |- Installing package... "); + Installer::install($zip, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true], $extracted); + } else { + $name = $this->getPackageName($extracted); + + if (!$name) { + $this->output->writeln("ERROR: Name could not be determined. Please specify with --name|-n"); + $this->output->writeln(''); + exit; + } + + $install_path = $this->getInstallPath($type, $name); + $is_update = file_exists($install_path); + + $this->output->write(" |- Checking destination... "); + + Installer::isValidDestination(GRAV_ROOT . DS . $install_path); + if (Installer::lastErrorCode() == Installer::IS_LINK) { + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... symbolic link"); + $this->output->writeln(" '- ERROR: symlink found... " . GRAV_ROOT . DS . $install_path . ''); + $this->output->writeln(''); + exit; + + } else { + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... ok"); + } + + $this->output->write(" |- Installing package... "); + + Installer::install($zip, GRAV_ROOT, ['install_path' => $install_path, 'theme' => (($type == 'theme')), 'is_update' => $is_update], $extracted); + } + + Folder::delete($tmp_source); + + $this->output->write("\x0D"); + + if(Installer::lastErrorCode()) { + $this->output->writeln(" '- Installation failed or aborted."); + $this->output->writeln(''); + } else { + $this->output->writeln(" |- Installing package... ok"); + $this->output->writeln(" '- Success! "); + $this->output->writeln(''); + } + + } else { + $this->output->writeln(" '- ERROR: ZIP package could not be found"); + } + + Folder::delete($tmp_zip); + + // clear cache after successful upgrade + $this->clearCache(); + + return true; + + } + + /** + * Get the install path for a name and a particular type of package + * + * @param $type + * @param $name + * @return string + */ + protected function getInstallPath($type, $name) + { + $locator = Grav::instance()['locator']; + + if ($type == 'theme') { + $install_path = $locator->findResource('themes://', false) . DS . $name; + } else { + $install_path = $locator->findResource('plugins://', false) . DS . $name; + } + return $install_path; + } + + /** + * Try to guess the package name from the source files + * + * @param $source + * @return bool|string + */ + protected function getPackageName($source) + { + foreach (glob($source . "*.yaml") as $filename) { + $name = strtolower(basename($filename, '.yaml')); + if ($name == 'blueprints') { + continue; + } + return $name; + } + return false; + } + + /** + * Try to guess the package type from the source files + * + * @param $source + * @return bool|string + */ + protected function getPackageType($source) + { + if ( + file_exists($source . 'system/defines.php') && + file_exists($source . 'system/config/system.yaml') + ) { + return 'grav'; + } else { + // must have a blueprint + if (!file_exists($source . 'blueprints.yaml')) { + return false; + } + + // either theme or plugin + $name = basename($source); + if (Utils::contains($name, 'theme')) { + return 'theme'; + } elseif (Utils::contains($name, 'plugin')) { + return 'plugin'; + } + foreach (glob($source . "*.php") as $filename) { + $contents = file_get_contents($filename); + if (Utils::contains($contents, 'Grav\Common\Theme')) { + return 'theme'; + } elseif (Utils::contains($contents, 'Grav\Common\Plugin')) { + return 'plugin'; + } + } + + // Assume it's a theme + return 'theme'; + } + } + + /** + * Determine if this is a local or a remote file + * + * @param $file + * @return bool + */ + protected function isRemote($file) + { + return (bool) filter_var($file, FILTER_VALIDATE_URL); + } + + /** + * Find/Parse the blueprint file + * + * @param $source + * @return array|bool + */ + protected function getBlueprints($source) + { + $blueprint_file = $source . 'blueprints.yaml'; + if (!file_exists($blueprint_file)) { + return false; + } + + $blueprint = (array)Yaml::parse(file_get_contents($blueprint_file)); + return $blueprint; + } + + /** + * Download the zip package via the URL + * + * @param $package_file + * @param $tmp + * @return null|string + */ + private function downloadPackage($package_file, $tmp) + { + $this->output->write(" |- Downloading package... 0%"); + + $package = parse_url($package_file); + + + $filename = basename($package['path']); + $output = Response::get($package_file, [], [$this, 'progress']); + + if ($output) { + Folder::mkdir($tmp); + + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... 100%"); + $this->output->writeln(''); + + file_put_contents($tmp . DS . $filename, $output); + + return $tmp . DS . $filename; + } + + return null; + + } + + /** + * Copy the local zip package to tmp + * + * @param $package_file + * @param $tmp + * @return null|string + */ + private function copyPackage($package_file, $tmp) + { + $this->output->write(" |- Copying package... 0%"); + + $package_file = realpath($package_file); + + if (file_exists($package_file)) { + $filename = basename($package_file); + + Folder::mkdir($tmp); + + $this->output->write("\x0D"); + $this->output->write(" |- Copying package... 100%"); + $this->output->writeln(''); + + copy(realpath($package_file), $tmp . DS . $filename); + + return $tmp . DS . $filename; + } + + return null; + + } +}