4
4
5
5
namespace Packagist \Api ;
6
6
7
+ use Composer \Semver \Semver ;
7
8
use GuzzleHttp \Client as HttpClient ;
8
9
use GuzzleHttp \ClientInterface ;
9
10
use GuzzleHttp \Exception \GuzzleException ;
11
+ use Packagist \Api \Result \Advisory ;
10
12
use Packagist \Api \Result \Factory ;
11
13
use Packagist \Api \Result \Package ;
12
14
@@ -187,6 +189,96 @@ public function popular(int $total): array
187
189
return array_slice ($ results , 0 , $ total );
188
190
}
189
191
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
+
190
282
/**
191
283
* Assemble the packagist URL with the route
192
284
*
@@ -212,6 +304,21 @@ protected function respond(string $url)
212
304
return $ this ->create ($ response );
213
305
}
214
306
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
+
215
322
/**
216
323
* Execute two URLs request, parse and merge the responses by adding the versions from the second URL
217
324
* into the versions from the first URL.
@@ -241,6 +348,22 @@ protected function multiRespond(string $url1, string $url2)
241
348
return $ this ->create ($ response1 );
242
349
}
243
350
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
+
244
367
/**
245
368
* Execute the request URL
246
369
*
0 commit comments