1<?php 2 3namespace Drupal\update; 4 5use Drupal\Core\Extension\ExtensionVersion; 6 7/** 8 * Calculates a project's security coverage information. 9 * 10 * @internal 11 * This class implements logic to determine security coverage for Drupal core 12 * according to Drupal core security policy. It should not be called directly. 13 */ 14final class ProjectSecurityData { 15 16 /** 17 * The number of minor versions of Drupal core that receive security coverage. 18 * 19 * For example, if this value is 2 and the existing version is 9.0.1, the 20 * 9.0.x branch will receive security coverage until the release of version 21 * 9.2.0. 22 * 23 * @todo In https://www.drupal.org/node/2998285 determine if we want this 24 * policy to be expressed in the updates.drupal.org feed, instead of relying 25 * on a hard-coded constant. 26 * 27 * @see https://www.drupal.org/core/release-cycle-overview 28 */ 29 const CORE_MINORS_WITH_SECURITY_COVERAGE = 2; 30 31 /** 32 * Define constants for versions with security coverage end dates. 33 * 34 * Two types of constants are supported: 35 * - SECURITY_COVERAGE_END_DATE_[VERSION_MAJOR]_[VERSION_MINOR]: A date in 36 * 'Y-m-d' or 'Y-m' format. 37 * - SECURITY_COVERAGE_ENDING_WARN_DATE_[VERSION_MAJOR]_[VERSION_MINOR]: A 38 * date in 'Y-m-d' format. 39 * 40 * @see \Drupal\update\ProjectSecurityRequirement::getDateEndRequirement() 41 */ 42 const SECURITY_COVERAGE_END_DATE_8_8 = '2020-12-02'; 43 44 const SECURITY_COVERAGE_ENDING_WARN_DATE_8_8 = '2020-06-02'; 45 46 const SECURITY_COVERAGE_END_DATE_8_9 = '2021-11'; 47 48 /** 49 * The existing (currently installed) version of the project. 50 * 51 * Because this class only handles the Drupal core project, values will be 52 * semantic version numbers such as 8.8.0, 8.8.0-alpha1, or 9.0.0. 53 * 54 * @var string|null 55 */ 56 protected $existingVersion; 57 58 /** 59 * Releases as returned by update_get_available(). 60 * 61 * @var array 62 * 63 * Each release item in the array has metadata about that release. This class 64 * uses the keys: 65 * - status (string): The status of the release. 66 * - version (string): The version number of the release. 67 * 68 * @see update_get_available() 69 */ 70 protected $releases; 71 72 /** 73 * Constructs a ProjectSecurityData object. 74 * 75 * @param string $existing_version 76 * The existing (currently installed) version of the project. 77 * @param array $releases 78 * Project releases as returned by update_get_available(). 79 */ 80 private function __construct($existing_version = NULL, array $releases = []) { 81 $this->existingVersion = $existing_version; 82 $this->releases = $releases; 83 } 84 85 /** 86 * Creates a ProjectSecurityData object from project data and releases. 87 * 88 * @param array $project_data 89 * Project data from Drupal\update\UpdateManagerInterface::getProjects() and 90 * processed by update_process_project_info(). 91 * @param array $releases 92 * Project releases as returned by update_get_available(). 93 * 94 * @return static 95 */ 96 public static function createFromProjectDataAndReleases(array $project_data, array $releases) { 97 if (!($project_data['project_type'] === 'core' && $project_data['name'] === 'drupal')) { 98 // Only Drupal core has an explicit coverage range. 99 return new static(); 100 } 101 return new static($project_data['existing_version'], $releases); 102 } 103 104 /** 105 * Gets the security coverage information for a project. 106 * 107 * Currently only Drupal core is supported. 108 * 109 * @return array 110 * The security coverage information, or an empty array if no security 111 * information is available for the project. If security coverage is based 112 * on release of a specific version, the array will have the following 113 * keys: 114 * - security_coverage_end_version (string): The minor version the existing 115 * version will receive security coverage until. 116 * - additional_minors_coverage (int): The number of additional minor 117 * versions the existing version will receive security coverage. 118 * If the security coverage is based on a specific date, the array will have 119 * the following keys: 120 * - security_coverage_end_date (string): The month or date security 121 * coverage will end for the existing version. It can be in either 122 * 'YYYY-MM' or 'YYYY-MM-DD' format. 123 * - (optional) security_coverage_ending_warn_date (string): The date, in 124 * the format 'YYYY-MM-DD', after which a warning should be displayed 125 * about upgrading to another version. 126 */ 127 public function getCoverageInfo() { 128 if (empty($this->releases[$this->existingVersion])) { 129 // If the existing version does not have a release, we cannot get the 130 // security coverage information. 131 return []; 132 } 133 $info = []; 134 $existing_release_version = ExtensionVersion::createFromVersionString($this->existingVersion); 135 136 // Check if the installed version has a specific end date defined. 137 $version_suffix = $existing_release_version->getMajorVersion() . '_' . $this->getSemanticMinorVersion($this->existingVersion); 138 if (defined("self::SECURITY_COVERAGE_END_DATE_$version_suffix")) { 139 $info['security_coverage_end_date'] = constant("self::SECURITY_COVERAGE_END_DATE_$version_suffix"); 140 $info['security_coverage_ending_warn_date'] = 141 defined("self::SECURITY_COVERAGE_ENDING_WARN_DATE_$version_suffix") 142 ? constant("self::SECURITY_COVERAGE_ENDING_WARN_DATE_$version_suffix") 143 : NULL; 144 } 145 elseif ($security_coverage_until_version = $this->getSecurityCoverageUntilVersion()) { 146 $info['security_coverage_end_version'] = $security_coverage_until_version; 147 $info['additional_minors_coverage'] = $this->getAdditionalSecurityCoveredMinors($security_coverage_until_version); 148 } 149 return $info; 150 } 151 152 /** 153 * Gets the release the current minor will receive security coverage until. 154 * 155 * For the sake of example, assume that the currently installed version of 156 * Drupal is 8.7.11 and that static::CORE_MINORS_WITH_SECURITY_COVERAGE is 2. 157 * When Drupal 8.9.0 is released, the supported minor versions will be 8.8 158 * and 8.9. At that point, Drupal 8.7 will no longer have security coverage. 159 * Therefore, this function would return "8.9.0". 160 * 161 * @todo In https://www.drupal.org/node/2998285 determine how we will know 162 * what the final minor release of a particular major version will be. This 163 * method should not return a version beyond that minor. 164 * 165 * @return string|null 166 * The version the existing version will receive security coverage until or 167 * NULL if this cannot be determined. 168 */ 169 private function getSecurityCoverageUntilVersion() { 170 $existing_release_version = ExtensionVersion::createFromVersionString($this->existingVersion); 171 if (!empty($existing_release_version->getVersionExtra())) { 172 // Only full releases receive security coverage. 173 return NULL; 174 } 175 176 return $existing_release_version->getMajorVersion() . '.' 177 . ($this->getSemanticMinorVersion($this->existingVersion) + static::CORE_MINORS_WITH_SECURITY_COVERAGE) 178 . '.0'; 179 } 180 181 /** 182 * Gets the number of additional minor releases with security coverage. 183 * 184 * This function compares the currently installed (existing) version of 185 * the project with two things: 186 * - The latest available official release of that project. 187 * - The target minor release where security coverage for the current release 188 * should expire. This target release is determined by 189 * getSecurityCoverageUntilVersion(). 190 * 191 * For the sake of example, assume that the currently installed version of 192 * Drupal is 8.7.11 and that static::CORE_MINORS_WITH_SECURITY_COVERAGE is 2. 193 * 194 * Before the release of Drupal 8.8.0, this function would return 2. 195 * 196 * After the release of Drupal 8.8.0 and before the release of 8.9.0, this 197 * function would return 1 to indicate that the next minor version release 198 * will end security coverage for 8.7. 199 * 200 * When Drupal 8.9.0 is released, this function would return 0 to indicate 201 * that security coverage is over for 8.7. 202 * 203 * If the currently installed version is 9.0.0, and there is no 9.1.0 release 204 * yet, the function would return 2. Once 9.1.0 is out, it would return 1. 205 * When 9.2.0 is released, it would again return 0. 206 * 207 * Note: callers should not test this function's return value with empty() 208 * since 0 is a valid return value that has different meaning than NULL. 209 * 210 * @param string $security_covered_version 211 * The version until which the existing version receives security coverage. 212 * 213 * @return int|null 214 * The number of additional minor releases that receive security coverage, 215 * or NULL if this cannot be determined. 216 * 217 * @see \Drupal\update\ProjectSecurityData\getSecurityCoverageUntilVersion() 218 */ 219 private function getAdditionalSecurityCoveredMinors($security_covered_version) { 220 $security_covered_version_major = ExtensionVersion::createFromVersionString($security_covered_version)->getMajorVersion(); 221 $security_covered_version_minor = $this->getSemanticMinorVersion($security_covered_version); 222 foreach ($this->releases as $release) { 223 $release_version = ExtensionVersion::createFromVersionString($release['version']); 224 if ($release_version->getMajorVersion() === $security_covered_version_major && $release['status'] === 'published' && !$release_version->getVersionExtra()) { 225 // The releases are ordered with the most recent releases first. 226 // Therefore, if we have found a published, official release with the 227 // same major version as $security_covered_version, then this release 228 // can be used to determine the latest minor. 229 $latest_minor = $this->getSemanticMinorVersion($release['version']); 230 break; 231 } 232 } 233 // If $latest_minor is set, we know that $security_covered_version_minor and 234 // $latest_minor have the same major version. Therefore, we can subtract to 235 // determine the number of additional minor releases with security coverage. 236 return isset($latest_minor) ? $security_covered_version_minor - $latest_minor : NULL; 237 } 238 239 /** 240 * Gets the minor version for a semantic version string. 241 * 242 * @param string $version 243 * The semantic version string. 244 * 245 * @return int 246 * The minor version as an integer. 247 */ 248 private function getSemanticMinorVersion($version) { 249 return (int) (explode('.', $version)[1]); 250 } 251 252} 253