Completed
Pull Request — master (#37)
by Pádraic
02:39
created

ManifestStrategy::getAvailableVersions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 4
nop 0
1
<?php
2
/**
3
 * Humbug
4
 *
5
 * @category   Humbug
6
 * @package    Humbug
7
 * @copyright  Copyright (c) 2017 Patrick Dawkins
8
 * @license    https://github.com/padraic/phar-updater/blob/master/LICENSE New BSD License
9
 *
10
 */
11
namespace Humbug\SelfUpdate\Strategy;
12
13
use Humbug\SelfUpdate\Exception\HttpRequestException;
14
use Humbug\SelfUpdate\Exception\JsonParsingException;
15
use Humbug\SelfUpdate\Updater;
16
use Humbug\SelfUpdate\VersionParser;
17
use Humbug\SelfUpdate\Exception\RuntimeException;
18
19
final class ManifestStrategy implements StrategyInterface
20
{
21
    /**
22
     * @var array
23
     */
24
    private static $requiredKeys = array('sha1', 'version', 'url');
25
26
    /**
27
     * @var string
28
     */
29
    private $manifestUrl;
30
31
    /**
32
     * @var array
33
     */
34
    private $manifest;
35
36
    /**
37
     * @var array
38
     */
39
    private $availableVersions;
40
41
    /**
42
     * @var string
43
     */
44
    private $localVersion;
45
46
    /**
47
     * @var bool
48
     */
49
    private $allowMajor = false;
50
51
    /**
52
     * @var bool
53
     */
54
    private $allowUnstable = false;
55
56
    /**
57
     * ManifestStrategy constructor.
58
     *
59
     * @param string $localVersion  The local version.
60
     * @param string $manifestUrl   The URL to a JSON manifest file. The
61
     *                              manifest contains an array of objects, each
62
     *                              containing a 'version', 'sha1', and 'url'.
63
     * @param bool   $allowMajor    Whether to allow updating between major
64
     *                              versions.
65
     * @param bool   $allowUnstable Whether to allow updating to an unstable
66
     *                              version. Ignored if $localVersion is unstable
67
     *                              and there are no new stable versions.
68
     */
69
    public function __construct($localVersion, $manifestUrl, $allowMajor = false, $allowUnstable = false)
70
    {
71
        $this->localVersion = $localVersion;
72
        $this->manifestUrl = $manifestUrl;
73
        $this->allowMajor = $allowMajor;
74
        $this->allowUnstable = $allowUnstable;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function getCurrentLocalVersion(Updater $updater)
81
    {
82
        return $this->localVersion;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function download(Updater $updater)
89
    {
90
        $version = $this->getCurrentRemoteVersion($updater);
91
        if ($version === false) {
92
            throw new RuntimeException('No remote versions found');
93
        }
94
95
        $versionInfo = $this->getAvailableVersions();
96
        if (!isset($versionInfo[$version])) {
97
            throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version));
98
        }
99
100
        $fileContents = file_get_contents($versionInfo[$version]['url']);
101
        if ($fileContents === false) {
102
            throw new HttpRequestException(sprintf('Failed to download file from URL: %s', $versionInfo[$version]['url']));
103
        }
104
105
        $tmpFilename = $updater->getTempPharFile();
106
        if (file_put_contents($tmpFilename, $fileContents) === false) {
107
            throw new RuntimeException(sprintf('Failed to write file: %s', $tmpFilename));
108
        }
109
110
        $tmpSha = sha1_file($tmpFilename);
111
        if ($tmpSha !== $versionInfo[$version]['sha1']) {
112
            unlink($tmpFilename);
113
            throw new RuntimeException(
114
                sprintf(
115
                    'SHA-1 verification failed: expected %s, actual %s',
116
                    $versionInfo[$version]['sha1'],
117
                    $tmpSha
118
                )
119
            );
120
        }
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function getCurrentRemoteVersion(Updater $updater)
127
    {
128
        $versions = array_keys($this->getAvailableVersions());
129
        if (!$this->allowMajor) {
130
            $versions = $this->filterByLocalMajorVersion($versions);
131
        }
132
133
        $versionParser = new VersionParser($versions);
134
135
        $mostRecent = $versionParser->getMostRecentStable();
136
137
        // Look for unstable updates if explicitly allowed, or if the local
138
        // version is already unstable and there is no new stable version.
139
        if ($this->allowUnstable || ($versionParser->isUnstable($this->localVersion) && version_compare($mostRecent, $this->localVersion, '<'))) {
140
            $mostRecent = $versionParser->getMostRecentAll();
141
        }
142
143
        return version_compare($mostRecent, $this->localVersion, '>') ? $mostRecent : false;
144
    }
145
146
    /**
147
     * Gets available versions to update to.
148
     *
149
     * @return array  An array keyed by the version name, whose elements are arrays
150
     *                containing version information ('name', 'sha1', and 'url').
151
     */
152
    private function getAvailableVersions()
153
    {
154
        if (isset($this->availableVersions)) {
155
            return $this->availableVersions;
156
        }
157
158
        $this->availableVersions = array();
159
        foreach ($this->retrieveManifest() as $key => $item) {
160
            if ($missing = array_diff(self::$requiredKeys, array_keys($item))) {
161
                throw new RuntimeException(sprintf('Manifest item %s missing required key(s): %s', $key, implode(',', $missing)));
162
            }
163
            $this->availableVersions[$item['version']] = $item;
164
        }
165
        return $this->availableVersions;
166
    }
167
168
    /**
169
     * Download and decode the JSON manifest file.
170
     *
171
     * @return array
172
     */
173
    private function retrieveManifest()
174
    {
175
        if (isset($this->manifest)) {
176
            return $this->manifest;
177
        }
178
179
        if (!isset($this->manifest)) {
180
            $manifestContents = file_get_contents($this->manifestUrl);
181
            if ($manifestContents === false) {
182
                throw new RuntimeException(sprintf('Failed to download manifest: %s', $this->manifestUrl));
183
            }
184
185
            $this->manifest = json_decode($manifestContents, true, 512, JSON_OBJECT_AS_ARRAY);
0 ignored issues
show
Documentation Bug introduced by
It seems like json_decode($manifestCon..., JSON_OBJECT_AS_ARRAY) of type * is incompatible with the declared type array of property $manifest.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
186 View Code Duplication
            if (json_last_error() !== JSON_ERROR_NONE) {
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...
187
                throw new JsonParsingException(
188
                    'Error parsing manifest file'
189
                    . (function_exists('json_last_error_msg') ? ': ' . json_last_error_msg() : '')
190
                );
191
            }
192
        }
193
194
        return $this->manifest;
195
    }
196
197
    /**
198
     * Filter a list of versions to those that match the current local version.
199
     *
200
     * @param string[] $versions
201
     *
202
     * @return string[]
203
     */
204
    private function filterByLocalMajorVersion(array $versions)
205
    {
206
        list($localMajorVersion, ) = explode('.', $this->localVersion, 2);
207
208
        return array_filter($versions, function ($version) use ($localMajorVersion) {
209
            list($majorVersion, ) = explode('.', $version, 2);
210
            return $majorVersion === $localMajorVersion;
211
        });
212
    }
213
}
214