Skip to content

Loading…

Verify package name(s) when using an update whitelist #1112

Closed
wants to merge 12 commits into from

5 participants

@thewilkybarkid

I found out about the composer update nothing command/hack at Symfony Live London. This seems like it is occasionally useful, but is only possible as the package name list isn't verified. This means that misspelling a package name would have the same effect, without the user necessarily realising that they've made a mistake.

This verifies the requested name(s) against the list of packages, returning an error when one isn't found

So:

php composer.phar update monolog/monolo

Would result in:

Package monolog/monolo not known

The nothing hack is still permitted (as long as it's on its own, not in a list of package names).

@stof

I think this forbids adding a new dependency in your project and doing a partial update to get it without updating the other dependencies, as the new package will not be available in the local repository.
And this case is the most common one where a partial update is needed IMO

@thewilkybarkid

That is now be fixed (along with dev packages) so you can add, update and remove individual packages.

@Seldaek Seldaek commented on an outdated diff
src/Composer/Installer.php
((5 lines not shown))
*/
public function setUpdateWhitelist(array $packages)
{
+ if (count($packages) == 0) {
+ return $this;
+ }
+
+ if (count($packages) > 1 || $packages[0] !== 'nothing') {
+ $knownPackages = array();
+ foreach ($this->repositoryManager->getLocalRepository()->getPackages() as $localPackage) {
+ $knownPackages[] = strtolower($localPackage->getName());
@Seldaek Composer member
Seldaek added a note

To be perfectly safe I think this should be getNames and not getName, that way if a package is installed because it provides something you require, it will still match .

Ah, hadn't spotted that. Just walked through the code and realised strtolower() is redundant too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Seldaek Seldaek commented on an outdated diff
src/Composer/Command/UpdateCommand.php
((9 lines not shown))
- ->setRunScripts(!$input->getOption('no-scripts'))
- ->setUpdate(true)
- ->setUpdateWhitelist($input->getArgument('packages'))
- ;
+ try {
+ $install
+ ->setDryRun($input->getOption('dry-run'))
+ ->setVerbose($input->getOption('verbose'))
+ ->setPreferSource($input->getOption('prefer-source'))
+ ->setDevMode($input->getOption('dev'))
+ ->setRunScripts(!$input->getOption('no-scripts'))
+ ->setUpdate(true)
+ ->setUpdateWhitelist($input->getArgument('packages'))
+ ;
+ } catch (\Exception $e) {
+ $io->write('<error>' . $e->getMessage() . '</error>');
@Seldaek Composer member
Seldaek added a note

This isn't a great idea, if you throw a very specific exception and catch that ok, but as such it will basically prevent us from seeing the backtrace when a real exception occurs.

Fair point; there aren't any Composer exceptions yet, ok to create something like Composer\Exception\UnknownPackageNameException which extends \UnexpectedValueException? Couldn't see a non-exception method to halt the flow here (without rather ugly refactoring).

@Seldaek Composer member
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof stof commented on an outdated diff
src/Composer/Command/UpdateCommand.php
((11 lines not shown))
- ->setUpdateWhitelist($input->getArgument('packages'))
- ;
+ try {
+ $install
+ ->setDryRun($input->getOption('dry-run'))
+ ->setVerbose($input->getOption('verbose'))
+ ->setPreferSource($input->getOption('prefer-source'))
+ ->setDevMode($input->getOption('dev'))
+ ->setRunScripts(!$input->getOption('no-scripts'))
+ ->setUpdate(true)
+ ->setUpdateWhitelist($input->getArgument('packages'))
+ ;
+ } catch (UnknownPackageException $e) {
+ $io->write('<error>' . $e->getMessage() . '</error>');
+
+ return false;
@stof
stof added a note

A command should return an integer (the exit code), not a boolean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof stof commented on an outdated diff
src/Composer/Installer.php
((5 lines not shown))
*/
public function setUpdateWhitelist(array $packages)
{
+ if (count($packages) == 0) {
@stof
stof added a note

Please use a strict comparison

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof

You should add some tests for this.

@thewilkybarkid

I've been able to create a test that passes (first of a few), can I just check that it's how you would expect it?

I've created a Fixtures/installer/update-whitelist-unknown-package.test file which contains:

[...]
--RUN--
update not/known
--EXPECT-OUTPUT--
<error>Package not/known not known</error>

--EXPECT--

Then inside InstallerTest::testIntegration() I have to catch the exception and output the error:

// line 203
$application->get('update')->setCode(
    function ($input, $output) use ($installer, $io) {
        try {
            $installer
                ->setDevMode($input->getOption('dev'))
                ->setUpdate(true)
                ->setUpdateWhitelist($input->getArgument('packages'));

            return $installer->run() ? 0 : 1;
        } catch (\Composer\Exception\UnknownPackageException $e) {
            $io->write('<error>' . $e->getMessage() . '</error>');

            return 0;
        }
    }
);

I've had to return 0 rather than the usual 1 to pass one of the assertions. Could --EXPECT-EXIT-CODE-- (which defaults to 0) be added like in the functional tests? That would make expected-error testing a little nicer.

@pborreli pborreli commented on an outdated diff
src/Composer/Installer.php
((20 lines not shown))
+ if ($this->devMode) {
+ foreach ($this->repositoryManager->getLocalDevRepository()->getPackages() as $localPackage) {
+ $knownPackages = array_merge($knownPackages, $localPackage->getNames());
+ }
+ foreach ($this->package->getDevRequires() as $requiredPackage) {
+ $knownPackages[] = $requiredPackage->getTarget();
+ }
+ }
+
+ foreach ($packages as $package) {
+ if (!in_array(strtolower($package), $knownPackages)) {
+ throw new UnknownPackageException('Package ' . $package . ' not known');
+ }
+ }
+ }
+
$this->updateWhitelist = array_flip(array_map('strtolower', $packages));

is it possible to make the array_map('strtolower', $packages)before line 754 so you can remove the strtolower($package)on line 772 ?

Only downside is that it would lose the case for the error message: I think such messages should ideally contain exactly what you typed, which includes the case used. (Though I'm not that fussed about it!)

well i was thinking more like that :

$lowercasedPackages = array_map('strtolower', $packages);

then

foreach ($lowercasedPackages as $key => $package) {
    if (!in_array($package, $knownPackages)) {
        throw new UnknownPackageException('Package ' . $packages[$key] . ' not known');
    }   
}

it might look like a micro-optimization but i always see in callgraph that strtolower is often too much called

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@thewilkybarkid

Does that test style look ok, or you would prefer it a different way?

@stloyd stloyd commented on the diff
src/Composer/Installer.php
((8 lines not shown))
*/
public function setUpdateWhitelist(array $packages)
{
- $this->updateWhitelist = array_flip(array_map('strtolower', $packages));
+ if (count($packages) === 0) {
@stloyd
stloyd added a note
if (count($packages) === 0 || (isset($packages[0]) && strtolower($packages[0]) === 'nothing')) {

With such you could remove the if below as well, as the array_map() and move strtolower() into just last loop (with change of $lowercasePackages to $packages).

@stof
stof added a note

@stloyd changing only the condition here would not be equivalent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof stof commented on an outdated diff
src/Composer/Installer.php
((8 lines not shown))
*/
public function setUpdateWhitelist(array $packages)
{
- $this->updateWhitelist = array_flip(array_map('strtolower', $packages));
+ if (count($packages) === 0) {
+ return $this;
@stof
stof added a note

this is not equivalent to the previous code. You should reset the update whitelist to an empty array here, otherwise resetting it is not possible anymore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof stof commented on the diff
src/Composer/Exception/UnknownPackageException.php
@@ -0,0 +1,24 @@
+<?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.
+*/
@stof
stof added a note

Wrong indentation here (the * should be aligned with the opening one)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
View
4 doc/03-cli.md
@@ -70,6 +70,10 @@ If you just want to update a few packages and not all, you can list them as such
$ php composer.phar update vendor/package vendor/package2
+To rewrite the lock file without updating any packages:
+
+ $ php composer.phar update nothing
+
### Options
* **--prefer-source:** Install packages from `source` when available.
View
2 src/Composer/Command/RequireCommand.php
@@ -100,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
->setPreferSource($input->getOption('prefer-source'))
->setDevMode($input->getOption('dev'))
->setUpdate(true)
- ->setUpdateWhitelist($requirements);
+ ->setUpdateWhitelist(array_keys($requirements));
;
return $install->run() ? 0 : 1;
View
25 src/Composer/Command/UpdateCommand.php
@@ -12,6 +12,7 @@
namespace Composer\Command;
+use Composer\Exception\UnknownPackageException;
use Composer\Installer;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -59,15 +60,21 @@ protected function execute(InputInterface $input, OutputInterface $output)
$io = $this->getIO();
$install = Installer::create($io, $composer);
- $install
- ->setDryRun($input->getOption('dry-run'))
- ->setVerbose($input->getOption('verbose'))
- ->setPreferSource($input->getOption('prefer-source'))
- ->setDevMode($input->getOption('dev'))
- ->setRunScripts(!$input->getOption('no-scripts'))
- ->setUpdate(true)
- ->setUpdateWhitelist($input->getArgument('packages'))
- ;
+ try {
+ $install
+ ->setDryRun($input->getOption('dry-run'))
+ ->setVerbose($input->getOption('verbose'))
+ ->setPreferSource($input->getOption('prefer-source'))
+ ->setDevMode($input->getOption('dev'))
+ ->setRunScripts(!$input->getOption('no-scripts'))
+ ->setUpdate(true)
+ ->setUpdateWhitelist($input->getArgument('packages'))
+ ;
+ } catch (UnknownPackageException $e) {
+ $io->write('<error>' . $e->getMessage() . '</error>');
+
+ return 1;
+ }
if ($input->getOption('no-custom-installers')) {
$install->disableCustomInstallers();
View
24 src/Composer/Exception/UnknownPackageException.php
@@ -0,0 +1,24 @@
+<?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.
+*/
@stof
stof added a note

Wrong indentation here (the * should be aligned with the opening one)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+namespace Composer\Exception;
+
+/**
+ * Unknown package exception.
+ *
+ * Used when a package isn't found in a list of valid packages.
+ *
+ * @author Chris Wilkinson <chriswilkinson84@gmail.com>
+ */
+class UnknownPackageException extends \UnexpectedValueException
+{
+}
View
37 src/Composer/Installer.php
@@ -20,6 +20,7 @@
use Composer\DependencyResolver\Solver;
use Composer\DependencyResolver\SolverProblemsException;
use Composer\Downloader\DownloadManager;
+use Composer\Exception\UnknownPackageException;
use Composer\Installer\InstallationManager;
use Composer\Config;
use Composer\Installer\NoopInstaller;
@@ -740,12 +741,44 @@ public function setVerbose($verbose = true)
* restrict the update operation to a few packages, all other packages
* that are already installed will be kept at their current version
*
- * @param array $packages
+ * @param array $packages Array of package names
* @return Installer
+ * @throws UnknownPackageException If a package name is not known
*/
public function setUpdateWhitelist(array $packages)
{
- $this->updateWhitelist = array_flip(array_map('strtolower', $packages));
+ if (count($packages) === 0) {
@stloyd
stloyd added a note
if (count($packages) === 0 || (isset($packages[0]) && strtolower($packages[0]) === 'nothing')) {

With such you could remove the if below as well, as the array_map() and move strtolower() into just last loop (with change of $lowercasePackages to $packages).

@stof
stof added a note

@stloyd changing only the condition here would not be equivalent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ $this->updateWhitelist = array();
+ return $this;
+ }
+
+ $lowercasePackages = array_map('strtolower', $packages);
+
+ if (count($packages) > 1 || $packages[0] !== 'nothing') {
+ $knownPackages = array();
+ foreach ($this->repositoryManager->getLocalRepository()->getPackages() as $localPackage) {
+ $knownPackages = array_merge($knownPackages, $localPackage->getNames());
+ }
+ foreach ($this->package->getRequires() as $requiredPackage) {
+ $knownPackages[] = $requiredPackage->getTarget();
+ }
+ if ($this->devMode) {
+ foreach ($this->repositoryManager->getLocalDevRepository()->getPackages() as $localPackage) {
+ $knownPackages = array_merge($knownPackages, $localPackage->getNames());
+ }
+ foreach ($this->package->getDevRequires() as $requiredPackage) {
+ $knownPackages[] = $requiredPackage->getTarget();
+ }
+ }
+
+ foreach ($lowercasePackages as $key => $package) {
+ if (!in_array($package, $knownPackages)) {
+ throw new UnknownPackageException('Package ' . $packages[$key] . ' not known');
+ }
+ }
+ }
+
+ $this->updateWhitelist = array_flip($lowercasePackages);
return $this;
}
Something went wrong with that request. Please try again.