* Jordi Boggiano * * 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\BasePackage; use Composer\Package\Version\VersionParser; /** * @author Jordi Boggiano */ class ValidatingArrayLoader implements LoaderInterface { private $loader; private $versionParser; private $ignoreErrors; private $errors; private $config; public function __construct(LoaderInterface $loader, $ignoreErrors = true, VersionParser $parser = null) { $this->loader = $loader; $this->ignoreErrors = $ignoreErrors; $this->versionParser = $parser ?: new VersionParser(); } public function load(array $config, $class = 'Composer\Package\CompletePackage') { $this->errors = array(); $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($this->config['version'])) { try { $this->versionParser->normalize($this->config['version']); } catch (\Exception $e) { unset($this->config['version']); $this->errors[] = 'version : invalid value ('.$this->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($this->config['license'])) { if (is_string($this->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($this->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 (!is_array($author)) { $this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given'; unset($this->config['authors'][$key]); continue; } 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'][$key])) { unset($this->config['authors'][$key]); } } 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']); } } foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { if (isset($this->config[$linkType])) { foreach ($this->config[$linkType] as $package => $constraint) { if (!is_string($constraint)) { $this->errors[] = $linkType.'.'.$package.' : invalid value, must be a string containing a version constraint'; unset($this->config[$linkType][$package]); } elseif ('self.version' !== $constraint) { try { $this->versionParser->parseConstraints($constraint); } catch (\Exception $e) { $this->errors[] = $linkType.'.'.$package.' : invalid version constraint ('.$e->getMessage().')'; unset($this->config[$linkType][$package]); } } } } } $this->validateArray('suggest'); if (!empty($this->config['suggest'])) { foreach ($this->config['suggest'] as $package => $description) { if (!is_string($description)) { $this->errors[] = 'suggest.'.$package.' : invalid value, must be a string describing why the package is suggested'; unset($this->config['suggest'][$package]); } } } $this->validateString('minimum-stability'); if (!empty($this->config['minimum-stability'])) { if (!isset(BasePackage::$stabilities[$this->config['minimum-stability']])) { $this->errors[] = 'minimum-stability : invalid value, must be one of '.implode(', ', array_keys(BasePackage::$stabilities)); unset($this->config['minimum-stability']); } } // TODO validate autoload // TODO validate dist // TODO validate source // TODO validate repositories // TODO validate package repositories' packages using this recursively $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 InvalidPackageException($this->errors, $config); } $package = $this->loader->load($this->config, $class); $this->errors = array(); $this->config = null; 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); } }