Completed
Pull Request — master (#37)
by Pádraic
01:59
created

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