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,88 @@ 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+ * @param array $packages
196+ * @param integer|null $updatedSince
197+ * @param boolean $filterByVersion
198+ * @return Advisory[]
199+ */
200+ public function advisories (array $ packages = [], ?int $ updatedSince = null , bool $ filterByVersion = false ): array
201+ {
202+ if (count ($ packages ) === 0 && $ updatedSince === null ) {
203+ throw new \InvalidArgumentException (
204+ 'At least one package or an $updatedSince timestamp must be passed in. '
205+ );
206+ }
207+
208+ if (count ($ packages ) === 0 && $ filterByVersion ) {
209+ return [];
210+ }
211+
212+ // Add updatedSince to query if passed in
213+ $ query = [];
214+ if ($ updatedSince !== null ) {
215+ $ query ['updatedSince ' ] = $ updatedSince ;
216+ }
217+ $ options = [
218+ 'query ' => array_filter ($ query ),
219+ ];
220+
221+ // Add packages if appropriate
222+ if (count ($ packages ) > 0 ) {
223+ $ content = ['packages ' => []];
224+ foreach ($ packages as $ package => $ version ) {
225+ if (is_numeric ($ package )) {
226+ $ package = $ version ;
227+ }
228+ $ content ['packages ' ][] = $ package ;
229+ }
230+ $ options ['headers ' ]['Content-type ' ] = 'application/x-www-form-urlencoded ' ;
231+ $ options ['body ' ] = http_build_query ($ content );
232+ }
233+
234+ // Get advisories from API
235+ /** @var Advisory[] $advisories */
236+ $ advisories = $ this ->respondPost ($ this ->url ('/api/security-advisories/ ' ), $ options );
237+
238+ // Filter advisories if necessary
239+ if (count ($ advisories ) > 0 && $ filterByVersion ) {
240+ return $ this ->filterAdvisories ($ advisories , $ packages );
241+ }
242+
243+ return $ advisories ;
244+ }
245+
246+ /**
247+ * Filter the advisories array to only include any advisories that affect
248+ * the versions of packages in the $packages array
249+ *
250+ * @param Advisory[] $advisories
251+ * @param array $packages
252+ * @return Advisory[] Filtered advisories array
253+ */
254+ private function filterAdvisories (array $ advisories , array $ packages ): array
255+ {
256+ $ filteredAdvisories = [];
257+ foreach ($ packages as $ package => $ version ) {
258+ // Skip any packages with no declared versions
259+ if (is_numeric ($ package )) {
260+ continue ;
261+ }
262+ // Filter advisories by version
263+ if (array_key_exists ($ package , $ advisories )) {
264+ foreach ($ advisories [$ package ] as $ advisory ) {
265+ if (Semver::satisfies ($ version , $ advisory ->getAffectedVersions ())) {
266+ $ filteredAdvisories [$ package ][] = $ advisory ;
267+ }
268+ }
269+ }
270+ }
271+ return $ filteredAdvisories ;
272+ }
273+
190274 /**
191275 * Assemble the packagist URL with the route
192276 *
@@ -212,6 +296,21 @@ protected function respond(string $url)
212296 return $ this ->create ($ response );
213297 }
214298
299+ /**
300+ * Execute the POST request and parse the response
301+ *
302+ * @param string $url
303+ * @param array $option
304+ * @return array|Package
305+ */
306+ protected function respondPost (string $ url , array $ options )
307+ {
308+ $ response = $ this ->postRequest ($ url , $ options );
309+ $ response = $ this ->parse ($ response );
310+
311+ return $ this ->create ($ response );
312+ }
313+
215314 /**
216315 * Execute two URLs request, parse and merge the responses by adding the versions from the second URL
217316 * into the versions from the first URL.
@@ -241,6 +340,22 @@ protected function multiRespond(string $url1, string $url2)
241340 return $ this ->create ($ response1 );
242341 }
243342
343+ /**
344+ * Execute the POST request
345+ *
346+ * @param string $url
347+ * @param array $options
348+ * @return string
349+ * @throws GuzzleException
350+ */
351+ protected function postRequest (string $ url , array $ options ): string
352+ {
353+ return $ this ->httpClient
354+ ->request ('POST ' , $ url , $ options )
355+ ->getBody ()
356+ ->getContents ();
357+ }
358+
244359 /**
245360 * Execute the request URL
246361 *
0 commit comments