99
1010use Composer \Composer ;
1111use Composer \DependencyResolver \Operation \OperationInterface ;
12+ use Composer \DependencyResolver \Request ;
1213use Composer \EventDispatcher \EventSubscriberInterface ;
1314use Composer \Installer ;
1415use Composer \Installer \PackageEvent ;
1516use Composer \IO \IOInterface ;
17+ use Composer \Plugin \PluginEvents ;
1618use Composer \Plugin \PluginInterface ;
19+ use Composer \Plugin \PrePoolCreateEvent ;
1720use Composer \Repository \ComposerRepository ;
21+ use Composer \Repository \FilterRepository ;
1822use Composer \Repository \RepositoryInterface ;
1923use Composer \Package \PackageInterface ;
2024use Exception ;
@@ -41,6 +45,11 @@ class Plugin implements PluginInterface, EventSubscriberInterface
4145 */
4246 private $ versionSelector ;
4347
48+ /**
49+ * @var array
50+ */
51+ private $ nonFixedPackages ;
52+
4453 /**#@+
4554 * Constant for VBE ALLOW LIST
4655 */
@@ -99,10 +108,81 @@ public function uninstall(Composer $composer, IOInterface $io)
99108 */
100109 public static function getSubscribedEvents (): array
101110 {
102- return [
111+ $ events = [
103112 Installer \PackageEvents::PRE_PACKAGE_INSTALL => 'packageUpdate ' ,
104113 Installer \PackageEvents::PRE_PACKAGE_UPDATE => 'packageUpdate '
105114 ];
115+
116+ if ((int )explode ('. ' , Composer::VERSION )[0 ] === 2 ) {
117+ $ events [PluginEvents::PRE_POOL_CREATE ] = 'prePoolCreate ' ;
118+ }
119+
120+ return $ events ;
121+ }
122+
123+ /**
124+ * Get all package installations that use non-fixed version constraints (IE: 2.4.*, ^2.4, etc.)
125+ * this needs to be done for Composer V1 installs since prePoolCreate event doesn't exist in V1
126+ *
127+ * @param Request $request
128+ * @return array
129+ */
130+ private function getNonFixedConstraintList (Request $ request ): array
131+ {
132+ if (!$ this ->nonFixedPackages ) {
133+ $ constraintList = [];
134+ foreach ($ request ->getJobs () as $ job ) {
135+ if ($ job ['cmd ' ] === 'install ' &&
136+ (strpbrk ($ job ['constraint ' ]->getPrettyString (), "*^-~ " ) ||
137+ preg_match ('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)} ' , $ job ['constraint ' ]->getPrettyString ()
138+ )
139+ )
140+ ) {
141+ $ constraintList [$ job ['packageName ' ]] = true ;
142+ }
143+ }
144+ $ this ->nonFixedPackages = $ constraintList ;
145+ }
146+ return $ this ->nonFixedPackages ;
147+ }
148+
149+ /**
150+ * Event listener for PrePoolCreate event that is used for composer V2
151+ *
152+ * @param PrePoolCreateEvent $event
153+ */
154+ public function prePoolCreate (PrePoolCreateEvent $ event ): void
155+ {
156+ if (!$ this ->nonFixedPackages ) {
157+ $ constraintList = [];
158+
159+ /**
160+ * get all packages that are in the composer.json under require section, this will be the only time
161+ * we will be able to get constraints for packages in the require section as this request data isn't
162+ * shared in the installer event on composer v2
163+ */
164+ foreach ($ event ->getRequest ()->getRequires () as $ name => $ constraint ) {
165+ $ prettyString = $ constraint ->getPrettyString ();
166+ $ multiConstraint = preg_match ('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)} ' , $ prettyString );
167+ if (strpbrk ($ prettyString , "*^-~ " ) || $ multiConstraint ){
168+ $ constraintList [$ name ] = true ;
169+ }
170+ }
171+
172+ /**
173+ * get all sub packages that are now requirements for new packages to install and store their constraints.
174+ */
175+ foreach ($ event ->getPackages () as $ package ) {
176+ foreach ($ package ->getRequires () as $ name => $ constraint ) {
177+ $ prettyConstraint = $ constraint ->getPrettyConstraint ();
178+ $ multiConstraint = preg_match ('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)} ' , $ prettyConstraint );
179+ if (strpbrk ($ prettyConstraint , "*^-~ " )|| $ multiConstraint )
180+ $ constraintList [$ name ] = true ;
181+ }
182+ }
183+
184+ $ this ->nonFixedPackages = $ constraintList ;
185+ }
106186 }
107187
108188 /**
@@ -130,34 +210,44 @@ public function packageUpdate(PackageEvent $event): void
130210 list ($ namespace , $ project ) = explode ("/ " , $ packageName );
131211 $ isPackageVBE = in_array ($ namespace , self ::VBE_ALLOW_LIST , true );
132212
133- if (!$ isPackageVBE ) {
213+ if ((int )explode ('. ' , Composer::VERSION )[0 ] === 1 ) {
214+ $ this ->getNonFixedConstraintList ($ event ->getRequest ());
215+ }
134216
217+ if (!$ isPackageVBE ) {
135218 foreach ($ this ->composer ->getRepositoryManager ()->getRepositories () as $ repository ) {
136-
219+ $ found = $ this -> versionSelector -> findBestCandidate ( $ this -> composer , $ packageName , $ repository );
137220 /** @var RepositoryInterface $repository */
138221 if ($ repository instanceof ComposerRepository) {
139- $ found = $ this ->versionSelector ->findBestCandidate ($ this ->composer , $ packageName , $ repository );
140222 $ repoUrl = $ repository ->getRepoConfig ()['url ' ];
141223
142- if ($ found ) {
143- if (strpos ($ repoUrl , self ::URL_REPO_PACKAGIST ) !== false ) {
144- $ publicRepoVersion = $ found ->getFullPrettyVersion ();
145- } else {
146- $ currentPrivateRepoVersion = $ found ->getFullPrettyVersion ();
147- //private repo version should hold highest version of package
148- if (empty ($ privateRepoVersion ) || version_compare ($ currentPrivateRepoVersion , $ privateRepoVersion , '> ' )) {
149- $ privateRepoVersion = $ currentPrivateRepoVersion ;
150- $ privateRepoUrl = $ repoUrl ;
151- }
224+ } else if ($ repository instanceof FilterRepository) {
225+ $ repoUrl = $ repository ->getRepository ()->getRepoConfig ()['url ' ];
226+ }
227+ if ($ found ) {
228+ if (strpos ($ repoUrl , self ::URL_REPO_PACKAGIST ) !== false ) {
229+ $ publicRepoVersion = $ found ->getFullPrettyVersion ();
230+ } else {
231+ $ currentPrivateRepoVersion = $ found ->getFullPrettyVersion ();
232+ //private repo version should hold highest version of package
233+ if (empty ($ privateRepoVersion ) || version_compare ($ currentPrivateRepoVersion , $ privateRepoVersion , '> ' )) {
234+ $ privateRepoVersion = $ currentPrivateRepoVersion ;
235+ $ privateRepoUrl = $ repoUrl ;
152236 }
153237 }
154238 }
155239 }
156- if ($ privateRepoVersion && $ publicRepoVersion && (version_compare ($ publicRepoVersion , $ privateRepoVersion , '> ' ))) {
240+
241+ if ($ privateRepoVersion && $ publicRepoVersion && version_compare ($ publicRepoVersion , $ privateRepoVersion , '> ' )) {
157242 $ exceptionMessage = "Higher matching version {$ publicRepoVersion } of {$ packageName } was found in public repository packagist.org
158243 than {$ privateRepoVersion } in private {$ privateRepoUrl }. Public package might've been taken over by a malicious entity,
159244 please investigate and update package requirement to match the version from the private repository " ;
160- throw new Exception ($ exceptionMessage );
245+
246+ if ($ this ->nonFixedPackages && array_key_exists ($ packageName , $ this ->nonFixedPackages )) {
247+ throw new Exception ($ exceptionMessage );
248+ } else {
249+ $ event ->getIO ()->writeError ('<warning> ' . $ exceptionMessage . '</warning> ' );
250+ }
161251 }
162252 }
163253 }
0 commit comments