setName('install')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force re-fetching the data from remote'
)
->addOption(
'all-yes',
'y',
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->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,
'Package(s) to install. Use "bin/gpm index" to list packages. Use "bin/gpm direct-install" to install a specific version'
)
->setDescription('Performs the installation of plugins and themes')
->setHelp('The install command allows to install plugins and themes');
}
/**
* Allows to set the GPM object, used for testing the class
*
* @param GPM $gpm
*/
public function setGpm(GPM $gpm)
{
$this->gpm = $gpm;
}
/**
* @return bool
*/
protected function serve()
{
$this->gpm = new GPM($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->displayGPMRelease();
$this->destination = realpath($this->input->getOption('destination'));
$packages = array_map('strtolower', $this->input->getArgument('package'));
$this->data = $this->gpm->findPackages($packages);
$this->loadLocalConfig();
if (
!Installer::isGravInstance($this->destination) ||
!Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
) {
$this->output->writeln('ERROR: ' . Installer::lastErrorMsg());
exit;
}
$this->output->writeln('');
if (!$this->data['total']) {
$this->output->writeln('Nothing to install.');
$this->output->writeln('');
exit;
}
if (\count($this->data['not_found'])) {
$this->output->writeln('These packages were not found on Grav: ' . implode(', ',
array_keys($this->data['not_found'])) . '');
}
unset($this->data['not_found'], $this->data['total']);
if (null !== $this->local_config) {
// Symlinks available, ask if Grav should use them
$this->use_symlinks = false;
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Should Grav use the symlinks if available? [y|N] ', false);
$answer = $this->all_yes ? false : $helper->ask($this->input, $this->output, $question);
if ($answer) {
$this->use_symlinks = true;
}
}
$this->output->writeln('');
try {
$dependencies = $this->gpm->getDependencies($packages);
} catch (\Exception $e) {
//Error out if there are incompatible packages requirements and tell which ones, and what to do
//Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken
$this->output->writeln("{$e->getMessage()}");
return false;
}
if ($dependencies) {
try {
$this->installDependencies($dependencies, 'install', 'The following dependencies need to be installed...');
$this->installDependencies($dependencies, 'update', 'The following dependencies need to be updated...');
$this->installDependencies($dependencies, 'ignore', "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false);
} catch (\Exception $e) {
$this->output->writeln('Installation aborted');
return false;
}
$this->output->writeln('Dependencies are OK');
$this->output->writeln('');
}
//We're done installing dependencies. Install the actual packages
foreach ($this->data as $data) {
foreach ($data as $package_name => $package) {
if (array_key_exists($package_name, $dependencies)) {
$this->output->writeln("Package {$package_name} already installed as dependency");
} else {
$is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path);
if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) {
$this->processPackage($package, false);
} else {
if (Installer::lastErrorCode() == Installer::EXISTS) {
try {
$this->askConfirmationIfMajorVersionUpdated($package);
$this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data));
} catch (\Exception $e) {
$this->output->writeln("{$e->getMessage()}");
return false;
}
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("The package {$package_name} is already installed, overwrite? [y|N] ", false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
if ($answer) {
$is_update = true;
$this->processPackage($package, $is_update);
} else {
$this->output->writeln("Package {$package_name} not overwritten");
}
} else {
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->writeln("Cannot overwrite existing symlink for {$package_name}");
$this->output->writeln('');
}
}
}
}
}
}
if (\count($this->demo_processing) > 0) {
foreach ($this->demo_processing as $package) {
$this->installDemoContent($package);
}
}
// clear cache after successful upgrade
$this->clearCache();
return true;
}
/**
* If the package is updated from an older major release, show warning and ask confirmation
*
* @param Package $package
*/
public function askConfirmationIfMajorVersionUpdated($package)
{
$helper = $this->getHelper('question');
$package_name = $package->name;
$new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug);
$old_version = $package->version;
$major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0];
if ($major_version_changed) {
if ($this->all_yes) {
$this->output->writeln("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}");
return;
}
$question = new ConfirmationQuestion("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}. Be sure to read what changed with the new major release. Continue? [y|N] ", false);
if (!$helper->ask($this->input, $this->output, $question)) {
$this->output->writeln("Package {$package_name} not updated");
exit;
}
}
}
/**
* Given a $dependencies list, filters their type according to $type and
* shows $message prior to listing them to the user. Then asks the user a confirmation prior
* to installing them.
*
* @param array $dependencies The dependencies array
* @param string $type The type of dependency to show: install, update, ignore
* @param string $message A message to be shown prior to listing the dependencies
* @param bool $required A flag that determines if the installation is required or optional
*
* @throws \Exception
*/
public function installDependencies($dependencies, $type, $message, $required = true)
{
$packages = array_filter($dependencies, function ($action) use ($type) { return $action === $type; });
if (\count($packages) > 0) {
$this->output->writeln($message);
foreach ($packages as $dependencyName => $dependencyVersion) {
$this->output->writeln(" |- Package {$dependencyName}");
}
$this->output->writeln('');
$helper = $this->getHelper('question');
if ($type === 'install') {
$questionAction = 'Install';
} else {
$questionAction = 'Update';
}
if (\count($packages) === 1) {
$questionArticle = 'this';
} else {
$questionArticle = 'these';
}
if (\count($packages) === 1) {
$questionNoun = 'package';
} else {
$questionNoun = 'packages';
}
$question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
if ($answer) {
foreach ($packages as $dependencyName => $dependencyVersion) {
$package = $this->gpm->findPackage($dependencyName);
$this->processPackage($package, $type === 'update');
}
$this->output->writeln('');
} else {
if ($required) {
throw new \Exception();
}
}
}
}
/**
* @param Package $package
* @param bool $is_update True if the package is an update
*/
private function processPackage($package, $is_update = false)
{
if (!$package) {
$this->output->writeln('Package not found on the GPM!');
$this->output->writeln('');
return;
}
$symlink = false;
if ($this->use_symlinks) {
if (!isset($package->version) || $this->getSymlinkSource($package)) {
$symlink = true;
}
}
$symlink ? $this->processSymlink($package) : $this->processGpm($package, $is_update);
$this->processDemo($package);
}
/**
* Add package to the queue to process the demo content, if demo content exists
*
* @param Package $package
*/
private function processDemo($package)
{
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
$this->demo_processing[] = $package;
}
}
/**
* Prompt to install the demo content of a package
*
* @param Package $package
*/
private function installDemoContent($package)
{
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
$dest_dir = $this->destination . DS . 'user';
$pages_dir = $dest_dir . DS . 'pages';
// Demo content exists, prompt to install it.
$this->output->writeln("Attention: {$package->name} contains demo content");
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
$answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" '- Skipped! ");
$this->output->writeln('');
return;
}
// if pages folder exists in demo
if (file_exists($demo_dir . DS . 'pages')) {
$pages_backup = 'pages.' . date('m-d-Y-H-i-s');
$question = new ConfirmationQuestion('This will backup your current `user/pages` folder to `user/' . $pages_backup . '`, continue? [y|N]', false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" '- Skipped! ");
$this->output->writeln('');
return;
}
// backup current pages folder
if (file_exists($dest_dir)) {
if (rename($pages_dir, $dest_dir . DS . $pages_backup)) {
$this->output->writeln(' |- Backing up pages... ok');
} else {
$this->output->writeln(' |- Backing up pages... failed');
}
}
}
// Confirmation received, copy over the data
$this->output->writeln(' |- Installing demo content... ok ');
Folder::rcopy($demo_dir, $dest_dir);
$this->output->writeln(" '- Success! ");
$this->output->writeln('');
}
}
/**
* @param Package $package
*
* @return array|bool
*/
private function getGitRegexMatches($package)
{
if (isset($package->repository)) {
$repository = $package->repository;
} else {
return false;
}
preg_match(GIT_REGEX, $repository, $matches);
return $matches;
}
/**
* @param Package $package
*
* @return bool|string
*/
private function getSymlinkSource($package)
{
$matches = $this->getGitRegexMatches($package);
foreach ($this->local_config as $paths) {
if (Utils::endsWith($matches[2], '.git')) {
$repo_dir = preg_replace('/\.git$/', '', $matches[2]);
} else {
$repo_dir = $matches[2];
}
$paths = (array) $paths;
foreach ($paths as $repo) {
$path = rtrim($repo, '/') . '/' . $repo_dir;
if (file_exists($path)) {
return $path;
}
}
}
return false;
}
/**
* @param Package $package
*/
private function processSymlink($package)
{
exec('cd ' . $this->destination);
$to = $this->destination . DS . $package->install_path;
$from = $this->getSymlinkSource($package);
$this->output->writeln("Preparing to Symlink {$package->name}");
$this->output->write(' |- Checking source... ');
if (file_exists($from)) {
$this->output->writeln('ok');
$this->output->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- Installation failed or aborted.");
$this->output->writeln('');
} else {
if (file_exists($to)) {
$this->output->writeln(" '- Symlink cannot overwrite an existing package, please remove first");
$this->output->writeln('');
} else {
symlink($from, $to);
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Symlinking package... ok ');
$this->output->writeln(" '- Success! ");
$this->output->writeln('');
}
}
return;
}
$this->output->writeln('not found!');
$this->output->writeln(" '- Installation failed or aborted.");
}
/**
* @param Package $package
* @param bool $is_update
*
* @return bool
*/
private function processGpm($package, $is_update = false)
{
$version = isset($package->available) ? $package->available : $package->version;
$license = Licenses::get($package->slug);
$this->output->writeln("Preparing to install {$package->name} [v{$version}]");
$this->output->write(' |- Downloading package... 0%');
$this->file = $this->downloadPackage($package, $license);
if (!$this->file) {
$this->output->writeln(" '- Installation failed or aborted.");
$this->output->writeln('');
return false;
}
$this->output->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- Installation failed or aborted.");
$this->output->writeln('');
} else {
$this->output->write(' |- Installing package... ');
$installation = $this->installPackage($package, $is_update);
if (!$installation) {
$this->output->writeln(" '- Installation failed or aborted.");
$this->output->writeln('');
} else {
$this->output->writeln(" '- Success! ");
$this->output->writeln('');
return true;
}
}
return false;
}
/**
* @param Package $package
*
* @param string $license
*
* @return string
*/
private function downloadPackage($package, $license = null)
{
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid();
$filename = $package->slug . basename($package->zipball_url);
$filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename);
$query = '';
if (!empty($package->premium)) {
$query = \json_encode(array_merge(
$package->premium,
[
'slug' => $package->slug,
'filename' => $package->premium['filename'],
'license_key' => $license,
'sid' => md5(GRAV_ROOT)
]
));
$query = '?d=' . base64_encode($query);
}
try {
$output = Response::get($package->zipball_url . $query, [], [$this, 'progress']);
} catch (\Exception $e) {
$error = str_replace("\n", "\n | '- ", $e->getMessage());
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Downloading package... error ');
$this->output->writeln(" | '- " . $error);
return false;
}
Folder::create($this->tmp);
$this->output->write("\x0D");
$this->output->write(' |- Downloading package... 100%');
$this->output->writeln('');
file_put_contents($this->tmp . DS . $filename, $output);
return $this->tmp . DS . $filename;
}
/**
* @param Package $package
*
* @return bool
*/
private function checkDestination($package)
{
$question_helper = $this->getHelper('question');
Installer::isValidDestination($this->destination . DS . $package->install_path);
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... symbolic link');
if ($this->all_yes) {
$this->output->writeln(" | '- Skipped automatically.");
return false;
}
$question = new ConfirmationQuestion(" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
false);
$answer = $question_helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" | '- You decided to not delete the symlink automatically.");
return false;
}
unlink($this->destination . DS . $package->install_path);
}
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... ok');
return true;
}
/**
* Install a package
*
* @param Package $package
* @param bool $is_update True if it's an update. False if it's an install
*
* @return bool
*/
private function installPackage($package, $is_update = false)
{
$type = $package->package_type;
Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]);
$error_code = Installer::lastErrorCode();
Folder::delete($this->tmp);
if ($error_code) {
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing package... error ');
$this->output->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$message = Installer::getMessage();
if ($message) {
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- {$message}");
}
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing package... ok ');
return true;
}
/**
* @param array $progress
*/
public function progress($progress)
{
$this->output->write("\x0D");
$this->output->write(' |- Downloading package... ' . str_pad($progress['percent'], 5, ' ',
STR_PAD_LEFT) . '%');
}
}