Completed
Push — master ( d66b80...474a37 )
by Konrad
05:52
created

Aggregator::clearProjectData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Xima\DepmonBundle\Service;
4
5
use Psr\Log\LoggerInterface;
6
use Symfony\Component\Cache\Adapter\AdapterInterface;
7
use Symfony\Component\Process\Exception\ProcessFailedException;
8
use Symfony\Component\Process\Process;
9
use Xima\DepmonBundle\Util\VersionHelper;
10
11
/**
12
 * Class Aggregator
13
 * @package Xima\DepmonBundle\Service
14
 */
15
class Aggregator
16
{
17
18
    /**
19
     * @var array
20
     * ToDo: Move them to the config
21
     * ToDo: Not really good coverage at all ...
22
     */
23
    private $projectTypes = [
24
        'typo3/cms' => 'TYPO3',
25
        'symfony/symfony' => 'Symfony',
26
        'drupal/drupal' => 'Drupal'
27
    ];
28
29
30
    /**
31
     * Get dependency data for a specific project by cloning the project composer files in the cache dir, installing
32
     * all dependencies (necessary for using composer show) and fetching the dependency information by "composer show"
33
     * ToDo: Check if git/composer is installed
34
     * ToDo: Optionally remove vendors after composer show was running
35
     *
36
     * @param array $project
37
     * @throws \Exception
38
     * @return array
39
     */
40
    public function fetchProjectData($project): array
41
    {
42
43
        $projectName = $project['name'];
44
45
        // Check if git is installed
46
        $process = new Process('command -v git');
47
        $process->run();
48
49
        if ($process->getOutput() == '') {
50
            throw new \Exception('Git is not installed!');
51
        }
52
53
        // Check if composer is installed
54
        $process = new Process('command -v composer');
55
        $process->run();
56
57
        if ($process->getOutput() == '') {
58
            throw new \Exception('Composer is not installed!');
59
        }
60
61
        // If project already exists, just pull updates. Otherwise clone the repository.
62
        // ToDo: "git reset" pulls every file of the git
63
        if (is_dir('var/data/' . $project['name'])) {
64
            $process = new Process('cd var/data/' . $project['name'] . ' && git reset --hard origin/master');
65
        } else {
66
            $process = new Process('git clone -n ' . $project['git'] . ' var/data/' . $project['name'] . ' --depth 1 -b master --single-branch');
67
        }
68
69
        $process->setTimeout(3600);
70
        $process->run();
71
72
        if (!$process->isSuccessful()) {
73
            throw new ProcessFailedException($process);
74
        }
75
76
        // Check if composer.json exists in project
77
        $process = new Process(
78
            'cd var/data/' . $project['name'] . '/ && ' .
79
            'git cat-file -e origin/master:' . $project['path'] . 'composer.json && echo true'
80
        );
81
        $process->run();
82
83
        if (trim($process->getOutput()) != 'true') {
84
            throw new \Exception('No composer.json found in the project ' . $projectName);
85
        }
86
87
        // Check if composer.lock exists in project
88
        $process = new Process(
89
            'cd var/data/' . $project['name'] . '/ && ' .
90
            'git cat-file -e origin/master:' . $project['path'] . 'composer.lock && echo true'
91
        );
92
        $process->run();
93
        $composerLock = false;
94
95
        if (trim($process->getOutput()) == 'true') {
96
            $composerLock = true;
97
        }
98
99
        // Preparing composer project setup
100
        // ToDo: Is it possible to combine multiple processes in a better way?
101
        $process = new Process(
102
            'cd var/data/' . $project['name'] . '/ && ' .
103
            // Checkout composer.json file
104
            'git checkout HEAD ' . $project['path'] . 'composer.json && ' .
105
            // Checkout composer.lock file
106
            (($composerLock) ? 'git checkout HEAD ' . $project['path'] . 'composer.lock && ' : '') .
107
            // Change directory
108
            (($project['path'] != '') ? 'cd ' . $project['path'] . ' && ' : '') .
109
            // Install composer dependencies
110
            'composer install --no-dev --no-autoloader --no-scripts --ignore-platform-reqs'
111
        );
112
113
        $process->setTimeout(3600);
114
        $process->run();
115
116
        if (!$process->isSuccessful()) {
117
            throw new ProcessFailedException($process);
118
        }
119
120
        $process = new Process(
121
            'cd var/data/' . $project['name'] . ' && ' .
122
            'git describe --tags $(git rev-list --tags --max-count=1)'
123
        );
124
        $process->run();
125
        $gitTag = $process->getOutput();
126
127
        $process = new Process(
128
            'cd var/data/' . $project['name'] . '/' . $project['path'] . ' && ' .
129
            'composer show --latest --minor-only --format json'
130
        );
131
        $process->run();
132
133
        if (!$process->isSuccessful()) {
134
            throw new ProcessFailedException($process);
135
        }
136
137
        $data = json_decode($process->getOutput());
138
139
        if (empty($data)) {
140
            throw new \Exception('Empty result of "composer show" for project ' . $projectName);
141
        }
142
143
        $process = new Process(
144
            'curl -H "Accept: application/json" https://security.sensiolabs.org/check_lock -F lock=@var/data/' . $project['name'] . '/' . $project['path'] . 'composer.lock'
145
        );
146
        $process->run();
147
148
        if (!$process->isSuccessful()) {
149
            throw new ProcessFailedException($process);
150
        }
151
        $vulnerabilities = json_decode($process->getOutput());
152
153
        //
154
        // Evaluate data
155
        //
156
157
        $result = [];
158
159
        // Saving composer information about project to result
160
        $result['composer'] = json_decode(file_get_contents('var/data/' . $project['name'] . '/' . $project['path'] . 'composer.json'));
161
        $result['self'] = $project;
162
163
        $requiredPackagesCount = 0;
164
        $statesCount = [
165
            VersionHelper::STATE_UP_TO_DATE => 0,
166
            VersionHelper::STATE_PINNED_OUT_OF_DATE => 0,
167
            VersionHelper::STATE_OUT_OF_DATE => 0,
168
            VersionHelper::STATE_INSECURE => 0
169
        ];
170
        $requiredStatesCount = [
171
            VersionHelper::STATE_UP_TO_DATE => 0,
172
            VersionHelper::STATE_PINNED_OUT_OF_DATE => 0,
173
            VersionHelper::STATE_OUT_OF_DATE => 0,
174
            VersionHelper::STATE_INSECURE => 0
175
        ];
176
        $projectState = VersionHelper::STATE_UP_TO_DATE;
177
178
        foreach ($data->installed as $dependency) {
179
            // Workaround for adding requirement information to dependencies
180
            foreach ($result['composer']->require as $name => $require) {
181
                if ($dependency->name == $name) {
182
                    $dependency->required = $require;
183
                    $requiredPackagesCount++;
184
                }
185
186
                // Which project type?
187
                if (array_key_exists($name, $this->projectTypes)) {
188
                    $result['self']['projectType'] = $this->projectTypes[$name];
189
                }
190
            }
191
192
            // Is dependency outdated?
193
            if (isset($dependency->latest)) {
194
                $require = isset($dependency->required) ? $dependency->required : null;
195
                $state = VersionHelper::compareVersions($dependency->version, $dependency->latest, $require);
196
                $statesCount[$state]++;
197
                if ($require) {
198
                    $requiredStatesCount[$state]++;
199
                }
200
                $dependency->state = $state;
201
            } else {
202
                $dependency->state = VersionHelper::STATE_UP_TO_DATE;
203
            }
204
205
            // Is dependency a security issue?
206
            foreach ($vulnerabilities as $name => $vulnerability) {
207
                if ($name == $dependency->name) {
208
                    $dependency->state = VersionHelper::STATE_INSECURE;
209
                    $array = [];
210
                    foreach ($vulnerability->advisories as $advisory) {
211
                        array_push($array, $advisory);
212
                    }
213
                    $dependency->vulnerability = $array;
214
                    if (isset($dependency->required)) {
215
                        $requiredStatesCount[VersionHelper::STATE_INSECURE]++;
216
                    } else {
217
                        $statesCount[VersionHelper::STATE_INSECURE]++;
218
                    }
219
                }
220
            }
221
222
            $result['dependencies'][] = $dependency;
223
        }
224
225
        if ($requiredStatesCount[4] > 0) {
226
            $projectState = VersionHelper::STATE_INSECURE;
227
        } elseif ($requiredStatesCount[3] > 2) {
228
            $projectState = VersionHelper::STATE_OUT_OF_DATE;
229
        } elseif ($requiredStatesCount[3] <= 2 && $requiredStatesCount[2] >= 1) {
230
            $projectState = VersionHelper::STATE_PINNED_OUT_OF_DATE;
231
        }
232
233
        $metadata = [
234
            'requiredPackagesCount' => $requiredPackagesCount,
235
            'statesCount' => $statesCount,
236
            'requiredStatesCount' => $requiredStatesCount,
237
            'projectState' => $projectState,
238
            'gitTag' => $gitTag
239
        ];
240
241
        $result['meta'] = $metadata;
242
243
        return $result;
244
    }
245
246
    /**
247
     * @param $project
248
     */
249
    public function clearProjectData($project)
250
    {
251
        $process = new Process(
252
            'rm -rf var/data/' . $project['name']
253
        );
254
        $process->run();
255
    }
256
}