1<?php
2
3/*
4 * This file is part of Composer.
5 *
6 * (c) Nils Adermann <naderman@naderman.de>
7 *     Jordi Boggiano <j.boggiano@seld.be>
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12
13namespace Composer\DependencyResolver;
14
15use Composer\Package\BasePackage;
16use Composer\Package\AliasPackage;
17use Composer\Package\Version\VersionParser;
18use Composer\Semver\Constraint\ConstraintInterface;
19use Composer\Semver\Constraint\Constraint;
20use Composer\Semver\Constraint\EmptyConstraint;
21use Composer\Repository\RepositoryInterface;
22use Composer\Repository\CompositeRepository;
23use Composer\Repository\ComposerRepository;
24use Composer\Repository\InstalledRepositoryInterface;
25use Composer\Repository\PlatformRepository;
26use Composer\Package\PackageInterface;
27
28/**
29 * A package pool contains repositories that provide packages.
30 *
31 * @author Nils Adermann <naderman@naderman.de>
32 * @author Jordi Boggiano <j.boggiano@seld.be>
33 */
34class Pool implements \Countable
35{
36    const MATCH_NAME = -1;
37    const MATCH_NONE = 0;
38    const MATCH = 1;
39    const MATCH_PROVIDE = 2;
40    const MATCH_REPLACE = 3;
41    const MATCH_FILTERED = 4;
42
43    protected $repositories = array();
44    protected $providerRepos = array();
45    protected $packages = array();
46    protected $packageByName = array();
47    protected $packageByExactName = array();
48    protected $acceptableStabilities;
49    protected $stabilityFlags;
50    protected $versionParser;
51    protected $providerCache = array();
52    protected $filterRequires;
53    protected $whitelist = null;
54    protected $id = 1;
55
56    public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array())
57    {
58        $this->versionParser = new VersionParser;
59        $this->acceptableStabilities = array();
60        foreach (BasePackage::$stabilities as $stability => $value) {
61            if ($value <= BasePackage::$stabilities[$minimumStability]) {
62                $this->acceptableStabilities[$stability] = $value;
63            }
64        }
65        $this->stabilityFlags = $stabilityFlags;
66        $this->filterRequires = $filterRequires;
67        foreach ($filterRequires as $name => $constraint) {
68            if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) {
69                unset($this->filterRequires[$name]);
70            }
71        }
72    }
73
74    public function setWhitelist($whitelist)
75    {
76        $this->whitelist = $whitelist;
77        $this->providerCache = array();
78    }
79
80    /**
81     * Adds a repository and its packages to this package pool
82     *
83     * @param RepositoryInterface $repo        A package repository
84     * @param array               $rootAliases
85     */
86    public function addRepository(RepositoryInterface $repo, $rootAliases = array())
87    {
88        if ($repo instanceof CompositeRepository) {
89            $repos = $repo->getRepositories();
90        } else {
91            $repos = array($repo);
92        }
93
94        foreach ($repos as $repo) {
95            $this->repositories[] = $repo;
96
97            $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface;
98
99            if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
100                $this->providerRepos[] = $repo;
101                $repo->setRootAliases($rootAliases);
102                $repo->resetPackageIds();
103            } else {
104                foreach ($repo->getPackages() as $package) {
105                    $names = $package->getNames();
106                    $stability = $package->getStability();
107                    if ($exempt || $this->isPackageAcceptable($names, $stability)) {
108                        $package->setId($this->id++);
109                        $this->packages[] = $package;
110                        $this->packageByExactName[$package->getName()][$package->id] = $package;
111
112                        foreach ($names as $provided) {
113                            $this->packageByName[$provided][] = $package;
114                        }
115
116                        // handle root package aliases
117                        $name = $package->getName();
118                        if (isset($rootAliases[$name][$package->getVersion()])) {
119                            $alias = $rootAliases[$name][$package->getVersion()];
120                            if ($package instanceof AliasPackage) {
121                                $package = $package->getAliasOf();
122                            }
123                            $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']);
124                            $aliasPackage->setRootPackageAlias(true);
125                            $aliasPackage->setId($this->id++);
126
127                            $package->getRepository()->addPackage($aliasPackage);
128                            $this->packages[] = $aliasPackage;
129                            $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage;
130
131                            foreach ($aliasPackage->getNames() as $name) {
132                                $this->packageByName[$name][] = $aliasPackage;
133                            }
134                        }
135                    }
136                }
137            }
138        }
139    }
140
141    public function getPriority(RepositoryInterface $repo)
142    {
143        $priority = array_search($repo, $this->repositories, true);
144
145        if (false === $priority) {
146            throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool.");
147        }
148
149        return -$priority;
150    }
151
152    /**
153     * Retrieves the package object for a given package id.
154     *
155     * @param  int              $id
156     * @return PackageInterface
157     */
158    public function packageById($id)
159    {
160        return $this->packages[$id - 1];
161    }
162
163    /**
164     * Returns how many packages have been loaded into the pool
165     */
166    public function count()
167    {
168        return count($this->packages);
169    }
170
171    /**
172     * Searches all packages providing the given package name and match the constraint
173     *
174     * @param  string              $name          The package name to be searched for
175     * @param  ConstraintInterface $constraint    A constraint that all returned
176     *                                            packages must match or null to return all
177     * @param  bool                $mustMatchName Whether the name of returned packages
178     *                                            must match the given name
179     * @param  bool                $bypassFilters If enabled, filterRequires and stability matching is ignored
180     * @return PackageInterface[]  A set of packages
181     */
182    public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false)
183    {
184        if ($bypassFilters) {
185            return $this->computeWhatProvides($name, $constraint, $mustMatchName, true);
186        }
187
188        $key = ((int) $mustMatchName).$constraint;
189        if (isset($this->providerCache[$name][$key])) {
190            return $this->providerCache[$name][$key];
191        }
192
193        return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters);
194    }
195
196    /**
197     * @see whatProvides
198     */
199    private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false)
200    {
201        $candidates = array();
202
203        foreach ($this->providerRepos as $repo) {
204            foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) {
205                $candidates[] = $candidate;
206                if ($candidate->id < 1) {
207                    $candidate->setId($this->id++);
208                    $this->packages[$this->id - 2] = $candidate;
209                }
210            }
211        }
212
213        if ($mustMatchName) {
214            $candidates = array_filter($candidates, function ($candidate) use ($name) {
215                return $candidate->getName() == $name;
216            });
217            if (isset($this->packageByExactName[$name])) {
218                $candidates = array_merge($candidates, $this->packageByExactName[$name]);
219            }
220        } elseif (isset($this->packageByName[$name])) {
221            $candidates = array_merge($candidates, $this->packageByName[$name]);
222        }
223
224        $matches = $provideMatches = array();
225        $nameMatch = false;
226
227        foreach ($candidates as $candidate) {
228            $aliasOfCandidate = null;
229
230            // alias packages are not white listed, make sure that the package
231            // being aliased is white listed
232            if ($candidate instanceof AliasPackage) {
233                $aliasOfCandidate = $candidate->getAliasOf();
234            }
235
236            if ($this->whitelist !== null && !$bypassFilters && (
237                (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) ||
238                ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id]))
239            )) {
240                continue;
241            }
242            switch ($this->match($candidate, $name, $constraint, $bypassFilters)) {
243                case self::MATCH_NONE:
244                    break;
245
246                case self::MATCH_NAME:
247                    $nameMatch = true;
248                    break;
249
250                case self::MATCH:
251                    $nameMatch = true;
252                    $matches[] = $candidate;
253                    break;
254
255                case self::MATCH_PROVIDE:
256                    $provideMatches[] = $candidate;
257                    break;
258
259                case self::MATCH_REPLACE:
260                    $matches[] = $candidate;
261                    break;
262
263                case self::MATCH_FILTERED:
264                    break;
265
266                default:
267                    throw new \UnexpectedValueException('Unexpected match type');
268            }
269        }
270
271        // if a package with the required name exists, we ignore providers
272        if ($nameMatch) {
273            return $matches;
274        }
275
276        return array_merge($matches, $provideMatches);
277    }
278
279    public function literalToPackage($literal)
280    {
281        $packageId = abs($literal);
282
283        return $this->packageById($packageId);
284    }
285
286    public function literalToPrettyString($literal, $installedMap)
287    {
288        $package = $this->literalToPackage($literal);
289
290        if (isset($installedMap[$package->id])) {
291            $prefix = ($literal > 0 ? 'keep' : 'remove');
292        } else {
293            $prefix = ($literal > 0 ? 'install' : 'don\'t install');
294        }
295
296        return $prefix.' '.$package->getPrettyString();
297    }
298
299    public function isPackageAcceptable($name, $stability)
300    {
301        foreach ((array) $name as $n) {
302            // allow if package matches the global stability requirement and has no exception
303            if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) {
304                return true;
305            }
306
307            // allow if package matches the package-specific stability flag
308            if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) {
309                return true;
310            }
311        }
312
313        return false;
314    }
315
316    /**
317     * Checks if the package matches the given constraint directly or through
318     * provided or replaced packages
319     *
320     * @param  array|PackageInterface $candidate
321     * @param  string                 $name       Name of the package to be matched
322     * @param  ConstraintInterface    $constraint The constraint to verify
323     * @return int                    One of the MATCH* constants of this class or 0 if there is no match
324     */
325    private function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters)
326    {
327        $candidateName = $candidate->getName();
328        $candidateVersion = $candidate->getVersion();
329        $isDev = $candidate->getStability() === 'dev';
330        $isAlias = $candidate instanceof AliasPackage;
331
332        if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) {
333            $requireFilter = $this->filterRequires[$name];
334        } else {
335            $requireFilter = new EmptyConstraint;
336        }
337
338        if ($candidateName === $name) {
339            $pkgConstraint = new Constraint('==', $candidateVersion);
340
341            if ($constraint === null || $constraint->matches($pkgConstraint)) {
342                return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED;
343            }
344
345            return self::MATCH_NAME;
346        }
347
348        $provides = $candidate->getProvides();
349        $replaces = $candidate->getReplaces();
350
351        // aliases create multiple replaces/provides for one target so they can not use the shortcut below
352        if (isset($replaces[0]) || isset($provides[0])) {
353            foreach ($provides as $link) {
354                if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
355                    return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
356                }
357            }
358
359            foreach ($replaces as $link) {
360                if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) {
361                    return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
362                }
363            }
364
365            return self::MATCH_NONE;
366        }
367
368        if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) {
369            return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED;
370        }
371
372        if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) {
373            return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED;
374        }
375
376        return self::MATCH_NONE;
377    }
378}
379