PageRenderTime 607ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Package/Loader/ValidatingArrayLoader.php

https://github.com/pborreli/composer
PHP | 358 lines | 280 code | 58 blank | 20 comment | 70 complexity | bd0ce476a438c6218aa288ccbea5dcc5 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Package\Loader;
  12. use Composer\Package;
  13. use Composer\Package\BasePackage;
  14. use Composer\Package\Version\VersionParser;
  15. /**
  16. * @author Jordi Boggiano <j.boggiano@seld.be>
  17. */
  18. class ValidatingArrayLoader implements LoaderInterface
  19. {
  20. private $loader;
  21. private $versionParser;
  22. private $errors;
  23. private $warnings;
  24. private $config;
  25. private $strictName;
  26. public function __construct(LoaderInterface $loader, $strictName = true, VersionParser $parser = null)
  27. {
  28. $this->loader = $loader;
  29. $this->versionParser = $parser ?: new VersionParser();
  30. $this->strictName = $strictName;
  31. }
  32. public function load(array $config, $class = 'Composer\Package\CompletePackage')
  33. {
  34. $this->errors = array();
  35. $this->warnings = array();
  36. $this->config = $config;
  37. if ($this->strictName) {
  38. $this->validateRegex('name', '[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*', true);
  39. } else {
  40. $this->validateString('name', true);
  41. }
  42. if (!empty($this->config['version'])) {
  43. try {
  44. $this->versionParser->normalize($this->config['version']);
  45. } catch (\Exception $e) {
  46. unset($this->config['version']);
  47. $this->errors[] = 'version : invalid value ('.$this->config['version'].'): '.$e->getMessage();
  48. }
  49. }
  50. $this->validateRegex('type', '[A-Za-z0-9-]+');
  51. $this->validateString('target-dir');
  52. $this->validateArray('extra');
  53. $this->validateFlatArray('bin');
  54. $this->validateArray('scripts'); // TODO validate event names & listener syntax
  55. $this->validateString('description');
  56. $this->validateUrl('homepage');
  57. $this->validateFlatArray('keywords', '[A-Za-z0-9 ._-]+');
  58. if (isset($this->config['license'])) {
  59. if (is_string($this->config['license'])) {
  60. $this->validateRegex('license', '[A-Za-z0-9+. ()-]+');
  61. } else {
  62. $this->validateFlatArray('license', '[A-Za-z0-9+. ()-]+');
  63. }
  64. }
  65. $this->validateString('time');
  66. if (!empty($this->config['time'])) {
  67. try {
  68. $date = new \DateTime($this->config['time'], new \DateTimeZone('UTC'));
  69. } catch (\Exception $e) {
  70. $this->errors[] = 'time : invalid value ('.$this->config['time'].'): '.$e->getMessage();
  71. unset($this->config['time']);
  72. }
  73. }
  74. if ($this->validateArray('authors') && !empty($this->config['authors'])) {
  75. foreach ($this->config['authors'] as $key => $author) {
  76. if (!is_array($author)) {
  77. $this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given';
  78. unset($this->config['authors'][$key]);
  79. continue;
  80. }
  81. foreach (array('homepage', 'email', 'name', 'role') as $authorData) {
  82. if (isset($author[$authorData]) && !is_string($author[$authorData])) {
  83. $this->errors[] = 'authors.'.$key.'.'.$authorData.' : invalid value, must be a string';
  84. unset($this->config['authors'][$key][$authorData]);
  85. }
  86. }
  87. if (isset($author['homepage']) && !$this->filterUrl($author['homepage'])) {
  88. $this->warnings[] = 'authors.'.$key.'.homepage : invalid value ('.$author['homepage'].'), must be an http/https URL';
  89. unset($this->config['authors'][$key]['homepage']);
  90. }
  91. if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) {
  92. $this->warnings[] = 'authors.'.$key.'.email : invalid value ('.$author['email'].'), must be a valid email address';
  93. unset($this->config['authors'][$key]['email']);
  94. }
  95. if (empty($this->config['authors'][$key])) {
  96. unset($this->config['authors'][$key]);
  97. }
  98. }
  99. if (empty($this->config['authors'])) {
  100. unset($this->config['authors']);
  101. }
  102. }
  103. if ($this->validateArray('support') && !empty($this->config['support'])) {
  104. foreach (array('issues', 'forum', 'wiki', 'source', 'email', 'irc') as $key) {
  105. if (isset($this->config['support'][$key]) && !is_string($this->config['support'][$key])) {
  106. $this->errors[] = 'support.'.$key.' : invalid value, must be a string';
  107. unset($this->config['support'][$key]);
  108. }
  109. }
  110. if (isset($this->config['support']['email']) && !filter_var($this->config['support']['email'], FILTER_VALIDATE_EMAIL)) {
  111. $this->warnings[] = 'support.email : invalid value ('.$this->config['support']['email'].'), must be a valid email address';
  112. unset($this->config['support']['email']);
  113. }
  114. if (isset($this->config['support']['irc']) && !$this->filterUrl($this->config['support']['irc'], array('irc'))) {
  115. $this->warnings[] = 'support.irc : invalid value ('.$this->config['support']['irc'].'), must be a irc://<server>/<channel> URL';
  116. unset($this->config['support']['irc']);
  117. }
  118. foreach (array('issues', 'forum', 'wiki', 'source') as $key) {
  119. if (isset($this->config['support'][$key]) && !$this->filterUrl($this->config['support'][$key])) {
  120. $this->warnings[] = 'support.'.$key.' : invalid value ('.$this->config['support'][$key].'), must be an http/https URL';
  121. unset($this->config['support'][$key]);
  122. }
  123. }
  124. if (empty($this->config['support'])) {
  125. unset($this->config['support']);
  126. }
  127. }
  128. foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) {
  129. if ($this->validateArray($linkType) && isset($this->config[$linkType])) {
  130. foreach ($this->config[$linkType] as $package => $constraint) {
  131. if (!preg_match('{^[A-Za-z0-9_./-]+$}', $package)) {
  132. $this->warnings[] = $linkType.'.'.$package.' : invalid key, package names must be strings containing only [A-Za-z0-9_./-]';
  133. }
  134. if (!is_string($constraint)) {
  135. $this->errors[] = $linkType.'.'.$package.' : invalid value, must be a string containing a version constraint';
  136. unset($this->config[$linkType][$package]);
  137. } elseif ('self.version' !== $constraint) {
  138. try {
  139. $this->versionParser->parseConstraints($constraint);
  140. } catch (\Exception $e) {
  141. $this->errors[] = $linkType.'.'.$package.' : invalid version constraint ('.$e->getMessage().')';
  142. unset($this->config[$linkType][$package]);
  143. }
  144. }
  145. }
  146. }
  147. }
  148. if ($this->validateArray('suggest') && !empty($this->config['suggest'])) {
  149. foreach ($this->config['suggest'] as $package => $description) {
  150. if (!is_string($description)) {
  151. $this->errors[] = 'suggest.'.$package.' : invalid value, must be a string describing why the package is suggested';
  152. unset($this->config['suggest'][$package]);
  153. }
  154. }
  155. }
  156. if ($this->validateString('minimum-stability') && !empty($this->config['minimum-stability'])) {
  157. if (!isset(BasePackage::$stabilities[$this->config['minimum-stability']])) {
  158. $this->errors[] = 'minimum-stability : invalid value ('.$this->config['minimum-stability'].'), must be one of '.implode(', ', array_keys(BasePackage::$stabilities));
  159. unset($this->config['minimum-stability']);
  160. }
  161. }
  162. // TODO validate autoload
  163. // TODO validate dist
  164. // TODO validate source
  165. // TODO validate repositories
  166. // TODO validate package repositories' packages using this recursively
  167. $this->validateFlatArray('include-path');
  168. // branch alias validation
  169. if (isset($this->config['extra']['branch-alias'])) {
  170. if (!is_array($this->config['extra']['branch-alias'])) {
  171. $this->errors[] = 'extra.branch-alias : must be an array of versions => aliases';
  172. } else {
  173. foreach ($this->config['extra']['branch-alias'] as $sourceBranch => $targetBranch) {
  174. // ensure it is an alias to a -dev package
  175. if ('-dev' !== substr($targetBranch, -4)) {
  176. $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must end in -dev';
  177. unset($this->config['extra']['branch-alias'][$sourceBranch]);
  178. continue;
  179. }
  180. // normalize without -dev and ensure it's a numeric branch that is parseable
  181. $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4));
  182. if ('-dev' !== substr($validatedTargetBranch, -4)) {
  183. $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must be a parseable number like 2.0-dev';
  184. unset($this->config['extra']['branch-alias'][$sourceBranch]);
  185. }
  186. }
  187. }
  188. }
  189. if ($this->errors) {
  190. throw new InvalidPackageException($this->errors, $this->warnings, $config);
  191. }
  192. $package = $this->loader->load($this->config, $class);
  193. $this->config = null;
  194. return $package;
  195. }
  196. public function getWarnings()
  197. {
  198. return $this->warnings;
  199. }
  200. public function getErrors()
  201. {
  202. return $this->errors;
  203. }
  204. private function validateRegex($property, $regex, $mandatory = false)
  205. {
  206. if (!$this->validateString($property, $mandatory)) {
  207. return false;
  208. }
  209. if (!preg_match('{^'.$regex.'$}u', $this->config[$property])) {
  210. $message = $property.' : invalid value ('.$this->config[$property].'), must match '.$regex;
  211. if ($mandatory) {
  212. $this->errors[] = $message;
  213. } else {
  214. $this->warnings[] = $message;
  215. }
  216. unset($this->config[$property]);
  217. return false;
  218. }
  219. return true;
  220. }
  221. private function validateString($property, $mandatory = false)
  222. {
  223. if (isset($this->config[$property]) && !is_string($this->config[$property])) {
  224. $this->errors[] = $property.' : should be a string, '.gettype($this->config[$property]).' given';
  225. unset($this->config[$property]);
  226. return false;
  227. }
  228. if (!isset($this->config[$property]) || trim($this->config[$property]) === '') {
  229. if ($mandatory) {
  230. $this->errors[] = $property.' : must be present';
  231. }
  232. unset($this->config[$property]);
  233. return false;
  234. }
  235. return true;
  236. }
  237. private function validateArray($property, $mandatory = false)
  238. {
  239. if (isset($this->config[$property]) && !is_array($this->config[$property])) {
  240. $this->errors[] = $property.' : should be an array, '.gettype($this->config[$property]).' given';
  241. unset($this->config[$property]);
  242. return false;
  243. }
  244. if (!isset($this->config[$property]) || !count($this->config[$property])) {
  245. if ($mandatory) {
  246. $this->errors[] = $property.' : must be present and contain at least one element';
  247. }
  248. unset($this->config[$property]);
  249. return false;
  250. }
  251. return true;
  252. }
  253. private function validateFlatArray($property, $regex = null, $mandatory = false)
  254. {
  255. if (!$this->validateArray($property, $mandatory)) {
  256. return false;
  257. }
  258. $pass = true;
  259. foreach ($this->config[$property] as $key => $value) {
  260. if (!is_string($value) && !is_numeric($value)) {
  261. $this->errors[] = $property.'.'.$key.' : must be a string or int, '.gettype($value).' given';
  262. unset($this->config[$property][$key]);
  263. $pass = false;
  264. continue;
  265. }
  266. if ($regex && !preg_match('{^'.$regex.'$}u', $value)) {
  267. $this->warnings[] = $property.'.'.$key.' : invalid value ('.$value.'), must match '.$regex;
  268. unset($this->config[$property][$key]);
  269. $pass = false;
  270. }
  271. }
  272. return $pass;
  273. }
  274. private function validateUrl($property, $mandatory = false)
  275. {
  276. if (!$this->validateString($property, $mandatory)) {
  277. return false;
  278. }
  279. if (!$this->filterUrl($this->config[$property])) {
  280. $this->warnings[] = $property.' : invalid value ('.$this->config[$property].'), must be an http/https URL';
  281. unset($this->config[$property]);
  282. return false;
  283. }
  284. return true;
  285. }
  286. private function filterUrl($value, array $schemes = array('http', 'https'))
  287. {
  288. if ($value === '') {
  289. return true;
  290. }
  291. $bits = parse_url($value);
  292. if (empty($bits['scheme']) || empty($bits['host'])) {
  293. return false;
  294. }
  295. if (!in_array($bits['scheme'], $schemes, true)) {
  296. return false;
  297. }
  298. return true;
  299. }
  300. }