Completed
Push — master ( 5bfdc5...fda08e )
by Sebastien
03:04
created

ReleaseHistory::getStatusLabel()   C

Complexity

Conditions 10
Paths 10

Size

Total Lines 22
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 22
rs 6.1369
cc 10
eloc 19
nc 10
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Drush Cerbere command line tools.
5
 * Copyright (C) 2015 - Sebastien Malot <[email protected]>
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
 */
21
22
namespace Cerbere\Model;
23
24
use Doctrine\Common\Cache\CacheProvider;
25
26
/**
27
 * Class ReleaseHistory
28
 * @package Cerbere\Action
29
 */
30
class ReleaseHistory
31
{
32
    /**
33
     * Project is up to date.
34
     */
35
    const UPDATE_CURRENT = 5;
36
37
    /**
38
     * Project has a new release available, but it is not a security release.
39
     */
40
    const UPDATE_NOT_CURRENT = 4;
41
42
    /**
43
     * Current release is no longer supported by the project maintainer.
44
     */
45
    const UPDATE_NOT_SUPPORTED = 3;
46
    /**
47
     * Current release has been unpublished and is no longer available.
48
     */
49
    const UPDATE_REVOKED = 2;
50
51
    /**
52
     * Project is missing security update(s).
53
     */
54
    const UPDATE_NOT_SECURE = 1;
55
56
    /**
57
     * Project's status cannot be checked.
58
     */
59
    const UPDATE_NOT_CHECKED = -1;
60
61
    /**
62
     * No available update data was found for project.
63
     */
64
    const UPDATE_UNKNOWN = -2;
65
66
    /**
67
     * There was a failure fetching available update data for this project.
68
     */
69
    const UPDATE_NOT_FETCHED = -3;
70
71
    /**
72
     * We need to (re)fetch available update data for this project.
73
     */
74
    const UPDATE_FETCH_PENDING = -4;
75
76
    /**
77
     * @var string
78
     */
79
    protected $url;
80
81
    /**
82
     * @var array
83
     */
84
    protected $data;
85
86
    /**
87
     * @var CacheProvider
88
     */
89
    protected $cache;
90
91
    /**
92
     * @param CacheProvider $cache
93
     * @param string $url
94
     */
95
    public function __construct(CacheProvider $cache = null, $url = null)
96
    {
97
        $this->cache = $cache;
98
        $this->url = $url;
99
    }
100
101
    /**
102
     * @param Project $project
103
     */
104
    public function compare(Project $project)
105
    {
106
        // If the project status is marked as something bad, there's nothing else
107
        // to consider.
108
        if ($this->getProjectStatus()) {
109
            switch ($this->getProjectStatus()) {
110
                case 'insecure':
111
                    $project->setStatus(self::UPDATE_NOT_SECURE);
112
                    break;
113
                case 'unpublished':
114
                case 'revoked':
115
                    $project->setStatus(self::UPDATE_REVOKED);
116
                    break;
117
                case 'unsupported':
118
                    $project->setStatus(self::UPDATE_NOT_SUPPORTED);
119
                    break;
120
                case 'not-fetched':
121
                    $project->setStatus(self::UPDATE_NOT_FETCHED);
122
                    break;
123
124
                default:
125
                    // Assume anything else (e.g. 'published') is valid and we should
126
                    // perform the rest of the logic in this function.
127
                    break;
128
            }
129
        }
130
131
        if ($project->getStatus()) {
132
            // We already know the status for this project, so there's nothing else to
133
            // compute. Record the project status into $project_data and we're done.
134
            $project->setProjectStatus($this->getProjectStatus());
135
136
            return;
137
        }
138
139
        // Figure out the target major version.
140
        $existing_major = $project->getExistingMajor();
141
        $supported_majors = array();
142
        if ($this->getSupportedMajors()) {
143
            $supported_majors = explode(',', $this->getSupportedMajors());
144
        } elseif ($this->getDefaultMajor()) {
145
            // Older release history XML file without supported or recommended.
146
            $supported_majors[] = $this->getDefaultMajor();
147
        }
148
149
        if (in_array($existing_major, $supported_majors)) {
150
            // Still supported, stay at the current major version.
151
            $target_major = $existing_major;
152
        } elseif ($this->getRecommendedMajor()) {
153
            // Since 'recommended_major' is defined, we know this is the new XML
154
            // format. Therefore, we know the current release is unsupported since
155
            // its major version was not in the 'supported_majors' list. We should
156
            // find the best release from the recommended major version.
157
            $target_major = $this->getRecommendedMajor();
158
            $project->setStatus(self::UPDATE_NOT_SUPPORTED);
159
        } elseif ($this->getDefaultMajor()) {
160
            // Older release history XML file without recommended, so recommend
161
            // the currently defined "default_major" version.
162
            $target_major = $this->getDefaultMajor();
163
        } else {
164
            // Malformed XML file? Stick with the current version.
165
            $target_major = $existing_major;
166
        }
167
168
        // Make sure we never tell the admin to downgrade. If we recommended an
169
        // earlier version than the one they're running, they'd face an
170
        // impossible data migration problem, since Drupal never supports a DB
171
        // downgrade path. In the unfortunate case that what they're running is
172
        // unsupported, and there's nothing newer for them to upgrade to, we
173
        // can't print out a "Recommended version", but just have to tell them
174
        // what they have is unsupported and let them figure it out.
175
        $target_major = max($existing_major, $target_major);
176
177
        $release_patch_changed = null;
178
        $patch = '';
179
180
        // If the project is marked as UPDATE_FETCH_PENDING, it means that the
181
        // data we currently have (if any) is stale, and we've got a task queued
182
        // up to (re)fetch the data. In that case, we mark it as such, merge in
183
        // whatever data we have (e.g. project title and link), and move on.
184
        if ($this->getFetchStatus() == self::UPDATE_FETCH_PENDING) {
185
            $project->setStatus(self::UPDATE_FETCH_PENDING);
186
            $project->setReason('No available update data');
187
            $project->setFetchStatus($this->getFetchStatus());
188
189
            return;
190
        }
191
192
        // Defend ourselves from XML history files that contain no releases.
193
        if (!$this->getReleases()) {
194
            $project->setStatus(self::UPDATE_UNKNOWN);
195
            $project->setReason('No available releases found');
196
197
            return;
198
        }
199
200
        foreach ($this->getReleases() as $version => $release) {
201
            // First, if this is the existing release, check a few conditions.
202
            if ($project->getExistingVersion() == $version) {
203
                if ($release->hasTerm('Release type') &&
204
                  in_array('Insecure', $release->getTerm('Release type'))
205
                ) {
206
                    $project->setStatus(self::UPDATE_NOT_SECURE);
207
                } elseif ($release->getStatus() == 'unpublished') {
208
                    $project->setStatus(self::UPDATE_REVOKED);
209
                } elseif ($release->hasTerm('Release type') &&
210
                  in_array('Unsupported', $release->getTerm('Release type'))
211
                ) {
212
                    $project->setStatus(self::UPDATE_NOT_SUPPORTED);
213
                }
214
            }
215
216
            // Otherwise, ignore unpublished, insecure, or unsupported releases.
217
            if ($release->getStatus() == 'unpublished' ||
218
              ($release->hasTerm('Release type') &&
219
                (in_array('Insecure', $release->getTerm('Release type')) ||
220
                  in_array('Unsupported', $release->getTerm('Release type'))))
221
            ) {
222
                continue;
223
            }
224
225
            // See if this is a higher major version than our target and yet still
226
            // supported. If so, record it as an "Also available" release.
227
            // Note: some projects have a HEAD release from CVS days, which could
228
            // be one of those being compared. They would not have version_major
229
            // set, so we must call isset first.
230
            if ($release->getVersionMajor() > $target_major) {
231
//                if (in_array($release['version_major'], $supported_majors)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
69% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
232
//                    if (!isset($project_data['also'][$release['version_major']])) {
233
//                        $project_data['also'][$release['version_major']] = $version;
234
//                        $project_data['releases'][$version] = $release;
235
//
236
//                        $project->addAlsoAvailable($version, $release);
237
//                    }
238
//                }
239
                // Otherwise, this release can't matter to us, since it's neither
240
                // from the release series we're currently using nor the recommended
241
                // release. We don't even care about security updates for this
242
                // branch, since if a project maintainer puts out a security release
243
                // at a higher major version and not at the lower major version,
244
                // they must remove the lower version from the supported major
245
                // versions at the same time, in which case we won't hit this code.
246
                continue;
247
            }
248
249
            // Look for the 'latest version' if we haven't found it yet. Latest is
250
            // defined as the most recent version for the target major version.
251
            if (!$project->getLatestVersion() && $release->getVersionMajor() == $target_major) {
252
                $project->setLatestVersion($version);
253
                $project->setRelease($version, $release);
254
            }
255
256
            // Look for the development snapshot release for this branch.
257
            if (!$project->getDevVersion()
258
              && $release->getVersionMajor() == $target_major
259
              && $release->getVersionExtra() == Project::INSTALL_TYPE_DEV
260
            ) {
261
                $project->setDevVersion($version);
262
                $project->setRelease($version, $release);
263
            }
264
265
            // Look for the 'recommended' version if we haven't found it yet (see
266
            // phpdoc at the top of this function for the definition).
267
            if (!$project->getRecommended()
268
              && $release->getVersionMajor() == $target_major
269
              && $release->getVersionPatch()
270
            ) {
271
                if ($patch != $release->getVersionPatch()) {
272
                    $patch = $release->getVersionPatch();
273
                    $release_patch_changed = $release;
274
                }
275
                if (!$release->getVersionExtra() && $patch == $release->getVersionPatch()) {
276
                    $project->setRecommended($release_patch_changed->getVersion());
277
                    if ($release_patch_changed instanceof Release) {
278
                        $project->setRelease($release_patch_changed->getVersion(), $release_patch_changed);
279
                    }
280
                }
281
            }
282
283
            // Stop searching once we hit the currently installed version.
284
            if ($project->getExistingVersion() == $version) {
285
                break;
286
            }
287
288
            // If we're running a dev snapshot and have a timestamp, stop
289
            // searching for security updates once we hit an official release
290
            // older than what we've got. Allow 100 seconds of leeway to handle
291
            // differences between the datestamp in the .info file and the
292
            // timestamp of the tarball itself (which are usually off by 1 or 2
293
            // seconds) so that we don't flag that as a new release.
294
            if ($project->getInstallType() == Project::INSTALL_TYPE_DEV) {
295
                if (!$project->getDatestamp()) {
296
                    // We don't have current timestamp info, so we can't know.
297
                    continue;
298
                } elseif ($release->getDate() && ($project->getDatestamp() + 100 > $release->getDate())) {
299
                    // We're newer than this, so we can skip it.
300
                    continue;
301
                }
302
            }
303
304
            // See if this release is a security update.
305
            if ($release->hasTerm('Release type') && in_array('Security update', $release->getTerm('Release type'))) {
306
                $project->addSecurityUpdate($release->getVersion(), $release);
307
            }
308
        }
309
310
        // If we were unable to find a recommended version, then make the latest
311
        // version the recommended version if possible.
312
        if (!$project->getRecommended() && $project->getLatestVersion()) {
313
            $project->setRecommended($project->getLatestVersion());
314
        }
315
316
        // Check to see if we need an update or not.
317
        if ($project->hasSecurityUpdates()) {
318
            // If we found security updates, that always trumps any other status.
319
            $project->setStatus(self::UPDATE_NOT_SECURE);
320
        }
321
322
        if ($project->getStatus()) {
323
            // If we already know the status, we're done.
324
            return;
325
        }
326
327
        // If we don't know what to recommend, there's nothing we can report.
328
        // Bail out early.
329
        if (!$project->getRecommended()) {
330
            $project->setStatus(self::UPDATE_UNKNOWN);
331
            $project->setReason('No available releases found');
332
333
            return;
334
        }
335
336
        // If we're running a dev snapshot, compare the date of the dev snapshot
337
        // with the latest official version, and record the absolute latest in
338
        // 'latest_dev' so we can correctly decide if there's a newer release
339
        // than our current snapshot.
340
        if ($project->getInstallType() == Project::INSTALL_TYPE_DEV) {
341
            if ($project->getDevVersion() && $this->getRelease($project->getDevVersion())->getDate(
342
              ) > $this->getRelease($project->getLatestVersion())->getDate()
343
            ) {
344
                $project->setLatestDev($project->getDevVersion());
345
            } else {
346
                $project->setLatestDev($project->getLatestVersion());
347
            }
348
        }
349
350
        // Figure out the status, based on what we've seen and the install type.
351
        switch ($project->getInstallType()) {
352
            case Project::INSTALL_TYPE_OFFICIAL:
353
                if ($project->getExistingVersion() == $project->getRecommended() ||
354
                  $project->getExistingVersion() == $project->getLatestVersion()
355
                ) {
356
                    $project->setStatus(self::UPDATE_CURRENT);
357
                } else {
358
                    $project->setStatus(self::UPDATE_NOT_CURRENT);
359
                }
360
                break;
361
362
            case Project::INSTALL_TYPE_DEV:
363
                $latest = $this->getRelease($project->getLatestDev());
364
365
                if (!$project->getDatestamp()) {
366
                    $project->setStatus(self::UPDATE_NOT_CHECKED);
367
                    $project->setReason('Unknown release date');
368
                } elseif (($project->getDatestamp() + 100 > $latest->getDate())) {
369
                    $project->setStatus(self::UPDATE_CURRENT);
370
                } else {
371
                    $project->setStatus(self::UPDATE_NOT_CURRENT);
372
                }
373
                break;
374
375
            default:
376
                $project->setStatus(self::UPDATE_UNKNOWN);
377
                $project->setReason('Invalid info');
378
        }
379
    }
380
381
    /**
382
     * @return string
383
     */
384
    public function getProjectStatus()
385
    {
386
        return $this->data['project_status'];
387
    }
388
389
    /**
390
     * @return string
391
     */
392
    public function getSupportedMajors()
393
    {
394
        return $this->data['supported_majors'];
395
    }
396
397
    /**
398
     * @return int
399
     */
400
    public function getDefaultMajor()
401
    {
402
        return $this->data['default_major'];
403
    }
404
405
    /**
406
     * @return int
407
     */
408
    public function getRecommendedMajor()
409
    {
410
        return $this->data['recommended_major'];
411
    }
412
413
    /**
414
     * @return mixed
415
     */
416
    public function getFetchStatus()
417
    {
418
        return isset($this->data['fetch_status']) ? $this->data['fetch_status'] : 0;
419
    }
420
421
    /**
422
     * @return Release[]
423
     */
424
    public function getReleases()
425
    {
426
        return $this->data['releases'];
427
    }
428
429
    /**
430
     * @param string $release
431
     *
432
     * @return Release|null
433
     */
434
    public function getRelease($release)
435
    {
436
        if (isset($this->data['releases'][$release])) {
437
            return $this->data['releases'][$release];
438
        }
439
440
        return null;
441
    }
442
443
    /**
444
     * @return string
445
     */
446
    public function getApiVersion()
447
    {
448
        return $this->data['api_version'];
449
    }
450
451
    /**
452
     * @return array
453
     */
454
    public function getData()
455
    {
456
        return $this->data;
457
    }
458
459
    /**
460
     * @return Release
461
     */
462
    public function getLastRelease()
463
    {
464
        $release = reset($this->data['releases']);
465
466
        return $release;
467
    }
468
469
    /**
470
     * @return string
471
     */
472
    public function getLink()
473
    {
474
        return $this->data['link'];
475
    }
476
477
    /**
478
     * @return mixed
479
     */
480
    public function getShortName()
481
    {
482
        return $this->data['short_name'];
483
    }
484
485
    /**
486
     * @param int $status
487
     *
488
     * @return string
489
     */
490
    public static function getStatusLabel($status)
491
    {
492
        switch ($status) {
493
            case self::UPDATE_NOT_SECURE:
494
                return 'SECURITY UPDATE available';
495
            case self::UPDATE_REVOKED:
496
                return 'Installed version REVOKED';
497
            case self::UPDATE_NOT_SUPPORTED:
498
                return 'Installed version not supported';
499
            case self::UPDATE_NOT_CURRENT:
500
                return 'Update available';
501
            case self::UPDATE_CURRENT:
502
                return 'Up to date';
503
            case self::UPDATE_NOT_CHECKED:
504
            case self::UPDATE_NOT_FETCHED:
505
            case self::UPDATE_FETCH_PENDING:
506
                return 'Unable to check status';
507
            case self::UPDATE_UNKNOWN:
508
            default:
509
                return 'Unknown';
510
        }
511
    }
512
513
    /**
514
     * @return string
515
     */
516
    public function getTerms()
517
    {
518
        return trim($this->data['terms']);
519
    }
520
521
    /**
522
     * @return string
523
     */
524
    public function getTitle()
525
    {
526
        return $this->data['title'];
527
    }
528
529
    /**
530
     * @return string
531
     */
532
    public function getType()
533
    {
534
        return $this->data['type'];
535
    }
536
537
    /**
538
     * @return string
539
     */
540
    public function getUrl()
541
    {
542
        return $this->url;
543
    }
544
545
    /**
546
     * @param string $url
547
     */
548
    public function setUrl($url)
549
    {
550
        $this->url = $url;
551
    }
552
553
    /**
554
     * @param Project $project
555
     * @param bool|false $reset
556
     */
557
    public function prepare(Project $project, $reset = false)
558
    {
559
        $cid_parts = array(
560
          'release_history',
561
          $project->getProject(),
562
          $project->getCore(),
563
        );
564
565
        $cid = implode(':', $cid_parts);
566
        $data = false;
567
568
        if ($this->cache && !$reset) {
569
            $data = $this->cache->fetch($cid);
570
        }
571
572
        // If not in cache, load from remote.
573
        if ($data === false) {
574
            $url = $project->getStatusUrl() . '/' .
575
              $project->getProject() . '/' .
576
              $project->getCore();
577
578
            // Todo: use guzzle library.
579
            $content = file_get_contents($url);
580
581
            $data = $this->parseUpdateXml($content);
582
583
            // If data, store into cache.
584
            if ($this->cache && !empty($data)) {
585
                $this->cache->save($cid, $data, 1800);
586
            }
587
        }
588
589
        $data += array(
590
          'project_status'    => '',
591
          'default_major'     => '',
592
          'recommended_major' => '',
593
          'supported_majors'  => '',
594
        );
595
596
        // Hydrate release objects.
597
        if (isset($data['releases']) && is_array($data['releases'])) {
598
            foreach ($data['releases'] as $key => $value) {
599
                $data['releases'][$key] = new Release($value);
600
            }
601
            $project->setReleases($data['releases']);
602
        } else {
603
            $data['releases'] = array();
604
        }
605
606
        if (!empty($data['type'])) {
607
            $project->setProjectType($data['type']);
608
        } else {
609
            $project->setProjectType(Project::TYPE_UNKNOWN);
610
        }
611
612
        $this->data = (array) $data;
613
    }
614
615
    /**
616
     * Parses the XML of the Drupal release history info files.
617
     *
618
     * @param string $raw_xml
619
     *   A raw XML string of available release data for a given project.
620
     *
621
     * @return array
622
     *   Array of parsed data about releases for a given project, or NULL if there
623
     *   was an error parsing the string.
624
     */
625
    protected function parseUpdateXml($raw_xml)
626
    {
627
        try {
628
            $xml = new \SimpleXMLElement($raw_xml);
629
        } catch (\Exception $e) {
630
            // SimpleXMLElement::__construct produces an E_WARNING error message for
631
            // each error found in the XML data and throws an exception if errors
632
            // were detected. Catch any exception and return failure (NULL).
633
            return array();
634
        }
635
636
        // If there is no valid project data, the XML is invalid, so return failure.
637
        if (!isset($xml->short_name)) {
638
            return array();
639
        }
640
641
        $data = array();
642
        foreach ($xml as $k => $v) {
643
            $data[$k] = (string) $v;
644
        }
645
        $data['releases'] = array();
646
647
        if (isset($xml->releases)) {
648
            foreach ($xml->releases->children() as $release) {
649
                $version = (string) $release->version;
650
                $data['releases'][$version] = array();
651
                foreach ($release->children() as $k => $v) {
652
                    $data['releases'][$version][$k] = (string) $v;
653
                }
654
                $data['releases'][$version]['terms'] = array();
655
                if ($release->terms) {
656
                    foreach ($release->terms->children() as $term) {
657
                        if (!isset($data['releases'][$version]['terms'][(string) $term->name])) {
658
                            $data['releases'][$version]['terms'][(string) $term->name] = array();
659
                        }
660
                        $data['releases'][$version]['terms'][(string) $term->name][] = (string) $term->value;
661
                    }
662
                }
663
            }
664
        }
665
666
        return $data;
667
    }
668
}
669