12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160 |
- <?php
- /**
- * @package Grav\Common\GPM
- *
- * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\GPM;
- use Grav\Common\Grav;
- use Grav\Common\Filesystem\Folder;
- use Grav\Common\Inflector;
- use Grav\Common\Iterator;
- use Grav\Common\Utils;
- use RocketTheme\Toolbox\File\YamlFile;
- class GPM extends Iterator
- {
- /**
- * Local installed Packages
- * @var Local\Packages
- */
- private $installed;
- /**
- * Remote available Packages
- * @var Remote\Packages
- */
- private $repository;
- /**
- * @var Remote\GravCore
- */
- public $grav;
- /**
- * Internal cache
- * @var array
- */
- protected $cache;
- protected $install_paths = [
- 'plugins' => 'user/plugins/%name%',
- 'themes' => 'user/themes/%name%',
- 'skeletons' => 'user/'
- ];
- /**
- * Creates a new GPM instance with Local and Remote packages available
- * @param bool $refresh Applies to Remote Packages only and forces a refetch of data
- * @param callable $callback Either a function or callback in array notation
- */
- public function __construct($refresh = false, $callback = null)
- {
- parent::__construct();
- $this->cache = [];
- $this->installed = new Local\Packages();
- try {
- $this->repository = new Remote\Packages($refresh, $callback);
- $this->grav = new Remote\GravCore($refresh, $callback);
- } catch (\Exception $e) {
- }
- }
- /**
- * Return the locally installed packages
- *
- * @return Local\Packages
- */
- public function getInstalled()
- {
- return $this->installed;
- }
- /**
- * Returns the Locally installable packages
- *
- * @param array $list_type_installed
- * @return array The installed packages
- */
- public function getInstallable($list_type_installed = ['plugins' => true, 'themes' => true])
- {
- $items = ['total' => 0];
- foreach ($list_type_installed as $type => $type_installed) {
- if ($type_installed === false) {
- continue;
- }
- $methodInstallableType = 'getInstalled' . ucfirst($type);
- $to_install = $this->$methodInstallableType();
- $items[$type] = $to_install;
- $items['total'] += count($to_install);
- }
- return $items;
- }
- /**
- * Returns the amount of locally installed packages
- * @return int Amount of installed packages
- */
- public function countInstalled()
- {
- $installed = $this->getInstalled();
- return count($installed['plugins']) + count($installed['themes']);
- }
- /**
- * Return the instance of a specific Package
- *
- * @param string $slug The slug of the Package
- * @return Local\Package The instance of the Package
- */
- public function getInstalledPackage($slug)
- {
- if (isset($this->installed['plugins'][$slug])) {
- return $this->installed['plugins'][$slug];
- }
- if (isset($this->installed['themes'][$slug])) {
- return $this->installed['themes'][$slug];
- }
- return null;
- }
- /**
- * Return the instance of a specific Plugin
- * @param string $slug The slug of the Plugin
- * @return Local\Package The instance of the Plugin
- */
- public function getInstalledPlugin($slug)
- {
- return $this->installed['plugins'][$slug];
- }
- /**
- * Returns the Locally installed plugins
- * @return Iterator The installed plugins
- */
- public function getInstalledPlugins()
- {
- return $this->installed['plugins'];
- }
- /**
- * Checks if a Plugin is installed
- * @param string $slug The slug of the Plugin
- * @return bool True if the Plugin has been installed. False otherwise
- */
- public function isPluginInstalled($slug)
- {
- return isset($this->installed['plugins'][$slug]);
- }
- public function isPluginInstalledAsSymlink($slug)
- {
- return $this->installed['plugins'][$slug]->symlink;
- }
- /**
- * Return the instance of a specific Theme
- * @param string $slug The slug of the Theme
- * @return Local\Package The instance of the Theme
- */
- public function getInstalledTheme($slug)
- {
- return $this->installed['themes'][$slug];
- }
- /**
- * Returns the Locally installed themes
- * @return Iterator The installed themes
- */
- public function getInstalledThemes()
- {
- return $this->installed['themes'];
- }
- /**
- * Checks if a Theme is installed
- * @param string $slug The slug of the Theme
- * @return bool True if the Theme has been installed. False otherwise
- */
- public function isThemeInstalled($slug)
- {
- return isset($this->installed['themes'][$slug]);
- }
- /**
- * Returns the amount of updates available
- * @return int Amount of available updates
- */
- public function countUpdates()
- {
- $count = 0;
- $count += count($this->getUpdatablePlugins());
- $count += count($this->getUpdatableThemes());
- return $count;
- }
- /**
- * Returns an array of Plugins and Themes that can be updated.
- * Plugins and Themes are extended with the `available` property that relies to the remote version
- * @param array $list_type_update specifies what type of package to update
- * @return array Array of updatable Plugins and Themes.
- * Format: ['total' => int, 'plugins' => array, 'themes' => array]
- */
- public function getUpdatable($list_type_update = ['plugins' => true, 'themes' => true])
- {
- $items = ['total' => 0];
- foreach ($list_type_update as $type => $type_updatable) {
- if ($type_updatable === false) {
- continue;
- }
- $methodUpdatableType = 'getUpdatable' . ucfirst($type);
- $to_update = $this->$methodUpdatableType();
- $items[$type] = $to_update;
- $items['total'] += count($to_update);
- }
- return $items;
- }
- /**
- * Returns an array of Plugins that can be updated.
- * The Plugins are extended with the `available` property that relies to the remote version
- * @return array Array of updatable Plugins
- */
- public function getUpdatablePlugins()
- {
- $items = [];
- $repository = $this->repository['plugins'];
- // local cache to speed things up
- if (isset($this->cache[__METHOD__])) {
- return $this->cache[__METHOD__];
- }
- foreach ($this->installed['plugins'] as $slug => $plugin) {
- if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
- continue;
- }
- $local_version = $plugin->version ?: 'Unknown';
- $remote_version = $repository[$slug]->version;
- if (version_compare($local_version, $remote_version) < 0) {
- $repository[$slug]->available = $remote_version;
- $repository[$slug]->version = $local_version;
- $repository[$slug]->type = $repository[$slug]->release_type;
- $items[$slug] = $repository[$slug];
- }
- }
- $this->cache[__METHOD__] = $items;
- return $items;
- }
- /**
- * Get the latest release of a package from the GPM
- *
- * @param string $package_name
- *
- * @return string|null
- */
- public function getLatestVersionOfPackage($package_name)
- {
- $repository = $this->repository['plugins'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->available ?: $repository[$package_name]->version;
- }
- //Not a plugin, it's a theme?
- $repository = $this->repository['themes'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->available ?: $repository[$package_name]->version;
- }
- return null;
- }
- /**
- * Check if a Plugin or Theme is updatable
- * @param string $slug The slug of the package
- * @return bool True if updatable. False otherwise or if not found
- */
- public function isUpdatable($slug)
- {
- return $this->isPluginUpdatable($slug) || $this->isThemeUpdatable($slug);
- }
- /**
- * Checks if a Plugin is updatable
- * @param string $plugin The slug of the Plugin
- * @return bool True if the Plugin is updatable. False otherwise
- */
- public function isPluginUpdatable($plugin)
- {
- return array_key_exists($plugin, (array)$this->getUpdatablePlugins());
- }
- /**
- * Returns an array of Themes that can be updated.
- * The Themes are extended with the `available` property that relies to the remote version
- * @return array Array of updatable Themes
- */
- public function getUpdatableThemes()
- {
- $items = [];
- $repository = $this->repository['themes'];
- // local cache to speed things up
- if (isset($this->cache[__METHOD__])) {
- return $this->cache[__METHOD__];
- }
- foreach ($this->installed['themes'] as $slug => $plugin) {
- if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
- continue;
- }
- $local_version = $plugin->version ?: 'Unknown';
- $remote_version = $repository[$slug]->version;
- if (version_compare($local_version, $remote_version) < 0) {
- $repository[$slug]->available = $remote_version;
- $repository[$slug]->version = $local_version;
- $repository[$slug]->type = $repository[$slug]->release_type;
- $items[$slug] = $repository[$slug];
- }
- }
- $this->cache[__METHOD__] = $items;
- return $items;
- }
- /**
- * Checks if a Theme is Updatable
- * @param string $theme The slug of the Theme
- * @return bool True if the Theme is updatable. False otherwise
- */
- public function isThemeUpdatable($theme)
- {
- return array_key_exists($theme, (array)$this->getUpdatableThemes());
- }
- /**
- * Get the release type of a package (stable / testing)
- *
- * @param string $package_name
- *
- * @return string|null
- */
- public function getReleaseType($package_name)
- {
- $repository = $this->repository['plugins'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->release_type;
- }
- //Not a plugin, it's a theme?
- $repository = $this->repository['themes'];
- if (isset($repository[$package_name])) {
- return $repository[$package_name]->release_type;
- }
- return null;
- }
- /**
- * Returns true if the package latest release is stable
- *
- * @param string $package_name
- *
- * @return bool
- */
- public function isStableRelease($package_name)
- {
- return $this->getReleaseType($package_name) === 'stable';
- }
- /**
- * Returns true if the package latest release is testing
- *
- * @param string $package_name
- *
- * @return bool
- */
- public function isTestingRelease($package_name)
- {
- $hasTesting = isset($this->getInstalledPackage($package_name)->testing);
- $testing = $hasTesting ? $this->getInstalledPackage($package_name)->testing : false;
- return $this->getReleaseType($package_name) === 'testing' || $testing;
- }
- /**
- * Returns a Plugin from the repository
- * @param string $slug The slug of the Plugin
- * @return mixed Package if found, NULL if not
- */
- public function getRepositoryPlugin($slug)
- {
- return @$this->repository['plugins'][$slug];
- }
- /**
- * Returns the list of Plugins available in the repository
- * @return Iterator The Plugins remotely available
- */
- public function getRepositoryPlugins()
- {
- return $this->repository['plugins'];
- }
- /**
- * Returns a Theme from the repository
- * @param string $slug The slug of the Theme
- * @return mixed Package if found, NULL if not
- */
- public function getRepositoryTheme($slug)
- {
- return @$this->repository['themes'][$slug];
- }
- /**
- * Returns the list of Themes available in the repository
- * @return Iterator The Themes remotely available
- */
- public function getRepositoryThemes()
- {
- return $this->repository['themes'];
- }
- /**
- * Returns the list of Plugins and Themes available in the repository
- * @return Remote\Packages Available Plugins and Themes
- * Format: ['plugins' => array, 'themes' => array]
- */
- public function getRepository()
- {
- return $this->repository;
- }
- /**
- * Searches for a Package in the repository
- * @param string $search Can be either the slug or the name
- * @param bool $ignore_exception True if should not fire an exception (for use in Twig)
- * @return Remote\Package|bool Package if found, FALSE if not
- */
- public function findPackage($search, $ignore_exception = false)
- {
- $search = strtolower($search);
- $found = $this->getRepositoryTheme($search);
- if ($found) {
- return $found;
- }
- $found = $this->getRepositoryPlugin($search);
- if ($found) {
- return $found;
- }
- $themes = $this->getRepositoryThemes();
- $plugins = $this->getRepositoryPlugins();
- if (!$themes && !$plugins) {
- if (!is_writable(ROOT_DIR . '/cache/gpm')) {
- throw new \RuntimeException("The cache/gpm folder is not writable. Please check the folder permissions.");
- }
- if ($ignore_exception) {
- return false;
- }
- throw new \RuntimeException("GPM not reachable. Please check your internet connection or check the Grav site is reachable");
- }
- if ($themes) {
- foreach ($themes as $slug => $theme) {
- if ($search == $slug || $search == $theme->name) {
- return $theme;
- }
- }
- }
- if ($plugins) {
- foreach ($plugins as $slug => $plugin) {
- if ($search == $slug || $search == $plugin->name) {
- return $plugin;
- }
- }
- }
- return false;
- }
- /**
- * Download the zip package via the URL
- *
- * @param string $package_file
- * @param string $tmp
- * @return null|string
- */
- public static function downloadPackage($package_file, $tmp)
- {
- $package = parse_url($package_file);
- $filename = basename($package['path']);
- if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && $package['host'] !== 'getgrav.org') {
- throw new \RuntimeException("Only official GPM URLs are allowed. You can modify this behavior in the System configuration.");
- }
- $output = Response::get($package_file, []);
- if ($output) {
- Folder::create($tmp);
- file_put_contents($tmp . DS . $filename, $output);
- return $tmp . DS . $filename;
- }
- return null;
- }
- /**
- * Copy the local zip package to tmp
- *
- * @param string $package_file
- * @param string $tmp
- * @return null|string
- */
- public static function copyPackage($package_file, $tmp)
- {
- $package_file = realpath($package_file);
- if (file_exists($package_file)) {
- $filename = basename($package_file);
- Folder::create($tmp);
- copy(realpath($package_file), $tmp . DS . $filename);
- return $tmp . DS . $filename;
- }
- return null;
- }
- /**
- * Try to guess the package type from the source files
- *
- * @param string $source
- * @return bool|string
- */
- public static function getPackageType($source)
- {
- $plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m';
- $theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m';
- if (
- file_exists($source . 'system/defines.php') &&
- file_exists($source . 'system/config/system.yaml')
- ) {
- return 'grav';
- }
- // must have a blueprint
- if (!file_exists($source . 'blueprints.yaml')) {
- return false;
- }
- // either theme or plugin
- $name = basename($source);
- if (Utils::contains($name, 'theme')) {
- return 'theme';
- }
- if (Utils::contains($name, 'plugin')) {
- return 'plugin';
- }
- foreach (glob($source . '*.php') as $filename) {
- $contents = file_get_contents($filename);
- if (preg_match($theme_regex, $contents)) {
- return 'theme';
- }
- if (preg_match($plugin_regex, $contents)) {
- return 'plugin';
- }
- }
- // Assume it's a theme
- return 'theme';
- }
- /**
- * Try to guess the package name from the source files
- *
- * @param string $source
- * @return bool|string
- */
- public static function getPackageName($source)
- {
- $ignore_yaml_files = ['blueprints', 'languages'];
- foreach (glob($source . '*.yaml') as $filename) {
- $name = strtolower(basename($filename, '.yaml'));
- if (in_array($name, $ignore_yaml_files)) {
- continue;
- }
- return $name;
- }
- return false;
- }
- /**
- * Find/Parse the blueprint file
- *
- * @param string $source
- * @return array|bool
- */
- public static function getBlueprints($source)
- {
- $blueprint_file = $source . 'blueprints.yaml';
- if (!file_exists($blueprint_file)) {
- return false;
- }
- $file = YamlFile::instance($blueprint_file);
- $blueprint = (array)$file->content();
- $file->free();
- return $blueprint;
- }
- /**
- * Get the install path for a name and a particular type of package
- *
- * @param string $type
- * @param string $name
- * @return string
- */
- public static function getInstallPath($type, $name)
- {
- $locator = Grav::instance()['locator'];
- if ($type === 'theme') {
- $install_path = $locator->findResource('themes://', false) . DS . $name;
- } else {
- $install_path = $locator->findResource('plugins://', false) . DS . $name;
- }
- return $install_path;
- }
- /**
- * Searches for a list of Packages in the repository
- * @param array $searches An array of either slugs or names
- * @return array Array of found Packages
- * Format: ['total' => int, 'not_found' => array, <found-slugs>]
- */
- public function findPackages($searches = [])
- {
- $packages = ['total' => 0, 'not_found' => []];
- $inflector = new Inflector();
- foreach ($searches as $search) {
- $repository = '';
- // if this is an object, get the search data from the key
- if (is_object($search)) {
- $search = (array)$search;
- $key = key($search);
- $repository = $search[$key];
- $search = $key;
- }
- $found = $this->findPackage($search);
- if ($found) {
- // set override repository if provided
- if ($repository) {
- $found->override_repository = $repository;
- }
- if (!isset($packages[$found->package_type])) {
- $packages[$found->package_type] = [];
- }
- $packages[$found->package_type][$found->slug] = $found;
- $packages['total']++;
- } else {
- // make a best guess at the type based on the repo URL
- if (Utils::contains($repository, '-theme')) {
- $type = 'themes';
- } else {
- $type = 'plugins';
- }
- $not_found = new \stdClass();
- $not_found->name = $inflector::camelize($search);
- $not_found->slug = $search;
- $not_found->package_type = $type;
- $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]);
- $not_found->override_repository = $repository;
- $packages['not_found'][$search] = $not_found;
- }
- }
- return $packages;
- }
- /**
- * Return the list of packages that have the passed one as dependency
- *
- * @param string $slug The slug name of the package
- *
- * @return array
- */
- public function getPackagesThatDependOnPackage($slug)
- {
- $plugins = $this->getInstalledPlugins();
- $themes = $this->getInstalledThemes();
- $packages = array_merge($plugins->toArray(), $themes->toArray());
- $dependent_packages = [];
- foreach ($packages as $package_name => $package) {
- if (isset($package['dependencies'])) {
- foreach ($package['dependencies'] as $dependency) {
- if (is_array($dependency) && isset($dependency['name'])) {
- $dependency = $dependency['name'];
- }
- if ($dependency === $slug) {
- $dependent_packages[] = $package_name;
- }
- }
- }
- }
- return $dependent_packages;
- }
- /**
- * Get the required version of a dependency of a package
- *
- * @param string $package_slug
- * @param string $dependency_slug
- *
- * @return mixed
- */
- public function getVersionOfDependencyRequiredByPackage($package_slug, $dependency_slug)
- {
- $dependencies = $this->getInstalledPackage($package_slug)->dependencies;
- foreach ($dependencies as $dependency) {
- if (isset($dependency[$dependency_slug])) {
- return $dependency[$dependency_slug];
- }
- }
- return null;
- }
- /**
- * Check the package identified by $slug can be updated to the version passed as argument.
- * Thrown an exception if it cannot be updated because another package installed requires it to be at an older version.
- *
- * @param string $slug
- * @param string $version_with_operator
- * @param array $ignore_packages_list
- *
- * @return bool
- * @throws \RuntimeException
- */
- public function checkNoOtherPackageNeedsThisDependencyInALowerVersion(
- $slug,
- $version_with_operator,
- $ignore_packages_list
- ) {
- // 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
- $dependent_packages = $this->getPackagesThatDependOnPackage($slug);
- $version = $this->calculateVersionNumberFromDependencyVersion($version_with_operator);
- if (count($dependent_packages)) {
- foreach ($dependent_packages as $dependent_package) {
- $other_dependency_version_with_operator = $this->getVersionOfDependencyRequiredByPackage($dependent_package,
- $slug);
- $other_dependency_version = $this->calculateVersionNumberFromDependencyVersion($other_dependency_version_with_operator);
- // check version is compatible with the one needed by the current package
- if ($this->versionFormatIsNextSignificantRelease($other_dependency_version_with_operator)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($version,
- $other_dependency_version);
- if (!$compatible) {
- if (!in_array($dependent_package, $ignore_packages_list, true)) {
- 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>.",
- 2);
- }
- }
- }
- }
- }
- return true;
- }
- /**
- * Check the passed packages list can be updated
- *
- * @param array $packages_names_list
- *
- * @throws \Exception
- */
- public function checkPackagesCanBeInstalled($packages_names_list)
- {
- foreach ($packages_names_list as $package_name) {
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name,
- $this->getLatestVersionOfPackage($package_name), $packages_names_list);
- }
- }
- /**
- * Fetch the dependencies, check the installed packages and return an array with
- * the list of packages with associated an information on what to do: install, update or ignore.
- *
- * `ignore` means the package is already installed and can be safely left as-is.
- * `install` means the package is not installed and must be installed.
- * `update` means the package is already installed and must be updated as a dependency needs a higher version.
- *
- * @param array $packages
- *
- * @return mixed
- * @throws \Exception
- */
- public function getDependencies($packages)
- {
- $dependencies = $this->calculateMergedDependenciesOfPackages($packages);
- foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) {
- if (\in_array($dependency_slug, $packages, true)) {
- unset($dependencies[$dependency_slug]);
- continue;
- }
- // Check PHP version
- if ($dependency_slug === 'php') {
- $current_php_version = phpversion();
- if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
- $current_php_version) === 1
- ) {
- //Needs a Grav update first
- throw new \RuntimeException("<red>One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this");
- }
- unset($dependencies[$dependency_slug]);
- continue;
- }
- //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell.
- if ($dependency_slug === 'grav') {
- if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator),
- GRAV_VERSION) === 1
- ) {
- //Needs a Grav update first
- throw new \RuntimeException("<red>One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release.");
- }
- unset($dependencies[$dependency_slug]);
- continue;
- }
- if ($this->isPluginInstalled($dependency_slug)) {
- if ($this->isPluginInstalledAsSymlink($dependency_slug)) {
- unset($dependencies[$dependency_slug]);
- continue;
- }
- $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
- // get currently installed version
- $locator = Grav::instance()['locator'];
- $blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml');
- $file = YamlFile::instance($blueprints_path);
- $package_yaml = $file->content();
- $file->free();
- $currentlyInstalledVersion = $package_yaml['version'];
- // if requirement is next significant release, check is compatible with currently installed version, might not be
- if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
- if ($this->firstVersionIsLower($dependencyVersion, $currentlyInstalledVersion)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
- $currentlyInstalledVersion);
- if (!$compatible) {
- 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.',
- 2);
- }
- }
- }
- //if I already have the latest release, remove the dependency
- $latestRelease = $this->getLatestVersionOfPackage($dependency_slug);
- if ($this->firstVersionIsLower($latestRelease, $dependencyVersion)) {
- //throw an exception if a required version cannot be found in the GPM yet
- 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',
- 1);
- }
- if ($this->firstVersionIsLower($currentlyInstalledVersion, $dependencyVersion)) {
- $dependencies[$dependency_slug] = 'update';
- } else {
- if ($currentlyInstalledVersion == $latestRelease) {
- unset($dependencies[$dependency_slug]);
- } else {
- // an update is not strictly required mark as 'ignore'
- $dependencies[$dependency_slug] = 'ignore';
- }
- }
- } else {
- $dependencyVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
- // if requirement is next significant release, check is compatible with latest available version, might not be
- if ($this->versionFormatIsNextSignificantRelease($dependencyVersionWithOperator)) {
- $latestVersionOfPackage = $this->getLatestVersionOfPackage($dependency_slug);
- if ($this->firstVersionIsLower($dependencyVersion, $latestVersionOfPackage)) {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($dependencyVersion,
- $latestVersionOfPackage);
- if (!$compatible) {
- 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.',
- 2);
- }
- }
- }
- $dependencies[$dependency_slug] = 'install';
- }
- }
- $dependencies_slugs = array_keys($dependencies);
- $this->checkNoOtherPackageNeedsTheseDependenciesInALowerVersion(array_merge($packages, $dependencies_slugs));
- return $dependencies;
- }
- public function checkNoOtherPackageNeedsTheseDependenciesInALowerVersion($dependencies_slugs)
- {
- foreach ($dependencies_slugs as $dependency_slug) {
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($dependency_slug,
- $this->getLatestVersionOfPackage($dependency_slug), $dependencies_slugs);
- }
- }
- private function firstVersionIsLower($firstVersion, $secondVersion)
- {
- return version_compare($firstVersion, $secondVersion) === -1;
- }
- /**
- * Calculates and merges the dependencies of a package
- *
- * @param string $packageName The package information
- *
- * @param array $dependencies The dependencies array
- *
- * @return array
- * @throws \Exception
- */
- private function calculateMergedDependenciesOfPackage($packageName, $dependencies)
- {
- $packageData = $this->findPackage($packageName);
- //Check for dependencies
- if (isset($packageData->dependencies)) {
- foreach ($packageData->dependencies as $dependency) {
- $current_package_name = $dependency['name'];
- if (isset($dependency['version'])) {
- $current_package_version_information = $dependency['version'];
- }
- if (!isset($dependencies[$current_package_name])) {
- // Dependency added for the first time
- if (!isset($current_package_version_information)) {
- $dependencies[$current_package_name] = '*';
- } else {
- $dependencies[$current_package_name] = $current_package_version_information;
- }
- //Factor in the package dependencies too
- $dependencies = $this->calculateMergedDependenciesOfPackage($current_package_name, $dependencies);
- } else {
- // Dependency already added by another package
- //if this package requires a version higher than the currently stored one, store this requirement instead
- if (isset($current_package_version_information) && $current_package_version_information !== '*') {
- $currently_stored_version_information = $dependencies[$current_package_name];
- $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currently_stored_version_information);
- $currently_stored_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($currently_stored_version_information)) {
- $currently_stored_version_is_in_next_significant_release_format = true;
- }
- if (!$currently_stored_version_number) {
- $currently_stored_version_number = '*';
- }
- $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($current_package_version_information);
- if (!$current_package_version_number) {
- throw new \RuntimeException('Bad format for version of dependency ' . $current_package_name . ' for package ' . $packageName,
- 1);
- }
- $current_package_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($current_package_version_information)) {
- $current_package_version_is_in_next_significant_release_format = true;
- }
- //If I had stored '*', change right away with the more specific version required
- if ($currently_stored_version_number === '*') {
- $dependencies[$current_package_name] = $current_package_version_information;
- } else {
- if (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) {
- //Comparing versions equals or higher, a simple version_compare is enough
- if (version_compare($currently_stored_version_number,
- $current_package_version_number) === -1
- ) { //Current package version is higher
- $dependencies[$current_package_name] = $current_package_version_information;
- }
- } else {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number,
- $current_package_version_number);
- if (!$compatible) {
- throw new \RuntimeException('Dependency ' . $current_package_name . ' is required in two incompatible versions',
- 2);
- }
- }
- }
- }
- }
- }
- }
- return $dependencies;
- }
- /**
- * Calculates and merges the dependencies of the passed packages
- *
- * @param array $packages
- *
- * @return mixed
- * @throws \Exception
- */
- public function calculateMergedDependenciesOfPackages($packages)
- {
- $dependencies = [];
- foreach ($packages as $package) {
- $dependencies = $this->calculateMergedDependenciesOfPackage($package, $dependencies);
- }
- return $dependencies;
- }
- /**
- * Returns the actual version from a dependency version string.
- * Examples:
- * $versionInformation == '~2.0' => returns '2.0'
- * $versionInformation == '>=2.0.2' => returns '2.0.2'
- * $versionInformation == '2.0.2' => returns '2.0.2'
- * $versionInformation == '*' => returns null
- * $versionInformation == '' => returns null
- *
- * @param string $version
- *
- * @return null|string
- */
- public function calculateVersionNumberFromDependencyVersion($version)
- {
- if ($version === '*') {
- return null;
- }
- if ($version === '') {
- return null;
- }
- if ($this->versionFormatIsNextSignificantRelease($version)) {
- return trim(substr($version, 1));
- }
- if ($this->versionFormatIsEqualOrHigher($version)) {
- return trim(substr($version, 2));
- }
- return $version;
- }
- /**
- * Check if the passed version information contains next significant release (tilde) operator
- *
- * Example: returns true for $version: '~2.0'
- *
- * @param string $version
- *
- * @return bool
- */
- public function versionFormatIsNextSignificantRelease($version): bool
- {
- return strpos($version, '~') === 0;
- }
- /**
- * Check if the passed version information contains equal or higher operator
- *
- * Example: returns true for $version: '>=2.0'
- *
- * @param string $version
- *
- * @return bool
- */
- public function versionFormatIsEqualOrHigher($version): bool
- {
- return strpos($version, '>=') === 0;
- }
- /**
- * Check if two releases are compatible by next significant release
- *
- * ~1.2 is equivalent to >=1.2 <2.0.0
- * ~1.2.3 is equivalent to >=1.2.3 <1.3.0
- *
- * In short, allows the last digit specified to go up
- *
- * @param string $version1 the version string (e.g. '2.0.0' or '1.0')
- * @param string $version2 the version string (e.g. '2.0.0' or '1.0')
- *
- * @return bool
- */
- public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool
- {
- $version1array = explode('.', $version1);
- $version2array = explode('.', $version2);
- if (\count($version1array) > \count($version2array)) {
- list($version1array, $version2array) = [$version2array, $version1array];
- }
- $i = 0;
- while ($i < \count($version1array) - 1) {
- if ($version1array[$i] != $version2array[$i]) {
- return false;
- }
- $i++;
- }
- return true;
- }
- }
|