Same name and namespace in other branches
  1. 8.9.x core/modules/update/src/ProjectSecurityRequirement.php \Drupal\update\ProjectSecurityRequirement
  2. 9 core/modules/update/src/ProjectSecurityRequirement.php \Drupal\update\ProjectSecurityRequirement

Class for generating a project's security requirement.

@internal This class implements logic to determine security coverage for Drupal core according to Drupal core security policy. It should not be called directly.

Hierarchy

Expanded class hierarchy of ProjectSecurityRequirement

See also

update_requirements()

1 file declares its use of ProjectSecurityRequirement
update.install in core/modules/update/update.install
Install, update, and uninstall functions for the Update Manager module.

File

core/modules/update/src/ProjectSecurityRequirement.php, line 17

Namespace

Drupal\update
View source
final class ProjectSecurityRequirement {
  use StringTranslationTrait;

  /**
   * The project title.
   *
   * @var string|null
   */
  protected $projectTitle;

  /**
   * Security coverage information for the project.
   *
   * @var array
   *
   * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
   */
  private $securityCoverageInfo;

  /**
   * The next version after the installed version in the format [MAJOR].[MINOR].
   *
   * @var string|null
   */
  private $nextMajorMinorVersion;

  /**
   * The existing (currently installed) version in the format [MAJOR].[MINOR].
   *
   * @var string|null
   */
  private $existingMajorMinorVersion;

  /**
   * Constructs a ProjectSecurityRequirement object.
   *
   * @param string|null $project_title
   *   The project title.
   * @param array $security_coverage_info
   *   Security coverage information as set by
   *   \Drupal\update\ProjectSecurityData::getCoverageInfo().
   * @param string|null $existing_major_minor_version
   *   The existing (currently installed) version in the format [MAJOR].[MINOR].
   * @param string|null $next_major_minor_version
   *   The next version after the installed version in the format
   *   [MAJOR].[MINOR].
   */
  private function __construct($project_title = NULL, array $security_coverage_info = [], $existing_major_minor_version = NULL, $next_major_minor_version = NULL) {
    $this->projectTitle = $project_title;
    $this->securityCoverageInfo = $security_coverage_info;
    $this->existingMajorMinorVersion = $existing_major_minor_version;
    $this->nextMajorMinorVersion = $next_major_minor_version;
  }

  /**
   * Creates a ProjectSecurityRequirement object from project data.
   *
   * @param array $project_data
   *   Project data from Drupal\update\UpdateManagerInterface::getProjects().
   *   The 'security_coverage_info' key should be set by
   *   calling \Drupal\update\ProjectSecurityData::getCoverageInfo() before
   *   calling this method. The following keys are used in this method:
   *   - existing_version (string): The version of the project that is installed
   *     on the site.
   *   - project_type (string): The type of project.
   *   - name (string): The project machine name.
   *   - title (string): The project title.
   * @param array $security_coverage_info
   *   The security coverage information as returned by
   *   \Drupal\update\ProjectSecurityData::getCoverageInfo().
   *
   * @return static
   *
   * @see \Drupal\update\UpdateManagerInterface::getProjects()
   * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
   * @see update_process_project_info()
   */
  public static function createFromProjectDataAndSecurityCoverageInfo(array $project_data, array $security_coverage_info) {
    if ($project_data['project_type'] !== 'core' || $project_data['name'] !== 'drupal' || empty($security_coverage_info)) {
      return new static();
    }
    if (isset($project_data['existing_version'])) {
      [
        $major,
        $minor,
      ] = explode('.', $project_data['existing_version']);
      $existing_version = "{$major}.{$minor}";
      $next_version = "{$major}." . ((int) $minor + 1);
      return new static($project_data['title'], $security_coverage_info, $existing_version, $next_version);
    }
    return new static($project_data['title'], $security_coverage_info);
  }

  /**
   * Gets the security coverage requirement, if any.
   *
   * @return array
   *   Requirements array as specified by hook_requirements(), or an empty array
   *   if no requirements can be determined.
   */
  public function getRequirement() {
    if (isset($this->securityCoverageInfo['security_coverage_end_version'])) {
      $requirement = $this
        ->getVersionEndRequirement();
    }
    elseif (isset($this->securityCoverageInfo['security_coverage_end_date'])) {
      $requirement = $this
        ->getDateEndRequirement();
    }
    else {
      return [];
    }
    $requirement['title'] = $this
      ->t('Drupal core security coverage');
    return $requirement;
  }

  /**
   * Gets the requirements based on security coverage until a specific version.
   *
   * @return array
   *   Requirements array as specified by hook_requirements().
   */
  private function getVersionEndRequirement() {
    $requirement = [];
    if ($security_coverage_message = $this
      ->getVersionEndCoverageMessage()) {
      $requirement['description'] = $security_coverage_message;
      if ($this->securityCoverageInfo['additional_minors_coverage'] > 0) {
        $requirement['value'] = $this
          ->t('Covered until @end_version', [
          '@end_version' => $this->securityCoverageInfo['security_coverage_end_version'],
        ]);
        $requirement['severity'] = $this->securityCoverageInfo['additional_minors_coverage'] > 1 ? REQUIREMENT_INFO : REQUIREMENT_WARNING;
      }
      else {
        $requirement['value'] = $this
          ->t('Coverage has ended');
        $requirement['severity'] = REQUIREMENT_ERROR;
      }
    }
    return $requirement;
  }

  /**
   * Gets the message for additional minor version security coverage.
   *
   * @return array[]
   *   A render array containing security coverage message.
   *
   * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
   */
  private function getVersionEndCoverageMessage() {
    if ($this->securityCoverageInfo['additional_minors_coverage'] > 0) {

      // If the installed minor version will receive security coverage until
      // newer minor versions are released, inform the user.
      if ($this->securityCoverageInfo['additional_minors_coverage'] === 1) {

        // If the installed minor version will only receive security coverage
        // for 1 newer minor core version, encourage the site owner to update
        // soon.
        $message['coverage_message'] = [
          '#markup' => $this
            ->t('<a href=":update_status_report">Update to @next_minor or higher</a> soon to continue receiving security updates.', [
            ':update_status_report' => Url::fromRoute('update.status')
              ->toString(),
            '@next_minor' => $this->nextMajorMinorVersion,
          ]),
          '#suffix' => ' ',
        ];
      }
    }
    else {

      // Because the current minor version no longer has security coverage,
      // advise the site owner to update.
      $message['coverage_message'] = [
        '#markup' => $this
          ->getVersionNoSecurityCoverageMessage(),
        '#suffix' => ' ',
      ];
    }
    $message['release_cycle_link'] = [
      '#markup' => $this
        ->getReleaseCycleLink(),
    ];
    return $message;
  }

  /**
   * Gets the security coverage requirement based on an end date.
   *
   * @return array
   *   Requirements array as specified by hook_requirements().
   */
  private function getDateEndRequirement() {
    $requirement = [];

    /** @var \Drupal\Component\Datetime\Time $time */
    $time = \Drupal::service('datetime.time');

    /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
    $date_formatter = \Drupal::service('date.formatter');

    // 'security_coverage_end_date' will either be in format 'Y-m-d' or 'Y-m'.
    if (substr_count($this->securityCoverageInfo['security_coverage_end_date'], '-') === 2) {
      $date_format = 'Y-m-d';
      $full_security_coverage_end_date = $this->securityCoverageInfo['security_coverage_end_date'];
    }
    else {
      $date_format = 'Y-m';

      // If the date does not include a day, use '15'. When calling
      // \DateTime::createFromFormat() the current day will be used if one is
      // not provided. This may cause the month to be wrong at the beginning or
      // end of the month. '15' will never be displayed because we are using the
      // 'Y-m' format.
      $full_security_coverage_end_date = $this->securityCoverageInfo['security_coverage_end_date'] . '-15';
    }
    $comparable_request_date = $date_formatter
      ->format($time
      ->getRequestTime(), 'custom', $date_format);
    if ($this->securityCoverageInfo['security_coverage_end_date'] <= $comparable_request_date) {

      // Security coverage is over.
      $requirement['value'] = $this
        ->t('Coverage has ended');
      $requirement['severity'] = REQUIREMENT_ERROR;
      $requirement['description']['coverage_message'] = [
        '#markup' => $this
          ->getVersionNoSecurityCoverageMessage(),
        '#suffix' => ' ',
      ];
    }
    else {
      $security_coverage_end_timestamp = \DateTime::createFromFormat('Y-m-d', $full_security_coverage_end_date)
        ->getTimestamp();
      $output_date_format = $date_format === 'Y-m-d' ? 'Y-M-d' : 'Y-M';
      $formatted_end_date = $date_formatter
        ->format($security_coverage_end_timestamp, 'custom', $output_date_format);
      $translation_arguments = [
        '@date' => $formatted_end_date,
      ];
      $requirement['value'] = $this
        ->t('Covered until @date', $translation_arguments);
      $requirement['severity'] = REQUIREMENT_INFO;

      // 'security_coverage_ending_warn_date' will always be in the format
      // 'Y-m-d'.
      $request_date = $date_formatter
        ->format($time
        ->getRequestTime(), 'custom', 'Y-m-d');
      if (!empty($this->securityCoverageInfo['security_coverage_ending_warn_date']) && $this->securityCoverageInfo['security_coverage_ending_warn_date'] <= $request_date) {
        $requirement['description']['coverage_message'] = [
          '#markup' => $this
            ->t('Update to a supported minor version soon to continue receiving security updates.'),
          '#suffix' => ' ',
        ];
        $requirement['severity'] = REQUIREMENT_WARNING;
      }
    }
    $requirement['description']['release_cycle_link'] = [
      '#markup' => $this
        ->getReleaseCycleLink(),
    ];
    return $requirement;
  }

  /**
   * Gets the formatted message for a project with no security coverage.
   *
   * @return string
   *   The message for a version with no security coverage.
   */
  private function getVersionNoSecurityCoverageMessage() {
    return $this
      ->t('<a href=":update_status_report">Update to a supported minor</a> as soon as possible to continue receiving security updates.', [
      ':update_status_report' => Url::fromRoute('update.status')
        ->toString(),
    ]);
  }

  /**
   * Gets a link the release cycle page on drupal.org.
   *
   * @return string
   *   A link to the release cycle page on drupal.org.
   */
  private function getReleaseCycleLink() {
    return $this
      ->t('Visit the <a href=":url">release cycle overview</a> for more information on supported releases.', [
      ':url' => 'https://www.drupal.org/core/release-cycle-overview',
    ]);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ProjectSecurityRequirement::$existingMajorMinorVersion private property The existing (currently installed) version in the format [MAJOR].[MINOR].
ProjectSecurityRequirement::$nextMajorMinorVersion private property The next version after the installed version in the format [MAJOR].[MINOR].
ProjectSecurityRequirement::$projectTitle protected property The project title.
ProjectSecurityRequirement::$securityCoverageInfo private property Security coverage information for the project.
ProjectSecurityRequirement::createFromProjectDataAndSecurityCoverageInfo public static function Creates a ProjectSecurityRequirement object from project data.
ProjectSecurityRequirement::getDateEndRequirement private function Gets the security coverage requirement based on an end date.
ProjectSecurityRequirement::getReleaseCycleLink private function Gets a link the release cycle page on drupal.org.
ProjectSecurityRequirement::getRequirement public function Gets the security coverage requirement, if any.
ProjectSecurityRequirement::getVersionEndCoverageMessage private function Gets the message for additional minor version security coverage.
ProjectSecurityRequirement::getVersionEndRequirement private function Gets the requirements based on security coverage until a specific version.
ProjectSecurityRequirement::getVersionNoSecurityCoverageMessage private function Gets the formatted message for a project with no security coverage.
ProjectSecurityRequirement::__construct private function Constructs a ProjectSecurityRequirement object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 1
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.