Skip to content
Snippets Groups Projects
Forked from project / automatic_updates
549 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ProjectInfo.php 5.19 KiB
<?php

namespace Drupal\automatic_updates;

use Composer\Semver\Comparator;
use Drupal\update\ProjectRelease;
use Drupal\Core\Extension\ExtensionVersion;
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 {

  /**
   * The project name.
   *
   * @var string
   */
  protected $name;

  /**
   * Constructs a ProjectInfo object.
   *
   * @param string $name
   *   The project name.
   */
  public function __construct(string $name) {
    $this->name = $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 array|null
   *   The retrieved project information.
   *
   * @throws \RuntimeException
   *   If data about available updates cannot be retrieved.
   */
  public function getProjectInfo(): ?array {
    $available_updates = update_get_available(TRUE);
    $project_data = update_calculate_project_data($available_updates);
    return $project_data[$this->name] ?? NULL;
  }

  /**
   * 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 = update_get_available()[$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']);
    }

    // If we're already up-to-date, there's nothing else we need to do.
    if ($project['status'] === UpdateManagerInterface::CURRENT) {
      return [];
    }

    if (empty($available_updates['releases'])) {
      // If project is not current we should always have at least one release.
      throw new \RuntimeException('There was a problem getting update information. Try again later.');
    }
    $installed_version = $this->getInstalledVersion();
    $support_branches = explode(',', $available_updates['supported_branches']);
    $installable_releases = [];
    foreach ($available_updates['releases'] as $release_info) {
      $release = ProjectRelease::createFromArray($release_info);
      $version = $release->getVersion();
      $semantic_version = LegacyVersionUtility::convertToSemanticVersion($version);
      $semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version);
      if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) {
        // 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 {
    if ($project_data = $this->getProjectInfo()) {
      if (empty($project_data['existing_version'])) {
        throw new \UnexpectedValueException("Project '{$this->name}' does not have 'existing_version' set");
      }
      return $project_data['existing_version'];
    }
    return NULL;
  }

}