Completed
Push — master ( e0bc2b...e3741e )
by Mike
03:00
created

UpgradeChanges::getCheckSum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Sugarcrm\UpgradeSpec\Element\Section;
4
5
use Sugarcrm\UpgradeSpec\Data\DataAwareInterface;
6
use Sugarcrm\UpgradeSpec\Data\DataAwareTrait;
7
use Sugarcrm\UpgradeSpec\Element\ElementInterface;
8
use Sugarcrm\UpgradeSpec\Spec\Context;
9
use Sugarcrm\UpgradeSpec\Template\RendererAwareInterface;
10
use Sugarcrm\UpgradeSpec\Template\RendererAwareTrait;
11
use Symfony\Component\Finder\Finder;
12
13
class UpgradeChanges implements ElementInterface, RendererAwareInterface, DataAwareInterface
14
{
15
    use RendererAwareTrait, DataAwareTrait;
16
17
    /**
18
     * @return string
19
     */
20
    public function getTitle()
21
    {
22
        return 'Review upgrade changes and fix possible customization conflicts';
23
    }
24
25
    /**
26
     * @return int
27
     */
28
    public function getOrder()
29
    {
30
        return 3;
31
    }
32
33
    /**
34
     * @param Context $context
35
     *
36
     * @return bool
37
     */
38
    public function isRelevantTo(Context $context)
39
    {
40
        return $context->getPackagesPath()
41
            && $this->getFlavPackages($context->getFlav(), $context->getPackagesPath());
42
    }
43
44
    /**
45
     * @param Context $context
46
     *
47
     * @return string
48
     */
49
    public function getBody(Context $context)
50
    {
51
        $packages = $this->getSuitablePackages($context);
52
53
        $modified = $deleted = [];
54
        if ($packages) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $packages of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
55
            list($modified, $deleted) = $this->getChangedFiles('/Users/m.kamornikov/Dev/sugarcrm/build/rome/builds/ult/sugarcrm', $packages);
56
        }
57
58
        return $this->renderer->render('upgrade_changes', [
59
            'packages' => $packages,
60
            'upgrade_to' => $context->getUpgradeVersion(),
61
            'packages_path' => $context->getPackagesPath(),
62
            'modified_files' => $modified,
63
            'deleted_files' => $deleted,
64
        ]);
65
    }
66
67
    private function getChangedFiles($buildPath, $packages)
68
    {
69
        $changedFiles = $deletedFiles = $packageZips = [];
70
71
        foreach ($packages as $package) {
72
            $zip = new \ZipArchive();
73
            if (!$zip->open($package)) {
74
                throw new \RuntimeException(sprintf('Can\'t open zip archive: %s', $package));
75
            }
76
77
            $packageZips[$package] = $zip;
78
79
            eval(str_replace(['<?php', '<?', '?>'], '', $zip->getFromName(basename($package, '.zip') . DS . 'files.md5')));
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
80
            $packageChangedFiles = array_keys($md5_string);
0 ignored issues
show
Bug introduced by
The variable $md5_string does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
81
82
            if ($filesToRemove = $zip->getFromName('filesToRemove.txt')) {
83
                $packageDeletedFiles = explode(PHP_EOL, str_replace(["\r\n", "\r", "\n"], PHP_EOL, $filesToRemove));
84
            } else if ($filesToRemove = $zip->getFromName('filesToRemove.json')) {
85
                $packageDeletedFiles = json_decode($filesToRemove);
86
            } else {
87
                throw new \RuntimeException('Can\'t open filesToRemove');
88
            }
89
90
            $changedFiles = array_merge($changedFiles, array_combine($packageChangedFiles, array_fill(0, count($packageChangedFiles), $package)));
91
            $deletedFiles = array_diff(array_merge($deletedFiles, $packageDeletedFiles), $packageChangedFiles);
92
        }
93
94
        $changedFiles = array_keys(array_filter($changedFiles, function ($package, $changedFile) use ($buildPath, $packageZips) {
95
            if (($buildFile = @file_get_contents($buildPath . DS . $changedFile)) === false) {
96
                return false;
97
            }
98
            $packageFile = $packageZips[$package]->getFromName(basename($package, '.zip') . DS . $changedFile);
99
100
            return $this->getCheckSum($buildFile) != $this->getCheckSum($packageFile);
101
        }, ARRAY_FILTER_USE_BOTH));
102
103
        foreach ($packageZips as $zip) {
104
            $zip->close();
105
        }
106
107
        $changedFiles = array_values(array_filter($changedFiles, function ($file) use ($buildPath) {
108
            return file_exists($buildPath . '/custom/' . $file);
109
        }));
110
111
        $deletedFiles = array_values(array_filter($deletedFiles, function ($file) use ($buildPath) {
112
            return file_exists($buildPath . '/custom/' . $file);
113
        }));
114
115
        return [$changedFiles, $deletedFiles];
116
    }
117
118
    /**
119
     * @param $content
120
     *
121
     * @return string
122
     */
123
    private function getCheckSum($content)
124
    {
125
        // remove license comments
126
        $content = preg_replace('/\/\*.*?Copyright \(C\) SugarCRM.*?\*\//is', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
127
128
        // remove blank lines
129
        $content = preg_replace('/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/', "\n", $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
130
131
        // change all line breaks to system line break
132
        $content = str_replace(["\r\n", "\r", "\n"], PHP_EOL, $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
133
134
        // remove trailing whitespaces
135
        $content = preg_replace('/^\s+|\s+$/m', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
136
137
        return md5($content);
138
    }
139
140
    /**
141
     * @param Context $context
142
     *
143
     * @return array
144
     */
145
    private function getSuitablePackages(Context $context)
146
    {
147
        $chains = $this->getUpgradeChains(
148
            $this->getFlavPackages($context->getFlav(), $context->getPackagesPath()),
149
            $context->getBuildVersion(),
150
            $context->getUpgradeVersion()
151
        );
152
153
        if (!$chains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
154
            return [];
155
        }
156
157
        usort($chains, function ($a1, $a2) {
158
            return count($a1) < count($a2) ? -1 : (count($a1) > count($a2) ? 1 : 0);
159
        });
160
161
        return $chains[0];
162
    }
163
164
    /**
165
     * Gets flav specific packages
166
     *
167
     * @param $flav
168
     * @param $packagesPath
169
     *
170
     * @return array
171
     */
172
    private function getFlavPackages($flav, $packagesPath)
173
    {
174
        $versionPattern = '/\d+\.\d+(\.\d+|\.x){1,2}/';
175
        $packagePattern = sprintf('/^Sugar%1$s-Upgrade-%2$s-to-%2$s.zip$/',
176
            ucfirst(mb_strtolower($flav)),
177
            trim($versionPattern, '/')
178
        );
179
180
        $packagesIterator = (new Finder())->files()->in($packagesPath)->name($packagePattern);
181
182
        $packages = [];
183
        foreach ($packagesIterator as $package) {
184
            if (preg_match_all($versionPattern, $package, $matches)) {
185
                $packages[$package->getFilename()] = [
186
                    'path' => $package->getRealPath(),
187
                    'from' => str_replace('.x', '', $matches[0][0]),
188
                    'to' => str_replace('.x', '', $matches[0][1])
189
                ];
190
            }
191
        }
192
193
        return $packages;
194
    }
195
196
    /**
197
     * Calculates an array of possible upgrade chains
198
     *
199
     * @param $packages
200
     * @param $buildVersion
201
     * @param $upgradeTo
202
     *
203
     * @return array
204
     */
205
    private function getUpgradeChains($packages, $buildVersion, $upgradeTo)
206
    {
207
        $versionMatrix = $this->getVersionMatrix($packages);
208
        $allVersions = array_keys($versionMatrix);
209
210
        $getExistingSubversions = function ($version) use ($allVersions) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $getExistingSubversions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
211
            $existingVersions = [];
212
213
            $fromParts = explode('.', $version);
214
            foreach (range(2, count($fromParts)) as $length) {
215
                $subversion = implode('.', array_slice($fromParts, 0, $length));
216
                if (in_array($subversion, $allVersions)) {
217
                    $existingVersions[] = $subversion;
218
                }
219
            }
220
221
            return $existingVersions;
222
        };
223
224
        // init chains with starting versions
225
        $chains = array_map(function ($version) use ($buildVersion) {
226
            return [$version => $buildVersion];
227
        }, $getExistingSubversions($buildVersion));
228
229
        // finish early if starting / ending version doesn't exist
230
        if (!$chains || !in_array($upgradeTo, $allVersions)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
231
            return [];
232
        }
233
234
        // gets last key of assoc array
235
        $getLastKey = function ($array) {
236
            end($array);
237
238
            return key($array);
239
        };
240
241
        // find all chains
242
        while (true) {
243
            $fullChains = [];
244
            foreach ($chains as $index => $chain) {
245
                $fromVersion = $getLastKey($chain);
246
247
                // skip not interesting chains
248
                if (version_compare($fromVersion, $upgradeTo, '>=')) {
249
                    continue;
250
                }
251
252
                $validChain = false;
253
                foreach ($allVersions as $version) {
254
                    if (!empty($versionMatrix[$fromVersion][$version])) {
255
                        $to = $getExistingSubversions($version);
256
                        foreach ($to as $toVersion) {
257
258
                            if ($toVersion === $fromVersion
259
                                || version_compare($toVersion, $upgradeTo, '>')
260
                                || $chain[$getLastKey($chain)] === $version
261
                            ) {
262
                                continue;
263
                            }
264
265
                            $validChain = true;
266
                            $fullChains[] = array_merge($chain, [$toVersion => $version]);
267
                        }
268
                    }
269
                }
270
271
                // remove invalid chain
272
                if (!$validChain) {
273
                    unset($chains[$index]);
274
                }
275
            }
276
277
            if (!$fullChains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fullChains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
278
                break;
279
            }
280
281
            $chains = $fullChains;
282
        }
283
284
        $chains = array_map(function ($chain) use ($versionMatrix) {
285
            $keys = array_keys($chain);
286
            $values = array_values($chain);
287
288
            $packages = [];
289
            foreach (range(1, count($keys) - 1) as $index) {
290
                $packages[] = $versionMatrix[$keys[$index - 1]][$values[$index]];
291
            }
292
293
            return $packages;
294
        }, array_values($chains));
295
296
        return $chains;
297
    }
298
299
    /**
300
     * Creates version matrix
301
     *
302
     *       v1       v2       v3       v4
303
     *  v1    0     <path>   <path>      0
304
     *  v2    0        0     <path>      0
305
     *  v3    0        0        0     <path>
306
     *  v4    0        0        0        0
307
     *
308
     * @param $packages
309
     *
310
     * @return array
311
     */
312
    private function getVersionMatrix($packages)
313
    {
314
        $allVersions = array_unique(call_user_func_array('array_merge', array_map(function ($package) {
315
            return [$package['from'], $package['to']];
316
        }, $packages)));
317
318
        // sort versions (ASC)
319 View Code Duplication
        usort($allVersions, function ($v1, $v2) {
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...
320
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
321
        });
322
323
        // create matrix and fill it with zeros
324
        $versionMatrix = call_user_func_array('array_merge',array_map(function ($version) use ($allVersions) {
325
            return [$version => array_combine($allVersions, array_fill(0, count($allVersions), 0))];
326
        }, $allVersions));
327
328
        // valid associations point to package path
329
        foreach ($packages as $name => $package) {
330
            $versionMatrix[$package['from']][$package['to']] = $package['path'];
331
        }
332
333
        return $versionMatrix;
334
    }
335
}
336