Completed
Pull Request — master (#37)
by Pádraic
06:07 queued 04:12
created

ManifestStrategy::setDownloadTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
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('sha256', '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
     * @var int
58
     */
59
    private $manifestTimeout = 60;
60
61
    /**
62
     * @var int
63
     */
64
    private $downloadTimeout = 60;
65
66
    /**
67
     * @var bool
68
     */
69
    private $ignorePhpReq = false;
70
71
    /**
72
     * Set version string of the local phar
73
     *
74
     * @param string $version
75
     */
76
    public function setCurrentLocalVersion($version)
77
    {
78
        $this->localVersion = $version;
79
        return $this;
80
    }
81
82
    /**
83
     * @param int $downloadTimeout
84
     * @return  self
85
     */
86
    public function setDownloadTimeout($downloadTimeout)
87
    {
88
        $this->downloadTimeout = $downloadTimeout;
89
        return $this;
90
    }
91
92
    /**
93
     * @param int $manifestTimeout
94
     * @return  self
95
     */
96
    public function setManifestTimeout($manifestTimeout)
97
    {
98
        $this->manifestTimeout = $manifestTimeout;
99
        return $this;
100
    }
101
102
    /**
103
     * If set, ignores any restrictions based on currently running PHP version.
104
     * @return  self
105
     */
106
    public function ignorePhpRequirements()
107
    {
108
        $this->ignorePhpReq = true;
109
        return $this;
110
    }
111
112
    /**
113
     * If set, ignores any restrictions based on currently running PHP version.
114
     * @return  self
115
     */
116
    public function allowMajorVersionUpdates()
117
    {
118
        $this->allowMajor = true;
119
        return $this;
120
    }
121
122
    /**
123
     * If set, ignores any restrictions based on currently running PHP version.
124
     * @return  self
125
     */
126
    public function allowUnstableVersionUpdates()
127
    {
128
        $this->allowUnstable = true;
129
        return $this;
130
    }
131
132
    public function setManifestUrl($url)
133
    {
134
        $this->manifestUrl = $url;
135
        return $this;
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function getCurrentLocalVersion(Updater $updater)
142
    {
143
        return $this->localVersion;
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function download(Updater $updater)
150
    {
151
        $version = $this->getCurrentRemoteVersion($updater);
152
        if ($version === false) {
153
            throw new RuntimeException('No remote versions found');
154
        }
155
156
        $versionInfo = $this->getAvailableVersions();
157
        if (!isset($versionInfo[$version])) {
158
            throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version));
159
        }
160
161
        $context = stream_context_create(['http' => ['timeout' => $this->downloadTimeout]]);
162
        /** Switch remote request errors to HttpRequestExceptions */
163
        set_error_handler(array($updater, 'throwHttpRequestException'));
164
        $fileContents = file_get_contents($versionInfo[$version]['url'], false, $context);
165
        restore_error_handler();
166
167
        if ($fileContents === false) {
168
            throw new HttpRequestException(sprintf('Failed to download file from URL: %s', $versionInfo[$version]['url']));
169
        }
170
171
        $tmpFilename = $updater->getTempPharFile();
172
        if (file_put_contents($tmpFilename, $fileContents) === false) {
173
            throw new RuntimeException(sprintf('Failed to write file: %s', $tmpFilename));
174
        }
175
176
        $tmpSha = hash_file('sha256', $tmpFilename);
177
        if ($tmpSha !== $versionInfo[$version]['sha256']) {
178
            unlink($tmpFilename);
179
            throw new RuntimeException(
180
                sprintf(
181
                    'SHA-256 verification failed: expected %s, actual %s',
182
                    $versionInfo[$version]['sha256'],
183
                    $tmpSha
184
                )
185
            );
186
        }
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function getCurrentRemoteVersion(Updater $updater)
193
    {
194
        $versions = array_keys($this->getAvailableVersions());
195
        if (!$this->allowMajor) {
196
            $versions = $this->filterByLocalMajorVersion($versions);
197
        }
198
        if (!$this->ignorePhpReq) {
199
            $versions = $this->filterByPhpVersion($versions);
200
        }
201
202
        $versionParser = new VersionParser($versions);
203
204
        $mostRecent = $versionParser->getMostRecentStable();
205
206
        // Look for unstable updates if explicitly allowed, or if the local
207
        // version is already unstable and there is no new stable version.
208
        if ($this->allowUnstable || ($versionParser->isUnstable($this->localVersion)
209
        && version_compare($mostRecent, $this->localVersion, '<'))) {
210
            $mostRecent = $versionParser->getMostRecentAll();
211
        }
212
213
        return version_compare($mostRecent, $this->localVersion, '>') ? $mostRecent : false;
214
    }
215
216
    /**
217
     * Find update/upgrade notes for the new remote version.
218
     *
219
     * @param Updater $updater
220
     * @param bool $useBaseNote Return if no version specific update notes found.
221
     *
222
     * @return string|false A string if notes are found, or false otherwise.
223
     */
224
    public function getUpdateNotes(Updater $updater, $useBaseNote = false)
225
    {
226
        $versionInfo = $this->getRemoteVersionInfo($updater);
227
        if (empty($versionInfo['updating'])) {
228
            return false;
229
        }
230
        $localVersion = $this->getCurrentLocalVersion($updater);
231
        $items = isset($versionInfo['updating'][0]) ? $versionInfo['updating'] : [$versionInfo['updating']];
232
        foreach ($items as $updating) {
233
            if (!isset($updating['notes'])) {
234
                continue;
235
            } elseif (isset($updating['hide from'])
236
            && version_compare($localVersion, $updating['hide from'], '>=')) {
237
                continue;
238
            } elseif (isset($updating['show from'])
239
            && version_compare($localVersion, $updating['show from'], '<')) {
240
                continue;
241
            }
242
243
            return $updating['notes'];
244
        }
245
246
        if (true === $useBaseNote && !empty($versionInfo['notes'])) {
247
            return $versionInfo['notes'];
248
        }
249
250
        return false;
251
    }
252
253
    /**
254
     * Gets available versions to update to.
255
     *
256
     * @return array  An array keyed by the version name, whose elements are arrays
257
     *                containing version information ('name', 'sha256', and 'url').
258
     */
259
    private function getAvailableVersions()
260
    {
261
        if (isset($this->availableVersions)) {
262
            return $this->availableVersions;
263
        }
264
265
        $this->availableVersions = array();
266
        foreach ($this->retrieveManifest() as $key => $item) {
267
            if ($missing = array_diff(self::$requiredKeys, array_keys($item))) {
268
                throw new RuntimeException(
269
                    sprintf(
270
                        'Manifest item %s missing required key(s): %s',
271
                        $key,
272
                        implode(',', $missing)
273
                    )
274
                );
275
            }
276
            $this->availableVersions[$item['version']] = $item;
277
        }
278
        return $this->availableVersions;
279
    }
280
281
    /**
282
     * Download and decode the JSON manifest file.
283
     *
284
     * @return array
285
     */
286
    private function retrieveManifest()
287
    {
288
        if (isset($this->manifest)) {
289
            return $this->manifest;
290
        }
291
292
        if (!isset($this->manifest)) {
293
            $context = stream_context_create(['http' => ['timeout' => $this->manifestTimeout]]);
294
            $manifestContents = file_get_contents($this->manifestUrl, false, $context);
295
            if ($manifestContents === false) {
296
                throw new RuntimeException(sprintf('Failed to download manifest: %s', $this->manifestUrl));
297
            }
298
299
            $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...
300 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...
301
                throw new JsonParsingException(
302
                    'Error parsing manifest file'
303
                    . (function_exists('json_last_error_msg') ? ': ' . json_last_error_msg() : '')
304
                );
305
            }
306
        }
307
308
        return $this->manifest;
309
    }
310
311
    /**
312
     * Get version information for the latest remote version.
313
     *
314
     * @param Updater $updater
315
     *
316
     * @return array
317
     */
318
    private function getRemoteVersionInfo(Updater $updater)
319
    {
320
        $version = $this->getCurrentRemoteVersion($updater);
321
        if ($version === false) {
322
            throw new RuntimeException('No remote versions found');
323
        }
324
        $versionInfo = $this->getAvailableVersions();
325
        if (!isset($versionInfo[$version])) {
326
            throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version));
327
        }
328
        return $versionInfo[$version];
329
    }
330
331
    /**
332
     * Filter a list of versions to those that match the current local version.
333
     *
334
     * @param string[] $versions
335
     *
336
     * @return string[]
337
     */
338
    private function filterByLocalMajorVersion(array $versions)
339
    {
340
        list($localMajorVersion, ) = explode('.', $this->localVersion, 2);
341
342
        return array_filter($versions, function ($version) use ($localMajorVersion) {
343
            list($majorVersion, ) = explode('.', $version, 2);
344
            return $majorVersion === $localMajorVersion;
345
        });
346
    }
347
348
        /**
349
     * Filter a list of versions to those that allow the current PHP version.
350
     *
351
     * @param string[] $versions
352
     *
353
     * @return string[]
354
     */
355
    private function filterByPhpVersion(array $versions)
356
    {
357
        $versionInfo = $this->getAvailableVersions();
358
        return array_filter($versions, function ($version) use ($versionInfo) {
359
            if (isset($versionInfo[$version]['php']['min'])
360
                && version_compare(PHP_VERSION, $versionInfo[$version]['php']['min'], '<')) {
361
                return false;
362
            } elseif (isset($versionInfo[$version]['php']['max'])
363
                && version_compare(PHP_VERSION, $versionInfo[$version]['php']['max'], '>')) {
364
                return false;
365
            }
366
            return true;
367
        });
368
    }
369
}
370