├── .gitignore ├── phpunit.xml.dist ├── tests └── ACFProInstaller │ ├── Exceptions │ └── MissingKeyExceptionTest.php │ ├── RemoteFilesystemTest.php │ └── PluginTest.php ├── src └── ACFProInstaller │ ├── Exceptions │ └── MissingKeyException.php │ ├── RemoteFilesystem.php │ └── Plugin.php ├── .travis.yml ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # composer.lock 2 | # See: https://getcomposer.org/doc/02-libraries.md#lock-file 3 | composer.lock 4 | 5 | # composer vendor directory 6 | vendor 7 | 8 | # phpunit code coverage report 9 | coverage 10 | build/logs/clover.xml 11 | 12 | # IDE 13 | .idea 14 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | src 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/ACFProInstaller/Exceptions/MissingKeyExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 12 | 'Could not find a key for ACF PRO. ' . 13 | 'Please make it available via the environment variable ' . 14 | $message, 15 | $e->getMessage() 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ACFProInstaller/Exceptions/MissingKeyException.php: -------------------------------------------------------------------------------- 1 | =5.5", 17 | "composer-plugin-api": "^1.0", 18 | "vlucas/phpdotenv": "^3.4" 19 | }, 20 | "require-dev": { 21 | "composer/composer": "1.0.*", 22 | "phpunit/phpunit": "4.8.*", 23 | "squizlabs/php_codesniffer": "2.*", 24 | "satooshi/php-coveralls": "1.*" 25 | }, 26 | "autoload": { 27 | "psr-4": { "PhilippBaschke\\ACFProInstaller\\": "src/ACFProInstaller" } 28 | }, 29 | "extra": { 30 | "class": "PhilippBaschke\\ACFProInstaller\\Plugin" 31 | }, 32 | "scripts": { 33 | "lint": "phpcs src tests --standard=PSR2", 34 | "test": "phpunit", 35 | "coverage": "phpunit --coverage-html coverage", 36 | "coveralls": "coveralls" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Philipp Baschke 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/ACFProInstaller/RemoteFilesystemTest.php: -------------------------------------------------------------------------------- 1 | io = $this->getMock('Composer\IO\IOInterface'); 13 | } 14 | 15 | public function testExtendsComposerRemoteFilesystem() 16 | { 17 | $this->assertInstanceOf( 18 | 'Composer\Util\RemoteFilesystem', 19 | new RemoteFilesystem('', $this->io) 20 | ); 21 | } 22 | 23 | // Inspired by testCopy of Composer 24 | public function testCopyUsesAcfFileUrl() 25 | { 26 | $acfFileUrl = 'file://'.__FILE__; 27 | $rfs = new RemoteFilesystem($acfFileUrl, $this->io); 28 | $file = tempnam(sys_get_temp_dir(), 'pb'); 29 | 30 | $this->assertTrue( 31 | $rfs->copy('http://example.org', 'does-not-exist', $file) 32 | ); 33 | $this->assertFileExists($file); 34 | unlink($file); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ACFProInstaller/RemoteFilesystem.php: -------------------------------------------------------------------------------- 1 | acfFileUrl = $acfFileUrl; 39 | parent::__construct($io, $config, $options, $disableTls); 40 | } 41 | 42 | /** 43 | * Copy the remote file in local 44 | * 45 | * Use $acfFileUrl instead of the provided $fileUrl 46 | * 47 | * @param string $originUrl The origin URL 48 | * @param string $fileUrl The file URL (ignored) 49 | * @param string $fileName the local filename 50 | * @param bool $progress Display the progression 51 | * @param array $options Additional context options 52 | * 53 | * @return bool true 54 | */ 55 | public function copy( 56 | $originUrl, 57 | $fileUrl, 58 | $fileName, 59 | $progress = true, 60 | $options = [] 61 | ) { 62 | return parent::copy( 63 | $originUrl, 64 | $this->acfFileUrl, 65 | $fileName, 66 | $progress, 67 | $options 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACF PRO Installer 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/philippbaschke/acf-pro-installer.svg?maxAge=3600)](https://packagist.org/packages/philippbaschke/acf-pro-installer) 4 | [![Packagist](https://img.shields.io/packagist/l/philippbaschke/acf-pro-installer.svg?maxAge=2592000)](https://github.com/PhilippBaschke/acf-pro-installer/blob/master/LICENSE) 5 | [![Travis](https://img.shields.io/travis/PhilippBaschke/acf-pro-installer.svg?maxAge=3600)](https://travis-ci.org/PhilippBaschke/acf-pro-installer) 6 | [![Coveralls](https://img.shields.io/coveralls/PhilippBaschke/acf-pro-installer.svg?maxAge=3600)](https://coveralls.io/github/PhilippBaschke/acf-pro-installer) 7 | 8 | A composer plugin that makes installing [ACF PRO] with [composer] easier. 9 | 10 | It reads your :key: ACF PRO key from the **environment** or a **.env file**. 11 | 12 | [ACF PRO]: https://www.advancedcustomfields.com/pro/ 13 | [composer]: https://github.com/composer/composer 14 | 15 | ## Usage 16 | 17 | **1. Add the package repository to the [`repositories`][composer-repositories] field in `composer.json` 18 | (based on this [gist][package-gist]):** 19 | 20 | ```json 21 | { 22 | "type": "package", 23 | "package": { 24 | "name": "advanced-custom-fields/advanced-custom-fields-pro", 25 | "version": "*.*.*(.*)", 26 | "type": "wordpress-plugin", 27 | "dist": { 28 | "type": "zip", 29 | "url": "https://connect.advancedcustomfields.com/index.php?p=pro&a=download" 30 | }, 31 | "require": { 32 | "philippbaschke/acf-pro-installer": "^1.0", 33 | "composer/installers": "^1.0" 34 | } 35 | } 36 | } 37 | ``` 38 | Replace `"version": "*.*.*(.*)"` with your desired version. 39 | 40 | Replace `"type": "wordpress-plugin"` with `"type": "library"` if you would like to have ACF PRO installed in the `./vendor` directory instead of `./wp-content/plugins`. This may be desireable if for example, you are including ACF PRO in a WordPress theme. 41 | 42 | **2. Make your ACF PRO key available** 43 | 44 | Set the environment variable **`ACF_PRO_KEY`** to your [ACF PRO key][acf-account]. 45 | 46 | Alternatively you can add an entry to your **`.env`** file: 47 | 48 | ```ini 49 | # .env (same directory as composer.json) 50 | ACF_PRO_KEY=Your-Key-Here 51 | ``` 52 | 53 | **3. Require ACF PRO** 54 | 55 | ```sh 56 | composer require advanced-custom-fields/advanced-custom-fields-pro:* 57 | ``` 58 | You can specify an [exact version][composer-versions] (that matches your desired version). 59 | 60 | If you use **`*`**, composer will install the version from the package repository (see 1). This has the benefit that you only need to change the version in the package repository when you want to update. 61 | 62 | *Be aware that `composer update` will only work if you change the `version` in the package repository. Decreasing the version only works if you require an [exact version][composer-versions].* 63 | 64 | [composer-repositories]: https://getcomposer.org/doc/04-schema.md#repositories 65 | [composer-versions]: https://getcomposer.org/doc/articles/versions.md 66 | [package-gist]: https://gist.github.com/fThues/705da4c6574a4441b488 67 | [acf-account]: https://www.advancedcustomfields.com/my-account/ 68 | -------------------------------------------------------------------------------- /src/ACFProInstaller/Plugin.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 73 | $this->io = $io; 74 | } 75 | 76 | /** 77 | * Subscribe this Plugin to relevant Events 78 | * 79 | * Pre Install/Update: The version needs to be added to the url 80 | * (will show up in composer.lock) 81 | * Pre Download: The key needs to be added to the url 82 | * (will not show up in composer.lock) 83 | * 84 | * @access public 85 | * @return array An array of events that the plugin subscribes to 86 | * @static 87 | */ 88 | public static function getSubscribedEvents() 89 | { 90 | return [ 91 | PackageEvents::PRE_PACKAGE_INSTALL => 'addVersion', 92 | PackageEvents::PRE_PACKAGE_UPDATE => 'addVersion', 93 | PluginEvents::PRE_FILE_DOWNLOAD => 'addKey' 94 | ]; 95 | } 96 | 97 | /** 98 | * Add the version to the package url 99 | * 100 | * The version needs to be added in the PRE_PACKAGE_INSTALL/UPDATE 101 | * event to make sure that different version save different urls 102 | * in composer.lock. Composer would load any available version from cache 103 | * although the version numbers might differ (because they have the same 104 | * url). 105 | * 106 | * @access public 107 | * @param PackageEvent $event The event that called the method 108 | * @throws UnexpectedValueException 109 | */ 110 | public function addVersion(PackageEvent $event) 111 | { 112 | $package = $this->getPackageFromOperation($event->getOperation()); 113 | 114 | if ($package->getName() === self::ACF_PRO_PACKAGE_NAME) { 115 | $version = $this->validateVersion($package->getPrettyVersion()); 116 | $package->setDistUrl( 117 | $this->addParameterToUrl($package->getDistUrl(), 't', $version) 118 | ); 119 | } 120 | } 121 | 122 | 123 | /** 124 | * Add the key from the environment to the event url 125 | * 126 | * The key is not added to the package because it would show up in the 127 | * composer.lock file in this case. A custom file system is used to 128 | * swap out the ACF PRO url with a url that contains the key. 129 | * 130 | * @access public 131 | * @param PreFileDownloadEvent $event The event that called this method 132 | * @throws MissingKeyException 133 | */ 134 | public function addKey(PreFileDownloadEvent $event) 135 | { 136 | $processedUrl = $event->getProcessedUrl(); 137 | 138 | if ($this->isAcfProPackageUrl($processedUrl)) { 139 | $rfs = $event->getRemoteFilesystem(); 140 | $acfRfs = new RemoteFilesystem( 141 | $this->addParameterToUrl( 142 | $processedUrl, 143 | 'k', 144 | $this->getKeyFromEnv() 145 | ), 146 | $this->io, 147 | $this->composer->getConfig(), 148 | $rfs->getOptions(), 149 | $rfs->isTlsDisabled() 150 | ); 151 | $event->setRemoteFilesystem($acfRfs); 152 | } 153 | } 154 | 155 | /** 156 | * Get the package from a given operation 157 | * 158 | * Is needed because update operations don't have a getPackage method 159 | * 160 | * @access protected 161 | * @param OperationInterface $operation The operation 162 | * @return PackageInterface The package of the operation 163 | */ 164 | protected function getPackageFromOperation(OperationInterface $operation) 165 | { 166 | if ($operation->getJobType() === 'update') { 167 | return $operation->getTargetPackage(); 168 | } 169 | return $operation->getPackage(); 170 | } 171 | 172 | /** 173 | * Validate that the version is an exact major.minor.patch.optional version 174 | * 175 | * The url to download the code for the package only works with exact 176 | * version numbers with 3 or 4 digits: e.g. 1.2.3 or 1.2.3.4 177 | * 178 | * @access protected 179 | * @param string $version The version that should be validated 180 | * @return string The valid version 181 | * @throws UnexpectedValueException 182 | */ 183 | protected function validateVersion($version) 184 | { 185 | // \A = start of string, \Z = end of string 186 | // See: http://stackoverflow.com/a/34994075 187 | $major_minor_patch_optional = '/\A\d\.\d\.\d{1,2}(?:\.\d)?\Z/'; 188 | 189 | if (!preg_match($major_minor_patch_optional, $version)) { 190 | throw new \UnexpectedValueException( 191 | 'The version constraint of ' . self::ACF_PRO_PACKAGE_NAME . 192 | ' should be exact (with 3 or 4 digits). ' . 193 | 'Invalid version string "' . $version . '"' 194 | ); 195 | } 196 | 197 | return $version; 198 | } 199 | 200 | /** 201 | * Test if the given url is the ACF PRO download url 202 | * 203 | * @access protected 204 | * @param string The url that should be checked 205 | * @return bool 206 | */ 207 | protected function isAcfProPackageUrl($url) 208 | { 209 | return strpos($url, self::ACF_PRO_PACKAGE_URL) !== false; 210 | } 211 | 212 | /** 213 | * Get the ACF PRO key from the environment 214 | * 215 | * Loads the .env file that is in the same directory as composer.json 216 | * and gets the key from the environment variable KEY_ENV_VARIABLE. 217 | * Already set variables will not be overwritten by the variables in .env 218 | * @link https://github.com/vlucas/phpdotenv#immutability 219 | * 220 | * @access protected 221 | * @return string The key from the environment 222 | * @throws PhilippBaschke\ACFProInstaller\Exceptions\MissingKeyException 223 | */ 224 | protected function getKeyFromEnv() 225 | { 226 | $this->loadDotEnv(); 227 | $key = getenv(self::KEY_ENV_VARIABLE); 228 | 229 | if (!$key) { 230 | throw new MissingKeyException(self::KEY_ENV_VARIABLE); 231 | } 232 | 233 | return $key; 234 | } 235 | 236 | /** 237 | * Make environment variables in .env available if .env exists 238 | * 239 | * getcwd() returns the directory of composer.json. 240 | * 241 | * @access protected 242 | */ 243 | protected function loadDotEnv() 244 | { 245 | if (file_exists(getcwd().DIRECTORY_SEPARATOR.'.env')) { 246 | $dotenv = Dotenv::create(getcwd()); 247 | $dotenv->load(); 248 | } 249 | } 250 | 251 | /** 252 | * Add a parameter to the given url 253 | * 254 | * Adds the given parameter at the end of the given url. It only works with 255 | * urls that already have parameters (e.g. test.com?p=true) because it 256 | * uses & as a separation character. 257 | * 258 | * @access protected 259 | * @param string $url The url that should be appended 260 | * @param string $parameter The name of the parameter 261 | * @param string $value The value of the parameter 262 | * @return string The url appended with ¶meter=value 263 | */ 264 | protected function addParameterToUrl($url, $parameter, $value) 265 | { 266 | $cleanUrl = $this->removeParameterFromUrl($url, $parameter); 267 | $urlParameter = '&' . $parameter . '=' . urlencode($value); 268 | 269 | return $cleanUrl .= $urlParameter; 270 | } 271 | 272 | /** 273 | * Remove a given parameter from the given url 274 | * 275 | * Removes ¶meter=value from the given url. Only works with urls that 276 | * have multiple parameters and the parameter that should be removed is 277 | * not the first (because of the & character). 278 | * 279 | * @access protected 280 | * @param string $url The url where the parameter should be removed 281 | * @param string $parameter The name of the parameter 282 | * @return string The url with the ¶meter=value removed 283 | */ 284 | protected function removeParameterFromUrl($url, $parameter) 285 | { 286 | // e.g. &t=1.2.3 in example.com?p=index.php&t=1.2.3&k=key 287 | $pattern = "/(&$parameter=[^&]*)/"; 288 | return preg_replace($pattern, '', $url); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /tests/ACFProInstaller/PluginTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf( 31 | 'Composer\Plugin\PluginInterface', 32 | new Plugin() 33 | ); 34 | } 35 | 36 | public function testImplementsEventSubscriberInterface() 37 | { 38 | $this->assertInstanceOf( 39 | 'Composer\EventDispatcher\EventSubscriberInterface', 40 | new Plugin() 41 | ); 42 | } 43 | 44 | public function testActivateMakesComposerAndIOAvailable() 45 | { 46 | $composer = $this->getMockBuilder('Composer\Composer')->getMock(); 47 | $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); 48 | $plugin = new Plugin; 49 | $plugin->activate($composer, $io); 50 | 51 | $this->assertAttributeEquals($composer, 'composer', $plugin); 52 | $this->assertAttributeEquals($io, 'io', $plugin); 53 | } 54 | 55 | public function testSubscribesToPrePackageInstallEvent() 56 | { 57 | $subscribedEvents = Plugin::getSubscribedEvents(); 58 | $this->assertEquals( 59 | $subscribedEvents[PackageEvents::PRE_PACKAGE_INSTALL], 60 | 'addVersion' 61 | ); 62 | } 63 | 64 | public function testSubscribesToPreUpdateInstallEvent() 65 | { 66 | $subscribedEvents = Plugin::getSubscribedEvents(); 67 | $this->assertEquals( 68 | $subscribedEvents[PackageEvents::PRE_PACKAGE_UPDATE], 69 | 'addVersion' 70 | ); 71 | } 72 | 73 | public function testSubscribesToPreFileDownloadEvent() 74 | { 75 | $subscribedEvents = Plugin::getSubscribedEvents(); 76 | $this->assertEquals( 77 | $subscribedEvents[PluginEvents::PRE_FILE_DOWNLOAD], 78 | 'addKey' 79 | ); 80 | } 81 | 82 | public function testAddVersionOnInstall() 83 | { 84 | // The version that should be required 85 | $version = '1.2.3'; 86 | 87 | // Make key available in the ENVIRONMENT 88 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 89 | 90 | // Mock a Package 91 | $package = $this 92 | ->getMockBuilder('Composer\Package\PackageInterface') 93 | ->setMethods([ 94 | 'getName', 95 | 'getPrettyVersion', 96 | 'getDistUrl', 97 | 'setDistUrl' 98 | ]) 99 | ->getMockForAbstractClass(); 100 | 101 | $package 102 | ->expects($this->once()) 103 | ->method('getName') 104 | ->willReturn(self::REPO_NAME); 105 | 106 | $package 107 | ->expects($this->once()) 108 | ->method('getPrettyVersion') 109 | ->willReturn($version); 110 | 111 | $package 112 | ->expects($this->once()) 113 | ->method('getDistUrl') 114 | ->willReturn(self::REPO_URL); 115 | 116 | $package 117 | ->expects($this->once()) 118 | ->method('setDistUrl') 119 | ->with(self::REPO_URL . "&t=$version"); 120 | 121 | // Mock an Operation 122 | $operationClass = 123 | 'Composer\DependencyResolver\Operation\InstallOperation'; 124 | $operation = $this 125 | ->getMockBuilder($operationClass) 126 | ->disableOriginalConstructor() 127 | ->setMethods(['getJobType', 'getPackage']) 128 | ->getMock(); 129 | 130 | $operation 131 | ->expects($this->once()) 132 | ->method('getJobType') 133 | ->willReturn('install'); 134 | 135 | $operation 136 | ->expects($this->once()) 137 | ->method('getPackage') 138 | ->willReturn($package); 139 | 140 | // Mock a PackageEvent 141 | $packageEvent = $this 142 | ->getMockBuilder('Composer\Installer\PackageEvent') 143 | ->disableOriginalConstructor() 144 | ->setMethods(['getOperation']) 145 | ->getMock(); 146 | 147 | $packageEvent 148 | ->expects($this->once()) 149 | ->method('getOperation') 150 | ->willReturn($operation); 151 | 152 | // Call addVersion 153 | $plugin = new Plugin(); 154 | $plugin->addVersion($packageEvent); 155 | } 156 | 157 | public function testAddVersionOnUpdate() 158 | { 159 | // The version that should be required 160 | $version = '1.2.3'; 161 | 162 | // Make key available in the ENVIRONMENT 163 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 164 | 165 | // Mock a Package 166 | $package = $this 167 | ->getMockBuilder('Composer\Package\PackageInterface') 168 | ->setMethods([ 169 | 'getName', 170 | 'getPrettyVersion', 171 | 'getDistUrl', 172 | 'setDistUrl' 173 | ]) 174 | ->getMockForAbstractClass(); 175 | 176 | $package 177 | ->expects($this->once()) 178 | ->method('getName') 179 | ->willReturn(self::REPO_NAME); 180 | 181 | $package 182 | ->expects($this->once()) 183 | ->method('getPrettyVersion') 184 | ->willReturn($version); 185 | 186 | $package 187 | ->expects($this->once()) 188 | ->method('getDistUrl') 189 | ->willReturn(self::REPO_URL); 190 | 191 | $package 192 | ->expects($this->once()) 193 | ->method('setDistUrl') 194 | ->with(self::REPO_URL . "&t=$version"); 195 | 196 | // Mock an Operation 197 | $operationClass = 198 | 'Composer\DependencyResolver\Operation\UpdateOperation'; 199 | $operation = $this 200 | ->getMockBuilder($operationClass) 201 | ->disableOriginalConstructor() 202 | ->setMethods(['getJobType', 'getTargetPackage']) 203 | ->getMock(); 204 | 205 | $operation 206 | ->expects($this->once()) 207 | ->method('getJobType') 208 | ->willReturn('update'); 209 | 210 | $operation 211 | ->expects($this->once()) 212 | ->method('getTargetPackage') 213 | ->willReturn($package); 214 | 215 | // Mock a PackageEvent 216 | $packageEvent = $this 217 | ->getMockBuilder('Composer\Installer\PackageEvent') 218 | ->disableOriginalConstructor() 219 | ->setMethods(['getOperation']) 220 | ->getMock(); 221 | 222 | $packageEvent 223 | ->expects($this->once()) 224 | ->method('getOperation') 225 | ->willReturn($operation); 226 | 227 | // Call addVersion 228 | $plugin = new Plugin(); 229 | $plugin->addVersion($packageEvent); 230 | } 231 | 232 | public function testDontAddVersionOnOtherPackages() 233 | { 234 | // Make key available in the ENVIRONMENT 235 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 236 | 237 | // Mock a Package 238 | $package = $this 239 | ->getMockBuilder('Composer\Package\PackageInterface') 240 | ->setMethods([ 241 | 'getName', 242 | 'getPrettyVersion', 243 | 'getDistUrl', 244 | 'setDistUrl' 245 | ]) 246 | ->getMockForAbstractClass(); 247 | 248 | $package 249 | ->expects($this->once()) 250 | ->method('getName') 251 | ->willReturn('another-package'); 252 | 253 | $package 254 | ->expects($this->never()) 255 | ->method('getPrettyVersion'); 256 | 257 | $package 258 | ->expects($this->never()) 259 | ->method('getDistUrl'); 260 | 261 | $package 262 | ->expects($this->never()) 263 | ->method('setDistUrl'); 264 | 265 | // Mock an Operation 266 | $operationClass = 267 | 'Composer\DependencyResolver\Operation\InstallOperation'; 268 | $operation = $this 269 | ->getMockBuilder($operationClass) 270 | ->disableOriginalConstructor() 271 | ->setMethods(['getJobType', 'getPackage']) 272 | ->getMock(); 273 | 274 | $operation 275 | ->expects($this->once()) 276 | ->method('getJobType') 277 | ->willReturn('install'); 278 | 279 | $operation 280 | ->expects($this->once()) 281 | ->method('getPackage') 282 | ->willReturn($package); 283 | 284 | // Mock a PackageEvent 285 | $packageEvent = $this 286 | ->getMockBuilder('Composer\Installer\PackageEvent') 287 | ->disableOriginalConstructor() 288 | ->setMethods(['getOperation']) 289 | ->getMock(); 290 | 291 | $packageEvent 292 | ->expects($this->once()) 293 | ->method('getOperation') 294 | ->willReturn($operation); 295 | 296 | // Call addVersion 297 | $plugin = new Plugin(); 298 | $plugin->addVersion($packageEvent); 299 | } 300 | 301 | protected function versionPassesValidationHelper($version) 302 | { 303 | // Make key available in the ENVIRONMENT 304 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 305 | 306 | // Mock a Package 307 | $package = $this 308 | ->getMockBuilder('Composer\Package\PackageInterface') 309 | ->setMethods([ 310 | 'getName', 311 | 'getPrettyVersion', 312 | 'getDistUrl', 313 | 'setDistUrl' 314 | ]) 315 | ->getMockForAbstractClass(); 316 | 317 | $package 318 | ->expects($this->once()) 319 | ->method('getName') 320 | ->willReturn(self::REPO_NAME); 321 | 322 | $package 323 | ->expects($this->once()) 324 | ->method('getPrettyVersion') 325 | ->willReturn($version); 326 | 327 | $package 328 | ->expects($this->once()) 329 | ->method('getDistUrl') 330 | ->willReturn(self::REPO_URL); 331 | 332 | $package 333 | ->expects($this->once()) 334 | ->method('setDistUrl'); 335 | 336 | // Mock an Operation 337 | $operationClass = 338 | 'Composer\DependencyResolver\Operation\InstallOperation'; 339 | $operation = $this 340 | ->getMockBuilder($operationClass) 341 | ->disableOriginalConstructor() 342 | ->setMethods(['getJobType', 'getPackage']) 343 | ->getMock(); 344 | 345 | $operation 346 | ->expects($this->once()) 347 | ->method('getJobType') 348 | ->willReturn('install'); 349 | 350 | $operation 351 | ->expects($this->once()) 352 | ->method('getPackage') 353 | ->willReturn($package); 354 | 355 | // Mock a PackageEvent 356 | $packageEvent = $this 357 | ->getMockBuilder('Composer\Installer\PackageEvent') 358 | ->disableOriginalConstructor() 359 | ->setMethods(['getOperation']) 360 | ->getMock(); 361 | 362 | $packageEvent 363 | ->expects($this->once()) 364 | ->method('getOperation') 365 | ->willReturn($operation); 366 | 367 | // Call addVersion 368 | $plugin = new Plugin(); 369 | $plugin->addVersion($packageEvent); 370 | } 371 | 372 | public function testExactVersionWith3DigitsPassesValidation() 373 | { 374 | $this->versionPassesValidationHelper('1.2.3'); 375 | } 376 | 377 | public function testExactVersionWith4DigitsPassesValidation() 378 | { 379 | $this->versionPassesValidationHelper('1.2.3.4'); 380 | } 381 | 382 | public function testExactVersionWithPatchDoubleDigitsPassesValidation() 383 | { 384 | $this->versionPassesValidationHelper('1.2.30'); 385 | } 386 | 387 | protected function versionFailsValidationHelper($version) 388 | { 389 | // Expect an Exception 390 | $this->setExpectedException( 391 | 'UnexpectedValueException', 392 | 'The version constraint of ' . self::REPO_NAME . 393 | ' should be exact (with 3 or 4 digits). ' . 394 | 'Invalid version string "' . $version . '"' 395 | ); 396 | 397 | // Mock a Package 398 | $package = $this 399 | ->getMockBuilder('Composer\Package\PackageInterface') 400 | ->setMethods([ 401 | 'getName', 402 | 'getPrettyVersion' 403 | ]) 404 | ->getMockForAbstractClass(); 405 | 406 | $package 407 | ->expects($this->once()) 408 | ->method('getName') 409 | ->willReturn(self::REPO_NAME); 410 | 411 | $package 412 | ->expects($this->once()) 413 | ->method('getPrettyVersion') 414 | ->willReturn($version); 415 | 416 | // Mock an Operation 417 | $operationClass = 418 | 'Composer\DependencyResolver\Operation\InstallOperation'; 419 | $operation = $this 420 | ->getMockBuilder($operationClass) 421 | ->disableOriginalConstructor() 422 | ->setMethods(['getJobType', 'getPackage']) 423 | ->getMock(); 424 | 425 | $operation 426 | ->expects($this->once()) 427 | ->method('getJobType') 428 | ->willReturn('install'); 429 | 430 | $operation 431 | ->expects($this->once()) 432 | ->method('getPackage') 433 | ->willReturn($package); 434 | 435 | // Mock a PackageEvent 436 | $packageEvent = $this 437 | ->getMockBuilder('Composer\Installer\PackageEvent') 438 | ->disableOriginalConstructor() 439 | ->setMethods(['getOperation']) 440 | ->getMock(); 441 | 442 | $packageEvent 443 | ->expects($this->once()) 444 | ->method('getOperation') 445 | ->willReturn($operation); 446 | 447 | // Call addVersion 448 | $plugin = new Plugin(); 449 | $plugin->addVersion($packageEvent); 450 | } 451 | 452 | public function testExactVersionWith2DigitsFailsValidation() 453 | { 454 | $this->versionFailsValidationHelper('1.2'); 455 | } 456 | 457 | public function testExactVersionWith1DigitsFailsValidation() 458 | { 459 | $this->versionFailsValidationHelper('1'); 460 | } 461 | 462 | public function testDontAddVersionTwice() 463 | { 464 | // The version that should be required 465 | $version = '1.2.3'; 466 | 467 | // Make key available in the ENVIRONMENT 468 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 469 | 470 | // Mock a Package 471 | $package = $this 472 | ->getMockBuilder('Composer\Package\PackageInterface') 473 | ->setMethods([ 474 | 'getName', 475 | 'getPrettyVersion', 476 | 'getDistUrl', 477 | 'setDistUrl' 478 | ]) 479 | ->getMockForAbstractClass(); 480 | 481 | $package 482 | ->expects($this->once()) 483 | ->method('getName') 484 | ->willReturn(self::REPO_NAME); 485 | 486 | $package 487 | ->expects($this->once()) 488 | ->method('getPrettyVersion') 489 | ->willReturn($version); 490 | 491 | $package 492 | ->expects($this->once()) 493 | ->method('getDistUrl') 494 | ->willReturn(self::REPO_URL . '&t=' . $version); 495 | 496 | $package 497 | ->expects($this->once()) 498 | ->method('setDistUrl') 499 | ->with(self::REPO_URL . "&t=$version"); 500 | 501 | // Mock an Operation 502 | $operationClass = 503 | 'Composer\DependencyResolver\Operation\InstallOperation'; 504 | $operation = $this 505 | ->getMockBuilder($operationClass) 506 | ->disableOriginalConstructor() 507 | ->setMethods(['getJobType', 'getPackage']) 508 | ->getMock(); 509 | 510 | $operation 511 | ->expects($this->once()) 512 | ->method('getJobType') 513 | ->willReturn('install'); 514 | 515 | $operation 516 | ->expects($this->once()) 517 | ->method('getPackage') 518 | ->willReturn($package); 519 | 520 | // Mock a PackageEvent 521 | $packageEvent = $this 522 | ->getMockBuilder('Composer\Installer\PackageEvent') 523 | ->disableOriginalConstructor() 524 | ->setMethods(['getOperation']) 525 | ->getMock(); 526 | 527 | $packageEvent 528 | ->expects($this->once()) 529 | ->method('getOperation') 530 | ->willReturn($operation); 531 | 532 | // Call addVersion 533 | $plugin = new Plugin(); 534 | $plugin->addVersion($packageEvent); 535 | } 536 | 537 | public function testReplaceVersionInUrl() 538 | { 539 | // The version that should be required 540 | $version = '1.2.3'; 541 | 542 | // Make key available in the ENVIRONMENT 543 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 544 | 545 | // Mock a Package 546 | $package = $this 547 | ->getMockBuilder('Composer\Package\PackageInterface') 548 | ->setMethods([ 549 | 'getName', 550 | 'getPrettyVersion', 551 | 'getDistUrl', 552 | 'setDistUrl' 553 | ]) 554 | ->getMockForAbstractClass(); 555 | 556 | $package 557 | ->expects($this->once()) 558 | ->method('getName') 559 | ->willReturn(self::REPO_NAME); 560 | 561 | $package 562 | ->expects($this->once()) 563 | ->method('getPrettyVersion') 564 | ->willReturn($version); 565 | 566 | $package 567 | ->expects($this->once()) 568 | ->method('getDistUrl') 569 | ->willReturn(self::REPO_URL . '&t=' . $version . '.4'); 570 | 571 | $package 572 | ->expects($this->once()) 573 | ->method('setDistUrl') 574 | ->with(self::REPO_URL . "&t=$version"); 575 | 576 | // Mock an Operation 577 | $operationClass = 578 | 'Composer\DependencyResolver\Operation\InstallOperation'; 579 | $operation = $this 580 | ->getMockBuilder($operationClass) 581 | ->disableOriginalConstructor() 582 | ->setMethods(['getJobType', 'getPackage']) 583 | ->getMock(); 584 | 585 | $operation 586 | ->expects($this->once()) 587 | ->method('getJobType') 588 | ->willReturn('install'); 589 | 590 | $operation 591 | ->expects($this->once()) 592 | ->method('getPackage') 593 | ->willReturn($package); 594 | 595 | // Mock a PackageEvent 596 | $packageEvent = $this 597 | ->getMockBuilder('Composer\Installer\PackageEvent') 598 | ->disableOriginalConstructor() 599 | ->setMethods(['getOperation']) 600 | ->getMock(); 601 | 602 | $packageEvent 603 | ->expects($this->once()) 604 | ->method('getOperation') 605 | ->willReturn($operation); 606 | 607 | // Call addVersion 608 | $plugin = new Plugin(); 609 | $plugin->addVersion($packageEvent); 610 | } 611 | 612 | public function testAddKeyCreatesCustomFilesystemWithOldValues() 613 | { 614 | // Make key available in the ENVIRONMENT 615 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 616 | 617 | // Mock a RemoteFilesystem 618 | $options = ['options' => 'array']; 619 | $tlsDisabled = true; 620 | 621 | $rfs = $this 622 | ->getMockBuilder('Composer\Util\RemoteFilesystem') 623 | ->disableOriginalConstructor() 624 | ->setMethods(['getOptions', 'isTlsDisabled']) 625 | ->getMock(); 626 | 627 | $rfs 628 | ->expects($this->once()) 629 | ->method('getOptions') 630 | ->willReturn($options); 631 | 632 | $rfs 633 | ->expects($this->once()) 634 | ->method('isTlsDisabled') 635 | ->willReturn($tlsDisabled); 636 | 637 | // Mock Config 638 | $config = $this 639 | ->getMockBuilder('Composer\Config') 640 | ->getMock(); 641 | 642 | // Mock Composer 643 | $composer = $this 644 | ->getMockBuilder('Composer\Composer') 645 | ->setMethods(['getConfig']) 646 | ->getMock(); 647 | 648 | $composer 649 | ->expects($this->once()) 650 | ->method('getConfig') 651 | ->willReturn($config); 652 | 653 | // Mock IOInterface 654 | $io = $this 655 | ->getMockBuilder('Composer\IO\IOInterface') 656 | ->getMock(); 657 | 658 | // Mock an Event 659 | $event = $this 660 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 661 | ->disableOriginalConstructor() 662 | ->setMethods([ 663 | 'getProcessedUrl', 664 | 'getRemoteFilesystem', 665 | 'setRemoteFilesystem' 666 | ]) 667 | ->getMock(); 668 | 669 | $event 670 | ->expects($this->once()) 671 | ->method('getProcessedUrl') 672 | ->willReturn(self::REPO_URL); 673 | 674 | $event 675 | ->expects($this->once()) 676 | ->method('getRemoteFilesystem') 677 | ->willReturn($rfs); 678 | 679 | $event 680 | ->expects($this->once()) 681 | ->method('setRemoteFilesystem') 682 | ->with($this->callback( 683 | function ($rfs) use ($config, $io, $options, $tlsDisabled) { 684 | $this->assertAttributeEquals($config, 'config', $rfs); 685 | $this->assertAttributeEquals($io, 'io', $rfs); 686 | $this->assertEquals($options, $rfs->getOptions()); 687 | $this->assertEquals($tlsDisabled, $rfs->isTlsDisabled()); 688 | return true; 689 | } 690 | )); 691 | 692 | // Call addKey 693 | $plugin = new Plugin(); 694 | $plugin->activate($composer, $io); 695 | $plugin->addKey($event); 696 | } 697 | 698 | public function testAddKeyFromENV() 699 | { 700 | // The key that should be available in the ENVIRONMENT 701 | $key = 'ENV_KEY'; 702 | 703 | // Make key available in the ENVIRONMENT 704 | putenv(self::KEY_ENV_VARIABLE . '=' . $key); 705 | 706 | // Mock a RemoteFilesystem 707 | $rfs = $this 708 | ->getMockBuilder('Composer\Util\RemoteFilesystem') 709 | ->disableOriginalConstructor() 710 | ->setMethods(['getOptions', 'isTlsDisabled']) 711 | ->getMock(); 712 | 713 | $rfs 714 | ->expects($this->once()) 715 | ->method('getOptions') 716 | ->willReturn([]); 717 | 718 | $rfs 719 | ->expects($this->once()) 720 | ->method('isTlsDisabled') 721 | ->willReturn(true); 722 | 723 | // Mock Config 724 | $config = $this 725 | ->getMockBuilder('Composer\Config') 726 | ->getMock(); 727 | 728 | // Mock Composer 729 | $composer = $this 730 | ->getMockBuilder('Composer\Composer') 731 | ->setMethods(['getConfig']) 732 | ->getMock(); 733 | 734 | $composer 735 | ->expects($this->once()) 736 | ->method('getConfig') 737 | ->willReturn($config); 738 | 739 | // Mock IOInterface 740 | $io = $this 741 | ->getMockBuilder('Composer\IO\IOInterface') 742 | ->getMock(); 743 | 744 | // Mock an Event 745 | $event = $this 746 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 747 | ->disableOriginalConstructor() 748 | ->setMethods([ 749 | 'getProcessedUrl', 750 | 'getRemoteFilesystem', 751 | 'setRemoteFilesystem' 752 | ]) 753 | ->getMock(); 754 | 755 | $event 756 | ->expects($this->once()) 757 | ->method('getProcessedUrl') 758 | ->willReturn(self::REPO_URL); 759 | 760 | $event 761 | ->expects($this->once()) 762 | ->method('getRemoteFilesystem') 763 | ->willReturn($rfs); 764 | 765 | $event 766 | ->expects($this->once()) 767 | ->method('setRemoteFilesystem') 768 | ->with($this->callback( 769 | function ($rfs) use ($key) { 770 | $this->assertAttributeContains( 771 | "&k=$key", 772 | 'acfFileUrl', 773 | $rfs 774 | ); 775 | return true; 776 | } 777 | )); 778 | 779 | // Call addKey 780 | $plugin = new Plugin(); 781 | $plugin->activate($composer, $io); 782 | $plugin->addKey($event); 783 | } 784 | 785 | public function testAddKeyFromDotEnv() 786 | { 787 | // The key that should be available in the .env file 788 | $key = 'DOT_ENV_KEY'; 789 | 790 | // Make key available in the .env file 791 | file_put_contents( 792 | getcwd().DIRECTORY_SEPARATOR.'.env', 793 | self::KEY_ENV_VARIABLE . '=' . $key 794 | ); 795 | 796 | // Mock a RemoteFilesystem 797 | $rfs = $this 798 | ->getMockBuilder('Composer\Util\RemoteFilesystem') 799 | ->disableOriginalConstructor() 800 | ->setMethods(['getOptions', 'isTlsDisabled']) 801 | ->getMock(); 802 | 803 | $rfs 804 | ->expects($this->once()) 805 | ->method('getOptions') 806 | ->willReturn([]); 807 | 808 | $rfs 809 | ->expects($this->once()) 810 | ->method('isTlsDisabled') 811 | ->willReturn(true); 812 | 813 | // Mock Config 814 | $config = $this 815 | ->getMockBuilder('Composer\Config') 816 | ->getMock(); 817 | 818 | // Mock Composer 819 | $composer = $this 820 | ->getMockBuilder('Composer\Composer') 821 | ->setMethods(['getConfig']) 822 | ->getMock(); 823 | 824 | $composer 825 | ->expects($this->once()) 826 | ->method('getConfig') 827 | ->willReturn($config); 828 | 829 | // Mock IOInterface 830 | $io = $this 831 | ->getMockBuilder('Composer\IO\IOInterface') 832 | ->getMock(); 833 | 834 | // Mock an Event 835 | $event = $this 836 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 837 | ->disableOriginalConstructor() 838 | ->setMethods([ 839 | 'getProcessedUrl', 840 | 'getRemoteFilesystem', 841 | 'setRemoteFilesystem' 842 | ]) 843 | ->getMock(); 844 | 845 | $event 846 | ->expects($this->once()) 847 | ->method('getProcessedUrl') 848 | ->willReturn(self::REPO_URL); 849 | 850 | $event 851 | ->expects($this->once()) 852 | ->method('getRemoteFilesystem') 853 | ->willReturn($rfs); 854 | 855 | $event 856 | ->expects($this->once()) 857 | ->method('setRemoteFilesystem') 858 | ->with($this->callback( 859 | function ($rfs) use ($key) { 860 | $this->assertAttributeContains( 861 | "&k=$key", 862 | 'acfFileUrl', 863 | $rfs 864 | ); 865 | return true; 866 | } 867 | )); 868 | 869 | // Call addKey 870 | $plugin = new Plugin(); 871 | $plugin->activate($composer, $io); 872 | $plugin->addKey($event); 873 | } 874 | 875 | public function testPreferKeyFromEnv() 876 | { 877 | // The key that should be available in the .env file 878 | $fileKey = 'DOT_ENV_KEY'; 879 | $key = 'ENV_KEY'; 880 | 881 | // Make key available in the .env file 882 | file_put_contents( 883 | getcwd().DIRECTORY_SEPARATOR.'.env', 884 | self::KEY_ENV_VARIABLE . '=' . $fileKey 885 | ); 886 | 887 | // Make key available in the ENVIRONMENT 888 | putenv(self::KEY_ENV_VARIABLE . '=' . $key); 889 | 890 | // Mock a RemoteFilesystem 891 | $rfs = $this 892 | ->getMockBuilder('Composer\Util\RemoteFilesystem') 893 | ->disableOriginalConstructor() 894 | ->setMethods(['getOptions', 'isTlsDisabled']) 895 | ->getMock(); 896 | 897 | $rfs 898 | ->expects($this->once()) 899 | ->method('getOptions') 900 | ->willReturn([]); 901 | 902 | $rfs 903 | ->expects($this->once()) 904 | ->method('isTlsDisabled') 905 | ->willReturn(true); 906 | 907 | // Mock Config 908 | $config = $this 909 | ->getMockBuilder('Composer\Config') 910 | ->getMock(); 911 | 912 | // Mock Composer 913 | $composer = $this 914 | ->getMockBuilder('Composer\Composer') 915 | ->setMethods(['getConfig']) 916 | ->getMock(); 917 | 918 | $composer 919 | ->expects($this->once()) 920 | ->method('getConfig') 921 | ->willReturn($config); 922 | 923 | // Mock IOInterface 924 | $io = $this 925 | ->getMockBuilder('Composer\IO\IOInterface') 926 | ->getMock(); 927 | 928 | // Mock an Event 929 | $event = $this 930 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 931 | ->disableOriginalConstructor() 932 | ->setMethods([ 933 | 'getProcessedUrl', 934 | 'getRemoteFilesystem', 935 | 'setRemoteFilesystem' 936 | ]) 937 | ->getMock(); 938 | 939 | $event 940 | ->expects($this->once()) 941 | ->method('getProcessedUrl') 942 | ->willReturn(self::REPO_URL); 943 | 944 | $event 945 | ->expects($this->once()) 946 | ->method('getRemoteFilesystem') 947 | ->willReturn($rfs); 948 | 949 | $event 950 | ->expects($this->once()) 951 | ->method('setRemoteFilesystem') 952 | ->with($this->callback( 953 | function ($rfs) use ($key) { 954 | $this->assertAttributeContains( 955 | "&k=$key", 956 | 'acfFileUrl', 957 | $rfs 958 | ); 959 | return true; 960 | } 961 | )); 962 | 963 | // Call addKey 964 | $plugin = new Plugin(); 965 | $plugin->activate($composer, $io); 966 | $plugin->addKey($event); 967 | } 968 | 969 | public function testThrowExceptionWhenKeyIsMissing() 970 | { 971 | // Expect an Exception 972 | $this->setExpectedException( 973 | 'PhilippBaschke\ACFProInstaller\Exceptions\MissingKeyException', 974 | 'ACF_PRO_KEY' 975 | ); 976 | 977 | // Mock a RemoteFilesystem 978 | $rfs = $this 979 | ->getMockBuilder('Composer\Util\RemoteFilesystem') 980 | ->disableOriginalConstructor() 981 | ->getMock(); 982 | 983 | // Mock an Event 984 | $event = $this 985 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 986 | ->disableOriginalConstructor() 987 | ->setMethods([ 988 | 'getProcessedUrl', 989 | 'getRemoteFilesystem' 990 | ]) 991 | ->getMock(); 992 | 993 | $event 994 | ->expects($this->once()) 995 | ->method('getProcessedUrl') 996 | ->willReturn(self::REPO_URL); 997 | 998 | $event 999 | ->expects($this->once()) 1000 | ->method('getRemoteFilesystem') 1001 | ->willReturn($rfs); 1002 | 1003 | // Call addKey 1004 | $plugin = new Plugin(); 1005 | $plugin->addKey($event); 1006 | } 1007 | 1008 | public function testOnlyAddKeyOnAcfUrl() 1009 | { 1010 | // Make key available in the ENVIRONMENT 1011 | putenv(self::KEY_ENV_VARIABLE . '=KEY'); 1012 | 1013 | // Mock an Event 1014 | $event = $this 1015 | ->getMockBuilder('Composer\Plugin\PreFileDownloadEvent') 1016 | ->disableOriginalConstructor() 1017 | ->setMethods([ 1018 | 'getProcessedUrl', 1019 | 'getRemoteFilesystem', 1020 | 'setRemoteFilesystem' 1021 | ]) 1022 | ->getMock(); 1023 | 1024 | $event 1025 | ->expects($this->once()) 1026 | ->method('getProcessedUrl') 1027 | ->willReturn('another-url'); 1028 | 1029 | $event 1030 | ->expects($this->never()) 1031 | ->method('getRemoteFilesystem'); 1032 | 1033 | $event 1034 | ->expects($this->never()) 1035 | ->method('setRemoteFilesystem'); 1036 | 1037 | // Call addKey 1038 | $plugin = new Plugin(); 1039 | $plugin->addKey($event); 1040 | } 1041 | } 1042 | --------------------------------------------------------------------------------