"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/GPM/GPM.php" (1 Sep 2020, 40440 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 "GPM.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Common\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\Common\GPM;
   11 
   12 use Grav\Common\Grav;
   13 use Grav\Common\Filesystem\Folder;
   14 use Grav\Common\Inflector;
   15 use Grav\Common\Iterator;
   16 use Grav\Common\Utils;
   17 use RocketTheme\Toolbox\File\YamlFile;
   18 
   19 class GPM extends Iterator
   20 {
   21     /**
   22      * Local installed Packages
   23      * @var Local\Packages
   24      */
   25     private $installed;
   26 
   27     /**
   28      * Remote available Packages
   29      * @var Remote\Packages
   30      */
   31     private $repository;
   32 
   33     /**
   34      * @var Remote\GravCore
   35      */
   36     public $grav;
   37 
   38     /**
   39      * Internal cache
   40      * @var array
   41      */
   42     protected $cache;
   43 
   44     protected $install_paths = [
   45         'plugins' => 'user/plugins/%name%',
   46         'themes' => 'user/themes/%name%',
   47         'skeletons' => 'user/'
   48     ];
   49 
   50     /**
   51      * Creates a new GPM instance with Local and Remote packages available
   52      * @param bool $refresh Applies to Remote Packages only and forces a refetch of data
   53      * @param callable $callback Either a function or callback in array notation
   54      */
   55     public function __construct($refresh = false, $callback = null)
   56     {
   57         parent::__construct();
   58         $this->cache = [];
   59         $this->installed = new Local\Packages();
   60         try {
   61             $this->repository = new Remote\Packages($refresh, $callback);
   62             $this->grav = new Remote\GravCore($refresh, $callback);
   63         } catch (\Exception $e) {
   64         }
   65     }
   66 
   67     /**
   68      * Return the locally installed packages
   69      *
   70      * @return Local\Packages
   71      */
   72     public function getInstalled()
   73     {
   74         return $this->installed;
   75     }
   76 
   77     /**
   78      * Returns the Locally installable packages
   79      *
   80      * @param array $list_type_installed
   81      * @return array The installed packages
   82      */
   83     public function getInstallable($list_type_installed = ['plugins' => true, 'themes' => true])
   84     {
   85         $items = ['total' => 0];
   86         foreach ($list_type_installed as $type => $type_installed) {
   87             if ($type_installed === false) {
   88                 continue;
   89             }
   90             $methodInstallableType = 'getInstalled' . ucfirst($type);
   91             $to_install = $this->$methodInstallableType();
   92             $items[$type] = $to_install;
   93             $items['total'] += count($to_install);
   94         }
   95         return $items;
   96     }
   97 
   98     /**
   99      * Returns the amount of locally installed packages
  100      * @return int Amount of installed packages
  101      */
  102     public function countInstalled()
  103     {
  104         $installed = $this->getInstalled();
  105 
  106         return count($installed['plugins']) + count($installed['themes']);
  107     }
  108 
  109     /**
  110      * Return the instance of a specific Package
  111      *
  112      * @param  string $slug The slug of the Package
  113      * @return Local\Package The instance of the Package
  114      */
  115     public function getInstalledPackage($slug)
  116     {
  117         if (isset($this->installed['plugins'][$slug])) {
  118             return $this->installed['plugins'][$slug];
  119         }
  120 
  121         if (isset($this->installed['themes'][$slug])) {
  122             return $this->installed['themes'][$slug];
  123         }
  124 
  125         return null;
  126     }
  127 
  128     /**
  129      * Return the instance of a specific Plugin
  130      * @param  string $slug The slug of the Plugin
  131      * @return Local\Package The instance of the Plugin
  132      */
  133     public function getInstalledPlugin($slug)
  134     {
  135         return $this->installed['plugins'][$slug];
  136     }
  137 
  138     /**
  139      * Returns the Locally installed plugins
  140      * @return Iterator The installed plugins
  141      */
  142     public function getInstalledPlugins()
  143     {
  144         return $this->installed['plugins'];
  145     }
  146 
  147     /**
  148      * Checks if a Plugin is installed
  149      * @param  string $slug The slug of the Plugin
  150      * @return bool True if the Plugin has been installed. False otherwise
  151      */
  152     public function isPluginInstalled($slug)
  153     {
  154         return isset($this->installed['plugins'][$slug]);
  155     }
  156 
  157     public function isPluginInstalledAsSymlink($slug)
  158     {
  159         return $this->installed['plugins'][$slug]->symlink;
  160     }
  161 
  162     /**
  163      * Return the instance of a specific Theme
  164      * @param  string $slug The slug of the Theme
  165      * @return Local\Package The instance of the Theme
  166      */
  167     public function getInstalledTheme($slug)
  168     {
  169         return $this->installed['themes'][$slug];
  170     }
  171 
  172     /**
  173      * Returns the Locally installed themes
  174      * @return Iterator The installed themes
  175      */
  176     public function getInstalledThemes()
  177     {
  178         return $this->installed['themes'];
  179     }
  180 
  181     /**
  182      * Checks if a Theme is installed
  183      * @param  string $slug The slug of the Theme
  184      * @return bool True if the Theme has been installed. False otherwise
  185      */
  186     public function isThemeInstalled($slug)
  187     {
  188         return isset($this->installed['themes'][$slug]);
  189     }
  190 
  191     /**
  192      * Returns the amount of updates available
  193      * @return int Amount of available updates
  194      */
  195     public function countUpdates()
  196     {
  197         $count = 0;
  198 
  199         $count += count($this->getUpdatablePlugins());
  200         $count += count($this->getUpdatableThemes());
  201 
  202         return $count;
  203     }
  204 
  205     /**
  206      * Returns an array of Plugins and Themes that can be updated.
  207      * Plugins and Themes are extended with the `available` property that relies to the remote version
  208      * @param array $list_type_update specifies what type of package to update
  209      * @return array Array of updatable Plugins and Themes.
  210      *               Format: ['total' => int, 'plugins' => array, 'themes' => array]
  211      */
  212     public function getUpdatable($list_type_update = ['plugins' => true, 'themes' => true])
  213     {
  214 
  215         $items = ['total' => 0];
  216         foreach ($list_type_update as $type => $type_updatable) {
  217             if ($type_updatable === false) {
  218                 continue;
  219             }
  220             $methodUpdatableType = 'getUpdatable' . ucfirst($type);
  221             $to_update = $this->$methodUpdatableType();
  222             $items[$type] = $to_update;
  223             $items['total'] += count($to_update);
  224         }
  225         return $items;
  226     }
  227 
  228     /**
  229      * Returns an array of Plugins that can be updated.
  230      * The Plugins are extended with the `available` property that relies to the remote version
  231      * @return array Array of updatable Plugins
  232      */
  233     public function getUpdatablePlugins()
  234     {
  235         $items = [];
  236         $repository = $this->repository['plugins'];
  237 
  238         // local cache to speed things up
  239         if (isset($this->cache[__METHOD__])) {
  240             return $this->cache[__METHOD__];
  241         }
  242 
  243         foreach ($this->installed['plugins'] as $slug => $plugin) {
  244             if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
  245                 continue;
  246             }
  247 
  248             $local_version = $plugin->version ?: 'Unknown';
  249             $remote_version = $repository[$slug]->version;
  250 
  251             if (version_compare($local_version, $remote_version) < 0) {
  252                 $repository[$slug]->available = $remote_version;
  253                 $repository[$slug]->version = $local_version;
  254                 $repository[$slug]->type = $repository[$slug]->release_type;
  255                 $items[$slug] = $repository[$slug];
  256             }
  257         }
  258 
  259         $this->cache[__METHOD__] = $items;
  260 
  261         return $items;
  262     }
  263 
  264     /**
  265      * Get the latest release of a package from the GPM
  266      *
  267      * @param string $package_name
  268      *
  269      * @return string|null
  270      */
  271     public function getLatestVersionOfPackage($package_name)
  272     {
  273         $repository = $this->repository['plugins'];
  274         if (isset($repository[$package_name])) {
  275             return $repository[$package_name]->available ?: $repository[$package_name]->version;
  276         }
  277 
  278         //Not a plugin, it's a theme?
  279         $repository = $this->repository['themes'];
  280         if (isset($repository[$package_name])) {
  281             return $repository[$package_name]->available ?: $repository[$package_name]->version;
  282         }
  283 
  284         return null;
  285     }
  286 
  287     /**
  288      * Check if a Plugin or Theme is updatable
  289      * @param  string $slug The slug of the package
  290      * @return bool True if updatable. False otherwise or if not found
  291      */
  292     public function isUpdatable($slug)
  293     {
  294         return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug);
  295     }
  296 
  297     /**
  298      * Checks if a Plugin is updatable
  299      * @param  string $plugin The slug of the Plugin
  300      * @return bool True if the Plugin is updatable. False otherwise
  301      */
  302     public function isPluginUpdatable($plugin)
  303     {
  304         return array_key_exists($plugin, (array)$this->getUpdatablePlugins());
  305     }
  306 
  307     /**
  308      * Returns an array of Themes that can be updated.
  309      * The Themes are extended with the `available` property that relies to the remote version
  310      * @return array Array of updatable Themes
  311      */
  312     public function getUpdatableThemes()
  313     {
  314         $items = [];
  315         $repository = $this->repository['themes'];
  316 
  317         // local cache to speed things up
  318         if (isset($this->cache[__METHOD__])) {
  319             return $this->cache[__METHOD__];
  320         }
  321 
  322         foreach ($this->installed['themes'] as $slug => $plugin) {
  323             if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
  324                 continue;
  325             }
  326 
  327             $local_version = $plugin->version ?: 'Unknown';
  328             $remote_version = $repository[$slug]->version;
  329 
  330             if (version_compare($local_version, $remote_version) < 0) {
  331                 $repository[$slug]->available = $remote_version;
  332                 $repository[$slug]->version = $local_version;
  333                 $repository[$slug]->type = $repository[$slug]->release_type;
  334                 $items[$slug] = $repository[$slug];
  335             }
  336         }
  337 
  338         $this->cache[__METHOD__] = $items;
  339 
  340         return $items;
  341     }
  342 
  343     /**
  344      * Checks if a Theme is Updatable
  345      * @param  string $theme The slug of the Theme
  346      * @return bool True if the Theme is updatable. False otherwise
  347      */
  348     public function isThemeUpdatable($theme)
  349     {
  350         return array_key_exists($theme, (array)$this->getUpdatableThemes());
  351     }
  352 
  353     /**
  354      * Get the release type of a package (stable / testing)
  355      *
  356      * @param string $package_name
  357      *
  358      * @return string|null
  359      */
  360     public function getReleaseType($package_name)
  361     {
  362         $repository = $this->repository['plugins'];
  363         if (isset($repository[$package_name])) {
  364             return $repository[$package_name]->release_type;
  365         }
  366 
  367         //Not a plugin, it's a theme?
  368         $repository = $this->repository['themes'];
  369         if (isset($repository[$package_name])) {
  370             return $repository[$package_name]->release_type;
  371         }
  372 
  373         return null;
  374     }
  375 
  376     /**
  377      * Returns true if the package latest release is stable
  378      *
  379      * @param string $package_name
  380      *
  381      * @return bool
  382      */
  383     public function isStableRelease($package_name)
  384     {
  385         return $this->getReleaseType($package_name) === 'stable';
  386     }
  387 
  388     /**
  389      * Returns true if the package latest release is testing
  390      *
  391      * @param string $package_name
  392      *
  393      * @return bool
  394      */
  395     public function isTestingRelease($package_name)
  396     {
  397         $hasTesting = isset($this->getInstalledPackage($package_name)->testing);
  398         $testing = $hasTesting ? $this->getInstalledPackage($package_name)->testing : false;
  399 
  400         return $this->getReleaseType($package_name) === 'testing' || $testing;
  401     }
  402 
  403     /**
  404      * Returns a Plugin from the repository
  405      * @param  string $slug The slug of the Plugin
  406      * @return mixed  Package if found, NULL if not
  407      */
  408     public function getRepositoryPlugin($slug)
  409     {
  410         return @$this->repository['plugins'][$slug];
  411     }
  412 
  413     /**
  414      * Returns the list of Plugins available in the repository
  415      * @return Iterator The Plugins remotely available
  416      */
  417     public function getRepositoryPlugins()
  418     {
  419         return $this->repository['plugins'];
  420     }
  421 
  422     /**
  423      * Returns a Theme from the repository
  424      * @param  string $slug The slug of the Theme
  425      * @return mixed  Package if found, NULL if not
  426      */
  427     public function getRepositoryTheme($slug)
  428     {
  429         return @$this->repository['themes'][$slug];
  430     }
  431 
  432     /**
  433      * Returns the list of Themes available in the repository
  434      * @return Iterator The Themes remotely available
  435      */
  436     public function getRepositoryThemes()
  437     {
  438         return $this->repository['themes'];
  439     }
  440 
  441     /**
  442      * Returns the list of Plugins and Themes available in the repository
  443      * @return Remote\Packages Available Plugins and Themes
  444      *               Format: ['plugins' => array, 'themes' => array]
  445      */
  446     public function getRepository()
  447     {
  448         return $this->repository;
  449     }
  450 
  451     /**
  452      * Searches for a Package in the repository
  453      * @param  string $search Can be either the slug or the name
  454      * @param  bool $ignore_exception True if should not fire an exception (for use in Twig)
  455      * @return Remote\Package|bool Package if found, FALSE if not
  456      */
  457     public function findPackage($search, $ignore_exception = false)
  458     {
  459         $search = strtolower($search);
  460 
  461         $found = $this->getRepositoryTheme($search);
  462         if ($found) {
  463             return $found;
  464         }
  465 
  466         $found = $this->getRepositoryPlugin($search);
  467         if ($found) {
  468             return $found;
  469         }
  470 
  471         $themes = $this->getRepositoryThemes();
  472         $plugins = $this->getRepositoryPlugins();
  473 
  474         if (!$themes && !$plugins) {
  475             if (!is_writable(ROOT_DIR . '/cache/gpm')) {
  476                 throw new \RuntimeException("The cache/gpm folder is not writable. Please check the folder permissions.");
  477             }
  478 
  479             if ($ignore_exception) {
  480                 return false;
  481             }
  482 
  483             throw new \RuntimeException("GPM not reachable. Please check your internet connection or check the Grav site is reachable");
  484         }
  485 
  486         if ($themes) {
  487             foreach ($themes as $slug => $theme) {
  488                 if ($search == $slug || $search == $theme->name) {
  489                     return $theme;
  490                 }
  491             }
  492         }
  493 
  494         if ($plugins) {
  495             foreach ($plugins as $slug => $plugin) {
  496                 if ($search == $slug || $search == $plugin->name) {
  497                     return $plugin;
  498                 }
  499             }
  500         }
  501 
  502         return false;
  503     }
  504 
  505     /**
  506      * Download the zip package via the URL
  507      *
  508      * @param string $package_file
  509      * @param string $tmp
  510      * @return null|string
  511      */
  512     public static function downloadPackage($package_file, $tmp)
  513     {
  514         $package = parse_url($package_file);
  515         $filename = basename($package['path']);
  516 
  517         if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && $package['host'] !== 'getgrav.org') {
  518             throw new \RuntimeException("Only official GPM URLs are allowed. You can modify this behavior in the System configuration.");
  519         }
  520 
  521         $output = Response::get($package_file, []);
  522 
  523         if ($output) {
  524             Folder::create($tmp);
  525             file_put_contents($tmp . DS . $filename, $output);
  526             return $tmp . DS . $filename;
  527         }
  528 
  529         return null;
  530     }
  531 
  532     /**
  533      * Copy the local zip package to tmp
  534      *
  535      * @param string $package_file
  536      * @param string $tmp
  537      * @return null|string
  538      */
  539     public static function copyPackage($package_file, $tmp)
  540     {
  541         $package_file = realpath($package_file);
  542 
  543         if (file_exists($package_file)) {
  544             $filename = basename($package_file);
  545             Folder::create($tmp);
  546             copy(realpath($package_file), $tmp . DS . $filename);
  547             return $tmp . DS . $filename;
  548         }
  549 
  550         return null;
  551     }
  552 
  553     /**
  554      * Try to guess the package type from the source files
  555      *
  556      * @param string $source
  557      * @return bool|string
  558      */
  559     public static function getPackageType($source)
  560     {
  561         $plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m';
  562         $theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m';
  563 
  564         if (
  565             file_exists($source . 'system/defines.php') &&
  566             file_exists($source . 'system/config/system.yaml')
  567         ) {
  568             return 'grav';
  569         }
  570 
  571         // must have a blueprint
  572         if (!file_exists($source . 'blueprints.yaml')) {
  573             return false;
  574         }
  575 
  576         // either theme or plugin
  577         $name = basename($source);
  578         if (Utils::contains($name, 'theme')) {
  579             return 'theme';
  580         }
  581         if (Utils::contains($name, 'plugin')) {
  582             return 'plugin';
  583         }
  584         foreach (glob($source . '*.php') as $filename) {
  585             $contents = file_get_contents($filename);
  586             if (preg_match($theme_regex, $contents)) {
  587                 return 'theme';
  588             }
  589             if (preg_match($plugin_regex, $contents)) {
  590                 return 'plugin';
  591             }
  592         }
  593 
  594         // Assume it's a theme
  595         return 'theme';
  596     }
  597 
  598     /**
  599      * Try to guess the package name from the source files
  600      *
  601      * @param string $source
  602      * @return bool|string
  603      */
  604     public static function getPackageName($source)
  605     {
  606         $ignore_yaml_files = ['blueprints', 'languages'];
  607 
  608         foreach (glob($source . '*.yaml') as $filename) {
  609             $name = strtolower(basename($filename, '.yaml'));
  610             if (in_array($name, $ignore_yaml_files)) {
  611                 continue;
  612             }
  613             return $name;
  614         }
  615         return false;
  616     }
  617 
  618     /**
  619      * Find/Parse the blueprint file
  620      *
  621      * @param string $source
  622      * @return array|bool
  623      */
  624     public static function getBlueprints($source)
  625     {
  626         $blueprint_file = $source . 'blueprints.yaml';
  627         if (!file_exists($blueprint_file)) {
  628             return false;
  629         }
  630 
  631         $file = YamlFile::instance($blueprint_file);
  632         $blueprint = (array)$file->content();
  633         $file->free();
  634 
  635         return $blueprint;
  636     }
  637 
  638     /**
  639      * Get the install path for a name and a particular type of package
  640      *
  641      * @param string $type
  642      * @param string $name
  643      * @return string
  644      */
  645     public static function getInstallPath($type, $name)
  646     {
  647         $locator = Grav::instance()['locator'];
  648 
  649         if ($type === 'theme') {
  650             $install_path = $locator->findResource('themes://', false) . DS . $name;
  651         } else {
  652             $install_path = $locator->findResource('plugins://', false) . DS . $name;
  653         }
  654         return $install_path;
  655     }
  656 
  657     /**
  658      * Searches for a list of Packages in the repository
  659      * @param  array $searches An array of either slugs or names
  660      * @return array Array of found Packages
  661      *                        Format: ['total' => int, 'not_found' => array, <found-slugs>]
  662      */
  663     public function findPackages($searches = [])
  664     {
  665         $packages = ['total' => 0, 'not_found' => []];
  666         $inflector = new Inflector();
  667 
  668         foreach ($searches as $search) {
  669             $repository = '';
  670             // if this is an object, get the search data from the key
  671             if (is_object($search)) {
  672                 $search = (array)$search;
  673                 $key = key($search);
  674                 $repository = $search[$key];
  675                 $search = $key;
  676             }
  677 
  678             $found = $this->findPackage($search);
  679             if ($found) {
  680                 // set override repository if provided
  681                 if ($repository) {
  682                     $found->override_repository = $repository;
  683                 }
  684                 if (!isset($packages[$found->package_type])) {
  685                     $packages[$found->package_type] = [];
  686                 }
  687 
  688                 $packages[$found->package_type][$found->slug] = $found;
  689                 $packages['total']++;
  690             } else {
  691                 // make a best guess at the type based on the repo URL
  692                 if (Utils::contains($repository, '-theme')) {
  693                     $type = 'themes';
  694                 } else {
  695                     $type = 'plugins';
  696                 }
  697 
  698                 $not_found = new \stdClass();
  699                 $not_found->name = $inflector::camelize($search);
  700                 $not_found->slug = $search;
  701                 $not_found->package_type = $type;
  702                 $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]);
  703                 $not_found->override_repository = $repository;
  704                 $packages['not_found'][$search] = $not_found;
  705             }
  706         }
  707 
  708         return $packages;
  709     }
  710 
  711     /**
  712      * Return the list of packages that have the passed one as dependency
  713      *
  714      * @param string $slug The slug name of the package
  715      *
  716      * @return array
  717      */
  718     public function getPackagesThatDependOnPackage($slug)
  719     {
  720         $plugins = $this->getInstalledPlugins();
  721         $themes = $this->getInstalledThemes();
  722         $packages = array_merge($plugins->toArray(), $themes->toArray());
  723 
  724         $dependent_packages = [];
  725 
  726         foreach ($packages as $package_name => $package) {
  727             if (isset($package['dependencies'])) {
  728                 foreach ($package['dependencies'] as $dependency) {
  729                     if (is_array($dependency) && isset($dependency['name'])) {
  730                         $dependency = $dependency['name'];
  731                     }
  732 
  733                     if ($dependency === $slug) {
  734                         $dependent_packages[] = $package_name;
  735                     }
  736                 }
  737             }
  738         }
  739 
  740         return $dependent_packages;
  741     }
  742 
  743 
  744     /**
  745      * Get the required version of a dependency of a package
  746      *
  747      * @param string $package_slug
  748      * @param string $dependency_slug
  749      *
  750      * @return mixed
  751      */
  752     public function getVersionOfDependencyRequiredByPackage($package_slug, $dependency_slug)
  753     {
  754         $dependencies = $this->getInstalledPackage($package_slug)->dependencies;
  755         foreach ($dependencies as $dependency) {
  756             if (isset($dependency[$dependency_slug])) {
  757                 return $dependency[$dependency_slug];
  758             }
  759         }
  760 
  761         return null;
  762     }
  763 
  764     /**
  765      * Check the package identified by $slug can be updated to the version passed as argument.
  766      * Thrown an exception if it cannot be updated because another package installed requires it to be at an older version.
  767      *
  768      * @param string $slug
  769      * @param string $version_with_operator
  770      * @param array $ignore_packages_list
  771      *
  772      * @return bool
  773      * @throws \RuntimeException
  774      */
  775     public function checkNoOtherPackageNeedsThisDependencyInALowerVersion(
  776         $slug,
  777         $version_with_operator,
  778         $ignore_packages_list
  779     ) {
  780 
  781         // check if any of the currently installed package need this in a lower version than the one we need. In case, abort and tell which package
  782         $dependent_packages = $this->getPackagesThatDependOnPackage($slug);
  783         $version = $this->calculateVersionNumberFromDependencyVersion($version_with_operator);
  784 
  785         if (count($dependent_packages)) {
  786             foreach ($dependent_packages as $dependent_package) {
  787                 $other_dependency_version_with_operator = $this->getVersionOfDependencyRequiredByPackage($dependent_package,
  788                     $slug);
  789                 $other_dependency_version = $this->calculateVersionNumberFromDependencyVersion($other_dependency_version_with_operator);
  790 
  791                 // check version is compatible with the one needed by the current package
  792                 if ($this->versionFormatIsNextSignificantRelease($other_dependency_version_with_operator)) {
  793                     $compatible = $this->checkNextSignificantReleasesAreCompatible($version,
  794                         $other_dependency_version);
  795                     if (!$compatible) {
  796                         if (!in_array($dependent_package, $ignore_packages_list, true)) {
  797                             throw new \RuntimeException("Package <cyan>$slug</cyan> is required in an older version by package <cyan>$dependent_package</cyan>. This package needs a newer version, and because of this it cannot be installed. The <cyan>$dependent_package</cyan> package must be updated to use a newer release of <cyan>$slug</cyan>.",
  798                                 2);
  799                         }
  800                     }
  801                 }
  802             }
  803         }
  804 
  805         return true;
  806     }
  807 
  808     /**
  809      * Check the passed packages list can be updated
  810      *
  811      * @param array $packages_names_list
  812      *
  813      * @throws \Exception
  814      */
  815     public function checkPackagesCanBeInstalled($packages_names_list)
  816     {
  817         foreach ($packages_names_list as $package_name) {
  818             $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name,
  819                 $this->getLatestVersionOfPackage($package_name), $packages_names_list);
  820         }
  821     }
  822 
  823     /**
  824      * Fetch the dependencies, check the installed packages and return an array with
  825      * the list of packages with associated an information on what to do: install, update or ignore.
  826      *
  827      * `ignore` means the package is already installed and can be safely left as-is.
  828      * `install` means the package is not installed and must be installed.
  829      * `update` means the package is already installed and must be updated as a dependency needs a higher version.
  830      *
  831      * @param array $packages
  832      *
  833      * @return mixed
  834      * @throws \Exception
  835      */
  836     public function getDependencies($packages)
  837     {
  838         $dependencies = $this->calculateMergedDependenciesOfPackages($packages);
  839         foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) {
  840             if (\in_array($dependency_slug, $packages, true)) {
  841                 unset($dependencies[$dependency_slug]);
  842                 continue;
  843             }
  844 
  845             // Check PHP version
  846             if ($dependency_slug === 'php') {
  847                 $current_php_version = phpversion();
  848                 if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
  849                         $current_php_version) === 1
  850                 ) {
  851                     //Needs a Grav update first
  852                     throw new \RuntimeException("<red>One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this");
  853                 }
  854 
  855                 unset($dependencies[$dependency_slug]);
  856                 continue;
  857             }
  858 
  859             //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell.
  860             if ($dependency_slug === 'grav') {
  861                 if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
  862                         GRAV_VERSION) === 1
  863                 ) {
  864                     //Needs a Grav update first
  865                     throw new \RuntimeException("<red>One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release.");
  866                 }
  867 
  868                 unset($dependencies[$dependency_slug]);
  869                 continue;
  870             }
  871 
  872             if ($this->isPluginInstalled($dependency_slug)) {
  873                 if ($this->isPluginInstalledAsSymlink($dependency_slug)) {
  874                     unset($dependencies[$dependency_slug]);
  875                     continue;
  876                 }
  877 
  878                 $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
  879 
  880                 // get currently installed version
  881                 $locator = Grav::instance()['locator'];
  882                 $blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml');
  883                 $file = YamlFile::instance($blueprints_path);
  884                 $package_yaml = $file->content();
  885                 $file->free();
  886                 $currentlyInstalledVersion = $package_yaml['version'];
  887 
  888                 // if requirement is next significant release, check is compatible with currently installed version, might not be
  889                 if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
  890                     if ($this->firstVersionIsLower($dependencyVersion, $currentlyInstalledVersion)) {
  891                         $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
  892                             $currentlyInstalledVersion);
  893 
  894                         if (!$compatible) {
  895                             throw new \RuntimeException('Dependency <cyan>' . $dependency_slug . '</cyan> is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.',
  896                                 2);
  897                         }
  898                     }
  899                 }
  900 
  901                 //if I already have the latest release, remove the dependency
  902                 $latestRelease = $this->getLatestVersionOfPackage($dependency_slug);
  903 
  904                 if ($this->firstVersionIsLower($latestRelease, $dependencyVersion)) {
  905                     //throw an exception if a required version cannot be found in the GPM yet
  906                     throw new \RuntimeException('Dependency <cyan>' . $package_yaml['name'] . '</cyan> is required in version <cyan>' . $dependencyVersion . '</cyan> which is higher than the latest release, <cyan>' . $latestRelease . '</cyan>. Try running `bin/gpm -f index` to force a refresh of the GPM cache',
  907                         1);
  908                 }
  909 
  910                 if ($this->firstVersionIsLower($currentlyInstalledVersion, $dependencyVersion)) {
  911                     $dependencies[$dependency_slug] = 'update';
  912                 } else {
  913                     if ($currentlyInstalledVersion == $latestRelease) {
  914                         unset($dependencies[$dependency_slug]);
  915                     } else {
  916                         // an update is not strictly required mark as 'ignore'
  917                         $dependencies[$dependency_slug] = 'ignore';
  918                     }
  919                 }
  920             } else {
  921                 $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
  922 
  923                 // if requirement is next significant release, check is compatible with latest available version, might not be
  924                 if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
  925                     $latestVersionOfPackage = $this->getLatestVersionOfPackage($dependency_slug);
  926                     if ($this->firstVersionIsLower($dependencyVersion, $latestVersionOfPackage)) {
  927                         $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
  928                             $latestVersionOfPackage);
  929 
  930                         if (!$compatible) {
  931                             throw new \Exception('Dependency <cyan>' . $dependency_slug . '</cyan> is required in an older version than the latest release available, and it cannot be installed. This package must be updated. Please get in touch with its developer.',
  932                                 2);
  933                         }
  934                     }
  935                 }
  936 
  937                 $dependencies[$dependency_slug] = 'install';
  938             }
  939         }
  940 
  941         $dependencies_slugs = array_keys($dependencies);
  942         $this->checkNoOtherPackageNeedsTheseDependenciesInALowerVersion(array_merge($packages, $dependencies_slugs));
  943 
  944         return $dependencies;
  945     }
  946 
  947     public function checkNoOtherPackageNeedsTheseDependenciesInALowerVersion($dependencies_slugs)
  948     {
  949         foreach ($dependencies_slugs as $dependency_slug) {
  950             $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($dependency_slug,
  951                 $this->getLatestVersionOfPackage($dependency_slug), $dependencies_slugs);
  952         }
  953     }
  954 
  955     private function firstVersionIsLower($firstVersion, $secondVersion)
  956     {
  957         return version_compare($firstVersion, $secondVersion) === -1;
  958     }
  959 
  960     /**
  961      * Calculates and merges the dependencies of a package
  962      *
  963      * @param string $packageName The package information
  964      *
  965      * @param array $dependencies The dependencies array
  966      *
  967      * @return array
  968      * @throws \Exception
  969      */
  970     private function calculateMergedDependenciesOfPackage($packageName, $dependencies)
  971     {
  972         $packageData = $this->findPackage($packageName);
  973 
  974         //Check for dependencies
  975         if (isset($packageData->dependencies)) {
  976             foreach ($packageData->dependencies as $dependency) {
  977                 $current_package_name = $dependency['name'];
  978                 if (isset($dependency['version'])) {
  979                     $current_package_version_information = $dependency['version'];
  980                 }
  981 
  982                 if (!isset($dependencies[$current_package_name])) {
  983                     // Dependency added for the first time
  984 
  985                     if (!isset($current_package_version_information)) {
  986                         $dependencies[$current_package_name] = '*';
  987                     } else {
  988                         $dependencies[$current_package_name] = $current_package_version_information;
  989                     }
  990 
  991                     //Factor in the package dependencies too
  992                     $dependencies = $this->calculateMergedDependenciesOfPackage($current_package_name, $dependencies);
  993                 } else {
  994                     // Dependency already added by another package
  995                     //if this package requires a version higher than the currently stored one, store this requirement instead
  996                     if (isset($current_package_version_information) && $current_package_version_information !== '*') {
  997 
  998                         $currently_stored_version_information = $dependencies[$current_package_name];
  999                         $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currently_stored_version_information);
 1000 
 1001                         $currently_stored_version_is_in_next_significant_release_format = false;
 1002                         if ($this->versionFormatIsNextSignificantRelease($currently_stored_version_information)) {
 1003                             $currently_stored_version_is_in_next_significant_release_format = true;
 1004                         }
 1005 
 1006                         if (!$currently_stored_version_number) {
 1007                             $currently_stored_version_number = '*';
 1008                         }
 1009 
 1010                         $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($current_package_version_information);
 1011                         if (!$current_package_version_number) {
 1012                             throw new \RuntimeException('Bad format for version of dependency ' . $current_package_name . ' for package ' . $packageName,
 1013                                 1);
 1014                         }
 1015 
 1016                         $current_package_version_is_in_next_significant_release_format = false;
 1017                         if ($this->versionFormatIsNextSignificantRelease($current_package_version_information)) {
 1018                             $current_package_version_is_in_next_significant_release_format = true;
 1019                         }
 1020 
 1021                         //If I had stored '*', change right away with the more specific version required
 1022                         if ($currently_stored_version_number === '*') {
 1023                             $dependencies[$current_package_name] = $current_package_version_information;
 1024                         } else {
 1025                             if (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) {
 1026                                 //Comparing versions equals or higher, a simple version_compare is enough
 1027                                 if (version_compare($currently_stored_version_number,
 1028                                         $current_package_version_number) === -1
 1029                                 ) { //Current package version is higher
 1030                                     $dependencies[$current_package_name] = $current_package_version_information;
 1031                                 }
 1032                             } else {
 1033                                 $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number,
 1034                                     $current_package_version_number);
 1035                                 if (!$compatible) {
 1036                                     throw new \RuntimeException('Dependency ' . $current_package_name . ' is required in two incompatible versions',
 1037                                         2);
 1038                                 }
 1039                             }
 1040                         }
 1041                     }
 1042                 }
 1043             }
 1044         }
 1045 
 1046         return $dependencies;
 1047     }
 1048 
 1049     /**
 1050      * Calculates and merges the dependencies of the passed packages
 1051      *
 1052      * @param array $packages
 1053      *
 1054      * @return mixed
 1055      * @throws \Exception
 1056      */
 1057     public function calculateMergedDependenciesOfPackages($packages)
 1058     {
 1059         $dependencies = [];
 1060 
 1061         foreach ($packages as $package) {
 1062             $dependencies = $this->calculateMergedDependenciesOfPackage($package, $dependencies);
 1063         }
 1064 
 1065         return $dependencies;
 1066     }
 1067 
 1068     /**
 1069      * Returns the actual version from a dependency version string.
 1070      * Examples:
 1071      *      $versionInformation == '~2.0' => returns '2.0'
 1072      *      $versionInformation == '>=2.0.2' => returns '2.0.2'
 1073      *      $versionInformation == '2.0.2' => returns '2.0.2'
 1074      *      $versionInformation == '*' => returns null
 1075      *      $versionInformation == '' => returns null
 1076      *
 1077      * @param string $version
 1078      *
 1079      * @return null|string
 1080      */
 1081     public function calculateVersionNumberFromDependencyVersion($version)
 1082     {
 1083         if ($version === '*') {
 1084             return null;
 1085         }
 1086         if ($version === '') {
 1087             return null;
 1088         }
 1089         if ($this->versionFormatIsNextSignificantRelease($version)) {
 1090             return trim(substr($version, 1));
 1091         }
 1092         if ($this->versionFormatIsEqualOrHigher($version)) {
 1093             return trim(substr($version, 2));
 1094         }
 1095 
 1096         return $version;
 1097     }
 1098 
 1099     /**
 1100      * Check if the passed version information contains next significant release (tilde) operator
 1101      *
 1102      * Example: returns true for $version: '~2.0'
 1103      *
 1104      * @param string $version
 1105      *
 1106      * @return bool
 1107      */
 1108     public function versionFormatIsNextSignificantRelease($version): bool
 1109     {
 1110         return strpos($version, '~') === 0;
 1111     }
 1112 
 1113     /**
 1114      * Check if the passed version information contains equal or higher operator
 1115      *
 1116      * Example: returns true for $version: '>=2.0'
 1117      *
 1118      * @param string $version
 1119      *
 1120      * @return bool
 1121      */
 1122     public function versionFormatIsEqualOrHigher($version): bool
 1123     {
 1124         return strpos($version, '>=') === 0;
 1125     }
 1126 
 1127     /**
 1128      * Check if two releases are compatible by next significant release
 1129      *
 1130      * ~1.2 is equivalent to >=1.2 <2.0.0
 1131      * ~1.2.3 is equivalent to >=1.2.3 <1.3.0
 1132      *
 1133      * In short, allows the last digit specified to go up
 1134      *
 1135      * @param string $version1 the version string (e.g. '2.0.0' or '1.0')
 1136      * @param string $version2 the version string (e.g. '2.0.0' or '1.0')
 1137      *
 1138      * @return bool
 1139      */
 1140     public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool
 1141     {
 1142         $version1array = explode('.', $version1);
 1143         $version2array = explode('.', $version2);
 1144 
 1145         if (\count($version1array) > \count($version2array)) {
 1146             list($version1array, $version2array) = [$version2array, $version1array];
 1147         }
 1148 
 1149         $i = 0;
 1150         while ($i < \count($version1array) - 1) {
 1151             if ($version1array[$i] != $version2array[$i]) {
 1152                 return false;
 1153             }
 1154             $i++;
 1155         }
 1156 
 1157         return true;
 1158     }
 1159 
 1160 }