Completed
Push — master ( 12b21a...f18807 )
by Christian
13:51
created

InstallProjectController   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 26
c 6
b 0
f 0
lcom 1
cbo 8
dl 0
loc 498
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
B configureAction() 0 29 4
B createProjectAction() 0 38 3
A getSelfTestAction() 0 6 1
A getAutoConfigAction() 0 6 1
B getProjectVersionsAction() 0 41 6
A getInstallationStateAction() 0 15 1
A checkUninstalled() 0 6 2
A createUser() 0 15 1
C handleConfiguration() 0 33 7
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
 * @copyright  2015 Christian Schiffler <[email protected]>
16
 * @license    https://github.com/tenside/core-bundle/blob/master/LICENSE MIT
17
 * @link       https://github.com/tenside/core-bundle
18
 * @filesource
19
 */
20
21
namespace Tenside\CoreBundle\Controller;
22
23
use Composer\Util\RemoteFilesystem;
24
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
25
use Symfony\Component\HttpFoundation\JsonResponse;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
28
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
29
use Tenside\Core\Composer\AuthJson;
30
use Tenside\CoreBundle\Security\UserInformation;
31
use Tenside\CoreBundle\Security\UserInformationInterface;
32
use Tenside\Core\Task\Composer\InstallTask;
33
use Tenside\Core\Util\JsonArray;
34
use Tenside\CoreBundle\Annotation\ApiDescription;
35
36
/**
37
 * Controller for manipulating the composer.json file.
38
 */
39
class InstallProjectController extends AbstractController
40
{
41
    /**
42
     * Configure tenside.
43
     *
44
     * NOTE: This method will become inaccessible after the first successful call.
45
     *
46
     * @param Request $request The request.
47
     *
48
     * @return JsonResponse
49
     *
50
     * @throws NotAcceptableHttpException When the configuration is already complete.
51
     *
52
     * @ApiDoc(
53
     *   section="install",
54
     *   statusCodes = {
55
     *     201 = "When everything worked out ok",
56
     *     406 = "When the configuration is already complete"
57
     *   }
58
     * )
59
     * @ApiDescription(
60
     *   request={
61
     *     "credentials" = {
62
     *       "description" = "The credentials of the admin user.",
63
     *       "children" = {
64
     *         "secret" = {
65
     *           "dataType" = "string",
66
     *           "description" = "The secret to use for encryption and signing.",
67
     *           "required" = true
68
     *         },
69
     *         "username" = {
70
     *           "dataType" = "string",
71
     *           "description" = "The name of the admin user.",
72
     *           "required" = true
73
     *         },
74
     *         "password" = {
75
     *           "dataType" = "string",
76
     *           "description" = "The password to use for the admin.",
77
     *           "required" = false
78
     *         }
79
     *       }
80
     *     },
81
     *     "configuration" = {
82
     *       "description" = "The application configuration.",
83
     *       "children" = {
84
     *         "php_cli" = {
85
     *           "dataType" = "string",
86
     *           "description" = "The PHP interpreter to run on command line."
87
     *         },
88
     *         "php_cli_arguments" = {
89
     *           "dataType" = "string",
90
     *           "description" = "Command line arguments to add."
91
     *         },
92
     *         "github_oauth_token" = {
93
     *           "dataType" = "string",
94
     *           "description" = "Github OAuth token.",
95
     *           "required" = false
96
     *         }
97
     *       }
98
     *     }
99
     *   },
100
     *   response={
101
     *     "token" = {
102
     *       "dataType" = "string",
103
     *       "description" = "The API token for the created user"
104
     *     }
105
     *   }
106
     * )
107
     */
108
    public function configureAction(Request $request)
109
    {
110
        if ($this->get('tenside.status')->isTensideConfigured()) {
111
            throw new NotAcceptableHttpException('Already configured.');
112
        }
113
        $inputData = 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...
114
115
        $secret = bin2hex(random_bytes(40));
116
        if ($inputData->has('credentials/secret')) {
117
            $secret = $inputData->get('credentials/secret');
118
        }
119
120
        // Add tenside configuration.
121
        $tensideConfig = $this->get('tenside.config');
122
        $tensideConfig->set('secret', $secret);
123
124
        if ($inputData->has('configuration')) {
125
            $this->handleConfiguration($inputData->get('configuration', true));
126
        }
127
        $user = $this->createUser($inputData->get('credentials/username'), $inputData->get('credentials/password'));
128
129
        return new JsonResponse(
130
            [
131
                'status' => 'OK',
132
                'token'  => $this->get('tenside.jwt_authenticator')->getTokenForData($user)
133
            ],
134
            JsonResponse::HTTP_CREATED
135
        );
136
    }
137
138
    /**
139
     * Create a project.
140
     *
141
     * NOTE: This method will become inaccessible after the returned task has been run successfully.
142
     *
143
     * @param Request $request The request.
144
     *
145
     * @return JsonResponse
146
     *
147
     * @throws NotAcceptableHttpException When the installation is already complete.
148
     *
149
     * @ApiDoc(
150
     *   section="install",
151
     *   statusCodes = {
152
     *     201 = "When everything worked out ok",
153
     *     406 = "When the installation is already been completed"
154
     *   },
155
     * )
156
     * @ApiDescription(
157
     *   request={
158
     *     "project" = {
159
     *       "description" = "The project to install.",
160
     *       "children" = {
161
     *         "name" = {
162
     *           "dataType" = "string",
163
     *           "description" = "The name of the project to install.",
164
     *           "required" = true
165
     *         },
166
     *         "version" = {
167
     *           "dataType" = "string",
168
     *           "description" = "The version of the project to install (optional).",
169
     *           "required" = false
170
     *         }
171
     *       }
172
     *     }
173
     *   },
174
     *   response={
175
     *     "task" = {
176
     *       "dataType" = "string",
177
     *       "description" = "The id of the created install task"
178
     *     }
179
     *   }
180
     * )
181
     */
182
    public function createProjectAction(Request $request)
183
    {
184
        $status = $this->get('tenside.status');
185
        if (!$status->isTensideConfigured()) {
186
            throw new NotAcceptableHttpException('Need to configure first.');
187
        }
188
189
        $this->checkUninstalled();
190
        $result = [];
191
        $header = [];
192
193
        $installDir = $this->getTensideHome();
194
        $inputData  = 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...
195
        $taskData   = new JsonArray();
196
197
        $taskData->set(InstallTask::SETTING_DESTINATION_DIR, $installDir);
198
        $taskData->set(InstallTask::SETTING_PACKAGE, $inputData->get('project/name'));
199
        if ($version = $inputData->get('project/version')) {
200
            $taskData->set(InstallTask::SETTING_VERSION, $version);
201
        }
202
203
        $taskId             = $this->getTensideTasks()->queue('install', $taskData);
204
        $result['task']     = $taskId;
205
        $header['Location'] = $this->generateUrl(
206
            'task_get',
207
            ['taskId' => $taskId],
208
            UrlGeneratorInterface::ABSOLUTE_URL
209
        );
210
211
        return new JsonResponse(
212
            [
213
                'status' => 'OK',
214
                'task'   => $taskId
215
            ],
216
            JsonResponse::HTTP_CREATED,
217
            $header
218
        );
219
    }
220
221
    /**
222
     * This is a gateway to the self test controller available only at install time.
223
     *
224
     * This is just here as the other route is protected with login.
225
     *
226
     * NOTE: This method will become inaccessible as soon as the installation is complete.
227
     *
228
     * @return JsonResponse
229
     *
230
     * @ApiDoc(
231
     *   section="install",
232
     *   description="Install time - self test.",
233
     *   statusCodes = {
234
     *     201 = "When everything worked out ok",
235
     *     406 = "When the installation is already complete"
236
     *   },
237
     * )
238
     * @ApiDescription(
239
     *   response={
240
     *     "results" = {
241
     *       "actualType" = "collection",
242
     *       "subType" = "object",
243
     *       "description" = "The test results.",
244
     *       "children" = {
245
     *         "name" = {
246
     *           "dataType" = "string",
247
     *           "description" = "The name of the test"
248
     *         },
249
     *         "state" = {
250
     *           "dataType" = "choice",
251
     *           "description" = "The test result state.",
252
     *           "format" = "[FAIL|SKIPPED|SUCCESS|WARNING]"
253
     *         },
254
     *         "message" = {
255
     *           "dataType" = "string",
256
     *           "description" = "The detailed message of the test result."
257
     *         },
258
     *         "explain" = {
259
     *           "dataType" = "string",
260
     *           "description" = "Optional description that could hint any problems and/or explain the error further."
261
     *         }
262
     *       }
263
     *     }
264
     *   }
265
     * )
266
     */
267
    public function getSelfTestAction()
268
    {
269
        $this->checkUninstalled();
270
271
        return $this->forward('TensideCoreBundle:SelfTest:getAllTests');
272
    }
273
274
    /**
275
     * Install time gateway to the auto config.
276
     *
277
     * This is just here as the other route is protected with login.
278
     *
279
     * NOTE: This method will become inaccessible as soon as the installation is complete.
280
     *
281
     * @return JsonResponse
282
     *
283
     * @ApiDoc(
284
     *   section="install",
285
     *   description="Install time - auto config.",
286
     *   statusCodes = {
287
     *     201 = "When everything worked out ok",
288
     *     406 = "When the installation is already complete"
289
     *   },
290
     * )
291
     * @ApiDescription(
292
     *   response={
293
     *     "php_cli" = {
294
     *       "dataType" = "string",
295
     *       "description" = "The PHP interpreter to run on command line."
296
     *     },
297
     *     "php_cli_arguments" = {
298
     *       "dataType" = "string",
299
     *       "description" = "Command line arguments to add."
300
     *     }
301
     *   }
302
     * )
303
     */
304
    public function getAutoConfigAction()
305
    {
306
        $this->checkUninstalled();
307
308
        return $this->forward('TensideCoreBundle:SelfTest:getAutoConfig');
309
    }
310
311
    /**
312
     * Retrieve the available versions of a package.
313
     *
314
     * NOTE: This method will become inaccessible as soon as the installation is complete.
315
     *
316
     * @param string $vendor  The vendor name of the package.
317
     *
318
     * @param string $project The name of the package.
319
     *
320
     * @return JsonResponse
321
     *
322
     * @ApiDoc(
323
     *   section="install",
324
     *   statusCodes = {
325
     *     201 = "When everything worked out ok",
326
     *     406 = "When the installation is already complete"
327
     *   },
328
     * )
329
     * @ApiDescription(
330
     *   response={
331
     *     "versions" = {
332
     *       "actualType" = "collection",
333
     *       "subType" = "object",
334
     *       "description" = "The list of versions",
335
     *       "children" = {
336
     *         "name" = {
337
     *           "dataType" = "string",
338
     *           "description" = "The name of the package"
339
     *         },
340
     *         "version" = {
341
     *           "dataType" = "string",
342
     *           "description" = "The version of the package"
343
     *         },
344
     *         "version_normalized" = {
345
     *           "dataType" = "string",
346
     *           "description" = "The normalized version of the package"
347
     *         },
348
     *         "reference" = {
349
     *           "dataType" = "string",
350
     *           "description" = "The optional reference"
351
     *         }
352
     *       }
353
     *     }
354
     *   }
355
     * )
356
     */
357
    public function getProjectVersionsAction($vendor, $project)
358
    {
359
        $this->checkUninstalled();
360
361
        $url     = sprintf('https://packagist.org/packages/%s/%s.json', $vendor, $project);
362
        $rfs     = new RemoteFilesystem($this->getInputOutput());
363
        $results = $rfs->getContents($url, $url);
364
        $data    = new JsonArray($results);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $rfs->getContents($url, $url) on line 363 can also be of type boolean; however, Tenside\Core\Util\JsonArray::__construct() does only seem to accept string|array, 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...
365
366
        $versions = [];
367
368
        foreach ($data->get('package/versions') as $information) {
0 ignored issues
show
Bug introduced by
The expression $data->get('package/versions') of type array|string|integer|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
369
            $version = [
370
                'name'               => $information['name'],
371
                'version'            => $information['version'],
372
                'version_normalized' => $information['version_normalized'],
373
            ];
374
375
            $normalized = $information['version'];
376
            if ('dev-' === substr($normalized, 0, 4)) {
377
                if (isset($information['extra']['branch-alias'][$normalized])) {
378
                    $version['version_normalized'] = $information['extra']['branch-alias'][$normalized];
379
                }
380
            }
381
382
            if (isset($information['source']['reference'])) {
383
                $version['reference'] = $information['source']['reference'];
384
            } elseif (isset($information['dist']['reference'])) {
385
                $version['reference'] = $information['dist']['reference'];
386
            }
387
388
            $versions[] = $version;
389
        }
390
391
        return new JsonResponse(
392
            [
393
                'status' => 'OK',
394
                'versions' => $versions
395
            ]
396
        );
397
    }
398
399
    /**
400
     * Check if installation is new, partial or complete.
401
     *
402
     * @return JsonResponse
403
     *
404
     * @ApiDoc(
405
     *   section="install",
406
     *   description="This method provides information about the installation.",
407
     *   statusCodes = {
408
     *     201 = "When everything worked out ok",
409
     *     406 = "When the installation is already complete"
410
     *   },
411
     * )
412
     * @ApiDescription(
413
     *   response={
414
     *     "state" = {
415
     *       "children" = {
416
     *         "tenside_configured" = {
417
     *           "dataType" = "bool",
418
     *           "description" = "Flag if tenside has been completely configured."
419
     *         },
420
     *         "project_created" = {
421
     *           "dataType" = "bool",
422
     *           "description" = "Flag determining if a composer.json is present."
423
     *         },
424
     *         "project_installed" = {
425
     *           "dataType" = "bool",
426
     *           "description" = "Flag determining if the composer project has been installed (vendor present)."
427
     *         }
428
     *       }
429
     *     },
430
     *     "status" = {
431
     *       "dataType" = "string",
432
     *       "description" = "Either OK or ERROR"
433
     *     },
434
     *     "message" = {
435
     *       "dataType" = "string",
436
     *       "description" = "The API error message if any (only present when status is ERROR)"
437
     *     }
438
     *   }
439
     * )
440
     */
441
    public function getInstallationStateAction()
442
    {
443
        $status = $this->get('tenside.status');
444
445
        return new JsonResponse(
446
            [
447
                'state'  => [
448
                    'tenside_configured' => $status->isTensideConfigured(),
449
                    'project_created'    => $status->isProjectPresent(),
450
                    'project_installed'  => $status->isProjectInstalled(),
451
                ],
452
                'status' => 'OK'
453
            ]
454
        );
455
    }
456
457
    /**
458
     * Ensure that we are not installed yet.
459
     *
460
     * @return void
461
     *
462
     * @throws NotAcceptableHttpException When the installation is already complete.
463
     */
464
    private function checkUninstalled()
465
    {
466
        if ($this->get('tenside.status')->isComplete()) {
467
            throw new NotAcceptableHttpException('Already installed in ' . $this->getTensideHome());
468
        }
469
    }
470
471
    /**
472
     * Add an user to the database.
473
     *
474
     * @param string $username The username.
475
     *
476
     * @param string $password The password.
477
     *
478
     * @return UserInformation
479
     */
480
    private function createUser($username, $password)
481
    {
482
        $user = new UserInformation(
483
            [
484
                'username' => $username,
485
                'acl'      => UserInformationInterface::ROLE_ALL
486
            ]
487
        );
488
489
        $user->set('password', $this->get('security.password_encoder')->encodePassword($user, $password));
490
491
        $user = $this->get('tenside.user_provider')->addUser($user)->refreshUser($user);
492
493
        return $user;
494
    }
495
496
    /**
497
     * Absorb the passed configuration.
498
     *
499
     * @param array $configuration The configuration to absorb.
500
     *
501
     * @return void
502
     */
503
    private function handleConfiguration($configuration)
504
    {
505
        $tensideConfig = $this->get('tenside.config');
506
507
        if (isset($configuration['php_cli'])) {
508
            $tensideConfig->setPhpCliBinary($configuration['php_cli']);
509
        }
510
511
        if (isset($configuration['php_cli_arguments'])) {
512
            $tensideConfig->setPhpCliArguments($configuration['php_cli_arguments']);
513
        }
514
515
        if (isset($configuration['php_cli_environment'])) {
516
            $tensideConfig->setPhpCliEnvironment($configuration['php_cli_environment']);
517
        }
518
519
        if (isset($configuration['php_force_background'])) {
520
            $tensideConfig->setForceToBackground($configuration['php_force_background']);
521
        }
522
523
        if (isset($configuration['php_can_fork'])) {
524
            $tensideConfig->setForkingAvailable($configuration['php_can_fork']);
525
        }
526
527
        if (isset($configuration['github_oauth_token'])) {
528
            $composerAuth = new AuthJson(
529
                $this->get('tenside.home')->tensideDataDir() . DIRECTORY_SEPARATOR . 'auth.json',
530
                null
531
            );
532
533
            $composerAuth->setGithubOAuthToken($configuration['github_oauth_token']);
534
        }
535
    }
536
}
537