├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── onepilot.php ├── phpunit.xml.dist ├── src ├── Classes │ ├── Composer.php │ ├── ComposerPackageDetector.php │ ├── FakePackageDetector.php │ ├── Files.php │ ├── LogsBrowser.php │ ├── LogsFiles.php │ └── LogsOverview.php ├── ClientServiceProvider.php ├── Contracts │ └── PackageDetector.php ├── Controllers │ ├── ErrorsController.php │ ├── MailTesterController.php │ ├── PingController.php │ └── VersionController.php ├── Exceptions │ └── OnePilotException.php ├── Middlewares │ └── Authentication.php ├── Traits │ └── Instantiable.php └── routes.php └── tests ├── Integration ├── AuthenticationTest.php ├── ComposerUpdatesTest.php ├── MailTesterTest.php └── ValidationsTest.php ├── TestCase.php └── data └── composer ├── installed-packages-light.json ├── laravel55-installed-packages.json ├── laravel56-installed-packages.json └── laravel57-installed-packages.json /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | schedule: 8 | - cron: '0 0 * * *' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | include: 18 | - php: 8.3 19 | laravel: ^11.0 20 | testbench: ^9.0 21 | 22 | - php: 8.2 23 | laravel: ^11.0 24 | testbench: ^9.0 25 | - php: 8.2 26 | laravel: ^10.0 27 | testbench: ^8.0 28 | - php: 8.2 29 | laravel: ^9.0 30 | testbench: ^7.0 31 | 32 | - php: 8.1 33 | laravel: ^10.0 34 | testbench: ^8.0 35 | - php: 8.1 36 | laravel: ^9.0 37 | testbench: ^7.0 38 | 39 | - php: 8.0 40 | laravel: ^9.0 41 | testbench: ^7.0 42 | - php: 8.0 43 | laravel: ^8.0 44 | testbench: ^6.0 45 | 46 | - php: 7.4 47 | laravel: ^8.0 48 | testbench: ^6.0 49 | - php: 7.4 50 | laravel: ^7.0 51 | testbench: ^5.0 52 | 53 | - php: 7.3 54 | laravel: ^8.0 55 | testbench: ^6.0 56 | - php: 7.3 57 | laravel: ^7.0 58 | testbench: ^5.0 59 | - php: 7.3 60 | laravel: ^6.0 61 | testbench: ^4.0 62 | - php: 7.3 63 | laravel: 5.8.* 64 | testbench: 3.8.* 65 | - php: 7.3 66 | laravel: 5.7.* 67 | testbench: 3.7.* 68 | 69 | - php: 7.2 70 | laravel: ^7.0 71 | testbench: ^5.0 72 | - php: 7.2 73 | laravel: ^6.0 74 | testbench: ^4.0 75 | - php: 7.2 76 | laravel: 5.8.* 77 | testbench: 3.8.* 78 | - php: 7.2 79 | laravel: 5.7.* 80 | testbench: 3.7.* 81 | - php: 7.2 82 | laravel: 5.6.* 83 | testbench: 3.6.* 84 | - php: 7.2 85 | laravel: 5.5.* 86 | testbench: 3.5.* 87 | 88 | - php: 7.1 89 | laravel: 5.8.* 90 | testbench: 3.8.* 91 | - php: 7.1 92 | laravel: 5.7.* 93 | testbench: 3.7.* 94 | - php: 7.1 95 | laravel: 5.6.* 96 | testbench: 3.6.* 97 | - php: 7.1 98 | laravel: 5.5.* 99 | testbench: 3.5.* 100 | 101 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 102 | 103 | steps: 104 | - name: Checkout code 105 | uses: actions/checkout@v4 106 | 107 | - name: Setup PHP 108 | uses: shivammathur/setup-php@v2 109 | with: 110 | php-version: ${{ matrix.php }} 111 | extensions: dom, curl, libxml, mbstring, zip 112 | tools: composer:v2 113 | coverage: none 114 | 115 | - name: Install dependencies 116 | run: | 117 | composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 118 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 119 | composer update --prefer-dist --no-interaction --no-plugins 120 | 121 | - name: Execute tests 122 | run: vendor/bin/phpunit 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | phpunit.xml 4 | vendor 5 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-client` will be documented in this file. 4 | 5 | ## 1.0.10 - 2023-02-07 6 | - Add support of Laravel 10 7 | - Email verification also sends HTML version 8 | - Increase the list of monitored configs 9 | 10 | ## 1.0.9 - 2022-02-09 11 | - Add support of Laravel 9 12 | 13 | ## 1.0.8 - 2021-07-27 14 | - Add support of composer/semver 3.* 15 | 16 | ## 1.0.7 - 2021-01-14 17 | - Avoid issue if Packagist is temporary unreachable 18 | 19 | ## 1.0.6 - 2020-11-16 20 | 21 | - Add support of composer 2 22 | 23 | ## 1.0.5 - 2020-10-09 24 | 25 | - Add support of Laravel 8 26 | 27 | ## 1.0.4 - 2020-03-11 28 | 29 | - Add support of Laravel 7 30 | 31 | ## 1.0.3 - 2019-09-05 32 | 33 | - Add support for Laravel 6 34 | - Fix for return installed packages not publicly available on Packagist 35 | 36 | ## 1.0.2 - 2019-03-28 37 | 38 | - Add Email Verification support 39 | - Add Central Error Logs support 40 | 41 | ## 1.0.1 - 2018-10-23 42 | 43 | - Fixed a bug that caused a validation error when a package without any requirements was installed 44 | 45 | ## 1.0.0 - 2018-10-15 46 | 47 | - initial release 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/1PilotApp/laravel-client). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Detailed description 4 | 5 | Provide a detailed description of the change or addition you are proposing. 6 | 7 | Make it clear if the issue is a bug, an enhancement or just a question. 8 | 9 | ## Context 10 | 11 | Why is this change important to you? How would you use it? 12 | 13 | How can it benefit other users? 14 | 15 | ## Possible implementation 16 | 17 | Not obligatory, but suggest an idea for implementing addition or change. 18 | 19 | ## Your environment 20 | 21 | Include as many relevant details about the environment you experienced the bug in and how to reproduce it. 22 | 23 | * Version used (e.g. PHP 5.6, HHVM 3): 24 | * Operating system and version (e.g. Ubuntu 16.04, Windows 7): 25 | * Link to your project: 26 | * ... 27 | * ... 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2018 :author_name <:author_email> 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 1Pilot.io - a universal dashboard to effortlessly manage all your sites 3 |

4 | 5 |

6 | Latest Version on Packagist 7 | Software License 8 | Build Status 9 | Total Downloads 10 |

11 | 12 |

13 | Website 14 | · 15 | Free Trial 16 | · 17 | Pricing 18 | · 19 | Documentation 20 | · 21 | API 22 | · 23 | Support 24 |


25 | 26 |

27 | 1Pilot dashboard 28 |

29 | 30 | ## Everything you need to know in just one dashboard. 31 | 32 | - **Uptime monitoring**
Get instant notifications about downtime and fix it before everyone else even knows it’s an issue. 33 | 34 | - **SSL certificate monitoring**
Keep track of certificates across all your applications and set reminders of their expiration dates. 35 | - **Config file and server version monitoring**
Be alerted when a config file is edited or when PHP, Database or WEB servers are updated. 36 | 37 | - **Composer package management**
See installed composer packages across all your applications and track their updates. Know exactly when new versions are available and log a central history of all changes. 38 | 39 | - **Robust notification system**
Get instant notifications across email, Slack and Discord. Too much? Then create fully customisable alerts and summaries for each function and comms channel at a frequency that suits you. 40 | 41 | - **Full-featured 15-day trial**
Then $2/site/month with volume discounts available. No setup fees. No long-term contracts. 42 | 43 | You have just discovered our advanced monitoring tool for your Laravel applications and all the individual sites that you manage. We have designed it as a central dashboard to harmonise the maintenance of your entire website roster. Because we believe that coders should be out there coding. Let computers monitor computers, so that we humans don’t have to worry about it. 44 | 45 | We searched the galaxy for a robust answer to our challenges, and found none. So, our team embarked on our greatest mission yet and 1Pilot was born. 46 | 47 |

48 | Get your first site onboard in under 3 minutes! Start the 15-day full-feature trial 49 |

50 | 51 |

52 | Try it for free without any limitations for 15 days. No credit card required. 53 |

54 | 55 | ## Install 56 | 57 | ``` bash 58 | composer require 1pilotapp/laravel-client 59 | ``` 60 | 61 | You need to publish the config and set `private_key` to a random string 62 | ``` 63 | php artisan vendor:publish --provider="OnePilot\Client\ClientServiceProvider" 64 | ``` 65 | 66 | ## Change log 67 | 68 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 69 | 70 | ## Testing 71 | 72 | ``` bash 73 | composer test 74 | ``` 75 | 76 | ## Contributing 77 | 78 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 79 | 80 | ## Security 81 | 82 | If you discover any security related issues, please email support@1pilot.io instead of using the issue tracker. 83 | 84 | ## Credits 85 | 86 | - [1Pilot.io](https://github.com/1PilotApp) 87 | - [All Contributors](https://github.com/1PilotApp/laravel-client/contributors) 88 | 89 | ## License 90 | 91 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "1pilotapp/laravel-client", 3 | "description": "Client to connect a Laravel app to 1Pilot.io service", 4 | "keywords": [ 5 | "1Pilot", 6 | "laravel", 7 | "remote", 8 | "monitoring" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "1Pilot", 14 | "homepage": "https://1Pilot.io", 15 | "email": "support@1pilot.io" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.0|^8.0", 20 | "ext-json": "*", 21 | "composer/semver": "^1.4|^2.0|^3.0", 22 | "guzzlehttp/guzzle": "^6.3|^7.0", 23 | "guzzlehttp/promises": "^1.0|^2.0", 24 | "illuminate/http": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", 25 | "illuminate/routing": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", 26 | "illuminate/support": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0|^10.0", 30 | "orchestra/testbench": "~3.5.0|~3.6.0|~3.7.0|~3.8.0|^4.0|^5.0|^6.0|^7.0|^8.0|^9.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "OnePilot\\Client\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "OnePilot\\Client\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit" 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "OnePilot\\Client\\ClientServiceProvider" 49 | ] 50 | } 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true 54 | } 55 | -------------------------------------------------------------------------------- /config/onepilot.php: -------------------------------------------------------------------------------- 1 | env('ONEPILOT_PRIVATE_KEY'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Disable timestamp verification 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Only do that if your server is not at time and you can't fix that 23 | | 24 | | Set this option to TRUE will considerably reduce the security 25 | | 26 | */ 27 | 28 | 'skip_time_stamp_validation' => false, 29 | 30 | ]; 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Classes/Composer.php: -------------------------------------------------------------------------------- 1 | getPackages(); 34 | 35 | self::$packagesConstraints = $detector->getPackagesConstraints(); 36 | } 37 | 38 | /** 39 | * Get information for composer installed packages (currently installed version and latest stable version) 40 | * 41 | * @return array 42 | */ 43 | public function getPackagesData() 44 | { 45 | $packages = []; 46 | 47 | collect(self::$installedPackages) 48 | ->chunk(50) 49 | ->each(function (Collection $chunk) use (&$packages) { 50 | $promises = []; 51 | $client = new Client(['allow_redirects' => false]); 52 | 53 | $chunk 54 | ->filter(function ($package) { 55 | return !empty($package->version) && !empty($package->name); 56 | }) 57 | ->each(function ($package) use (&$packages, &$promises, $client) { 58 | $promises[$package->name] = $client 59 | ->getAsync($this->getPackagistDetailUrl($package->name)) 60 | ->then(function (Response $response) use (&$packages, $package) { 61 | if ($response->getStatusCode() === 200) { 62 | $this->storePackagistVersions($package->name, $response->getBody()); 63 | } 64 | 65 | $packages[] = $this->generatePackageData($package); 66 | }, function ($e) use (&$packages, $package) { 67 | // if fail re-try with file_get_contents (@see self::getVersionsFromPackagist) 68 | $packages[] = $this->generatePackageData($package); 69 | }); 70 | }); 71 | 72 | $this->waitForPromises($promises); 73 | }); 74 | 75 | return $packages; 76 | } 77 | 78 | private function generatePackageData($package) 79 | { 80 | $currentVersion = $this->removePrefix($package->version); 81 | $latestVersion = $this->getLatestPackageVersion($package->name, $currentVersion); 82 | 83 | return [ 84 | 'name' => Str::after($package->name, '/'), 85 | 'code' => $package->name, 86 | 'type' => 'package', 87 | 'active' => 1, 88 | 'version' => $currentVersion, 89 | 'new_version' => $latestVersion['compatible'] ?? null, 90 | 'last_available_version' => $latestVersion['available'] ?? null, 91 | ]; 92 | } 93 | 94 | /** 95 | * Get latest (stable) version number of composer package 96 | * 97 | * @param string $packageName The name of the package as registered on packagist, e.g. 'laravel/framework' 98 | * @param string $currentVersion If provided will ignore this version (if last one is $currentVersion will return null) 99 | * 100 | * @return array ['compatible' => $version, 'available' => $version] 101 | */ 102 | public function getLatestPackageVersion($packageName, $currentVersion = null) 103 | { 104 | $packages = $this->getLatestPackage($packageName); 105 | 106 | return collect($packages)->map(function ($package) use ($currentVersion) { 107 | $version = $this->removePrefix(optional($package)->version); 108 | 109 | return $version == $currentVersion ? null : $version; 110 | }); 111 | } 112 | 113 | /** 114 | * Get latest (stable) package from packagist 115 | * 116 | * @param string $packageName , the name of the package as registered on packagist, e.g. 'laravel/framework' 117 | * 118 | * @return array ['compatible' => (object) $version, 'available' => (object) $version] 119 | */ 120 | private function getLatestPackage($packageName) 121 | { 122 | if (empty($versions = $this->getVersionsFromPackagist($packageName))) { 123 | return null; 124 | } 125 | 126 | $lastCompatibleVersion = null; 127 | $lastAvailableVersion = null; 128 | 129 | $packageConstraints = self::$packagesConstraints->get($packageName); 130 | 131 | foreach ($versions as $versionData) { 132 | $versionNumber = $versionData->version; 133 | $normalizeVersionNumber = $versionData->version_normalized; 134 | $stability = VersionParser::normalizeStability(VersionParser::parseStability($versionNumber)); 135 | 136 | // only use stable version numbers 137 | if ($stability !== 'stable') { 138 | continue; 139 | } 140 | 141 | if (version_compare($normalizeVersionNumber, $lastAvailableVersion->version_normalized ?? '', '>=')) { 142 | $lastAvailableVersion = $versionData; 143 | } 144 | 145 | if (empty($packageConstraints)) { 146 | $lastCompatibleVersion = $lastAvailableVersion; 147 | continue; 148 | } 149 | 150 | // only use version that follow constraint 151 | if ( 152 | version_compare($normalizeVersionNumber, $lastCompatibleVersion->version_normalized ?? '', '>=') 153 | && $this->checkConstraints($normalizeVersionNumber, $packageConstraints) 154 | ) { 155 | $lastCompatibleVersion = $versionData; 156 | } 157 | } 158 | 159 | if ($lastCompatibleVersion === $lastAvailableVersion) { 160 | $lastAvailableVersion = null; 161 | } 162 | 163 | return [ 164 | 'compatible' => $lastCompatibleVersion, 165 | 'available' => $lastAvailableVersion, 166 | ]; 167 | } 168 | 169 | /** 170 | * @param string $version 171 | * 172 | * @param string $prefix 173 | * 174 | * @return string 175 | */ 176 | private function removePrefix($version, $prefix = 'v') 177 | { 178 | if (empty($version) || !Str::startsWith($version, $prefix)) { 179 | return $version; 180 | } 181 | 182 | return substr($version, strlen($prefix)); 183 | } 184 | 185 | private function checkConstraints($version, $constraints) 186 | { 187 | foreach ($constraints as $constraint) { 188 | if (Semver::satisfies($version, $constraint) !== true) { 189 | return false; 190 | } 191 | } 192 | 193 | return true; 194 | } 195 | 196 | private function getPackagistDetailUrl(string $packageName): string 197 | { 198 | return 'https://packagist.org/packages/' . $packageName . '.json'; 199 | } 200 | 201 | private function storePackagistVersions(string $package, string $response) 202 | { 203 | $packagistInfo = json_decode($response); 204 | 205 | $this->packagist[$package] = $packagistInfo->package->versions; 206 | } 207 | 208 | private function getVersionsFromPackagist(string $package) 209 | { 210 | if (empty($versions = Arr::get($this->packagist, $package))) { 211 | try { 212 | $packagistInfo = json_decode(file_get_contents($this->getPackagistDetailUrl($package))); 213 | $versions = $packagistInfo->package->versions; 214 | } catch (\Exception $e) { 215 | return null; 216 | } 217 | } 218 | 219 | unset($this->packagist[$package]); 220 | 221 | if (!is_object($versions)) { 222 | return null; 223 | } 224 | 225 | return $versions; 226 | } 227 | 228 | private function waitForPromises(array $promises) { 229 | 230 | if (method_exists(Utils::class, 'settle')){ 231 | Utils::settle($promises)->wait(); 232 | 233 | return; 234 | } 235 | 236 | // Support of guzzle/promises < 1.4 237 | \GuzzleHttp\Promise\settle($promises)->wait(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Classes/ComposerPackageDetector.php: -------------------------------------------------------------------------------- 1 | appRoot = $projectRoot; 16 | } 17 | 18 | public function getPackages(): Collection 19 | { 20 | $installedJsonFile = $this->appRoot . '/vendor/composer/installed.json'; 21 | $installedPackages = json_decode(file_get_contents($installedJsonFile)); 22 | 23 | if (!empty($installedPackages->packages)) { 24 | return collect($installedPackages->packages); // composer v2 25 | } 26 | 27 | return collect($installedPackages); 28 | } 29 | 30 | public function getPackagesConstraints(): Collection 31 | { 32 | $composers = $this->getPackages() 33 | ->push($this->appComposerData()) 34 | ->filter() 35 | ->map(function ($package) { 36 | return $package->require ?? null; 37 | }) 38 | ->filter(); 39 | 40 | $constraints = []; 41 | 42 | foreach ($composers as $packages) { 43 | foreach ($packages as $package => $constraint) { 44 | if (strpos($package, '/') === false) { 45 | continue; 46 | } 47 | 48 | if (!isset($constraints[$package])) { 49 | $constraints[$package] = []; 50 | } 51 | 52 | $constraints[$package][] = $constraint; 53 | } 54 | } 55 | 56 | return collect($constraints); 57 | } 58 | 59 | private function appComposerData() 60 | { 61 | if (!file_exists($appComposer = $this->appRoot . '/composer.json')) { 62 | return null; 63 | } 64 | 65 | $content = file_get_contents($appComposer); 66 | 67 | return json_decode($content) ?? null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Classes/FakePackageDetector.php: -------------------------------------------------------------------------------- 1 | filter() 50 | ->map(function ($package) { 51 | return $package->require ?? null; 52 | }) 53 | ->filter(); 54 | 55 | $constraints = []; 56 | 57 | foreach ($composers as $packages) { 58 | foreach ($packages as $package => $constraint) { 59 | if (strpos($package, '/') === false) { 60 | continue; 61 | } 62 | 63 | if (!isset($constraints[$package])) { 64 | $constraints[$package] = []; 65 | } 66 | 67 | $constraints[$package][] = $constraint; 68 | } 69 | } 70 | 71 | self::$packagesConstraints = collect($constraints); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Classes/Files.php: -------------------------------------------------------------------------------- 1 | getConfigFiles(); 30 | 31 | foreach ($files + $configFiles as $absolutePath => $relativePath) { 32 | 33 | if (is_int($absolutePath)) { 34 | $absolutePath = base_path($relativePath); 35 | } 36 | 37 | if (!file_exists($absolutePath) || !is_file($absolutePath)) { 38 | continue; 39 | } 40 | 41 | $fp = fopen($absolutePath, 'r'); 42 | $fstat = fstat($fp); 43 | fclose($fp); 44 | 45 | $filesProperties[] = [ 46 | 'path' => $relativePath, 47 | 'size' => $fstat['size'], 48 | 'mtime' => $fstat['mtime'], 49 | 'checksum' => md5_file($absolutePath), 50 | ]; 51 | } 52 | 53 | return $filesProperties; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | private function getConfigFiles() 60 | { 61 | /** @var SplFileInfo[] $iterator */ 62 | $iterator = new RecursiveIteratorIterator( 63 | new RecursiveDirectoryIterator(base_path('config')), 64 | RecursiveIteratorIterator::SELF_FIRST 65 | ); 66 | 67 | $files = []; 68 | $basePath = realpath(base_path()) . DIRECTORY_SEPARATOR; 69 | 70 | foreach ($iterator as $file) { 71 | if (!$file->isFile()) { 72 | continue; 73 | } 74 | 75 | $absolutePath = $file->getRealPath(); 76 | $relativePath = str_replace($basePath, '', $absolutePath); 77 | 78 | $files[$absolutePath] = $relativePath; 79 | } 80 | 81 | return $files; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Classes/LogsBrowser.php: -------------------------------------------------------------------------------- 1 | currentPage = $currentPage; 43 | $this->perPage = $perPage; 44 | $this->from = (($currentPage - 1) * $perPage) + 1; 45 | $this->to = (($currentPage - 1) * $perPage) + $perPage; 46 | } 47 | 48 | /** @return array */ 49 | public function getPagination() 50 | { 51 | return [ 52 | 'current_page' => $this->currentPage, 53 | 'per_page' => $this->perPage, 54 | 'from' => empty($this->logs) ? null : $this->from, 55 | 'to' => empty($this->logs) ? null : $this->from + count($this->logs) - 1, 56 | 'total' => $this->total, 57 | 'last_page' => (int)ceil($this->total / $this->perPage), 58 | ]; 59 | } 60 | 61 | /** @return array */ 62 | public function get() 63 | { 64 | foreach ($this->getLogsFiles() as $filePath) { 65 | $file = new SplFileObject($filePath, 'r'); 66 | 67 | $this->browseFile($file); 68 | } 69 | 70 | return $this->logs; 71 | } 72 | 73 | private function browseFile(SplFileObject $file) 74 | { 75 | $fileIndex = []; 76 | 77 | while (!$file->eof()) { 78 | $position = $file->ftell(); 79 | $line = $file->current(); 80 | 81 | if (isset($line[0]) && $line[0] == '[') { 82 | $fileIndex[] = $position; 83 | } 84 | 85 | $file->next(); 86 | } 87 | 88 | $fileIndex = array_reverse($fileIndex, true); 89 | 90 | foreach ($fileIndex as $row => $position) { 91 | $file->fseek($position); 92 | $line = $file->current(); 93 | 94 | if (!isset($line[0]) || $line[0] != '[') { 95 | continue; 96 | } 97 | 98 | if (!preg_match(self::LOG_PATTERN, $line, $matches)) { 99 | continue; 100 | } 101 | 102 | if (empty($date = $matches['date']) || empty($level = $matches['level']) || empty($message = $matches['message'])) { 103 | continue; 104 | } 105 | 106 | if (!$this->matchFilters($date, $level, $message)) { 107 | continue; 108 | } 109 | 110 | $this->total++; 111 | 112 | if ($this->total < $this->from || $this->total > $this->to) { 113 | continue; 114 | } 115 | 116 | $stackTrace = ""; 117 | $stackTraceRow = 1; 118 | 119 | if (!$file->eof()) { 120 | $file->next(); 121 | 122 | while (!$file->eof()) { 123 | $stackLine = $file->fgets(); 124 | 125 | if ($stackTraceRow >= 80) { 126 | break; 127 | } 128 | 129 | if (preg_match(self::LOG_PATTERN, $stackLine)) { 130 | break; 131 | } 132 | 133 | $stackTrace .= $stackLine; 134 | $stackTraceRow++; 135 | } 136 | } 137 | 138 | $this->logs[] = [ 139 | 'date' => $date, 140 | 'level' => $level, 141 | 'channel' => $matches['channel'] ?: null, 142 | 'message' => $stackTrace ? $message . PHP_EOL . $stackTrace : $message, 143 | ]; 144 | } 145 | } 146 | 147 | public function setRange(string $from = null, string $to = null) 148 | { 149 | $this->dateFrom = $from ? Carbon::parse($from)->format('Y-m-d H\:i\:s') : null; 150 | $this->dateTo = $to ? Carbon::parse($to)->format('Y-m-d H\:i\:s') : null; 151 | } 152 | 153 | public function setSearch(string $search = null) 154 | { 155 | $this->searchString = $search; 156 | } 157 | 158 | public function setLevels(array $levels = null) 159 | { 160 | if (empty($levels)) { 161 | return; 162 | } 163 | 164 | array_walk($levels, function (&$value) { 165 | $value = strtolower($value); 166 | }); 167 | 168 | $this->levelsFilter = $levels; 169 | } 170 | 171 | /** 172 | * @param string $date 173 | * @param string $level 174 | * @param string $message 175 | * 176 | * @return bool 177 | */ 178 | private function matchFilters($date, $level, $message) 179 | { 180 | if (!empty($this->dateFrom) && $date < $this->dateFrom) { 181 | return false; 182 | } 183 | 184 | if (!empty($this->dateTo) && $date > $this->dateTo) { 185 | return false; 186 | } 187 | 188 | if (!empty($this->levelsFilter) && !in_array(strtolower($level), $this->levelsFilter)) { 189 | return false; 190 | } 191 | 192 | if (!empty($this->searchString) && stripos($message, $this->searchString) === false) { 193 | return false; 194 | } 195 | 196 | return true; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Classes/LogsFiles.php: -------------------------------------------------------------------------------- 1 | \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})(?>[\+-]\d{4})?\]\s{1}(?>(?[^\s:\-]+)\.)?(?\w+)(?>\:)?\s(?.*)#'; 16 | 17 | const LEVELS = [ 18 | 'emergency', 19 | 'alert', 20 | 'critical', 21 | 'error', 22 | 'warning', 23 | 'notice', 24 | 'info', 25 | 'debug', 26 | ]; 27 | 28 | /** @var array intervals in minutes */ 29 | const LOGS_OVERVIEW_INTERVALS = [ 30 | 30 * 24 * 60, 31 | 7 * 24 * 60, 32 | 1 * 24 * 60, 33 | ]; 34 | 35 | private $startedAt; 36 | private $lastStepStartedAt; 37 | 38 | public function __construct() 39 | { 40 | $this->startedAt = microtime(true); 41 | $this->lastStepStartedAt = microtime(true); 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getLogsFiles() 48 | { 49 | $files = glob(storage_path('logs/*.log')); 50 | 51 | return array_reverse($files); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Classes/LogsOverview.php: -------------------------------------------------------------------------------- 1 | intervalDates[$interval] = Carbon::now()->subMinutes($interval)->format('Y-m-d H\:i\:s'); 18 | } 19 | 20 | foreach ($this->getLogsFiles() as $filePath) { 21 | $file = new SplFileObject($filePath, 'r'); 22 | 23 | $this->logsOverviewOfFile($file); 24 | } 25 | 26 | $overview = []; 27 | 28 | foreach ($this->overview as $interval => $levels) { 29 | $overview[$interval] = []; 30 | 31 | foreach ($levels as $level => $total) { 32 | $overview[$interval][] = [ 33 | 'level' => $level, 34 | 'total' => $total, 35 | ]; 36 | } 37 | } 38 | 39 | return $overview; 40 | } 41 | 42 | private function logsOverviewOfFile(SplFileObject $file) 43 | { 44 | while (!$file->eof()) { 45 | $line = $file->fgets(); 46 | 47 | if (!isset($line[0]) || $line[0] != '[') { 48 | continue; 49 | } 50 | 51 | if (!preg_match(self::LOG_PATTERN, $line, $matches)) { 52 | continue; 53 | } 54 | 55 | if (empty($matches['date']) || empty($matches['level'])) { 56 | continue; 57 | } 58 | 59 | foreach ($this->intervalDates as $interval => $date) { 60 | if ($matches['date'] >= $date) { 61 | $this->incrementLogsOverview($interval, $matches['level']); 62 | } 63 | } 64 | } 65 | } 66 | 67 | private function incrementLogsOverview($interval, $level) 68 | { 69 | $level = strtolower($level); 70 | 71 | if (!in_array($level, self::LEVELS)) { 72 | return; 73 | } 74 | 75 | if (!isset($this->overview[$interval][$level])) { 76 | $this->overview[$interval][$level] = 0; 77 | } 78 | 79 | $this->overview[$interval][$level]++; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ClientServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__ . '/routes.php'); 19 | 20 | $this->publishes([ 21 | __DIR__ . '/../config/onepilot.php' => config_path('onepilot.php'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Register any package services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->app->bind(PackageDetector::class, function($app) { 33 | return new ComposerPackageDetector(base_path()); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Contracts/PackageDetector.php: -------------------------------------------------------------------------------- 1 | middleware(Authentication::class); 15 | } 16 | 17 | /** 18 | * @param Request $request 19 | * 20 | * @return array 21 | */ 22 | public function browse(Request $request) 23 | { 24 | $browser = new LogsBrowser; 25 | $browser->setPagination($request->get('page', 1), $request->get('per_page', 50)); 26 | $browser->setRange($request->get('from'), $request->get('to')); 27 | $browser->setSearch($request->get('search')); 28 | $browser->setLevels($request->get('levels')); 29 | 30 | return ['data' => $browser->get(), 'base_path' => base_path()] + $browser->getPagination(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Controllers/MailTesterController.php: -------------------------------------------------------------------------------- 1 | middleware(Authentication::class); 17 | } 18 | 19 | /** 20 | * @throws OnePilotException 21 | */ 22 | public function send(Request $request) 23 | { 24 | if (empty($email = $request->email)) { 25 | throw new OnePilotException('Email parameter is missing', 400); 26 | } 27 | 28 | try { 29 | $this->sendEmail($email); 30 | } catch (Exception $e) { 31 | throw new OnePilotException('Error when sending email', 500, $e); 32 | } 33 | 34 | return [ 35 | 'message' => 'Sent', 36 | ]; 37 | } 38 | 39 | /** 40 | * @param $email 41 | */ 42 | protected function sendEmail($email) 43 | { 44 | Mail::send([], [], function (\Illuminate\Mail\Message $message) use ($email) { 45 | $message 46 | ->to($email) 47 | ->subject('Test mail from 1Pilot.io to ensure emails are properly sent'); 48 | 49 | // Laravel < 9.x SwiftMailer 50 | if (method_exists($message, 'getSwiftMessage')) { 51 | $message->setBody($this->getBody()); 52 | 53 | return; 54 | } 55 | 56 | // Laravel >= 9.x Symfony Mailer 57 | $message->text($this->getBody()); 58 | $message->html($this->getBodyHtml()); 59 | }); 60 | } 61 | 62 | protected function getBody() 63 | { 64 | $siteUrl = config('app.url'); 65 | 66 | return <<This email was automatically sent by the 1Pilot Client installed on $siteUrl.

129 | 130 |

Ground control to Major Tom
131 | Ground control to Major Tom
132 | Take your protein pills and put your helmet on

133 | 134 |

Ground control to Major Tom
135 | (10, 9, 8, 7)
136 | Commencing countdown, engines on
137 | (6, 5, 4, 3)
138 | Check ignition, and may God's love be with you
139 | (2, 1, liftoff)

140 | 141 |

This is ground control to Major Tom,

142 | 143 |

You've really made the grade
144 | And the papers want to know whose shirts you wear
145 | Now it's time to leave the capsule if you dare

146 | 147 |

This is Major Tom to ground control
148 | I'm stepping through the door
149 | And I'm floating in the most of peculiar way
150 | And the stars look very different today

151 | 152 |

For here am I sitting in a tin can
153 | Far above the world
154 | Planet Earth is blue, and there's nothing I can do

155 | 156 |

Though I'm past 100,000 miles
157 | I'm feeling very still
158 | And I think my spaceship knows which way to go
159 | Tell my wife I love her very much, she knows

160 | 161 |

Ground control to Major Tom,
162 | Your circuit's dead, there's something wrong
163 | Can you hear me Major Tom?
164 | Can you hear me Major Tom?
165 | Can you hear me Major Tom?
166 | Can you...

167 | 168 |

Here am I floating round my tin can
169 | Far above the moon
170 | Planet Earth is blue, and there's nothing I can do...

171 | 172 |

Ground control to Major Tom,
173 | Your circuit's dead, there's something wrong
174 | Can you hear me Major Tom?
175 | Can you hear me Major Tom?
176 | Can you hear me Major Tom?
177 | Can you...

178 | 179 |

Space Oddity
180 | David Bowie

181 | EOF; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Controllers/PingController.php: -------------------------------------------------------------------------------- 1 | middleware(Authentication::class); 16 | } 17 | 18 | /** 19 | * Retrieve runtime and composer package version information 20 | */ 21 | public function index() 22 | { 23 | return [ 24 | 'message' => "pong", 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controllers/VersionController.php: -------------------------------------------------------------------------------- 1 | middleware(Authentication::class); 17 | } 18 | 19 | /** 20 | * Retrieve runtime and composer package version information 21 | */ 22 | public function index() 23 | { 24 | return [ 25 | 'core' => $this->getVersions(), 26 | 'servers' => $this->getServers(), 27 | 'plugins' => Composer::instance()->getPackagesData(), 28 | 'extra' => $this->getExtra(), 29 | 'files' => Files::instance()->getFilesProperties(), 30 | 'errors' => $this->errorsOverview(), 31 | ]; 32 | } 33 | 34 | public function getVersions() 35 | { 36 | $laravel = Composer::instance()->getLatestPackageVersion('laravel/framework', app()->version()); 37 | 38 | return [ 39 | 'version' => app()->version(), 40 | 'new_version' => $laravel['compatible'] ?? null, 41 | 'last_available_version' => $laravel['available'] ?? null, 42 | ]; 43 | } 44 | 45 | /** 46 | * Get system versions 47 | * 48 | * @return array 49 | */ 50 | public function getServers() 51 | { 52 | $serverWeb = $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE') ?? null; 53 | 54 | return [ 55 | 'php' => phpversion(), 56 | 'web' => $serverWeb, 57 | 'mysql' => $this->getDbVersion(), 58 | ]; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | private function getExtra() 65 | { 66 | $configs = $this->config('app.debug', 'app.env', 'app.timezone'); 67 | 68 | if (version_compare(app()->version(), '6', '<=')) { 69 | $configs += $this->config('mail.driver', 'mail.host'); 70 | } else { 71 | $configs += $this->config('mail.default'); 72 | 73 | if (config('mail.default') == 'smtp') { 74 | $configs += $this->config('mail.mailers.smtp.host'); 75 | } 76 | } 77 | 78 | $configs += $this->config( 79 | 'mail.from.address', 80 | 'mail.from.name', 81 | 'queue.default', 82 | 'cache.default', 83 | 'logging.default', 84 | 'session.driver', 85 | 'session.lifetime' 86 | ); 87 | 88 | $configs['storage_dir_writable'] = is_writable(base_path('storage')); 89 | $configs['cache_dir_writable'] = is_writable(base_path('bootstrap/cache')); 90 | 91 | return $configs; 92 | } 93 | 94 | private function errorsOverview() 95 | { 96 | try { 97 | return (new LogsOverview())->get(); 98 | } catch (\Exception $e) { 99 | } 100 | } 101 | 102 | /** 103 | * @return string|null 104 | */ 105 | private function getDbVersion() 106 | { 107 | $connection = config('database.default'); 108 | $driver = config("database.connections.{$connection}.driver"); 109 | 110 | try { 111 | switch ($driver) { 112 | case 'mysql': 113 | return $this->mysqlVersion(); 114 | case 'sqlite': 115 | return $this->sqliteVersion(); 116 | } 117 | } catch (\Exception $e) { 118 | // nothing 119 | } 120 | } 121 | 122 | 123 | /** 124 | * @return string|null 125 | */ 126 | private function mysqlVersion() 127 | { 128 | $connection = config('database.default'); 129 | 130 | if ( 131 | in_array(config("database.connections.{$connection}.database"), ['homestead', 'forge', '']) && 132 | in_array(config("database.connections.{$connection}.username"), ['homestead', 'forge', '']) && 133 | in_array(config("database.connections.{$connection}.password"), ['secret', '']) 134 | ) { 135 | // default config value, connection will not work 136 | return null; 137 | } 138 | 139 | $result = DB::select('SELECT VERSION() as version;'); 140 | 141 | return $result[0]->version ?? null; 142 | } 143 | 144 | /** 145 | * @return string|null 146 | */ 147 | private function sqliteVersion() 148 | { 149 | $result = DB::select('select "SQLite " || sqlite_version() as version'); 150 | 151 | return $result[0]->version ?? null; 152 | } 153 | 154 | /** 155 | * @param ...$keys 156 | * 157 | * @return array 158 | */ 159 | private function config(...$keys) 160 | { 161 | return collect($keys)->mapWithKeys(function ($item) { 162 | return [$item => config($item)]; 163 | })->toArray(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Exceptions/OnePilotException.php: -------------------------------------------------------------------------------- 1 | code >= 400 && $this->code < 600) ? $this->code : 500; 40 | 41 | $content = [ 42 | 'message' => $this->getMessage(), 43 | 'status' => $httpCode, 44 | 'data' => [], 45 | ]; 46 | 47 | if (!empty($previous = $this->getPrevious())) { 48 | $content['data']['previous'] = [ 49 | 'message' => $previous->getMessage(), 50 | ]; 51 | } 52 | 53 | return response($content, $httpCode); 54 | } 55 | 56 | public function report() 57 | { 58 | // disable reporting 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Middlewares/Authentication.php: -------------------------------------------------------------------------------- 1 | header('hash'); 13 | $stamp = $request->header('stamp'); 14 | 15 | if (!$signature) { 16 | throw OnePilotException::missingSignature(); 17 | } 18 | 19 | if (!$this->isValidateTimeStamp($stamp)) { 20 | throw OnePilotException::invalidTimestamp(); 21 | } 22 | 23 | if (!$this->isValidSignature($signature, $stamp)) { 24 | throw OnePilotException::invalidSignature($signature); 25 | } 26 | 27 | return $next($request); 28 | } 29 | 30 | protected function isValidSignature(string $signature, string $stamp) 31 | { 32 | $secret = config('onepilot.private_key'); 33 | 34 | if (empty($secret)) { 35 | throw OnePilotException::signingPrivateKeyNotSet(); 36 | } 37 | 38 | $computedSignature = hash_hmac('sha256', $stamp, $secret); 39 | 40 | return hash_equals($signature, $computedSignature); 41 | } 42 | 43 | /** 44 | * Validate timestamp. The meaning of this check is to enhance security by 45 | * making sure any token can only be used in a short period of time. 46 | * 47 | * @return boolean 48 | */ 49 | private function isValidateTimeStamp($stamp) 50 | { 51 | if ($secret = config('onepilot.skip_time_stamp_validation')) { 52 | return true; 53 | } 54 | 55 | if (($stamp > time() - 360) && ($stamp < time() + 360)) { 56 | return true; 57 | } 58 | 59 | return false; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Traits/Instantiable.php: -------------------------------------------------------------------------------- 1 | getJson('onepilot/validate'); 13 | 14 | $response 15 | ->assertStatus(400) 16 | ->assertJson([ 17 | 'message' => "The request did not contain a header named `HTTP_HASH`.", 18 | 'status' => 400, 19 | 'data' => [], 20 | ]); 21 | } 22 | 23 | /** @test */ 24 | public function it_will_fail_when_no_authentication_headers_are_set() 25 | { 26 | $response = $this->getJson('onepilot/ping', []); 27 | 28 | $response 29 | ->assertStatus(400) 30 | ->assertJson([ 31 | 'message' => "The request did not contain a header named `HTTP_HASH`.", 32 | 'status' => 400, 33 | 'data' => [], 34 | ]); 35 | } 36 | 37 | /** @test */ 38 | public function it_will_fail_when_using_past_stamp() 39 | { 40 | $this->setTimestamp(1500000000); 41 | 42 | $response = $this->getJson('onepilot/ping', $this->authenticationHeaders()); 43 | 44 | $response 45 | ->assertStatus(400) 46 | ->assertJson([ 47 | 'message' => "The timestamp found in the header is invalid", 48 | 'status' => 400, 49 | 'data' => [], 50 | ]); 51 | } 52 | 53 | /** @test */ 54 | public function it_will_fail_when_using_empty_stamp() 55 | { 56 | $this->setTimestamp(""); 57 | 58 | $response = $this->getJson('onepilot/ping', $this->authenticationHeaders()); 59 | 60 | $response 61 | ->assertStatus(400) 62 | ->assertJson([ 63 | 'message' => "The timestamp found in the header is invalid", 64 | 'status' => 400, 65 | 'data' => [], 66 | ]); 67 | } 68 | 69 | /** @test */ 70 | public function it_will_work_when_using_past_stamp_with_skip_time_stamp_validation_enabled() 71 | { 72 | $this->setTimestamp(1500000000); 73 | 74 | config(['onepilot.skip_time_stamp_validation' => true]); 75 | 76 | $response = $this->getJson('onepilot/ping', $this->authenticationHeaders()); 77 | 78 | $response 79 | ->assertStatus(200) 80 | ->assertJson([ 81 | 'message' => "pong", 82 | ]); 83 | } 84 | 85 | /** @test */ 86 | public function it_will_work_when_using_valid_stamp_and_hash() 87 | { 88 | $response = $this->getJson('onepilot/ping', $this->authenticationHeaders()); 89 | 90 | $response 91 | ->assertStatus(200) 92 | ->assertJson([ 93 | 'message' => "pong", 94 | ]); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /tests/Integration/ComposerUpdatesTest.php: -------------------------------------------------------------------------------- 1 | getPackagesData()); 19 | 20 | $laravelFramework = $this->findPackage($packages, 'laravel/framework'); 21 | 22 | $this->assertVersionGreaterThan('5.5.40', $laravelFramework['new_version']); 23 | $this->assertVersionLessThan('5.6.0', $laravelFramework['new_version']); 24 | $this->assertVersionGreaterThan('5.7.0', $laravelFramework['last_available_version']); 25 | 26 | $symfonyConsole = $this->findPackage($packages, 'symfony/console'); 27 | 28 | $this->assertVersionGreaterThan('3.4.1', $symfonyConsole['new_version']); 29 | $this->assertVersionLessThan('4.0.0', $symfonyConsole['new_version']); 30 | $this->assertVersionGreaterThan('4.1.0', $symfonyConsole['last_available_version']); 31 | } 32 | 33 | /** @test */ 34 | public function laravel_56() 35 | { 36 | FakePackageDetector::setPackagesFromPath(__DIR__ . '/../data/composer/laravel56-installed-packages.json'); 37 | FakePackageDetector::generatePackagesConstraints(); 38 | 39 | $packages = collect((new Composer)->getPackagesData()); 40 | 41 | $laravelFramework = $this->findPackage($packages, 'laravel/framework'); 42 | 43 | $this->assertVersionGreaterThan('5.6.35', $laravelFramework['new_version']); 44 | $this->assertVersionLessThan('5.7.0', $laravelFramework['new_version']); 45 | $this->assertVersionGreaterThan('5.7.0', $laravelFramework['last_available_version']); 46 | 47 | $symfonyConsole = $this->findPackage($packages, 'symfony/console'); 48 | 49 | $this->assertVersionGreaterThan('4.0.0', $symfonyConsole['new_version']); 50 | $this->assertVersionLessThan('5.0.0', $symfonyConsole['new_version']); 51 | 52 | $carbon = $this->findPackage($packages, 'nesbot/carbon'); 53 | 54 | $this->assertEquals('1.25.0', $carbon['version']); 55 | $this->assertVersionLessThan('1.26.0', $carbon['new_version'] ?? 0); 56 | $this->assertVersionGreaterThan('2.0.0', $carbon['last_available_version']); 57 | } 58 | 59 | /** @test */ 60 | public function laravel_57() 61 | { 62 | FakePackageDetector::setPackagesFromPath(__DIR__ . '/../data/composer/laravel57-installed-packages.json'); 63 | FakePackageDetector::generatePackagesConstraints(); 64 | 65 | $packages = collect((new Composer)->getPackagesData()); 66 | 67 | $laravelFramework = $this->findPackage($packages, 'laravel/framework'); 68 | 69 | $this->assertVersionGreaterThan('5.7.0', $laravelFramework['new_version']); 70 | $this->assertVersionLessThan('5.8.0', $laravelFramework['new_version']); 71 | 72 | $symfonyConsole = $this->findPackage($packages, 'symfony/console'); 73 | 74 | $this->assertVersionGreaterThan('4.1.0', $symfonyConsole['new_version']); 75 | $this->assertVersionLessThan('5.0.0', $symfonyConsole['new_version']); 76 | 77 | $carbon = $this->findPackage($packages, 'nesbot/carbon'); 78 | 79 | $this->assertVersionGreaterThan('1.26.3', $carbon['new_version']); 80 | $this->assertVersionLessThan('2.0.0', $carbon['new_version']); 81 | $this->assertVersionGreaterThan('2.0.0', $carbon['last_available_version']); 82 | } 83 | 84 | /** @test */ 85 | public function packages_not_publicly_available_on_packagist_are_also_returned() 86 | { 87 | FakePackageDetector::setPackagesFromArray([ 88 | [ 89 | 'name' => 'composer/semver', 90 | 'version' => '1.4.0', 91 | 'require' => ['php' => '>=7.0.0'], 92 | ], 93 | [ 94 | 'name' => 'laravel/socialite', 95 | 'version' => '3.2.0', 96 | ], 97 | [ 98 | 'name' => 'laravel/nova', 99 | 'version' => '1.0.1', 100 | ], 101 | [ 102 | 'name' => '1pilotapp/unknown', 103 | 'version' => '1.0.1', 104 | 'require' => ['laravel/socialite' => '^3.2'], 105 | ], 106 | ]); 107 | 108 | FakePackageDetector::generatePackagesConstraints(); 109 | 110 | $packages = collect((new Composer)->getPackagesData()); 111 | 112 | $this->assertCount(4, $packages); 113 | 114 | $composerSemver = $this->findPackage($packages, 'composer/semver'); 115 | $this->assertVersionGreaterThanOrEqual('1.5.0', $composerSemver['new_version']); 116 | $this->assertNull($composerSemver['last_available_version']); 117 | 118 | $laravelSocialite = $this->findPackage($packages, 'laravel/socialite'); 119 | $this->assertVersionGreaterThan('3.2.9', $laravelSocialite['new_version']); 120 | $this->assertVersionLessThan('4.0.0', $laravelSocialite['new_version']); 121 | $this->assertVersionGreaterThan('4.1.0', $laravelSocialite['last_available_version']); 122 | 123 | $laravelNova = $this->findPackage($packages, 'laravel/nova'); 124 | $this->assertEquals('1.0.1', $laravelNova['version']); 125 | $this->assertNull($laravelNova['new_version']); 126 | $this->assertNull($laravelNova['last_available_version']); 127 | } 128 | 129 | private function findPackage(Collection $packages, string $name) 130 | { 131 | return $packages->first(function ($package) use ($name) { 132 | return $package['code'] === $name; 133 | }); 134 | } 135 | 136 | private function assertVersionGreaterThan($expected, $actual) 137 | { 138 | $this->assertVersionCompare($expected, $actual, '>'); 139 | } 140 | 141 | private function assertVersionGreaterThanOrEqual($expected, $actual) 142 | { 143 | $this->assertVersionCompare($expected, $actual, '>='); 144 | } 145 | 146 | private function assertVersionLessThan($expected, $actual) 147 | { 148 | $this->assertVersionCompare($expected, $actual, '<'); 149 | } 150 | 151 | private function assertVersionCompare($expected, $actual, $operator) 152 | { 153 | $this->assertTrue( 154 | version_compare($actual, $expected, $operator), 155 | "Failed asserting that '{$actual}' is $operator than '$expected'" 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /tests/Integration/MailTesterTest.php: -------------------------------------------------------------------------------- 1 | postJson('onepilot/mail-tester', [ 21 | 'email' => 'test-mail@example.com', 22 | ], $this->authenticationHeaders()); 23 | } 24 | } 25 | 26 | /** @test */ 27 | public function require_authentication_headers() 28 | { 29 | $response = $this->postJson('onepilot/mail-tester'); 30 | 31 | $response->assertStatus(400); 32 | } 33 | 34 | /** @test */ 35 | public function response_is_success() 36 | { 37 | self::$response->assertStatus(200); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Integration/ValidationsTest.php: -------------------------------------------------------------------------------- 1 | getJson('onepilot/validate', $this->authenticationHeaders()); 21 | } 22 | } 23 | 24 | /** @test */ 25 | public function response_is_success() 26 | { 27 | self::$response->assertStatus(200); 28 | } 29 | 30 | /** @test */ 31 | public function core_and_php_versions_are_right() 32 | { 33 | $data = self::$response->getOriginalContent(); 34 | 35 | $this->assertEquals(app()->version(), Arr::get($data, 'core.version')); 36 | 37 | $this->assertEquals(phpversion(), Arr::get($data, 'servers.php')); 38 | } 39 | 40 | /** @test */ 41 | public function all_real_packages_are_returned() 42 | { 43 | // ignore the project package (if it not have a version number) 44 | $packages = (new FakePackageDetector)->getPackages() 45 | ->filter(function ($package) { 46 | return !empty($package->version); 47 | }) 48 | ->count(); 49 | 50 | $data = self::$response->getOriginalContent(); 51 | $this->assertEquals($packages, count($data['plugins'])); 52 | } 53 | 54 | /** @test */ 55 | public function extra_parameters() 56 | { 57 | $data = self::$response->getOriginalContent(); 58 | 59 | $this->assertEquals(App::environment(), $data['extra']['app.env'] ?? null); 60 | 61 | $this->assertEquals(config('app.debug'), $data['extra']['app.debug'] ?? null); 62 | } 63 | 64 | /** @test */ 65 | public function files_contains_config_app() 66 | { 67 | $files = collect(self::$response->getOriginalContent()['files'] ?? []); 68 | 69 | $configApp = $files->first(function ($file) { 70 | return $file['path'] == 'config'. DIRECTORY_SEPARATOR .'app.php'; 71 | }); 72 | 73 | $this->assertNotNull($configApp['checksum']); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | $this->privateKey]); 21 | 22 | $this->setTimestamp(); 23 | 24 | $this->app->bind(PackageDetector::class, FakePackageDetector::class); 25 | 26 | FakePackageDetector::setPackagesFromPath(__DIR__ . '/data/composer/installed-packages-light.json'); 27 | FakePackageDetector::generatePackagesConstraints(); 28 | } 29 | 30 | /** 31 | * @param \Illuminate\Foundation\Application $app 32 | * 33 | * @return array 34 | */ 35 | protected function getPackageProviders($app) 36 | { 37 | return [ 38 | ClientServiceProvider::class, 39 | ]; 40 | } 41 | 42 | /** 43 | * Set timestamp and regenerate hash 44 | * 45 | * @param null $timestamp 46 | */ 47 | protected function setTimestamp($timestamp = null) 48 | { 49 | $this->timestamp = $timestamp ?? time(); 50 | 51 | $this->hash = $this->generateAuthenticationHash($this->privateKey, $this->timestamp); 52 | } 53 | 54 | /** 55 | * @param string $privateKey 56 | * @param int $timestamp 57 | * 58 | * @return string Hash 59 | */ 60 | protected function generateAuthenticationHash($privateKey, $timestamp) 61 | { 62 | return hash_hmac('sha256', $timestamp, $privateKey); 63 | } 64 | 65 | protected function authenticationHeaders() 66 | { 67 | return [ 68 | 'hash' => $this->hash, 69 | 'stamp' => $this->timestamp, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/data/composer/installed-packages-light.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "require": { 4 | "php": ">=7.0.0", 5 | "fideloper/proxy": "~3.3", 6 | "laravel/framework": "5.5.*", 7 | "laravel/tinker": "~1.0" 8 | } 9 | }, 10 | { 11 | "name": "laravel/framework", 12 | "version": "v5.5.14", 13 | "version_normalized": "5.5.14.0", 14 | "require": { 15 | "doctrine/inflector": "~1.1", 16 | "erusev/parsedown": "~1.6", 17 | "ext-mbstring": "*", 18 | "ext-openssl": "*", 19 | "league/flysystem": "~1.0", 20 | "monolog/monolog": "~1.12", 21 | "mtdowling/cron-expression": "~1.0", 22 | "nesbot/carbon": "~1.20", 23 | "php": ">=7.0", 24 | "psr/container": "~1.0", 25 | "psr/simple-cache": "^1.0", 26 | "ramsey/uuid": "~3.0", 27 | "swiftmailer/swiftmailer": "~6.0", 28 | "symfony/console": "~3.3", 29 | "symfony/debug": "~3.3", 30 | "symfony/finder": "~3.3", 31 | "symfony/http-foundation": "~3.3", 32 | "symfony/http-kernel": "~3.3", 33 | "symfony/process": "~3.3", 34 | "symfony/routing": "~3.3", 35 | "symfony/var-dumper": "~3.3", 36 | "tijsverkoyen/css-to-inline-styles": "~2.2", 37 | "vlucas/phpdotenv": "~2.2" 38 | } 39 | }, 40 | { 41 | "name": "symfony/http-kernel", 42 | "version": "v3.3.10", 43 | "version_normalized": "3.3.10.0", 44 | "require": { 45 | "php": "^5.5.9|>=7.0.8", 46 | "psr/log": "~1.0", 47 | "symfony/debug": "~2.8|~3.0", 48 | "symfony/event-dispatcher": "~2.8|~3.0", 49 | "symfony/http-foundation": "~3.3" 50 | } 51 | }, 52 | { 53 | "name": "erusev/parsedown", 54 | "version": "1.6.0", 55 | "version_normalized": "1.6.0.0" 56 | } 57 | ] -------------------------------------------------------------------------------- /tests/data/composer/laravel55-installed-packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "laravel/laravel", 4 | "require": { 5 | "php": ">=7.0.0", 6 | "fideloper/proxy": "~3.3", 7 | "laravel/framework": "5.5.*", 8 | "laravel/tinker": "~1.0" 9 | } 10 | }, 11 | { 12 | "name": "composer/semver", 13 | "version": "1.4.2", 14 | "version_normalized": "1.4.2.0", 15 | "require": { 16 | "php": "^5.3.2 || ^7.0" 17 | } 18 | }, 19 | { 20 | "name": "dnoegel/php-xdg-base-dir", 21 | "version": "0.1", 22 | "version_normalized": "0.1.0.0", 23 | "require": { 24 | "php": ">=5.3.2" 25 | } 26 | }, 27 | { 28 | "name": "doctrine/inflector", 29 | "version": "v1.2.0", 30 | "version_normalized": "1.2.0.0", 31 | "require": { 32 | "php": "^7.0" 33 | } 34 | }, 35 | { 36 | "name": "doctrine/instantiator", 37 | "version": "1.0.5", 38 | "version_normalized": "1.0.5.0", 39 | "require": { 40 | "php": ">=5.3,<8.0-DEV" 41 | } 42 | }, 43 | { 44 | "name": "doctrine/lexer", 45 | "version": "v1.0.1", 46 | "version_normalized": "1.0.1.0", 47 | "require": { 48 | "php": ">=5.3.2" 49 | } 50 | }, 51 | { 52 | "name": "egulias/email-validator", 53 | "version": "2.1.2", 54 | "version_normalized": "2.1.2.0", 55 | "require": { 56 | "doctrine/lexer": "^1.0.1", 57 | "php": ">= 5.5" 58 | } 59 | }, 60 | { 61 | "name": "erusev/parsedown", 62 | "version": "1.6.3", 63 | "version_normalized": "1.6.3.0", 64 | "require": { 65 | "php": ">=5.3.0" 66 | } 67 | }, 68 | { 69 | "name": "fideloper/proxy", 70 | "version": "3.3.4", 71 | "version_normalized": "3.3.4.0", 72 | "require": { 73 | "illuminate/contracts": "~5.0", 74 | "php": ">=5.4.0" 75 | } 76 | }, 77 | { 78 | "name": "filp/whoops", 79 | "version": "2.1.10", 80 | "version_normalized": "2.1.10.0", 81 | "require": { 82 | "php": "^5.5.9 || ^7.0", 83 | "psr/log": "^1.0.1" 84 | } 85 | }, 86 | { 87 | "name": "fzaninotto/faker", 88 | "version": "v1.7.1", 89 | "version_normalized": "1.7.1.0", 90 | "require": { 91 | "php": "^5.3.3 || ^7.0" 92 | } 93 | }, 94 | { 95 | "name": "hamcrest/hamcrest-php", 96 | "version": "v1.2.2", 97 | "version_normalized": "1.2.2.0", 98 | "require": { 99 | "php": ">=5.3.2" 100 | } 101 | }, 102 | { 103 | "name": "jakub-onderka/php-console-color", 104 | "version": "0.1", 105 | "version_normalized": "0.1.0.0", 106 | "require": { 107 | "php": ">=5.3.2" 108 | } 109 | }, 110 | { 111 | "name": "jakub-onderka/php-console-highlighter", 112 | "version": "v0.3.2", 113 | "version_normalized": "0.3.2.0", 114 | "require": { 115 | "jakub-onderka/php-console-color": "~0.1", 116 | "php": ">=5.3.0" 117 | } 118 | }, 119 | { 120 | "name": "laravel/framework", 121 | "version": "v5.5.14", 122 | "version_normalized": "5.5.14.0", 123 | "require": { 124 | "doctrine/inflector": "~1.1", 125 | "erusev/parsedown": "~1.6", 126 | "ext-mbstring": "*", 127 | "ext-openssl": "*", 128 | "league/flysystem": "~1.0", 129 | "monolog/monolog": "~1.12", 130 | "mtdowling/cron-expression": "~1.0", 131 | "nesbot/carbon": "~1.20", 132 | "php": ">=7.0", 133 | "psr/container": "~1.0", 134 | "psr/simple-cache": "^1.0", 135 | "ramsey/uuid": "~3.0", 136 | "swiftmailer/swiftmailer": "~6.0", 137 | "symfony/console": "~3.3", 138 | "symfony/debug": "~3.3", 139 | "symfony/finder": "~3.3", 140 | "symfony/http-foundation": "~3.3", 141 | "symfony/http-kernel": "~3.3", 142 | "symfony/process": "~3.3", 143 | "symfony/routing": "~3.3", 144 | "symfony/var-dumper": "~3.3", 145 | "tijsverkoyen/css-to-inline-styles": "~2.2", 146 | "vlucas/phpdotenv": "~2.2" 147 | } 148 | }, 149 | { 150 | "name": "laravel/tinker", 151 | "version": "v1.0.2", 152 | "version_normalized": "1.0.2.0", 153 | "require": { 154 | "illuminate/console": "~5.1", 155 | "illuminate/contracts": "~5.1", 156 | "illuminate/support": "~5.1", 157 | "php": ">=5.5.9", 158 | "psy/psysh": "0.7.*|0.8.*", 159 | "symfony/var-dumper": "~3.0" 160 | } 161 | }, 162 | { 163 | "name": "league/flysystem", 164 | "version": "1.0.41", 165 | "version_normalized": "1.0.41.0", 166 | "require": { 167 | "php": ">=5.5.9" 168 | } 169 | }, 170 | { 171 | "name": "mockery/mockery", 172 | "version": "0.9.9", 173 | "version_normalized": "0.9.9.0", 174 | "require": { 175 | "hamcrest/hamcrest-php": "~1.1", 176 | "lib-pcre": ">=7.0", 177 | "php": ">=5.3.2" 178 | } 179 | }, 180 | { 181 | "name": "monolog/monolog", 182 | "version": "1.23.0", 183 | "version_normalized": "1.23.0.0", 184 | "require": { 185 | "php": ">=5.3.0", 186 | "psr/log": "~1.0" 187 | } 188 | }, 189 | { 190 | "name": "mtdowling/cron-expression", 191 | "version": "v1.2.0", 192 | "version_normalized": "1.2.0.0", 193 | "require": { 194 | "php": ">=5.3.2" 195 | } 196 | }, 197 | { 198 | "name": "myclabs/deep-copy", 199 | "version": "1.6.1", 200 | "version_normalized": "1.6.1.0", 201 | "require": { 202 | "php": ">=5.4.0" 203 | } 204 | }, 205 | { 206 | "name": "nesbot/carbon", 207 | "version": "1.22.1", 208 | "version_normalized": "1.22.1.0", 209 | "require": { 210 | "php": ">=5.3.0", 211 | "symfony/translation": "~2.6 || ~3.0" 212 | } 213 | }, 214 | { 215 | "name": "nikic/php-parser", 216 | "version": "v3.1.1", 217 | "version_normalized": "3.1.1.0", 218 | "require": { 219 | "ext-tokenizer": "*", 220 | "php": ">=5.5" 221 | } 222 | }, 223 | { 224 | "name": "paragonie/random_compat", 225 | "version": "v2.0.11", 226 | "version_normalized": "2.0.11.0", 227 | "require": { 228 | "php": ">=5.2.0" 229 | } 230 | }, 231 | { 232 | "name": "phar-io/manifest", 233 | "version": "1.0.1", 234 | "version_normalized": "1.0.1.0", 235 | "require": { 236 | "ext-dom": "*", 237 | "ext-phar": "*", 238 | "phar-io/version": "^1.0.1", 239 | "php": "^5.6 || ^7.0" 240 | } 241 | }, 242 | { 243 | "name": "phar-io/version", 244 | "version": "1.0.1", 245 | "version_normalized": "1.0.1.0", 246 | "require": { 247 | "php": "^5.6 || ^7.0" 248 | } 249 | }, 250 | { 251 | "name": "phpdocumentor/reflection-common", 252 | "version": "1.0.1", 253 | "version_normalized": "1.0.1.0", 254 | "require": { 255 | "php": ">=5.5" 256 | } 257 | }, 258 | { 259 | "name": "phpdocumentor/reflection-docblock", 260 | "version": "4.1.1", 261 | "version_normalized": "4.1.1.0", 262 | "require": { 263 | "php": "^7.0", 264 | "phpdocumentor/reflection-common": "^1.0@dev", 265 | "phpdocumentor/type-resolver": "^0.4.0", 266 | "webmozart/assert": "^1.0" 267 | } 268 | }, 269 | { 270 | "name": "phpdocumentor/type-resolver", 271 | "version": "0.4.0", 272 | "version_normalized": "0.4.0.0", 273 | "require": { 274 | "php": "^5.5 || ^7.0", 275 | "phpdocumentor/reflection-common": "^1.0" 276 | } 277 | }, 278 | { 279 | "name": "phpspec/prophecy", 280 | "version": "v1.7.2", 281 | "version_normalized": "1.7.2.0", 282 | "require": { 283 | "doctrine/instantiator": "^1.0.2", 284 | "php": "^5.3|^7.0", 285 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 286 | "sebastian/comparator": "^1.1|^2.0", 287 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 288 | } 289 | }, 290 | { 291 | "name": "phpunit/php-code-coverage", 292 | "version": "5.2.2", 293 | "version_normalized": "5.2.2.0", 294 | "require": { 295 | "ext-dom": "*", 296 | "ext-xmlwriter": "*", 297 | "php": "^7.0", 298 | "phpunit/php-file-iterator": "^1.4.2", 299 | "phpunit/php-text-template": "^1.2.1", 300 | "phpunit/php-token-stream": "^1.4.11 || ^2.0", 301 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 302 | "sebastian/environment": "^3.0", 303 | "sebastian/version": "^2.0.1", 304 | "theseer/tokenizer": "^1.1" 305 | } 306 | }, 307 | { 308 | "name": "phpunit/php-file-iterator", 309 | "version": "1.4.2", 310 | "version_normalized": "1.4.2.0", 311 | "require": { 312 | "php": ">=5.3.3" 313 | } 314 | }, 315 | { 316 | "name": "phpunit/php-text-template", 317 | "version": "1.2.1", 318 | "version_normalized": "1.2.1.0", 319 | "require": { 320 | "php": ">=5.3.3" 321 | } 322 | }, 323 | { 324 | "name": "phpunit/php-timer", 325 | "version": "1.0.9", 326 | "version_normalized": "1.0.9.0", 327 | "require": { 328 | "php": "^5.3.3 || ^7.0" 329 | } 330 | }, 331 | { 332 | "name": "phpunit/php-token-stream", 333 | "version": "2.0.1", 334 | "version_normalized": "2.0.1.0", 335 | "require": { 336 | "ext-tokenizer": "*", 337 | "php": "^7.0" 338 | } 339 | }, 340 | { 341 | "name": "phpunit/phpunit", 342 | "version": "6.4.1", 343 | "version_normalized": "6.4.1.0", 344 | "require": { 345 | "ext-dom": "*", 346 | "ext-json": "*", 347 | "ext-libxml": "*", 348 | "ext-mbstring": "*", 349 | "ext-xml": "*", 350 | "myclabs/deep-copy": "^1.6.1", 351 | "phar-io/manifest": "^1.0.1", 352 | "phar-io/version": "^1.0", 353 | "php": "^7.0", 354 | "phpspec/prophecy": "^1.7", 355 | "phpunit/php-code-coverage": "^5.2.2", 356 | "phpunit/php-file-iterator": "^1.4.2", 357 | "phpunit/php-text-template": "^1.2.1", 358 | "phpunit/php-timer": "^1.0.9", 359 | "phpunit/phpunit-mock-objects": "^4.0.3", 360 | "sebastian/comparator": "^2.0.2", 361 | "sebastian/diff": "^2.0", 362 | "sebastian/environment": "^3.1", 363 | "sebastian/exporter": "^3.1", 364 | "sebastian/global-state": "^2.0", 365 | "sebastian/object-enumerator": "^3.0.3", 366 | "sebastian/resource-operations": "^1.0", 367 | "sebastian/version": "^2.0.1" 368 | } 369 | }, 370 | { 371 | "name": "phpunit/phpunit-mock-objects", 372 | "version": "4.0.4", 373 | "version_normalized": "4.0.4.0", 374 | "require": { 375 | "doctrine/instantiator": "^1.0.5", 376 | "php": "^7.0", 377 | "phpunit/php-text-template": "^1.2.1", 378 | "sebastian/exporter": "^3.0" 379 | } 380 | }, 381 | { 382 | "name": "psr/container", 383 | "version": "1.0.0", 384 | "version_normalized": "1.0.0.0", 385 | "require": { 386 | "php": ">=5.3.0" 387 | } 388 | }, 389 | { 390 | "name": "psr/log", 391 | "version": "1.0.2", 392 | "version_normalized": "1.0.2.0", 393 | "require": { 394 | "php": ">=5.3.0" 395 | } 396 | }, 397 | { 398 | "name": "psr/simple-cache", 399 | "version": "1.0.0", 400 | "version_normalized": "1.0.0.0", 401 | "require": { 402 | "php": ">=5.3.0" 403 | } 404 | }, 405 | { 406 | "name": "psy/psysh", 407 | "version": "v0.8.11", 408 | "version_normalized": "0.8.11.0", 409 | "require": { 410 | "dnoegel/php-xdg-base-dir": "0.1", 411 | "jakub-onderka/php-console-highlighter": "0.3.*", 412 | "nikic/php-parser": "~1.3|~2.0|~3.0", 413 | "php": ">=5.3.9", 414 | "symfony/console": "~2.3.10|^2.4.2|~3.0", 415 | "symfony/var-dumper": "~2.7|~3.0" 416 | } 417 | }, 418 | { 419 | "name": "ramsey/uuid", 420 | "version": "3.7.1", 421 | "version_normalized": "3.7.1.0", 422 | "require": { 423 | "paragonie/random_compat": "^1.0|^2.0", 424 | "php": "^5.4 || ^7.0" 425 | } 426 | }, 427 | { 428 | "name": "sebastian/code-unit-reverse-lookup", 429 | "version": "1.0.1", 430 | "version_normalized": "1.0.1.0", 431 | "require": { 432 | "php": "^5.6 || ^7.0" 433 | } 434 | }, 435 | { 436 | "name": "sebastian/comparator", 437 | "version": "2.0.2", 438 | "version_normalized": "2.0.2.0", 439 | "require": { 440 | "php": "^7.0", 441 | "sebastian/diff": "^2.0", 442 | "sebastian/exporter": "^3.0" 443 | } 444 | }, 445 | { 446 | "name": "sebastian/diff", 447 | "version": "2.0.1", 448 | "version_normalized": "2.0.1.0", 449 | "require": { 450 | "php": "^7.0" 451 | } 452 | }, 453 | { 454 | "name": "sebastian/environment", 455 | "version": "3.1.0", 456 | "version_normalized": "3.1.0.0", 457 | "require": { 458 | "php": "^7.0" 459 | } 460 | }, 461 | { 462 | "name": "sebastian/exporter", 463 | "version": "3.1.0", 464 | "version_normalized": "3.1.0.0", 465 | "require": { 466 | "php": "^7.0", 467 | "sebastian/recursion-context": "^3.0" 468 | } 469 | }, 470 | { 471 | "name": "sebastian/global-state", 472 | "version": "2.0.0", 473 | "version_normalized": "2.0.0.0", 474 | "require": { 475 | "php": "^7.0" 476 | } 477 | }, 478 | { 479 | "name": "sebastian/object-enumerator", 480 | "version": "3.0.3", 481 | "version_normalized": "3.0.3.0", 482 | "require": { 483 | "php": "^7.0", 484 | "sebastian/object-reflector": "^1.1.1", 485 | "sebastian/recursion-context": "^3.0" 486 | } 487 | }, 488 | { 489 | "name": "sebastian/object-reflector", 490 | "version": "1.1.1", 491 | "version_normalized": "1.1.1.0", 492 | "require": { 493 | "php": "^7.0" 494 | } 495 | }, 496 | { 497 | "name": "sebastian/recursion-context", 498 | "version": "3.0.0", 499 | "version_normalized": "3.0.0.0", 500 | "require": { 501 | "php": "^7.0" 502 | } 503 | }, 504 | { 505 | "name": "sebastian/resource-operations", 506 | "version": "1.0.0", 507 | "version_normalized": "1.0.0.0", 508 | "require": { 509 | "php": ">=5.6.0" 510 | } 511 | }, 512 | { 513 | "name": "sebastian/version", 514 | "version": "2.0.1", 515 | "version_normalized": "2.0.1.0", 516 | "require": { 517 | "php": ">=5.6" 518 | } 519 | }, 520 | { 521 | "name": "swiftmailer/swiftmailer", 522 | "version": "v6.0.2", 523 | "version_normalized": "6.0.2.0", 524 | "require": { 525 | "egulias/email-validator": "~2.0", 526 | "php": ">=7.0.0" 527 | } 528 | }, 529 | { 530 | "name": "symfony/console", 531 | "version": "v3.3.10", 532 | "version_normalized": "3.3.10.0", 533 | "require": { 534 | "php": "^5.5.9|>=7.0.8", 535 | "symfony/debug": "~2.8|~3.0", 536 | "symfony/polyfill-mbstring": "~1.0" 537 | } 538 | }, 539 | { 540 | "name": "symfony/css-selector", 541 | "version": "v3.3.10", 542 | "version_normalized": "3.3.10.0", 543 | "require": { 544 | "php": "^5.5.9|>=7.0.8" 545 | } 546 | }, 547 | { 548 | "name": "symfony/debug", 549 | "version": "v3.3.10", 550 | "version_normalized": "3.3.10.0", 551 | "require": { 552 | "php": "^5.5.9|>=7.0.8", 553 | "psr/log": "~1.0" 554 | } 555 | }, 556 | { 557 | "name": "symfony/event-dispatcher", 558 | "version": "v3.3.10", 559 | "version_normalized": "3.3.10.0", 560 | "require": { 561 | "php": "^5.5.9|>=7.0.8" 562 | } 563 | }, 564 | { 565 | "name": "symfony/finder", 566 | "version": "v3.3.10", 567 | "version_normalized": "3.3.10.0", 568 | "require": { 569 | "php": "^5.5.9|>=7.0.8" 570 | } 571 | }, 572 | { 573 | "name": "symfony/http-foundation", 574 | "version": "v3.3.10", 575 | "version_normalized": "3.3.10.0", 576 | "require": { 577 | "php": "^5.5.9|>=7.0.8", 578 | "symfony/polyfill-mbstring": "~1.1" 579 | } 580 | }, 581 | { 582 | "name": "symfony/http-kernel", 583 | "version": "v3.3.10", 584 | "version_normalized": "3.3.10.0", 585 | "require": { 586 | "php": "^5.5.9|>=7.0.8", 587 | "psr/log": "~1.0", 588 | "symfony/debug": "~2.8|~3.0", 589 | "symfony/event-dispatcher": "~2.8|~3.0", 590 | "symfony/http-foundation": "~3.3" 591 | } 592 | }, 593 | { 594 | "name": "symfony/polyfill-mbstring", 595 | "version": "v1.5.0", 596 | "version_normalized": "1.5.0.0", 597 | "require": { 598 | "php": ">=5.3.3" 599 | } 600 | }, 601 | { 602 | "name": "symfony/process", 603 | "version": "v3.3.10", 604 | "version_normalized": "3.3.10.0", 605 | "require": { 606 | "php": "^5.5.9|>=7.0.8" 607 | } 608 | }, 609 | { 610 | "name": "symfony/routing", 611 | "version": "v3.3.10", 612 | "version_normalized": "3.3.10.0", 613 | "require": { 614 | "php": "^5.5.9|>=7.0.8" 615 | } 616 | }, 617 | { 618 | "name": "symfony/translation", 619 | "version": "v3.3.10", 620 | "version_normalized": "3.3.10.0", 621 | "require": { 622 | "php": "^5.5.9|>=7.0.8", 623 | "symfony/polyfill-mbstring": "~1.0" 624 | } 625 | }, 626 | { 627 | "name": "symfony/var-dumper", 628 | "version": "v3.3.10", 629 | "version_normalized": "3.3.10.0", 630 | "require": { 631 | "php": "^5.5.9|>=7.0.8", 632 | "symfony/polyfill-mbstring": "~1.0" 633 | } 634 | }, 635 | { 636 | "name": "theseer/tokenizer", 637 | "version": "1.1.0", 638 | "version_normalized": "1.1.0.0", 639 | "require": { 640 | "ext-dom": "*", 641 | "ext-tokenizer": "*", 642 | "ext-xmlwriter": "*", 643 | "php": "^7.0" 644 | } 645 | }, 646 | { 647 | "name": "tijsverkoyen/css-to-inline-styles", 648 | "version": "2.2.0", 649 | "version_normalized": "2.2.0.0", 650 | "require": { 651 | "php": "^5.5 || ^7", 652 | "symfony/css-selector": "^2.7|~3.0" 653 | } 654 | }, 655 | { 656 | "name": "vlucas/phpdotenv", 657 | "version": "v2.4.0", 658 | "version_normalized": "2.4.0.0", 659 | "require": { 660 | "php": ">=5.3.9" 661 | } 662 | }, 663 | { 664 | "name": "webmozart/assert", 665 | "version": "1.2.0", 666 | "version_normalized": "1.2.0.0", 667 | "require": { 668 | "php": "^5.3.3 || ^7.0" 669 | } 670 | } 671 | ] --------------------------------------------------------------------------------