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;
+
+ }
+}