ProjectSecurityRequirement.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <?php
  2. namespace Drupal\update;
  3. use Drupal\Core\StringTranslation\StringTranslationTrait;
  4. use Drupal\Core\Url;
  5. /**
  6. * Class for generating a project's security requirement.
  7. *
  8. * @see update_requirements()
  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. */
  14. final class ProjectSecurityRequirement {
  15. use StringTranslationTrait;
  16. /**
  17. * The project title.
  18. *
  19. * @var string|null
  20. */
  21. protected $projectTitle;
  22. /**
  23. * Security coverage information for the project.
  24. *
  25. * @var array
  26. *
  27. * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
  28. */
  29. private $securityCoverageInfo;
  30. /**
  31. * The next version after the installed version in the format [MAJOR].[MINOR].
  32. *
  33. * @var string|null
  34. */
  35. private $nextMajorMinorVersion;
  36. /**
  37. * The existing (currently installed) version in the format [MAJOR].[MINOR].
  38. *
  39. * @var string|null
  40. */
  41. private $existingMajorMinorVersion;
  42. /**
  43. * Constructs a ProjectSecurityRequirement object.
  44. *
  45. * @param string|null $project_title
  46. * The project title.
  47. * @param array $security_coverage_info
  48. * Security coverage information as set by
  49. * \Drupal\update\ProjectSecurityData::getCoverageInfo().
  50. * @param string|null $existing_major_minor_version
  51. * The existing (currently installed) version in the format [MAJOR].[MINOR].
  52. * @param string|null $next_major_minor_version
  53. * The next version after the installed version in the format
  54. * [MAJOR].[MINOR].
  55. */
  56. private function __construct($project_title = NULL, array $security_coverage_info = [], $existing_major_minor_version = NULL, $next_major_minor_version = NULL) {
  57. $this->projectTitle = $project_title;
  58. $this->securityCoverageInfo = $security_coverage_info;
  59. $this->existingMajorMinorVersion = $existing_major_minor_version;
  60. $this->nextMajorMinorVersion = $next_major_minor_version;
  61. }
  62. /**
  63. * Creates a ProjectSecurityRequirement object from project data.
  64. *
  65. * @param array $project_data
  66. * Project data from Drupal\update\UpdateManagerInterface::getProjects().
  67. * The 'security_coverage_info' key should be set by
  68. * calling \Drupal\update\ProjectSecurityData::getCoverageInfo() before
  69. * calling this method. The following keys are used in this method:
  70. * - existing_version (string): The version of the project that is installed
  71. * on the site.
  72. * - project_type (string): The type of project.
  73. * - name (string): The project machine name.
  74. * - title (string): The project title.
  75. * @param array $security_coverage_info
  76. * The security coverage information as returned by
  77. * \Drupal\update\ProjectSecurityData::getCoverageInfo().
  78. *
  79. * @return static
  80. *
  81. * @see \Drupal\update\UpdateManagerInterface::getProjects()
  82. * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
  83. * @see update_process_project_info()
  84. */
  85. public static function createFromProjectDataAndSecurityCoverageInfo(array $project_data, array $security_coverage_info) {
  86. if ($project_data['project_type'] !== 'core' || $project_data['name'] !== 'drupal' || empty($security_coverage_info)) {
  87. return new static();
  88. }
  89. if (isset($project_data['existing_version'])) {
  90. list($major, $minor) = explode('.', $project_data['existing_version']);
  91. $existing_version = "$major.$minor";
  92. $next_version = "$major." . ((int) $minor + 1);
  93. return new static($project_data['title'], $security_coverage_info, $existing_version, $next_version);
  94. }
  95. return new static($project_data['title'], $security_coverage_info);
  96. }
  97. /**
  98. * Gets the security coverage requirement, if any.
  99. *
  100. * @return array
  101. * Requirements array as specified by hook_requirements(), or an empty array
  102. * if no requirements can be determined.
  103. */
  104. public function getRequirement() {
  105. if (isset($this->securityCoverageInfo['security_coverage_end_version'])) {
  106. $requirement = $this->getVersionEndRequirement();
  107. }
  108. elseif (isset($this->securityCoverageInfo['security_coverage_end_date'])) {
  109. $requirement = $this->getDateEndRequirement();
  110. }
  111. else {
  112. return [];
  113. }
  114. $requirement['title'] = $this->t('Drupal core security coverage');
  115. return $requirement;
  116. }
  117. /**
  118. * Gets the requirements based on security coverage until a specific version.
  119. *
  120. * @return array
  121. * Requirements array as specified by hook_requirements().
  122. */
  123. private function getVersionEndRequirement() {
  124. $requirement = [];
  125. if ($security_coverage_message = $this->getVersionEndCoverageMessage()) {
  126. $requirement['description'] = $security_coverage_message;
  127. if ($this->securityCoverageInfo['additional_minors_coverage'] > 0) {
  128. $requirement['value'] = $this->t(
  129. 'Covered until @end_version',
  130. ['@end_version' => $this->securityCoverageInfo['security_coverage_end_version']]
  131. );
  132. $requirement['severity'] = $this->securityCoverageInfo['additional_minors_coverage'] > 1 ? REQUIREMENT_INFO : REQUIREMENT_WARNING;
  133. }
  134. else {
  135. $requirement['value'] = $this->t('Coverage has ended');
  136. $requirement['severity'] = REQUIREMENT_ERROR;
  137. }
  138. }
  139. return $requirement;
  140. }
  141. /**
  142. * Gets the message for additional minor version security coverage.
  143. *
  144. * @return array[]
  145. * A render array containing security coverage message.
  146. *
  147. * @see \Drupal\update\ProjectSecurityData::getCoverageInfo()
  148. */
  149. private function getVersionEndCoverageMessage() {
  150. if ($this->securityCoverageInfo['additional_minors_coverage'] > 0) {
  151. // If the installed minor version will receive security coverage until
  152. // newer minor versions are released, inform the user.
  153. if ($this->securityCoverageInfo['additional_minors_coverage'] === 1) {
  154. // If the installed minor version will only receive security coverage
  155. // for 1 newer minor core version, encourage the site owner to update
  156. // soon.
  157. $message['coverage_message'] = [
  158. '#markup' => $this->t(
  159. '<a href=":update_status_report">Update to @next_minor or higher</a> soon to continue receiving security updates.',
  160. [
  161. ':update_status_report' => Url::fromRoute('update.status')->toString(),
  162. '@next_minor' => $this->nextMajorMinorVersion,
  163. ]
  164. ),
  165. '#suffix' => ' ',
  166. ];
  167. }
  168. }
  169. else {
  170. // Because the current minor version no longer has security coverage,
  171. // advise the site owner to update.
  172. $message['coverage_message'] = [
  173. '#markup' => $this->getVersionNoSecurityCoverageMessage(),
  174. '#suffix' => ' ',
  175. ];
  176. }
  177. $message['release_cycle_link'] = [
  178. '#markup' => $this->getReleaseCycleLink(),
  179. ];
  180. return $message;
  181. }
  182. /**
  183. * Gets the security coverage requirement based on an end date.
  184. *
  185. * @return array
  186. * Requirements array as specified by hook_requirements().
  187. */
  188. private function getDateEndRequirement() {
  189. $requirement = [];
  190. /** @var \Drupal\Component\Datetime\Time $time */
  191. $time = \Drupal::service('datetime.time');
  192. /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
  193. $date_formatter = \Drupal::service('date.formatter');
  194. // 'security_coverage_end_date' will either be in format 'Y-m-d' or 'Y-m'.
  195. if (substr_count($this->securityCoverageInfo['security_coverage_end_date'], '-') === 2) {
  196. $date_format = 'Y-m-d';
  197. $full_security_coverage_end_date = $this->securityCoverageInfo['security_coverage_end_date'];
  198. }
  199. else {
  200. $date_format = 'Y-m';
  201. // If the date does not include a day, use '15'. When calling
  202. // \DateTime::createFromFormat() the current day will be used if one is
  203. // not provided. This may cause the month to be wrong at the beginning or
  204. // end of the month. '15' will never be displayed because we are using the
  205. // 'Y-m' format.
  206. $full_security_coverage_end_date = $this->securityCoverageInfo['security_coverage_end_date'] . '-15';
  207. }
  208. $comparable_request_date = $date_formatter->format($time->getRequestTime(), 'custom', $date_format);
  209. if ($this->securityCoverageInfo['security_coverage_end_date'] <= $comparable_request_date) {
  210. // Security coverage is over.
  211. $requirement['value'] = $this->t('Coverage has ended');
  212. $requirement['severity'] = REQUIREMENT_ERROR;
  213. $requirement['description']['coverage_message'] = [
  214. '#markup' => $this->getVersionNoSecurityCoverageMessage(),
  215. '#suffix' => ' ',
  216. ];
  217. }
  218. else {
  219. $security_coverage_end_timestamp = \DateTime::createFromFormat('Y-m-d', $full_security_coverage_end_date)->getTimestamp();
  220. $output_date_format = $date_format === 'Y-m-d' ? 'Y-M-d' : 'Y-M';
  221. $formatted_end_date = $date_formatter
  222. ->format($security_coverage_end_timestamp, 'custom', $output_date_format);
  223. $translation_arguments = ['@date' => $formatted_end_date];
  224. $requirement['value'] = $this->t('Covered until @date', $translation_arguments);
  225. $requirement['severity'] = REQUIREMENT_INFO;
  226. // 'security_coverage_ending_warn_date' will always be in the format
  227. // 'Y-m-d'.
  228. $request_date = $date_formatter->format($time->getRequestTime(), 'custom', 'Y-m-d');
  229. if (!empty($this->securityCoverageInfo['security_coverage_ending_warn_date']) && $this->securityCoverageInfo['security_coverage_ending_warn_date'] <= $request_date) {
  230. $requirement['description']['coverage_message'] = [
  231. '#markup' => $this->t('Update to a supported minor version soon to continue receiving security updates.'),
  232. '#suffix' => ' ',
  233. ];
  234. $requirement['severity'] = REQUIREMENT_WARNING;
  235. }
  236. }
  237. $requirement['description']['release_cycle_link'] = ['#markup' => $this->getReleaseCycleLink()];
  238. return $requirement;
  239. }
  240. /**
  241. * Gets the formatted message for a project with no security coverage.
  242. *
  243. * @return string
  244. * The message for a version with no security coverage.
  245. */
  246. private function getVersionNoSecurityCoverageMessage() {
  247. return $this->t(
  248. '<a href=":update_status_report">Update to a supported minor</a> as soon as possible to continue receiving security updates.',
  249. [':update_status_report' => Url::fromRoute('update.status')->toString()]
  250. );
  251. }
  252. /**
  253. * Gets a link the release cycle page on drupal.org.
  254. *
  255. * @return string
  256. * A link to the release cycle page on drupal.org.
  257. */
  258. private function getReleaseCycleLink() {
  259. return $this->t(
  260. 'Visit the <a href=":url">release cycle overview</a> for more information on supported releases.',
  261. [':url' => 'https://www.drupal.org/core/release-cycle-overview']
  262. );
  263. }
  264. }