Completed
Push — master ( ba30b6...904d28 )
by Christian
13:26
created

getProjectVersionsAction()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 42
Code Lines 24

Duplication

Lines 5
Ratio 11.9 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
dl 5
loc 42
rs 8.439
c 2
b 0
f 1
cc 6
eloc 24
nc 10
nop 2
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
     * Create a project.
45
     *
46
     * @param Request $request The request.
47
     *
48
     * @return JsonResponse
49
     *
50
     * @ApiDoc(
51
     *   section="install",
52
     *   statusCodes = {
53
     *     201 = "When everything worked out ok"
54
     *   },
55
     * )
56
     * @ApiDescription(
57
     *   request={
58
     *     "project" = {
59
     *       "description" = "The name of the project to install.",
60
     *       "children" = {
61
     *         "name" = {
62
     *           "dataType" = "string",
63
     *           "description" = "The name of the project to install.",
64
     *           "required" = true
65
     *         },
66
     *         "version" = {
67
     *           "dataType" = "string",
68
     *           "description" = "The name of the project to install.",
69
     *           "required" = false
70
     *         }
71
     *       }
72
     *     },
73
     *     "credentials" = {
74
     *       "description" = "The name of the project to install.",
75
     *       "children" = {
76
     *         "secret" = {
77
     *           "dataType" = "string",
78
     *           "description" = "The secret to use for encryption and signing.",
79
     *           "required" = true
80
     *         },
81
     *         "username" = {
82
     *           "dataType" = "string",
83
     *           "description" = "The name of the admin user.",
84
     *           "required" = true
85
     *         },
86
     *         "password" = {
87
     *           "dataType" = "string",
88
     *           "description" = "The password to use for the admin.",
89
     *           "required" = false
90
     *         }
91
     *       }
92
     *     }
93
     *   },
94
     *   response={
95
     *     "token" = {
96
     *       "dataType" = "string",
97
     *       "description" = "The API token for the created user"
98
     *     },
99
     *     "task" = {
100
     *       "dataType" = "string",
101
     *       "description" = "The id of the created install task"
102
     *     }
103
     *   }
104
     * )
105
     */
106
    public function createProjectAction(Request $request)
107
    {
108
        // FIXME: We definately should split this here up into config method and installation method which is already
0 ignored issues
show
Coding Style introduced by
Comment refers to a FIXME task "We definately should split this here up into config method and installation method which is already"
Loading history...
109
        // auth'ed.
110
        $this->checkUninstalled();
111
        $status = $this->get('tenside.status');
112
        $result = [];
113
        $header = [];
114
115
        $installDir = $this->get('tenside.home')->homeDir();
116
        $dataDir    = $this->get('tenside.home')->tensideDataDir();
117
        $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...
118
        $taskData   = new JsonArray();
119
120
        $taskData->set(InstallTask::SETTING_DESTINATION_DIR, $installDir);
121
        $taskData->set(InstallTask::SETTING_PACKAGE, $inputData->get('project/name'));
122
        if ($version = $inputData->get('project/version')) {
123
            $taskData->set(InstallTask::SETTING_VERSION, $version);
124
        }
125
126
        if (!$status->isTensideConfigured()) {
127
            // Add tenside configuration.
128
            $tensideConfig = $this->get('tenside.config');
129
            $tensideConfig->set('secret', $inputData->get('credentials/secret'));
130
131
            // Add the user now.
132
            $user = new UserInformation([
133
                'username' => $inputData->get('credentials/username'),
134
                'acl'      => UserInformationInterface::ROLE_ALL
135
            ]);
136
137
            $user->set(
138
                'password',
139
                $this->get('security.password_encoder')->encodePassword($user, $inputData->get('credentials/password'))
140
            );
141
142
            $user            = $this->get('tenside.user_provider')->addUser($user)->refreshUser($user);
143
            $result['token'] = $this->get('tenside.jwt_authenticator')->getTokenForData($user);
144
        }
145
146
        if (!$status->isProjectPresent() && !$status->isProjectInstalled()) {
147
            $taskId             = $this->getTensideTasks()->queue('install', $taskData);
148
            $result['task']     = $taskId;
149
            $header['Location'] = $this->generateUrl(
150
                'task_get',
151
                ['taskId' => $taskId],
152
                UrlGeneratorInterface::ABSOLUTE_URL
153
            );
154
155
            try {
156
                $this->runInstaller($taskId);
157
            } catch (\Exception $e) {
158
                // Error starting the install task, roll back and output the error.
159
                $fileSystem = new Filesystem();
160
                $fileSystem->remove($installDir . DIRECTORY_SEPARATOR . 'composer.json');
161
                $fileSystem->remove(
162
                    array_map(
163
                        function ($file) use ($dataDir) {
164
                            return $dataDir . DIRECTORY_SEPARATOR . $file;
165
                        },
166
                        [
167
                            'tenside.json',
168
                            'tenside.json~',
169
                            'tenside-tasks.json',
170
                            'tenside-task-' . $taskId . '.json',
171
                            'tenside-task-' . $taskId . '.json~'
172
                        ]
173
                    )
174
                );
175
176
                return new JsonResponse(
177
                    [
178
                        'status'  => 'ERROR',
179
                        'message' => 'The install task could not be started.'
180
                    ],
181
                    JsonResponse::HTTP_INTERNAL_SERVER_ERROR
182
                );
183
            }
184
        }
185
186
        return new JsonResponse(
187
            $result,
188
            JsonResponse::HTTP_CREATED,
189
            $header
190
        );
191
    }
192
193
    /**
194
     * This is a gateway to the self test controller available only at install time.
195
     *
196
     * This is just here as the other route is protected with login.
197
     * This method is inaccessible as soon as the installation is complete.
198
     *
199
     * @return JsonResponse
200
     *
201
     * @ApiDoc(
202
     *   section="install",
203
     *   description="Install time - self test."
204
     * )
205
     * @ApiDescription(
206
     *   response={
207
     *     "results" = {
208
     *       "actualType" = "collection",
209
     *       "subType" = "object",
210
     *       "description" = "The test results.",
211
     *       "children" = {
212
     *         "name" = {
213
     *           "dataType" = "string",
214
     *           "description" = "The name of the test"
215
     *         },
216
     *         "state" = {
217
     *           "dataType" = "choice",
218
     *           "description" = "The test result state.",
219
     *           "format" = "[FAIL|SKIPPED|SUCCESS|WARNING]"
220
     *         },
221
     *         "message" = {
222
     *           "dataType" = "string",
223
     *           "description" = "The detailed message of the test result."
224
     *         },
225
     *         "explain" = {
226
     *           "dataType" = "string",
227
     *           "description" = "Optional description that could hint any problems and/or explain the error further."
228
     *         }
229
     *       }
230
     *     }
231
     *   }
232
     * )
233
     */
234
    public function getSelfTestAction()
235
    {
236
        $this->checkUninstalled();
237
238
        return $this->forward('TensideCoreBundle:SelfTest:getAllTests');
239
    }
240
241
    /**
242
     * Install time gateway to the auto config.
243
     *
244
     * This is just here as the other route is protected with login.
245
     * This method is inaccessible as soon as the installation is complete.
246
     *
247
     * @return JsonResponse
248
     *
249
     * @ApiDoc(
250
     *   section="install",
251
     *   description="Install time - auto config."
252
     * )
253
     * @ApiDescription(
254
     *   response={
255
     *     "php_cli" = {
256
     *       "dataType" = "string",
257
     *       "description" = "The PHP interpreter to run on command line."
258
     *     },
259
     *     "php_cli_arguments" = {
260
     *       "dataType" = "string",
261
     *       "description" = "Command line arguments to add."
262
     *     }
263
     *   }
264
     * )
265
     */
266
    public function getAutoConfigAction()
267
    {
268
        $this->checkUninstalled();
269
270
        return $this->forward('TensideCoreBundle:SelfTest:getAutoConfig');
271
    }
272
273
    /**
274
     * Retrieve the available versions of a package.
275
     *
276
     * @param string $vendor  The vendor name of the package.
277
     *
278
     * @param string $project The name of the package.
279
     *
280
     * @return JsonResponse
281
     *
282
     * @ApiDoc(
283
     *   section="install",
284
     *   statusCodes = {
285
     *     200 = "When everything worked out ok"
286
     *   }
287
     * )
288
     * @ApiDescription(
289
     *   response={
290
     *     "versions" = {
291
     *       "actualType" = "collection",
292
     *       "subType" = "object",
293
     *       "description" = "The list of versions",
294
     *       "children" = {
295
     *         "name" = {
296
     *           "dataType" = "string",
297
     *           "description" = "The name of the package"
298
     *         },
299
     *         "version" = {
300
     *           "dataType" = "string",
301
     *           "description" = "The version of the package"
302
     *         },
303
     *         "version_normalized" = {
304
     *           "dataType" = "string",
305
     *           "description" = "The normalized version of the package"
306
     *         },
307
     *         "reference" = {
308
     *           "dataType" = "string",
309
     *           "description" = "The optional reference"
310
     *         }
311
     *       }
312
     *     }
313
     *   }
314
     * )
315
     */
316
    public function getProjectVersionsAction($vendor, $project)
317
    {
318
        $this->checkUninstalled();
319
320
        // 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...
321
        $url     = sprintf('https://packagist.org/packages/%s/%s.json', $vendor, $project);
322
        $rfs     = new RemoteFilesystem($this->getInputOutput());
323
        $results = $rfs->getContents($url, $url);
324
        $data    = new JsonArray($results);
0 ignored issues
show
Bug introduced by
It seems like $results defined by $rfs->getContents($url, $url) on line 323 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...
325
326
        $versions = [];
327
328
        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...
329
            $version = [
330
                'name'               => $information['name'],
331
                'version'            => $information['version'],
332
                'version_normalized' => $information['version_normalized'],
333
            ];
334
335
            $normalized = $information['version'];
336
            if ('dev-' === substr($normalized, 0, 4)) {
337
                if (isset($information['extra']['branch-alias'][$normalized])) {
338
                    $version['version_normalized'] = $information['extra']['branch-alias'][$normalized];
339
                }
340
            }
341
342 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...
343
                $version['reference'] = $information['source']['reference'];
344
            } elseif (isset($information['dist']['reference'])) {
345
                $version['reference'] = $information['dist']['reference'];
346
            }
347
348
            $versions[] = $version;
349
        }
350
351
        return new JsonResponse(
352
            [
353
                'status' => 'OK',
354
                'versions' => $versions
355
            ]
356
        );
357
    }
358
359
    /**
360
     * Check if installation is new, partial or complete.
361
     *
362
     * @return JsonResponse
363
     *
364
     * @ApiDoc(
365
     *   section="install",
366
     *   description="This method provides information about the installation.",
367
     *   authentication=false,
368
     *   statusCodes = {
369
     *     200 = "When everything worked out ok"
370
     *   }
371
     * )
372
     * @ApiDescription(
373
     *   response={
374
     *     "state" = {
375
     *       "children" = {
376
     *         "tenside_configured" = {
377
     *           "dataType" = "bool",
378
     *           "description" = "Flag if tenside has been completely configured."
379
     *         },
380
     *         "project_created" = {
381
     *           "dataType" = "bool",
382
     *           "description" = "Flag determining if a composer.json is present."
383
     *         },
384
     *         "project_installed" = {
385
     *           "dataType" = "bool",
386
     *           "description" = "Flag determining if the composer project has been installed (vendor present)."
387
     *         }
388
     *       }
389
     *     },
390
     *     "status" = {
391
     *       "dataType" = "string",
392
     *       "description" = "Either OK or ERROR"
393
     *     },
394
     *     "message" = {
395
     *       "dataType" = "string",
396
     *       "description" = "The API error message if any (only present when status is ERROR)"
397
     *     }
398
     *   }
399
     * )
400
     */
401
    public function getInstallationStateAction()
402
    {
403
        $status = $this->get('tenside.status');
404
405
        return new JsonResponse(
406
            [
407
                'state'  => [
408
                    'tenside_configured' => $status->isTensideConfigured(),
409
                    'project_created'    => $status->isProjectPresent(),
410
                    'project_installed'  => $status->isProjectInstalled(),
411
                ],
412
                'status' => 'OK'
413
            ]
414
        );
415
    }
416
417
    /**
418
     * Ensure that we are not installed yet.
419
     *
420
     * @return void
421
     *
422
     * @throws NotAcceptableHttpException When the installation is already complete.
423
     */
424
    private function checkUninstalled()
425
    {
426
        if ($this->get('tenside.status')->isComplete()) {
427
            throw new NotAcceptableHttpException('Already installed in ' . $this->get('tenside.home')->homeDir());
428
        }
429
    }
430
431
    /**
432
     * Run the given task and return a response when an error occurred or null if it worked out.
433
     *
434
     * @param string $taskId The task id.
435
     *
436
     * @return void
437
     *
438
     * @throws \RuntimeException When the process could not be started.
439
     */
440
    private function runInstaller($taskId)
441
    {
442
        $runnerResponse = $this->forward('TensideCoreBundle:TaskRunner:run');
443
444
        $runnerStarted = json_decode($runnerResponse->getContent(), true);
445
        if ($runnerStarted['status'] !== 'OK' || $runnerStarted['task'] !== $taskId) {
446
            throw new \RuntimeException('Status was not ok');
447
        }
448
    }
449
}
450