"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Console/Gpm/InstallCommand.php" (1 Sep 2020, 24665 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "InstallCommand.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Console\Gpm
    5  *
    6  * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
    7  * @license    MIT License; see LICENSE file for details.
    8  */
    9 
   10 namespace Grav\Console\Gpm;
   11 
   12 use Grav\Common\Filesystem\Folder;
   13 use Grav\Common\GPM\GPM;
   14 use Grav\Common\GPM\Installer;
   15 use Grav\Common\GPM\Licenses;
   16 use Grav\Common\GPM\Response;
   17 use Grav\Common\GPM\Remote\Package;
   18 use Grav\Common\Grav;
   19 use Grav\Common\Utils;
   20 use Grav\Console\ConsoleCommand;
   21 use Symfony\Component\Console\Input\InputArgument;
   22 use Symfony\Component\Console\Input\InputOption;
   23 use Symfony\Component\Console\Question\ConfirmationQuestion;
   24 
   25 \define('GIT_REGEX', '/http[s]?:\/\/(?:.*@)?(github|bitbucket)(?:.org|.com)\/.*\/(.*)/');
   26 
   27 class InstallCommand extends ConsoleCommand
   28 {
   29     /** @var array */
   30     protected $data;
   31 
   32     /** @var GPM */
   33     protected $gpm;
   34 
   35     /** @var string */
   36     protected $destination;
   37 
   38     /** @var string */
   39     protected $file;
   40 
   41     /** @var string */
   42     protected $tmp;
   43 
   44     /** @var array */
   45     protected $local_config;
   46 
   47     /** @var bool */
   48     protected $use_symlinks;
   49 
   50     /** @var array */
   51     protected $demo_processing = [];
   52 
   53     /** @var string */
   54     protected $all_yes;
   55 
   56     protected function configure()
   57     {
   58         $this
   59             ->setName('install')
   60             ->addOption(
   61                 'force',
   62                 'f',
   63                 InputOption::VALUE_NONE,
   64                 'Force re-fetching the data from remote'
   65             )
   66             ->addOption(
   67                 'all-yes',
   68                 'y',
   69                 InputOption::VALUE_NONE,
   70                 'Assumes yes (or best approach) instead of prompting'
   71             )
   72             ->addOption(
   73                 'destination',
   74                 'd',
   75                 InputOption::VALUE_OPTIONAL,
   76                 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from',
   77                 GRAV_ROOT
   78             )
   79             ->addArgument(
   80                 'package',
   81                 InputArgument::IS_ARRAY | InputArgument::REQUIRED,
   82                 'Package(s) to install. Use "bin/gpm index" to list packages. Use "bin/gpm direct-install" to install a specific version'
   83             )
   84             ->setDescription('Performs the installation of plugins and themes')
   85             ->setHelp('The <info>install</info> command allows to install plugins and themes');
   86     }
   87 
   88     /**
   89      * Allows to set the GPM object, used for testing the class
   90      *
   91      * @param GPM $gpm
   92      */
   93     public function setGpm(GPM $gpm)
   94     {
   95         $this->gpm = $gpm;
   96     }
   97 
   98     /**
   99      * @return bool
  100      */
  101     protected function serve()
  102     {
  103         $this->gpm = new GPM($this->input->getOption('force'));
  104 
  105         $this->all_yes = $this->input->getOption('all-yes');
  106 
  107         $this->displayGPMRelease();
  108 
  109         $this->destination = realpath($this->input->getOption('destination'));
  110 
  111         $packages = array_map('strtolower', $this->input->getArgument('package'));
  112         $this->data = $this->gpm->findPackages($packages);
  113         $this->loadLocalConfig();
  114 
  115         if (
  116             !Installer::isGravInstance($this->destination) ||
  117             !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
  118         ) {
  119             $this->output->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
  120             exit;
  121         }
  122 
  123         $this->output->writeln('');
  124 
  125         if (!$this->data['total']) {
  126             $this->output->writeln('Nothing to install.');
  127             $this->output->writeln('');
  128             exit;
  129         }
  130 
  131         if (\count($this->data['not_found'])) {
  132             $this->output->writeln('These packages were not found on Grav: <red>' . implode('</red>, <red>',
  133                     array_keys($this->data['not_found'])) . '</red>');
  134         }
  135 
  136         unset($this->data['not_found'], $this->data['total']);
  137 
  138         if (null !== $this->local_config) {
  139             // Symlinks available, ask if Grav should use them
  140             $this->use_symlinks = false;
  141             $helper = $this->getHelper('question');
  142             $question = new ConfirmationQuestion('Should Grav use the symlinks if available? [y|N] ', false);
  143 
  144             $answer = $this->all_yes ? false : $helper->ask($this->input, $this->output, $question);
  145 
  146             if ($answer) {
  147                 $this->use_symlinks = true;
  148             }
  149         }
  150 
  151         $this->output->writeln('');
  152 
  153         try {
  154             $dependencies = $this->gpm->getDependencies($packages);
  155         } catch (\Exception $e) {
  156             //Error out if there are incompatible packages requirements and tell which ones, and what to do
  157             //Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken
  158             $this->output->writeln("<red>{$e->getMessage()}</red>");
  159             return false;
  160         }
  161 
  162         if ($dependencies) {
  163             try {
  164                 $this->installDependencies($dependencies, 'install', 'The following dependencies need to be installed...');
  165                 $this->installDependencies($dependencies, 'update',  'The following dependencies need to be updated...');
  166                 $this->installDependencies($dependencies, 'ignore',  "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false);
  167             } catch (\Exception $e) {
  168                 $this->output->writeln('<red>Installation aborted</red>');
  169                 return false;
  170             }
  171 
  172             $this->output->writeln('<green>Dependencies are OK</green>');
  173             $this->output->writeln('');
  174         }
  175 
  176 
  177         //We're done installing dependencies. Install the actual packages
  178         foreach ($this->data as $data) {
  179             foreach ($data as $package_name => $package) {
  180                 if (array_key_exists($package_name, $dependencies)) {
  181                     $this->output->writeln("<green>Package {$package_name} already installed as dependency</green>");
  182                 } else {
  183                     $is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path);
  184                     if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) {
  185                         $this->processPackage($package, false);
  186                     } else {
  187                         if (Installer::lastErrorCode() == Installer::EXISTS) {
  188 
  189                             try {
  190                                 $this->askConfirmationIfMajorVersionUpdated($package);
  191                                 $this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data));
  192                             } catch (\Exception $e) {
  193                                 $this->output->writeln("<red>{$e->getMessage()}</red>");
  194                                 return false;
  195                             }
  196 
  197                             $helper = $this->getHelper('question');
  198                             $question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> is already installed, overwrite? [y|N] ", false);
  199                             $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
  200 
  201                             if ($answer) {
  202                                 $is_update = true;
  203                                 $this->processPackage($package, $is_update);
  204                             } else {
  205                                 $this->output->writeln("<yellow>Package {$package_name} not overwritten</yellow>");
  206                             }
  207                         } else {
  208                             if (Installer::lastErrorCode() == Installer::IS_LINK) {
  209                                 $this->output->writeln("<red>Cannot overwrite existing symlink for </red><cyan>{$package_name}</cyan>");
  210                                 $this->output->writeln('');
  211                             }
  212                         }
  213                     }
  214                 }
  215             }
  216         }
  217 
  218         if (\count($this->demo_processing) > 0) {
  219             foreach ($this->demo_processing as $package) {
  220                 $this->installDemoContent($package);
  221             }
  222         }
  223 
  224         // clear cache after successful upgrade
  225         $this->clearCache();
  226 
  227         return true;
  228     }
  229 
  230     /**
  231      * If the package is updated from an older major release, show warning and ask confirmation
  232      *
  233      * @param Package $package
  234      */
  235     public function askConfirmationIfMajorVersionUpdated($package)
  236     {
  237         $helper = $this->getHelper('question');
  238         $package_name = $package->name;
  239         $new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug);
  240         $old_version = $package->version;
  241 
  242         $major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0];
  243 
  244         if ($major_version_changed) {
  245             if ($this->all_yes) {
  246                 $this->output->writeln("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>");
  247                 return;
  248             }
  249 
  250             $question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>. Be sure to read what changed with the new major release. Continue? [y|N] ", false);
  251 
  252             if (!$helper->ask($this->input, $this->output, $question)) {
  253                 $this->output->writeln("<yellow>Package {$package_name} not updated</yellow>");
  254                 exit;
  255             }
  256         }
  257     }
  258 
  259     /**
  260      * Given a $dependencies list, filters their type according to $type and
  261      * shows $message prior to listing them to the user. Then asks the user a confirmation prior
  262      * to installing them.
  263      *
  264      * @param array  $dependencies The dependencies array
  265      * @param string $type         The type of dependency to show: install, update, ignore
  266      * @param string $message      A message to be shown prior to listing the dependencies
  267      * @param bool   $required     A flag that determines if the installation is required or optional
  268      *
  269      * @throws \Exception
  270      */
  271     public function installDependencies($dependencies, $type, $message, $required = true)
  272     {
  273         $packages = array_filter($dependencies, function ($action) use ($type) { return $action === $type; });
  274         if (\count($packages) > 0) {
  275             $this->output->writeln($message);
  276 
  277             foreach ($packages as $dependencyName => $dependencyVersion) {
  278                 $this->output->writeln("  |- Package <cyan>{$dependencyName}</cyan>");
  279             }
  280 
  281             $this->output->writeln('');
  282 
  283             $helper = $this->getHelper('question');
  284 
  285             if ($type === 'install') {
  286                 $questionAction = 'Install';
  287             } else {
  288                 $questionAction = 'Update';
  289             }
  290 
  291             if (\count($packages) === 1) {
  292                 $questionArticle = 'this';
  293             } else {
  294                 $questionArticle = 'these';
  295             }
  296 
  297             if (\count($packages) === 1) {
  298                 $questionNoun = 'package';
  299             } else {
  300                 $questionNoun = 'packages';
  301             }
  302 
  303             $question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true);
  304             $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
  305 
  306             if ($answer) {
  307                 foreach ($packages as $dependencyName => $dependencyVersion) {
  308                     $package = $this->gpm->findPackage($dependencyName);
  309                     $this->processPackage($package, $type === 'update');
  310                 }
  311                 $this->output->writeln('');
  312             } else {
  313                 if ($required) {
  314                     throw new \Exception();
  315                 }
  316             }
  317         }
  318     }
  319 
  320     /**
  321      * @param Package $package
  322      * @param bool    $is_update      True if the package is an update
  323      */
  324     private function processPackage($package, $is_update = false)
  325     {
  326         if (!$package) {
  327             $this->output->writeln('<red>Package not found on the GPM!</red>');
  328             $this->output->writeln('');
  329             return;
  330         }
  331 
  332         $symlink = false;
  333         if ($this->use_symlinks) {
  334             if (!isset($package->version) || $this->getSymlinkSource($package)) {
  335                 $symlink = true;
  336             }
  337         }
  338 
  339         $symlink ? $this->processSymlink($package) : $this->processGpm($package, $is_update);
  340 
  341         $this->processDemo($package);
  342     }
  343 
  344     /**
  345      * Add package to the queue to process the demo content, if demo content exists
  346      *
  347      * @param Package $package
  348      */
  349     private function processDemo($package)
  350     {
  351         $demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
  352         if (file_exists($demo_dir)) {
  353             $this->demo_processing[] = $package;
  354         }
  355     }
  356 
  357     /**
  358      * Prompt to install the demo content of a package
  359      *
  360      * @param Package $package
  361      */
  362     private function installDemoContent($package)
  363     {
  364         $demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
  365 
  366         if (file_exists($demo_dir)) {
  367             $dest_dir = $this->destination . DS . 'user';
  368             $pages_dir = $dest_dir . DS . 'pages';
  369 
  370             // Demo content exists, prompt to install it.
  371             $this->output->writeln("<white>Attention: </white><cyan>{$package->name}</cyan> contains demo content");
  372             $helper = $this->getHelper('question');
  373             $question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
  374 
  375             $answer = $helper->ask($this->input, $this->output, $question);
  376 
  377             if (!$answer) {
  378                 $this->output->writeln("  '- <red>Skipped!</red>  ");
  379                 $this->output->writeln('');
  380 
  381                 return;
  382             }
  383 
  384             // if pages folder exists in demo
  385             if (file_exists($demo_dir . DS . 'pages')) {
  386                 $pages_backup = 'pages.' . date('m-d-Y-H-i-s');
  387                 $question = new ConfirmationQuestion('This will backup your current `user/pages` folder to `user/' . $pages_backup . '`, continue? [y|N]', false);
  388                 $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
  389 
  390                 if (!$answer) {
  391                     $this->output->writeln("  '- <red>Skipped!</red>  ");
  392                     $this->output->writeln('');
  393 
  394                     return;
  395                 }
  396 
  397                 // backup current pages folder
  398                 if (file_exists($dest_dir)) {
  399                     if (rename($pages_dir, $dest_dir . DS . $pages_backup)) {
  400                         $this->output->writeln('  |- Backing up pages...    <green>ok</green>');
  401                     } else {
  402                         $this->output->writeln('  |- Backing up pages...    <red>failed</red>');
  403                     }
  404                 }
  405             }
  406 
  407             // Confirmation received, copy over the data
  408             $this->output->writeln('  |- Installing demo content...    <green>ok</green>                             ');
  409             Folder::rcopy($demo_dir, $dest_dir);
  410             $this->output->writeln("  '- <green>Success!</green>  ");
  411             $this->output->writeln('');
  412         }
  413     }
  414 
  415     /**
  416      * @param Package $package
  417      *
  418      * @return array|bool
  419      */
  420     private function getGitRegexMatches($package)
  421     {
  422         if (isset($package->repository)) {
  423             $repository = $package->repository;
  424         } else {
  425             return false;
  426         }
  427 
  428         preg_match(GIT_REGEX, $repository, $matches);
  429 
  430         return $matches;
  431     }
  432 
  433     /**
  434      * @param Package $package
  435      *
  436      * @return bool|string
  437      */
  438     private function getSymlinkSource($package)
  439     {
  440         $matches = $this->getGitRegexMatches($package);
  441 
  442         foreach ($this->local_config as $paths) {
  443             if (Utils::endsWith($matches[2], '.git')) {
  444                 $repo_dir = preg_replace('/\.git$/', '', $matches[2]);
  445             } else {
  446                 $repo_dir = $matches[2];
  447             }
  448             
  449             $paths = (array) $paths;
  450             foreach ($paths as $repo) {
  451                 $path = rtrim($repo, '/') . '/' . $repo_dir;
  452                 if (file_exists($path)) {
  453                     return $path;
  454                 }
  455             }
  456 
  457         }
  458 
  459         return false;
  460     }
  461 
  462     /**
  463      * @param  Package    $package
  464      */
  465     private function processSymlink($package)
  466     {
  467 
  468         exec('cd ' . $this->destination);
  469 
  470         $to = $this->destination . DS . $package->install_path;
  471         $from = $this->getSymlinkSource($package);
  472 
  473         $this->output->writeln("Preparing to Symlink <cyan>{$package->name}</cyan>");
  474         $this->output->write('  |- Checking source...  ');
  475 
  476         if (file_exists($from)) {
  477             $this->output->writeln('<green>ok</green>');
  478 
  479             $this->output->write('  |- Checking destination...  ');
  480             $checks = $this->checkDestination($package);
  481 
  482             if (!$checks) {
  483                 $this->output->writeln("  '- <red>Installation failed or aborted.</red>");
  484                 $this->output->writeln('');
  485             } else {
  486                 if (file_exists($to)) {
  487                     $this->output->writeln("  '- <red>Symlink cannot overwrite an existing package, please remove first</red>");
  488                     $this->output->writeln('');
  489                 } else {
  490                     symlink($from, $to);
  491 
  492                     // extra white spaces to clear out the buffer properly
  493                     $this->output->writeln('  |- Symlinking package...    <green>ok</green>                             ');
  494                     $this->output->writeln("  '- <green>Success!</green>  ");
  495                     $this->output->writeln('');
  496                 }
  497             }
  498 
  499             return;
  500         }
  501 
  502         $this->output->writeln('<red>not found!</red>');
  503         $this->output->writeln("  '- <red>Installation failed or aborted.</red>");
  504     }
  505 
  506     /**
  507      * @param Package   $package
  508      * @param bool      $is_update
  509      *
  510      * @return bool
  511      */
  512     private function processGpm($package, $is_update = false)
  513     {
  514         $version = isset($package->available) ? $package->available : $package->version;
  515         $license = Licenses::get($package->slug);
  516 
  517         $this->output->writeln("Preparing to install <cyan>{$package->name}</cyan> [v{$version}]");
  518 
  519         $this->output->write('  |- Downloading package...     0%');
  520         $this->file = $this->downloadPackage($package, $license);
  521 
  522         if (!$this->file) {
  523             $this->output->writeln("  '- <red>Installation failed or aborted.</red>");
  524             $this->output->writeln('');
  525 
  526             return false;
  527         }
  528 
  529         $this->output->write('  |- Checking destination...  ');
  530         $checks = $this->checkDestination($package);
  531 
  532         if (!$checks) {
  533             $this->output->writeln("  '- <red>Installation failed or aborted.</red>");
  534             $this->output->writeln('');
  535         } else {
  536             $this->output->write('  |- Installing package...  ');
  537             $installation = $this->installPackage($package, $is_update);
  538             if (!$installation) {
  539                 $this->output->writeln("  '- <red>Installation failed or aborted.</red>");
  540                 $this->output->writeln('');
  541             } else {
  542                 $this->output->writeln("  '- <green>Success!</green>  ");
  543                 $this->output->writeln('');
  544 
  545                 return true;
  546             }
  547         }
  548 
  549         return false;
  550     }
  551 
  552     /**
  553      * @param Package $package
  554      *
  555      * @param string    $license
  556      *
  557      * @return string
  558      */
  559     private function downloadPackage($package, $license = null)
  560     {
  561         $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
  562         $this->tmp = $tmp_dir . '/Grav-' . uniqid();
  563         $filename = $package->slug . basename($package->zipball_url);
  564         $filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename);
  565         $query = '';
  566 
  567         if (!empty($package->premium)) {
  568             $query = \json_encode(array_merge(
  569                 $package->premium,
  570                 [
  571                     'slug' => $package->slug,
  572                     'filename' => $package->premium['filename'],
  573                     'license_key' => $license
  574                 ]
  575             ));
  576 
  577             $query = '?d=' . base64_encode($query);
  578         }
  579 
  580         try {
  581             $output = Response::get($package->zipball_url . $query, [], [$this, 'progress']);
  582         } catch (\Exception $e) {
  583             $error = str_replace("\n", "\n  |  '- ", $e->getMessage());
  584             $this->output->write("\x0D");
  585             // extra white spaces to clear out the buffer properly
  586             $this->output->writeln('  |- Downloading package...    <red>error</red>                             ');
  587             $this->output->writeln("  |  '- " . $error);
  588 
  589             return false;
  590         }
  591 
  592         Folder::create($this->tmp);
  593 
  594         $this->output->write("\x0D");
  595         $this->output->write('  |- Downloading package...   100%');
  596         $this->output->writeln('');
  597 
  598         file_put_contents($this->tmp . DS . $filename, $output);
  599 
  600         return $this->tmp . DS . $filename;
  601     }
  602 
  603     /**
  604      * @param Package     $package
  605      *
  606      * @return bool
  607      */
  608     private function checkDestination($package)
  609     {
  610         $question_helper = $this->getHelper('question');
  611 
  612         Installer::isValidDestination($this->destination . DS . $package->install_path);
  613 
  614         if (Installer::lastErrorCode() == Installer::IS_LINK) {
  615             $this->output->write("\x0D");
  616             $this->output->writeln('  |- Checking destination...  <yellow>symbolic link</yellow>');
  617 
  618             if ($this->all_yes) {
  619                 $this->output->writeln("  |     '- <yellow>Skipped automatically.</yellow>");
  620 
  621                 return false;
  622             }
  623 
  624             $question = new ConfirmationQuestion("  |  '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
  625                 false);
  626             $answer = $question_helper->ask($this->input, $this->output, $question);
  627 
  628             if (!$answer) {
  629                 $this->output->writeln("  |     '- <red>You decided to not delete the symlink automatically.</red>");
  630 
  631                 return false;
  632             }
  633 
  634             unlink($this->destination . DS . $package->install_path);
  635         }
  636 
  637         $this->output->write("\x0D");
  638         $this->output->writeln('  |- Checking destination...  <green>ok</green>');
  639 
  640         return true;
  641     }
  642 
  643     /**
  644      * Install a package
  645      *
  646      * @param Package $package
  647      * @param bool    $is_update True if it's an update. False if it's an install
  648      *
  649      * @return bool
  650      */
  651     private function installPackage($package, $is_update = false)
  652     {
  653         $type = $package->package_type;
  654 
  655         Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]);
  656         $error_code = Installer::lastErrorCode();
  657         Folder::delete($this->tmp);
  658 
  659         if ($error_code) {
  660             $this->output->write("\x0D");
  661             // extra white spaces to clear out the buffer properly
  662             $this->output->writeln('  |- Installing package...    <red>error</red>                             ');
  663             $this->output->writeln("  |  '- " . Installer::lastErrorMsg());
  664 
  665             return false;
  666         }
  667 
  668         $message = Installer::getMessage();
  669         if ($message) {
  670             $this->output->write("\x0D");
  671             // extra white spaces to clear out the buffer properly
  672             $this->output->writeln("  |- {$message}");
  673         }
  674 
  675         $this->output->write("\x0D");
  676         // extra white spaces to clear out the buffer properly
  677         $this->output->writeln('  |- Installing package...    <green>ok</green>                             ');
  678 
  679         return true;
  680     }
  681 
  682     /**
  683      * @param array $progress
  684      */
  685     public function progress($progress)
  686     {
  687         $this->output->write("\x0D");
  688         $this->output->write('  |- Downloading package... ' . str_pad($progress['percent'], 5, ' ',
  689                 STR_PAD_LEFT) . '%');
  690     }
  691 }