Completed
Push — master ( 75e91c...b3846f )
by Christian
02:44
created

PackageController::putPackageAction()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 26
rs 8.439
cc 6
eloc 15
nc 4
nop 3
1
<?php
2
3
/**
4
 * This file is part of tenside/core-bundle.
5
 *
6
 * (c) Christian Schiffler <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * This project is provided in good faith and hope to be usable by anyone.
12
 *
13
 * @package    tenside/core-bundle
14
 * @author     Christian Schiffler <[email protected]>
15
 * @author     Andreas Schempp <[email protected]>
16
 * @copyright  2015 Christian Schiffler <[email protected]>
17
 * @license    https://github.com/tenside/core-bundle/blob/master/LICENSE MIT
18
 * @link       https://github.com/tenside/core-bundle
19
 * @filesource
20
 */
21
22
namespace Tenside\CoreBundle\Controller;
23
24
use Composer\Package\AliasPackage;
25
use Composer\Package\Loader\ArrayLoader;
26
use Composer\Package\PackageInterface;
27
use Composer\Repository\RepositoryInterface;
28
use Composer\Repository\WritableArrayRepository;
29
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
30
use Symfony\Component\HttpFoundation\JsonResponse;
31
use Symfony\Component\HttpFoundation\Request;
32
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
33
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
34
use Tenside\Core\Composer\PackageConverter;
35
use Tenside\Core\Util\JsonArray;
36
use Tenside\Core\Util\JsonFile;
37
use Tenside\CoreBundle\Annotation\ApiDescription;
38
39
/**
40
 * List and manipulate the installed packages.
41
 */
42
class PackageController extends AbstractController
43
{
44
    /**
45
     * Retrieve the package list.
46
     *
47
     * @param Request $request The request to process.
48
     *
49
     * @return JsonResponse
50
     *
51
     * @ApiDoc(
52
     *   section="package",
53
     *   statusCodes = {
54
     *     200 = "When everything worked out ok"
55
     *   },
56
     *   authentication = true,
57
     *   authenticationRoles = {
58
     *     "ROLE_MANIPULATE_REQUIREMENTS"
59
     *   },
60
     *   filters = {
61
     *     {
62
     *       "name"="all",
63
     *       "description"="If present, all packages will get listed, only directly required ones otherwise."
64
     *     }
65
     *   }
66
     * )
67
     * @ApiDescription(
68
     *   response={
69
     *     "package name 1...n" = {
70
     *       "actualType" = "object",
71
     *       "subType" = "object",
72
     *       "description" = "The content of the packages",
73
     *       "children" = {
74
     *         "name" = {
75
     *           "dataType" = "string",
76
     *           "description" = "The name of the package"
77
     *         },
78
     *         "version" = {
79
     *           "dataType" = "string",
80
     *           "description" = "The version of the package"
81
     *         },
82
     *         "constraint" = {
83
     *           "dataType" = "string",
84
     *           "description" = "The constraint of the package (when package is installed)"
85
     *         },
86
     *         "type" = {
87
     *           "dataType" = "string",
88
     *           "description" = "The noted package type"
89
     *         },
90
     *         "locked" = {
91
     *           "dataType" = "string",
92
     *           "description" = "Flag if the package has been locked for updates"
93
     *         },
94
     *         "time" = {
95
     *           "dataType" = "datetime",
96
     *           "description" = "The release date"
97
     *         },
98
     *         "upgrade_version" = {
99
     *           "dataType" = "string",
100
     *           "description" = "The version available for upgrade (optional, if any)"
101
     *         },
102
     *         "description" = {
103
     *           "dataType" = "string",
104
     *           "description" = "The package description"
105
     *         },
106
     *         "license" = {
107
     *           "actualType" = "collection",
108
     *           "subType" = "string",
109
     *           "description" = "The licenses"
110
     *         },
111
     *         "keywords" = {
112
     *           "actualType" = "collection",
113
     *           "subType" = "string",
114
     *           "description" = "The keywords"
115
     *         },
116
     *         "homepage" = {
117
     *           "dataType" = "string",
118
     *           "description" = "The support website (optional, if any)"
119
     *         },
120
     *         "authors" = {
121
     *           "actualType" = "collection",
122
     *           "subType" = "object",
123
     *           "description" = "The authors",
124
     *           "children" = {
125
     *             "name" = {
126
     *               "dataType" = "string",
127
     *               "description" = "Full name of the author (optional, if any)"
128
     *             },
129
     *             "homepage" = {
130
     *               "dataType" = "string",
131
     *               "description" = "Email address of the author (optional, if any)"
132
     *             },
133
     *             "email" = {
134
     *               "dataType" = "string",
135
     *               "description" = "Homepage URL for the author (optional, if any)"
136
     *             },
137
     *             "role" = {
138
     *               "dataType" = "string",
139
     *               "description" = "Author's role in the project (optional, if any)"
140
     *             }
141
     *           }
142
     *         },
143
     *         "support" = {
144
     *           "actualType" = "collection",
145
     *           "subType" = "object",
146
     *           "description" = "The support options",
147
     *           "children" = {
148
     *             "email" = {
149
     *               "dataType" = "string",
150
     *               "description" = "Email address for support (optional, if any)"
151
     *             },
152
     *             "issues" = {
153
     *               "dataType" = "string",
154
     *               "description" = "URL to the issue tracker (optional, if any)"
155
     *             },
156
     *             "forum" = {
157
     *               "dataType" = "string",
158
     *               "description" = "URL to the forum (optional, if any)"
159
     *             },
160
     *             "wiki" = {
161
     *               "dataType" = "string",
162
     *               "description" = "URL to the wiki (optional, if any)"
163
     *             },
164
     *             "irc" = {
165
     *               "dataType" = "string",
166
     *               "description" = "IRC channel for support, as irc://server/channel (optional, if any)"
167
     *             },
168
     *             "source" = {
169
     *               "dataType" = "string",
170
     *               "description" = "URL to browse or download the sources (optional, if any)"
171
     *             },
172
     *             "docs" = {
173
     *               "dataType" = "string",
174
     *               "description" = "URL to the documentation (optional, if any)"
175
     *             },
176
     *           }
177
     *         },
178
     *         "extra" = {
179
     *             "dataType" = "collection",
180
     *             "description" = "The extra data from composer.json"
181
     *         },
182
     *         "abandoned" = {
183
     *           "dataType" = "boolean",
184
     *           "description" = "Flag if this package is abandoned"
185
     *         },
186
     *         "replacement" = {
187
     *           "dataType" = "string",
188
     *           "description" = "Replacement for this package (optional, if any)"
189
     *         }
190
     *       }
191
     *     }
192
     *   }
193
     * )
194
     */
195
    public function packageListAction(Request $request)
196
    {
197
        $composer  = $this->getComposer();
198
        $converter = new PackageConverter($composer->getPackage());
199
        $upgrades  = $this->getUpgradeRepository();
200
        $packages  = $converter->convertRepositoryToArray(
201
            $composer->getRepositoryManager()->getLocalRepository(),
202
            !$request->query->has('all'),
203
            $upgrades
0 ignored issues
show
Bug introduced by
It seems like $upgrades defined by $this->getUpgradeRepository() on line 199 can also be of type object<Composer\Reposito...ritableArrayRepository>; however, Tenside\Core\Composer\Pa...vertRepositoryToArray() does only seem to accept null|object<Tenside\Core\Util\JsonArray>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
204
        )->getData();
205
        ksort($packages);
206
207
        return new JsonResponse($packages, 200);
208
    }
209
210
    /**
211
     * Retrieve a package.
212
     *
213
     * @param string $vendor  The name of the vendor.
214
     *
215
     * @param string $package The name of the package.
216
     *
217
     * @return JsonResponse
218
     *
219
     * @throws NotFoundHttpException When the package has not been found.
220
     *
221
     * @ApiDoc(
222
     *   section="package",
223
     *   statusCodes = {
224
     *     200 = "When everything worked out ok"
225
     *   },
226
     *   authentication = true,
227
     *   authenticationRoles = {
228
     *     "ROLE_MANIPULATE_REQUIREMENTS"
229
     *   }
230
     * )
231
     * @ApiDescription(
232
     *   response={
233
     *     "name" = {
234
     *       "dataType" = "string",
235
     *       "description" = "The name of the package"
236
     *     },
237
     *     "version" = {
238
     *       "dataType" = "string",
239
     *       "description" = "The version of the package"
240
     *     },
241
     *     "constraint" = {
242
     *       "dataType" = "string",
243
     *       "description" = "The constraint of the package (when package is installed)"
244
     *     },
245
     *     "type" = {
246
     *       "dataType" = "string",
247
     *       "description" = "The noted package type"
248
     *     },
249
     *     "locked" = {
250
     *       "dataType" = "string",
251
     *       "description" = "Flag if the package has been locked for updates"
252
     *     },
253
     *     "time" = {
254
     *       "dataType" = "datetime",
255
     *       "description" = "The release date"
256
     *     },
257
     *     "upgrade_version" = {
258
     *       "dataType" = "string",
259
     *       "description" = "The version available for upgrade (optional, if any)"
260
     *     },
261
     *     "description" = {
262
     *       "dataType" = "string",
263
     *       "description" = "The package description"
264
     *     },
265
     *     "license" = {
266
     *       "actualType" = "collection",
267
     *       "subType" = "string",
268
     *       "description" = "The licenses"
269
     *     },
270
     *     "keywords" = {
271
     *       "actualType" = "collection",
272
     *       "subType" = "string",
273
     *       "description" = "The keywords"
274
     *     },
275
     *     "homepage" = {
276
     *       "dataType" = "string",
277
     *       "description" = "The support website (optional, if any)"
278
     *     },
279
     *     "authors" = {
280
     *       "actualType" = "collection",
281
     *       "subType" = "object",
282
     *       "description" = "The authors",
283
     *       "children" = {
284
     *         "name" = {
285
     *           "dataType" = "string",
286
     *           "description" = "Full name of the author (optional, if any)"
287
     *         },
288
     *         "homepage" = {
289
     *           "dataType" = "string",
290
     *           "description" = "Email address of the author (optional, if any)"
291
     *         },
292
     *         "email" = {
293
     *           "dataType" = "string",
294
     *           "description" = "Homepage URL for the author (optional, if any)"
295
     *         },
296
     *         "role" = {
297
     *           "dataType" = "string",
298
     *           "description" = "Author's role in the project (optional, if any)"
299
     *         }
300
     *       }
301
     *     },
302
     *     "support" = {
303
     *       "actualType" = "collection",
304
     *       "subType" = "object",
305
     *       "description" = "The support options",
306
     *       "children" = {
307
     *         "email" = {
308
     *           "dataType" = "string",
309
     *           "description" = "Email address for support (optional, if any)"
310
     *         },
311
     *         "issues" = {
312
     *           "dataType" = "string",
313
     *           "description" = "URL to the issue tracker (optional, if any)"
314
     *         },
315
     *         "forum" = {
316
     *           "dataType" = "string",
317
     *           "description" = "URL to the forum (optional, if any)"
318
     *         },
319
     *         "wiki" = {
320
     *           "dataType" = "string",
321
     *           "description" = "URL to the wiki (optional, if any)"
322
     *         },
323
     *         "irc" = {
324
     *           "dataType" = "string",
325
     *           "description" = "IRC channel for support, as irc://server/channel (optional, if any)"
326
     *         },
327
     *         "source" = {
328
     *           "dataType" = "string",
329
     *           "description" = "URL to browse or download the sources (optional, if any)"
330
     *         },
331
     *         "docs" = {
332
     *           "dataType" = "string",
333
     *           "description" = "URL to the documentation (optional, if any)"
334
     *         },
335
     *       }
336
     *     },
337
     *     "abandoned" = {
338
     *       "dataType" = "boolean",
339
     *       "description" = "Flag if this package is abandoned"
340
     *     },
341
     *     "replacement" = {
342
     *       "dataType" = "string",
343
     *       "description" = "Replacement for this package (optional, if any)"
344
     *     }
345
     *   }
346
     * )
347
     */
348
    public function getPackageAction($vendor, $package)
349
    {
350
        $packageName = $vendor . '/' . $package;
351
        $composer    = $this->getComposer();
352
353
        if ($package   = $this->findPackage($packageName, $composer->getRepositoryManager()->getLocalRepository())) {
354
            $converter = new PackageConverter($composer->getPackage());
355
            return new JsonResponse($converter->convertPackageToArray($package), 200);
356
        }
357
358
        throw new NotFoundHttpException('Package ' . $packageName . ' not found.');
359
    }
360
361
    /**
362
     * Update the information of a package in the composer.json.
363
     *
364
     * Note that the payload name of the package must match the vendor and package passed as parameter.
365
     *
366
     * @param string  $vendor  The name of the vendor.
367
     *
368
     * @param string  $package The name of the package.
369
     *
370
     * @param Request $request The request to process.
371
     *
372
     * @return JsonResponse
373
     *
374
     * @throws NotAcceptableHttpException When the passed payload is invalid.
375
     * @throws NotFoundHttpException When the package has not been found.
376
     *
377
     * @ApiDoc(
378
     *   section="package",
379
     *   statusCodes = {
380
     *     200 = "When everything worked out ok"
381
     *   },
382
     *   authentication = true,
383
     *   authenticationRoles = {
384
     *     "ROLE_MANIPULATE_REQUIREMENTS"
385
     *   }
386
     * )
387
     *
388
     * @ApiDescription(
389
     *   request={
390
     *     "name" = {
391
     *       "dataType" = "string",
392
     *       "description" = "The name of the package",
393
     *       "required" = true
394
     *     },
395
     *     "constraint" = {
396
     *       "dataType" = "string",
397
     *       "description" = "The constraint of the package (when package is installed)",
398
     *       "required" = true
399
     *     },
400
     *     "locked" = {
401
     *       "dataType" = "string",
402
     *       "description" = "Flag if the package has been locked for updates",
403
     *       "required" = true
404
     *     },
405
     *   },
406
     *   response={
407
     *     "name" = {
408
     *       "dataType" = "string",
409
     *       "description" = "The name of the package"
410
     *     },
411
     *     "version" = {
412
     *       "dataType" = "string",
413
     *       "description" = "The version of the package"
414
     *     },
415
     *     "constraint" = {
416
     *       "dataType" = "string",
417
     *       "description" = "The constraint of the package (when package is installed)"
418
     *     },
419
     *     "type" = {
420
     *       "dataType" = "string",
421
     *       "description" = "The noted package type"
422
     *     },
423
     *     "locked" = {
424
     *       "dataType" = "string",
425
     *       "description" = "Flag if the package has been locked for updates"
426
     *     },
427
     *     "time" = {
428
     *       "dataType" = "datetime",
429
     *       "description" = "The release date"
430
     *     },
431
     *     "upgrade_version" = {
432
     *       "dataType" = "string",
433
     *       "description" = "The version available for upgrade (optional, if any)"
434
     *     },
435
     *     "description" = {
436
     *       "dataType" = "string",
437
     *       "description" = "The package description"
438
     *     },
439
     *     "license" = {
440
     *       "actualType" = "collection",
441
     *       "subType" = "string",
442
     *       "description" = "The licenses"
443
     *     },
444
     *     "keywords" = {
445
     *       "actualType" = "collection",
446
     *       "subType" = "string",
447
     *       "description" = "The keywords"
448
     *     },
449
     *     "homepage" = {
450
     *       "dataType" = "string",
451
     *       "description" = "The support website (optional, if any)"
452
     *     },
453
     *     "authors" = {
454
     *       "actualType" = "collection",
455
     *       "subType" = "object",
456
     *       "description" = "The authors",
457
     *       "children" = {
458
     *         "name" = {
459
     *           "dataType" = "string",
460
     *           "description" = "Full name of the author (optional, if any)"
461
     *         },
462
     *         "homepage" = {
463
     *           "dataType" = "string",
464
     *           "description" = "Email address of the author (optional, if any)"
465
     *         },
466
     *         "email" = {
467
     *           "dataType" = "string",
468
     *           "description" = "Homepage URL for the author (optional, if any)"
469
     *         },
470
     *         "role" = {
471
     *           "dataType" = "string",
472
     *           "description" = "Author's role in the project (optional, if any)"
473
     *         }
474
     *       }
475
     *     },
476
     *     "support" = {
477
     *       "actualType" = "collection",
478
     *       "subType" = "object",
479
     *       "description" = "The support options",
480
     *       "children" = {
481
     *         "email" = {
482
     *           "dataType" = "string",
483
     *           "description" = "Email address for support (optional, if any)"
484
     *         },
485
     *         "issues" = {
486
     *           "dataType" = "string",
487
     *           "description" = "URL to the issue tracker (optional, if any)"
488
     *         },
489
     *         "forum" = {
490
     *           "dataType" = "string",
491
     *           "description" = "URL to the forum (optional, if any)"
492
     *         },
493
     *         "wiki" = {
494
     *           "dataType" = "string",
495
     *           "description" = "URL to the wiki (optional, if any)"
496
     *         },
497
     *         "irc" = {
498
     *           "dataType" = "string",
499
     *           "description" = "IRC channel for support, as irc://server/channel (optional, if any)"
500
     *         },
501
     *         "source" = {
502
     *           "dataType" = "string",
503
     *           "description" = "URL to browse or download the sources (optional, if any)"
504
     *         },
505
     *         "docs" = {
506
     *           "dataType" = "string",
507
     *           "description" = "URL to the documentation (optional, if any)"
508
     *         },
509
     *       }
510
     *     },
511
     *     "abandoned" = {
512
     *       "dataType" = "boolean",
513
     *       "description" = "Flag if this package is abandoned"
514
     *     },
515
     *     "replacement" = {
516
     *       "dataType" = "string",
517
     *       "description" = "Replacement for this package (optional, if any)"
518
     *     }
519
     *   }
520
     * )
521
     */
522
    public function putPackageAction($vendor, $package, Request $request)
523
    {
524
        $packageName = $vendor . '/' . $package;
525
        $info        = new JsonArray($request->getContent());
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Tenside\Core\Util\JsonArray::__construct() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
526
        $name        = $info->get('name');
527
528
        if (!($info->has('name') && $info->has('locked') && $info->has('constraint'))) {
529
            throw new NotAcceptableHttpException('Invalid package information.');
530
        }
531
532
        if ($name !== $packageName) {
533
            throw new NotAcceptableHttpException('Package name mismatch ' . $packageName . ' vs. ' . $name . '.');
534
        }
535
536
        $composer = $this->getComposer();
537
        $json     = $this->get('tenside.composer_json');
538
539
        $package = $this->findPackage($name, $composer->getRepositoryManager()->getLocalRepository());
540
541
        if (null === $package) {
542
            throw new NotFoundHttpException('Package ' . $packageName . ' not found.');
543
        }
544
545
        $json->setLock($package, $info->get('locked'));
546
        return $this->forward('TensideCoreBundle:Package:getPackage');
547
    }
548
549
    /**
550
     * Search the repository for a package.
551
     *
552
     * @param string              $name       The pretty name of the package to search.
553
     *
554
     * @param RepositoryInterface $repository The repository to be searched.
555
     *
556
     * @return null|PackageInterface
557
     */
558
    private function findPackage($name, RepositoryInterface $repository)
559
    {
560
        /** @var PackageInterface[] $packages */
561
        $packages = $repository->findPackages($name);
562
563
        while (!empty($packages) && $packages[0] instanceof AliasPackage) {
564
            array_shift($packages);
565
        }
566
567
        if (empty($packages)) {
568
            return null;
569
        }
570
571
        return $packages[0];
572
    }
573
574
    /**
575
     * Load a repository containing available upgrades.
576
     *
577
     * @return null|RepositoryInterface
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|WritableArrayRepository.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
578
     */
579
    private function getUpgradeRepository()
580
    {
581
        $upgradeFile = $this->get('tenside.home')->tensideDataDir() . DIRECTORY_SEPARATOR . 'upgrades.json';
582
        if (!file_exists($upgradeFile)) {
583
            return null;
584
        }
585
586
        $packageLoader  = new ArrayLoader();
587
        $packageChanges = new WritableArrayRepository();
588
        $upgrades       = new JsonFile($upgradeFile, null);
589
        foreach ($upgrades->getEntries('/') as $packageName) {
590
            if ($pkgData = $upgrades->get($packageName . '/target')) {
591
                $packageChanges->addPackage($packageLoader->load($pkgData));
592
            }
593
        }
594
595
        return $packageChanges;
596
    }
597
}
598