44
55namespace Packagist \Api ;
66
7+ use Composer \Semver \Semver ;
78use GuzzleHttp \Client as HttpClient ;
89use GuzzleHttp \ClientInterface ;
910use GuzzleHttp \Exception \GuzzleException ;
11+ use Packagist \Api \Result \Advisory ;
1012use Packagist \Api \Result \Factory ;
1113use Packagist \Api \Result \Package ;
1214
@@ -187,6 +189,96 @@ public function popular(int $total): array
187189 return array_slice ($ results , 0 , $ total );
188190 }
189191
192+ /**
193+ * Get a list of known security vulnerability advisories
194+ *
195+ * $packages can be a simple array of package names, or an array with package names
196+ * as keys and version strings as values.
197+ *
198+ * If $filterByVersion is true, any packages which are not accompanied by a version
199+ * number will be ignored.
200+ *
201+ * @param array $packages
202+ * @param integer|null $updatedSince A unix timestamp.
203+ * Only advisories updated after this date/time will be included
204+ * @param boolean $filterByVersion If true, only advisories which affect the version of packages in the
205+ * $packages array will be included
206+ * @return Advisory[]
207+ */
208+ public function advisories (array $ packages = [], ?int $ updatedSince = null , bool $ filterByVersion = false ): array
209+ {
210+ if (count ($ packages ) === 0 && $ updatedSince === null ) {
211+ throw new \InvalidArgumentException (
212+ 'At least one package or an $updatedSince timestamp must be passed in. '
213+ );
214+ }
215+
216+ if (count ($ packages ) === 0 && $ filterByVersion ) {
217+ return [];
218+ }
219+
220+ // Add updatedSince to query if passed in
221+ $ query = [];
222+ if ($ updatedSince !== null ) {
223+ $ query ['updatedSince ' ] = $ updatedSince ;
224+ }
225+ $ options = [
226+ 'query ' => array_filter ($ query ),
227+ ];
228+
229+ // Add packages if appropriate
230+ if (count ($ packages ) > 0 ) {
231+ $ content = ['packages ' => []];
232+ foreach ($ packages as $ package => $ version ) {
233+ if (is_numeric ($ package )) {
234+ $ package = $ version ;
235+ }
236+ $ content ['packages ' ][] = $ package ;
237+ }
238+ $ options ['headers ' ]['Content-type ' ] = 'application/x-www-form-urlencoded ' ;
239+ $ options ['body ' ] = http_build_query ($ content );
240+ }
241+
242+ // Get advisories from API
243+ /** @var Advisory[] $advisories */
244+ $ advisories = $ this ->respondPost ($ this ->url ('/api/security-advisories/ ' ), $ options );
245+
246+ // Filter advisories if necessary
247+ if (count ($ advisories ) > 0 && $ filterByVersion ) {
248+ return $ this ->filterAdvisories ($ advisories , $ packages );
249+ }
250+
251+ return $ advisories ;
252+ }
253+
254+ /**
255+ * Filter the advisories array to only include any advisories that affect
256+ * the versions of packages in the $packages array
257+ *
258+ * @param Advisory[] $advisories
259+ * @param array $packages
260+ * @return Advisory[] Filtered advisories array
261+ */
262+ private function filterAdvisories (array $ advisories , array $ packages ): array
263+ {
264+ $ filteredAdvisories = [];
265+ foreach ($ packages as $ package => $ version ) {
266+ // Skip any packages with no declared versions
267+ if (is_numeric ($ package )) {
268+ continue ;
269+ }
270+ // Filter advisories by version
271+ if (array_key_exists ($ package , $ advisories )) {
272+ foreach ($ advisories [$ package ] as $ advisory ) {
273+ if (Semver::satisfies ($ version , $ advisory ->getAffectedVersions ())) {
274+ $ filteredAdvisories [$ package ][] = $ advisory ;
275+ }
276+ }
277+ }
278+ }
279+ return $ filteredAdvisories ;
280+ }
281+
190282 /**
191283 * Assemble the packagist URL with the route
192284 *
@@ -212,6 +304,21 @@ protected function respond(string $url)
212304 return $ this ->create ($ response );
213305 }
214306
307+ /**
308+ * Execute the POST request and parse the response
309+ *
310+ * @param string $url
311+ * @param array $option
312+ * @return array|Package
313+ */
314+ protected function respondPost (string $ url , array $ options )
315+ {
316+ $ response = $ this ->postRequest ($ url , $ options );
317+ $ response = $ this ->parse ($ response );
318+
319+ return $ this ->create ($ response );
320+ }
321+
215322 /**
216323 * Execute two URLs request, parse and merge the responses by adding the versions from the second URL
217324 * into the versions from the first URL.
@@ -241,6 +348,22 @@ protected function multiRespond(string $url1, string $url2)
241348 return $ this ->create ($ response1 );
242349 }
243350
351+ /**
352+ * Execute the POST request
353+ *
354+ * @param string $url
355+ * @param array $options
356+ * @return string
357+ * @throws GuzzleException
358+ */
359+ protected function postRequest (string $ url , array $ options ): string
360+ {
361+ return $ this ->httpClient
362+ ->request ('POST ' , $ url , $ options )
363+ ->getBody ()
364+ ->getContents ();
365+ }
366+
244367 /**
245368 * Execute the request URL
246369 *
0 commit comments