Completed
Push — master ( 580007...3ce141 )
by
unknown
9s
created

src/Tasks/CheckComposerUpdatesTask.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use BringYourOwnIdeas\Maintenance\Util\ComposerLoader;
4
use Packagist\Api\Client;
5
6
/**
7
 * Task which does the actual checking of updates
8
 *
9
 * Originally from https://github.com/XploreNet/silverstripe-composerupdates
10
 *
11
 * @author Matt Dwen
12
 * @license MIT
13
 */
14
class CheckComposerUpdatesTask extends BuildTask
15
{
16
    /**
17
     * @var string
18
     */
19
    protected $title = 'Composer update checker';
20
21
    /**
22
     * @var string
23
     */
24
    protected $description = 'Checks if any composer dependencies can be updated.';
25
26
    private static $dependencies = [
0 ignored issues
show
The property $dependencies is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
27
        'PackagistClient' => '%$Packagist\\Api\\Client',
28
        'ComposerLoader' => '%$BringYourOwnIdeas\\Maintenance\\Util\\ComposerLoader',
29
    ];
30
31
    /**
32
     * Minimum required stability defined in the site composer.json
33
     *
34
     * @var string
35
     */
36
    protected $minimumStability;
37
38
    /**
39
     * Whether or not to prefer stable packages
40
     *
41
     * @var bool
42
     */
43
    protected $preferStable;
44
45
    /**
46
     * Known stability values
47
     *
48
     * @var array
49
     */
50
    protected $stabilityOptions = array(
51
        'dev',
52
        'alpha',
53
        'beta',
54
        'rc',
55
        'stable'
56
    );
57
58
    /**
59
     * @var Client
60
     */
61
    protected $packagistClient;
62
63
    /**
64
     * @var ComposerLoader
65
     */
66
    protected $composerLoader;
67
68
    /**
69
     * Retrieve an array of primary composer dependencies from composer.json
70
     *
71
     * @return array
72
     */
73
    protected function getPackages()
74
    {
75
        $composerJson = $this->getComposerLoader()->getJson();
76
77
        ini_set('display_errors', 1);
78
        error_reporting(E_ALL);
79
80
        // Set the stability parameters
81
        $this->setMinimumStability(
82
            isset($composerJson->{'minimum-stability'}) ? $composerJson->{'minimum-stability'} : 'stable'
83
        );
84
85
        $this->setPreferStable(
86
            isset($composerJson->{'prefer-stable'}) ? $composerJson->{'prefer-stable'} : true
87
        );
88
89
        $packages = [];
90
        foreach ($composerJson->require as $package => $version) {
91
            // Ensure there's a / in the name, probably not an addon with it
92
            if (!strpos($package, '/')) {
93
                continue;
94
            }
95
96
            $packages[] = $package;
97
        }
98
99
        return $packages;
100
    }
101
102
    /**
103
     * Return an array of all Composer dependencies from composer.lock
104
     *
105
     * Example: `['silverstripe/cms' => '3.4.1', ...]`
106
     *
107
     * @return array
108
     */
109
    protected function getDependencies()
110
    {
111
        $packages = [];
112
        foreach ($this->getComposerLoader()->getLock()->packages as $package) {
113
            $packages[$package->name] = $package->version;
114
        }
115
        return $packages;
116
    }
117
118
    /**
119
     * Check if an available version is better than the current version,
120
     * considering stability requirements
121
     *
122
     * Returns FALSE if no update is available.
123
     * Returns the best available version if an update is available.
124
     *
125
     * @param string $currentVersion
126
     * @param string[] $availableVersions
127
     * @return bool|string
128
     */
129
    protected function hasUpdate($currentVersion, $availableVersions)
130
    {
131
        $currentVersion = strtolower($currentVersion);
132
133
        // Check there are some versions
134
        if (count($availableVersions) < 1) {
135
            return false;
136
        }
137
138
        // If this is dev-master, compare the hashes
139
        if ($currentVersion === 'dev-master') {
140
            return $this->hasUpdateOnDevMaster($availableVersions);
141
        }
142
143
        // Loop through each available version
144
        $currentStability = $this->getStability($currentVersion);
145
        $bestVersion = $currentVersion;
146
        $bestStability = $currentStability;
147
        $availableVersions = array_reverse($availableVersions, true);
148
        foreach ($availableVersions as $version => $details) {
149
            // Get the stability of the version
150
            $versionStability = $this->getStability($version);
151
152
            // Does this meet minimum stability
153
            if (!$this->isStableEnough($this->getMinimumStability(), $versionStability)) {
154
                continue;
155
            }
156
157
            if ($this->getPreferStable()) {
158
                // A simple php version compare rules out the dumb stuff
159
                if (version_compare($bestVersion, $version) !== -1) {
160
                    continue;
161
                }
162
            } else {
163
                // We're doing a straight version compare
164
                $pureBestVersion = $this->getPureVersion($bestVersion);
165
                $pureVersion = $this->getPureVersion($version);
166
167
                // Checkout the version
168
                $continue = false;
169
                switch (version_compare($pureBestVersion, $pureVersion)) {
170
                    case -1:
171
                        // The version is better, take it
172
                        break;
173
174
                    case 0:
175
                        // The version is the same.
176
                        // Do another straight version compare to rule out rc1 vs rc2 etc...
177
                        if ($bestStability == $versionStability) {
178
                            if (version_compare($bestVersion, $version) !== -1) {
179
                                $continue = true;
180
                                break;
181
                            }
182
                        }
183
                        break;
184
185
                    case 1:
186
                        // The version is worse, ignore it
187
                        $continue = true;
188
                        break;
189
                }
190
191
                if ($continue) {
192
                    continue;
193
                }
194
            }
195
196
            $bestVersion = $version;
197
            $bestStability = $versionStability;
198
        }
199
200
        if ($bestVersion !== $currentVersion || $bestStability !== $currentStability) {
201
            if ($bestStability === 'stable') {
202
                return $bestVersion;
203
            }
204
205
            return $bestVersion . '-' . $bestStability;
206
        }
207
208
        return false;
209
    }
210
211
    /**
212
     * Check the latest hash on the dev-master branch, and return it if different to the local hash
213
     *
214
     * FALSE is returned if the hash is the same.
215
     *
216
     * @param $availableVersions
217
     * @return bool|string
218
     */
219
    protected function hasUpdateOnDevMaster($availableVersions)
220
    {
221
        // Get the dev-master version
222
        $devMaster = $availableVersions['dev-master'];
223
224
        // Sneak the name of the package
225
        $packageName = $devMaster->getName();
226
227
        // Get the local package details
228
        $localPackage = $this->getLocalPackage($packageName);
229
230
        // What's the current hash?
231
        $localHash = $localPackage->source->reference;
232
233
        // What's the latest hash in the available versions
234
        $remoteHash = $devMaster->getSource()->getReference();
235
236
        // return either the new hash or false
237
        return ($localHash != $remoteHash) ? $remoteHash : false;
238
    }
239
240
    /**
241
     * Return details from composer.lock for a specific package
242
     *
243
     * @param string $packageName
244
     * @return object
245
     * @throws Exception if package cannot be found in composer.lock
246
     */
247
    protected function getLocalPackage($packageName)
248
    {
249
        foreach ($this->getComposerLoader()->getLock()->packages as $package) {
250
            if ($package->name == $packageName) {
251
                return $package;
252
            }
253
        }
254
255
        throw new Exception('Cannot locate local package ' . $packageName);
256
    }
257
258
    /**
259
     * Retrieve the pure numerical version
260
     *
261
     * @param string $version
262
     * @return string|null
263
     */
264
    protected function getPureVersion($version)
265
    {
266
        $matches = [];
267
268
        preg_match("/^(\d+\\.)?(\d+\\.)?(\\*|\d+)/", $version, $matches);
269
270
        if (count($matches) > 0) {
271
            return $matches[0];
272
        }
273
274
        return null;
275
    }
276
277
    /**
278
     * Determine the stability of a given version
279
     *
280
     * @param string $version
281
     * @return string
282
     */
283
    protected function getStability($version)
284
    {
285
        $version = strtolower($version);
286
287
        foreach ($this->getStabilityOptions() as $option) {
288
            if (strpos($version, $option) !== false) {
289
                return $option;
290
            }
291
        }
292
293
        return 'stable';
294
    }
295
296
    /**
297
     * Return a numerical representation of a stability
298
     *
299
     * Higher is more stable
300
     *
301
     * @param string $stability
302
     * @return int
303
     * @throws Exception If the stability is unknown
304
     */
305
    protected function getStabilityIndex($stability)
306
    {
307
        $stability = strtolower($stability);
308
309
        $index = array_search($stability, $this->getStabilityOptions(), true);
310
311
        if ($index === false) {
312
            throw new Exception("Unknown stability: $stability");
313
        }
314
315
        return $index;
316
    }
317
318
    /**
319
     * Check if a stability meets a given minimum requirement
320
     *
321
     * @param $currentStability
322
     * @param $possibleStability
323
     * @return bool
324
     */
325
    protected function isStableEnough($currentStability, $possibleStability)
326
    {
327
        $minimumIndex = $this->getStabilityIndex($currentStability);
328
        $possibleIndex = $this->getStabilityIndex($possibleStability);
329
330
        return ($possibleIndex >= $minimumIndex);
331
    }
332
333
    /**
334
     * Record package details in the database
335
     *
336
     * @param string $package Name of the Composer Package
337
     * @param string $installed Currently installed version
338
     * @param string|boolean $latest The latest available version
339
     */
340
    protected function recordUpdate($package, $installed, $latest)
341
    {
342
        // Is there a record already for the package? If so find it.
343
        $packages = Package::get()->filter(['Name' => $package]);
344
345
        // Get the hash installed
346
        $localPackage = $this->getLocalPackage($package);
347
        $installedHash = $localPackage->source->reference;
348
349
        // if there is already one use it otherwise create a new data object
350
        if ($packages->count() > 0) {
351
            $update = $packages->first();
352
        } else {
353
            $update = Package::create();
354
            $update->Name = $package;
355
            $update->Version = $installed;
356
            $update->VersionHash = $installedHash;
357
        }
358
359
        // Set the new details and save it
360
        $update->LatestVersion = $latest;
361
        $update->write();
362
    }
363
364
    /**
365
     * runs the actual steps to verify if there are updates available
366
     *
367
     * @param SS_HTTPRequest $request
368
     */
369
    public function run($request)
370
    {
371
        // Retrieve the packages
372
        $packages = $this->getPackages();
373
        $dependencies = $this->getDependencies();
374
375
        // run through the packages and check each for updates
376
        foreach ($packages as $package) {
377
            // verify that we need to check this package.
378
            if (!isset($dependencies[$package])) {
379
                continue;
380
            } else {
381
                // get information about this package from packagist.
382
                try {
383
                    $latest = $this->getPackagistClient()->get($package);
384
                } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
0 ignored issues
show
The class Guzzle\Http\Exception\ClientErrorResponseException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
385
                    SS_Log::log($e->getMessage(), SS_Log::WARN);
386
                    continue;
387
                }
388
389
                // Check if there is a newer version
390
                $currentVersion = $dependencies[$package];
391
                $result = $this->hasUpdate($currentVersion, $latest->getVersions());
392
393
                // Check if there is a newer version and if so record the update
394
                if ($result !== false) {
395
                    $this->recordUpdate($package, $currentVersion, $result);
396
                }
397
            }
398
        }
399
400
        // finished message
401
        $this->message('The task finished running. You can find the updated information in the database now.');
402
    }
403
404
    /**
405
     * prints a message during the run of the task
406
     *
407
     * @param string $text
408
     */
409
    protected function message($text)
410
    {
411
        if (!Director::is_cli()) {
412
            $text = '<p>' . $text . '</p>';
413
        }
414
415
        echo $text . PHP_EOL;
416
    }
417
418
    /**
419
     * @param string $minimumStability
420
     * @return $this
421
     */
422
    public function setMinimumStability($minimumStability)
423
    {
424
        $this->minimumStability = $minimumStability;
425
        return $this;
426
    }
427
428
    /**
429
     * @return string
430
     */
431
    public function getMinimumStability()
432
    {
433
        return $this->minimumStability;
434
    }
435
436
    /**
437
     * @param bool $preferStable
438
     * @return $this
439
     */
440
    public function setPreferStable($preferStable)
441
    {
442
        $this->preferStable = $preferStable;
443
        return $this;
444
    }
445
446
    /**
447
     * @return bool
448
     */
449
    public function getPreferStable()
450
    {
451
        return $this->preferStable;
452
    }
453
454
    /**
455
     * @param array $stabilityOptions
456
     * @return $this
457
     */
458
    public function setStabilityOptions($stabilityOptions)
459
    {
460
        $this->stabilityOptions = $stabilityOptions;
461
        return $this;
462
    }
463
464
    /**
465
     * @return array
466
     */
467
    public function getStabilityOptions()
468
    {
469
        return $this->stabilityOptions;
470
    }
471
472
    /**
473
     * @return Client
474
     */
475
    public function getPackagistClient()
476
    {
477
        return $this->packagistClient;
478
    }
479
480
    /**
481
     * @param Client $packagistClient
482
     */
483
    public function setPackagistClient(Client $packagistClient)
484
    {
485
        $this->packagistClient = $packagistClient;
486
        return $this;
487
    }
488
489
    /**
490
     * @param ComposerLoader $composerLoader
491
     * @return $this
492
     */
493
    public function setComposerLoader(ComposerLoader $composerLoader)
494
    {
495
        $this->composerLoader = $composerLoader;
496
        return $this;
497
    }
498
499
    /**
500
     * @return ComposerLoader
501
     */
502
    public function getComposerLoader()
503
    {
504
        return $this->composerLoader;
505
    }
506
}
507