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

ManifestStrategy::allowMajorVersionUpdates()   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 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
    const SHA256 = 'sha256';
22
23
    const SHA1 = 'sha1';
24
25
    const STABLE = 'stable';
26
27
    const UNSTABLE = 'unstable';
28
29
    const ANY = 'any';
30
31
    /**
32
     * @var array
33
     */
34
    private $requiredKeys = array(self::SHA256, 'version', 'url');
35
36
    /**
37
     * @var string
38
     */
39
    private $hashAlgo = self::SHA256;
40
41
    /**
42
     * @var string
43
     */
44
    private $manifestUrl;
45
46
    /**
47
     * @var array
48
     */
49
    private $manifest;
50
51
    /**
52
     * @var array
53
     */
54
    private $availableVersions;
55
56
    /**
57
     * @var string
58
     */
59
    private $localVersion;
60
61
    /**
62
     * @var bool
63
     */
64
    private $allowMajor = false;
65
66
    /**
67
     * @var bool
68
     */
69
    private $allowUnstable = false;
70
71
    /**
72
     * @var bool
73
     */
74
    private $requireUnstable = false;
75
76
    /**
77
     * @var int
78
     */
79
    private $manifestTimeout = 60;
80
81
    /**
82
     * @var int
83
     */
84
    private $downloadTimeout = 60;
85
86
    /**
87
     * @var bool
88
     */
89
    private $ignorePhpReq = false;
90
91
    /**
92
     * Set version string of the local phar
93
     *
94
     * @param string $version
95
     */
96
    public function setCurrentLocalVersion($version)
97
    {
98
        $this->localVersion = $version;
99
        return $this;
100
    }
101
102
    /**
103
     * @param int $downloadTimeout
104
     * @return  self
105
     */
106
    public function setDownloadTimeout($downloadTimeout)
107
    {
108
        $this->downloadTimeout = $downloadTimeout;
109
        return $this;
110
    }
111
112
    /**
113
     * @param int $manifestTimeout
114
     * @return  self
115
     */
116
    public function setManifestTimeout($manifestTimeout)
117
    {
118
        $this->manifestTimeout = $manifestTimeout;
119
        return $this;
120
    }
121
122
    public function useSha1()
123
    {
124
        $this->requiredKeys[0] = self::SHA1;
125
        $this->hashAlgo = self::SHA1;
126
    }
127
128
    /**
129
     * If set, ignores any restrictions based on currently running PHP version.
130
     * @return  self
131
     */
132
    public function ignorePhpRequirements()
133
    {
134
        $this->ignorePhpReq = true;
135
        return $this;
136
    }
137
138
    /**
139
     * If set, ignores any restrictions based on currently running PHP version.
140
     * @return  self
141
     */
142
    public function allowMajorVersionUpdates()
143
    {
144
        $this->allowMajor = true;
145
        return $this;
146
    }
147
148
    /**
149
     * Set target stability
150
     *
151
     * @param string $stability
152
     */
153
    public function setStability($stability)
154
    {
155
        if ($stability !== self::STABLE && $stability !== self::UNSTABLE && $stability !== self::ANY) {
156
            throw new InvalidArgumentException(
157
                'Invalid stability value. Must be one of "stable", "unstable"'
158
            );
159
        }
160
        
161
        switch ($stability) {
162
            case self::ANY:
163
                $this->allowUnstableVersionUpdates();
164
                break;
165
166
            case self::UNSTABLE:
167
                $this->requireUnstable = true;
168
                break;
169
            
170
            default:
171
                break;
172
        }
173
    }
174
175
    /**
176
     * If set, ignores any restrictions based on currently running PHP version.
177
     * @return  self
178
     */
179
    public function allowUnstableVersionUpdates()
180
    {
181
        $this->allowUnstable = true;
182
        return $this;
183
    }
184
185
    public function setManifestUrl($url)
186
    {
187
        $this->manifestUrl = $url;
188
        return $this;
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function getCurrentLocalVersion(Updater $updater)
195
    {
196
        return $this->localVersion;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function download(Updater $updater)
203
    {
204
        $version = $this->getCurrentRemoteVersion($updater);
205
        if ($version === false) {
206
            throw new RuntimeException('No remote versions found');
207
        }
208
209
        $versionInfo = $this->getAvailableVersions();
210
        if (!isset($versionInfo[$version])) {
211
            throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version));
212
        }
213
214
        $context = stream_context_create(['http' => ['timeout' => $this->downloadTimeout]]);
215
        /** Switch remote request errors to HttpRequestExceptions */
216
        set_error_handler(array($updater, 'throwHttpRequestException'));
217
        $fileContents = file_get_contents($versionInfo[$version]['url'], false, $context);
218
        restore_error_handler();
219
220
        if ($fileContents === false) {
221
            throw new HttpRequestException(sprintf('Failed to download file from URL: %s', $versionInfo[$version]['url']));
222
        }
223
224
        $tmpFilename = $updater->getTempPharFile();
225
        if (file_put_contents($tmpFilename, $fileContents) === false) {
226
            throw new RuntimeException(sprintf('Failed to write file: %s', $tmpFilename));
227
        }
228
229
        $tmpSha = hash_file($this->hashAlgo, $tmpFilename);
230
        if ($tmpSha !== $versionInfo[$version][$this->hashAlgo]) {
231
            unlink($tmpFilename);
232
            throw new RuntimeException(
233
                sprintf(
234
                    '%s verification failed: expected %s, actual %s',
235
                    strtoupper($this->hashAlgo),
236
                    $versionInfo[$version][$this->hashAlgo],
237
                    $tmpSha
238
                )
239
            );
240
        }
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function getCurrentRemoteVersion(Updater $updater)
247
    {
248
        $versions = array_keys($this->getAvailableVersions());
249
        if (!$this->allowMajor) {
250
            $versions = $this->filterByLocalMajorVersion($versions);
251
        }
252
        if (!$this->ignorePhpReq) {
253
            $versions = $this->filterByPhpVersion($versions);
254
        }
255
256
        $versionParser = new VersionParser($versions);
257
258
        $mostRecent = $versionParser->getMostRecentStable();
259
260
        // Look for unstable updates if explicitly allowed, or if the local
261
        // version is already unstable and there is no new stable version.
262
        if (true === $this->requireUnstable) {
263
            $mostRecent = $versionParser->getMostRecentUnstable();
264
        } elseif ($this->allowUnstable || ($versionParser->isUnstable($this->localVersion)
265
        && version_compare($mostRecent, $this->localVersion, '<'))) {
266
            $mostRecent = $versionParser->getMostRecentAll();
267
        }
268
269
        return version_compare($mostRecent, $this->localVersion, '>') ? $mostRecent : false;
270
    }
271
272
    /**
273
     * Find update/upgrade notes for the new remote version.
274
     *
275
     * @param Updater $updater
276
     * @param bool $useBaseNote Return main note if no version specific update notes found.
277
     *
278
     * @return string|false A string if notes are found, or false otherwise.
279
     */
280
    public function getUpdateNotes(Updater $updater, $useBaseNote = false)
281
    {
282
        $versionInfo = $this->getRemoteVersionInfo($updater);
283
        if (empty($versionInfo['updating'])) {
284
            return false;
285
        }
286
        $localVersion = $this->getCurrentLocalVersion($updater);
287
        $items = isset($versionInfo['updating'][0]) ? $versionInfo['updating'] : [$versionInfo['updating']];
288
        foreach ($items as $updating) {
289
            if (!isset($updating['notes'])) {
290
                continue;
291
            } elseif (isset($updating['hide from'])
292
            && version_compare($localVersion, $updating['hide from'], '>=')) {
293
                continue;
294
            } elseif (isset($updating['show from'])
295
            && version_compare($localVersion, $updating['show from'], '<')) {
296
                continue;
297
            }
298
299
            return $updating['notes'];
300
        }
301
302
        if (true === $useBaseNote && !empty($versionInfo['notes'])) {
303
            return $versionInfo['notes'];
304
        }
305
306
        return false;
307
    }
308
309
    /**
310
     * Gets available versions to update to.
311
     *
312
     * @return array  An array keyed by the version name, whose elements are arrays
313
     *                containing version information ('name', $this->hashAlgo, and 'url').
314
     */
315
    private function getAvailableVersions()
316
    {
317
        if (isset($this->availableVersions)) {
318
            return $this->availableVersions;
319
        }
320
321
        $this->availableVersions = array();
322
        foreach ($this->retrieveManifest() as $key => $item) {
323
            if ($missing = array_diff($this->requiredKeys, array_keys($item))) {
324
                throw new RuntimeException(
325
                    sprintf(
326
                        'Manifest item %s missing required key(s): %s',
327
                        $key,
328
                        implode(',', $missing)
329
                    )
330
                );
331
            }
332
            $this->availableVersions[$item['version']] = $item;
333
        }
334
        return $this->availableVersions;
335
    }
336
337
    /**
338
     * Download and decode the JSON manifest file.
339
     *
340
     * @return array
341
     */
342
    private function retrieveManifest()
343
    {
344
        if (isset($this->manifest)) {
345
            return $this->manifest;
346
        }
347
348
        if (!isset($this->manifest)) {
349
            $context = stream_context_create(['http' => ['timeout' => $this->manifestTimeout]]);
350
            $manifestContents = file_get_contents($this->manifestUrl, false, $context);
351
            if ($manifestContents === false) {
352
                throw new RuntimeException(sprintf('Failed to download manifest: %s', $this->manifestUrl));
353
            }
354
355
            $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...
356 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...
357
                throw new JsonParsingException(
358
                    'Error parsing manifest file'
359
                    . (function_exists('json_last_error_msg') ? ': ' . json_last_error_msg() : '')
360
                );
361
            }
362
        }
363
364
        return $this->manifest;
365
    }
366
367
    /**
368
     * Get version information for the latest remote version.
369
     *
370
     * @param Updater $updater
371
     *
372
     * @return array
373
     */
374
    private function getRemoteVersionInfo(Updater $updater)
375
    {
376
        $version = $this->getCurrentRemoteVersion($updater);
377
        if ($version === false) {
378
            throw new RuntimeException('No remote versions found');
379
        }
380
        $versionInfo = $this->getAvailableVersions();
381
        if (!isset($versionInfo[$version])) {
382
            throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version));
383
        }
384
        return $versionInfo[$version];
385
    }
386
387
    /**
388
     * Filter a list of versions to those that match the current local version.
389
     *
390
     * @param string[] $versions
391
     *
392
     * @return string[]
393
     */
394
    private function filterByLocalMajorVersion(array $versions)
395
    {
396
        list($localMajorVersion, ) = explode('.', $this->localVersion, 2);
397
398
        return array_filter($versions, function ($version) use ($localMajorVersion) {
399
            list($majorVersion, ) = explode('.', $version, 2);
400
            return $majorVersion === $localMajorVersion;
401
        });
402
    }
403
404
        /**
405
     * Filter a list of versions to those that allow the current PHP version.
406
     *
407
     * @param string[] $versions
408
     *
409
     * @return string[]
410
     */
411
    private function filterByPhpVersion(array $versions)
412
    {
413
        $versionInfo = $this->getAvailableVersions();
414
        return array_filter($versions, function ($version) use ($versionInfo) {
415
            if (isset($versionInfo[$version]['php']['min'])
416
                && version_compare(PHP_VERSION, $versionInfo[$version]['php']['min'], '<')) {
417
                return false;
418
            } elseif (isset($versionInfo[$version]['php']['max'])
419
                && version_compare(PHP_VERSION, $versionInfo[$version]['php']['max'], '>')) {
420
                return false;
421
            }
422
            return true;
423
        });
424
    }
425
}
426