ReleaseHistory::compare()   F
last analyzed

Complexity

Conditions 62
Paths > 20000

Size

Total Lines 275
Code Lines 133

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 62
eloc 133
nc 2963695
nop 1
dl 0
loc 275
rs 2
c 2
b 0
f 1

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