You are here

final class ProjectSecurityRequirement in Drupal 10

Same name and namespace in other branches
  1. 8 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