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

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