This repository has been archived by the owner on Mar 15, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
Manifest file strategy (updated) #37
Open
padraic
wants to merge
16
commits into
master
Choose a base branch
from
pjcdawkins-manifest-strategy
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
a241016
Add ManifestStrategy
pjcdawkins a35b4c1
Write ManifestStrategy tests
pjcdawkins 6cee97b
Merge branch 'manifest-strategy' of git://github.com/pjcdawkins/phar-…
padraic 5244f20
Update tests to PHPUnit 6. Resolve risky test.
padraic 809200c
Address review points from @theofidry
padraic ae8a3a0
Properly set json_decode depth
padraic 163dd04
Integrate additional features; switch to SHA-256
padraic 760cab6
Revise API to better reflect current approach (methods v construct pa…
padraic fc2093c
Adjustments and README entry blurb
padraic 39d005c
Update README with code examples
padraic 44176d5
Add tests and some fixes for note retrieval; update README
padraic acb6710
Missing test for hasUpdate() and getNewVersion()
padraic b72e183
Allow optional SHA-1 support if specifically enabled
padraic 24e836f
Add missing manifest file for SHA1 test
padraic 42c4e3c
Include setStability() function similar to GH strategy
padraic 18101d9
Fix typo
padraic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
<?php | ||
/** | ||
* Humbug | ||
* | ||
* @category Humbug | ||
* @package Humbug | ||
* @copyright Copyright (c) 2017 Patrick Dawkins | ||
* @license https://github.com/padraic/phar-updater/blob/master/LICENSE New BSD License | ||
* | ||
*/ | ||
namespace Humbug\SelfUpdate\Strategy; | ||
|
||
use Humbug\SelfUpdate\Exception\HttpRequestException; | ||
use Humbug\SelfUpdate\Exception\JsonParsingException; | ||
use Humbug\SelfUpdate\Updater; | ||
use Humbug\SelfUpdate\VersionParser; | ||
use Humbug\SelfUpdate\Exception\RuntimeException; | ||
|
||
final class ManifestStrategy implements StrategyInterface | ||
{ | ||
/** | ||
* @var array | ||
*/ | ||
private static $requiredKeys = array('sha1', 'version', 'url'); | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $manifestUrl; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $manifest; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $availableVersions; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $localVersion; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $allowMajor = false; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $allowUnstable = false; | ||
|
||
/** | ||
* ManifestStrategy constructor. | ||
* | ||
* @param string $localVersion The local version. | ||
* @param string $manifestUrl The URL to a JSON manifest file. The | ||
* manifest contains an array of objects, each | ||
* containing a 'version', 'sha1', and 'url'. | ||
* @param bool $allowMajor Whether to allow updating between major | ||
* versions. | ||
* @param bool $allowUnstable Whether to allow updating to an unstable | ||
* version. Ignored if $localVersion is unstable | ||
* and there are no new stable versions. | ||
*/ | ||
public function __construct($localVersion, $manifestUrl, $allowMajor = false, $allowUnstable = false) | ||
{ | ||
$this->localVersion = $localVersion; | ||
$this->manifestUrl = $manifestUrl; | ||
$this->allowMajor = $allowMajor; | ||
$this->allowUnstable = $allowUnstable; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getCurrentLocalVersion(Updater $updater) | ||
{ | ||
return $this->localVersion; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function download(Updater $updater) | ||
{ | ||
$version = $this->getCurrentRemoteVersion($updater); | ||
if ($version === false) { | ||
throw new RuntimeException('No remote versions found'); | ||
} | ||
|
||
$versionInfo = $this->getAvailableVersions(); | ||
if (!isset($versionInfo[$version])) { | ||
throw new RuntimeException(sprintf('Failed to find manifest item for version %s', $version)); | ||
} | ||
|
||
$fileContents = file_get_contents($versionInfo[$version]['url']); | ||
if ($fileContents === false) { | ||
throw new HttpRequestException(sprintf('Failed to download file from URL: %s', $versionInfo[$version]['url'])); | ||
} | ||
|
||
$tmpFilename = $updater->getTempPharFile(); | ||
if (file_put_contents($tmpFilename, $fileContents) === false) { | ||
throw new RuntimeException(sprintf('Failed to write file: %s', $tmpFilename)); | ||
} | ||
|
||
$tmpSha = sha1_file($tmpFilename); | ||
if ($tmpSha !== $versionInfo[$version]['sha1']) { | ||
unlink($tmpFilename); | ||
throw new RuntimeException( | ||
sprintf( | ||
'SHA-1 verification failed: expected %s, actual %s', | ||
$versionInfo[$version]['sha1'], | ||
$tmpSha | ||
) | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getCurrentRemoteVersion(Updater $updater) | ||
{ | ||
$versions = array_keys($this->getAvailableVersions()); | ||
if (!$this->allowMajor) { | ||
$versions = $this->filterByLocalMajorVersion($versions); | ||
} | ||
|
||
$versionParser = new VersionParser($versions); | ||
|
||
$mostRecent = $versionParser->getMostRecentStable(); | ||
|
||
// Look for unstable updates if explicitly allowed, or if the local | ||
// version is already unstable and there is no new stable version. | ||
if ($this->allowUnstable || ($versionParser->isUnstable($this->localVersion) && version_compare($mostRecent, $this->localVersion, '<'))) { | ||
$mostRecent = $versionParser->getMostRecentAll(); | ||
} | ||
|
||
return version_compare($mostRecent, $this->localVersion, '>') ? $mostRecent : false; | ||
} | ||
|
||
/** | ||
* Gets available versions to update to. | ||
* | ||
* @return array An array keyed by the version name, whose elements are arrays | ||
* containing version information ('name', 'sha1', and 'url'). | ||
*/ | ||
private function getAvailableVersions() | ||
{ | ||
if (isset($this->availableVersions)) { | ||
return $this->availableVersions; | ||
} | ||
|
||
$this->availableVersions = array(); | ||
foreach ($this->retrieveManifest() as $key => $item) { | ||
if ($missing = array_diff(self::$requiredKeys, array_keys($item))) { | ||
throw new RuntimeException(sprintf('Manifest item %s missing required key(s): %s', $key, implode(',', $missing))); | ||
} | ||
$this->availableVersions[$item['version']] = $item; | ||
} | ||
return $this->availableVersions; | ||
} | ||
|
||
/** | ||
* Download and decode the JSON manifest file. | ||
* | ||
* @return array | ||
*/ | ||
private function retrieveManifest() | ||
{ | ||
if (isset($this->manifest)) { | ||
return $this->manifest; | ||
} | ||
|
||
if (!isset($this->manifest)) { | ||
$manifestContents = file_get_contents($this->manifestUrl); | ||
if ($manifestContents === false) { | ||
throw new RuntimeException(sprintf('Failed to download manifest: %s', $this->manifestUrl)); | ||
} | ||
|
||
$this->manifest = json_decode($manifestContents, true, 512, JSON_OBJECT_AS_ARRAY); | ||
if (json_last_error() !== JSON_ERROR_NONE) { | ||
throw new JsonParsingException( | ||
'Error parsing manifest file' | ||
. (function_exists('json_last_error_msg') ? ': ' . json_last_error_msg() : '') | ||
); | ||
} | ||
} | ||
|
||
return $this->manifest; | ||
} | ||
|
||
/** | ||
* Filter a list of versions to those that match the current local version. | ||
* | ||
* @param string[] $versions | ||
* | ||
* @return string[] | ||
*/ | ||
private function filterByLocalMajorVersion(array $versions) | ||
{ | ||
list($localMajorVersion, ) = explode('.', $this->localVersion, 2); | ||
|
||
return array_filter($versions, function ($version) use ($localMajorVersion) { | ||
list($majorVersion, ) = explode('.', $version, 2); | ||
return $majorVersion === $localMajorVersion; | ||
}); | ||
} | ||
} |
95 changes: 95 additions & 0 deletions
95
tests/Humbug/Test/SelfUpdate/UpdaterManifestStrategyTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?php | ||
|
||
namespace Humbug\Test\SelfUpdate; | ||
|
||
use Humbug\SelfUpdate\Updater; | ||
use Humbug\SelfUpdate\Strategy\ManifestStrategy; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
class UpdaterManifestStrategyTest extends TestCase | ||
{ | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $files; | ||
|
||
/** | ||
* @var Updater | ||
*/ | ||
private $updater; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $manifestFile; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $tmp; | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function setup() | ||
{ | ||
$this->tmp = sys_get_temp_dir(); | ||
$this->files = __DIR__ . '/_files'; | ||
$this->updater = new Updater($this->files . '/test.phar', false); | ||
$this->manifestFile = $this->files . '/manifest.json'; | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function teardown() | ||
{ | ||
@unlink($this->tmp . '/test.phar'); | ||
@unlink($this->tmp . '/backup.phar'); | ||
} | ||
|
||
public function testGetLocalVersion() | ||
{ | ||
$strategy = new ManifestStrategy('1.0.0', $this->manifestFile); | ||
$this->assertEquals('1.0.0', $strategy->getCurrentLocalVersion($this->updater)); | ||
} | ||
|
||
public function testSuggestMostRecentStable() | ||
{ | ||
$strategy = new ManifestStrategy('1.0.0', $this->manifestFile); | ||
$this->assertEquals('1.2.0', $strategy->getCurrentRemoteVersion($this->updater)); | ||
} | ||
|
||
public function testSuggestNewestUnstable() | ||
{ | ||
$strategy = new ManifestStrategy('1.0.0', $this->manifestFile, false, true); | ||
$this->assertEquals('1.3.0-beta', $strategy->getCurrentRemoteVersion($this->updater)); | ||
} | ||
|
||
public function testSuggestNewestStableFromUnstable() | ||
{ | ||
$strategy = new ManifestStrategy('1.0.0-beta', $this->manifestFile); | ||
$this->assertEquals('1.2.0', $strategy->getCurrentRemoteVersion($this->updater)); | ||
} | ||
|
||
public function testSuggestNewestUnstableFromUnstable() | ||
{ | ||
$strategy = new ManifestStrategy('1.2.9-beta', $this->manifestFile); | ||
$this->assertEquals('1.3.0-beta', $strategy->getCurrentRemoteVersion($this->updater)); | ||
} | ||
|
||
public function testUpdate() | ||
{ | ||
copy($this->files . '/test.phar', $this->tmp . '/test.phar'); | ||
$updater = new Updater($this->tmp . '/test.phar', false); | ||
$strategy = new ManifestStrategy('1.0.0', $this->manifestFile); | ||
$updater->setStrategyObject($strategy); | ||
$updater->setBackupPath($this->tmp . '/backup.phar'); | ||
$cwd = getcwd(); | ||
chdir(__DIR__); | ||
$this->assertTrue($updater->update()); | ||
chdir($cwd); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
[ | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "2.0.0" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "2.0.0-beta" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "1.3.0-beta" | ||
}, | ||
{ | ||
"sha1": "0bc24f886bc0c7563187167b334e56cfb8e1151a", | ||
"url": "_files/build/nosig.phar", | ||
"version": "1.2.0" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "1.1.0" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "1.0.0" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "1.0.0-beta" | ||
}, | ||
{ | ||
"sha1": "", | ||
"url": "", | ||
"version": "0.9.0" | ||
} | ||
] |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure, that sha1 should be used for version verification? Considering recent findings on http://shattered.io/ it is possible to create another phar with same sha1 hash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's on the todo list. I'm thinking about scrapping sha1 altogether since this is a new feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight backtrack. I'm allowing SHA-1, but not by default, to allow for any transitioning, e.g. platform.sh which this is based of off.