├── .actrc ├── .distignore ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml └── workflows │ ├── code-quality.yml │ ├── regenerate-readme.yml │ └── testing.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── behat.yml ├── composer.json ├── features ├── package-install.feature ├── package-update.feature └── package.feature ├── package-command.php ├── phpcs.xml.dist ├── phpunit.xml.dist ├── src ├── Package_Command.php └── WP_CLI │ ├── JsonManipulator.php │ └── Package │ ├── Compat │ ├── Min_Composer_1_10 │ │ └── NullIOMethodsTrait.php │ ├── Min_Composer_2_3 │ │ └── NullIOMethodsTrait.php │ └── NullIOMethodsTrait.php │ └── ComposerIO.php ├── tests ├── ComposerJsonTest.php └── JsonManipulatorTest.php └── wp-cli.yml /.actrc: -------------------------------------------------------------------------------- 1 | # Configuration file for nektos/act. 2 | # See https://github.com/nektos/act#configuration 3 | -P ubuntu-latest=shivammathur/node:latest 4 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | .gitignore 4 | .gitlab-ci.yml 5 | .editorconfig 6 | .travis.yml 7 | behat.yml 8 | circle.yml 9 | phpcs.xml.dist 10 | phpunit.xml.dist 11 | bin/ 12 | features/ 13 | utils/ 14 | *.zip 15 | *.tar.gz 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | # From https://github.com/WordPress/wordpress-develop/blob/trunk/.editorconfig with a couple of additions. 8 | 9 | root = true 10 | 11 | [*] 12 | charset = utf-8 13 | end_of_line = lf 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | indent_style = tab 17 | 18 | [{*.yml,*.feature,.jshintrc,*.json}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [{*.txt,wp-config-sample.php}] 26 | end_of_line = crlf 27 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @wp-cli/committers 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - scope:distribution 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | open-pull-requests-limit: 10 15 | labels: 16 | - scope:distribution 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | 10 | jobs: 11 | code-quality: 12 | uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/regenerate-readme.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate README file 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | paths-ignore: 10 | - "features/**" 11 | - "README.md" 12 | 13 | jobs: 14 | regenerate-readme: 15 | uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | - master 10 | schedule: 11 | - cron: '17 1 * * *' # Run every day on a seemly random time. 12 | 13 | jobs: 14 | test: 15 | uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | wp-cli.local.yml 3 | node_modules/ 4 | vendor/ 5 | *.zip 6 | *.tar.gz 7 | composer.lock 8 | *.log 9 | phpunit.xml 10 | phpcs.xml 11 | .phpcs.xml 12 | .phpunit.cache 13 | .phpunit.result.cache 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We appreciate you taking the initiative to contribute to this project. 5 | 6 | Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. 7 | 8 | For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2011-2018 WP-CLI Development Group (https://github.com/wp-cli/package-command/contributors) 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 | wp-cli/package-command 2 | ====================== 3 | 4 | Lists, installs, and removes WP-CLI packages. 5 | 6 | [![Testing](https://github.com/wp-cli/package-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/package-command/actions/workflows/testing.yml) 7 | 8 | Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) 9 | 10 | ## Using 11 | 12 | This package implements the following commands: 13 | 14 | ### wp package 15 | 16 | Lists, installs, and removes WP-CLI packages. 17 | 18 | ~~~ 19 | wp package 20 | ~~~ 21 | 22 | WP-CLI packages are community-maintained projects built on WP-CLI. They can 23 | contain WP-CLI commands, but they can also just extend WP-CLI in some way. 24 | 25 | Learn how to create your own command from the 26 | [Commands Cookbook](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/) 27 | 28 | **EXAMPLES** 29 | 30 | # List installed packages. 31 | $ wp package list 32 | +-----------------------+------------------+----------+-----------+----------------+ 33 | | name | authors | version | update | update_version | 34 | +-----------------------+------------------+----------+-----------+----------------+ 35 | | wp-cli/server-command | Daniel Bachhuber | dev-main | available | 2.x-dev | 36 | +-----------------------+------------------+----------+-----------+----------------+ 37 | 38 | # Install the latest development version of the package. 39 | $ wp package install wp-cli/server-command 40 | Installing package wp-cli/server-command (dev-main) 41 | Updating /home/person/.wp-cli/packages/composer.json to require the package... 42 | Using Composer to install the package... 43 | --- 44 | Loading composer repositories with package information 45 | Updating dependencies 46 | Resolving dependencies through SAT 47 | Dependency resolution completed in 0.005 seconds 48 | Analyzed 732 packages to resolve dependencies 49 | Analyzed 1034 rules to resolve dependencies 50 | - Installing package 51 | Writing lock file 52 | Generating autoload files 53 | --- 54 | Success: Package installed. 55 | 56 | # Uninstall package. 57 | $ wp package uninstall wp-cli/server-command 58 | Removing require statement for package 'wp-cli/server-command' from /home/person/.wp-cli/packages/composer.json 59 | Removing repository details from /home/person/.wp-cli/packages/composer.json 60 | Removing package directories and regenerating autoloader... 61 | Success: Uninstalled package. 62 | 63 | 64 | 65 | ### wp package browse 66 | 67 | Browses WP-CLI packages available for installation. 68 | 69 | ~~~ 70 | wp package browse [--fields=] [--format=] 71 | ~~~ 72 | 73 | Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/). 74 | Although the package index will remain in place for backward compatibility reasons, it has been 75 | deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 76 | to read about its potential replacement. 77 | 78 | **OPTIONS** 79 | 80 | [--fields=] 81 | Limit the output to specific fields. Defaults to all fields. 82 | 83 | [--format=] 84 | Render output in a particular format. 85 | --- 86 | default: table 87 | options: 88 | - table 89 | - csv 90 | - ids 91 | - json 92 | - yaml 93 | --- 94 | 95 | **AVAILABLE FIELDS** 96 | 97 | These fields will be displayed by default for each package: 98 | 99 | * name 100 | * description 101 | * authors 102 | * version 103 | 104 | There are no optionally available fields. 105 | 106 | **EXAMPLES** 107 | 108 | $ wp package browse --format=yaml 109 | --- 110 | 10up/mu-migration: 111 | name: 10up/mu-migration 112 | description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite 113 | authors: Nícholas André 114 | version: dev-main, dev-develop 115 | aaemnnosttv/wp-cli-dotenv-command: 116 | name: aaemnnosttv/wp-cli-dotenv-command 117 | description: Dotenv commands for WP-CLI 118 | authors: Evan Mattson 119 | version: v0.1, v0.1-beta.1, v0.2, dev-main, dev-dev, dev-develop, dev-tests/behat 120 | aaemnnosttv/wp-cli-http-command: 121 | name: aaemnnosttv/wp-cli-http-command 122 | description: WP-CLI command for using the WordPress HTTP API 123 | authors: Evan Mattson 124 | version: dev-main 125 | 126 | 127 | 128 | ### wp package install 129 | 130 | Installs a WP-CLI package. 131 | 132 | ~~~ 133 | wp package install [--insecure] 134 | ~~~ 135 | 136 | Packages are required to be a valid Composer package, and can be 137 | specified as: 138 | 139 | * Package name from WP-CLI's package index. 140 | * Git URL accessible by the current shell user. 141 | * Path to a directory on the local machine. 142 | * Local or remote .zip file. 143 | 144 | Packages are installed to `~/.wp-cli/packages/` by default. Use the 145 | `WP_CLI_PACKAGES_DIR` environment variable to provide a custom path. 146 | 147 | When installing a local directory, WP-CLI simply registers a 148 | reference to the directory. If you move or delete the directory, WP-CLI's 149 | reference breaks. 150 | 151 | When installing a .zip file, WP-CLI extracts the package to 152 | `~/.wp-cli/packages/local/`. 153 | 154 | If Github token authorization is required, a GitHub Personal Access Token 155 | (https://github.com/settings/tokens) can be used. The following command 156 | will add a GitHub Personal Access Token to Composer's global configuration: 157 | composer config -g github-oauth.github.com 158 | Once this has been added, the value used for will be used 159 | for future authorization requests. 160 | 161 | **OPTIONS** 162 | 163 | 164 | Name, git URL, directory path, or .zip file for the package to install. 165 | Names can optionally include a version constraint 166 | (e.g. wp-cli/server-command:@stable). 167 | 168 | [--insecure] 169 | Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 170 | 171 | **EXAMPLES** 172 | 173 | # Install a package hosted at a git URL. 174 | $ wp package install runcommand/hook 175 | 176 | # Install the latest stable version. 177 | $ wp package install wp-cli/server-command:@stable 178 | 179 | # Install a package hosted at a GitLab.com URL. 180 | $ wp package install https://gitlab.com/foo/wp-cli-bar-command.git 181 | 182 | # Install a package in a .zip file. 183 | $ wp package install google-sitemap-generator-cli.zip 184 | 185 | 186 | 187 | ### wp package list 188 | 189 | Lists installed WP-CLI packages. 190 | 191 | ~~~ 192 | wp package list [--fields=] [--format=] 193 | ~~~ 194 | 195 | **OPTIONS** 196 | 197 | [--fields=] 198 | Limit the output to specific fields. Defaults to all fields. 199 | 200 | [--format=] 201 | Render output in a particular format. 202 | --- 203 | default: table 204 | options: 205 | - table 206 | - csv 207 | - ids 208 | - json 209 | - yaml 210 | --- 211 | 212 | **AVAILABLE FIELDS** 213 | 214 | These fields will be displayed by default for each package: 215 | 216 | * name 217 | * authors 218 | * version 219 | * update 220 | * update_version 221 | 222 | These fields are optionally available: 223 | 224 | * description 225 | 226 | **EXAMPLES** 227 | 228 | # List installed packages. 229 | $ wp package list 230 | +-----------------------+------------------+----------+-----------+----------------+ 231 | | name | authors | version | update | update_version | 232 | +-----------------------+------------------+----------+-----------+----------------+ 233 | | wp-cli/server-command | Daniel Bachhuber | dev-main | available | 2.x-dev | 234 | +-----------------------+------------------+----------+-----------+----------------+ 235 | 236 | 237 | 238 | ### wp package update 239 | 240 | Updates all installed WP-CLI packages to their latest version. 241 | 242 | ~~~ 243 | wp package update 244 | ~~~ 245 | 246 | **EXAMPLES** 247 | 248 | $ wp package update 249 | Using Composer to update packages... 250 | --- 251 | Loading composer repositories with package information 252 | Updating dependencies 253 | Resolving dependencies through SAT 254 | Dependency resolution completed in 0.074 seconds 255 | Analyzed 1062 packages to resolve dependencies 256 | Analyzed 22383 rules to resolve dependencies 257 | Writing lock file 258 | Generating autoload files 259 | --- 260 | Success: Packages updated. 261 | 262 | 263 | 264 | ### wp package uninstall 265 | 266 | Uninstalls a WP-CLI package. 267 | 268 | ~~~ 269 | wp package uninstall [--insecure] 270 | ~~~ 271 | 272 | **OPTIONS** 273 | 274 | 275 | Name of the package to uninstall. 276 | 277 | [--insecure] 278 | Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 279 | 280 | **EXAMPLES** 281 | 282 | # Uninstall package. 283 | $ wp package uninstall wp-cli/server-command 284 | Removing require statement for package 'wp-cli/server-command' from /home/person/.wp-cli/packages/composer.json 285 | Removing repository details from /home/person/.wp-cli/packages/composer.json 286 | Removing package directories and regenerating autoloader... 287 | Success: Uninstalled package. 288 | 289 | ## Installing 290 | 291 | This package is included with WP-CLI itself, no additional installation necessary. 292 | 293 | To install the latest version of this package over what's included in WP-CLI, run: 294 | 295 | wp package install git@github.com:wp-cli/package-command.git 296 | 297 | ## Contributing 298 | 299 | We appreciate you taking the initiative to contribute to this project. 300 | 301 | Contributing isn’t limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation. 302 | 303 | For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines. 304 | 305 | ### Reporting a bug 306 | 307 | Think you’ve found a bug? We’d love for you to help us get it fixed. 308 | 309 | Before you create a new issue, you should [search existing issues](https://github.com/wp-cli/package-command/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version. 310 | 311 | Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/wp-cli/package-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/). 312 | 313 | ### Creating a pull request 314 | 315 | Want to contribute a new feature? Please first [open a new issue](https://github.com/wp-cli/package-command/issues/new) to discuss whether the feature is a good fit for the project. 316 | 317 | Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. 318 | 319 | ## Support 320 | 321 | GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support 322 | 323 | 324 | *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.* 325 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - WP_CLI\Tests\Context\FeatureContext 6 | paths: 7 | - features 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-cli/package-command", 3 | "type": "wp-cli-package", 4 | "description": "Lists, installs, and removes WP-CLI packages.", 5 | "homepage": "https://github.com/wp-cli/package-command", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Daniel Bachhuber", 10 | "email": "daniel@runcommand.io", 11 | "homepage": "https://runcommand.io" 12 | } 13 | ], 14 | "require": { 15 | "ext-json": "*", 16 | "composer/composer": "^2.2.25", 17 | "wp-cli/wp-cli": "^2.12" 18 | }, 19 | "require-dev": { 20 | "wp-cli/scaffold-command": "^1 || ^2", 21 | "wp-cli/wp-cli-tests": "^4" 22 | }, 23 | "config": { 24 | "process-timeout": 7200, 25 | "sort-packages": true, 26 | "allow-plugins": { 27 | "dealerdirect/phpcodesniffer-composer-installer": true, 28 | "johnpbloch/wordpress-core-installer": true 29 | }, 30 | "lock": false 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-main": "2.x-dev" 35 | }, 36 | "bundled": true, 37 | "commands": [ 38 | "package", 39 | "package browse", 40 | "package install", 41 | "package list", 42 | "package update", 43 | "package uninstall" 44 | ] 45 | }, 46 | "autoload": { 47 | "classmap": [ 48 | "src/" 49 | ], 50 | "files": [ 51 | "package-command.php" 52 | ] 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true, 56 | "scripts": { 57 | "behat": "run-behat-tests", 58 | "behat-rerun": "rerun-behat-tests", 59 | "lint": "run-linter-tests", 60 | "phpcs": "run-phpcs-tests", 61 | "phpcbf": "run-phpcbf-cleanup", 62 | "phpunit": "run-php-unit-tests", 63 | "prepare-tests": "install-package-tests", 64 | "test": [ 65 | "@lint", 66 | "@phpcs", 67 | "@phpunit", 68 | "@behat" 69 | ] 70 | }, 71 | "support": { 72 | "issues": "https://github.com/wp-cli/package-command/issues" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /features/package-install.feature: -------------------------------------------------------------------------------- 1 | Feature: Install WP-CLI packages 2 | 3 | Background: 4 | When I run `wp package path` 5 | Then save STDOUT as {PACKAGE_PATH} 6 | 7 | Scenario: Install a package with an http package index url in package composer.json 8 | Given an empty directory 9 | And a composer.json file: 10 | """ 11 | { 12 | "repositories": { 13 | "test" : { 14 | "type": "path", 15 | "url": "./dummy-package/" 16 | }, 17 | "wp-cli": { 18 | "type": "composer", 19 | "url": "http://wp-cli.org/package-index/" 20 | } 21 | } 22 | } 23 | """ 24 | And a dummy-package/composer.json file: 25 | """ 26 | { 27 | "name": "wp-cli/restful", 28 | "description": "This is a dummy package we will install instead of actually installing the real package. This prevents the test from hanging indefinitely for some reason, even though it passes. The 'name' must match a real package as it is checked against the package index." 29 | } 30 | """ 31 | When I run `WP_CLI_PACKAGES_DIR=. wp package install wp-cli/restful` 32 | Then STDOUT should contain: 33 | """ 34 | Updating package index repository url... 35 | """ 36 | And STDOUT should contain: 37 | """ 38 | Success: Package installed 39 | """ 40 | And the composer.json file should contain: 41 | """ 42 | "url": "https://wp-cli.org/package-index/" 43 | """ 44 | And the composer.json file should not contain: 45 | """ 46 | "url": "http://wp-cli.org/package-index/" 47 | """ 48 | 49 | @require-php-5.6 50 | Scenario: Install a package with 'wp-cli/wp-cli' as a dependency 51 | Given a WP install 52 | 53 | When I run `wp package install wp-cli-test/test-command:v0.2.0` 54 | Then STDOUT should contain: 55 | """ 56 | Success: Package installed 57 | """ 58 | And STDOUT should not contain: 59 | """ 60 | requires wp-cli/wp-cli 61 | """ 62 | 63 | When I run `wp test-command` 64 | Then STDOUT should contain: 65 | """ 66 | Version C. 67 | """ 68 | 69 | @require-php-5.6 @broken 70 | Scenario: Install a package with a dependency 71 | Given an empty directory 72 | 73 | When I run `wp package install yoast/wp-cli-faker` 74 | Then STDOUT should contain: 75 | """ 76 | Success: Package installed 77 | """ 78 | And the {PACKAGE_PATH}/vendor/yoast directory should contain: 79 | """ 80 | wp-cli-faker 81 | """ 82 | And the {PACKAGE_PATH}/vendor/fzaninotto directory should contain: 83 | """ 84 | faker 85 | """ 86 | 87 | When I run `wp package list --fields=name` 88 | Then STDOUT should be a table containing rows: 89 | | name | 90 | | yoast/wp-cli-faker | 91 | And STDOUT should not contain: 92 | """ 93 | fzaninotto/faker 94 | """ 95 | 96 | When I run `wp package uninstall yoast/wp-cli-faker` 97 | Then STDOUT should contain: 98 | """ 99 | Removing require statement for package 'yoast/wp-cli-faker' from 100 | """ 101 | And STDOUT should contain: 102 | """ 103 | Success: Uninstalled package. 104 | """ 105 | And the {PACKAGE_PATH}/vendor directory should not contain: 106 | """ 107 | yoast 108 | """ 109 | And the {PACKAGE_PATH}/vendor directory should not contain: 110 | """ 111 | fzaninotto 112 | """ 113 | 114 | When I run `wp package list` 115 | Then STDOUT should not contain: 116 | """ 117 | trendwerk/faker 118 | """ 119 | 120 | @github-api 121 | Scenario: Install a package from a Git URL 122 | Given an empty directory 123 | 124 | When I try `wp package install git@github.com:wp-cli-test/repository-name.git` 125 | Then the return code should be 0 126 | And STDERR should contain: 127 | """ 128 | Warning: Package name mismatch...Updating from git name 'wp-cli-test/repository-name' to composer.json name 'wp-cli-test/package-name'. 129 | """ 130 | And STDOUT should contain: 131 | """ 132 | Success: Package installed. 133 | """ 134 | And the {PACKAGE_PATH}composer.json file should contain: 135 | """ 136 | "wp-cli-test/package-name": "dev-master" 137 | """ 138 | 139 | When I try `wp package install git@github.com:wp-cli.git` 140 | Then STDERR should contain: 141 | """ 142 | Error: Couldn't parse package name from expected path '/'. 143 | """ 144 | 145 | When I run `wp package install git@github.com:wp-cli/google-sitemap-generator-cli.git` 146 | Then STDOUT should contain: 147 | """ 148 | Installing package wp-cli/google-sitemap-generator-cli (dev-main) 149 | """ 150 | # This path is sometimes changed on Macs to prefix with /private 151 | And STDOUT should contain: 152 | """ 153 | {PACKAGE_PATH}composer.json to require the package... 154 | """ 155 | And STDOUT should contain: 156 | """ 157 | Registering git@github.com:wp-cli/google-sitemap-generator-cli.git as a VCS repository... 158 | Using Composer to install the package... 159 | """ 160 | And STDOUT should contain: 161 | """ 162 | Success: Package installed. 163 | """ 164 | 165 | When I run `wp package list --fields=name` 166 | Then STDOUT should be a table containing rows: 167 | | name | 168 | | wp-cli/google-sitemap-generator-cli | 169 | 170 | When I run `wp google-sitemap` 171 | Then STDOUT should contain: 172 | """ 173 | usage: wp google-sitemap rebuild 174 | """ 175 | 176 | When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` 177 | Then STDOUT should contain: 178 | """ 179 | Removing require statement for package 'wp-cli/google-sitemap-generator-cli' from 180 | """ 181 | And STDOUT should contain: 182 | """ 183 | Removing repository details from 184 | """ 185 | And the {PACKAGE_PATH}composer.json file should not contain: 186 | """ 187 | "wp-cli/google-sitemap-generator-cli": "dev-master" 188 | """ 189 | And the {PACKAGE_PATH}composer.json file should not contain: 190 | """ 191 | "url": "git@github.com:wp-cli/google-sitemap-generator-cli.git" 192 | """ 193 | And STDOUT should contain: 194 | """ 195 | Success: Uninstalled package. 196 | """ 197 | 198 | When I run `wp package list --fields=name` 199 | Then STDOUT should not contain: 200 | """ 201 | wp-cli/google-sitemap-generator-cli 202 | """ 203 | 204 | @github-api 205 | Scenario: Install a package from a Git URL with mixed-case git name but lowercase composer.json name 206 | Given an empty directory 207 | 208 | When I try `wp package install https://github.com/CapitalWPCLI/examplecommand.git` 209 | Then the return code should be 0 210 | And STDERR should contain: 211 | """ 212 | Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'. 213 | """ 214 | And STDOUT should contain: 215 | """ 216 | Installing package capitalwpcli/examplecommand (dev-master) 217 | """ 218 | # This path is sometimes changed on Macs to prefix with /private 219 | And STDOUT should contain: 220 | """ 221 | {PACKAGE_PATH}composer.json to require the package... 222 | """ 223 | And STDOUT should contain: 224 | """ 225 | Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository... 226 | Using Composer to install the package... 227 | """ 228 | And STDOUT should contain: 229 | """ 230 | Success: Package installed. 231 | """ 232 | And the {PACKAGE_PATH}composer.json file should contain: 233 | """ 234 | "capitalwpcli/examplecommand" 235 | """ 236 | And the {PACKAGE_PATH}composer.json file should not contain: 237 | """ 238 | "CapitalWPCLI/examplecommand" 239 | """ 240 | 241 | When I run `wp package list --fields=name` 242 | Then STDOUT should be a table containing rows: 243 | | name | 244 | | capitalwpcli/examplecommand | 245 | 246 | When I run `wp hello-world` 247 | Then STDOUT should contain: 248 | """ 249 | Success: Hello world. 250 | """ 251 | 252 | @github-api 253 | Scenario: Install a package from a Git URL with mixed-case git name and the same mixed-case composer.json name 254 | Given an empty directory 255 | 256 | When I run `wp package install https://github.com/gitlost/TestMixedCaseCommand.git` 257 | Then STDERR should be empty 258 | And STDOUT should contain: 259 | """ 260 | Success: Package installed. 261 | """ 262 | And the contents of the {PACKAGE_PATH}composer.json file should match /\"gitlost\/(?:TestMixedCaseCommand|testmixedcasecommand)\"/ 263 | 264 | When I run `wp package list --fields=name` 265 | Then STDOUT should be a table containing rows: 266 | | name | 267 | | gitlost/TestMixedCaseCommand | 268 | 269 | When I run `wp TestMixedCaseCommand` 270 | Then STDOUT should contain: 271 | """ 272 | Success: Test Mixed Case Command Name 273 | """ 274 | 275 | @github-api @shortened 276 | Scenario: Install a package from Git using a shortened package identifier 277 | Given an empty directory 278 | 279 | When I run `wp package install wp-cli-test/github-test-command` 280 | Then STDOUT should contain: 281 | """ 282 | Installing package wp-cli-test/github-test-command (dev-master) 283 | """ 284 | # This path is sometimes changed on Macs to prefix with /private 285 | And STDOUT should contain: 286 | """ 287 | {PACKAGE_PATH}composer.json to require the package... 288 | """ 289 | And STDOUT should contain: 290 | """ 291 | Registering https://github.com/wp-cli-test/github-test-command.git as a VCS repository... 292 | Using Composer to install the package... 293 | """ 294 | And STDOUT should contain: 295 | """ 296 | Success: Package installed. 297 | """ 298 | 299 | When I run `wp package list --fields=name,version` 300 | Then STDOUT should be a table containing rows: 301 | | name | version | 302 | | wp-cli-test/github-test-command | dev-master | 303 | 304 | When I run `wp test-command` 305 | Then STDOUT should contain: 306 | """ 307 | Success: Version E. 308 | """ 309 | 310 | When I run `wp package uninstall wp-cli-test/github-test-command` 311 | Then STDOUT should contain: 312 | """ 313 | Removing require statement for package 'wp-cli-test/github-test-command' from 314 | """ 315 | And STDOUT should contain: 316 | """ 317 | Success: Uninstalled package. 318 | """ 319 | 320 | When I run `wp package list --fields=name` 321 | Then STDOUT should not contain: 322 | """ 323 | wp-cli-test/github-test-command 324 | """ 325 | 326 | @github-api @shortened 327 | Scenario: Install a package from Git using a shortened package identifier with a version requirement 328 | Given an empty directory 329 | 330 | When I try `wp package install wp-cli-test/github-test-command:^0` 331 | Then STDOUT should contain: 332 | """ 333 | Installing package wp-cli-test/github-test-command (^0) 334 | """ 335 | # This path is sometimes changed on Macs to prefix with /private 336 | And STDOUT should contain: 337 | """ 338 | {PACKAGE_PATH}composer.json to require the package... 339 | """ 340 | And STDOUT should contain: 341 | """ 342 | Registering https://github.com/wp-cli-test/github-test-command.git as a VCS repository... 343 | Using Composer to install the package... 344 | """ 345 | And STDOUT should contain: 346 | """ 347 | Success: Package installed. 348 | """ 349 | 350 | When I run `wp package list --fields=name,version` 351 | Then STDOUT should be a table containing rows: 352 | | name | version | 353 | | wp-cli-test/github-test-command | v0.2.0 | 354 | 355 | When I run `wp test-command` 356 | Then STDOUT should contain: 357 | """ 358 | Success: Version C. 359 | """ 360 | 361 | When I run `wp package uninstall wp-cli-test/github-test-command` 362 | Then STDOUT should contain: 363 | """ 364 | Removing require statement for package 'wp-cli-test/github-test-command' from 365 | """ 366 | And STDOUT should contain: 367 | """ 368 | Success: Uninstalled package. 369 | """ 370 | 371 | When I run `wp package list --fields=name` 372 | Then STDOUT should not contain: 373 | """ 374 | wp-cli-test/github-test-command 375 | """ 376 | 377 | @github-api @shortened 378 | Scenario: Install a package from Git using a shortened package identifier with a specific version 379 | Given an empty directory 380 | 381 | # Need to specify actual tag. 382 | When I try `wp package install wp-cli-test/github-test-command:0.1.0` 383 | Then STDERR should contain: 384 | """ 385 | Warning: Couldn't download composer.json file from 'https://raw.githubusercontent.com/wp-cli-test/github-test-command/0.1.0/composer.json' (HTTP code 404). 386 | """ 387 | 388 | When I run `wp package install wp-cli-test/github-test-command:v0.1.0` 389 | Then STDOUT should contain: 390 | """ 391 | Installing package wp-cli-test/github-test-command (v0.1.0) 392 | """ 393 | # This path is sometimes changed on Macs to prefix with /private 394 | And STDOUT should contain: 395 | """ 396 | {PACKAGE_PATH}composer.json to require the package... 397 | """ 398 | And STDOUT should contain: 399 | """ 400 | Registering https://github.com/wp-cli-test/github-test-command.git as a VCS repository... 401 | Using Composer to install the package... 402 | """ 403 | And STDOUT should contain: 404 | """ 405 | Success: Package installed. 406 | """ 407 | 408 | When I run `wp package list --fields=name,version` 409 | Then STDOUT should be a table containing rows: 410 | | name | version | 411 | | wp-cli-test/github-test-command | v0.1.0 | 412 | 413 | When I run `wp test-command` 414 | Then STDOUT should contain: 415 | """ 416 | Success: Version A. 417 | """ 418 | 419 | When I run `wp package uninstall wp-cli-test/github-test-command` 420 | Then STDOUT should contain: 421 | """ 422 | Removing require statement for package 'wp-cli-test/github-test-command' from 423 | """ 424 | And STDOUT should contain: 425 | """ 426 | Success: Uninstalled package. 427 | """ 428 | 429 | When I run `wp package list --fields=name` 430 | Then STDOUT should not contain: 431 | """ 432 | wp-cli-test/github-test-command 433 | """ 434 | 435 | @github-api @shortened 436 | Scenario: Install a package from Git using a shortened package identifier and a specific commit hash 437 | Given an empty directory 438 | 439 | When I run `wp package install wp-cli-test/github-test-command:dev-master#bcfac95e2193e9f5f8fbd3004fab9d902a5e4de3` 440 | Then STDOUT should contain: 441 | """ 442 | Installing package wp-cli-test/github-test-command (dev-master#bcfac95e2193e9f5f8fbd3004fab9d902a5e4de3) 443 | """ 444 | # This path is sometimes changed on Macs to prefix with /private 445 | And STDOUT should contain: 446 | """ 447 | {PACKAGE_PATH}composer.json to require the package... 448 | """ 449 | And STDOUT should contain: 450 | """ 451 | Registering https://github.com/wp-cli-test/github-test-command.git as a VCS repository... 452 | Using Composer to install the package... 453 | """ 454 | And STDOUT should contain: 455 | """ 456 | Success: Package installed. 457 | """ 458 | 459 | When I run `wp package list --fields=name,version` 460 | Then STDOUT should be a table containing rows: 461 | | name | version | 462 | | wp-cli-test/github-test-command | dev-master | 463 | 464 | When I run `wp test-command` 465 | Then STDOUT should contain: 466 | """ 467 | Success: Version B. 468 | """ 469 | 470 | When I run `wp package uninstall wp-cli-test/github-test-command` 471 | Then STDOUT should contain: 472 | """ 473 | Removing require statement for package 'wp-cli-test/github-test-command' from 474 | """ 475 | And STDOUT should contain: 476 | """ 477 | Success: Uninstalled package. 478 | """ 479 | 480 | When I run `wp package list --fields=name` 481 | Then STDOUT should not contain: 482 | """ 483 | wp-cli-test/github-test-command 484 | """ 485 | 486 | @github-api @shortened 487 | Scenario: Install a package from Git using a shortened package identifier and a branch 488 | Given an empty directory 489 | 490 | When I run `wp package install wp-cli-test/github-test-command:dev-custom-branch` 491 | Then STDOUT should contain: 492 | """ 493 | Installing package wp-cli-test/github-test-command (dev-custom-branch) 494 | """ 495 | # This path is sometimes changed on Macs to prefix with /private 496 | And STDOUT should contain: 497 | """ 498 | {PACKAGE_PATH}composer.json to require the package... 499 | """ 500 | And STDOUT should contain: 501 | """ 502 | Registering https://github.com/wp-cli-test/github-test-command.git as a VCS repository... 503 | Using Composer to install the package... 504 | """ 505 | And STDOUT should contain: 506 | """ 507 | Success: Package installed. 508 | """ 509 | 510 | When I run `wp package list --fields=name,version` 511 | Then STDOUT should be a table containing rows: 512 | | name | version | 513 | | wp-cli-test/github-test-command | dev-custom-branch | 514 | 515 | When I run `wp test-command` 516 | Then STDOUT should contain: 517 | """ 518 | Success: Version D. 519 | """ 520 | 521 | When I run `wp package uninstall wp-cli-test/github-test-command` 522 | Then STDOUT should contain: 523 | """ 524 | Removing require statement for package 'wp-cli-test/github-test-command' from 525 | """ 526 | And STDOUT should contain: 527 | """ 528 | Success: Uninstalled package. 529 | """ 530 | 531 | When I run `wp package list --fields=name` 532 | Then STDOUT should not contain: 533 | """ 534 | wp-cli-test/github-test-command 535 | """ 536 | 537 | @github-api 538 | Scenario: Install a package from the wp-cli package index with a mixed-case name 539 | Given an empty directory 540 | 541 | # Install and uninstall with case-sensitive name 542 | When I try `wp package install GeekPress/wp-rocket-cli` 543 | Then STDERR should contain: 544 | """ 545 | Warning: Package name mismatch...Updating from git name 'GeekPress/wp-rocket-cli' to composer.json name 'wp-media/wp-rocket-cli'. 546 | """ 547 | And STDOUT should match /Installing package wp-media\/wp-rocket-cli \(dev-/ 548 | # This path is sometimes changed on Macs to prefix with /private 549 | And STDOUT should contain: 550 | """ 551 | {PACKAGE_PATH}composer.json to require the package... 552 | """ 553 | And STDOUT should contain: 554 | """ 555 | Using Composer to install the package... 556 | """ 557 | And STDOUT should contain: 558 | """ 559 | Success: Package installed. 560 | """ 561 | And the contents of the {PACKAGE_PATH}composer.json file should match /"wp-media\/wp-rocket-cli"/ 562 | 563 | When I run `wp package list --fields=name` 564 | Then STDOUT should be a table containing rows: 565 | | name | 566 | | wp-media/wp-rocket-cli | 567 | 568 | When I run `wp help rocket` 569 | Then STDOUT should contain: 570 | """ 571 | wp rocket 572 | """ 573 | 574 | When I try `wp package uninstall GeekPress/wp-rocket-cli` 575 | Then STDOUT should contain: 576 | """ 577 | Removing require statement for package 'wp-media/wp-rocket-cli' from 578 | """ 579 | And STDOUT should contain: 580 | """ 581 | Success: Uninstalled package. 582 | """ 583 | And STDERR should contain: 584 | """ 585 | Warning: Package name mismatch...Updating from git name 'GeekPress/wp-rocket-cli' to composer.json name 'wp-media/wp-rocket-cli'. 586 | """ 587 | And the {PACKAGE_PATH}composer.json file should not contain: 588 | """ 589 | rocket 590 | """ 591 | 592 | # Install with lowercase name (for BC - no warning) and uninstall with lowercase name (for BC and convenience) 593 | When I run `wp package install geekpress/wp-rocket-cli` 594 | Then STDERR should be empty 595 | And STDOUT should match /Installing package (?:GeekPress|geekpress)\/wp-rocket-cli \(dev-/ 596 | # This path is sometimes changed on Macs to prefix with /private 597 | And STDOUT should contain: 598 | """ 599 | {PACKAGE_PATH}composer.json to require the package... 600 | """ 601 | And STDOUT should contain: 602 | """ 603 | Using Composer to install the package... 604 | """ 605 | And STDOUT should contain: 606 | """ 607 | Success: Package installed. 608 | """ 609 | And the contents of the {PACKAGE_PATH}composer.json file should match /("?:GeekPress|geekpress)\/wp-rocket-cli"/ 610 | 611 | When I run `wp package list --fields=name` 612 | Then STDOUT should be a table containing rows: 613 | | name | 614 | | geekpress/wp-rocket-cli | 615 | 616 | When I run `wp help rocket` 617 | Then STDOUT should contain: 618 | """ 619 | wp rocket 620 | """ 621 | 622 | When I run `wp package uninstall geekpress/wp-rocket-cli` 623 | Then STDOUT should contain: 624 | """ 625 | Removing require statement for package 'geekpress/wp-rocket-cli' from 626 | """ 627 | And STDOUT should contain: 628 | """ 629 | Success: Uninstalled package. 630 | """ 631 | And the {PACKAGE_PATH}composer.json file should not contain: 632 | """ 633 | rocket 634 | """ 635 | 636 | @github-api 637 | Scenario: Install a package with a composer.json that differs between versions 638 | Given an empty directory 639 | 640 | When I run `wp package install wp-cli-test/version-composer-json-different:v1.0.0` 641 | Then STDOUT should contain: 642 | """ 643 | Installing package wp-cli-test/version-composer-json-different (v1.0.0) 644 | """ 645 | # This path is sometimes changed on Macs to prefix with /private 646 | And STDOUT should contain: 647 | """ 648 | {PACKAGE_PATH}composer.json to require the package... 649 | """ 650 | And STDOUT should contain: 651 | """ 652 | Success: Package installed. 653 | """ 654 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should exist 655 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should contain: 656 | """ 657 | 1.0.0 658 | """ 659 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should not contain: 660 | """ 661 | 1.0.1 662 | """ 663 | And the {PACKAGE_PATH}/vendor/wp-cli/profile-command directory should not exist 664 | 665 | When I run `wp package install wp-cli-test/version-composer-json-different:v1.0.1` 666 | Then STDOUT should contain: 667 | """ 668 | Installing package wp-cli-test/version-composer-json-different (v1.0.1) 669 | """ 670 | # This path is sometimes changed on Macs to prefix with /private 671 | And STDOUT should contain: 672 | """ 673 | {PACKAGE_PATH}composer.json to require the package... 674 | """ 675 | And STDOUT should contain: 676 | """ 677 | Success: Package installed. 678 | """ 679 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should exist 680 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should contain: 681 | """ 682 | 1.0.1 683 | """ 684 | And the {PACKAGE_PATH}/vendor/wp-cli-test/version-composer-json-different/composer.json file should not contain: 685 | """ 686 | 1.0.0 687 | """ 688 | And the {PACKAGE_PATH}/vendor/wp-cli/profile-command directory should exist 689 | 690 | Scenario: Install a package from a local zip 691 | Given an empty directory 692 | And I run `wget -q -O google-sitemap-generator-cli.zip https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip` 693 | 694 | When I run `wp package install google-sitemap-generator-cli.zip` 695 | Then STDOUT should contain: 696 | """ 697 | Installing package wp-cli/google-sitemap-generator-cli 698 | """ 699 | # This path is sometimes changed on Macs to prefix with /private 700 | And STDOUT should contain: 701 | """ 702 | {PACKAGE_PATH}composer.json to require the package... 703 | """ 704 | And STDOUT should contain: 705 | """ 706 | Registering {PACKAGE_PATH}local/wp-cli-google-sitemap-generator-cli as a path repository... 707 | Using Composer to install the package... 708 | """ 709 | And STDOUT should contain: 710 | """ 711 | Success: Package installed. 712 | """ 713 | 714 | When I run `wp package list --fields=name` 715 | Then STDOUT should be a table containing rows: 716 | | name | 717 | | wp-cli/google-sitemap-generator-cli | 718 | 719 | When I run `wp google-sitemap` 720 | Then STDOUT should contain: 721 | """ 722 | usage: wp google-sitemap rebuild 723 | """ 724 | 725 | When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` 726 | Then STDOUT should contain: 727 | """ 728 | Removing require statement for package 'wp-cli/google-sitemap-generator-cli' from 729 | """ 730 | And STDOUT should contain: 731 | """ 732 | Success: Uninstalled package. 733 | """ 734 | 735 | When I run `wp package list --fields=name` 736 | Then STDOUT should not contain: 737 | """ 738 | wp-cli/google-sitemap-generator-cli 739 | """ 740 | 741 | @github-api 742 | Scenario: Install a package from Git using a shortened mixed-case package identifier but lowercase composer.json name 743 | Given an empty directory 744 | 745 | When I try `wp package install CapitalWPCLI/examplecommand` 746 | Then the return code should be 0 747 | And STDERR should contain: 748 | """ 749 | Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'. 750 | """ 751 | And STDOUT should contain: 752 | """ 753 | Installing package capitalwpcli/examplecommand (dev-master) 754 | """ 755 | # This path is sometimes changed on Macs to prefix with /private 756 | And STDOUT should contain: 757 | """ 758 | {PACKAGE_PATH}composer.json to require the package... 759 | """ 760 | And STDOUT should contain: 761 | """ 762 | Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository... 763 | Using Composer to install the package... 764 | """ 765 | And STDOUT should contain: 766 | """ 767 | Success: Package installed. 768 | """ 769 | And the {PACKAGE_PATH}composer.json file should contain: 770 | """ 771 | "capitalwpcli/examplecommand" 772 | """ 773 | And the {PACKAGE_PATH}composer.json file should not contain: 774 | """ 775 | "CapitalWPCLI/examplecommand" 776 | """ 777 | 778 | When I run `wp package list --fields=name` 779 | Then STDOUT should be a table containing rows: 780 | | name | 781 | | capitalwpcli/examplecommand | 782 | 783 | When I run `wp hello-world` 784 | Then STDOUT should contain: 785 | """ 786 | Success: Hello world. 787 | """ 788 | 789 | When I run `wp package uninstall capitalwpcli/examplecommand` 790 | Then STDOUT should contain: 791 | """ 792 | Removing require statement for package 'capitalwpcli/examplecommand' from 793 | """ 794 | And STDOUT should contain: 795 | """ 796 | Success: Uninstalled package. 797 | """ 798 | And the {PACKAGE_PATH}composer.json file should not contain: 799 | """ 800 | capital 801 | """ 802 | 803 | @github-api 804 | Scenario: Install a package from a remote ZIP 805 | Given an empty directory 806 | 807 | When I try `wp package install https://github.com/wp-cli/google-sitemap-generator.zip` 808 | Then STDERR should be: 809 | """ 810 | Error: Couldn't download package from 'https://github.com/wp-cli/google-sitemap-generator.zip' (HTTP code 404). 811 | """ 812 | 813 | When I run `wp package install https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip` 814 | Then STDOUT should contain: 815 | """ 816 | Installing package wp-cli/google-sitemap-generator-cli (dev- 817 | """ 818 | # This path is sometimes changed on Macs to prefix with /private 819 | And STDOUT should contain: 820 | """ 821 | {PACKAGE_PATH}composer.json to require the package... 822 | """ 823 | And STDOUT should contain: 824 | """ 825 | Registering {PACKAGE_PATH}local/wp-cli-google-sitemap-generator-cli as a path repository... 826 | Using Composer to install the package... 827 | """ 828 | And STDOUT should contain: 829 | """ 830 | Success: Package installed. 831 | """ 832 | 833 | When I run `wp package list --fields=name` 834 | Then STDOUT should be a table containing rows: 835 | | name | 836 | | wp-cli/google-sitemap-generator-cli | 837 | 838 | When I run `wp google-sitemap` 839 | Then STDOUT should contain: 840 | """ 841 | usage: wp google-sitemap rebuild 842 | """ 843 | 844 | When I run `wp package uninstall wp-cli/google-sitemap-generator-cli` 845 | Then STDOUT should contain: 846 | """ 847 | Removing require statement for package 'wp-cli/google-sitemap-generator-cli' from 848 | """ 849 | And STDOUT should contain: 850 | """ 851 | Success: Uninstalled package. 852 | """ 853 | 854 | When I run `wp package list --fields=name` 855 | Then STDOUT should not contain: 856 | """ 857 | wp-cli/google-sitemap-generator-cli 858 | """ 859 | 860 | @gitlab-api 861 | Scenario: Install a package from a GitLab URL 862 | Given an empty directory 863 | 864 | When I try `wp package install https://gitlab.com/gitlab-examples/php.git` 865 | Then the return code should be 1 866 | And STDERR should contain: 867 | """ 868 | Error: Invalid package: no name in composer.json file 'https://gitlab.com/gitlab-examples/php/-/raw/master/composer.json'. 869 | """ 870 | 871 | Scenario: Install a package at an existing path 872 | Given an empty directory 873 | And a path-command/command.php file: 874 | """ 875 | 'before_wp_load' ) ); 879 | """ 880 | And a path-command/composer.json file: 881 | """ 882 | { 883 | "name": "wp-cli/community-command", 884 | "description": "A demo community command.", 885 | "license": "MIT", 886 | "minimum-stability": "dev", 887 | "require": { 888 | }, 889 | "autoload": { 890 | "files": [ "command.php" ] 891 | }, 892 | "require-dev": { 893 | "behat/behat": "~2.5" 894 | } 895 | } 896 | """ 897 | 898 | When I run `pwd` 899 | Then save STDOUT as {CURRENT_PATH} 900 | 901 | When I run `wp package install path-command` 902 | Then STDOUT should contain: 903 | """ 904 | Installing package wp-cli/community-command 905 | """ 906 | # This path is sometimes changed on Macs to prefix with /private 907 | And STDOUT should contain: 908 | """ 909 | {PACKAGE_PATH}composer.json to require the package... 910 | """ 911 | And STDOUT should contain: 912 | """ 913 | Registering {CURRENT_PATH}/path-command as a path repository... 914 | Using Composer to install the package... 915 | """ 916 | And STDOUT should contain: 917 | """ 918 | Success: Package installed. 919 | """ 920 | 921 | When I run `wp package list --fields=name` 922 | Then STDOUT should be a table containing rows: 923 | | name | 924 | | wp-cli/community-command | 925 | 926 | When I run `wp community-command` 927 | Then STDOUT should be: 928 | """ 929 | Success: success! 930 | """ 931 | 932 | When I run `wp package uninstall wp-cli/community-command` 933 | Then STDOUT should contain: 934 | """ 935 | Removing require statement for package 'wp-cli/community-command' from 936 | """ 937 | And STDOUT should contain: 938 | """ 939 | Success: Uninstalled package. 940 | """ 941 | And the path-command directory should exist 942 | 943 | When I run `wp package list --fields=name` 944 | Then STDOUT should not contain: 945 | """ 946 | wp-cli/community-command 947 | """ 948 | 949 | Scenario: Install a package at an existing path with a version constraint 950 | Given an empty directory 951 | And a path-command/command.php file: 952 | """ 953 | 'before_wp_load' ) ); 957 | """ 958 | And a path-command/composer.json file: 959 | """ 960 | { 961 | "name": "wp-cli/community-command", 962 | "description": "A demo community command.", 963 | "license": "MIT", 964 | "minimum-stability": "dev", 965 | "version": "0.2.0-beta", 966 | "require": { 967 | }, 968 | "autoload": { 969 | "files": [ "command.php" ] 970 | }, 971 | "require-dev": { 972 | "behat/behat": "~2.5" 973 | } 974 | } 975 | """ 976 | 977 | When I run `pwd` 978 | Then save STDOUT as {CURRENT_PATH} 979 | 980 | When I run `wp package install path-command` 981 | Then STDOUT should contain: 982 | """ 983 | Installing package wp-cli/community-command (0.2.0-beta) 984 | """ 985 | # This path is sometimes changed on Macs to prefix with /private 986 | And STDOUT should contain: 987 | """ 988 | {PACKAGE_PATH}composer.json to require the package... 989 | """ 990 | And STDOUT should contain: 991 | """ 992 | Registering {CURRENT_PATH}/path-command as a path repository... 993 | Using Composer to install the package... 994 | """ 995 | And STDOUT should contain: 996 | """ 997 | Success: Package installed. 998 | """ 999 | 1000 | When I run `wp package list --fields=name` 1001 | Then STDOUT should be a table containing rows: 1002 | | name | 1003 | | wp-cli/community-command | 1004 | 1005 | When I run `wp community-command` 1006 | Then STDOUT should be: 1007 | """ 1008 | Success: success! 1009 | """ 1010 | 1011 | When I run `wp package uninstall wp-cli/community-command` 1012 | Then STDOUT should contain: 1013 | """ 1014 | Removing require statement for package 'wp-cli/community-command' from 1015 | """ 1016 | And STDOUT should contain: 1017 | """ 1018 | Success: Uninstalled package. 1019 | """ 1020 | And the path-command directory should exist 1021 | 1022 | When I run `wp package list --fields=name` 1023 | Then STDOUT should not contain: 1024 | """ 1025 | wp-cli/community-command 1026 | """ 1027 | 1028 | Scenario: Try to install bad packages 1029 | Given an empty directory 1030 | And a package-dir/composer.json file: 1031 | """ 1032 | { 1033 | } 1034 | """ 1035 | And a package-dir-bad-composer/composer.json file: 1036 | """ 1037 | { 1038 | """ 1039 | And a package-dir/zero.zip file: 1040 | """ 1041 | """ 1042 | 1043 | When I try `wp package install https://github.com/non-existent-git-user-asdfasdf/non-existent-git-repo-asdfasdf.git` 1044 | Then the return code should be 1 1045 | And STDERR should contain: 1046 | """ 1047 | Warning: Couldn't download composer.json file from 'https://raw.githubusercontent.com/non-existent-git-user-asdfasdf/non-existent-git-repo-asdfasdf/master/composer.json' (HTTP code 404). Presuming package name is 'non-existent-git-user-asdfasdf/non-existent-git-repo-asdfasdf'. 1048 | """ 1049 | 1050 | When I try `wp package install https://github.com/wp-cli-test/private-repository.git` 1051 | Then STDERR should contain: 1052 | """ 1053 | Warning: Couldn't download composer.json file from 'https://raw.githubusercontent.com/wp-cli-test/private-repository/master/composer.json' (HTTP code 404). Presuming package name is 'wp-cli-test/private-repository'. 1054 | """ 1055 | 1056 | When I try `wp package install non-existent-git-user-asdfasdf/non-existent-git-repo-asdfasdf` 1057 | Then the return code should be 1 1058 | And STDERR should be: 1059 | """ 1060 | Error: Invalid package: shortened identifier 'non-existent-git-user-asdfasdf/non-existent-git-repo-asdfasdf' not found. 1061 | """ 1062 | And STDOUT should be empty 1063 | 1064 | When I try `wp package install https://example.com/non-existent-zip-asdfasdf.zip` 1065 | Then the return code should be 1 1066 | And STDERR should contain: 1067 | """ 1068 | Error: Couldn't download package from 'https://example.com/non-existent-zip-asdfasdf.zip' 1069 | """ 1070 | And STDOUT should be empty 1071 | 1072 | When I try `wp package install package-dir-bad-composer` 1073 | Then the return code should be 1 1074 | And STDERR should contain: 1075 | """ 1076 | Error: Invalid package: failed to parse composer.json file 1077 | """ 1078 | # Split string up to get around Mac OS X inconsistencies with RUN_DIR 1079 | And STDERR should contain: 1080 | """ 1081 | /package-dir-bad-composer/composer.json' as json. 1082 | """ 1083 | And STDOUT should be empty 1084 | 1085 | When I try `wp package install package-dir` 1086 | Then the return code should be 1 1087 | And STDERR should contain: 1088 | """ 1089 | Error: Invalid package: no name in composer.json file 1090 | """ 1091 | # Split string up to get around Mac OS X inconsistencies with RUN_DIR 1092 | And STDERR should contain: 1093 | """ 1094 | /package-dir/composer.json'. 1095 | """ 1096 | And STDOUT should be empty 1097 | 1098 | When I try `wp package install package-dir/zero.zip` 1099 | Then the return code should be 1 1100 | And STDERR should be: 1101 | """ 1102 | Error: ZipArchive failed to unzip 'package-dir/zero.zip': Not a zip archive (19). 1103 | """ 1104 | And STDOUT should be empty 1105 | -------------------------------------------------------------------------------- /features/package-update.feature: -------------------------------------------------------------------------------- 1 | Feature: Update WP-CLI packages 2 | 3 | Background: 4 | When I run `wp package path` 5 | Then save STDOUT as {PACKAGE_PATH} 6 | 7 | Scenario: Updating WP-CLI packages runs successfully 8 | Given an empty directory 9 | 10 | When I run `wp package install danielbachhuber/wp-cli-reset-post-date-command` 11 | Then STDOUT should contain: 12 | """ 13 | Success: Package installed. 14 | """ 15 | And STDERR should be empty 16 | 17 | When I run `wp package update` 18 | Then STDOUT should contain: 19 | """ 20 | Using Composer to update packages... 21 | """ 22 | And STDOUT should contain: 23 | """ 24 | Packages updated. 25 | """ 26 | And STDERR should be empty 27 | 28 | Scenario: Update a package with an update available 29 | Given an empty directory 30 | 31 | When I run `wp package install wp-cli-test/updateable-package:v1.0.0` 32 | Then STDOUT should contain: 33 | """ 34 | Installing package wp-cli-test/updateable-package (v1.0.0) 35 | """ 36 | And STDOUT should contain: 37 | """ 38 | Success: Package installed. 39 | """ 40 | 41 | When I run `cat {PACKAGE_PATH}/composer.json` 42 | Then STDOUT should contain: 43 | """ 44 | "wp-cli-test/updateable-package": "v1.0.0" 45 | """ 46 | 47 | When I run `wp help updateable-package v1` 48 | Then STDOUT should contain: 49 | """ 50 | wp updateable-package v1 51 | """ 52 | 53 | When I run `wp package update` 54 | Then STDOUT should match /Nothing to install(?: or update|, update or remove)/ 55 | And STDOUT should contain: 56 | """ 57 | Success: Packages updated. 58 | """ 59 | 60 | When I run `wp package list --fields=name,update` 61 | Then STDOUT should be a table containing rows: 62 | | name | update | 63 | | wp-cli-test/updateable-package | available | 64 | 65 | When I run `sed -i.bak s/v1.0.0/\>=1.0.0/g {PACKAGE_PATH}/composer.json` 66 | Then the return code should be 0 67 | 68 | When I run `cat {PACKAGE_PATH}/composer.json` 69 | Then STDOUT should contain: 70 | """ 71 | "wp-cli-test/updateable-package": ">=1.0.0" 72 | """ 73 | 74 | When I run `wp package list --fields=name,update` 75 | Then STDOUT should be a table containing rows: 76 | | name | update | 77 | | wp-cli-test/updateable-package | available | 78 | 79 | When I run `wp package update` 80 | Then STDOUT should contain: 81 | """ 82 | Writing lock file 83 | """ 84 | And STDOUT should contain: 85 | """ 86 | Success: Packages updated. 87 | """ 88 | And STDOUT should not match /Nothing to install(?: or update|, update or remove)/ 89 | 90 | When I run `wp package list --fields=name,update` 91 | Then STDOUT should be a table containing rows: 92 | | name | update | 93 | | wp-cli-test/updateable-package | none | 94 | 95 | When I run `wp package update` 96 | Then STDOUT should match /Nothing to install(?: or update|, update or remove)/ 97 | And STDOUT should contain: 98 | """ 99 | Success: Packages updated. 100 | """ 101 | -------------------------------------------------------------------------------- /features/package.feature: -------------------------------------------------------------------------------- 1 | Feature: Manage WP-CLI packages 2 | 3 | Scenario: Package CRUD 4 | Given an empty directory 5 | 6 | When I run `wp package browse` 7 | Then STDOUT should contain: 8 | """ 9 | runcommand/hook 10 | """ 11 | 12 | When I run `wp package install runcommand/hook` 13 | Then STDERR should be empty 14 | 15 | When I run `wp help hook` 16 | Then STDERR should be empty 17 | And STDOUT should contain: 18 | """ 19 | List callbacks registered to a given action or filter. 20 | """ 21 | 22 | When I try `wp --skip-packages --debug help hook` 23 | Then STDERR should contain: 24 | """ 25 | Debug (bootstrap): Skipped loading packages. 26 | """ 27 | And STDERR should contain: 28 | """ 29 | Warning: No WordPress install 30 | """ 31 | 32 | When I run `wp package list` 33 | Then STDOUT should contain: 34 | """ 35 | runcommand/hook 36 | """ 37 | 38 | When I run `wp package uninstall runcommand/hook` 39 | Then STDERR should be empty 40 | 41 | When I run `wp package list` 42 | Then STDOUT should not contain: 43 | """ 44 | runcommand/hook 45 | """ 46 | 47 | Scenario: Run package commands early, before any bad code can break them 48 | Given an empty directory 49 | And a bad-command.php file: 50 | """ 51 | 2 | 3 | Custom ruleset for WP-CLI package-command 4 | 5 | 12 | 13 | 14 | . 15 | 16 | 18 | */src/WP_CLI/JsonManipulator\.php$ 19 | */src/WP_CLI/Package/Compat/Min_Composer_1_10/NullIOMethodsTrait\.php$ 20 | */src/WP_CLI/Package/Compat/Min_Composer_2_3/NullIOMethodsTrait\.php$ 21 | */tests/JsonManipulatorTest\.php$ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 45 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | */src/Package_Command\.php$ 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Package_Command.php: -------------------------------------------------------------------------------- 1 | 'WP-CLI', 88 | 'email' => 'noreply@wpcli.org', 89 | ]; 90 | 91 | /** 92 | * Default repository data used while creating default WP-CLI packages composer.json. 93 | * @var array 94 | */ 95 | private $composer_type_package = [ 96 | 'type' => 'composer', 97 | 'url' => self::PACKAGE_INDEX_URL, 98 | ]; 99 | 100 | /** 101 | * Browses WP-CLI packages available for installation. 102 | * 103 | * Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/). 104 | * Although the package index will remain in place for backward compatibility reasons, it has been 105 | * deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 106 | * to read about its potential replacement. 107 | * 108 | * ## OPTIONS 109 | * 110 | * [--fields=] 111 | * : Limit the output to specific fields. Defaults to all fields. 112 | * 113 | * [--format=] 114 | * : Render output in a particular format. 115 | * --- 116 | * default: table 117 | * options: 118 | * - table 119 | * - csv 120 | * - ids 121 | * - json 122 | * - yaml 123 | * --- 124 | * 125 | * ## AVAILABLE FIELDS 126 | * 127 | * These fields will be displayed by default for each package: 128 | * 129 | * * name 130 | * * description 131 | * * authors 132 | * * version 133 | * 134 | * There are no optionally available fields. 135 | * 136 | * ## EXAMPLES 137 | * 138 | * $ wp package browse --format=yaml 139 | * --- 140 | * 10up/mu-migration: 141 | * name: 10up/mu-migration 142 | * description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite 143 | * authors: Nícholas André 144 | * version: dev-main, dev-develop 145 | * aaemnnosttv/wp-cli-dotenv-command: 146 | * name: aaemnnosttv/wp-cli-dotenv-command 147 | * description: Dotenv commands for WP-CLI 148 | * authors: Evan Mattson 149 | * version: v0.1, v0.1-beta.1, v0.2, dev-main, dev-dev, dev-develop, dev-tests/behat 150 | * aaemnnosttv/wp-cli-http-command: 151 | * name: aaemnnosttv/wp-cli-http-command 152 | * description: WP-CLI command for using the WordPress HTTP API 153 | * authors: Evan Mattson 154 | * version: dev-main 155 | */ 156 | public function browse( $_, $assoc_args ) { 157 | $this->set_composer_auth_env_var(); 158 | if ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) { 159 | WP_CLI::line( WP_CLI::colorize( '%CAlthough the package index will remain in place for backward compatibility reasons, it has been deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 to read about its potential replacement.%n' ) ); 160 | } 161 | $this->show_packages( 'browse', $this->get_community_packages(), $assoc_args ); 162 | } 163 | 164 | /** 165 | * Installs a WP-CLI package. 166 | * 167 | * Packages are required to be a valid Composer package, and can be 168 | * specified as: 169 | * 170 | * * Package name from WP-CLI's package index. 171 | * * Git URL accessible by the current shell user. 172 | * * Path to a directory on the local machine. 173 | * * Local or remote .zip file. 174 | * 175 | * Packages are installed to `~/.wp-cli/packages/` by default. Use the 176 | * `WP_CLI_PACKAGES_DIR` environment variable to provide a custom path. 177 | * 178 | * When installing a local directory, WP-CLI simply registers a 179 | * reference to the directory. If you move or delete the directory, WP-CLI's 180 | * reference breaks. 181 | * 182 | * When installing a .zip file, WP-CLI extracts the package to 183 | * `~/.wp-cli/packages/local/`. 184 | * 185 | * If Github token authorization is required, a GitHub Personal Access Token 186 | * (https://github.com/settings/tokens) can be used. The following command 187 | * will add a GitHub Personal Access Token to Composer's global configuration: 188 | * composer config -g github-oauth.github.com 189 | * Once this has been added, the value used for will be used 190 | * for future authorization requests. 191 | * 192 | * ## OPTIONS 193 | * 194 | * 195 | * : Name, git URL, directory path, or .zip file for the package to install. 196 | * Names can optionally include a version constraint 197 | * (e.g. wp-cli/server-command:@stable). 198 | * 199 | * [--insecure] 200 | * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 201 | * 202 | * ## EXAMPLES 203 | * 204 | * # Install a package hosted at a git URL. 205 | * $ wp package install runcommand/hook 206 | * 207 | * # Install the latest stable version. 208 | * $ wp package install wp-cli/server-command:@stable 209 | * 210 | * # Install a package hosted at a GitLab.com URL. 211 | * $ wp package install https://gitlab.com/foo/wp-cli-bar-command.git 212 | * 213 | * # Install a package in a .zip file. 214 | * $ wp package install google-sitemap-generator-cli.zip 215 | */ 216 | public function install( $args, $assoc_args ) { 217 | list( $package_name ) = $args; 218 | 219 | $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); 220 | 221 | $this->set_composer_auth_env_var(); 222 | $git_package = false; 223 | $dir_package = false; 224 | $version = ''; 225 | if ( $this->is_git_repository( $package_name ) ) { 226 | if ( '' === $version ) { 227 | $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; 228 | } 229 | $git_package = $package_name; 230 | $matches = []; 231 | if ( preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches ) ) { 232 | $package_name = $this->check_git_package_name( $matches[1], $package_name, $version, $insecure ); 233 | } else { 234 | WP_CLI::error( "Couldn't parse package name from expected path '/'." ); 235 | } 236 | } elseif ( ( false !== strpos( $package_name, '://' ) && false !== stripos( $package_name, '.zip' ) ) 237 | || ( pathinfo( $package_name, PATHINFO_EXTENSION ) === 'zip' && is_file( $package_name ) ) ) { 238 | // Download the remote ZIP file to a temp directory 239 | $temp = false; 240 | if ( false !== strpos( $package_name, '://' ) ) { 241 | $temp = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ) . '.zip'; 242 | $options = [ 243 | 'timeout' => 600, 244 | 'filename' => $temp, 245 | 'insecure' => $insecure, 246 | ]; 247 | $gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting. 248 | $headers = $gitlab_token && strpos( $package_name, '://gitlab.com/' ) !== false ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : []; 249 | $response = Utils\http_request( 'GET', $package_name, null, $headers, $options ); 250 | if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { 251 | @unlink( $temp ); // @codingStandardsIgnoreLine 252 | WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $package_name, $response->status_code ) ); 253 | } 254 | $package_name = $temp; 255 | } 256 | $dir_package = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ); 257 | try { 258 | // Extract the package to get the package name 259 | Extractor::extract( $package_name, $dir_package ); 260 | if ( $temp ) { 261 | unlink( $temp ); 262 | $temp = false; 263 | } 264 | list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package ); 265 | // Move to a location based on the package name 266 | $local_dir = rtrim( WP_CLI::get_runner()->get_packages_dir_path(), '/' ) . '/local/'; 267 | $actual_dir_package = $local_dir . str_replace( '/', '-', $package_name ); 268 | Extractor::copy_overwrite_files( $dir_package, $actual_dir_package ); 269 | Extractor::rmdir( $dir_package ); 270 | // Behold, the extracted package 271 | $dir_package = $actual_dir_package; 272 | } catch ( Exception $e ) { 273 | if ( $temp ) { 274 | unlink( $temp ); 275 | } 276 | if ( file_exists( $dir_package ) ) { 277 | try { 278 | Extractor::rmdir( $dir_package ); 279 | } catch ( Exception $rmdir_e ) { 280 | WP_CLI::warning( $rmdir_e->getMessage() ); 281 | } 282 | } 283 | WP_CLI::error( $e->getMessage() ); 284 | } 285 | } elseif ( is_dir( $package_name ) && file_exists( $package_name . '/composer.json' ) ) { 286 | $dir_package = $package_name; 287 | if ( ! Utils\is_path_absolute( $dir_package ) ) { 288 | $dir_package = getcwd() . DIRECTORY_SEPARATOR . $dir_package; 289 | } 290 | list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package ); 291 | } else { 292 | if ( false !== strpos( $package_name, ':' ) ) { 293 | list( $package_name, $version ) = explode( ':', $package_name ); 294 | } 295 | $package = $this->get_package_by_shortened_identifier( $package_name ); 296 | if ( ! $package ) { 297 | WP_CLI::error( sprintf( "Invalid package: shortened identifier '%s' not found.", $package_name ) ); 298 | } 299 | if ( is_string( $package ) ) { 300 | if ( $this->is_git_repository( $package ) ) { 301 | $git_package = $package; 302 | 303 | if ( '' === $version ) { 304 | $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; 305 | } 306 | 307 | if ( '@stable' === $version ) { 308 | $tag = $this->get_github_latest_release_tag( $package_name, $insecure ); 309 | $version = $this->guess_version_constraint_from_tag( $tag ); 310 | } 311 | $package_name = $this->check_github_package_name( $package_name, $version, $insecure ); 312 | } 313 | } elseif ( $package_name !== $package->getPrettyName() ) { 314 | // BC support for specifying lowercase names for mixed-case package index packages - don't bother warning. 315 | $package_name = $package->getPrettyName(); 316 | } 317 | } 318 | 319 | if ( $this->is_composer_v2() ) { 320 | $package_name = function_exists( 'mb_strtolower' ) 321 | ? mb_strtolower( $package_name ) 322 | : strtolower( $package_name ); 323 | } 324 | 325 | if ( '' === $version ) { 326 | $version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS; 327 | } 328 | 329 | WP_CLI::log( sprintf( 'Installing package %s (%s)', $package_name, $version ) ); 330 | 331 | // Read the WP-CLI packages composer.json and do some initial error checking. 332 | list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded(); 333 | 334 | // Revert on shutdown if `$revert` is true (set to false on success). 335 | $revert = true; 336 | $this->register_revert_shutdown_function( $json_path, $composer_backup, $revert ); 337 | 338 | // Add the 'require' to composer.json 339 | WP_CLI::log( sprintf( 'Updating %s to require the package...', $json_path ) ); 340 | $json_manipulator = new JsonManipulator( $composer_backup ); 341 | $json_manipulator->addMainKey( 'name', 'wp-cli/wp-cli' ); 342 | $json_manipulator->addMainKey( 'version', self::get_wp_cli_version_composer() ); 343 | $json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ ); 344 | $json_manipulator->addConfigSetting( 'secure-http', true ); 345 | 346 | $package_args = []; 347 | if ( $git_package ) { 348 | WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) ); 349 | $package_args = [ 350 | 'type' => 'vcs', 351 | 'url' => $git_package, 352 | ]; 353 | $json_manipulator->addSubNode( 354 | 'repositories', 355 | $package_name, 356 | $package_args, 357 | true /*caseInsensitive*/ 358 | ); 359 | } elseif ( $dir_package ) { 360 | WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) ); 361 | $package_args = [ 362 | 'type' => 'path', 363 | 'url' => $dir_package, 364 | ]; 365 | $json_manipulator->addSubNode( 366 | 'repositories', 367 | $package_name, 368 | $package_args, 369 | true /*caseInsensitive*/ 370 | ); 371 | } 372 | // If the composer file does not contain the current package index repository, refresh the repository definition. 373 | if ( empty( $composer_backup_decoded['repositories']['wp-cli']['url'] ) || self::PACKAGE_INDEX_URL !== $composer_backup_decoded['repositories']['wp-cli']['url'] ) { 374 | WP_CLI::log( 'Updating package index repository url...' ); 375 | $package_args = $this->composer_type_package; 376 | $json_manipulator->addRepository( 377 | 'wp-cli', 378 | $package_args 379 | ); 380 | } 381 | 382 | file_put_contents( $json_path, $json_manipulator->getContents() ); 383 | $composer = $this->get_composer(); 384 | 385 | // Set up the EventSubscriber 386 | $event_subscriber = new PackageManagerEventSubscriber(); 387 | $composer->getEventDispatcher()->addSubscriber( $event_subscriber ); 388 | // Set up the installer 389 | $install = Installer::create( new ComposerIO(), $composer ); 390 | $install->setUpdate( true ); // Installer class will only override composer.lock with this flag 391 | $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. 392 | 393 | // Try running the installer, but revert composer.json if failed 394 | WP_CLI::log( 'Using Composer to install the package...' ); 395 | WP_CLI::log( '---' ); 396 | $res = false; 397 | try { 398 | $res = $install->run(); 399 | } catch ( Exception $e ) { 400 | WP_CLI::warning( $e->getMessage() ); 401 | } 402 | 403 | // TODO: The --insecure flag should cause another Composer run with verify disabled. 404 | 405 | WP_CLI::log( '---' ); 406 | 407 | if ( 0 === $res ) { 408 | $revert = false; 409 | WP_CLI::success( 'Package installed.' ); 410 | } else { 411 | $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. 412 | WP_CLI::debug( "composer.json content:\n" . file_get_contents( $json_path ), 'packages' ); 413 | WP_CLI::error( "Package installation failed{$res_msg}." ); 414 | } 415 | } 416 | 417 | /** 418 | * Lists installed WP-CLI packages. 419 | * 420 | * ## OPTIONS 421 | * 422 | * [--fields=] 423 | * : Limit the output to specific fields. Defaults to all fields. 424 | * 425 | * [--format=] 426 | * : Render output in a particular format. 427 | * --- 428 | * default: table 429 | * options: 430 | * - table 431 | * - csv 432 | * - ids 433 | * - json 434 | * - yaml 435 | * --- 436 | * 437 | * ## AVAILABLE FIELDS 438 | * 439 | * These fields will be displayed by default for each package: 440 | * 441 | * * name 442 | * * authors 443 | * * version 444 | * * update 445 | * * update_version 446 | * 447 | * These fields are optionally available: 448 | * 449 | * * description 450 | * 451 | * ## EXAMPLES 452 | * 453 | * # List installed packages. 454 | * $ wp package list 455 | * +-----------------------+------------------+----------+-----------+----------------+ 456 | * | name | authors | version | update | update_version | 457 | * +-----------------------+------------------+----------+-----------+----------------+ 458 | * | wp-cli/server-command | Daniel Bachhuber | dev-main | available | 2.x-dev | 459 | * +-----------------------+------------------+----------+-----------+----------------+ 460 | * 461 | * @subcommand list 462 | */ 463 | public function list_( $args, $assoc_args ) { 464 | $this->set_composer_auth_env_var(); 465 | $this->show_packages( 'list', $this->get_installed_packages(), $assoc_args ); 466 | } 467 | 468 | /** 469 | * Gets the path to an installed WP-CLI package, or the package directory. 470 | * 471 | * If you want to contribute to a package, this is a great way to jump to it. 472 | * 473 | * ## OPTIONS 474 | * 475 | * [] 476 | * : Name of the package to get the directory for. 477 | * 478 | * ## EXAMPLES 479 | * 480 | * # Get package path. 481 | * $ wp package path 482 | * /home/person/.wp-cli/packages/ 483 | * 484 | * # Get path to an installed package. 485 | * $ wp package path wp-cli/server-command 486 | * /home/person/.wp-cli/packages/vendor/wp-cli/server-command 487 | * 488 | * # Change directory to package path. 489 | * $ cd $(wp package path) && pwd 490 | * /home/vagrant/.wp-cli/packages 491 | */ 492 | public function path( $args ) { 493 | $packages_dir = WP_CLI::get_runner()->get_packages_dir_path(); 494 | if ( ! empty( $args ) ) { 495 | $packages_dir .= 'vendor/' . $args[0]; 496 | if ( ! is_dir( $packages_dir ) ) { 497 | WP_CLI::error( 'Invalid package name.' ); 498 | } 499 | } 500 | WP_CLI::line( $packages_dir ); 501 | } 502 | 503 | /** 504 | * Updates all installed WP-CLI packages to their latest version. 505 | * 506 | * ## EXAMPLES 507 | * 508 | * $ wp package update 509 | * Using Composer to update packages... 510 | * --- 511 | * Loading composer repositories with package information 512 | * Updating dependencies 513 | * Resolving dependencies through SAT 514 | * Dependency resolution completed in 0.074 seconds 515 | * Analyzed 1062 packages to resolve dependencies 516 | * Analyzed 22383 rules to resolve dependencies 517 | * Writing lock file 518 | * Generating autoload files 519 | * --- 520 | * Success: Packages updated. 521 | */ 522 | public function update() { 523 | $this->set_composer_auth_env_var(); 524 | $composer = $this->get_composer(); 525 | 526 | // Set up the EventSubscriber 527 | $event_subscriber = new PackageManagerEventSubscriber(); 528 | $composer->getEventDispatcher()->addSubscriber( $event_subscriber ); 529 | 530 | // Set up the installer 531 | $install = Installer::create( new ComposerIO(), $composer ); 532 | $install->setUpdate( true ); // Installer class will only override composer.lock with this flag 533 | $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. 534 | WP_CLI::log( 'Using Composer to update packages...' ); 535 | WP_CLI::log( '---' ); 536 | $res = false; 537 | try { 538 | $res = $install->run(); 539 | } catch ( Exception $e ) { 540 | WP_CLI::warning( $e->getMessage() ); 541 | } 542 | WP_CLI::log( '---' ); 543 | 544 | // TODO: The --insecure (to be added here) flag should cause another Composer run with verify disabled. 545 | 546 | if ( 0 === $res ) { 547 | WP_CLI::success( 'Packages updated.' ); 548 | } else { 549 | $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. 550 | WP_CLI::error( "Failed to update packages{$res_msg}." ); 551 | } 552 | } 553 | 554 | /** 555 | * Uninstalls a WP-CLI package. 556 | * 557 | * ## OPTIONS 558 | * 559 | * 560 | * : Name of the package to uninstall. 561 | * 562 | * [--insecure] 563 | * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 564 | * 565 | * ## EXAMPLES 566 | * 567 | * # Uninstall package. 568 | * $ wp package uninstall wp-cli/server-command 569 | * Removing require statement for package 'wp-cli/server-command' from /home/person/.wp-cli/packages/composer.json 570 | * Removing repository details from /home/person/.wp-cli/packages/composer.json 571 | * Removing package directories and regenerating autoloader... 572 | * Success: Uninstalled package. 573 | */ 574 | public function uninstall( $args, $assoc_args ) { 575 | list( $package_name ) = $args; 576 | 577 | $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); 578 | 579 | $this->set_composer_auth_env_var(); 580 | $package = $this->get_installed_package_by_name( $package_name ); 581 | if ( false === $package ) { 582 | $package_name = $this->get_package_by_shortened_identifier( $package_name ); 583 | if ( false === $package_name ) { 584 | WP_CLI::error( 'Package not installed.' ); 585 | } 586 | $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; 587 | $matches = []; 588 | if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?.*?).git$#', $package_name, $matches ) ) { 589 | $package_name = $this->check_git_package_name( $matches['repo_name'], $package_name, $version, $insecure ); 590 | } 591 | } else { 592 | $package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json. 593 | } 594 | 595 | // Read the WP-CLI packages composer.json and do some initial error checking. 596 | list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded(); 597 | 598 | // Revert on shutdown if `$revert` is true (set to false on success). 599 | $revert = true; 600 | $this->register_revert_shutdown_function( $json_path, $composer_backup, $revert ); 601 | 602 | // Remove the 'require' from composer.json. 603 | WP_CLI::log( sprintf( 'Removing require statement for package \'%s\' from %s', $package_name, $json_path ) ); 604 | $manipulator = new JsonManipulator( $composer_backup ); 605 | $manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ ); 606 | 607 | // Remove the 'repository' details from composer.json. 608 | WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) ); 609 | $manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ ); 610 | 611 | file_put_contents( $json_path, $manipulator->getContents() ); 612 | $composer = $this->get_composer(); 613 | 614 | // Set up the installer. 615 | $install = Installer::create( new NullIO(), $composer ); 616 | $install->setUpdate( true ); // Installer class will only override composer.lock with this flag 617 | $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. 618 | 619 | WP_CLI::log( 'Removing package directories and regenerating autoloader...' ); 620 | $res = false; 621 | try { 622 | $res = $install->run(); 623 | } catch ( Exception $e ) { 624 | WP_CLI::warning( $e->getMessage() ); 625 | } 626 | 627 | if ( 0 === $res ) { 628 | $revert = false; 629 | WP_CLI::success( 'Uninstalled package.' ); 630 | } else { 631 | $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. 632 | WP_CLI::error( "Package removal failed{$res_msg}." ); 633 | } 634 | } 635 | 636 | /** 637 | * Checks whether a package is a WP-CLI community package based 638 | * on membership in our package index. 639 | * 640 | * @param object $package A package object 641 | * @return bool 642 | */ 643 | private function is_community_package( $package ) { 644 | return $this->package_index()->hasPackage( $package ); 645 | } 646 | 647 | /** 648 | * Gets a Composer instance. 649 | */ 650 | private function get_composer() { 651 | $this->avoid_composer_ca_bundle(); 652 | try { 653 | $composer_path = $this->get_composer_json_path(); 654 | 655 | // Composer's auto-load generating code makes some assumptions about where 656 | // the 'vendor-dir' is, and where Composer is running from. 657 | // Best to just pretend we're installing a package from ~/.wp-cli or similar 658 | chdir( pathinfo( $composer_path, PATHINFO_DIRNAME ) ); 659 | 660 | // Prevent DateTime error/warning when no timezone set. 661 | // Note: The package is loaded before WordPress load, For environments that don't have set time in php.ini. 662 | // phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set,WordPress.PHP.NoSilencedErrors.Discouraged 663 | date_default_timezone_set( @date_default_timezone_get() ); 664 | 665 | $composer = Factory::create( new NullIO(), $composer_path ); 666 | } catch ( Exception $e ) { 667 | WP_CLI::error( sprintf( 'Failed to get composer instance: %s', $e->getMessage() ) ); 668 | } 669 | return $composer; 670 | } 671 | 672 | /** 673 | * Gets all of the community packages. 674 | * 675 | * @return array 676 | */ 677 | private function get_community_packages() { 678 | static $community_packages; 679 | 680 | if ( null === $community_packages ) { 681 | $this->avoid_composer_ca_bundle(); 682 | try { 683 | $community_packages = $this->package_index()->getPackages(); 684 | } catch ( Exception $e ) { 685 | WP_CLI::error( $e->getMessage() ); 686 | } 687 | } 688 | 689 | return $community_packages; 690 | } 691 | 692 | /** 693 | * Gets the package index instance 694 | * 695 | * We need to construct the instance manually, because there's no way to select 696 | * a particular instance using $composer->getRepositoryManager() 697 | * 698 | * @return ComposerRepository 699 | */ 700 | private function package_index() { 701 | static $package_index; 702 | 703 | if ( ! $package_index ) { 704 | $config_args = [ 705 | 'config' => [ 706 | 'secure-http' => true, 707 | 'home' => dirname( $this->get_composer_json_path() ), 708 | ], 709 | ]; 710 | $config = new Config(); 711 | $config->merge( $config_args ); 712 | $config->setConfigSource( new JsonConfigSource( $this->get_composer_json() ) ); 713 | 714 | $io = new NullIO(); 715 | try { 716 | if ( $this->is_composer_v2() ) { 717 | $http_downloader = new HttpDownloader( $io, $config ); 718 | $package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config, $http_downloader ); 719 | } else { 720 | $package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config ); 721 | } 722 | } catch ( Exception $e ) { 723 | WP_CLI::error( $e->getMessage() ); 724 | } 725 | } 726 | 727 | return $package_index; 728 | } 729 | 730 | /** 731 | * Displays a set of packages 732 | * 733 | * @param string $context 734 | * @param array 735 | * @param array 736 | */ 737 | private function show_packages( $context, $packages, $assoc_args ) { 738 | if ( 'list' === $context ) { 739 | $default_fields = [ 740 | 'name', 741 | 'authors', 742 | 'version', 743 | 'update', 744 | 'update_version', 745 | ]; 746 | } elseif ( 'browse' === $context ) { 747 | $default_fields = [ 748 | 'name', 749 | 'description', 750 | 'authors', 751 | 'version', 752 | ]; 753 | } 754 | $defaults = [ 755 | 'fields' => implode( ',', $default_fields ), 756 | 'format' => 'table', 757 | ]; 758 | $assoc_args = array_merge( $defaults, $assoc_args ); 759 | 760 | $composer = $this->get_composer(); 761 | $list = []; 762 | foreach ( $packages as $package ) { 763 | $name = $package->getPrettyName(); 764 | if ( isset( $list[ $name ] ) ) { 765 | $list[ $name ]['version'][] = $package->getPrettyVersion(); 766 | } else { 767 | $package_output = []; 768 | $package_output['name'] = $package->getPrettyName(); 769 | $package_output['description'] = $package->getDescription(); 770 | $package_output['authors'] = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) ); 771 | $package_output['version'] = [ $package->getPrettyVersion() ]; 772 | $update = 'none'; 773 | $update_version = ''; 774 | if ( 'list' === $context ) { 775 | try { 776 | $latest = $this->find_latest_package( $package, $composer, null ); 777 | if ( $latest && $latest->getFullPrettyVersion() !== $package->getFullPrettyVersion() ) { 778 | $update = 'available'; 779 | $update_version = $latest->getPrettyVersion(); 780 | } 781 | } catch ( Exception $e ) { 782 | WP_CLI::warning( $e->getMessage() ); 783 | $update = 'error'; 784 | $update_version = $update; 785 | } 786 | } 787 | $package_output['update'] = $update; 788 | $package_output['update_version'] = $update_version; 789 | $package_output['pretty_name'] = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8. 790 | $list[ $package_output['name'] ] = $package_output; 791 | } 792 | } 793 | 794 | $list = array_map( 795 | function ( $package ) { 796 | $package['version'] = implode( ', ', $package['version'] ); 797 | return $package; 798 | }, 799 | $list 800 | ); 801 | 802 | ksort( $list ); 803 | if ( 'ids' === $assoc_args['format'] ) { 804 | $list = array_keys( $list ); 805 | } 806 | Utils\format_items( $assoc_args['format'], $list, $assoc_args['fields'] ); 807 | } 808 | 809 | /** 810 | * Gets a package by its shortened identifier. 811 | * 812 | * A shortened identifier has the form `/`. 813 | * 814 | * This method first checks the deprecated package index, for BC reasons, 815 | * and then falls back to the corresponding GitHub URL. 816 | * 817 | * @param string $package_name Name of the package to get. 818 | * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults 819 | * to false. 820 | */ 821 | private function get_package_by_shortened_identifier( $package_name, $insecure = false ) { 822 | // Check the package index first, so we don't break existing behavior. 823 | $lc_package_name = strtolower( $package_name ); // For BC check. 824 | foreach ( $this->get_community_packages() as $package ) { 825 | if ( $package_name === $package->getPrettyName() ) { 826 | return $package; 827 | } 828 | // For BC allow getting by lowercase name. 829 | if ( $lc_package_name === $package->getName() ) { 830 | return $package; 831 | } 832 | } 833 | 834 | $options = [ 'insecure' => $insecure ]; 835 | 836 | // Check if the package exists on Packagist. 837 | $url = "https://repo.packagist.org/p/{$package_name}.json"; 838 | $response = Utils\http_request( 'GET', $url, null, [], $options ); 839 | if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) { 840 | return $package_name; 841 | } 842 | 843 | // Fall back to GitHub URL if we had no match yet. 844 | $url = "https://github.com/{$package_name}.git"; 845 | $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. 846 | $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; 847 | $response = Utils\http_request( 'GET', $url, null /*data*/, $headers, $options ); 848 | if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) { 849 | return $url; 850 | } 851 | 852 | // Fall back to GitLab URL if we had no match yet. 853 | $url = "https://gitlab.com/{$package_name}.git"; 854 | $gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting. 855 | $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; 856 | $headers = $gitlab_token && strpos( $package_name, '://gitlab.com/' ) !== false ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : []; 857 | $response = Utils\http_request( 'GET', $url, null /*data*/, $headers, $options ); 858 | if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) { 859 | return $url; 860 | } 861 | 862 | return false; 863 | } 864 | 865 | /** 866 | * Gets the installed community packages. 867 | */ 868 | private function get_installed_packages() { 869 | $composer = $this->get_composer(); 870 | 871 | $repo = $composer->getRepositoryManager()->getLocalRepository(); 872 | $existing = json_decode( file_get_contents( $this->get_composer_json_path() ), true ); 873 | $installed_package_keys = ! empty( $existing['require'] ) ? array_keys( $existing['require'] ) : []; 874 | if ( empty( $installed_package_keys ) ) { 875 | return []; 876 | } 877 | // For use by legacy incorrect name check. 878 | $lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys ); 879 | $installed_packages = []; 880 | foreach ( $repo->getCanonicalPackages() as $package ) { 881 | $idx = array_search( $package->getName(), $lc_installed_package_keys, true ); 882 | // Use pretty name as it's case sensitive and what's in composer.json (or at least should be). 883 | if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) { 884 | $installed_packages[] = $package; 885 | } elseif ( false !== $idx ) { // Legacy incorrect name check. 886 | if ( ! $this->is_composer_v2() ) { 887 | WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) ); 888 | } 889 | $installed_packages[] = $package; 890 | } 891 | } 892 | return $installed_packages; 893 | } 894 | 895 | /** 896 | * Gets an installed package by its name. 897 | */ 898 | private function get_installed_package_by_name( $package_name ) { 899 | foreach ( $this->get_installed_packages() as $package ) { 900 | if ( $package_name === $package->getPrettyName() ) { 901 | return $package; 902 | } 903 | // Also check non-pretty (lowercase) name in case of legacy incorrect name. 904 | if ( $package_name === $package->getName() ) { 905 | return $package; 906 | } 907 | } 908 | return false; 909 | } 910 | 911 | /** 912 | * Checks if the package name provided is already installed. 913 | */ 914 | private function is_package_installed( $package_name ) { 915 | if ( $this->get_installed_package_by_name( $package_name ) ) { 916 | return true; 917 | } else { 918 | return false; 919 | } 920 | } 921 | 922 | /** 923 | * Gets the name of the package from the composer.json in a directory path 924 | * 925 | * @param string $dir_package 926 | * @return array Two-element array containing package name and version. 927 | */ 928 | private static function get_package_name_and_version_from_dir_package( $dir_package ) { 929 | $composer_file = $dir_package . '/composer.json'; 930 | if ( ! file_exists( $composer_file ) ) { 931 | WP_CLI::error( sprintf( "Invalid package: composer.json file '%s' not found.", $composer_file ) ); 932 | } 933 | $composer_data = json_decode( file_get_contents( $composer_file ), true ); 934 | if ( null === $composer_data ) { 935 | WP_CLI::error( sprintf( "Invalid package: failed to parse composer.json file '%s' as json.", $composer_file ) ); 936 | } 937 | if ( empty( $composer_data['name'] ) ) { 938 | WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $composer_file ) ); 939 | } 940 | $package_name = $composer_data['name']; 941 | $version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS; 942 | if ( ! empty( $composer_data['version'] ) ) { 943 | $version = $composer_data['version']; 944 | } 945 | return [ $package_name, $version ]; 946 | } 947 | 948 | /** 949 | * Gets the WP-CLI packages composer.json object. 950 | */ 951 | private function get_composer_json() { 952 | return new JsonFile( $this->get_composer_json_path() ); 953 | } 954 | 955 | /** 956 | * Gets the absolute path to the WP-CLI packages composer.json. 957 | */ 958 | private function get_composer_json_path() { 959 | static $composer_path; 960 | 961 | if ( null === $composer_path || getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ) ) { 962 | 963 | if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) { 964 | $composer_path = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ) . 'composer.json'; 965 | } else { 966 | $composer_path = Utils\trailingslashit( Utils\get_home_dir() ) . '.wp-cli/packages/composer.json'; 967 | } 968 | 969 | // `composer.json` and its directory might need to be created 970 | if ( ! file_exists( $composer_path ) ) { 971 | $composer_path = $this->create_default_composer_json( $composer_path ); 972 | } else { 973 | $composer_path = realpath( $composer_path ); 974 | if ( false === $composer_path ) { 975 | $error = error_get_last(); 976 | WP_CLI::error( sprintf( "Composer path '%s' for packages/composer.json not found: %s", $composer_path, $error['message'] ) ); 977 | } 978 | } 979 | } 980 | 981 | return $composer_path; 982 | } 983 | 984 | /** 985 | * Gets the WP-CLI version for composer.json 986 | */ 987 | private static function get_wp_cli_version_composer() { 988 | preg_match( '#^[0-9\.]+(-(alpha|beta)[^-]{0,})?#', WP_CLI_VERSION, $matches ); 989 | $version = isset( $matches[0] ) ? $matches[0] : ''; 990 | return $version; 991 | } 992 | 993 | /** 994 | * Creates a default WP-CLI packages composer.json. 995 | * 996 | * @param string $composer_path Where the composer.json should be created 997 | * @return string Returns the absolute path of the newly created default WP-CLI packages composer.json. 998 | */ 999 | private function create_default_composer_json( $composer_path ) { 1000 | 1001 | $composer_dir = pathinfo( $composer_path, PATHINFO_DIRNAME ); 1002 | if ( ! is_dir( $composer_dir ) ) { 1003 | if ( ! @mkdir( $composer_dir, 0777, true ) ) { // @codingStandardsIgnoreLine 1004 | $error = error_get_last(); 1005 | WP_CLI::error( sprintf( "Composer directory '%s' for packages couldn't be created: %s", $composer_dir, $error['message'] ) ); 1006 | } 1007 | } 1008 | 1009 | $composer_dir = realpath( $composer_dir ); 1010 | if ( false === $composer_dir ) { 1011 | $error = error_get_last(); 1012 | WP_CLI::error( sprintf( "Composer directory '%s' for packages not found: %s", $composer_dir, $error['message'] ) ); 1013 | } 1014 | 1015 | $composer_path = Utils\trailingslashit( $composer_dir ) . Utils\basename( $composer_path ); 1016 | 1017 | $json_file = new JsonFile( $composer_path ); 1018 | 1019 | $repositories = (object) [ 1020 | 'wp-cli' => (object) $this->composer_type_package, 1021 | ]; 1022 | 1023 | $options = [ 1024 | 'name' => 'wp-cli/wp-cli', 1025 | 'description' => 'Installed community packages used by WP-CLI', 1026 | 'version' => self::get_wp_cli_version_composer(), 1027 | 'authors' => [ (object) $this->author_data ], 1028 | 'homepage' => self::PACKAGE_INDEX_URL, 1029 | 'require' => new stdClass(), 1030 | 'require-dev' => new stdClass(), 1031 | 'minimum-stability' => 'dev', 1032 | 'prefer-stable' => true, 1033 | 'license' => 'MIT', 1034 | 'repositories' => $repositories, 1035 | ]; 1036 | 1037 | try { 1038 | $json_file->write( $options ); 1039 | } catch ( Exception $e ) { 1040 | WP_CLI::error( $e->getMessage() ); 1041 | } 1042 | 1043 | return $composer_path; 1044 | } 1045 | 1046 | /** 1047 | * Given a package, this finds the latest package matching it 1048 | * 1049 | * @param PackageInterface $package 1050 | * @param Composer $composer 1051 | * @param string $phpVersion 1052 | * @param bool $minorOnly 1053 | * 1054 | * @return PackageInterface|null 1055 | */ 1056 | private function find_latest_package( PackageInterface $package, Composer $composer, $php_version, $minor_only = false ) { 1057 | // Find the latest version allowed in this pool/repository set. 1058 | $name = $package->getPrettyName(); 1059 | $version_selector = $this->get_version_selector( $composer ); 1060 | $stability = $composer->getPackage()->getMinimumStability(); 1061 | $flags = $composer->getPackage()->getStabilityFlags(); 1062 | if ( isset( $flags[ $name ] ) ) { 1063 | $stability = array_search( $flags[ $name ], BasePackage::$stabilities, true ); 1064 | } 1065 | $best_stability = $stability; 1066 | if ( $composer->getPackage()->getPreferStable() ) { 1067 | $best_stability = $package->getStability(); 1068 | } 1069 | $target_version = null; 1070 | if ( 0 === strpos( $package->getVersion(), 'dev-' ) ) { 1071 | $target_version = $package->getVersion(); 1072 | } 1073 | if ( null === $target_version && $minor_only ) { 1074 | $target_version = '^' . $package->getVersion(); 1075 | } 1076 | 1077 | if ( $this->is_composer_v2() ) { 1078 | return $version_selector->findBestCandidate( $name, $target_version, $best_stability ); 1079 | } 1080 | 1081 | return $version_selector->findBestCandidate( $name, $target_version, $php_version, $best_stability ); 1082 | } 1083 | 1084 | private function get_version_selector( Composer $composer ) { 1085 | if ( ! $this->version_selector ) { 1086 | if ( $this->is_composer_v2() ) { 1087 | $repository_set = new Repository\RepositorySet( 1088 | $composer->getPackage()->getMinimumStability(), 1089 | $composer->getPackage()->getStabilityFlags() 1090 | ); 1091 | $repository_set->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) ); 1092 | $this->version_selector = new VersionSelector( $repository_set ); 1093 | } else { 1094 | $pool = new Pool( $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags() ); 1095 | $pool->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) ); 1096 | $this->version_selector = new VersionSelector( $pool ); 1097 | } 1098 | } 1099 | 1100 | return $this->version_selector; 1101 | } 1102 | 1103 | /** 1104 | * Checks whether a given package is a git repository. 1105 | * 1106 | * @param string $package Package name to check. 1107 | * 1108 | * @return bool Whether the package is a git repository. 1109 | */ 1110 | private function is_git_repository( $package ) { 1111 | return '.git' === strtolower( substr( $package, -4, 4 ) ); 1112 | } 1113 | 1114 | /** 1115 | * Checks that `$package_name` matches the name in composer.json at Github.com, and return corrected value if not. 1116 | * 1117 | * @param string $package_name Package name to check. 1118 | * @param string $version Optional. Package version. Defaults to empty string. 1119 | * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults 1120 | * to false. 1121 | */ 1122 | private function check_github_package_name( $package_name, $version = '', $insecure = false ) { 1123 | $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. 1124 | $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; 1125 | $options = [ 'insecure' => $insecure ]; 1126 | 1127 | // Generate raw git URL of composer.json file. 1128 | $raw_content_url = "https://raw.githubusercontent.com/{$package_name}/{$this->get_raw_git_version( $version )}/composer.json"; 1129 | 1130 | $response = Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers, $options ); 1131 | if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { 1132 | // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). 1133 | WP_CLI::warning( 1134 | sprintf( 1135 | "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", 1136 | $raw_content_url, 1137 | $response->status_code, 1138 | $package_name 1139 | ) 1140 | ); 1141 | return $package_name; 1142 | } 1143 | 1144 | // Convert composer.json JSON to Array. 1145 | $composer_content_as_array = json_decode( $response->body, true ); 1146 | if ( null === $composer_content_as_array ) { 1147 | WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $raw_content_url ) ); 1148 | } 1149 | if ( empty( $composer_content_as_array['name'] ) ) { 1150 | WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $raw_content_url ) ); 1151 | } 1152 | 1153 | // Package name in composer.json that is hosted on GitHub. 1154 | $package_name_on_repo = $composer_content_as_array['name']; 1155 | 1156 | // If package name and repository name are not identical, then fix it. 1157 | if ( $package_name !== $package_name_on_repo ) { 1158 | WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) ); 1159 | $package_name = $package_name_on_repo; 1160 | } 1161 | 1162 | return $package_name; 1163 | } 1164 | 1165 | /** 1166 | * Checks that `$package_name` matches the name in composer.json at the corresponding upstream repository, and return corrected value if not. 1167 | * 1168 | * @param string $package_name Package name to check. 1169 | * @param string $url URL to fetch the package from. 1170 | * @param string $version Optional. Package version. Defaults to empty string. 1171 | * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults 1172 | * to false. 1173 | */ 1174 | private function check_git_package_name( $package_name, $url = '', $version = '', $insecure = false ) { 1175 | if ( $url && ( ( strpos( $url, '://gitlab.com/' ) !== false ) || ( strpos( $url, 'git@gitlab.com:' ) !== false ) ) ) { 1176 | $matches = []; 1177 | preg_match( '#gitlab.com[:/](.*?)\.git#', $url, $matches ); 1178 | return $this->check_gitlab_package_name( $matches[1], $version, $insecure ); 1179 | } 1180 | 1181 | return $this->check_github_package_name( $package_name, $version, $insecure ); 1182 | } 1183 | 1184 | /** 1185 | * Checks that `$package_name` matches the name in composer.json at GitLab.com, and return corrected value if not. 1186 | * 1187 | * @param string $project_name Package name to check. 1188 | * @param string $version Optional. Package version. Defaults to empty string. 1189 | * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults 1190 | * to false. 1191 | */ 1192 | private function check_gitlab_package_name( $project_name, $version = '', $insecure = false ) { 1193 | $options = [ 'insecure' => $insecure ]; 1194 | // Generate raw git URL of composer.json file. 1195 | $raw_content_public_url = 'https://gitlab.com/' . $project_name . '/-/raw/' . $this->get_raw_git_version( $version ) . '/composer.json'; 1196 | $raw_content_private_url = 'https://gitlab.com/api/v4/projects/' . rawurlencode( $project_name ) . '/repository/files/composer.json/raw?ref=' . $this->get_raw_git_version( $version ); 1197 | 1198 | $matches = []; 1199 | preg_match( '#([^:\/]+\/[^\/]+$)#', $project_name, $matches ); 1200 | $package_name = $matches[1]; 1201 | 1202 | $gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting. 1203 | $response = Utils\http_request( 'GET', $raw_content_public_url, null /*data*/, [], $options ); 1204 | if ( ! $gitlab_token && ( $response->status_code < 200 || $response->status_code >= 300 ) ) { 1205 | // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). 1206 | WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_public_url, $response->status_code, $package_name ) ); 1207 | return $package_name; 1208 | } 1209 | 1210 | if ( strpos( $response->headers['content-type'], 'text/html' ) === 0 ) { 1211 | $headers = $gitlab_token ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : []; 1212 | $response = Utils\http_request( 'GET', $raw_content_private_url, null /*data*/, $headers, $options ); 1213 | 1214 | if ( $response->status_code < 200 || $response->status_code >= 300 ) { 1215 | // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). 1216 | WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_private_url, $response->status_code, $package_name ) ); 1217 | return $package_name; 1218 | } 1219 | } 1220 | 1221 | // Convert composer.json JSON to Array. 1222 | $composer_content_as_array = json_decode( $response->body, true ); 1223 | if ( null === $composer_content_as_array ) { 1224 | WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $response->url ) ); 1225 | } 1226 | if ( empty( $composer_content_as_array['name'] ) ) { 1227 | WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $response->url ) ); 1228 | } 1229 | 1230 | // Package name in composer.json that is hosted on Gitlab. 1231 | $package_name_on_repo = $composer_content_as_array['name']; 1232 | 1233 | // If package name and repository name are not identical, then fix it. 1234 | if ( $package_name !== $package_name_on_repo ) { 1235 | WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) ); 1236 | $package_name = $package_name_on_repo; 1237 | } 1238 | return $package_name; 1239 | } 1240 | 1241 | /** 1242 | * Get the version to use for raw GitHub request. Very basic. 1243 | * 1244 | * @string $version Package version. 1245 | * @string Version to use for GitHub request. 1246 | */ 1247 | private function get_raw_git_version( $version ) { 1248 | if ( '' === $version ) { 1249 | return 'master'; 1250 | } 1251 | 1252 | // If Composer hash given then just use whatever's after it. 1253 | $hash_pos = strpos( $version, '#' ); 1254 | if ( false !== $hash_pos ) { 1255 | return substr( $version, $hash_pos + 1 ); 1256 | } 1257 | 1258 | // Strip any Composer 'dev-' prefix. 1259 | if ( 0 === strncmp( $version, 'dev-', 4 ) ) { 1260 | $version = substr( $version, 4 ); 1261 | } 1262 | 1263 | // Ignore/strip any relative suffixes. 1264 | return str_replace( [ '^', '~' ], '', $version ); 1265 | } 1266 | 1267 | /** 1268 | * Gets the release tag for the latest stable release of a GitHub repository. 1269 | * 1270 | * @param string $package_name Name of the repository. 1271 | * 1272 | * @return string Release tag. 1273 | */ 1274 | private function get_github_latest_release_tag( $package_name, $insecure ) { 1275 | $url = "https://api.github.com/repos/{$package_name}/releases/latest"; 1276 | $options = [ 'insecure' => $insecure ]; 1277 | $response = Utils\http_request( 'GET', $url, null, [], $options ); 1278 | if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { 1279 | WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' ); 1280 | return 'master'; 1281 | } 1282 | 1283 | $package_data = json_decode( $response->body ); 1284 | if ( JSON_ERROR_NONE !== json_last_error() ) { 1285 | WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' ); 1286 | return 'master'; 1287 | } 1288 | 1289 | $tag = $package_data->tag_name; 1290 | WP_CLI::debug( "Extracted latest stable release tag: {$tag}", 'packages' ); 1291 | 1292 | return $tag; 1293 | } 1294 | 1295 | /** 1296 | * Guesses the version constraint from a release tag. 1297 | * 1298 | * @param string $tag Release tag to guess the version constraint from. 1299 | * 1300 | * @return string Version constraint. 1301 | */ 1302 | private function guess_version_constraint_from_tag( $tag ) { 1303 | $matches = []; 1304 | if ( 1 !== preg_match( '/(?:version|v)\s*((?:[0-9]+\.?)+)(?:-.*)/i', $tag, $matches ) ) { 1305 | return $tag; 1306 | } 1307 | 1308 | $constraint = "^{$matches[1]}"; 1309 | WP_CLI::debug( "Guessing version constraint to use: {$constraint}", 'packages' ); 1310 | 1311 | return $constraint; 1312 | } 1313 | 1314 | /** 1315 | * Sets `COMPOSER_AUTH` environment variable (which Composer merges into the config setup in `Composer\Factory::createConfig()`) depending on available environment variables. 1316 | * Avoids authorization failures when accessing various sites. 1317 | */ 1318 | private function set_composer_auth_env_var() { 1319 | $changed = false; 1320 | $composer_auth = getenv( 'COMPOSER_AUTH' ); 1321 | if ( false !== $composer_auth ) { 1322 | $composer_auth = json_decode( $composer_auth, true /*assoc*/ ); 1323 | } 1324 | if ( empty( $composer_auth ) || ! is_array( $composer_auth ) ) { 1325 | $composer_auth = []; 1326 | } 1327 | $github_token = getenv( 'GITHUB_TOKEN' ); 1328 | if ( ! isset( $composer_auth['github-oauth'] ) && is_string( $github_token ) ) { 1329 | $composer_auth['github-oauth'] = [ 'github.com' => $github_token ]; 1330 | $changed = true; 1331 | } 1332 | if ( $changed ) { 1333 | putenv( 'COMPOSER_AUTH=' . json_encode( $composer_auth ) ); 1334 | } 1335 | } 1336 | 1337 | /** 1338 | * Avoid using default Composer CA bundle if in phar as we don't include it. 1339 | * See https://github.com/composer/ca-bundle/blob/1.1.0/src/CaBundle.php#L64 1340 | */ 1341 | private function avoid_composer_ca_bundle() { 1342 | if ( Utils\inside_phar() && ! getenv( 'SSL_CERT_FILE' ) && ! getenv( 'SSL_CERT_DIR' ) && ! ini_get( 'openssl.cafile' ) && ! ini_get( 'openssl.capath' ) ) { 1343 | $certificate_path = Utils\extract_from_phar( RequestsLibrary::get_bundled_certificate_path() ); 1344 | putenv( "SSL_CERT_FILE={$certificate_path}" ); 1345 | } 1346 | } 1347 | 1348 | /** 1349 | * Reads the WP-CLI packages composer.json, checking validity and returning array containing its path, contents, and decoded contents. 1350 | * 1351 | * @return array Indexed array containing the path, the contents, and the decoded contents of the WP-CLI packages composer.json. 1352 | */ 1353 | private function get_composer_json_path_backup_decoded() { 1354 | $composer_json_obj = $this->get_composer_json(); 1355 | $json_path = $composer_json_obj->getPath(); 1356 | $composer_backup = file_get_contents( $json_path ); 1357 | if ( false === $composer_backup ) { 1358 | $error = error_get_last(); 1359 | WP_CLI::error( sprintf( "Failed to read '%s': %s", $json_path, $error['message'] ) ); 1360 | } 1361 | try { 1362 | $composer_backup_decoded = $composer_json_obj->read(); 1363 | } catch ( Exception $e ) { 1364 | WP_CLI::error( sprintf( "Failed to parse '%s' as json: %s", $json_path, $e->getMessage() ) ); 1365 | } 1366 | 1367 | return [ $json_path, $composer_backup, $composer_backup_decoded ]; 1368 | } 1369 | 1370 | /** 1371 | * Registers a PHP shutdown function to revert composer.json unless 1372 | * referenced `$revert` flag is false. 1373 | * 1374 | * @param string $json_path Path to composer.json. 1375 | * @param string $composer_backup Original contents of composer.json. 1376 | * @param bool &$revert Flags whether to revert or not. 1377 | */ 1378 | private function register_revert_shutdown_function( $json_path, $composer_backup, &$revert ) { 1379 | // Allocate all needed memory beforehand as much as possible. 1380 | $revert_msg = "Reverted composer.json.\n"; 1381 | $revert_fail_msg = "Failed to revert composer.json.\n"; 1382 | $memory_msg = "WP-CLI ran out of memory. Please see https://bit.ly/wpclimem for further help.\n"; 1383 | $memory_string = 'Allowed memory size of'; 1384 | $error_array = [ 1385 | 'type' => 42, 1386 | 'message' => 'Some random dummy string to take up memory', 1387 | 'file' => 'Another random string, which would be a filename this time', 1388 | 'line' => 314, 1389 | ]; 1390 | 1391 | register_shutdown_function( 1392 | static function () use ( 1393 | $json_path, 1394 | $composer_backup, 1395 | &$revert, 1396 | $revert_msg, 1397 | $revert_fail_msg, 1398 | $memory_msg, 1399 | $memory_string, 1400 | $error_array 1401 | ) { 1402 | if ( $revert ) { 1403 | if ( false !== file_put_contents( $json_path, $composer_backup ) ) { 1404 | fwrite( STDERR, $revert_msg ); 1405 | } else { 1406 | fwrite( STDERR, $revert_fail_msg ); 1407 | } 1408 | } 1409 | 1410 | $error_array = error_get_last(); 1411 | if ( is_array( $error_array ) && false !== strpos( $error_array['message'], $memory_string ) ) { 1412 | fwrite( STDERR, $memory_msg ); 1413 | } 1414 | } 1415 | ); 1416 | } 1417 | 1418 | /** 1419 | * Check whether we are dealing with Composer version 2.0.0+. 1420 | * 1421 | * @return bool 1422 | */ 1423 | private function is_composer_v2() { 1424 | return version_compare( Composer::getVersion(), '2.0.0', '>=' ); 1425 | } 1426 | 1427 | /** 1428 | * Try to retrieve default branch via GitHub API. 1429 | * 1430 | * @param string $package_name GitHub package name to retrieve the default branch from. 1431 | * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults 1432 | * to false. 1433 | * @return string Default branch, or 'master' if it could not be retrieved. 1434 | */ 1435 | private function get_github_default_branch( $package_name, $insecure = false ) { 1436 | $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. 1437 | $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; 1438 | $options = [ 'insecure' => $insecure ]; 1439 | 1440 | $matches = []; 1441 | if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?.*?).git$#', $package_name, $matches ) ) { 1442 | $package_name = $matches['repo_name']; 1443 | } 1444 | 1445 | $github_api_repo_url = "https://api.github.com/repos/{$package_name}"; 1446 | $response = Utils\http_request( 'GET', $github_api_repo_url, null /*data*/, $headers, $options ); 1447 | if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { 1448 | WP_CLI::warning( 1449 | sprintf( 1450 | "Couldn't fetch default branch for package '%s' (HTTP code %d). Presuming default branch is 'master'.", 1451 | $package_name, 1452 | $response->status_code 1453 | ) 1454 | ); 1455 | return 'master'; 1456 | } 1457 | 1458 | $package_data = json_decode( $response->body ); 1459 | 1460 | if ( JSON_ERROR_NONE !== json_last_error() ) { 1461 | WP_CLI::warning( "Couldn't fetch default branch for package '%s' (failed to decode JSON response). Presuming default branch is 'master'." ); 1462 | return 'master'; 1463 | } 1464 | 1465 | $default_branch = $package_data->default_branch; 1466 | 1467 | if ( ! is_string( $default_branch ) || empty( $default_branch ) ) { 1468 | WP_CLI::warning( 1469 | sprintf( 1470 | "Couldn't fetch default branch for package '%s'. Presuming default branch is 'master'.", 1471 | $package_name 1472 | ) 1473 | ); 1474 | return 'master'; 1475 | } 1476 | 1477 | WP_CLI::debug( "Detected package default branch: {$default_branch}", 'packages' ); 1478 | 1479 | return $default_branch; 1480 | } 1481 | } 1482 | -------------------------------------------------------------------------------- /src/WP_CLI/JsonManipulator.php: -------------------------------------------------------------------------------- 1 | 9 | * Jordi Boggiano 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | namespace WP_CLI; // WP_CLI 16 | 17 | use Composer\Json\JsonFile; // WP_CLI 18 | use Composer\Repository\PlatformRepository; 19 | 20 | /** 21 | * @author Jordi Boggiano 22 | */ 23 | class JsonManipulator 24 | { 25 | private static $DEFINES = '(?(DEFINE) 26 | (? -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? ) 27 | (? true | false | null ) 28 | (? " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " ) 29 | (? \[ (?: (?&json) \s* (?: , (?&json) \s* )* )? \s* \] ) 30 | (? \s* (?&string) \s* : (?&json) \s* ) 31 | (? \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} ) 32 | (? \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) ) 33 | )'; 34 | 35 | private $contents; 36 | private $newline; 37 | private $indent; 38 | 39 | public function __construct($contents) 40 | { 41 | $contents = trim($contents); 42 | if ($contents === '') { 43 | $contents = '{}'; 44 | } 45 | if (!$this->pregMatch('#^\{(.*)\}$#s', $contents)) { 46 | throw new \InvalidArgumentException('The json file must be an object ({})'); 47 | } 48 | $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; 49 | $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; 50 | $this->detectIndenting(); 51 | } 52 | 53 | public function getContents() 54 | { 55 | return $this->contents . $this->newline; 56 | } 57 | 58 | public function addLink($type, $package, $constraint, $sortPackages = false, $caseInsensitive = false) // WP_CLI: caseInsensitive. 59 | { 60 | $decoded = JsonFile::parseJson($this->contents); 61 | 62 | // no link of that type yet 63 | if (!isset($decoded[$type])) { 64 | return $this->addMainKey($type, array($package => $constraint)); 65 | } 66 | 67 | $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. 68 | '(?P'.preg_quote(JsonFile::encode($type)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; 69 | if (!$this->pregMatch($regex, $this->contents, $matches)) { 70 | return false; 71 | } 72 | 73 | // WP_CLI: begin caseInsensitive. 74 | if ($caseInsensitive) { 75 | // Just zap any existing packages first in a case insensitive manner. 76 | $this->removeSubNode($type, $package, $caseInsensitive); 77 | return $this->addLink($type, $package, $constraint, $sortPackages); 78 | } 79 | // WP_CLI: end caseInsensitive. 80 | 81 | $links = $matches['value']; 82 | 83 | // try to find existing link 84 | $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); 85 | $regex = '{'.self::$DEFINES.'"(?P'.$packageRegex.')"(\s*:\s*)(?&string)}ix'; 86 | if ($this->pregMatch($regex, $links, $packageMatches)) { 87 | // update existing link 88 | $existingPackage = $packageMatches['package']; 89 | $packageRegex = str_replace('/', '\\\\?/', preg_quote($existingPackage)); 90 | $links = preg_replace_callback('{'.self::$DEFINES.'"'.$packageRegex.'"(?P\s*:\s*)(?&string)}ix', function ($m) use ($existingPackage, $constraint) { 91 | return JsonFile::encode(str_replace('\\/', '/', $existingPackage)) . $m['separator'] . '"' . $constraint . '"'; 92 | }, $links); 93 | } else { 94 | if ($this->pregMatch('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { 95 | // link missing but non empty links 96 | $links = preg_replace( 97 | '{'.preg_quote($match[1]).'$}', 98 | // addcslashes is used to double up backslashes/$ since preg_replace resolves them as back references otherwise, see #1588 99 | addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\$'), 100 | $links 101 | ); 102 | } else { 103 | // links empty 104 | $links = '{' . $this->newline . 105 | $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . 106 | $this->indent . '}'; 107 | } 108 | } 109 | 110 | if (true === $sortPackages) { 111 | $requirements = json_decode($links, true); 112 | $this->sortPackages($requirements); 113 | $links = $this->format($requirements); 114 | } 115 | 116 | $this->contents = $matches['start'] . $matches['property'] . $links . $matches['end']; 117 | 118 | return true; 119 | } 120 | 121 | /** 122 | * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. 123 | * 124 | * @link https://getcomposer.org/doc/02-libraries.md#platform-packages 125 | * 126 | * @param array $packages 127 | */ 128 | private function sortPackages(array &$packages = array()) 129 | { 130 | $prefix = function ($requirement) { 131 | if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $requirement)) { 132 | return preg_replace( 133 | array( 134 | '/^php/', 135 | '/^hhvm/', 136 | '/^ext/', 137 | '/^lib/', 138 | '/^\D/', 139 | ), 140 | array( 141 | '0-$0', 142 | '1-$0', 143 | '2-$0', 144 | '3-$0', 145 | '4-$0', 146 | ), 147 | $requirement 148 | ); 149 | } 150 | 151 | return '5-'.$requirement; 152 | }; 153 | 154 | uksort($packages, function ($a, $b) use ($prefix) { 155 | return strnatcmp($prefix($a), $prefix($b)); 156 | }); 157 | } 158 | 159 | public function addRepository($name, $config) 160 | { 161 | return $this->addSubNode('repositories', $name, $config); 162 | } 163 | 164 | public function removeRepository($name) 165 | { 166 | return $this->removeSubNode('repositories', $name); 167 | } 168 | 169 | public function addConfigSetting($name, $value) 170 | { 171 | return $this->addSubNode('config', $name, $value); 172 | } 173 | 174 | public function removeConfigSetting($name) 175 | { 176 | return $this->removeSubNode('config', $name); 177 | } 178 | 179 | public function addProperty($name, $value) 180 | { 181 | if (substr($name, 0, 6) === 'extra.') { 182 | return $this->addSubNode('extra', substr($name, 6), $value); 183 | } 184 | 185 | return $this->addMainKey($name, $value); 186 | } 187 | 188 | public function removeProperty($name) 189 | { 190 | if (substr($name, 0, 6) === 'extra.') { 191 | return $this->removeSubNode('extra', substr($name, 6)); 192 | } 193 | 194 | return $this->removeMainKey($name); 195 | } 196 | 197 | public function addSubNode($mainNode, $name, $value, $caseInsensitive = false) // WP_CLI: caseInsensitive. 198 | { 199 | $decoded = JsonFile::parseJson($this->contents); 200 | 201 | $subName = null; 202 | if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { 203 | list($name, $subName) = explode('.', $name, 2); 204 | } 205 | 206 | // no main node yet 207 | if (!isset($decoded[$mainNode])) { 208 | if ($subName !== null) { 209 | $this->addMainKey($mainNode, array($name => array($subName => $value))); 210 | } else { 211 | $this->addMainKey($mainNode, array($name => $value)); 212 | } 213 | 214 | return true; 215 | } 216 | 217 | // main node content not match-able 218 | $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. 219 | preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; 220 | 221 | try { 222 | if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { 223 | return false; 224 | } 225 | } catch (\RuntimeException $e) { 226 | if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { 227 | return false; 228 | } 229 | throw $e; 230 | } 231 | 232 | // WP_CLI: begin caseInsensitive. 233 | if ($caseInsensitive) { 234 | // Just zap any existing names first in a case insensitive manner. 235 | $this->removeSubNode($mainNode, $name, $caseInsensitive); 236 | return $this->addSubNode($mainNode, $name, $value); 237 | } 238 | // WP_CLI: end caseInsensitive. 239 | 240 | $children = $match['content']; 241 | // invalid match due to un-regexable content, abort 242 | if (!@json_decode($children)) { 243 | return false; 244 | } 245 | 246 | $that = $this; 247 | 248 | // child exists 249 | $childRegex = '{'.self::$DEFINES.'(?P"'.preg_quote($name).'"\s*:\s*)(?P(?&json))(?P,?)}x'; 250 | if ($this->pregMatch($childRegex, $children, $matches)) { 251 | $children = preg_replace_callback($childRegex, function ($matches) use ($name, $subName, $value, $that) { 252 | if ($subName !== null) { 253 | $curVal = json_decode($matches['content'], true); 254 | if (!is_array($curVal)) { 255 | $curVal = array(); 256 | } 257 | $curVal[$subName] = $value; 258 | $value = $curVal; 259 | } 260 | 261 | return $matches['start'] . $that->format($value, 1) . $matches['end']; 262 | }, $children); 263 | } else { 264 | $this->pregMatch('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $children, $match); 265 | 266 | $whitespace = ''; 267 | if (!empty($match['trailingspace'])) { 268 | $whitespace = $match['trailingspace']; 269 | } 270 | 271 | if (!empty($match['content'])) { 272 | if ($subName !== null) { 273 | $value = array($subName => $value); 274 | } 275 | 276 | // child missing but non empty children 277 | $children = preg_replace( 278 | '#'.$whitespace.'}$#', 279 | addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}', '\\$'), 280 | $children 281 | ); 282 | } else { 283 | if ($subName !== null) { 284 | $value = array($subName => $value); 285 | } 286 | 287 | // children present but empty 288 | $children = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}'; 289 | } 290 | } 291 | 292 | $this->contents = preg_replace_callback($nodeRegex, function ($m) use ($children) { 293 | return $m['start'] . $children . $m['end']; 294 | }, $this->contents); 295 | 296 | return true; 297 | } 298 | 299 | public function removeSubNode($mainNode, $name, $caseInsensitive = false) // WP_CLI: caseInsensitive. 300 | { 301 | $decoded = JsonFile::parseJson($this->contents); 302 | 303 | // no node or empty node 304 | if (empty($decoded[$mainNode])) { 305 | return true; 306 | } 307 | 308 | // WP_CLI: begin caseInsensitive. 309 | if ( $caseInsensitive ) { 310 | // This is more or less a copy of the code at the start of `addLink()` above. 311 | $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. 312 | '(?P'.preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; 313 | if (!$this->pregMatch($regex, $this->contents, $matches)) { 314 | return true; 315 | } 316 | 317 | $value = $matches['value']; // Renamed from `$links` in `addLink()` case above. 318 | 319 | // try to find existing values 320 | $nameRegex = str_replace('/', '\\\\?/', preg_quote($name)); // Renamed from `$packageRegex` in `addLink()` case above. 321 | $regex = '{'.self::$DEFINES.'"(?P'.$nameRegex.')"(\s*:\s*)(?&json)}ix'; // Need `(?&json)` PCRE subroutine here, as opposed to `(?&string)` in `addLink()` case. 322 | if (preg_match_all($regex, $value, $nameMatches)) { 323 | // Just zap them all individually in a case sensitive manner. 324 | foreach ( $nameMatches['name'] as $nameMatch ) { 325 | $this->removeSubNode($mainNode, $nameMatch); 326 | } 327 | } 328 | 329 | return true; 330 | } 331 | // WP_CLI: end caseInsensitive. 332 | 333 | // no node content match-able 334 | $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. 335 | preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; 336 | try { 337 | if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { 338 | return false; 339 | } 340 | } catch (\RuntimeException $e) { 341 | if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { 342 | return false; 343 | } 344 | throw $e; 345 | } 346 | 347 | $children = $match['content']; 348 | 349 | // invalid match due to un-regexable content, abort 350 | if (!@json_decode($children, true)) { 351 | return false; 352 | } 353 | 354 | $subName = null; 355 | if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { 356 | list($name, $subName) = explode('.', $name, 2); 357 | } 358 | 359 | // no node to remove 360 | if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { 361 | return true; 362 | } 363 | 364 | // try and find a match for the subkey 365 | if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) { 366 | // find best match for the value of "name" 367 | if (preg_match_all('{'.self::$DEFINES.'"'.preg_quote($name).'"\s*:\s*(?:(?&json))}x', $children, $matches)) { 368 | $bestMatch = ''; 369 | foreach ($matches[0] as $match) { 370 | if (strlen($bestMatch) < strlen($match)) { 371 | $bestMatch = $match; 372 | } 373 | } 374 | $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}', '', $children, -1, $count); // WP_CLI: As the preg_match_all() above is case-sensitive, so should this be. 375 | if (1 !== $count) { 376 | $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}', '', $childrenClean, -1, $count); // WP_CLI: ditto. 377 | if (1 !== $count) { 378 | return false; 379 | } 380 | } 381 | } 382 | } else { 383 | $childrenClean = $children; 384 | } 385 | 386 | // no child data left, $name was the only key in 387 | $this->pregMatch('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $childrenClean, $match); 388 | if (empty($match['content'])) { 389 | $newline = $this->newline; 390 | $indent = $this->indent; 391 | 392 | $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($indent, $newline) { 393 | return $matches['start'] . '{' . $newline . $indent . '}' . $matches['end']; 394 | }, $this->contents); 395 | 396 | // we have a subname, so we restore the rest of $name 397 | if ($subName !== null) { 398 | $curVal = json_decode($children, true); 399 | unset($curVal[$name][$subName]); 400 | $this->addSubNode($mainNode, $name, $curVal[$name]); 401 | } 402 | 403 | return true; 404 | } 405 | 406 | $that = $this; 407 | $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { 408 | if ($subName !== null) { 409 | $curVal = json_decode($matches['content'], true); 410 | unset($curVal[$name][$subName]); 411 | $childrenClean = $that->format($curVal, 0); 412 | } 413 | 414 | return $matches['start'] . $childrenClean . $matches['end']; 415 | }, $this->contents); 416 | 417 | return true; 418 | } 419 | 420 | public function addMainKey($key, $content) 421 | { 422 | $decoded = JsonFile::parseJson($this->contents); 423 | $content = $this->format($content); 424 | 425 | // key exists already 426 | $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. 427 | '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))(?P.*)}sx'; 428 | if (isset($decoded[$key]) && $this->pregMatch($regex, $this->contents, $matches)) { 429 | // invalid match due to un-regexable content, abort 430 | if (!@json_decode('{'.$matches['key'].'}')) { 431 | return false; 432 | } 433 | 434 | $this->contents = $matches['start'] . JsonFile::encode($key).': '.$content . $matches['end']; 435 | 436 | return true; 437 | } 438 | 439 | // append at the end of the file and keep whitespace 440 | if ($this->pregMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { 441 | $this->contents = preg_replace( 442 | '#'.$match[1].'\}$#', 443 | addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\$'), 444 | $this->contents 445 | ); 446 | 447 | return true; 448 | } 449 | 450 | // append at the end of the file 451 | $this->contents = preg_replace( 452 | '#\}$#', 453 | addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\$'), 454 | $this->contents 455 | ); 456 | 457 | return true; 458 | } 459 | 460 | public function removeMainKey($key) 461 | { 462 | $decoded = JsonFile::parseJson($this->contents); 463 | 464 | if (!isset($decoded[$key])) { 465 | return true; 466 | } 467 | 468 | // key exists already 469 | $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. 470 | '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))\s*,?\s*(?P.*)}sx'; 471 | if ($this->pregMatch($regex, $this->contents, $matches)) { 472 | // invalid match due to un-regexable content, abort 473 | if (!@json_decode('{'.$matches['removal'].'}')) { 474 | return false; 475 | } 476 | 477 | // check that we are not leaving a dangling comma on the previous line if the last line was removed 478 | if (preg_match('#,\s*$#', $matches['start']) && preg_match('#^\}$#', $matches['end'])) { 479 | $matches['start'] = rtrim(preg_replace('#,(\s*)$#', '$1', $matches['start']), $this->indent); 480 | } 481 | 482 | $this->contents = $matches['start'] . $matches['end']; 483 | if (preg_match('#^\{\s*\}\s*$#', $this->contents)) { 484 | $this->contents = "{\n}"; 485 | } 486 | 487 | return true; 488 | } 489 | 490 | return false; 491 | } 492 | 493 | public function format($data, $depth = 0) 494 | { 495 | if (is_array($data)) { 496 | reset($data); 497 | 498 | if (is_numeric(key($data))) { 499 | foreach ($data as $key => $val) { 500 | $data[$key] = $this->format($val, $depth + 1); 501 | } 502 | 503 | return '['.implode(', ', $data).']'; 504 | } 505 | 506 | $out = '{' . $this->newline; 507 | $elems = array(); 508 | foreach ($data as $key => $val) { 509 | $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); 510 | } 511 | 512 | return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; 513 | } 514 | 515 | return JsonFile::encode($data); 516 | } 517 | 518 | protected function detectIndenting() 519 | { 520 | if ($this->pregMatch('{^([ \t]+)"}m', $this->contents, $match)) { 521 | $this->indent = $match[1]; 522 | } else { 523 | $this->indent = ' '; 524 | } 525 | } 526 | 527 | protected function pregMatch($re, $str, &$matches = array()) 528 | { 529 | $count = preg_match($re, $str, $matches); 530 | 531 | if ($count === false) { 532 | switch (preg_last_error()) { 533 | case PREG_NO_ERROR: 534 | throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR', PREG_NO_ERROR); 535 | case PREG_INTERNAL_ERROR: 536 | throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR', PREG_INTERNAL_ERROR); 537 | case PREG_BACKTRACK_LIMIT_ERROR: 538 | throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR', PREG_BACKTRACK_LIMIT_ERROR); 539 | case PREG_RECURSION_LIMIT_ERROR: 540 | throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR', PREG_RECURSION_LIMIT_ERROR); 541 | case PREG_BAD_UTF8_ERROR: 542 | throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR', PREG_BAD_UTF8_ERROR); 543 | case PREG_BAD_UTF8_OFFSET_ERROR: 544 | throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR', PREG_BAD_UTF8_OFFSET_ERROR); 545 | case 6: // PREG_JIT_STACKLIMIT_ERROR 546 | if (PHP_VERSION_ID > 70000) { 547 | throw new \RuntimeException('Failed to execute regex: PREG_JIT_STACKLIMIT_ERROR', 6); 548 | } 549 | // fallthrough 550 | 551 | default: 552 | throw new \RuntimeException('Failed to execute regex: Unknown error'); 553 | } 554 | } 555 | 556 | return $count; 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /src/WP_CLI/Package/Compat/Min_Composer_1_10/NullIOMethodsTrait.php: -------------------------------------------------------------------------------- 1 | ]+)>#', '$1$2', $messages ); 31 | foreach ( $messages as $message ) { 32 | // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags 33 | WP_CLI::log( strip_tags( trim( $message ) ) ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WP_CLI/Package/Compat/Min_Composer_2_3/NullIOMethodsTrait.php: -------------------------------------------------------------------------------- 1 | ]+)>#', '$1$2', $messages ); 31 | foreach ( $messages as $message ) { 32 | // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags 33 | WP_CLI::log( strip_tags( trim( $message ) ) ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/WP_CLI/Package/Compat/NullIOMethodsTrait.php: -------------------------------------------------------------------------------- 1 | setAccessible( true ); 25 | $this->prev_logger = $class_wp_cli_logger->getValue(); 26 | 27 | $this->logger = new Execution(); 28 | WP_CLI::set_logger( $this->logger ); 29 | 30 | // Enable exit exception. 31 | 32 | $class_wp_cli_capture_exit = new \ReflectionProperty( 'WP_CLI', 'capture_exit' ); 33 | $class_wp_cli_capture_exit->setAccessible( true ); 34 | $class_wp_cli_capture_exit->setValue( null, true ); 35 | 36 | $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-package-composer-json-', true ) . '/'; 37 | mkdir( $this->temp_dir ); 38 | } 39 | 40 | public function tear_down() { 41 | // Restore logger. 42 | WP_CLI::set_logger( $this->prev_logger ); 43 | 44 | // Restore exit exception. 45 | $class_wp_cli_capture_exit = new \ReflectionProperty( 'WP_CLI', 'capture_exit' ); 46 | $class_wp_cli_capture_exit->setAccessible( true ); 47 | $class_wp_cli_capture_exit->setValue( null, $this->prev_capture_exit ); 48 | 49 | rmdir( $this->temp_dir ); 50 | 51 | parent::tear_down(); 52 | } 53 | 54 | public function test_create_default_composer_json() { 55 | $create_default_composer_json = new \ReflectionMethod( 'Package_Command', 'create_default_composer_json' ); 56 | $create_default_composer_json->setAccessible( true ); 57 | 58 | $package = new Package_Command(); 59 | 60 | // Fail with bad directory. 61 | $exception = null; 62 | try { 63 | $actual = $create_default_composer_json->invoke( $package, '' ); 64 | } catch ( ExitException $ex ) { 65 | $exception = $ex; 66 | } 67 | $this->assertTrue( null !== $exception ); 68 | $this->assertTrue( 1 === $exception->getCode() ); 69 | $this->assertTrue( empty( $this->logger->stdout ) ); 70 | $this->assertTrue( false !== strpos( $this->logger->stderr, 'Error: Composer directory' ) ); 71 | 72 | // Succeed. 73 | $expected = $this->temp_dir . 'packages/composer.json'; 74 | $actual = $create_default_composer_json->invoke( $package, $expected ); 75 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 76 | $this->assertTrue( false !== strpos( file_get_contents( $actual ), 'wp-cli/wp-cli' ) ); 77 | unlink( $actual ); 78 | rmdir( dirname( $actual ) ); 79 | } 80 | 81 | public function test_get_composer_json_path() { 82 | $env_test = getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ); 83 | $env_home = getenv( 'HOME' ); 84 | $env_wp_cli_packages_dir = getenv( 'WP_CLI_PACKAGES_DIR' ); 85 | 86 | $get_composer_json_path = new \ReflectionMethod( 'Package_Command', 'get_composer_json_path' ); 87 | $get_composer_json_path->setAccessible( true ); 88 | 89 | $package = new Package_Command(); 90 | 91 | putenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH=1' ); 92 | putenv( 'HOME=' . $this->temp_dir ); 93 | 94 | // Create in HOME. 95 | putenv( 'WP_CLI_PACKAGES_DIR' ); 96 | $expected = $this->temp_dir . '.wp-cli/packages/composer.json'; 97 | $actual = $get_composer_json_path->invoke( $package ); 98 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 99 | $this->assertTrue( false !== strpos( file_get_contents( $actual ), 'wp-cli/wp-cli' ) ); 100 | unlink( $actual ); 101 | rmdir( dirname( $actual ) ); 102 | rmdir( dirname( dirname( $actual ) ) ); 103 | 104 | // Create in WP_CLI_PACKAGES_DIR. 105 | putenv( 'WP_CLI_PACKAGES_DIR=' . $this->temp_dir . 'packages' ); 106 | $expected = $this->temp_dir . 'packages/composer.json'; 107 | $actual = $get_composer_json_path->invoke( $package ); 108 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 109 | $this->assertTrue( false !== strpos( file_get_contents( $actual ), 'wp-cli/wp-cli' ) ); 110 | unlink( $actual ); 111 | rmdir( dirname( $actual ) ); 112 | 113 | // Do nothing as already exists. 114 | putenv( 'WP_CLI_PACKAGES_DIR=' . $this->temp_dir . 'packages' ); 115 | $expected = $this->temp_dir . 'packages/composer.json'; 116 | mkdir( $this->temp_dir . 'packages' ); 117 | touch( $expected ); 118 | $actual = $get_composer_json_path->invoke( $package ); 119 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 120 | $this->assertSame( 0, filesize( $actual ) ); 121 | unlink( $actual ); 122 | rmdir( dirname( $actual ) ); 123 | 124 | putenv( false === $env_test ? 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' : "WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH=$env_test" ); 125 | putenv( false === $env_home ? 'HOME' : "HOME=$env_home" ); 126 | putenv( false === $env_wp_cli_packages_dir ? 'WP_CLI_PACKAGES_DIR' : "WP_CLI_PACKAGES_DIR=$env_wp_cli_packages_dir" ); 127 | } 128 | 129 | public function test_get_composer_json_path_backup_decoded() { 130 | $env_test = getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ); 131 | $env_wp_cli_packages_dir = getenv( 'WP_CLI_PACKAGES_DIR' ); 132 | 133 | putenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH=1' ); 134 | putenv( 'WP_CLI_PACKAGES_DIR=' . $this->temp_dir . 'packages' ); 135 | 136 | $get_composer_json_path_backup_decoded = new \ReflectionMethod( 'Package_Command', 'get_composer_json_path_backup_decoded' ); 137 | $get_composer_json_path_backup_decoded->setAccessible( true ); 138 | 139 | $package = new Package_Command(); 140 | 141 | // Fail with bad json. 142 | $expected = $this->temp_dir . 'packages/composer.json'; 143 | mkdir( $this->temp_dir . 'packages' ); 144 | file_put_contents( $expected, '{' ); 145 | $exception = null; 146 | try { 147 | $actual = $get_composer_json_path_backup_decoded->invoke( $package ); 148 | } catch ( ExitException $ex ) { 149 | $exception = $ex; 150 | } 151 | $this->assertTrue( null !== $exception ); 152 | $this->assertTrue( 1 === $exception->getCode() ); 153 | $this->assertTrue( empty( $this->logger->stdout ) ); 154 | $this->assertTrue( false !== strpos( $this->logger->stderr, 'Error: Failed to parse' ) ); 155 | unlink( $expected ); 156 | rmdir( dirname( $expected ) ); 157 | 158 | // Succeed with newly created. 159 | $expected = $this->temp_dir . 'packages/composer.json'; 160 | list( $actual, $content, $decoded ) = $get_composer_json_path_backup_decoded->invoke( $package ); 161 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 162 | $this->assertTrue( false !== strpos( file_get_contents( $actual ), 'wp-cli/wp-cli' ) ); 163 | $this->assertSame( file_get_contents( $actual ), $content ); 164 | $this->assertFalse( empty( $decoded ) ); 165 | unlink( $expected ); 166 | rmdir( dirname( $expected ) ); 167 | 168 | // Succeed with blank. 169 | $expected = $this->temp_dir . 'packages/composer.json'; 170 | mkdir( $this->temp_dir . 'packages' ); 171 | file_put_contents( $expected, '{}' ); 172 | list( $actual, $content, $decoded ) = $get_composer_json_path_backup_decoded->invoke( $package ); 173 | $this->assertSame( $expected, $this->mac_safe_path( $actual ) ); 174 | $this->assertSame( '{}', $content ); 175 | $this->assertTrue( empty( $decoded ) ); 176 | unlink( $expected ); 177 | rmdir( dirname( $expected ) ); 178 | 179 | putenv( false === $env_test ? 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' : "WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH=$env_test" ); 180 | putenv( false === $env_wp_cli_packages_dir ? 'WP_CLI_PACKAGES_DIR' : "WP_CLI_PACKAGES_DIR=$env_wp_cli_packages_dir" ); 181 | } 182 | 183 | private function mac_safe_path( $path ) { 184 | return preg_replace( '#^/private/(var|tmp)/#i', '/$1/', $path ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - package-command.php 3 | --------------------------------------------------------------------------------