PageRenderTime 48ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/core/lib/Drupal/Core/Extension/InfoParserDynamic.php

http://github.com/drupal/drupal
PHP | 215 lines | 163 code | 8 blank | 44 comment | 25 complexity | fafc0a069f5c60219ea35e810e8a3278 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\Extension;
  3. use Composer\Semver\Semver;
  4. use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
  5. use Drupal\Core\Serialization\Yaml;
  6. /**
  7. * Parses dynamic .info.yml files that might change during the page request.
  8. */
  9. class InfoParserDynamic implements InfoParserInterface {
  10. /**
  11. * The root directory of the Drupal installation.
  12. *
  13. * @var string
  14. */
  15. protected $root;
  16. /**
  17. * The earliest Drupal version that supports the 'core_version_requirement'.
  18. */
  19. const FIRST_CORE_VERSION_REQUIREMENT_SUPPORTED_VERSION = '8.7.7';
  20. /**
  21. * InfoParserDynamic constructor.
  22. *
  23. * @param string|null $app_root
  24. * The root directory of the Drupal installation.
  25. */
  26. public function __construct(string $app_root = NULL) {
  27. if ($app_root === NULL) {
  28. // @todo https://www.drupal.org/project/drupal/issues/3087975 Require
  29. // $app_root argument.
  30. $app_root = \Drupal::hasService('app.root') ? (string) \Drupal::service('app.root') : DRUPAL_ROOT;
  31. }
  32. $this->root = $app_root;
  33. }
  34. /**
  35. * {@inheritdoc}
  36. */
  37. public function parse($filename) {
  38. if (!file_exists($filename)) {
  39. $parsed_info = [];
  40. }
  41. else {
  42. try {
  43. $parsed_info = Yaml::decode(file_get_contents($filename));
  44. }
  45. catch (InvalidDataTypeException $e) {
  46. throw new InfoParserException("Unable to parse $filename " . $e->getMessage());
  47. }
  48. $missing_keys = array_diff($this->getRequiredKeys(), array_keys($parsed_info));
  49. if (!empty($missing_keys)) {
  50. throw new InfoParserException('Missing required keys (' . implode(', ', $missing_keys) . ') in ' . $filename);
  51. }
  52. if (!isset($parsed_info['core']) && !isset($parsed_info['core_version_requirement'])) {
  53. if (strpos($filename, 'core/') === 0 || strpos($filename, $this->root . '/core/') === 0) {
  54. // Core extensions do not need to specify core compatibility: they are
  55. // by definition compatible so a sensible default is used. Core
  56. // modules are allowed to provide these for testing purposes.
  57. $parsed_info['core_version_requirement'] = \Drupal::VERSION;
  58. }
  59. elseif (isset($parsed_info['package']) && $parsed_info['package'] === 'Testing') {
  60. // Modules in the testing package are exempt as well. This makes it
  61. // easier for contrib to use test modules.
  62. $parsed_info['core_version_requirement'] = \Drupal::VERSION;
  63. }
  64. else {
  65. // Non-core extensions must specify core compatibility.
  66. throw new InfoParserException("The 'core' or the 'core_version_requirement' key must be present in " . $filename);
  67. }
  68. }
  69. if (isset($parsed_info['core']) && !preg_match("/^\d\.x$/", $parsed_info['core'])) {
  70. throw new InfoParserException("Invalid 'core' value \"{$parsed_info['core']}\" in " . $filename);
  71. }
  72. if (isset($parsed_info['core_version_requirement'])) {
  73. try {
  74. $supports_pre_core_version_requirement_version = static::isConstraintSatisfiedByPreviousVersion($parsed_info['core_version_requirement'], static::FIRST_CORE_VERSION_REQUIREMENT_SUPPORTED_VERSION);
  75. }
  76. catch (\UnexpectedValueException $e) {
  77. throw new InfoParserException("The 'core_version_requirement' constraint ({$parsed_info['core_version_requirement']}) is not a valid value in $filename");
  78. }
  79. // If the 'core_version_requirement' constraint does not satisfy any
  80. // Drupal 8 versions before 8.7.7 then 'core' cannot be set or it will
  81. // effectively support all versions of Drupal 8 because
  82. // 'core_version_requirement' will be ignored in previous versions.
  83. if (!$supports_pre_core_version_requirement_version && isset($parsed_info['core'])) {
  84. throw new InfoParserException("The 'core_version_requirement' constraint ({$parsed_info['core_version_requirement']}) requires the 'core' key not be set in " . $filename);
  85. }
  86. // 'core_version_requirement' can not be used to specify Drupal 8
  87. // versions before 8.7.7 because these versions do not use the
  88. // 'core_version_requirement' key. Do not throw the exception if the
  89. // constraint also is satisfied by 8.0.0-alpha1 to allow constraints
  90. // such as '^8' or '^8 || ^9'.
  91. if ($supports_pre_core_version_requirement_version && !Semver::satisfies('8.0.0-alpha1', $parsed_info['core_version_requirement'])) {
  92. throw new InfoParserException("The 'core_version_requirement' can not be used to specify compatibility for a specific version before " . static::FIRST_CORE_VERSION_REQUIREMENT_SUPPORTED_VERSION . " in $filename");
  93. }
  94. }
  95. // Determine if the extension is compatible with the current version of
  96. // Drupal core.
  97. $core_version_constraint = isset($parsed_info['core_version_requirement']) ? $parsed_info['core_version_requirement'] : $parsed_info['core'];
  98. $parsed_info['core_incompatible'] = !Semver::satisfies(\Drupal::VERSION, $core_version_constraint);
  99. if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
  100. $parsed_info['version'] = \Drupal::VERSION;
  101. }
  102. // Special backwards compatible handling profiles and their 'dependencies'
  103. // key.
  104. if ($parsed_info['type'] === 'profile' && isset($parsed_info['dependencies']) && !array_key_exists('install', $parsed_info)) {
  105. // Only trigger the deprecation message if we are actually using the
  106. // profile with the missing 'install' key. This avoids triggering the
  107. // deprecation when scanning all the available install profiles.
  108. global $install_state;
  109. if (isset($install_state['parameters']['profile'])) {
  110. $pattern = '@' . preg_quote(DIRECTORY_SEPARATOR . $install_state['parameters']['profile'] . '.info.yml') . '$@';
  111. if (preg_match($pattern, $filename)) {
  112. @trigger_error("The install profile $filename only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.", E_USER_DEPRECATED);
  113. }
  114. }
  115. // Move dependencies to install so that if a profile has both
  116. // dependencies and install then dependencies are real.
  117. $parsed_info['install'] = $parsed_info['dependencies'];
  118. $parsed_info['dependencies'] = [];
  119. }
  120. }
  121. return $parsed_info;
  122. }
  123. /**
  124. * Returns an array of keys required to exist in .info.yml file.
  125. *
  126. * @return array
  127. * An array of required keys.
  128. */
  129. protected function getRequiredKeys() {
  130. return ['type', 'name'];
  131. }
  132. /**
  133. * Determines if a constraint is satisfied by earlier versions of Drupal 8.
  134. *
  135. * @param string $constraint
  136. * A core semantic version constraint.
  137. * @param string $version
  138. * A core version.
  139. *
  140. * @return bool
  141. * TRUE if the constraint is satisfied by a core version prior to the
  142. * provided version.
  143. */
  144. protected static function isConstraintSatisfiedByPreviousVersion($constraint, $version) {
  145. static $evaluated_constraints = [];
  146. // Any particular constraint and version combination only needs to be
  147. // evaluated once.
  148. if (!isset($evaluated_constraints[$constraint][$version])) {
  149. $evaluated_constraints[$constraint][$version] = FALSE;
  150. foreach (static::getAllPreviousCoreVersions($version) as $previous_version) {
  151. if (Semver::satisfies($previous_version, $constraint)) {
  152. $evaluated_constraints[$constraint][$version] = TRUE;
  153. // The constraint only has to satisfy one previous version so break
  154. // when the first one is found.
  155. break;
  156. }
  157. }
  158. }
  159. return $evaluated_constraints[$constraint][$version];
  160. }
  161. /**
  162. * Gets all the versions of Drupal 8 before a specific version.
  163. *
  164. * @param string $version
  165. * The version to get versions before.
  166. *
  167. * @return array
  168. * All of the applicable Drupal 8 releases.
  169. */
  170. protected static function getAllPreviousCoreVersions($version) {
  171. static $versions_lists = [];
  172. // Check if list of previous versions for the specified version has already
  173. // been created.
  174. if (empty($versions_lists[$version])) {
  175. // Loop through all minor versions including 8.7.
  176. foreach (range(0, 7) as $minor) {
  177. // The largest patch number in a release was 17 in 8.6.17. Use 27 to
  178. // leave room for future security releases.
  179. foreach (range(0, 27) as $patch) {
  180. $patch_version = "8.$minor.$patch";
  181. if ($patch_version === $version) {
  182. // Reverse the order of the versions so that they will be evaluated
  183. // from the most recent versions first.
  184. $versions_lists[$version] = array_reverse($versions_lists[$version]);
  185. return $versions_lists[$version];
  186. }
  187. if ($patch === 0) {
  188. // If this is a '0' patch release like '8.1.0' first create the
  189. // pre-release versions such as '8.1.0-alpha1' and '8.1.0-rc1'.
  190. foreach (['alpha', 'beta', 'rc'] as $prerelease) {
  191. // The largest prerelease number was in 8.0.0-beta16.
  192. foreach (range(0, 16) as $prerelease_number) {
  193. $versions_lists[$version][] = "$patch_version-$prerelease$prerelease_number";
  194. }
  195. }
  196. }
  197. $versions_lists[$version][] = $patch_version;
  198. }
  199. }
  200. }
  201. return $versions_lists[$version];
  202. }
  203. }