1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270 |
- <?php
- /**
- * @package Grav\Common\GPM
- *
- * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\GPM;
- use Exception;
- use Grav\Common\Grav;
- use Grav\Common\Filesystem\Folder;
- use Grav\Common\HTTP\Response;
- use Grav\Common\Inflector;
- use Grav\Common\Iterator;
- use Grav\Common\Utils;
- use RocketTheme\Toolbox\File\YamlFile;
- use RuntimeException;
- use stdClass;
- use function array_key_exists;
- use function count;
- use function in_array;
- use function is_array;
- use function is_object;
- /**
- * Class GPM
- * @package Grav\Common\GPM
- */
- class GPM extends Iterator
- {
- /** @var Local\Packages Local installed Packages */
- private $installed;
- /** @var Remote\Packages|null Remote available Packages */
- private $repository;
- /** @var Remote\GravCore|null Remove Grav Packages */
- private $grav;
- /** @var bool */
- private $refresh;
- /** @var callable|null */
- private $callback;
- /** @var array Internal cache */
- protected $cache;
- /** @var array */
- 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|null $callback Either a function or callback in array notation
- */
- public function __construct($refresh = false, $callback = null)
- {
- parent::__construct();
- Folder::create(CACHE_DIR . '/gpm');
- $this->cache = [];
- $this->installed = new Local\Packages();
- $this->refresh = $refresh;
- $this->callback = $callback;
- }
- /**
- * Magic getter method
- *
- * @param string $offset Asset name value
- * @return mixed Asset value
- */
- #[\ReturnTypeWillChange]
- public function __get($offset)
- {
- switch ($offset) {
- case 'grav':
- return $this->getGrav();
- }
- return parent::__get($offset);
- }
- /**
- * Magic method to determine if the attribute is set
- *
- * @param string $offset Asset name value
- * @return bool True if the value is set
- */
- #[\ReturnTypeWillChange]
- public function __isset($offset)
- {
- switch ($offset) {
- case 'grav':
- return $this->getGrav() !== null;
- }
- return parent::__isset($offset);
- }
- /**
- * 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|null The instance of the Package
- */
- public function getInstalledPackage($slug)
- {
- return $this->getInstalledPlugin($slug) ?? $this->getInstalledTheme($slug);
- }
- /**
- * Return the instance of a specific Plugin
- *
- * @param string $slug The slug of the Plugin
- * @return Local\Package|null The instance of the Plugin
- */
- public function getInstalledPlugin($slug)
- {
- return $this->installed['plugins'][$slug] ?? null;
- }
- /**
- * Returns the Locally installed plugins
- * @return Iterator The installed plugins
- */
- public function getInstalledPlugins()
- {
- return $this->installed['plugins'];
- }
- /**
- * Returns the plugin's enabled state
- *
- * @param string $slug
- * @return bool True if the Plugin is Enabled. False if manually set to enable:false. Null otherwise.
- */
- public function isPluginEnabled($slug): bool
- {
- $grav = Grav::instance();
- return ($grav['config']['plugins'][$slug]['enabled'] ?? false) === true;
- }
- /**
- * 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): bool
- {
- return isset($this->installed['plugins'][$slug]);
- }
- /**
- * @param string $slug
- * @return bool
- */
- public function isPluginInstalledAsSymlink($slug)
- {
- $plugin = $this->getInstalledPlugin($slug);
- return (bool)($plugin->symlink ?? false);
- }
- /**
- * Return the instance of a specific Theme
- *
- * @param string $slug The slug of the Theme
- * @return Local\Package|null The instance of the Theme
- */
- public function getInstalledTheme($slug)
- {
- return $this->installed['themes'][$slug] ?? null;
- }
- /**
- * Returns the Locally installed themes
- *
- * @return Iterator The installed themes
- */
- public function getInstalledThemes()
- {
- return $this->installed['themes'];
- }
- /**
- * Checks if a Theme is enabled
- *
- * @param string $slug The slug of the Theme
- * @return bool True if the Theme has been set to the default theme. False if installed, but not enabled. Null otherwise.
- */
- public function isThemeEnabled($slug): bool
- {
- $grav = Grav::instance();
- $current_theme = $grav['config']['system']['pages']['theme'] ?? null;
- return $current_theme === $slug;
- }
- /**
- * 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): bool
- {
- return isset($this->installed['themes'][$slug]);
- }
- /**
- * Returns the amount of updates available
- *
- * @return int Amount of available updates
- */
- public function countUpdates()
- {
- return count($this->getUpdatablePlugins()) + count($this->getUpdatableThemes());
- }
- /**
- * 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->getRepository();
- if (null === $repository) {
- return $items;
- }
- $plugins = $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($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
- continue;
- }
- $local_version = $plugin->version ?? 'Unknown';
- $remote_version = $plugins[$slug]->version;
- if (version_compare($local_version, $remote_version) < 0) {
- $plugins[$slug]->available = $remote_version;
- $plugins[$slug]->version = $local_version;
- $plugins[$slug]->type = $plugins[$slug]->release_type;
- $items[$slug] = $plugins[$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->getRepository();
- if (null === $repository) {
- return null;
- }
- $plugins = $repository['plugins'];
- if (isset($plugins[$package_name])) {
- return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
- }
- //Not a plugin, it's a theme?
- $themes = $repository['themes'];
- if (isset($themes[$package_name])) {
- return $themes[$package_name]->available ?: $themes[$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->getRepository();
- if (null === $repository) {
- return $items;
- }
- $themes = $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($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
- continue;
- }
- $local_version = $plugin->version ?? 'Unknown';
- $remote_version = $themes[$slug]->version;
- if (version_compare($local_version, $remote_version) < 0) {
- $themes[$slug]->available = $remote_version;
- $themes[$slug]->version = $local_version;
- $themes[$slug]->type = $themes[$slug]->release_type;
- $items[$slug] = $themes[$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->getRepository();
- if (null === $repository) {
- return null;
- }
- $plugins = $repository['plugins'];
- if (isset($plugins[$package_name])) {
- return $plugins[$package_name]->release_type;
- }
- //Not a plugin, it's a theme?
- $themes = $repository['themes'];
- if (isset($themes[$package_name])) {
- return $themes[$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)
- {
- $package = $this->getInstalledPackage($package_name);
- $testing = $package->testing ?? false;
- return $this->getReleaseType($package_name) === 'testing' || $testing;
- }
- /**
- * Returns a Plugin from the repository
- *
- * @param string $slug The slug of the Plugin
- * @return Remote\Package|null Package if found, NULL if not
- */
- public function getRepositoryPlugin($slug)
- {
- $packages = $this->getRepositoryPlugins();
- return $packages ? ($packages[$slug] ?? null) : null;
- }
- /**
- * Returns the list of Plugins available in the repository
- *
- * @return Iterator|null The Plugins remotely available
- */
- public function getRepositoryPlugins()
- {
- return $this->getRepository()['plugins'] ?? null;
- }
- /**
- * Returns a Theme from the repository
- *
- * @param string $slug The slug of the Theme
- * @return Remote\Package|null Package if found, NULL if not
- */
- public function getRepositoryTheme($slug)
- {
- $packages = $this->getRepositoryThemes();
- return $packages ? ($packages[$slug] ?? null) : null;
- }
- /**
- * Returns the list of Themes available in the repository
- *
- * @return Iterator|null The Themes remotely available
- */
- public function getRepositoryThemes()
- {
- return $this->getRepository()['themes'] ?? null;
- }
- /**
- * Returns the list of Plugins and Themes available in the repository
- *
- * @return Remote\Packages|null Available Plugins and Themes
- * Format: ['plugins' => array, 'themes' => array]
- */
- public function getRepository()
- {
- if (null === $this->repository) {
- try {
- $this->repository = new Remote\Packages($this->refresh, $this->callback);
- } catch (Exception $e) {}
- }
- return $this->repository;
- }
- /**
- * Returns Grav version available in the repository
- *
- * @return Remote\GravCore|null
- */
- public function getGrav()
- {
- if (null === $this->grav) {
- try {
- $this->grav = new Remote\GravCore($this->refresh, $this->callback);
- } catch (Exception $e) {}
- }
- return $this->grav;
- }
- /**
- * 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|false Package if found, FALSE if not
- */
- public function findPackage($search, $ignore_exception = false)
- {
- $search = strtolower($search);
- $found = $this->getRepositoryPlugin($search) ?? $this->getRepositoryTheme($search);
- if ($found) {
- return $found;
- }
- $themes = $this->getRepositoryThemes();
- $plugins = $this->getRepositoryPlugins();
- if (null === $themes || null === $plugins) {
- if (!is_writable(GRAV_ROOT . '/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');
- }
- foreach ($themes as $slug => $theme) {
- if ($search === $slug || $search === $theme->name) {
- return $theme;
- }
- }
- 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 string|null
- */
- public static function downloadPackage($package_file, $tmp)
- {
- $package = parse_url($package_file);
- if (!is_array($package)) {
- throw new \RuntimeException("Malformed GPM URL: {$package_file}");
- }
- $filename = Utils::basename($package['path'] ?? '');
- if (Grav::instance()['config']->get('system.gpm.official_gpm_only') && ($package['host'] ?? null) !== '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 string|null
- */
- public static function copyPackage($package_file, $tmp)
- {
- $package_file = realpath($package_file);
- if ($package_file && file_exists($package_file)) {
- $filename = Utils::basename($package_file);
- Folder::create($tmp);
- copy($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 string|false
- */
- 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 = Utils::basename($source);
- if (Utils::contains($name, 'theme')) {
- return 'theme';
- }
- if (Utils::contains($name, 'plugin')) {
- return 'plugin';
- }
- $glob = glob($source . '*.php') ?: [];
- foreach ($glob as $filename) {
- $contents = file_get_contents($filename);
- if (!$contents) {
- continue;
- }
- 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 string|false
- */
- public static function getPackageName($source)
- {
- $ignore_yaml_files = ['blueprints', 'languages'];
- $glob = glob($source . '*.yaml') ?: [];
- foreach ($glob as $filename) {
- $name = strtolower(Utils::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|false
- */
- 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());
- $list = [];
- foreach ($packages as $package_name => $package) {
- $dependencies = $package['dependencies'] ?? [];
- foreach ($dependencies as $dependency) {
- if (is_array($dependency) && isset($dependency['name'])) {
- $dependency = $dependency['name'];
- }
- if ($dependency === $slug) {
- $list[] = $package_name;
- }
- }
- }
- return $list;
- }
- /**
- * Get the required version of a dependency of a package
- *
- * @param string $package_slug
- * @param string $dependency_slug
- * @return mixed|null
- */
- 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 && !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
- * @return void
- * @throws Exception
- */
- public function checkPackagesCanBeInstalled($packages_names_list)
- {
- foreach ($packages_names_list as $package_name) {
- $latest = $this->getLatestVersionOfPackage($package_name);
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package_name, $latest, $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 array
- * @throws RuntimeException
- */
- public function getDependencies($packages)
- {
- $dependencies = $this->calculateMergedDependenciesOfPackages($packages);
- foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) {
- $dependency_slug = (string)$dependency_slug;
- if (in_array($dependency_slug, $packages, true)) {
- unset($dependencies[$dependency_slug]);
- continue;
- }
- // Check PHP version
- if ($dependency_slug === 'php') {
- $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
- if (version_compare($testVersion, 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') {
- $testVersion = $this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator);
- if (version_compare($testVersion, 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)
- && $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';
- } elseif ($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 RuntimeException(
- '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;
- }
- /**
- * @param array $dependencies_slugs
- * @return void
- */
- public function checkNoOtherPackageNeedsTheseDependenciesInALowerVersion($dependencies_slugs)
- {
- foreach ($dependencies_slugs as $dependency_slug) {
- $this->checkNoOtherPackageNeedsThisDependencyInALowerVersion(
- $dependency_slug,
- $this->getLatestVersionOfPackage($dependency_slug),
- $dependencies_slugs
- );
- }
- }
- /**
- * @param string $firstVersion
- * @param string $secondVersion
- * @return bool
- */
- 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
- */
- private function calculateMergedDependenciesOfPackage($packageName, $dependencies)
- {
- $packageData = $this->findPackage($packageName);
- if (empty($packageData->dependencies)) {
- return $dependencies;
- }
- foreach ($packageData->dependencies as $dependency) {
- $dependencyName = $dependency['name'] ?? null;
- if (!$dependencyName) {
- continue;
- }
- $dependencyVersion = $dependency['version'] ?? '*';
- if (!isset($dependencies[$dependencyName])) {
- // Dependency added for the first time
- $dependencies[$dependencyName] = $dependencyVersion;
- //Factor in the package dependencies too
- $dependencies = $this->calculateMergedDependenciesOfPackage($dependencyName, $dependencies);
- } elseif ($dependencyVersion !== '*') {
- // Dependency already added by another package
- // If this package requires a version higher than the currently stored one, store this requirement instead
- $currentDependencyVersion = $dependencies[$dependencyName];
- $currently_stored_version_number = $this->calculateVersionNumberFromDependencyVersion($currentDependencyVersion);
- $currently_stored_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($currentDependencyVersion)) {
- $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($dependencyVersion);
- if (!$current_package_version_number) {
- throw new RuntimeException("Bad format for version of dependency {$dependencyName} for package {$packageName}", 1);
- }
- $current_package_version_is_in_next_significant_release_format = false;
- if ($this->versionFormatIsNextSignificantRelease($dependencyVersion)) {
- $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[$dependencyName] = $dependencyVersion;
- } elseif (!$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[$dependencyName] = $dependencyVersion;
- }
- } else {
- $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number, $current_package_version_number);
- if (!$compatible) {
- throw new RuntimeException("Dependency {$dependencyName} is required in two incompatible versions", 2);
- }
- }
- }
- }
- return $dependencies;
- }
- /**
- * Calculates and merges the dependencies of the passed packages
- *
- * @param array $packages
- * @return array
- */
- 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 string|null
- */
- 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)) {
- [$version1array, $version2array] = [$version2array, $version1array];
- }
- $i = 0;
- while ($i < count($version1array) - 1) {
- if ($version1array[$i] !== $version2array[$i]) {
- return false;
- }
- $i++;
- }
- return true;
- }
- }
|