Browse Source

Merge branch 'master' into 2.0

Jordi Boggiano 5 years ago
parent
commit
6c4357a7ed
39 changed files with 491 additions and 135 deletions
  1. 2 0
      .travis.yml
  2. 4 0
      doc/03-cli.md
  3. 13 0
      doc/articles/handling-private-packages-with-satis.md
  4. 6 0
      src/Composer/Command/CheckPlatformReqsCommand.php
  5. 17 5
      src/Composer/Command/InitCommand.php
  6. 15 6
      src/Composer/Command/RequireCommand.php
  7. 3 0
      src/Composer/Console/Application.php
  8. 6 2
      src/Composer/DependencyResolver/RuleSetGenerator.php
  9. 0 2
      src/Composer/Downloader/PerforceDownloader.php
  10. 3 1
      src/Composer/EventDispatcher/EventDispatcher.php
  11. 8 0
      src/Composer/IO/BaseIO.php
  12. 3 2
      src/Composer/Json/JsonManipulator.php
  13. 4 64
      src/Composer/Repository/ArtifactRepository.php
  14. 4 0
      src/Composer/Repository/Vcs/GitHubDriver.php
  15. 14 14
      src/Composer/Repository/Vcs/GitLabDriver.php
  16. 44 0
      src/Composer/Script/Event.php
  17. 3 0
      src/Composer/Util/ErrorHandler.php
  18. 10 1
      src/Composer/Util/GitLab.php
  19. 0 2
      src/Composer/Util/Perforce.php
  20. 4 0
      src/Composer/Util/RemoteFilesystem.php
  21. 0 2
      src/Composer/Util/TlsHelper.php
  22. 3 0
      src/Composer/Util/Url.php
  23. 108 0
      src/Composer/Util/Zip.php
  24. 4 4
      tests/Composer/Test/AllFunctionalTest.php
  25. 0 7
      tests/Composer/Test/DependencyResolver/RuleSetTest.php
  26. 16 0
      tests/Composer/Test/Json/JsonManipulatorTest.php
  27. 0 1
      tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
  28. 0 9
      tests/Composer/Test/Repository/Vcs/FossilDriverTest.php
  29. 0 9
      tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
  30. 80 0
      tests/Composer/Test/Script/EventTest.php
  31. BIN
      tests/Composer/Test/Util/Fixtures/Zip/empty.zip
  32. BIN
      tests/Composer/Test/Util/Fixtures/Zip/folder.zip
  33. BIN
      tests/Composer/Test/Util/Fixtures/Zip/multiple.zip
  34. BIN
      tests/Composer/Test/Util/Fixtures/Zip/nojson.zip
  35. BIN
      tests/Composer/Test/Util/Fixtures/Zip/root.zip
  36. BIN
      tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip
  37. 0 3
      tests/Composer/Test/Util/GitHubTest.php
  38. 0 1
      tests/Composer/Test/Util/GitLabTest.php
  39. 117 0
      tests/Composer/Test/Util/ZipTest.php

+ 2 - 0
.travis.yml

@@ -30,9 +30,11 @@ matrix:
       env:
         - deps=high
     - php: nightly
+    - php: 7.4snapshot
   fast_finish: true
   allow_failures:
     - php: nightly
+    - php: 7.4snapshot
 
 before_install:
   # disable xdebug if available

+ 4 - 0
doc/03-cli.md

@@ -259,6 +259,10 @@ match the platform requirements of the installed packages. This can be used
 to verify that a production server has all the extensions needed to run a
 project after installing it for example.
 
+Unlike update/install, this command will ignore config.platform settings and
+check the real platform packages so you can be certain you have the required
+platform dependencies.
+
 ## global
 
 The global command allows you to run other commands like `install`, `remove`, `require`

+ 13 - 0
doc/articles/handling-private-packages-with-satis.md

@@ -112,6 +112,19 @@ Note that this will still need to pull and scan all of your VCS repositories
 because any VCS repository might contain (on any branch) one of the selected
 packages.
 
+If you want to scan only the selected package and not all VCS repositories you need
+to declare a *name* for all your package (this only work on VCS repositories type) :
+
+```json
+{
+  "repositories": [
+    { "name": "company/privaterepo", "type": "vcs", "url": "https://github.com/mycompany/privaterepo" },
+    { "name": "private/repo", "type": "vcs", "url": "http://svn.example.org/private/repo" },
+    { "name": "mycompany/privaterepo2", "type": "vcs", "url": "https://github.com/mycompany/privaterepo2" }
+  ]
+}
+```
+
 If you want to scan only a single repository and update all packages found in
 it, pass the VCS repository URL as an optional argument:
 

+ 6 - 0
src/Composer/Command/CheckPlatformReqsCommand.php

@@ -34,6 +34,8 @@ class CheckPlatformReqsCommand extends BaseCommand
                 <<<EOT
 Checks that your PHP and extensions versions match the platform requirements of the installed packages.
 
+Unlike update/install, this command will ignore config.platform settings and check the real platform packages so you can be certain you have the required platform dependencies.
+
 <info>php composer.phar check-platform-reqs</info>
 
 EOT
@@ -49,6 +51,10 @@ EOT
             $dependencies = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
         } else {
             $dependencies = $composer->getRepositoryManager()->getLocalRepository()->getPackages();
+            // fallback to lockfile if installed repo is empty
+            if (!$dependencies) {
+                $dependencies = $composer->getLocker()->getLockedRepository(true)->getPackages();
+            }
             $requires += $composer->getPackage()->getDevRequires();
         }
         foreach ($requires as $require => $link) {

+ 17 - 5
src/Composer/Command/InitCommand.php

@@ -168,13 +168,25 @@ EOT
         if ($repositories) {
             $config = Factory::createConfig($io);
             $repos = array(new PlatformRepository);
+            $createDefaultPackagistRepo = true;
             foreach ($repositories as $repo) {
-                $repos[] = RepositoryFactory::fromString($io, $config, $repo);
+                $repoConfig = RepositoryFactory::configFromString($io, $config, $repo);
+                if (
+                    (isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false))
+                    || (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false))
+                ) {
+                    $createDefaultPackagistRepo = false;
+                    continue;
+                }
+                $repos[] = RepositoryFactory::createRepo($io, $config, $repoConfig);
+            }
+
+            if ($createDefaultPackagistRepo) {
+                $repos[] = RepositoryFactory::createRepo($io, $config, array(
+                    'type' => 'composer',
+                    'url' => 'https://repo.packagist.org',
+                ));
             }
-            $repos[] = RepositoryFactory::createRepo($io, $config, array(
-                'type' => 'composer',
-                'url' => 'https://repo.packagist.org',
-            ));
 
             $this->repos = new CompositeRepository($repos);
             unset($repos, $config, $repositories);

+ 15 - 6
src/Composer/Command/RequireCommand.php

@@ -26,6 +26,7 @@ use Composer\Plugin\PluginEvents;
 use Composer\Repository\CompositeRepository;
 use Composer\Repository\PlatformRepository;
 use Composer\IO\IOInterface;
+use Composer\Util\Silencer;
 
 /**
  * @author Jérémy Romey <jeremy@free-agent.fr>
@@ -103,11 +104,6 @@ EOT
 
             return 1;
         }
-        if (!is_writable($this->file)) {
-            $io->writeError('<error>'.$this->file.' is not writable.</error>');
-
-            return 1;
-        }
 
         if (filesize($this->file) === 0) {
             file_put_contents($this->file, "{\n}\n");
@@ -116,6 +112,14 @@ EOT
         $this->json = new JsonFile($this->file);
         $this->composerBackup = file_get_contents($this->json->getPath());
 
+        // check for writability by writing to the file as is_writable can not be trusted on network-mounts
+        // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
+        if (!is_writable($this->file) && !Silencer::call('file_put_contents', $this->file, $this->composerBackup)) {
+            $io->writeError('<error>'.$this->file.' is not writable.</error>');
+
+            return 1;
+        }
+
         $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $repos = $composer->getRepositoryManager()->getRepositories();
 
@@ -141,7 +145,12 @@ EOT
 
         // validate requirements format
         $versionParser = new VersionParser();
-        foreach ($requirements as $constraint) {
+        foreach ($requirements as $package => $constraint) {
+            if (strtolower($package) === $composer->getPackage()->getName()) {
+                $io->writeError(sprintf('<error>Root package \'%s\' cannot require itself in its composer.json</error>', $package));
+
+                return 1;
+            }
             $versionParser->parseConstraints($constraint);
         }
 

+ 3 - 0
src/Composer/Console/Application.php

@@ -379,6 +379,9 @@ class Application extends BaseApplication
     public function resetComposer()
     {
         $this->composer = null;
+        if ($this->getIO() && method_exists($this->getIO(), 'resetAuthentications')) {
+            $this->getIO()->resetAuthentications();
+        }
     }
 
     /**

+ 6 - 2
src/Composer/DependencyResolver/RuleSetGenerator.php

@@ -197,7 +197,7 @@ class RuleSetGenerator
         }
     }
 
-    protected function addConflictRules()
+    protected function addConflictRules($ignorePlatformReqs = false)
     {
         /** @var PackageInterface $package */
         foreach ($this->addedPackages as $package) {
@@ -206,6 +206,10 @@ class RuleSetGenerator
                     continue;
                 }
 
+                if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
+                    continue;
+                }
+
                 /** @var PackageInterface $possibleConflict */
                 foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
                     $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
@@ -304,7 +308,7 @@ class RuleSetGenerator
 
         $this->addRulesForJobs($ignorePlatformReqs);
 
-        $this->addConflictRules();
+        $this->addConflictRules($ignorePlatformReqs);
 
         // Remove references to packages
         $this->addedPackages = $this->addedPackagesByNames = null;

+ 0 - 2
src/Composer/Downloader/PerforceDownloader.php

@@ -87,8 +87,6 @@ class PerforceDownloader extends VcsDownloader
     public function getLocalChanges(PackageInterface $package, $path)
     {
         $this->io->writeError('Perforce driver does not check for local changes before overriding', true);
-
-        return null;
     }
 
     /**

+ 3 - 1
src/Composer/EventDispatcher/EventDispatcher.php

@@ -200,7 +200,9 @@ class EventDispatcher
 
                     try {
                         /** @var InstallerEvent $event */
-                        $return = $this->dispatch($scriptName, new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags));
+                        $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
+                        $scriptEvent->setOriginatingEvent($event);
+                        $return = $this->dispatch($scriptName, $scriptEvent);
                     } catch (ScriptExecutionException $e) {
                         $this->io->writeError(sprintf('<error>Script %s was called via %s</error>', $callable, $event->getName()), true, IOInterface::QUIET);
                         throw $e;

+ 8 - 0
src/Composer/IO/BaseIO.php

@@ -28,6 +28,14 @@ abstract class BaseIO implements IOInterface
         return $this->authentications;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function resetAuthentications()
+    {
+        $this->authentications = array();
+    }
+
     /**
      * {@inheritDoc}
      */

+ 3 - 2
src/Composer/Json/JsonManipulator.php

@@ -326,9 +326,10 @@ class JsonManipulator
         }
 
         // try and find a match for the subkey
-        if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) {
+        $keyRegex = str_replace('/', '\\\\?/', preg_quote($name));
+        if ($this->pregMatch('{"'.$keyRegex.'"\s*:}i', $children)) {
             // find best match for the value of "name"
-            if (preg_match_all('{'.self::$DEFINES.'"'.preg_quote($name).'"\s*:\s*(?:(?&json))}x', $children, $matches)) {
+            if (preg_match_all('{'.self::$DEFINES.'"'.$keyRegex.'"\s*:\s*(?:(?&json))}x', $children, $matches)) {
                 $bestMatch = '';
                 foreach ($matches[0] as $match) {
                     if (strlen($bestMatch) < strlen($match)) {

+ 4 - 64
src/Composer/Repository/ArtifactRepository.php

@@ -16,6 +16,7 @@ use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\Loader\LoaderInterface;
+use Composer\Util\Zip;
 
 /**
  * @author Serge Smertin <serg.smertin@gmail.com>
@@ -80,76 +81,15 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
         }
     }
 
-    /**
-     * Find a file by name, returning the one that has the shortest path.
-     *
-     * @param \ZipArchive $zip
-     * @param string $filename
-     * @return bool|int
-     */
-    private function locateFile(\ZipArchive $zip, $filename)
-    {
-        $indexOfShortestMatch = false;
-        $lengthOfShortestMatch = -1;
-
-        for ($i = 0; $i < $zip->numFiles; $i++) {
-            $stat = $zip->statIndex($i);
-            if (strcmp(basename($stat['name']), $filename) === 0) {
-                $directoryName = dirname($stat['name']);
-                if ($directoryName == '.') {
-                    //if composer.json is in root directory
-                    //it has to be the one to use.
-                    return $i;
-                }
-
-                if (strpos($directoryName, '\\') !== false ||
-                   strpos($directoryName, '/') !== false) {
-                    //composer.json files below first directory are rejected
-                    continue;
-                }
-
-                $length = strlen($stat['name']);
-                if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) {
-                    //Check it's not a directory.
-                    $contents = $zip->getFromIndex($i);
-                    if ($contents !== false) {
-                        $indexOfShortestMatch = $i;
-                        $lengthOfShortestMatch = $length;
-                    }
-                }
-            }
-        }
-
-        return $indexOfShortestMatch;
-    }
-
     private function getComposerInformation(\SplFileInfo $file)
     {
-        $zip = new \ZipArchive();
-        if ($zip->open($file->getPathname()) !== true) {
-            return false;
-        }
-
-        if (0 == $zip->numFiles) {
-            $zip->close();
+        $json = Zip::getComposerJson($file->getPathname());
 
+        if (null === $json) {
             return false;
         }
 
-        $foundFileIndex = $this->locateFile($zip, 'composer.json');
-        if (false === $foundFileIndex) {
-            $zip->close();
-
-            return false;
-        }
-
-        $configurationFileName = $zip->getNameIndex($foundFileIndex);
-        $zip->close();
-
-        $composerFile = "zip://{$file->getPathname()}#$configurationFileName";
-        $json = file_get_contents($composerFile);
-
-        $package = JsonFile::parseJson($json, $composerFile);
+        $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json');
         $package['dist'] = array(
             'type' => 'zip',
             'url' => strtr($file->getPathname(), '\\', '/'),

+ 4 - 0
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -308,6 +308,10 @@ class GitHubDriver extends VcsDriver
      */
     protected function generateSshUrl()
     {
+        if (false !== strpos($this->originUrl, ':')) {
+            return 'ssh://git@' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git';
+        }
+
         return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git';
     }
 

+ 14 - 14
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -68,9 +68,9 @@ class GitLabDriver extends VcsDriver
     private $isPrivate = true;
 
     /**
-     * @var int port number
+     * @var bool true if the origin has a port number or a path component in it
      */
-    protected $portNumber;
+    private $hasNonstandardOrigin = false;
 
     const URL_REGEX = '#^(?:(?P<scheme>https?)://(?P<domain>.+?)(?::(?P<port>[0-9]+))?/|git@(?P<domain2>[^:]+):)(?P<parts>.+)/(?P<repo>[^/]+?)(?:\.git|/)?$#';
 
@@ -95,11 +95,10 @@ class GitLabDriver extends VcsDriver
             ? $match['scheme']
             : (isset($this->repoConfig['secure-http']) && $this->repoConfig['secure-http'] === false ? 'http' : 'https')
         ;
-        $this->originUrl = $this->determineOrigin($configuredDomains, $guessedDomain, $urlParts);
+        $this->originUrl = $this->determineOrigin($configuredDomains, $guessedDomain, $urlParts, $match['port']);
 
-        if (!empty($match['port']) && true === is_numeric($match['port'])) {
-            // If it is an HTTP based URL, and it has a port
-            $this->portNumber = (int) $match['port'];
+        if (false !== strpos($this->originUrl, ':') || false !== strpos($this->originUrl, '/')) {
+            $this->hasNonstandardOrigin = true;
         }
 
         $this->namespace = implode('/', $urlParts);
@@ -260,10 +259,7 @@ class GitLabDriver extends VcsDriver
      */
     public function getApiUrl()
     {
-        $domainName = $this->originUrl;
-        $portNumber = (true === is_numeric($this->portNumber)) ? sprintf(':%s', $this->portNumber) : '';
-
-        return $this->scheme.'://'.$domainName.$portNumber.'/api/v4/projects/'.$this->urlEncodeAll($this->namespace).'%2F'.$this->urlEncodeAll($this->repository);
+        return $this->scheme.'://'.$this->originUrl.'/api/v4/projects/'.$this->urlEncodeAll($this->namespace).'%2F'.$this->urlEncodeAll($this->repository);
     }
 
     /**
@@ -362,6 +358,10 @@ class GitLabDriver extends VcsDriver
      */
     protected function generateSshUrl()
     {
+        if ($this->hasNonstandardOrigin) {
+            return 'ssh://git@'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository.'.git';
+        }
+
         return 'git@' . $this->originUrl . ':'.$this->namespace.'/'.$this->repository.'.git';
     }
 
@@ -464,7 +464,7 @@ class GitLabDriver extends VcsDriver
         $guessedDomain = !empty($match['domain']) ? $match['domain'] : $match['domain2'];
         $urlParts = explode('/', $match['parts']);
 
-        if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts)) {
+        if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts, $match['port'])) {
             return false;
         }
 
@@ -495,16 +495,16 @@ class GitLabDriver extends VcsDriver
      * @param  array       $urlParts
      * @return bool|string
      */
-    private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts)
+    private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts, $portNumber)
     {
-        if (in_array($guessedDomain, $configuredDomains)) {
+        if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array($guessedDomain.':'.$portNumber, $configuredDomains))) {
             return $guessedDomain;
         }
 
         while (null !== ($part = array_shift($urlParts))) {
             $guessedDomain .= '/' . $part;
 
-            if (in_array($guessedDomain, $configuredDomains)) {
+            if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array(preg_replace('{/}', ':'.$portNumber.'/', $guessedDomain, 1), $configuredDomains))) {
                 return $guessedDomain;
             }
         }

+ 44 - 0
src/Composer/Script/Event.php

@@ -39,6 +39,11 @@ class Event extends BaseEvent
      */
     private $devMode;
 
+    /**
+     * @var BaseEvent
+     */
+    private $originatingEvent;
+
     /**
      * Constructor.
      *
@@ -55,6 +60,7 @@ class Event extends BaseEvent
         $this->composer = $composer;
         $this->io = $io;
         $this->devMode = $devMode;
+        $this->originatingEvent = null;
     }
 
     /**
@@ -86,4 +92,42 @@ class Event extends BaseEvent
     {
         return $this->devMode;
     }
+
+    /**
+     * Set the originating event.
+     *
+     * @return \Composer\EventDispatcher\Event|null
+     */
+    public function getOriginatingEvent()
+    {
+        return $this->originatingEvent;
+    }
+
+    /**
+     * Set the originating event.
+     *
+     * @param \Composer\EventDispatcher\Event $event
+     * @return $this
+     */
+    public function setOriginatingEvent(BaseEvent $event)
+    {
+        $this->originatingEvent = $this->calculateOriginatingEvent($event);
+
+        return $this;
+    }
+
+    /**
+     * Returns the upper-most event in chain.
+     *
+     * @param \Composer\EventDispatcher\Event $event
+     * @return \Composer\EventDispatcher\Event
+     */
+    private function calculateOriginatingEvent(BaseEvent $event)
+    {
+        if ($event instanceof Event && $event->getOriginatingEvent()) {
+            return $this->calculateOriginatingEvent($event->getOriginatingEvent());
+        }
+
+        return $event;
+    }
 }

+ 3 - 0
src/Composer/Util/ErrorHandler.php

@@ -33,6 +33,7 @@ class ErrorHandler
      *
      * @static
      * @throws \ErrorException
+     * @return bool
      */
     public static function handle($level, $message, $file, $line)
     {
@@ -63,6 +64,8 @@ class ErrorHandler
                 }, array_slice(debug_backtrace(), 2))));
             }
         }
+
+        return true;
     }
 
     /**

+ 10 - 1
src/Composer/Util/GitLab.php

@@ -57,7 +57,10 @@ class GitLab
      */
     public function authorizeOAuth($originUrl)
     {
-        if (!in_array($originUrl, $this->config->get('gitlab-domains'), true)) {
+        // before composer 1.9, origin URLs had no port number in them
+        $bcOriginUrl = preg_replace('{:\d+}', '', $originUrl);
+
+        if (!in_array($originUrl, $this->config->get('gitlab-domains'), true) && !in_array($bcOriginUrl, $this->config->get('gitlab-domains'), true)) {
             return false;
         }
 
@@ -77,6 +80,12 @@ class GitLab
             return true;
         }
 
+        if (isset($authTokens[$bcOriginUrl])) {
+            $this->io->setAuthentication($originUrl, $authTokens[$bcOriginUrl], 'private-token');
+
+            return true;
+        }
+
         return false;
     }
 

+ 0 - 2
src/Composer/Util/Perforce.php

@@ -363,8 +363,6 @@ class Perforce
         while ($line !== false) {
             $line = fgets($pipe);
         }
-
-        return;
     }
 
     public function windowsLogin($password)

+ 4 - 0
src/Composer/Util/RemoteFilesystem.php

@@ -286,6 +286,8 @@ class RemoteFilesystem
                 $errorMessage .= "\n";
             }
             $errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
+
+            return true;
         });
         try {
             $result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header);
@@ -459,6 +461,8 @@ class RemoteFilesystem
                     $errorMessage .= "\n";
                 }
                 $errorMessage .= preg_replace('{^file_put_contents\(.*?\): }', '', $msg);
+
+                return true;
             });
             $result = (bool) file_put_contents($fileName, $result);
             restore_error_handler();

+ 0 - 2
src/Composer/Util/TlsHelper.php

@@ -19,8 +19,6 @@ use Composer\CaBundle\CaBundle;
  */
 final class TlsHelper
 {
-    private static $useOpensslParse;
-
     /**
      * Match hostname against a certificate.
      *

+ 3 - 0
src/Composer/Util/Url.php

@@ -70,6 +70,9 @@ class Url
         }
 
         $origin = (string) parse_url($url, PHP_URL_HOST);
+        if ($port = parse_url($url, PHP_URL_PORT)) {
+            $origin .= ':'.$port;
+        }
 
         if (strpos($origin, '.github.com') === (strlen($origin) - 11)) {
             return 'github.com';

+ 108 - 0
src/Composer/Util/Zip.php

@@ -0,0 +1,108 @@
+<?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\Util;
+
+/**
+ * @author Andreas Schempp <andreas.schempp@terminal42.ch>
+ */
+class Zip
+{
+    /**
+     * Gets content of the root composer.json inside a ZIP archive.
+     *
+     * @param string $pathToZip
+     * @param string $filename
+     *
+     * @return string|null
+     */
+    public static function getComposerJson($pathToZip)
+    {
+        if (!extension_loaded('zip')) {
+            throw new \RuntimeException('The Zip Util requires PHP\'s zip extension');
+        }
+
+        $zip = new \ZipArchive();
+        if ($zip->open($pathToZip) !== true) {
+            return null;
+        }
+
+        if (0 == $zip->numFiles) {
+            $zip->close();
+
+            return null;
+        }
+
+        $foundFileIndex = self::locateFile($zip, 'composer.json');
+        if (false === $foundFileIndex) {
+            $zip->close();
+
+            return null;
+        }
+
+        $content = null;
+        $configurationFileName = $zip->getNameIndex($foundFileIndex);
+        $stream = $zip->getStream($configurationFileName);
+
+        if (false !== $stream) {
+            $content = stream_get_contents($stream);
+        }
+
+        $zip->close();
+
+        return $content;
+    }
+
+    /**
+     * Find a file by name, returning the one that has the shortest path.
+     *
+     * @param \ZipArchive $zip
+     * @param string      $filename
+     *
+     * @return bool|int
+     */
+    private static function locateFile(\ZipArchive $zip, $filename)
+    {
+        $indexOfShortestMatch = false;
+        $lengthOfShortestMatch = -1;
+
+        for ($i = 0; $i < $zip->numFiles; $i++) {
+            $stat = $zip->statIndex($i);
+            if (strcmp(basename($stat['name']), $filename) === 0) {
+                $directoryName = dirname($stat['name']);
+                if ($directoryName === '.') {
+                    //if composer.json is in root directory
+                    //it has to be the one to use.
+                    return $i;
+                }
+
+                if (strpos($directoryName, '\\') !== false ||
+                    strpos($directoryName, '/') !== false) {
+                    //composer.json files below first directory are rejected
+                    continue;
+                }
+
+                $length = strlen($stat['name']);
+                if ($indexOfShortestMatch === false || $length < $lengthOfShortestMatch) {
+                    //Check it's not a directory.
+                    $contents = $zip->getFromIndex($i);
+                    if ($contents !== false) {
+                        $indexOfShortestMatch = $i;
+                        $lengthOfShortestMatch = $length;
+                    }
+                }
+            }
+        }
+
+        return $indexOfShortestMatch;
+    }
+}

+ 4 - 4
tests/Composer/Test/AllFunctionalTest.php

@@ -162,18 +162,18 @@ class AllFunctionalTest extends TestCase
             }
         };
 
-        for ($i = 0, $c = count($tokens); $i < $c; $i++) {
-            if ('' === $tokens[$i] && null === $section) {
+        foreach ($tokens as $token) {
+            if ('' === $token && null === $section) {
                 continue;
             }
 
             // Handle section headers.
             if (null === $section) {
-                $section = $tokens[$i];
+                $section = $token;
                 continue;
             }
 
-            $sectionData = $tokens[$i];
+            $sectionData = $token;
 
             // Allow sections to validate, or modify their section data.
             switch ($section) {

+ 0 - 7
tests/Composer/Test/DependencyResolver/RuleSetTest.php

@@ -152,11 +152,4 @@ class RuleSetTest extends TestCase
 
         $this->assertContains('JOB     : Install command rule (install foo 2.1)', $ruleSet->getPrettyString($pool));
     }
-
-    private function getRuleMock()
-    {
-        return $this->getMockBuilder('Composer\DependencyResolver\Rule')
-            ->disableOriginalConstructor()
-            ->getMock();
-    }
 }

+ 16 - 0
tests/Composer/Test/Json/JsonManipulatorTest.php

@@ -1448,6 +1448,22 @@ class JsonManipulatorTest extends TestCase
     "repositories": {
     }
 }
+',
+            ),
+            'works on simple ones escaped slash' => array(
+                '{
+    "repositories": {
+        "foo\/bar": {
+            "bar": "baz"
+        }
+    }
+}',
+                'foo/bar',
+                true,
+                '{
+    "repositories": {
+    }
+}
 ',
             ),
             'works on simple ones middle' => array(

+ 0 - 1
tests/Composer/Test/Package/Loader/ArrayLoaderTest.php

@@ -148,7 +148,6 @@ class ArrayLoaderTest extends TestCase
     {
         $package = $this->loader->load($config);
         $dumper = new ArrayDumper;
-        $expectedConfig = $config;
         $expectedConfig = $this->fixConfigWhenLoadConfigIsFalse($config);
         $this->assertEquals($expectedConfig, $dumper->dump($package));
     }

+ 0 - 9
tests/Composer/Test/Repository/Vcs/FossilDriverTest.php

@@ -40,15 +40,6 @@ class FossilDriverTest extends TestCase
         $fs->removeDirectory($this->home);
     }
 
-    private function getCmd($cmd)
-    {
-        if (Platform::isWindows()) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
-    }
-
     public static function supportProvider()
     {
         return array(

+ 0 - 9
tests/Composer/Test/Repository/Vcs/SvnDriverTest.php

@@ -71,15 +71,6 @@ class SvnDriverTest extends TestCase
         $svn->initialize();
     }
 
-    private function getCmd($cmd)
-    {
-        if (Platform::isWindows()) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
-    }
-
     public static function supportProvider()
     {
         return array(

+ 80 - 0
tests/Composer/Test/Script/EventTest.php

@@ -0,0 +1,80 @@
+<?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\Test\Script;
+
+use Composer\Composer;
+use Composer\Config;
+use Composer\Script\Event;
+use Composer\Test\TestCase;
+
+class EventTest extends TestCase
+{
+    public function testEventSetsOriginatingEvent()
+    {
+        $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
+        $composer = $this->createComposerInstance();
+
+        $originatingEvent = new \Composer\EventDispatcher\Event('originatingEvent');
+
+        $scriptEvent = new Event('test', $composer, $io, true);
+
+        $this->assertNull(
+            $scriptEvent->getOriginatingEvent(),
+            'originatingEvent is initialized as null'
+        );
+
+        $scriptEvent->setOriginatingEvent($originatingEvent);
+
+        $this->assertSame(
+            $originatingEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD return test event'
+        );
+    }
+
+    public function testEventCalculatesNestedOriginatingEvent()
+    {
+        $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
+        $composer = $this->createComposerInstance();
+
+        $originatingEvent = new \Composer\EventDispatcher\Event('upperOriginatingEvent');
+        $intermediateEvent = new Event('intermediate', $composer, $io, true);
+        $intermediateEvent->setOriginatingEvent($originatingEvent);
+
+        $scriptEvent = new Event('test', $composer, $io, true);
+        $scriptEvent->setOriginatingEvent($intermediateEvent);
+
+        $this->assertNotSame(
+            $intermediateEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD NOT return intermediate events'
+        );
+
+        $this->assertSame(
+            $originatingEvent,
+            $scriptEvent->getOriginatingEvent(),
+            'getOriginatingEvent() SHOULD return upper-most event'
+        );
+    }
+
+    private function createComposerInstance()
+    {
+        $composer = new Composer;
+        $config = new Config;
+        $composer->setConfig($config);
+        $package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock();
+        $composer->setPackage($package);
+
+        return $composer;
+    }
+}

BIN
tests/Composer/Test/Util/Fixtures/Zip/empty.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/folder.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/multiple.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/nojson.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/root.zip


BIN
tests/Composer/Test/Util/Fixtures/Zip/subfolder.zip


+ 0 - 3
tests/Composer/Test/Util/GitHubTest.php

@@ -24,12 +24,9 @@ use RecursiveIteratorIterator;
  */
 class GitHubTest extends TestCase
 {
-    private $username = 'username';
     private $password = 'password';
-    private $authcode = 'authcode';
     private $message = 'mymessage';
     private $origin = 'github.com';
-    private $token = 'githubtoken';
 
     public function testUsernamePasswordAuthenticationFlow()
     {

+ 0 - 1
tests/Composer/Test/Util/GitLabTest.php

@@ -24,7 +24,6 @@ class GitLabTest extends TestCase
 {
     private $username = 'username';
     private $password = 'password';
-    private $authcode = 'authcode';
     private $message = 'mymessage';
     private $origin = 'gitlab.com';
     private $token = 'gitlabtoken';

+ 117 - 0
tests/Composer/Test/Util/ZipTest.php

@@ -0,0 +1,117 @@
+<?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\Test\Util;
+
+use Composer\Util\Zip;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @author Andreas Schempp <andreas.schempp@terminal42.ch>
+ */
+class ZipTest extends TestCase
+{
+    public function testThrowsExceptionIfZipExcentionIsNotLoaded()
+    {
+        if (extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is loaded.');
+        }
+
+        $this->setExpectedException('\RuntimeException', 'The Zip Util requires PHP\'s zip extension');
+
+        Zip::getComposerJson('');
+    }
+
+    public function testReturnsNullifTheZipIsNotFound()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/invalid.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheZipIsEmpty()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/empty.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheZipHasNoComposerJson()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/nojson.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsNullIfTheComposerJsonIsInASubSubfolder()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/subfolder.zip');
+
+        $this->assertNull($result);
+    }
+
+    public function testReturnsComposerJsonInZipRoot()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/root.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+
+    public function testReturnsComposerJsonInFirstFolder()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+
+    public function testReturnsRootComposerJsonAndSkipsSubfolders()
+    {
+        if (!extension_loaded('zip')) {
+            $this->markTestSkipped('The PHP zip extension is not loaded.');
+            return;
+        }
+
+        $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/multiple.zip');
+
+        $this->assertEquals("{\n    \"name\": \"foo/bar\"\n}\n", $result);
+    }
+}