├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpstan-baseline.neon └── src ├── Exception ├── ExceptionInterface.php ├── FilesystemException.php ├── HttpRequestException.php ├── InvalidArgumentException.php ├── JsonParsingException.php ├── NoSignatureException.php └── RuntimeException.php ├── Strategy ├── DirectDownloadStrategyAbstract.php ├── GithubStrategy.php ├── Sha256Strategy.php ├── Sha512Strategy.php ├── ShaStrategy.php ├── ShaStrategyAbstract.php └── StrategyInterface.php ├── Updater.php └── VersionParser.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). 6 | 7 | ## Unreleased 8 | 9 | ## [v1.4.2 - 2025-04-07](https://github.com/laravel-zero/phar-updater/compare/v1.4.1...v1.4.2) 10 | 11 | ### Changed 12 | - Update Packagist repository URL ([#15](https://github.com/laravel-zero/phar-updater/pull/15)) 13 | 14 | ## [v1.4.1 - 2025-03-31](https://github.com/laravel-zero/phar-updater/compare/v1.4.0...v1.4.1) 15 | 16 | ### Changed 17 | - Update to Pint 1.21.x ([#14](https://github.com/laravel-zero/phar-updater/pull/14)) 18 | - Update to PHPStan 2.x ([#14](https://github.com/laravel-zero/phar-updater/pull/14)) 19 | - Update to PHPUnit 11.x ([#14](https://github.com/laravel-zero/phar-updater/pull/14)) 20 | 21 | ### Removed 22 | - Drop support for PHP 8.1 ([#14](https://github.com/laravel-zero/phar-updater/pull/14)) 23 | 24 | ## [v1.4.0 - 2023-09-01](https://github.com/laravel-zero/phar-updater/compare/v1.3.0...v1.4.0) 25 | 26 | ### Changed 27 | - Update to Pint 1.12.x ([#13](https://github.com/laravel-zero/phar-updater/pull/13)) 28 | 29 | ### Fixed 30 | - Resolve `getCurrentRemoteVersion()` when latest is unstable ([#12](https://github.com/laravel-zero/phar-updater/pull/12)) 31 | 32 | ### Removed 33 | - Drop support for PHP 8.0 ([#13](https://github.com/laravel-zero/phar-updater/pull/13)) 34 | 35 | ## [v1.3.0 - 2022-02-04](https://github.com/laravel-zero/phar-updater/compare/v1.2.0...v1.3.0) 36 | 37 | ### Added 38 | - Add a new abstract Direct Download class ([#8](https://github.com/laravel-zero/phar-updater/pull/8)) 39 | 40 | ## [v1.2.0 - 2022-02-04](https://github.com/laravel-zero/phar-updater/compare/v1.1.1...v1.2.0) 41 | 42 | ### Added 43 | - Add support for PHP 8.1 ([#7](https://github.com/laravel-zero/phar-updater/pull/7)) 44 | 45 | ### Removed 46 | - Drop support for PHP `<8.0` ([#7](https://github.com/laravel-zero/phar-updater/pull/7)) 47 | 48 | ## [v1.1.1 - 2021-08-03](https://github.com/laravel-zero/phar-updater/compare/v1.1.0...v1.1.1) 49 | 50 | ### Fixed 51 | - Apply packagist changes to GithubStrategy ([#5](https://github.com/laravel-zero/phar-updater/pull/5)) 52 | 53 | ## [v1.1.0 - 2021-06-29](https://github.com/laravel-zero/phar-updater/compare/v1.0.6...v1.1.0) 54 | 55 | ### Added 56 | - Add support for PHP 8 ([#1](https://github.com/laravel-zero/phar-updater/pull/1)) 57 | - Add PHPStan for static analysis ([#3](https://github.com/laravel-zero/phar-updater/pull/3)) 58 | - Add new SHA-256 strategy ([7632ea0](https://github.com/laravel-zero/phar-updater/commit/7632ea05325049700463743bffdadb29d072bb94)) 59 | - Add new SHA-512 strategy ([#4](https://github.com/laravel-zero/phar-updater/pull/4)) 60 | 61 | ### Changed 62 | - Update to use PHPUnit 9.4 ([fe5cfcc](https://github.com/laravel-zero/phar-updater/commit/fe5cfccb47b91920fc7cecb327c77e28650f3815)) 63 | - Update to use Packagist Composer 2.x API for `GitHubStrategy` ([162c9af](https://github.com/laravel-zero/phar-updater/commit/162c9af6cf53fabb4985c6e402e00fda3ed51654)) 64 | 65 | ### Removed 66 | - Drop support for PHP `<7.3` ([646c693](https://github.com/laravel-zero/phar-updater/commit/646c693f4fc03a2e1ec65eaf399a6eb014519397)) 67 | - Remove `humbug_get_contents` ([7737a2f](https://github.com/laravel-zero/phar-updater/commit/7737a2f6c2e2414252e89f0163be843f23615f28)) 68 | 69 | ## v1.0.6 - 2020-10-29 70 | 71 | ### Added 72 | - Initial version (the same as [`padraic/phar-updater:v1.0.6`](https://github.com/humbug/phar-updater)) 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Pádraic Brady 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Pádraic Brady, Humbug, nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHAR Updater 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/laravel-zero/phar-updater.svg?style=flat-square)](https://packagist.org/packages/laravel-zero/phar-updater) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/laravel-zero/phar-updater/tests.yml.svg?branch=main&style=flat-square)](https://github.com/laravel-zero/phar-updater/actions) 6 | [![Static Analysis](https://img.shields.io/github/actions/workflow/status/laravel-zero/phar-updater/static.yml.svg?branch=main&style=flat-square&label=Static%20Analysis)](https://github.com/laravel-zero/phar-updater/actions) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/laravel-zero/phar-updater.svg?style=flat-square)](https://packagist.org/packages/laravel-zero/phar-updater) 8 | 9 | > This is a fork of the [Humbug PHAR Updater](https://github.com/humbug/phar-updater) for internal use in Laravel Zero. 10 | 11 | The backend for the self-update command in Laravel Zero PHARs. Originally created by Humbug. 12 | 13 | **Table of Contents** 14 | 15 | - [Introduction](#introduction) 16 | - [Installation](#installation) 17 | - [Usage](#usage) 18 | - [Basic SHA-1 / SHA-256 / SHA-512 Strategy](#basic-sha-1--sha-256--sha-512-strategy) 19 | - [Github Release Strategy](#github-release-strategy) 20 | - [Rollback Support](#rollback-support) 21 | - [Constructor Parameters](#constructor-parameters) 22 | - [Check For Updates](#check-for-updates) 23 | - [Avoid Post Update File Includes](#avoid-post-update-file-includes) 24 | - [Custom Update Strategies](#custom-update-strategies) 25 | - [Update Strategies](#update-strategies) 26 | - [SHA-1 / SHA-256 / SHA-512 Hash Synchronisation](#sha-1--sha-256--sha-512-hash-synchronisation) 27 | - [Github Releases](#github-releases) 28 | - [Direct Downloads](#direct-downloads) 29 | 30 | ## Introduction 31 | 32 | The `laravel-zero/phar-updater` package has the following features: 33 | 34 | * Full support for SSL/TLS verification. 35 | * Support for OpenSSL phar signatures. 36 | * Simple API where it either updates or Exceptions will go wild. 37 | * Support for SHA-1/SHA-256/SHA-512 version synchronisation and Github Releases as update strategies. 38 | 39 | Apart from the detailed documentation below, you can find the package being used within 40 | [Laravel Zero's self-update component](https://github.com/laravel-zero/framework/blob/master/src/Components/Updater). 41 | 42 | ## Installation 43 | 44 | Via Composer 45 | 46 | ```bash 47 | composer require laravel-zero/phar-updater 48 | ``` 49 | 50 | Via the Laravel Zero component installer 51 | 52 | ```bash 53 | php app:install self-update 54 | ``` 55 | 56 | The package utilises PHP Streams for remote requests, so it will require the openssl extension and the `allow_url_open` 57 | setting to both be enabled. Support for curl will follow in time. 58 | 59 | ## Usage 60 | 61 | The default update strategy uses an SHA-1 hash of the current remote phar in a version file, and will update the local 62 | phar when this version is changed. There is also a Github strategy which tracks Github Releases where you can upload a 63 | new phar file for a release. 64 | 65 | ### Basic SHA-1 / SHA-256 / SHA-512 Strategy 66 | 67 | > NOTE: The SHA-1 strategy is marked as deprecated, you should prefer the SHA-256 or SHA-512 strategies instead. 68 | 69 | Create your self-update command, or even an update command for some other phar other than the current one, and include 70 | this. 71 | 72 | ```php 73 | /** 74 | * The simplest usage assumes the currently running phar is to be updated and 75 | * that it has been signed with a private key (using OpenSSL). 76 | * 77 | * The first constructor parameter is the path to a phar if you are not updating 78 | * the currently running phar. 79 | */ 80 | 81 | use Humbug\SelfUpdate\Updater; 82 | 83 | $updater = new Updater(); 84 | 85 | // Add the below to use the SHA-256 strategy. It will default to SHA-1 if excluded. 86 | $updater->setStrategy(Updater::STRATEGY_SHA256); 87 | 88 | $updater->getStrategy()->setPharUrl('https://example.com/current.phar'); 89 | $updater->getStrategy()->setVersionUrl('https://example.com/current.version'); 90 | try { 91 | $result = $updater->update(); 92 | echo $result ? "Updated!\n" : "No update needed!\n"; 93 | } catch (\Exception $e) { 94 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 95 | exit(1); 96 | } 97 | ``` 98 | 99 | If you are not signing the phar using OpenSSL: 100 | 101 | ```php 102 | /** 103 | * The second parameter to the constructor must be false if your phars are 104 | * not signed using OpenSSL. 105 | */ 106 | 107 | use Humbug\SelfUpdate\Updater; 108 | 109 | $updater = new Updater(null, false); 110 | $updater->getStrategy()->setPharUrl('https://example.com/current.phar'); 111 | $updater->getStrategy()->setVersionUrl('https://example.com/current.version'); 112 | try { 113 | $result = $updater->update(); 114 | echo $result ? "Updated!\n" : "No update needed!\n"; 115 | } catch (\Exception $e) { 116 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 117 | exit(1); 118 | } 119 | ``` 120 | 121 | If you need version information: 122 | 123 | ```php 124 | use Humbug\SelfUpdate\Updater; 125 | 126 | $updater = new Updater(); 127 | $updater->getStrategy()->setPharUrl('https://example.com/current.phar'); 128 | $updater->getStrategy()->setVersionUrl('https://example.com/current.version'); 129 | try { 130 | $result = $updater->update(); 131 | if ($result) { 132 | $new = $updater->getNewVersion(); 133 | $old = $updater->getOldVersion(); 134 | printf( 135 | 'Updated from SHA-1 %s to SHA-1 %s', $old, $new 136 | ); 137 | } else { 138 | echo "No update needed!\n"; 139 | } 140 | } catch (\Exception $e) { 141 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 142 | exit(1); 143 | } 144 | ``` 145 | 146 | See the [Update Strategies](#update-strategies) section for an overview of how to set up the SHA-1 or SHA-256 strategy. 147 | It's a simple to maintain choice for development or nightly versions of phars which are released to a specific numbered 148 | version. 149 | 150 | ### Github Release Strategy 151 | 152 | Beyond development or nightly phars, if you are releasing numbered versions on Github (i.e. tags), you can upload 153 | additional files (such as phars) to include in the Github Release. 154 | 155 | ```php 156 | /** 157 | * Other than somewhat different setters for the strategy, all other operations 158 | * are identical. 159 | */ 160 | 161 | use Humbug\SelfUpdate\Updater; 162 | 163 | $updater = new Updater(); 164 | $updater->setStrategy(Updater::STRATEGY_GITHUB); 165 | $updater->getStrategy()->setPackageName('myvendor/myapp'); 166 | $updater->getStrategy()->setPharName('myapp.phar'); 167 | $updater->getStrategy()->setCurrentLocalVersion('v1.0.1'); 168 | try { 169 | $result = $updater->update(); 170 | echo $result ? "Updated!\n" : "No update needed!\n"; 171 | } catch (\Exception $e) { 172 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 173 | exit(1); 174 | } 175 | ``` 176 | 177 | Package name refers to the name used by Packagist, and phar name is the phar's file name assumed to be constant across 178 | versions. 179 | 180 | It's left to the implementation to supply the current release version associated with the local phar. This needs to be 181 | stored within the phar and should match the version string used by Github. This can follow any standard practice with 182 | recognisable pre- and postfixes, e.g. 183 | `v1.0.3`, `1.0.3`, `1.1`, `1.3rc`, `1.3.2pl2`. 184 | 185 | If you wish to update to a non-stable version, for example where users want to update according to a development track, 186 | you can set the stability flag for the Github strategy. By default this is set to `stable` or, in constant form, 187 | `\Humbug\SelfUpdate\Strategy\GithubStrategy::STABLE`: 188 | 189 | ```php 190 | $updater->getStrategy()->setStability('unstable'); 191 | ``` 192 | 193 | If you want to ignore stability and just update to the most recent version regardless: 194 | 195 | ```php 196 | $updater->getStrategy()->setStability('any'); 197 | ``` 198 | 199 | ### Rollback Support 200 | 201 | The Updater automatically copies a backup of the original phar to myname-old.phar. You can trigger a rollback quite 202 | easily using this convention: 203 | 204 | ```php 205 | use Humbug\SelfUpdate\Updater; 206 | 207 | /** 208 | * Same constructor parameters as you would use for updating. Here, just defaults. 209 | */ 210 | $updater = new Updater(); 211 | try { 212 | $result = $updater->rollback(); 213 | if (!$result) { 214 | echo "Failure!\n"; 215 | exit(1); 216 | } 217 | echo "Success!\n"; 218 | } catch (\Exception $e) { 219 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 220 | exit(1); 221 | } 222 | ``` 223 | 224 | As users may have diverse requirements in naming and locating backups, you can explicitly manage the precise path to 225 | where a backup should be written, or read from using the `setBackupPath()` function when updating a current phar or the 226 | `setRestorePath()` prior to triggering a rollback. These will be used instead of the simple built in convention. 227 | 228 | ### Constructor Parameters 229 | 230 | The Updater constructor is fairly simple. The three basic variations are: 231 | 232 | ```php 233 | /** 234 | * Default: Update currently running phar which has been signed. 235 | */ 236 | $updater = new Updater; 237 | ``` 238 | 239 | ```php 240 | /** 241 | * Update currently running phar which has NOT been signed. 242 | */ 243 | $updater = new Updater(null, false); 244 | ``` 245 | 246 | ```php 247 | /** 248 | * Use a strategy other than the default SHA Hash. 249 | */ 250 | $updater = new Updater(null, false, Updater::STRATEGY_GITHUB); 251 | ``` 252 | 253 | ```php 254 | /** 255 | * Update a different phar which has NOT been signed. 256 | */ 257 | $updater = new Updater('/path/to/impersonatephil.phar', false); 258 | ``` 259 | 260 | ### Check For Updates 261 | 262 | You can tell users what updates are available, across any stability track, by making use of the `hasUpdate` method. This 263 | gets the most recent remote version for a stability level, compares it to the current version, and returns a simple 264 | true/false result, i.e. it will only be false where the local version is identical or where there was no remote version 265 | for that stability level at all. You can easily differentiate between the two false states as the new version will be a 266 | string where a version did exist, but `false` if not. 267 | 268 | ```php 269 | use Humbug\SelfUpdate\Updater; 270 | 271 | /** 272 | * Configuration is identical in every way for actual updates. You can run this 273 | * across multiple configuration variants to get recent stable, unstable, and dev 274 | * versions available. 275 | * 276 | * This would configure update for an unsigned phar (second constructor must be 277 | * false in this case). 278 | */ 279 | $updater = new Updater(null, false); 280 | $updater->setStrategy(Updater::STRATEGY_GITHUB); 281 | $updater->getStrategy()->setPackageName('myvendor/myapp'); 282 | $updater->getStrategy()->setPharName('myapp.phar'); 283 | $updater->getStrategy()->setCurrentLocalVersion('v1.0.1'); 284 | 285 | try { 286 | $result = $updater->hasUpdate(); 287 | if ($result) { 288 | printf( 289 | 'The current stable build available remotely is: %s', 290 | $updater->getNewVersion() 291 | ); 292 | } elseif (false === $updater->getNewVersion()) { 293 | echo "There are no stable builds available.\n"; 294 | } else { 295 | echo "You have the current stable build installed.\n"; 296 | } 297 | } catch (\Exception $e) { 298 | echo "Well, something happened! Either an oopsie or something involving hackers.\n"; 299 | exit(1); 300 | } 301 | ``` 302 | 303 | ### Avoid Post Update File Includes 304 | 305 | Updating a currently running phar is made trickier since, once replaced, attempts to load files from it within a process 306 | originating from an older phar is likely to create an `internal corruption of phar` error. For example, if you're using 307 | Symfony Console and have created an event dispatcher for your commands, the lazy loading of some event classes will have 308 | this impact. 309 | 310 | The solution is to disable or remove the dispatcher for your self-update command. 311 | 312 | In general, when writing your self-update CLI commands, either pre-load any classes likely needed prior to updating, or 313 | disable their loading if not essential. 314 | 315 | ### Custom Update Strategies 316 | 317 | All update strategies revolve around checking for updates, and downloading updates. The actual work behind replacing 318 | local files and backups is handled separately. To create a custom strategy, you can 319 | implement `Humbug\SelfUpdate\Strategy\StrategyInterface` 320 | and pass a new instance of your implementation post-construction. 321 | 322 | ```php 323 | $updater = new Updater(null, false); 324 | $updater->setStrategyObject(new MyStrategy); 325 | ``` 326 | 327 | The similar `setStrategy()` method is solely used to pass flags matching internal strategies. 328 | 329 | ## Update Strategies 330 | 331 | ### SHA-1 / SHA-256 / SHA-512 Hash Synchronisation 332 | 333 | The phar-updater package supports an update strategy where phars are updated according to the SHA-1, SHA-256, or SHA-512 334 | hash of the current PHAR file available remotely. This assumes the existence of only two to three remote files: 335 | 336 | * myname.phar 337 | * myname.version 338 | * myname.phar.pubkey (optional) 339 | 340 | The `myname.phar` is the most recently built phar. 341 | 342 | The `myname.version` contains the SHA-1, SHA-256, or SHA-512 hash of the most recently built phar where the hash is the very first 343 | string (if not the only string). You can generate this quite easily from bash using: 344 | 345 | ```bash 346 | # For SHA-1 347 | sha1sum myname.phar > myname.version 348 | 349 | # For SHA-256 350 | sha256sum myname.phar > myname.version 351 | 352 | # For SHA-512 353 | sha512sum myname.phar > myname.version 354 | ``` 355 | 356 | Remember to regenerate the version file for each new phar build you want to distribute. Using `sha1sum`/`sha256sum`/`sha512sum` 357 | adds additional data after the hash, but it's fine since the hash is the first string in the file which is the only 358 | requirement. 359 | 360 | If using OpenSSL signing, which is very much recommended, you can also put the public key online as `myname.phar.pubkey` 361 | , for the initial installation of your phar. However, please note that phar-updater itself will never download this key, 362 | will never replace this key on your filesystem, and will never install a phar whose signature cannot be verified by the 363 | locally cached public key. 364 | 365 | If you need to switch keys for any reason whatsoever, users will need to manually download a new phar along with the new 366 | key. While that sounds extreme, it's generally not a good idea to allow for arbitrary key changes that occur without 367 | user knowledge. The openssl signing has no mechanism such as a central authority, or a browser's trusted certificate 368 | stash with which to automate such key changes in a safe manner. 369 | 370 | ### Github Releases 371 | 372 | When tagging new versions on Github, these are created and hosted as `Github Releases` 373 | which allow you to attach a changelog and additional file downloads. Using this Github feature allows you to attach new 374 | phars to releases, associating them with a version string that is published on Packagist. 375 | 376 | Taking advantage of this architecture, the Github Strategy for updating phars can compare the existing local phar's 377 | version against remote release versions and update to the most recent stable (or unstable) version from Github. 378 | 379 | At present, it's assume that phar files all bear the same name across releases, i.e. just a plain name like `myapp.phar` 380 | without versioning information in the file name. You can also upload your phar's public key in the same way. Using the 381 | established convention of being the phar name with `.pubkey` appended, e.g. 382 | `myapp.phar` would be matched with `myapp.phar.pubkey`. 383 | 384 | You can read more about Github releases [here](https://help.github.com/articles/creating-releases). 385 | 386 | While you can draft a release, Github releases are created automatically whenever you create a new git tag. If you use 387 | git tagging, you can go to the matching release on Github, click the `Edit` button and attach files. It's recommended to 388 | do this as soon as possible after tagging to limit the window whereby a new release exists without an updated phar 389 | attached. 390 | 391 | ### Direct Downloads 392 | 393 | PHAR Updater provides an abstract [`Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract` class](src/Strategy/DirectDownloadStrategyAbstract.php) 394 | which can be used to quickly and easily create download strategies with just a `getDownloadUrl(): string` method. 395 | 396 | For example, if a PHAR downloads it's latest updates from `https://example.com/latest/example.phar`, you can utilise this 397 | with the following code: 398 | 399 | ```php 400 | use Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract; 401 | 402 | class ExampleDirectDownloadStrategy extends DirectDownloadStrategyAbstract 403 | { 404 | public function getDownloadUrl(): string 405 | { 406 | return 'https://example.com/latest/example.phar'; 407 | } 408 | } 409 | ``` 410 | 411 | The abstract strategy also supports overriding the `getCurrentRemoteVersion()` method, so that you could add a custom 412 | HTTP call or other method for seeing what the latest version is. By default, it returns the string `latest`. 413 | 414 | ```php 415 | use Illuminate\Support\Facades\Http; 416 | use Humbug\SelfUpdate\Strategy\DirectDownloadStrategyAbstract; 417 | 418 | class ExampleDirectDownloadStrategy extends DirectDownloadStrategyAbstract 419 | { 420 | /** {@inheritdoc} */ 421 | public function getCurrentRemoteVersion(Updater $updater) 422 | { 423 | return Http::get('https://example.com/example-releases.json')->object()->latest_version; 424 | } 425 | 426 | public function getDownloadUrl(): string 427 | { 428 | return "https://example.com/{$this->getCurrentRemoteVersion()}/example.phar"; 429 | } 430 | } 431 | ``` 432 | 433 | You can also set and retrieve the current local version using the `setCurrentLocalVersion()` and `getCurrentLocalVersion()` 434 | methods, which will be used for comparison with the remote version. 435 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-zero/phar-updater", 3 | "description": "A thing to make PHAR self-updating easy and secure.", 4 | "type": "library", 5 | "keywords": ["phar", "self-update", "update", "humbug"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Padraic Brady", 10 | "email": "padraic.brady@gmail.com", 11 | "homepage": "http://blog.astrumfutura.com" 12 | }, 13 | { 14 | "name": "Owen Voke", 15 | "email": "development@voke.dev", 16 | "homepage": "https://voke.dev" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2" 21 | }, 22 | "require-dev": { 23 | "ext-json": "*", 24 | "laravel/pint": "^1.21", 25 | "phpstan/phpstan": "^2.0", 26 | "phpunit/phpunit": "^11.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { "Humbug\\SelfUpdate\\": "src/" } 30 | }, 31 | "scripts": { 32 | "test:types": "phpstan analyse --ansi --memory-limit=-1", 33 | "test:unit": "phpunit --colors=always", 34 | "test": [ 35 | "@test:types", 36 | "@test:unit" 37 | ] 38 | }, 39 | "conflict": { 40 | "padraic/phar-updater": "*" 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Method Humbug\\SelfUpdate\\Strategy\\Sha256Strategy\:\:getCurrentRemoteVersion\(\) never returns bool so it can be removed from the return type\.$#' 5 | identifier: return.unusedType 6 | count: 1 7 | path: src/Strategy/Sha256Strategy.php 8 | 9 | - 10 | message: '#^Method Humbug\\SelfUpdate\\Strategy\\Sha512Strategy\:\:getCurrentRemoteVersion\(\) never returns bool so it can be removed from the return type\.$#' 11 | identifier: return.unusedType 12 | count: 1 13 | path: src/Strategy/Sha512Strategy.php 14 | 15 | - 16 | message: '#^Call to function is_bool\(\) with bool will always evaluate to true\.$#' 17 | identifier: function.alreadyNarrowedType 18 | count: 1 19 | path: src/Updater.php 20 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getDownloadUrl()); 20 | restore_error_handler(); 21 | if ($result === false) { 22 | throw new HttpRequestException(sprintf( 23 | 'Request to URL failed: %s', 24 | $this->getDownloadUrl() 25 | )); 26 | } 27 | 28 | file_put_contents($updater->getTempPharFile(), $result); 29 | } 30 | 31 | /** {@inheritdoc} */ 32 | public function getCurrentRemoteVersion(Updater $updater) 33 | { 34 | return 'latest'; 35 | } 36 | 37 | public function setCurrentLocalVersion(string $version): void 38 | { 39 | $this->localVersion = $version; 40 | } 41 | 42 | /** {@inheritdoc} */ 43 | public function getCurrentLocalVersion(Updater $updater) 44 | { 45 | return $this->localVersion; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Strategy/GithubStrategy.php: -------------------------------------------------------------------------------- 1 | remoteUrl); 70 | restore_error_handler(); 71 | if ($result === false) { 72 | throw new HttpRequestException(sprintf( 73 | 'Request to URL failed: %s', 74 | $this->remoteUrl 75 | )); 76 | } 77 | 78 | file_put_contents($updater->getTempPharFile(), $result); 79 | } 80 | 81 | /** {@inheritdoc} */ 82 | public function getCurrentRemoteVersion(Updater $updater) 83 | { 84 | /** Switch remote request errors to HttpRequestExceptions */ 85 | set_error_handler([$updater, 'throwHttpRequestException']); 86 | $packageUrl = $this->getApiUrl(); 87 | $package = json_decode(file_get_contents($packageUrl), true); 88 | restore_error_handler(); 89 | 90 | if ($package === null || json_last_error() !== JSON_ERROR_NONE) { 91 | throw new JsonParsingException( 92 | 'Error parsing JSON package data' 93 | .(function_exists('json_last_error_msg') ? ': '.json_last_error_msg() : '') 94 | ); 95 | } 96 | 97 | $versions = array_column($package['packages'][$this->getPackageName()], 'version'); 98 | $versionParser = new VersionParser($versions); 99 | if ($this->getStability() === self::STABLE) { 100 | $this->remoteVersion = $versionParser->getMostRecentStable(); 101 | } elseif ($this->getStability() === self::UNSTABLE) { 102 | $this->remoteVersion = $versionParser->getMostRecentUnstable(); 103 | } else { 104 | $this->remoteVersion = $versionParser->getMostRecentAll(); 105 | } 106 | 107 | /** 108 | * Setup remote URL if there's an actual version to download. 109 | */ 110 | if (! empty($this->remoteVersion)) { 111 | $remoteVersionPackages = array_filter($package['packages'][$this->getPackageName()], function (array $package) { 112 | return $package['version'] === $this->remoteVersion; 113 | }); 114 | $chosenVersion = reset($remoteVersionPackages); 115 | 116 | $this->remoteUrl = $this->getDownloadUrl($chosenVersion); 117 | } 118 | 119 | return $this->remoteVersion; 120 | } 121 | 122 | /** {@inheritdoc} */ 123 | public function getCurrentLocalVersion(Updater $updater) 124 | { 125 | return $this->localVersion; 126 | } 127 | 128 | /** 129 | * Set version string of the local phar. 130 | * 131 | * @param string $version 132 | */ 133 | public function setCurrentLocalVersion($version) 134 | { 135 | $this->localVersion = $version; 136 | } 137 | 138 | /** 139 | * Set Package name. 140 | * 141 | * @param string $name 142 | */ 143 | public function setPackageName($name) 144 | { 145 | $this->packageName = $name; 146 | } 147 | 148 | /** 149 | * Get Package name. 150 | * 151 | * @return string 152 | */ 153 | public function getPackageName() 154 | { 155 | return $this->packageName; 156 | } 157 | 158 | /** 159 | * Set phar file's name. 160 | * 161 | * @param string $name 162 | */ 163 | public function setPharName($name) 164 | { 165 | $this->pharName = $name; 166 | } 167 | 168 | /** 169 | * Get phar file's name. 170 | * 171 | * @return string 172 | */ 173 | public function getPharName() 174 | { 175 | return $this->pharName; 176 | } 177 | 178 | /** 179 | * Set target stability. 180 | * 181 | * @param string $stability 182 | */ 183 | public function setStability($stability) 184 | { 185 | if ($stability !== self::STABLE && $stability !== self::UNSTABLE && $stability !== self::ANY) { 186 | throw new InvalidArgumentException( 187 | 'Invalid stability value. Must be one of "stable", "unstable" or "any".' 188 | ); 189 | } 190 | $this->stability = $stability; 191 | } 192 | 193 | /** 194 | * Get target stability. 195 | * 196 | * @return string 197 | */ 198 | public function getStability() 199 | { 200 | return $this->stability; 201 | } 202 | 203 | protected function getApiUrl() 204 | { 205 | return sprintf(self::API_URL, $this->getPackageName()); 206 | } 207 | 208 | /** @param array $package */ 209 | protected function getDownloadUrl(array $package) 210 | { 211 | $baseUrl = preg_replace( 212 | '{\.git$}', 213 | '', 214 | $package['source']['url'] 215 | ); 216 | 217 | return sprintf( 218 | '%s/releases/download/%s/%s', 219 | $baseUrl, 220 | $this->remoteVersion, 221 | $this->getPharName() 222 | ); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Strategy/Sha256Strategy.php: -------------------------------------------------------------------------------- 1 | getVersionUrl()); 29 | restore_error_handler(); 30 | if ($version === false) { 31 | throw new HttpRequestException(sprintf( 32 | 'Request to URL failed: %s', 33 | $this->getVersionUrl() 34 | )); 35 | } 36 | if (empty($version)) { 37 | throw new HttpRequestException( 38 | 'Version request returned empty response.' 39 | ); 40 | } 41 | if (! preg_match('%^[a-z0-9]{64}%', $version, $matches)) { 42 | throw new HttpRequestException( 43 | 'Version request returned incorrectly formatted response.' 44 | ); 45 | } 46 | 47 | return $matches[0]; 48 | } 49 | 50 | /** {@inheritdoc} */ 51 | public function getCurrentLocalVersion(Updater $updater) 52 | { 53 | return hash_file('sha256', $updater->getLocalPharFile()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Strategy/Sha512Strategy.php: -------------------------------------------------------------------------------- 1 | getVersionUrl()); 18 | restore_error_handler(); 19 | if ($version === false) { 20 | throw new HttpRequestException(sprintf( 21 | 'Request to URL failed: %s', 22 | $this->getVersionUrl() 23 | )); 24 | } 25 | if (empty($version)) { 26 | throw new HttpRequestException( 27 | 'Version request returned empty response.' 28 | ); 29 | } 30 | if (! preg_match('%^[a-z0-9]{128}%', $version, $matches)) { 31 | throw new HttpRequestException( 32 | 'Version request returned incorrectly formatted response.' 33 | ); 34 | } 35 | 36 | return $matches[0]; 37 | } 38 | 39 | /** {@inheritdoc} */ 40 | public function getCurrentLocalVersion(Updater $updater) 41 | { 42 | return hash_file('sha512', $updater->getLocalPharFile()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Strategy/ShaStrategy.php: -------------------------------------------------------------------------------- 1 | getVersionUrl()); 32 | restore_error_handler(); 33 | if ($version === false) { 34 | throw new HttpRequestException(sprintf( 35 | 'Request to URL failed: %s', 36 | $this->getVersionUrl() 37 | )); 38 | } 39 | if (empty($version)) { 40 | throw new HttpRequestException( 41 | 'Version request returned empty response.' 42 | ); 43 | } 44 | if (! preg_match('%^[a-z0-9]{40}%', $version, $matches)) { 45 | throw new HttpRequestException( 46 | 'Version request returned incorrectly formatted response.' 47 | ); 48 | } 49 | 50 | return $matches[0]; 51 | } 52 | 53 | /** {@inheritdoc} */ 54 | public function getCurrentLocalVersion(Updater $updater) 55 | { 56 | return sha1_file($updater->getLocalPharFile()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Strategy/ShaStrategyAbstract.php: -------------------------------------------------------------------------------- 1 | getPharUrl()); 47 | restore_error_handler(); 48 | if ($result === false) { 49 | throw new HttpRequestException(sprintf( 50 | 'Request to URL failed: %s', 51 | $this->getPharUrl() 52 | )); 53 | } 54 | 55 | file_put_contents($updater->getTempPharFile(), $result); 56 | } 57 | 58 | /** 59 | * Set URL to phar file. 60 | * 61 | * @param string $url 62 | */ 63 | public function setPharUrl($url) 64 | { 65 | if (! $this->validateAllowedUrl($url)) { 66 | throw new InvalidArgumentException( 67 | sprintf('Invalid url passed as argument: %s.', $url) 68 | ); 69 | } 70 | $this->pharUrl = $url; 71 | } 72 | 73 | /** 74 | * Get URL for phar file. 75 | * 76 | * @return string 77 | */ 78 | public function getPharUrl() 79 | { 80 | return $this->pharUrl; 81 | } 82 | 83 | /** 84 | * Set URL to version file. 85 | * 86 | * @param string $url 87 | */ 88 | public function setVersionUrl($url) 89 | { 90 | if (! $this->validateAllowedUrl($url)) { 91 | throw new InvalidArgumentException( 92 | sprintf('Invalid url passed as argument: %s.', $url) 93 | ); 94 | } 95 | $this->versionUrl = $url; 96 | } 97 | 98 | /** 99 | * Get URL for version file. 100 | * 101 | * @return string 102 | */ 103 | public function getVersionUrl() 104 | { 105 | return $this->versionUrl; 106 | } 107 | 108 | protected function validateAllowedUrl($url) 109 | { 110 | return 111 | filter_var($url, FILTER_VALIDATE_URL) 112 | && in_array(parse_url($url, PHP_URL_SCHEME), self::SUPPORTED_SCHEMES); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Strategy/StrategyInterface.php: -------------------------------------------------------------------------------- 1 | setLocalPharFile($localPharFile); 109 | if (! is_bool($hasPubKey)) { 110 | throw new InvalidArgumentException( 111 | 'Constructor parameter $hasPubKey must be boolean or null.' 112 | ); 113 | } else { 114 | $this->hasPubKey = $hasPubKey; 115 | } 116 | if ($this->hasPubKey) { 117 | $this->setLocalPubKeyFile(); 118 | } 119 | $this->setTempDirectory(); 120 | $this->setStrategy($strategy); 121 | } 122 | 123 | /** 124 | * Check for update. 125 | * 126 | * @return bool 127 | */ 128 | public function hasUpdate() 129 | { 130 | $this->newVersionAvailable = $this->newVersionAvailable(); 131 | 132 | return $this->newVersionAvailable; 133 | } 134 | 135 | /** 136 | * Perform an update. 137 | * 138 | * @return bool 139 | */ 140 | public function update() 141 | { 142 | if ($this->newVersionAvailable === false 143 | || (! is_bool($this->newVersionAvailable) && ! $this->hasUpdate())) { 144 | return false; 145 | } 146 | $this->backupPhar(); 147 | $this->downloadPhar(); 148 | $this->replacePhar(); 149 | 150 | return true; 151 | } 152 | 153 | /** 154 | * Perform an rollback to previous version. 155 | * 156 | * @return bool 157 | */ 158 | public function rollback() 159 | { 160 | if (! $this->restorePhar()) { 161 | return false; 162 | } 163 | 164 | return true; 165 | } 166 | 167 | /** 168 | * @param string $strategy 169 | */ 170 | public function setStrategy($strategy) 171 | { 172 | switch ($strategy) { 173 | case self::STRATEGY_GITHUB: 174 | $this->strategy = new GithubStrategy; 175 | break; 176 | 177 | case self::STRATEGY_SHA256: 178 | $this->strategy = new Sha256Strategy; 179 | break; 180 | 181 | case self::STRATEGY_SHA512: 182 | $this->strategy = new Sha512Strategy; 183 | break; 184 | 185 | default: 186 | $this->strategy = new ShaStrategy; 187 | break; 188 | } 189 | } 190 | 191 | public function setStrategyObject(StrategyInterface $strategy) 192 | { 193 | $this->strategy = $strategy; 194 | } 195 | 196 | public function getStrategy() 197 | { 198 | return $this->strategy; 199 | } 200 | 201 | /** 202 | * Set backup extension for old phar versions. 203 | * 204 | * @param string $extension 205 | */ 206 | public function setBackupExtension($extension) 207 | { 208 | $this->backupExtension = $extension; 209 | } 210 | 211 | /** 212 | * Get backup extension for old phar versions. 213 | * 214 | * @return string 215 | */ 216 | public function getBackupExtension() 217 | { 218 | return $this->backupExtension; 219 | } 220 | 221 | public function getLocalPharFile() 222 | { 223 | return $this->localPharFile; 224 | } 225 | 226 | public function getLocalPharFileBasename() 227 | { 228 | return $this->localPharFileBasename; 229 | } 230 | 231 | public function getLocalPubKeyFile() 232 | { 233 | return $this->localPubKeyFile; 234 | } 235 | 236 | public function getTempDirectory() 237 | { 238 | return $this->tempDirectory; 239 | } 240 | 241 | public function getTempPharFile() 242 | { 243 | return $this->getTempDirectory() 244 | .'/' 245 | .sprintf('%s.phar.temp', $this->getLocalPharFileBasename()); 246 | } 247 | 248 | public function getNewVersion() 249 | { 250 | return $this->newVersion; 251 | } 252 | 253 | public function getOldVersion() 254 | { 255 | return $this->oldVersion; 256 | } 257 | 258 | /** 259 | * Set backup path for old phar versions. 260 | * 261 | * @param string $filePath 262 | */ 263 | public function setBackupPath($filePath) 264 | { 265 | $path = realpath(dirname($filePath)); 266 | if (! is_dir($path)) { 267 | throw new FilesystemException(sprintf( 268 | 'The backup directory does not exist: %s.', 269 | $path 270 | )); 271 | } 272 | if (! is_writable($path)) { 273 | throw new FilesystemException(sprintf( 274 | 'The backup directory is not writeable: %s.', 275 | $path 276 | )); 277 | } 278 | $this->backupPath = $filePath; 279 | } 280 | 281 | /** 282 | * Get backup path for old phar versions. 283 | * 284 | * @return string|null 285 | */ 286 | public function getBackupPath() 287 | { 288 | return $this->backupPath; 289 | } 290 | 291 | /** 292 | * Set path for the backup phar to rollback/restore from. 293 | * 294 | * @param string $filePath 295 | */ 296 | public function setRestorePath($filePath) 297 | { 298 | $path = realpath(dirname($filePath)); 299 | if (! file_exists($path)) { 300 | throw new FilesystemException(sprintf( 301 | 'The restore phar does not exist: %s.', 302 | $path 303 | )); 304 | } 305 | if (! is_readable($path)) { 306 | throw new FilesystemException(sprintf( 307 | 'The restore file is not readable: %s.', 308 | $path 309 | )); 310 | } 311 | $this->restorePath = $filePath; 312 | } 313 | 314 | /** 315 | * Get path for the backup phar to rollback/restore from. 316 | * 317 | * @return string|null 318 | */ 319 | public function getRestorePath() 320 | { 321 | return $this->restorePath; 322 | } 323 | 324 | public function throwRuntimeException($errno, $errstr) 325 | { 326 | if ($errno === E_USER_DEPRECATED) { 327 | return; 328 | } 329 | 330 | throw new RuntimeException($errstr); 331 | } 332 | 333 | public function throwHttpRequestException($errno, $errstr) 334 | { 335 | if ($errno === E_USER_DEPRECATED) { 336 | return; 337 | } 338 | 339 | throw new HttpRequestException($errstr); 340 | } 341 | 342 | protected function hasPubKey() 343 | { 344 | return $this->hasPubKey; 345 | } 346 | 347 | protected function newVersionAvailable() 348 | { 349 | $this->newVersion = $this->strategy->getCurrentRemoteVersion($this); 350 | $this->oldVersion = $this->strategy->getCurrentLocalVersion($this); 351 | 352 | if (! empty($this->newVersion) && ($this->newVersion !== $this->oldVersion)) { 353 | return true; 354 | } 355 | 356 | return false; 357 | } 358 | 359 | protected function backupPhar() 360 | { 361 | $result = copy($this->getLocalPharFile(), $this->getBackupPharFile()); 362 | if ($result === false) { 363 | $this->cleanupAfterError(); 364 | throw new FilesystemException(sprintf( 365 | 'Unable to backup %s to %s.', 366 | $this->getLocalPharFile(), 367 | $this->getBackupPharFile() 368 | )); 369 | } 370 | } 371 | 372 | protected function downloadPhar() 373 | { 374 | $this->strategy->download($this); 375 | 376 | if (! file_exists($this->getTempPharFile())) { 377 | throw new FilesystemException( 378 | 'Creation of download file failed.' 379 | ); 380 | } 381 | 382 | $strategy = $this->getStrategy(); 383 | 384 | if ($strategy instanceof ShaStrategyAbstract) { 385 | if ($strategy instanceof ShaStrategy) { 386 | $tmpVersion = sha1_file($this->getTempPharFile()); 387 | $algo = 'SHA-1'; 388 | } elseif ($strategy instanceof Sha512Strategy) { 389 | $tmpVersion = hash_file('sha512', $this->getTempPharFile()); 390 | $algo = 'SHA-512'; 391 | } else { 392 | $tmpVersion = hash_file('sha256', $this->getTempPharFile()); 393 | $algo = 'SHA-256'; 394 | } 395 | 396 | if ($tmpVersion !== $this->getNewVersion()) { 397 | $this->cleanupAfterError(); 398 | throw new HttpRequestException(sprintf( 399 | 'Download file appears to be corrupted or outdated. The file ' 400 | .'received does not have the expected %s hash: %s.', 401 | $algo, 402 | $this->getNewVersion() 403 | )); 404 | } 405 | } 406 | 407 | try { 408 | $this->validatePhar($this->getTempPharFile()); 409 | } catch (\Exception $e) { 410 | restore_error_handler(); 411 | $this->cleanupAfterError(); 412 | throw $e; 413 | } 414 | } 415 | 416 | protected function replacePhar() 417 | { 418 | rename($this->getTempPharFile(), $this->getLocalPharFile()); 419 | } 420 | 421 | protected function restorePhar() 422 | { 423 | $backup = $this->getRestorePharFile(); 424 | if (! file_exists($backup)) { 425 | throw new RuntimeException(sprintf( 426 | 'The backup file does not exist: %s.', 427 | $backup 428 | )); 429 | } 430 | $this->validatePhar($backup); 431 | 432 | return rename($backup, $this->getLocalPharFile()); 433 | } 434 | 435 | protected function getBackupPharFile() 436 | { 437 | if ($this->getBackupPath() !== null) { 438 | return $this->getBackupPath(); 439 | } 440 | 441 | return $this->getTempDirectory() 442 | .'/' 443 | .sprintf('%s%s', $this->getLocalPharFileBasename(), $this->getBackupExtension()); 444 | } 445 | 446 | protected function getRestorePharFile() 447 | { 448 | if ($this->getRestorePath() !== null) { 449 | return $this->getRestorePath(); 450 | } 451 | 452 | return $this->getTempDirectory() 453 | .'/' 454 | .sprintf( 455 | '%s%s', 456 | $this->getLocalPharFileBasename(), 457 | $this->getBackupExtension() 458 | ); 459 | } 460 | 461 | protected function getTempPubKeyFile() 462 | { 463 | return $this->getTempDirectory() 464 | .'/' 465 | .sprintf('%s.phar.temp.pubkey', $this->getLocalPharFileBasename()); 466 | } 467 | 468 | protected function setLocalPharFile($localPharFile) 469 | { 470 | if (! is_null($localPharFile)) { 471 | $localPharFile = realpath($localPharFile); 472 | } else { 473 | $localPharFile = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; 474 | } 475 | if (! file_exists($localPharFile)) { 476 | throw new RuntimeException(sprintf( 477 | 'The set phar file does not exist: %s.', 478 | $localPharFile 479 | )); 480 | } 481 | if (! is_writable($localPharFile)) { 482 | throw new FilesystemException(sprintf( 483 | 'The current phar file is not writeable and cannot be replaced: %s.', 484 | $localPharFile 485 | )); 486 | } 487 | $this->localPharFile = $localPharFile; 488 | $this->localPharFileBasename = basename($localPharFile, '.phar'); 489 | } 490 | 491 | protected function setLocalPubKeyFile() 492 | { 493 | $localPubKeyFile = $this->getLocalPharFile().'.pubkey'; 494 | if (! file_exists($localPubKeyFile)) { 495 | throw new RuntimeException(sprintf( 496 | 'The phar pubkey file does not exist: %s.', 497 | $localPubKeyFile 498 | )); 499 | } 500 | $this->localPubKeyFile = $localPubKeyFile; 501 | } 502 | 503 | protected function setTempDirectory() 504 | { 505 | $tempDirectory = dirname($this->getLocalPharFile()); 506 | if (! is_writable($tempDirectory)) { 507 | throw new FilesystemException(sprintf( 508 | 'The directory is not writeable: %s.', 509 | $tempDirectory 510 | )); 511 | } 512 | $this->tempDirectory = $tempDirectory; 513 | } 514 | 515 | protected function validatePhar($phar) 516 | { 517 | $phar = realpath($phar); 518 | if ($this->hasPubKey()) { 519 | copy($this->getLocalPubKeyFile(), $phar.'.pubkey'); 520 | } 521 | chmod($phar, fileperms($this->getLocalPharFile())); 522 | /** Switch invalid key errors to RuntimeExceptions */ 523 | set_error_handler([$this, 'throwRuntimeException']); 524 | $phar = new \Phar($phar); 525 | $signature = $phar->getSignature(); 526 | if ($this->hasPubKey() && strtolower($signature['hash_type']) !== 'openssl') { 527 | throw new NoSignatureException( 528 | 'The downloaded phar file has no OpenSSL signature.' 529 | ); 530 | } 531 | restore_error_handler(); 532 | if ($this->hasPubKey()) { 533 | @unlink($phar.'.pubkey'); 534 | } 535 | unset($phar); 536 | } 537 | 538 | protected function cleanupAfterError() 539 | { 540 | // @unlink($this->getBackupPharFile()); 541 | @unlink($this->getTempPharFile()); 542 | @unlink($this->getTempPubKeyFile()); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /src/VersionParser.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private $versions; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private $modifier = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; 27 | 28 | /** 29 | * @param array $versions 30 | */ 31 | public function __construct(array $versions = []) 32 | { 33 | $this->versions = $versions; 34 | } 35 | 36 | /** 37 | * Get the most recent stable numbered version from versions passed to 38 | * constructor (if any). 39 | * 40 | * @return string 41 | */ 42 | public function getMostRecentStable() 43 | { 44 | return $this->selectRecentStable(); 45 | } 46 | 47 | /** 48 | * Get the most recent unstable numbered version from versions passed to 49 | * constructor (if any). 50 | * 51 | * @return string 52 | */ 53 | public function getMostRecentUnStable() 54 | { 55 | return $this->selectRecentUnstable(); 56 | } 57 | 58 | /** 59 | * Get the most recent stable or unstable numbered version from versions passed to 60 | * constructor (if any). 61 | * 62 | * @return string 63 | */ 64 | public function getMostRecentAll() 65 | { 66 | return $this->selectRecentAll(); 67 | } 68 | 69 | /** 70 | * Checks if given version string represents a stable numbered version. 71 | * 72 | * @param string $version 73 | * @return bool 74 | */ 75 | public function isStable($version) 76 | { 77 | return $this->stable($version); 78 | } 79 | 80 | /** 81 | * Checks if given version string represents a 'pre-release' version, i.e. 82 | * it's unstable but not development level. 83 | * 84 | * @param string $version 85 | * @return bool 86 | */ 87 | public function isPreRelease($version) 88 | { 89 | return ! $this->stable($version) && ! $this->development($version); 90 | } 91 | 92 | /** 93 | * Checks if given version string represents an unstable or dev-level 94 | * numbered version. 95 | * 96 | * @param string $version 97 | * @return bool 98 | */ 99 | public function isUnstable($version) 100 | { 101 | return ! $this->stable($version); 102 | } 103 | 104 | /** 105 | * Checks if given version string represents a dev-level numbered version. 106 | * 107 | * @param string $version 108 | * @return bool 109 | */ 110 | public function isDevelopment($version) 111 | { 112 | return $this->development($version); 113 | } 114 | 115 | private function selectRecentStable() 116 | { 117 | $candidates = []; 118 | foreach ($this->versions as $version) { 119 | if (! $this->stable($version)) { 120 | continue; 121 | } 122 | $candidates[] = $version; 123 | } 124 | if (empty($candidates)) { 125 | return false; 126 | } 127 | 128 | return $this->findMostRecent($candidates); 129 | } 130 | 131 | private function selectRecentUnstable() 132 | { 133 | $candidates = []; 134 | foreach ($this->versions as $version) { 135 | if ($this->stable($version) || $this->development($version)) { 136 | continue; 137 | } 138 | $candidates[] = $version; 139 | } 140 | if (empty($candidates)) { 141 | return false; 142 | } 143 | 144 | return $this->findMostRecent($candidates); 145 | } 146 | 147 | private function selectRecentAll() 148 | { 149 | $candidates = []; 150 | foreach ($this->versions as $version) { 151 | if ($this->development($version)) { 152 | continue; 153 | } 154 | $candidates[] = $version; 155 | } 156 | if (empty($candidates)) { 157 | return false; 158 | } 159 | 160 | return $this->findMostRecent($candidates); 161 | } 162 | 163 | /** @param array $candidates */ 164 | private function findMostRecent(array $candidates) 165 | { 166 | $candidate = ''; 167 | foreach ($candidates as $version) { 168 | if (version_compare($candidate, $version, '<')) { 169 | $candidate = $version; 170 | } 171 | } 172 | 173 | return $candidate; 174 | } 175 | 176 | private function stable($version) 177 | { 178 | $version = preg_replace('{#.+$}i', '', $version); 179 | if ($this->development($version)) { 180 | return false; 181 | } 182 | preg_match('{'.$this->modifier.'$}i', strtolower($version), $match); 183 | if (! empty($match[3])) { 184 | return false; 185 | } 186 | if (! empty($match[1])) { 187 | if ($match[1] === 'beta' || $match[1] === 'b' 188 | || $match[1] === 'alpha' || $match[1] === 'a' 189 | || $match[1] === 'rc') { 190 | return false; 191 | } 192 | } 193 | 194 | return true; 195 | } 196 | 197 | private function development($version) 198 | { 199 | if (substr($version, 0, 4) === 'dev-' || substr($version, -4) === '-dev') { 200 | return true; 201 | } 202 | if (preg_match("/-\d+-[a-z0-9]{8,}$/", $version) == 1) { 203 | return true; 204 | } 205 | 206 | return false; 207 | } 208 | } 209 | --------------------------------------------------------------------------------