ProjectSecurityData.php 10 KB

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