diff --git a/bin/gpm b/bin/gpm index 60713cdb4..04b67656c 100755 --- a/bin/gpm +++ b/bin/gpm @@ -25,5 +25,6 @@ $app->addCommands(array( new \Grav\Console\Gpm\FetchCommand($grav), new \Grav\Console\Gpm\IndexCommand($grav), new \Grav\Console\Gpm\InfoCommand($grav), + new \Grav\Console\Gpm\InstallCommand($grav), )); $app->run(); diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php new file mode 100644 index 000000000..efcdb02d4 --- /dev/null +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -0,0 +1,319 @@ +grav = $grav; + + // just for the gpm cli we force the filesystem driver cache + $this->grav['config']->set('system.cache.driver', 'default'); + $this->argv = $_SERVER['argv'][0]; + + parent::__construct(); + } + + protected function configure() { + $this + ->setName("install") + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'Force re-fetching the data from remote' + ) + ->addOption( + 'destination', + 'd', + InputOption::VALUE_OPTIONAL, + 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from', + GRAV_ROOT + ) + ->addArgument( + 'package', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'The package of which more informations are desired. Use the "index" command for a list of packages' + ) + ->setDescription("Lists the plugins and themes available for installation") + ->setHelp('The index command lists the plugins and themes available for installation'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $this->destination = realpath($this->input->getOption('destination')); + + $packages_to_install = array_map('strtolower', $this->input->getArgument('package')); + + $this->setColors(); + $this->isGravRoot($this->destination); + + $fetchCommand = $this->getApplication()->find('fetch'); + $args = new ArrayInput(array('command' => 'fetch', '-f' => $input->getOption('force'))); + $commandExec = $fetchCommand->run($args, $output); + + if ($commandExec != 0){ + $output->writeln("Error: An error occured while trying to fetch data from getgrav.org"); + exit; + } + + $this->data = $this->grav['cache']->fetch(md5('cli:gpm')); + + $this->output->writeln(''); + + $found_packages = $this->findPackages($packages_to_install); + + $found = array_intersect($packages_to_install, array_keys($found_packages)); + $not_found = array_diff($packages_to_install, array_keys($found_packages)); + + if (count($not_found)){ + $this->output->writeln("These packages were not found on Grav: ".implode(', ', $not_found).""); + } + + if (!count($found)){ + $this->output->writeln("Nothing to install."); + $this->output->writeln(''); + exit; + } + + $this->output->writeln(''); + foreach ($found as $package) { + $this->output->writeln("Preparing to install ".$found_packages[$package]->name." [v".$found_packages[$package]->version."]"); + + $this->output->write(" |- Downloading package... 0%"); + $this->file = $this->downloadPackage($found_packages[$package]); + + $this->output->write(" |- Checking destination... "); + $checks = $this->checkDestination($found_packages[$package]); + + if (!$checks){ + $this->output->writeln(" '- Installation failed or aborted. See errors above"); + } else { + $this->output->write(" |- Installing package... "); + $installation = $this->installPackage($found_packages[$package]); + if (!$installation){ + $this->output->writeln(" '- Installation failed or aborted. See errors above"); + $this->output->writeln(''); + } else { + $this->output->writeln(" '- Success! "); + $this->output->writeln(''); + } + } + } + + $this->output->writeln(''); + } + + private function setColors() + { + $this->output->getFormatter()->setStyle('normal', new OutputFormatterStyle('white')); + $this->output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold'))); + $this->output->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, array('bold'))); + $this->output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, array('bold'))); + $this->output->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, array('bold'))); + $this->output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, array('bold'))); + $this->output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold'))); + } + + private function findPackages($haystack) + { + $found = array(); + + foreach ($this->data as $type => $result) { + $result = json_decode($result)->results; + + foreach ($result->data as $index => $package) { + if ($this->in_arrayi($package->slug, $haystack) || $this->in_arrayi($package->name, $haystack)){ + $found[$package->slug] = $package; + } + } + } + + return $found; + } + + private function downloadPackage($package) + { + $curl = $this->getCurl($package->download); + $tmp = $this->destination.DS.'tmp-gpm'; + $filename = $package->slug.basename($package->download); + $output = curl_exec($curl); + + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... 100%"); + + curl_close($curl); + + $this->output->writeln(''); + + if (!file_exists($tmp)) @mkdir($tmp); + file_put_contents($tmp.DS.$filename, $output); + + return $tmp.DS.$filename; + } + + private function checkDestination($package) + { + $destination = $this->destination . DS . $package->install_path; + $helper = $this->getHelper('question'); + + if (is_dir($destination) && !is_link($destination)){ + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... exists"); + + $question = new ConfirmationQuestion(" | '- The package has been detected as installed already, do you want to overwrite it? [y|N] ", false); + $answer = $helper->ask($this->input, $this->output, $question); + + if (!$answer){ + $this->output->writeln(" | '- You decided to not overwrite the already installed package."); + return false; + } + + $this->rrmdir($destination); + @mkdir($destination, 0777, true); + } + + if (is_link($destination)){ + $this->output->write("\x0D"); + $this->output->writeln(" |- Checking destination... symbolic link"); + + $question = new ConfirmationQuestion(" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ", false); + $answer = $helper->ask($this->input, $this->output, $question); + + if (!$answer){ + $this->output->writeln(" | '- You decided to not delete the symlink automatically."); + return false; + } + + @unlink($destination); + } + + return true; + } + + private function installPackage($package) + { + $destination = $this->destination . DS . $package->install_path; + $zip = new \ZipArchive; + $openZip = $zip->open($this->file); + $tmp = $this->destination.DS.'tmp-gpm'; + + + if (!$openZip){ + $this->output->write("\x0D"); + $this->output->writeln(" |- Installing package... error "); + $this->output->writeln(" | '- Unable to open the downloaded package: ".$package->download.""); + + return false; + } + + $innerFolder = $zip->getNameIndex(0); + + $zip->extractTo($tmp); + $zip->close(); + + rename($tmp.DS.$innerFolder, $destination); + + $this->output->write("\x0D"); + $this->output->writeln(" |- Installing package... ok "); + return true; + } + + + private function unpackPackage($package) + { + + } + + private function isGravRoot($path) + { + if (!file_exists($path)){ + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination doesn't exist:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + + if (!is_dir($path)){ + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination chosen to install is not a directory:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + + if (!file_exists($path.DS.'index.php') || !file_exists($path.DS.'.dependencies') || !file_exists($path.DS.'system'.DS.'config'.DS.'system.yaml')){ + $this->output->writeln(''); + $this->output->writeln("ERROR: Destination chosen to install does not appear to be a Grav instance:"); + $this->output->writeln(" $path"); + $this->output->writeln(''); + exit; + } + } + + + private function getCurl($url) + { + $curl = curl_init(); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_REFERER, 'Grav GPM v'.GRAV_VERSION); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_USERAGENT, 'Grav GPM v'.GRAV_VERSION); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_NOPROGRESS, false); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, array($this, 'progress')); + + return $curl; + } + + private function progress($curl, $download_size, $downloaded) + { + if ($download_size > 0) + { + $this->output->write("\x0D"); + $this->output->write(" |- Downloading package... " . str_pad(round($downloaded / $download_size * 100, 2), 5, " ", STR_PAD_LEFT) . '%'); + } + } + + private function in_arrayi($needle, $haystack) + { + return in_array(strtolower($needle), array_map('strtolower', $haystack)); + } + + // Recursively Delete folder - DANGEROUS! USE WITH CARE!!!! + private function rrmdir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir."/".$object) == "dir") $this->rrmdir($dir."/".$object); else unlink($dir."/".$object); + } + } + reset($objects); + rmdir($dir); + return true; + } + } +}