|
@@ -0,0 +1,279 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+/*
|
|
|
+ * This file is part of Composer.
|
|
|
+ *
|
|
|
+ * (c) Nils Adermann <naderman@naderman.de>
|
|
|
+ * Jordi Boggiano <j.boggiano@seld.be>
|
|
|
+ *
|
|
|
+ * For the full copyright and license information, please view the LICENSE
|
|
|
+ * file that was distributed with this source code.
|
|
|
+ */
|
|
|
+
|
|
|
+namespace Composer\Package\Loader;
|
|
|
+
|
|
|
+use Composer\Package;
|
|
|
+use Composer\Package\Version\VersionParser;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @author Jordi Boggiano <j.boggiano@seld.be>
|
|
|
+ */
|
|
|
+class ValidatingArrayLoader implements LoaderInterface
|
|
|
+{
|
|
|
+ private $loader;
|
|
|
+ private $versionParser;
|
|
|
+ private $ignoreErrors;
|
|
|
+ private $errors = array();
|
|
|
+
|
|
|
+ public function __construct(LoaderInterface $loader, $ignoreErrors = true, VersionParser $parser = null)
|
|
|
+ {
|
|
|
+ $this->loader = $loader;
|
|
|
+ $this->ignoreErrors = $ignoreErrors;
|
|
|
+ if (!$parser) {
|
|
|
+ $parser = new VersionParser();
|
|
|
+ }
|
|
|
+ $this->versionParser = $parser;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function load(array $config)
|
|
|
+ {
|
|
|
+ $this->config = $config;
|
|
|
+
|
|
|
+ $this->validateRegex('name', '[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*', true);
|
|
|
+
|
|
|
+ if (!empty($config['version'])) {
|
|
|
+ try {
|
|
|
+ $this->versionParser->normalize($config['version']);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ unset($this->config['version']);
|
|
|
+ $this->errors[] = 'version : invalid value ('.$config['version'].'): '.$e->getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->validateRegex('type', '[a-z0-9-]+');
|
|
|
+ $this->validateString('target-dir');
|
|
|
+ $this->validateArray('extra');
|
|
|
+ $this->validateFlatArray('bin');
|
|
|
+ $this->validateArray('scripts'); // TODO validate event names & listener syntax
|
|
|
+ $this->validateString('description');
|
|
|
+ $this->validateUrl('homepage');
|
|
|
+ $this->validateFlatArray('keywords', '[A-Za-z0-9 -]+');
|
|
|
+
|
|
|
+ if (isset($config['license'])) {
|
|
|
+ if (is_string($config['license'])) {
|
|
|
+ $this->validateRegex('license', '[A-Za-z0-9+. ()-]+');
|
|
|
+ } else {
|
|
|
+ $this->validateFlatArray('license', '[A-Za-z0-9+. ()-]+');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->validateString('time');
|
|
|
+ if (!empty($this->config['time'])) {
|
|
|
+ try {
|
|
|
+ $date = new \DateTime($config['time']);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $this->errors[] = 'time : invalid value ('.$this->config['time'].'): '.$e->getMessage();
|
|
|
+ unset($this->config['time']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->validateArray('authors');
|
|
|
+ if (!empty($this->config['authors'])) {
|
|
|
+ foreach ($this->config['authors'] as $key => $author) {
|
|
|
+ if (isset($author['homepage']) && !$this->filterUrl($author['homepage'])) {
|
|
|
+ $this->errors[] = 'authors.'.$key.'.homepage : invalid value, must be a valid http/https URL';
|
|
|
+ unset($this->config['authors'][$key]['homepage']);
|
|
|
+ }
|
|
|
+ if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) {
|
|
|
+ $this->errors[] = 'authors.'.$key.'.email : invalid value, must be a valid email address';
|
|
|
+ unset($this->config['authors'][$key]['email']);
|
|
|
+ }
|
|
|
+ if (isset($author['name']) && !is_string($author['name'])) {
|
|
|
+ $this->errors[] = 'authors.'.$key.'.name : invalid value, must be a string';
|
|
|
+ unset($this->config['authors'][$key]['name']);
|
|
|
+ }
|
|
|
+ if (isset($author['role']) && !is_string($author['role'])) {
|
|
|
+ $this->errors[] = 'authors.'.$key.'.role : invalid value, must be a string';
|
|
|
+ unset($this->config['authors'][$key]['role']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (empty($this->config['authors'])) {
|
|
|
+ unset($this->config['authors']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->validateArray('support');
|
|
|
+ if (!empty($this->config['support'])) {
|
|
|
+ if (isset($this->config['support']['email']) && !filter_var($this->config['support']['email'], FILTER_VALIDATE_EMAIL)) {
|
|
|
+ $this->errors[] = 'support.email : invalid value, must be a valid email address';
|
|
|
+ unset($this->config['support']['email']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($this->config['support']['irc'])
|
|
|
+ && (!filter_var($this->config['support']['irc'], FILTER_VALIDATE_URL) || !preg_match('{^irc://}iu', $this->config['support']['irc']))
|
|
|
+ ) {
|
|
|
+ $this->errors[] = 'support.irc : invalid value, must be ';
|
|
|
+ unset($this->config['support']['irc']);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (array('issues', 'forum', 'wiki', 'source') as $key) {
|
|
|
+ if (isset($this->config['support'][$key]) && !$this->filterUrl($this->config['support'][$key])) {
|
|
|
+ $this->errors[] = 'support.'.$key.' : invalid value, must be a valid http/https URL';
|
|
|
+ unset($this->config['support'][$key]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (empty($this->config['support'])) {
|
|
|
+ unset($this->config['support']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO validate require/require-dev/replace/provide
|
|
|
+ // TODO validate suggest
|
|
|
+ // TODO validate autoload
|
|
|
+ // TODO validate minimum-stability
|
|
|
+
|
|
|
+ // TODO validate dist
|
|
|
+ // TODO validate source
|
|
|
+
|
|
|
+ // TODO validate repositories
|
|
|
+
|
|
|
+ $this->validateFlatArray('include-path');
|
|
|
+
|
|
|
+ // branch alias validation
|
|
|
+ if (isset($this->config['extra']['branch-alias'])) {
|
|
|
+ if (!is_array($this->config['extra']['branch-alias'])) {
|
|
|
+ $this->errors[] = 'extra.branch-alias : must be an array of versions => aliases';
|
|
|
+ } else {
|
|
|
+ foreach ($this->config['extra']['branch-alias'] as $sourceBranch => $targetBranch) {
|
|
|
+ // ensure it is an alias to a -dev package
|
|
|
+ if ('-dev' !== substr($targetBranch, -4)) {
|
|
|
+ $this->errors[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must end in -dev';
|
|
|
+ unset($this->config['extra']['branch-alias'][$sourceBranch]);
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // normalize without -dev and ensure it's a numeric branch that is parseable
|
|
|
+ $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4));
|
|
|
+ if ('-dev' !== substr($validatedTargetBranch, -4)) {
|
|
|
+ $this->errors[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must be a parseable number like 2.0-dev';
|
|
|
+ unset($this->config['extra']['branch-alias'][$sourceBranch]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->errors && !$this->ignoreErrors) {
|
|
|
+ throw new \Exception(implode("\n", $this->errors));
|
|
|
+ }
|
|
|
+
|
|
|
+ $package = $this->loader->load($this->config);
|
|
|
+ $this->errors = array();
|
|
|
+ unset($this->config);
|
|
|
+
|
|
|
+ return $package;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function validateRegex($property, $regex, $mandatory = false)
|
|
|
+ {
|
|
|
+ if (!$this->validateString($property, $mandatory)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!preg_match('{^'.$regex.'$}u', $this->config[$property])) {
|
|
|
+ $this->errors[] = $property.' : invalid value, must match '.$regex;
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function validateString($property, $mandatory = false)
|
|
|
+ {
|
|
|
+ if (isset($this->config[$property]) && !is_string($this->config[$property])) {
|
|
|
+ $this->errors[] = $property.' : should be a string, '.gettype($this->config[$property]).' given';
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($this->config[$property]) || trim($this->config[$property]) === '') {
|
|
|
+ if ($mandatory) {
|
|
|
+ $this->errors[] = $property.' : must be present';
|
|
|
+ }
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function validateArray($property, $mandatory = false)
|
|
|
+ {
|
|
|
+ if (isset($this->config[$property]) && !is_array($this->config[$property])) {
|
|
|
+ $this->errors[] = $property.' : should be an array, '.gettype($this->config[$property]).' given';
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($this->config[$property]) || !count($this->config[$property])) {
|
|
|
+ if ($mandatory) {
|
|
|
+ $this->errors[] = $property.' : must be present and contain at least one element';
|
|
|
+ }
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function validateFlatArray($property, $regex = null, $mandatory = false)
|
|
|
+ {
|
|
|
+ if (!$this->validateArray($property, $mandatory)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ $pass = true;
|
|
|
+ foreach ($this->config[$property] as $key => $value) {
|
|
|
+ if (!is_string($value) && !is_numeric($value)) {
|
|
|
+ $this->errors[] = $property.'.'.$key.' : must be a string or int, '.gettype($value).' given';
|
|
|
+ unset($this->config[$property][$key]);
|
|
|
+ $pass = false;
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($regex && !preg_match('{^'.$regex.'$}u', $value)) {
|
|
|
+ $this->errors[] = $property.'.'.$key.' : invalid value, must match '.$regex;
|
|
|
+ unset($this->config[$property][$key]);
|
|
|
+ $pass = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $pass;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function validateUrl($property, $mandatory = false)
|
|
|
+ {
|
|
|
+ if (!$this->validateString($property, $mandatory)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$this->filterUrl($this->config[$property])) {
|
|
|
+ $this->errors[] = $property.' : invalid value, must be a valid http/https URL';
|
|
|
+ unset($this->config[$property]);
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function filterUrl($value)
|
|
|
+ {
|
|
|
+ return filter_var($value, FILTER_VALIDATE_URL) && preg_match('{^https?://}iu', $value);
|
|
|
+ }
|
|
|
+}
|