Completed
Push — master ( 904d28...64ab80 )
by Christian
03:21
created

InstallProjectController::configureAction()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 40
rs 8.5806
cc 4
eloc 21
nc 5
nop 1
1
<?php
2
3
/**
4
 * This file is part of tenside/core.
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
14
 * @author     Christian Schiffler <[email protected]>
15
 * @author     Nico Schneider <[email protected]>
16
 * @author     Andreas Schempp <[email protected]>
17
 * @copyright  2015 Christian Schiffler <[email protected]>
18
 * @license    https://github.com/tenside/core/blob/master/LICENSE MIT
19
 * @link       https://github.com/tenside/core
20
 * @filesource
21
 */
22
23
namespace Tenside\CoreBundle\Controller;
24
25
use Composer\Util\RemoteFilesystem;
26
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
27
use Symfony\Component\Filesystem\Filesystem;
28
use Symfony\Component\HttpFoundation\JsonResponse;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
31
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
32
use Tenside\CoreBundle\Annotation\ApiDescription;
33
use Tenside\CoreBundle\Security\UserInformation;
34
use Tenside\CoreBundle\Security\UserInformationInterface;
35
use Tenside\Task\InstallTask;
36
use Tenside\Util\JsonArray;
37
38
/**
39
 * Controller for manipulating the composer.json file.
40
 */
41
class InstallProjectController extends AbstractController
42
{
43
    /**
44
     * Configure tenside.
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
     *   },
57
     * )
58
     * @ApiDescription(
59
     *   request={
60
     *     "credentials" = {
61
     *       "description" = "The credentials of the admin user.",
62
     *       "children" = {
63
     *         "secret" = {
64
     *           "dataType" = "string",
65
     *           "description" = "The secret to use for encryption and signing.",
66
     *           "required" = true
67
     *         },
68
     *         "username" = {
69
     *           "dataType" = "string",
70
     *           "description" = "The name of the admin user.",
71
     *           "required" = true
72
     *         },
73
     *         "password" = {
74
     *           "dataType" = "string",
75
     *           "description" = "The password to use for the admin.",
76
     *           "required" = false
77
     *         }
78
     *       }
79
     *     },
80
     *     "configuration" = {
81
     *       "description" = "The credentials of the admin user.",
82
     *       "children" = {
83
     *         "php_cli" = {
84
     *           "dataType" = "string",
85
     *           "description" = "The PHP interpreter to run on command line."
86
     *         },
87
     *         "php_cli_arguments" = {
88
     *           "dataType" = "string",
89
     *           "description" = "Command line arguments to add."
90
     *         }
91
     *       }
92
     *     }
93
     *   },
94
     *   response={
95
     *     "token" = {
96
     *       "dataType" = "string",
97
     *       "description" = "The API token for the created user"
98
     *     }
99
     *   }
100
     * )
101
     */
102
    public function configureAction(Request $request)
103
    {
104
        if ($this->get('tenside.status')->isTensideConfigured()) {
105
            throw new NotAcceptableHttpException('Already configured.');
106
        }
107
        $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\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...
108
109
        // Add tenside configuration.
110
        $tensideConfig = $this->get('tenside.config');
111
        $tensideConfig->set('secret', $inputData->get('credentials/secret'));
112
113
        if ($inputData->has('configuration/php_cli')) {
114
            $tensideConfig->set('php_cli', $inputData->get('configuration/php_cli'));
115
        }
116
117
        if ($inputData->has('configuration/php_cli_arguments')) {
118
            $tensideConfig->set('php_cli_arguments', $inputData->get('configuration/php_cli_arguments'));
119
        }
120
121
        // Add the user now.
122
        $user = new UserInformation([
123
            'username' => $inputData->get('credentials/username'),
124
            'acl'      => UserInformationInterface::ROLE_ALL
125
        ]);
126
127
        $user->set(
128
            'password',
129
            $this->get('security.password_encoder')->encodePassword($user, $inputData->get('credentials/password'))
130
        );
131
132
        $user = $this->get('tenside.user_provider')->addUser($user)->refreshUser($user);
133
134
        return new JsonResponse(
135
            [
136
                'status' => 'OK',
137
                'token'  => $this->get('tenside.jwt_authenticator')->getTokenForData($user)
138
            ],
139
            JsonResponse::HTTP_CREATED
140
        );
141
    }
142
143
    /**
144
     * Create a project.
145
     *
146
     * @param Request $request The request.
147
     *
148
     * @return JsonResponse
149
     *
150
     * @throws NotAcceptableHttpException When the installation is already complete.
151
     *
152
     * @ApiDoc(
153
     *   section="install",
154
     *   statusCodes = {
155
     *     201 = "When everything worked out ok"
156
     *   },
157
     * )
158
     * @ApiDescription(
159
     *   request={
160
     *     "project" = {
161
     *       "description" = "The name of the project to install.",
162
     *       "children" = {
163
     *         "name" = {
164
     *           "dataType" = "string",
165
     *           "description" = "The name of the project to install.",
166
     *           "required" = true
167
     *         },
168
     *         "version" = {
169
     *           "dataType" = "string",
170
     *           "description" = "The name of the project to install.",
171
     *           "required" = false
172
     *         }
173
     *       }
174
     *     }
175
     *   },
176
     *   response={
177
     *     "task" = {
178
     *       "dataType" = "string",
179
     *       "description" = "The id of the created install task"
180
     *     }
181
     *   }
182
     * )
183
     */
184
    public function createProjectAction(Request $request)
185
    {
186
        $status = $this->get('tenside.status');
187
        if ($status->isProjectPresent() || $status->isProjectInstalled()) {
188
            throw new NotAcceptableHttpException('Already configured.');
189
        }
190
191
        $this->checkUninstalled();
192
        $result = [];
193
        $header = [];
194
195
        $installDir = $this->get('tenside.home')->homeDir();
196
        $dataDir    = $this->get('tenside.home')->tensideDataDir();
197
        $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\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...
198
        $taskData   = new JsonArray();
199
200
        $taskData->set(InstallTask::SETTING_DESTINATION_DIR, $installDir);
201
        $taskData->set(InstallTask::SETTING_PACKAGE, $inputData->get('project/name'));
202
        if ($version = $inputData->get('project/version')) {
203
            $taskData->set(InstallTask::SETTING_VERSION, $version);
204
        }
205
206
        $taskId             = $this->getTensideTasks()->queue('install', $taskData);
207
        $result['task']     = $taskId;
208
        $header['Location'] = $this->generateUrl(
209
            'task_get',
210
            ['taskId' => $taskId],
211
            UrlGeneratorInterface::ABSOLUTE_URL
212
        );
213
214
        try {
215
            $this->runInstaller($taskId);
216
        } catch (\Exception $e) {
217
            // Error starting the install task, roll back and output the error.
218
            $fileSystem = new Filesystem();
219
            $fileSystem->remove($installDir . DIRECTORY_SEPARATOR . 'composer.json');
220
            $fileSystem->remove(
221
                array_map(
222
                    function ($file) use ($dataDir) {
223
                        return $dataDir . DIRECTORY_SEPARATOR . $file;
224
                    },
225
                    [
226
                        'tenside-tasks.json',
227
                        'tenside-task-' . $taskId . '.json',
228
                        'tenside-task-' . $taskId . '.json~'
229
                    ]
230
                )
231
            );
232
233
            return new JsonResponse(
234
                [
235
                    'status'  => 'ERROR',
236
                    'message' => 'The install task could not be started.'
237
                ],
238
                JsonResponse::HTTP_INTERNAL_SERVER_ERROR
239
            );
240
        }
241
242
        return new JsonResponse(
243
            [
244
                'status' => 'OK',
245
                'task'   => $taskId
246
            ],
247
            JsonResponse::HTTP_CREATED,
248
            $header
249
        );
250
    }
251
252
    /**
253
     * This is a gateway to the self test controller available only at install time.
254
     *
255
     * This is just here as the other route is protected with login.
256
     * This method is inaccessible as soon as the installation is complete.
257
     *
258
     * @return JsonResponse
259
     *
260
     * @ApiDoc(
261
     *   section="install",
262
     *   description="Install time - self test."
263
     * )
264
     * @ApiDescription(
265
     *   response={
266
     *     "results" = {
267
     *       "actualType" = "collection",
268
     *       "subType" = "object",
269
     *       "description" = "The test results.",
270
     *       "children" = {
271
     *         "name" = {
272
     *           "dataType" = "string",
273
     *           "description" = "The name of the test"
274
     *         },
275
     *         "state" = {
276
     *           "dataType" = "choice",
277
     *           "description" = "The test result state.",
278
     *           "format" = "[FAIL|SKIPPED|SUCCESS|WARNING]"
279
     *         },
280
     *         "message" = {
281
     *           "dataType" = "string",
282
     *           "description" = "The detailed message of the test result."
283
     *         },
284
     *         "explain" = {
285
     *           "dataType" = "string",
286
     *           "description" = "Optional description that could hint any problems and/or explain the error further."
287
     *         }
288
     *       }
289
     *     }
290
     *   }
291
     * )
292
     */
293
    public function getSelfTestAction()
294
    {
295
        $this->checkUninstalled();
296
297
        return $this->forward('TensideCoreBundle:SelfTest:getAllTests');
298
    }
299
300
    /**
301
     * Install time gateway to the auto config.
302
     *
303
     * This is just here as the other route is protected with login.
304
     * This method is inaccessible as soon as the installation is complete.
305
     *
306
     * @return JsonResponse
307
     *
308
     * @ApiDoc(
309
     *   section="install",
310
     *   description="Install time - auto config."
311
     * )
312
     * @ApiDescription(
313
     *   response={
314
     *     "php_cli" = {
315
     *       "dataType" = "string",
316
     *       "description" = "The PHP interpreter to run on command line."
317
     *     },
318
     *     "php_cli_arguments" = {
319
     *       "dataType" = "string",
320
     *       "description" = "Command line arguments to add."
321
     *     }
322
     *   }
323
     * )
324
     */
325
    public function getAutoConfigAction()
326
    {
327
        $this->checkUninstalled();
328
329
        return $this->forward('TensideCoreBundle:SelfTest:getAutoConfig');
330
    }
331
332
    /**
333
     * Retrieve the available versions of a package.
334
     *
335
     * @param string $vendor  The vendor name of the package.
336
     *
337
     * @param string $project The name of the package.
338
     *
339
     * @return JsonResponse
340
     *
341
     * @ApiDoc(
342
     *   section="install",
343
     *   statusCodes = {
344
     *     200 = "When everything worked out ok"
345
     *   }
346
     * )
347
     * @ApiDescription(
348
     *   response={
349
     *     "versions" = {
350
     *       "actualType" = "collection",
351
     *       "subType" = "object",
352
     *       "description" = "The list of versions",
353
     *       "children" = {
354
     *         "name" = {
355
     *           "dataType" = "string",
356
     *           "description" = "The name of the package"
357
     *         },
358
     *         "version" = {
359
     *           "dataType" = "string",
360
     *           "description" = "The version of the package"
361
     *         },
362
     *         "version_normalized" = {
363
     *           "dataType" = "string",
364
     *           "description" = "The normalized version of the package"
365
     *         },
366
     *         "reference" = {
367
     *           "dataType" = "string",
368
     *           "description" = "The optional reference"
369
     *         }
370
     *       }
371
     *     }
372
     *   }
373
     * )
374
     */
375
    public function getProjectVersionsAction($vendor, $project)
376
    {
377
        $this->checkUninstalled();
378
379
        // FIXME: we only search the packagist API here.
0 ignored issues
show
Coding Style introduced by
Comment refers to a FIXME task "we only search the packagist API here"
Loading history...
380
        $url     = sprintf('https://packagist.org/packages/%s/%s.json', $vendor, $project);
381
        $rfs     = new RemoteFilesystem($this->getInputOutput());
382
        $results = $rfs->getContents($url, $url);
383
        $data    = new JsonArray($results);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $rfs->getContents($url, $url) on line 382 can also be of type boolean; however, Tenside\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...
384
385
        $versions = [];
386
387
        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...
388
            $version = [
389
                'name'               => $information['name'],
390
                'version'            => $information['version'],
391
                'version_normalized' => $information['version_normalized'],
392
            ];
393
394
            $normalized = $information['version'];
395
            if ('dev-' === substr($normalized, 0, 4)) {
396
                if (isset($information['extra']['branch-alias'][$normalized])) {
397
                    $version['version_normalized'] = $information['extra']['branch-alias'][$normalized];
398
                }
399
            }
400
401 View Code Duplication
            if (isset($information['source']['reference'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
402
                $version['reference'] = $information['source']['reference'];
403
            } elseif (isset($information['dist']['reference'])) {
404
                $version['reference'] = $information['dist']['reference'];
405
            }
406
407
            $versions[] = $version;
408
        }
409
410
        return new JsonResponse(
411
            [
412
                'status' => 'OK',
413
                'versions' => $versions
414
            ]
415
        );
416
    }
417
418
    /**
419
     * Check if installation is new, partial or complete.
420
     *
421
     * @return JsonResponse
422
     *
423
     * @ApiDoc(
424
     *   section="install",
425
     *   description="This method provides information about the installation.",
426
     *   authentication=false,
427
     *   statusCodes = {
428
     *     200 = "When everything worked out ok"
429
     *   }
430
     * )
431
     * @ApiDescription(
432
     *   response={
433
     *     "state" = {
434
     *       "children" = {
435
     *         "tenside_configured" = {
436
     *           "dataType" = "bool",
437
     *           "description" = "Flag if tenside has been completely configured."
438
     *         },
439
     *         "project_created" = {
440
     *           "dataType" = "bool",
441
     *           "description" = "Flag determining if a composer.json is present."
442
     *         },
443
     *         "project_installed" = {
444
     *           "dataType" = "bool",
445
     *           "description" = "Flag determining if the composer project has been installed (vendor present)."
446
     *         }
447
     *       }
448
     *     },
449
     *     "status" = {
450
     *       "dataType" = "string",
451
     *       "description" = "Either OK or ERROR"
452
     *     },
453
     *     "message" = {
454
     *       "dataType" = "string",
455
     *       "description" = "The API error message if any (only present when status is ERROR)"
456
     *     }
457
     *   }
458
     * )
459
     */
460
    public function getInstallationStateAction()
461
    {
462
        $status = $this->get('tenside.status');
463
464
        return new JsonResponse(
465
            [
466
                'state'  => [
467
                    'tenside_configured' => $status->isTensideConfigured(),
468
                    'project_created'    => $status->isProjectPresent(),
469
                    'project_installed'  => $status->isProjectInstalled(),
470
                ],
471
                'status' => 'OK'
472
            ]
473
        );
474
    }
475
476
    /**
477
     * Ensure that we are not installed yet.
478
     *
479
     * @return void
480
     *
481
     * @throws NotAcceptableHttpException When the installation is already complete.
482
     */
483
    private function checkUninstalled()
484
    {
485
        if ($this->get('tenside.status')->isComplete()) {
486
            throw new NotAcceptableHttpException('Already installed in ' . $this->get('tenside.home')->homeDir());
487
        }
488
    }
489
490
    /**
491
     * Run the given task and return a response when an error occurred or null if it worked out.
492
     *
493
     * @param string $taskId The task id.
494
     *
495
     * @return void
496
     *
497
     * @throws \RuntimeException When the process could not be started.
498
     */
499
    private function runInstaller($taskId)
500
    {
501
        $runnerResponse = $this->forward('TensideCoreBundle:TaskRunner:run');
502
503
        $runnerStarted = json_decode($runnerResponse->getContent(), true);
504
        if ($runnerStarted['status'] !== 'OK' || $runnerStarted['task'] !== $taskId) {
505
            throw new \RuntimeException('Status was not ok');
506
        }
507
    }
508
}
509