ProjectInfo.php

Same filename in this branch
  1. 11.x core/lib/Drupal/Core/Utility/ProjectInfo.php
Same filename in other branches
  1. 9 core/lib/Drupal/Core/Utility/ProjectInfo.php
  2. 8.9.x core/lib/Drupal/Core/Utility/ProjectInfo.php
  3. 10 core/lib/Drupal/Core/Utility/ProjectInfo.php

Namespace

Drupal\package_manager

File

core/modules/package_manager/src/ProjectInfo.php

View source
<?php

declare (strict_types=1);
namespace Drupal\package_manager;

use Composer\Semver\Comparator;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\update\ProjectRelease;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\Utility\Error;
use Drupal\update\UpdateManagerInterface;

/**
 * Defines a class for retrieving project information from Update module.
 *
 * @internal
 *   This is an internal part of Automatic Updates and may be changed or removed
 *   at any time without warning. External code should use the Update API
 *   directly.
 */
final class ProjectInfo {
    public function __construct(string $name) {
    }
    
    /**
     * Determines if a release can be installed.
     *
     * @param \Drupal\update\ProjectRelease $release
     *   The project release.
     * @param string[] $support_branches
     *   The supported branches.
     *
     * @return bool
     *   TRUE if the release is installable, otherwise FALSE. A release will be
     *   considered installable if it is secure, published, supported, and in
     *   a supported branch.
     */
    private function isInstallable(ProjectRelease $release, array $support_branches) : bool {
        if ($release->isInsecure() || !$release->isPublished() || $release->isUnsupported()) {
            return FALSE;
        }
        $version = ExtensionVersion::createFromVersionString($release->getVersion());
        if ($version->getVersionExtra() === 'dev') {
            return FALSE;
        }
        foreach ($support_branches as $support_branch) {
            $support_branch_version = ExtensionVersion::createFromSupportBranch($support_branch);
            if ($support_branch_version->getMajorVersion() === $version->getMajorVersion() && $support_branch_version->getMinorVersion() === $version->getMinorVersion()) {
                return TRUE;
            }
        }
        return FALSE;
    }
    
    /**
     * Returns up-to-date project information.
     *
     * @return mixed[]|null
     *   The retrieved project information.
     *
     * @throws \RuntimeException
     *   If data about available updates cannot be retrieved.
     */
    public function getProjectInfo() : ?array {
        $available_updates = $this->getAvailableProjects();
        $project_data = update_calculate_project_data($available_updates);
        if (!isset($project_data[$this->name])) {
            return $available_updates[$this->name] ?? NULL;
        }
        return $project_data[$this->name];
    }
    
    /**
     * Gets all project releases to which the site can update.
     *
     * @return \Drupal\update\ProjectRelease[]|null
     *   If the project information is available, an array of releases that can be
     *   installed, keyed by version number; otherwise NULL. The releases are in
     *   descending order by version number (i.e., higher versions are listed
     *   first). The currently installed version of the project, and any older
     *   versions, are not considered installable releases.
     *
     * @throws \RuntimeException
     *   Thrown if there are no available releases.
     *
     * @todo Remove or simplify this function in https://www.drupal.org/i/3252190.
     */
    public function getInstallableReleases() : ?array {
        $project = $this->getProjectInfo();
        if (!$project) {
            return NULL;
        }
        $available_updates = $this->getAvailableProjects()[$this->name];
        if ($available_updates['project_status'] !== 'published') {
            throw new \RuntimeException("The project '{$this->name}' can not be updated because its status is " . $available_updates['project_status']);
        }
        $installed_version = $this->getInstalledVersion();
        if ($installed_version && empty($available_updates['releases'])) {
            // If project is installed but not current we should always have at
            // least one release.
            throw new \RuntimeException('There was a problem getting update information. Try again later.');
        }
        $support_branches = explode(',', $available_updates['supported_branches']);
        $installable_releases = [];
        foreach ($available_updates['releases'] as $release_info) {
            try {
                $release = ProjectRelease::createFromArray($release_info);
            } catch (\UnexpectedValueException $exception) {
                // Ignore releases that are in an invalid format. Although this is
                // unlikely we should still only process releases in the correct format.
                \Drupal::logger('package_manager')->error(sprintf('Invalid project format: %s', print_r($release_info, TRUE)), Error::decodeException($exception));
                continue;
            }
            $version = $release->getVersion();
            if ($installed_version) {
                $semantic_version = LegacyVersionUtility::convertToSemanticVersion($version);
                $semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version);
                if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) {
                    // If the project is installed stop searching for releases as soon as
                    // we find the installed version.
                    break;
                }
            }
            if ($this->isInstallable($release, $support_branches)) {
                $installable_releases[$version] = $release;
            }
        }
        return $installable_releases;
    }
    
    /**
     * Returns the installed project version, according to the Update module.
     *
     * @return string|null
     *   The installed project version as known to the Update module or NULL if
     *   the project information is not available.
     */
    public function getInstalledVersion() : ?string {
        $project_data = $this->getProjectInfo();
        if ($project_data && array_key_exists('existing_version', $project_data)) {
            $existing_version = $project_data['existing_version'];
            // Treat an unknown version the same as a project whose project
            // information is not available, so return NULL.
            // @see \update_process_project_info()
            if ($existing_version instanceof TranslatableMarkup && $existing_version->getUntranslatedString() === 'Unknown') {
                return NULL;
            }
            // TRICKY: Since this is relying on data coming from
            // \Drupal\update\UpdateManager::getProjects(), we cannot be certain that
            // we are actually receiving strings.
            // @see \Drupal\update\UpdateManager::getProjects()
            if (!is_string($existing_version)) {
                return NULL;
            }
            return $existing_version;
        }
        return NULL;
    }
    
    /**
     * Gets the available projects.
     *
     * @return array
     *   The available projects keyed by project machine name in the format
     *   returned by update_get_available(). If the project specified in ::name is
     *   not returned from update_get_available() this project will be explicitly
     *   fetched and added the return value of this function.
     *
     * @see \update_get_available()
     */
    private function getAvailableProjects() : array {
        $available_projects = update_get_available(TRUE);
        // update_get_available() will only returns projects that are in the active
        // codebase. If the project specified by ::name is not returned in
        // $available_projects, it means it is not in the active codebase, therefore
        // we will retrieve the project information from Package Manager's own
        // update processor service.
        if (!isset($available_projects[$this->name])) {
            
            /** @var \Drupal\package_manager\PackageManagerUpdateProcessor $update_processor */
            $update_processor = \Drupal::service(PackageManagerUpdateProcessor::class);
            if ($project_data = $update_processor->getProjectData($this->name)) {
                $available_projects[$this->name] = $project_data;
            }
        }
        return $available_projects;
    }
    
    /**
     * Checks if the installed version of this project is safe to use.
     *
     * @return bool
     *   TRUE if the installed version of this project is secure, supported, and
     *   published. Otherwise, or if the project information could not be
     *   retrieved, returns FALSE.
     */
    public function isInstalledVersionSafe() : bool {
        $project_data = $this->getProjectInfo();
        if ($project_data) {
            $unsafe = [
                UpdateManagerInterface::NOT_SECURE,
                UpdateManagerInterface::NOT_SUPPORTED,
                UpdateManagerInterface::REVOKED,
            ];
            return !in_array($project_data['status'], $unsafe, TRUE);
        }
        // If we couldn't get project data, assume the installed version is unsafe.
        return FALSE;
    }
    
    /**
     * Gets the supported branches of the project.
     *
     * @return string[]
     *   The supported branches.
     */
    public function getSupportedBranches() : array {
        $available_updates = $this->getAvailableProjects()[$this->name];
        return isset($available_updates['supported_branches']) ? explode(',', $available_updates['supported_branches']) : [];
    }

}

Classes

Title Deprecated Summary
ProjectInfo Defines a class for retrieving project information from Update module.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.