├── .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 ├── core-command.php ├── features ├── core-check-update.feature ├── core-download.feature ├── core-install.feature ├── core-update-db.feature ├── core-update.feature ├── core-version.feature └── core.feature ├── phpcs.xml.dist ├── src ├── Core_Command.php └── WP_CLI │ └── Core │ ├── CoreUpgrader.php │ └── NonDestructiveCoreUpgrader.php ├── templates └── versions.mustache └── 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 | .phpcs.xml 3 | wp-cli.local.yml 4 | node_modules/ 5 | vendor/ 6 | *.zip 7 | *.tar.gz 8 | composer.lock 9 | phpunit.xml 10 | phpcs.xml 11 | *.log 12 | -------------------------------------------------------------------------------- /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/core-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/core-command 2 | =================== 3 | 4 | Downloads, installs, updates, and manages a WordPress installation. 5 | 6 | [![Testing](https://github.com/wp-cli/core-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/core-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 core 15 | 16 | Downloads, installs, updates, and manages a WordPress installation. 17 | 18 | ~~~ 19 | wp core 20 | ~~~ 21 | 22 | **EXAMPLES** 23 | 24 | # Download WordPress core 25 | $ wp core download --locale=nl_NL 26 | Downloading WordPress 4.5.2 (nl_NL)... 27 | md5 hash verified: c5366d05b521831dd0b29dfc386e56a5 28 | Success: WordPress downloaded. 29 | 30 | # Install WordPress 31 | $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com 32 | Success: WordPress installed successfully. 33 | 34 | # Display the WordPress version 35 | $ wp core version 36 | 4.5.2 37 | 38 | 39 | 40 | ### wp core check-update 41 | 42 | Checks for WordPress updates via Version Check API. 43 | 44 | ~~~ 45 | wp core check-update [--minor] [--major] [--force-check] [--field=] [--fields=] [--format=] 46 | ~~~ 47 | 48 | Lists the most recent versions when there are updates available, 49 | or success message when up to date. 50 | 51 | **OPTIONS** 52 | 53 | [--minor] 54 | Compare only the first two parts of the version number. 55 | 56 | [--major] 57 | Compare only the first part of the version number. 58 | 59 | [--force-check] 60 | Bypass the transient cache and force a fresh update check. 61 | 62 | [--field=] 63 | Prints the value of a single field for each update. 64 | 65 | [--fields=] 66 | Limit the output to specific object fields. Defaults to version,update_type,package_url. 67 | 68 | [--format=] 69 | Render output in a particular format. 70 | --- 71 | default: table 72 | options: 73 | - table 74 | - csv 75 | - count 76 | - json 77 | - yaml 78 | --- 79 | 80 | **EXAMPLES** 81 | 82 | $ wp core check-update 83 | +---------+-------------+-------------------------------------------------------------+ 84 | | version | update_type | package_url | 85 | +---------+-------------+-------------------------------------------------------------+ 86 | | 4.5.2 | major | https://downloads.wordpress.org/release/wordpress-4.5.2.zip | 87 | +---------+-------------+-------------------------------------------------------------+ 88 | 89 | 90 | 91 | ### wp core download 92 | 93 | Downloads core WordPress files. 94 | 95 | ~~~ 96 | wp core download [] [--path=] [--locale=] [--version=] [--skip-content] [--force] [--insecure] [--extract] 97 | ~~~ 98 | 99 | Downloads and extracts WordPress core files to the specified path. Uses 100 | current directory when no path is specified. Downloaded build is verified 101 | to have the correct md5 and then cached to the local filesystem. 102 | Subsequent uses of command will use the local cache if it still exists. 103 | 104 | **OPTIONS** 105 | 106 | [] 107 | Download directly from a provided URL instead of fetching the URL from the wordpress.org servers. 108 | 109 | [--path=] 110 | Specify the path in which to install WordPress. Defaults to current 111 | directory. 112 | 113 | [--locale=] 114 | Select which language you want to download. 115 | 116 | [--version=] 117 | Select which version you want to download. Accepts a version number, 'latest' or 'nightly'. 118 | 119 | [--skip-content] 120 | Download WP without the default themes and plugins. 121 | 122 | [--force] 123 | Overwrites existing files, if present. 124 | 125 | [--insecure] 126 | Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 127 | 128 | [--extract] 129 | Whether to extract the downloaded file. Defaults to true. 130 | 131 | **EXAMPLES** 132 | 133 | $ wp core download --locale=nl_NL 134 | Downloading WordPress 4.5.2 (nl_NL)... 135 | md5 hash verified: c5366d05b521831dd0b29dfc386e56a5 136 | Success: WordPress downloaded. 137 | 138 | 139 | 140 | ### wp core install 141 | 142 | Runs the standard WordPress installation process. 143 | 144 | ~~~ 145 | wp core install --url= --title= --admin_user= [--admin_password=] --admin_email= [--locale=] [--skip-email] 146 | ~~~ 147 | 148 | Creates the WordPress tables in the database using the URL, title, and 149 | default admin user details provided. Performs the famous 5 minute install 150 | in seconds or less. 151 | 152 | Note: if you've installed WordPress in a subdirectory, then you'll need 153 | to `wp option update siteurl` after `wp core install`. For instance, if 154 | WordPress is installed in the `/wp` directory and your domain is example.com, 155 | then you'll need to run `wp option update siteurl http://example.com/wp` for 156 | your WordPress installation to function properly. 157 | 158 | Note: When using custom user tables (e.g. `CUSTOM_USER_TABLE`), the admin 159 | email and password are ignored if the user_login already exists. If the 160 | user_login doesn't exist, a new user will be created. 161 | 162 | **OPTIONS** 163 | 164 | --url= 165 | The address of the new site. 166 | 167 | --title= 168 | The title of the new site. 169 | 170 | --admin_user= 171 | The name of the admin user. 172 | 173 | [--admin_password=] 174 | The password for the admin user. Defaults to randomly generated string. 175 | 176 | --admin_email= 177 | The email address for the admin user. 178 | 179 | [--locale=] 180 | The locale/language for the installation (e.g. `de_DE`). Default is `en_US`. 181 | 182 | [--skip-email] 183 | Don't send an email notification to the new admin user. 184 | 185 | **EXAMPLES** 186 | 187 | # Install WordPress in 5 seconds 188 | $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com 189 | Success: WordPress installed successfully. 190 | 191 | # Install WordPress without disclosing admin_password to bash history 192 | $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_email=info@example.com --prompt=admin_password < admin_password.txt 193 | 194 | 195 | 196 | ### wp core is-installed 197 | 198 | Checks if WordPress is installed. 199 | 200 | ~~~ 201 | wp core is-installed [--network] 202 | ~~~ 203 | 204 | Determines whether WordPress is installed by checking if the standard 205 | database tables are installed. Doesn't produce output; uses exit codes 206 | to communicate whether WordPress is installed. 207 | 208 | **OPTIONS** 209 | 210 | [--network] 211 | Check if this is a multisite installation. 212 | 213 | **EXAMPLES** 214 | 215 | # Bash script for checking if WordPress is not installed. 216 | 217 | if ! wp core is-installed 2>/dev/null; then 218 | # WP is not installed. Let's try installing it. 219 | wp core install 220 | fi 221 | 222 | # Bash script for checking if WordPress is installed, with fallback. 223 | 224 | if wp core is-installed 2>/dev/null; then 225 | # WP is installed. Let's do some things we should only do in a confirmed WP environment. 226 | wp core verify-checksums 227 | else 228 | # Fallback if WP is not installed. 229 | echo 'Hey Friend, you are in the wrong spot. Move in to your WordPress directory and try again.' 230 | fi 231 | 232 | 233 | 234 | ### wp core multisite-convert 235 | 236 | Transforms an existing single-site installation into a multisite installation. 237 | 238 | ~~~ 239 | wp core multisite-convert [--title=] [--base=] [--subdomains] [--skip-config] 240 | ~~~ 241 | 242 | Creates the multisite database tables, and adds the multisite constants 243 | to wp-config.php. 244 | 245 | For those using WordPress with Apache, remember to update the `.htaccess` 246 | file with the appropriate multisite rewrite rules. 247 | 248 | [Review the multisite documentation](https://wordpress.org/support/article/create-a-network/) 249 | for more details about how multisite works. 250 | 251 | **OPTIONS** 252 | 253 | [--title=] 254 | The title of the new network. 255 | 256 | [--base=] 257 | Base path after the domain name that each site url will start with. 258 | --- 259 | default: / 260 | --- 261 | 262 | [--subdomains] 263 | If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. 264 | 265 | [--skip-config] 266 | Don't add multisite constants to wp-config.php. 267 | 268 | **EXAMPLES** 269 | 270 | $ wp core multisite-convert 271 | Set up multisite database tables. 272 | Added multisite constants to wp-config.php. 273 | Success: Network installed. Don't forget to set up rewrite rules. 274 | 275 | 276 | 277 | ### wp core multisite-install 278 | 279 | Installs WordPress multisite from scratch. 280 | 281 | ~~~ 282 | wp core multisite-install [--url=] [--base=] [--subdomains] --title= --admin_user= [--admin_password=] --admin_email= [--skip-email] [--skip-config] 283 | ~~~ 284 | 285 | Creates the WordPress tables in the database using the URL, title, and 286 | default admin user details provided. Then, creates the multisite tables 287 | in the database and adds multisite constants to the wp-config.php. 288 | 289 | For those using WordPress with Apache, remember to update the `.htaccess` 290 | file with the appropriate multisite rewrite rules. 291 | 292 | **OPTIONS** 293 | 294 | [--url=] 295 | The address of the new site. 296 | 297 | [--base=] 298 | Base path after the domain name that each site url in the network will start with. 299 | --- 300 | default: / 301 | --- 302 | 303 | [--subdomains] 304 | If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. 305 | 306 | --title= 307 | The title of the new site. 308 | 309 | --admin_user= 310 | The name of the admin user. 311 | --- 312 | default: admin 313 | --- 314 | 315 | [--admin_password=] 316 | The password for the admin user. Defaults to randomly generated string. 317 | 318 | --admin_email= 319 | The email address for the admin user. 320 | 321 | [--skip-email] 322 | Don't send an email notification to the new admin user. 323 | 324 | [--skip-config] 325 | Don't add multisite constants to wp-config.php. 326 | 327 | **EXAMPLES** 328 | 329 | $ wp core multisite-install --title="Welcome to the WordPress" \ 330 | > --admin_user="admin" --admin_password="password" \ 331 | > --admin_email="user@example.com" 332 | Single site database tables already present. 333 | Set up multisite database tables. 334 | Added multisite constants to wp-config.php. 335 | Success: Network installed. Don't forget to set up rewrite rules. 336 | 337 | 338 | 339 | ### wp core update 340 | 341 | Updates WordPress to a newer version. 342 | 343 | ~~~ 344 | wp core update [] [--minor] [--version=] [--force] [--locale=] [--insecure] 345 | ~~~ 346 | 347 | Defaults to updating WordPress to the latest version. 348 | 349 | If you see "Error: Another update is currently in progress.", you may 350 | need to run `wp option delete core_updater.lock` after verifying another 351 | update isn't actually running. 352 | 353 | **OPTIONS** 354 | 355 | [] 356 | Path to zip file to use, instead of downloading from wordpress.org. 357 | 358 | [--minor] 359 | Only perform updates for minor releases (e.g. update from WP 4.3 to 4.3.3 instead of 4.4.2). 360 | 361 | [--version=] 362 | Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly'. 363 | 364 | [--force] 365 | Update even when installed WP version is greater than the requested version. 366 | 367 | [--locale=] 368 | Select which language you want to download. 369 | 370 | [--insecure] 371 | Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 372 | 373 | **EXAMPLES** 374 | 375 | # Update WordPress 376 | $ wp core update 377 | Updating to version 4.5.2 (en_US)... 378 | Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip... 379 | Unpacking the update... 380 | Cleaning up files... 381 | No files found that need cleaning up 382 | Success: WordPress updated successfully. 383 | 384 | # Update WordPress using zip file. 385 | $ wp core update ../latest.zip 386 | Starting update... 387 | Unpacking the update... 388 | Success: WordPress updated successfully. 389 | 390 | # Update WordPress to 3.1 forcefully 391 | $ wp core update --version=3.1 --force 392 | Updating to version 3.1 (en_US)... 393 | Downloading update from https://wordpress.org/wordpress-3.1.zip... 394 | Unpacking the update... 395 | Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually. 396 | Success: WordPress updated successfully. 397 | 398 | 399 | 400 | ### wp core update-db 401 | 402 | Runs the WordPress database update procedure. 403 | 404 | ~~~ 405 | wp core update-db [--network] [--dry-run] 406 | ~~~ 407 | 408 | **OPTIONS** 409 | 410 | [--network] 411 | Update databases for all sites on a network 412 | 413 | [--dry-run] 414 | Compare database versions without performing the update. 415 | 416 | **EXAMPLES** 417 | 418 | # Update the WordPress database. 419 | $ wp core update-db 420 | Success: WordPress database upgraded successfully from db version 36686 to 35700. 421 | 422 | # Update databases for all sites on a network. 423 | $ wp core update-db --network 424 | WordPress database upgraded successfully from db version 35700 to 29630 on example.com/ 425 | Success: WordPress database upgraded on 123/123 sites. 426 | 427 | 428 | 429 | ### wp core version 430 | 431 | Displays the WordPress version. 432 | 433 | ~~~ 434 | wp core version [--extra] 435 | ~~~ 436 | 437 | **OPTIONS** 438 | 439 | [--extra] 440 | Show extended version information. 441 | 442 | **EXAMPLES** 443 | 444 | # Display the WordPress version 445 | $ wp core version 446 | 4.5.2 447 | 448 | # Display WordPress version along with other information 449 | $ wp core version --extra 450 | WordPress version: 4.5.2 451 | Database revision: 36686 452 | TinyMCE version: 4.310 (4310-20160418) 453 | Package language: en_US 454 | 455 | ## Installing 456 | 457 | This package is included with WP-CLI itself, no additional installation necessary. 458 | 459 | To install the latest version of this package over what's included in WP-CLI, run: 460 | 461 | wp package install git@github.com:wp-cli/core-command.git 462 | 463 | ## Contributing 464 | 465 | We appreciate you taking the initiative to contribute to this project. 466 | 467 | 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. 468 | 469 | 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. 470 | 471 | ### Reporting a bug 472 | 473 | Think you’ve found a bug? We’d love for you to help us get it fixed. 474 | 475 | Before you create a new issue, you should [search existing issues](https://github.com/wp-cli/core-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. 476 | 477 | 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/core-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/). 478 | 479 | ### Creating a pull request 480 | 481 | Want to contribute a new feature? Please first [open a new issue](https://github.com/wp-cli/core-command/issues/new) to discuss whether the feature is a good fit for the project. 482 | 483 | 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. 484 | 485 | ## Support 486 | 487 | GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support 488 | 489 | 490 | *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.* 491 | -------------------------------------------------------------------------------- /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/core-command", 3 | "type": "wp-cli-package", 4 | "description": "Downloads, installs, updates, and manages a WordPress installation.", 5 | "homepage": "https://github.com/wp-cli/core-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 | "composer/semver": "^1.4 || ^2 || ^3", 16 | "wp-cli/wp-cli": "^2.12" 17 | }, 18 | "require-dev": { 19 | "wp-cli/checksum-command": "^1 || ^2", 20 | "wp-cli/db-command": "^1.3 || ^2", 21 | "wp-cli/entity-command": "^1.3 || ^2", 22 | "wp-cli/extension-command": "^1.2 || ^2", 23 | "wp-cli/wp-cli-tests": "^4" 24 | }, 25 | "config": { 26 | "process-timeout": 7200, 27 | "sort-packages": true, 28 | "allow-plugins": { 29 | "dealerdirect/phpcodesniffer-composer-installer": true, 30 | "johnpbloch/wordpress-core-installer": true 31 | }, 32 | "lock": false 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-main": "2.x-dev" 37 | }, 38 | "bundled": true, 39 | "commands": [ 40 | "core", 41 | "core check-update", 42 | "core download", 43 | "core install", 44 | "core is-installed", 45 | "core multisite-convert", 46 | "core multisite-install", 47 | "core update", 48 | "core update-db", 49 | "core version" 50 | ] 51 | }, 52 | "autoload": { 53 | "classmap": [ 54 | "src/" 55 | ], 56 | "files": [ 57 | "core-command.php" 58 | ] 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true, 62 | "scripts": { 63 | "behat": "run-behat-tests", 64 | "behat-rerun": "rerun-behat-tests", 65 | "lint": "run-linter-tests", 66 | "phpcs": "run-phpcs-tests", 67 | "phpcbf": "run-phpcbf-cleanup", 68 | "phpunit": "run-php-unit-tests", 69 | "prepare-tests": "install-package-tests", 70 | "test": [ 71 | "@lint", 72 | "@phpcs", 73 | "@phpunit", 74 | "@behat" 75 | ] 76 | }, 77 | "support": { 78 | "issues": "https://github.com/wp-cli/core-command/issues" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core-command.php: -------------------------------------------------------------------------------- 1 | updates = []; 49 | $obj->last_checked = strtotime( '1 January 2099' ); 50 | $obj->version_checked = $wp_version; 51 | $obj->translations = []; 52 | set_site_transient( 'update_core', $obj ); 53 | """ 54 | And I run `wp eval-file setup.php` 55 | 56 | When I run `wp core check-update` 57 | Then STDOUT should be: 58 | """ 59 | Success: WordPress is at the latest version. 60 | """ 61 | 62 | When I run `wp core check-update --format=json` 63 | Then STDOUT should be: 64 | """ 65 | [] 66 | """ 67 | 68 | When I run `wp core check-update --format=yaml` 69 | Then STDOUT should be: 70 | """ 71 | --- 72 | """ 73 | -------------------------------------------------------------------------------- /features/core-download.feature: -------------------------------------------------------------------------------- 1 | Feature: Download WordPress 2 | 3 | Scenario: Empty dir 4 | Given an empty directory 5 | And an empty cache 6 | 7 | When I try `wp core is-installed` 8 | Then the return code should be 1 9 | And STDERR should contain: 10 | """ 11 | Error: This does not seem to be a WordPress install 12 | """ 13 | And STDOUT should be empty 14 | 15 | When I run `wp core download` 16 | And save STDOUT 'Downloading WordPress ([\d\.]+)' as {VERSION} 17 | Then the wp-settings.php file should exist 18 | And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-en_US.tar.gz file should exist 19 | 20 | When I run `mkdir inner` 21 | And I run `cd inner && wp core download` 22 | Then the inner/wp-settings.php file should exist 23 | 24 | When I try `wp core download --path=inner` 25 | Then STDERR should be: 26 | """ 27 | Error: WordPress files seem to already be present here. 28 | """ 29 | And the return code should be 1 30 | 31 | When I try `WP_CLI_STRICT_ARGS_MODE=1 wp core download --path=inner` 32 | Then STDERR should be: 33 | """ 34 | Error: WordPress files seem to already be present here. 35 | """ 36 | And the return code should be 1 37 | 38 | # test core tarball cache 39 | When I run `wp core download --force` 40 | Then the wp-settings.php file should exist 41 | And STDOUT should contain: 42 | """ 43 | Using cached file '{SUITE_CACHE_DIR}/core/wordpress-{VERSION}-en_US.tar.gz'... 44 | """ 45 | 46 | Scenario: Localized install 47 | Given an empty directory 48 | And an empty cache 49 | When I run `wp core download --version=4.4.2 --locale=de_DE` 50 | And save STDOUT 'Downloading WordPress ([\d\.]+)' as {VERSION} 51 | Then the wp-settings.php file should exist 52 | And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-de_DE.tar.gz file should exist 53 | 54 | Scenario: Catch download of non-existent WP version 55 | Given an empty directory 56 | 57 | When I try `wp core download --version=1.0.3 --force` 58 | Then STDERR should contain: 59 | """ 60 | Error: Release not found. 61 | """ 62 | And the return code should be 1 63 | 64 | Scenario: Core download from a URL 65 | Given an empty directory 66 | And an empty cache 67 | 68 | When I run `wp core download https://wordpress.org/wordpress-4.9.12.zip` 69 | Then the wp-settings.php file should exist 70 | And the {SUITE_CACHE_DIR}/core directory should not exist 71 | And STDOUT should contain: 72 | """ 73 | Downloading from https://wordpress.org/wordpress-4.9.12.zip ... 74 | md5 hash verified: 702c94bc3aa8a37091f9fb075d57d847 75 | Success: WordPress downloaded. 76 | """ 77 | 78 | Scenario: Verify release hash when downloading new version 79 | Given an empty directory 80 | And an empty cache 81 | 82 | When I run `wp core download --version=4.4.1` 83 | Then STDOUT should contain: 84 | """ 85 | md5 hash verified: 1907d1dbdac7a009d89224a516496b8d 86 | Success: WordPress downloaded. 87 | """ 88 | 89 | Scenario: Core download to a directory specified by `--path` in custom command 90 | Given a WP install 91 | And a download-command.php file: 92 | """ 93 | 'src/' ) ); 97 | } 98 | } 99 | WP_CLI::add_command( 'custom-download', 'Download_Command' ); 100 | """ 101 | 102 | When I run `wp --require=download-command.php custom-download` 103 | Then STDOUT should not be empty 104 | And the src directory should contain: 105 | """ 106 | wp-load.php 107 | """ 108 | 109 | When I try `wp --require=download-command.php custom-download` 110 | Then STDERR should be: 111 | """ 112 | Error: WordPress files seem to already be present here. 113 | """ 114 | And the return code should be 1 115 | 116 | Scenario: Make sure files are cleaned up 117 | Given an empty directory 118 | 119 | When I run `wp core download --version=4.4` 120 | Then the wp-includes/rest-api.php file should exist 121 | And the wp-includes/class-wp-comment.php file should exist 122 | And STDERR should not contain: 123 | """ 124 | Warning: Failed to find WordPress version. Please cleanup files manually. 125 | """ 126 | 127 | When I run `wp core download --version=4.3.2 --force` 128 | Then the wp-includes/rest-api.php file should not exist 129 | And the wp-includes/class-wp-comment.php file should not exist 130 | And STDOUT should not contain: 131 | """ 132 | File removed: wp-content 133 | """ 134 | 135 | Scenario: Installing nightly 136 | Given an empty directory 137 | And an empty cache 138 | 139 | When I try `wp core download --version=nightly` 140 | Then the wp-settings.php file should exist 141 | And the {SUITE_CACHE_DIR}/core/wordpress-nightly-en_US.zip file should not exist 142 | And STDOUT should contain: 143 | """ 144 | Downloading WordPress nightly (en_US)... 145 | """ 146 | And STDERR should contain: 147 | """ 148 | Warning: md5 hash checks are not available for nightly downloads. 149 | """ 150 | And STDOUT should contain: 151 | """ 152 | Success: WordPress downloaded. 153 | """ 154 | And the return code should be 0 155 | 156 | # we shouldn't cache nightly builds 157 | When I try `wp core download --version=nightly --force` 158 | Then the wp-settings.php file should exist 159 | And STDOUT should not contain: 160 | """ 161 | Using cached file '{SUITE_CACHE_DIR}/core/wordpress-nightly-en_US.zip'... 162 | """ 163 | And STDERR should contain: 164 | """ 165 | Warning: md5 hash checks are not available for nightly downloads. 166 | """ 167 | And STDOUT should contain: 168 | """ 169 | Success: WordPress downloaded. 170 | """ 171 | And the return code should be 0 172 | 173 | Scenario: Installing nightly over an existing install 174 | Given an empty directory 175 | And an empty cache 176 | When I run `wp core download --version=4.5.3` 177 | Then the wp-settings.php file should exist 178 | When I try `wp core download --version=nightly --force` 179 | Then STDERR should not contain: 180 | """ 181 | Failed to find WordPress version 182 | """ 183 | And STDERR should contain: 184 | """ 185 | Warning: Checksums not available for WordPress nightly/en_US. Please cleanup files manually. 186 | """ 187 | And STDOUT should contain: 188 | """ 189 | Success: WordPress downloaded. 190 | """ 191 | And the return code should be 0 192 | 193 | Scenario: Installing a version over nightly 194 | Given an empty directory 195 | And an empty cache 196 | When I try `wp core download --version=nightly` 197 | Then the wp-settings.php file should exist 198 | And STDERR should not contain: 199 | """ 200 | Warning: Failed to find WordPress version. Please cleanup files manually. 201 | """ 202 | And STDOUT should contain: 203 | """ 204 | Success: WordPress downloaded. 205 | """ 206 | And the return code should be 0 207 | 208 | When I run `wp core download --version=4.3.2 --force` 209 | Then the wp-includes/rest-api.php file should not exist 210 | And the wp-includes/class-wp-comment.php file should not exist 211 | And STDOUT should not contain: 212 | """ 213 | File removed: wp-content 214 | """ 215 | 216 | Scenario: Trunk is an alias for nightly 217 | Given an empty directory 218 | And an empty cache 219 | When I try `wp core download --version=trunk` 220 | Then the wp-settings.php file should exist 221 | And STDOUT should contain: 222 | """ 223 | Downloading WordPress nightly (en_US)... 224 | """ 225 | And STDERR should contain: 226 | """ 227 | Warning: md5 hash checks are not available for nightly downloads. 228 | """ 229 | And STDOUT should contain: 230 | """ 231 | Success: WordPress downloaded. 232 | """ 233 | And the return code should be 0 234 | 235 | Scenario: Installing nightly for a non-default locale 236 | Given an empty directory 237 | And an empty cache 238 | 239 | When I try `wp core download --version=nightly --locale=de_DE` 240 | Then the return code should be 1 241 | And STDERR should contain: 242 | """ 243 | Error: Nightly builds are only available for the en_US locale. 244 | """ 245 | 246 | Scenario: Installing a release candidate or beta version 247 | Given an empty directory 248 | And an empty cache 249 | 250 | # Test with incorrect case. 251 | When I try `wp core download --version=4.6-rc2` 252 | Then the return code should be 1 253 | And STDERR should contain: 254 | """ 255 | Error: Release not found. 256 | """ 257 | 258 | When I run `wp core download --version=4.6-RC2` 259 | Then the wp-settings.php file should exist 260 | And STDOUT should contain: 261 | """ 262 | Downloading WordPress 4.6-RC2 (en_US)... 263 | md5 hash verified: 90c93a15092b2d5d4c960ec1fc183e07 264 | Success: WordPress downloaded. 265 | """ 266 | 267 | Scenario: Using --version=latest should produce a cache key of the version number, not 'latest' 268 | Given an empty directory 269 | And an empty cache 270 | 271 | When I run `wp core download --version=latest` 272 | Then STDOUT should contain: 273 | """ 274 | Success: WordPress downloaded. 275 | """ 276 | 277 | When I run `wp core version` 278 | Then save STDOUT as {VERSION} 279 | And the {SUITE_CACHE_DIR}/core/wordpress-latest-en_US.tar.gz file should not exist 280 | And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-en_US.tar.gz file should exist 281 | 282 | Scenario: Fail if path can't be created 283 | Given an empty directory 284 | And a non-directory-path file: 285 | """ 286 | """ 287 | 288 | When I try `wp core download --path=non-directory-path` 289 | Then STDERR should contain: 290 | """ 291 | Failed to create directory 292 | """ 293 | And STDERR should contain: 294 | """ 295 | /non-directory-path/ 296 | """ 297 | And the return code should be 1 298 | 299 | When I try `WP_CLI_STRICT_ARGS_MODE=1 wp core download --path=non-directory-path` 300 | Then STDERR should contain: 301 | """ 302 | Failed to create directory 303 | """ 304 | And STDERR should contain: 305 | """ 306 | non-directory-path/ 307 | """ 308 | And the return code should be 1 309 | 310 | When I try `WP_CLI_STRICT_ARGS_MODE=1 wp core download --path=non-directory-path\\` 311 | Then STDERR should contain: 312 | """ 313 | Failed to create directory 314 | """ 315 | And STDERR should contain: 316 | """ 317 | non-directory-path/ 318 | """ 319 | And the return code should be 1 320 | 321 | When I try `wp core download --path=/root-level-directory` 322 | Then STDERR should contain: 323 | """ 324 | Insufficient permission to create directory 325 | """ 326 | And STDERR should contain: 327 | """ 328 | /root-level-directory/ 329 | """ 330 | And the return code should be 1 331 | 332 | When I try `WP_CLI_STRICT_ARGS_MODE=1 wp core download --path=/root-level-directory` 333 | Then STDERR should contain: 334 | """ 335 | Insufficient permission to create directory 336 | """ 337 | And STDERR should contain: 338 | """ 339 | /root-level-directory/ 340 | """ 341 | And the return code should be 1 342 | 343 | Scenario: Core download without the full wp-content/plugins dir 344 | Given an empty directory 345 | 346 | When I run `wp core download --skip-content` 347 | Then STDOUT should contain: 348 | """ 349 | Success: WordPress downloaded. 350 | """ 351 | And the wp-includes directory should exist 352 | And the wp-content/plugins directory should exist 353 | And the wp-content/plugins directory should be: 354 | """ 355 | index.php 356 | """ 357 | And the wp-includes/js/tinymce/plugins directory should exist 358 | 359 | Scenario: Core download without the full wp-content/themes dir 360 | Given an empty directory 361 | 362 | When I run `wp core download --skip-content` 363 | Then STDOUT should contain: 364 | """ 365 | Success: WordPress downloaded. 366 | """ 367 | And the wp-includes directory should exist 368 | And the wp-content/themes directory should exist 369 | And the wp-content/themes directory should be: 370 | """ 371 | index.php 372 | """ 373 | And the wp-includes/js/tinymce/themes directory should exist 374 | 375 | Scenario: Core download without the full wp-content/plugins dir should work non US locale 376 | Given an empty directory 377 | 378 | When I run `wp core download --skip-content --version=4.9.11 --locale=nl_NL` 379 | Then STDOUT should contain: 380 | """ 381 | Success: WordPress downloaded. 382 | """ 383 | And the wp-includes directory should exist 384 | And the wp-content/plugins directory should exist 385 | And the wp-content/plugins directory should be: 386 | """ 387 | index.php 388 | """ 389 | And the wp-includes/js/tinymce/plugins directory should exist 390 | 391 | Scenario: Core download without the full wp-content/themes dir should work non US locale 392 | Given an empty directory 393 | 394 | When I run `wp core download --skip-content --version=4.9.11 --locale=nl_NL` 395 | Then STDOUT should contain: 396 | """ 397 | Success: WordPress downloaded. 398 | """ 399 | And the wp-includes directory should exist 400 | And the wp-content/themes directory should exist 401 | And the wp-content/themes directory should be: 402 | """ 403 | index.php 404 | """ 405 | And the wp-includes/js/tinymce/themes directory should exist 406 | 407 | Scenario: Core download without the full wp-content/plugins dir should work if a version is set 408 | Given an empty directory 409 | 410 | When I try `wp core download --skip-content --version=4.7` 411 | Then STDOUT should contain: 412 | """ 413 | Success: WordPress downloaded. 414 | """ 415 | And the wp-includes directory should exist 416 | And the wp-content/plugins directory should exist 417 | And the wp-content/plugins directory should be: 418 | """ 419 | index.php 420 | """ 421 | And the wp-content/themes directory should exist 422 | And the wp-content/themes directory should be: 423 | """ 424 | index.php 425 | """ 426 | And the wp-includes/js/tinymce/themes directory should exist 427 | And the wp-includes/js/tinymce/plugins directory should exist 428 | 429 | Scenario: Core download without extract parameter should unzip the download file 430 | Given an empty directory 431 | 432 | When I run `wp core download --version=4.5 --locale=de_DE` 433 | Then the wp-content directory should exist 434 | And the wordpress-4.5-de_DE.tar.gz file should not exist 435 | 436 | Scenario: Core download with extract parameter should unzip the download file 437 | Given an empty directory 438 | 439 | When I run `wp core download --version=4.5 --locale=de_DE --extract` 440 | Then the wp-content directory should exist 441 | And the wordpress-4.5-de_DE.tar.gz file should not exist 442 | 443 | Scenario: Core download with extract parameter should unzip the download file (already cached) 444 | Given an empty directory 445 | 446 | When I run `wp core download --version=4.5 --locale=de_DE --extract` 447 | And I run `rm -rf *` 448 | And I run `wp core download --version=4.5 --locale=de_DE --extract` 449 | Then the wp-content directory should exist 450 | And the wordpress-4.5-de_DE.tar.gz file should not exist 451 | 452 | Scenario: Core download with no-extract should not unzip the download file 453 | Given an empty directory 454 | 455 | When I run `wp core download --version=4.5 --locale=de_DE --no-extract` 456 | Then the wp-content directory should not exist 457 | And the wordpress-4.5-de_DE.tar.gz file should exist 458 | 459 | Scenario: Core download with no-extract should not unzip the download file (already cached) 460 | Given an empty directory 461 | 462 | When I run `wp core download --version=4.5 --locale=de_DE --no-extract` 463 | And I run `rm -rf wordpress-4.5-de_DE.tar.gz` 464 | And I run `wp core download --version=4.5 --locale=de_DE --no-extract` 465 | Then the wp-content directory should not exist 466 | And the wordpress-4.5-de_DE.tar.gz file should exist 467 | 468 | Scenario: Error when using both --skip-content and --no-extract 469 | Given an empty directory 470 | 471 | When I try `wp core download --skip-content --no-extract` 472 | Then STDERR should contain: 473 | """ 474 | Error: Cannot use both --skip-content and --no-extract at the same time. 475 | """ 476 | And the return code should be 1 477 | 478 | Scenario: Allow installing major version with trailing zero 479 | Given an empty directory 480 | 481 | When I run `wp core download --version=6.7.0` 482 | Then STDOUT should contain: 483 | """ 484 | Success: 485 | """ 486 | 487 | -------------------------------------------------------------------------------- /features/core-install.feature: -------------------------------------------------------------------------------- 1 | Feature: Install WordPress core 2 | 3 | # TODO: Requires investigation for SQLite support. 4 | # See https://github.com/wp-cli/core-command/issues/244 5 | @require-mysql 6 | Scenario: Two WordPress installs sharing the same user table won't update existing user 7 | Given an empty directory 8 | And WP files 9 | And a WP install in 'second' 10 | And a extra-config file: 11 | """ 12 | define( 'CUSTOM_USER_TABLE', 'secondusers' ); 13 | define( 'CUSTOM_USER_META_TABLE', 'secondusermeta' ); 14 | """ 15 | 16 | When I run `wp --path=second user create testadmin testadmin@example.org --role=administrator` 17 | Then STDOUT should contain: 18 | """ 19 | Success: Created user 2. 20 | """ 21 | 22 | When I run `wp --path=second db tables` 23 | Then STDOUT should contain: 24 | """ 25 | secondposts 26 | """ 27 | And STDOUT should contain: 28 | """ 29 | secondusers 30 | """ 31 | 32 | When I run `wp --path=second user list --field=user_login` 33 | Then STDOUT should be: 34 | """ 35 | admin 36 | testadmin 37 | """ 38 | 39 | When I run `wp --path=second user get testadmin --field=user_pass` 40 | Then save STDOUT as {ORIGINAL_PASSWORD} 41 | 42 | When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check --extra-php < extra-config` 43 | Then STDOUT should be: 44 | """ 45 | Success: Generated 'wp-config.php' file. 46 | """ 47 | 48 | When I run `wp core install --url=example.org --title=Test --admin_user=testadmin --admin_email=testadmin@example.com --admin_password=newpassword` 49 | Then STDOUT should contain: 50 | """ 51 | Success: WordPress installed successfully. 52 | """ 53 | 54 | When I run `wp user list --field=user_login` 55 | Then STDOUT should be: 56 | """ 57 | admin 58 | testadmin 59 | """ 60 | 61 | When I run `wp user get testadmin --field=email` 62 | Then STDOUT should be: 63 | """ 64 | testadmin@example.org 65 | """ 66 | 67 | When I run `wp user get testadmin --field=user_pass` 68 | Then STDOUT should be: 69 | """ 70 | {ORIGINAL_PASSWORD} 71 | """ 72 | 73 | When I run `wp db tables` 74 | Then STDOUT should contain: 75 | """ 76 | wp_posts 77 | """ 78 | And STDOUT should contain: 79 | """ 80 | secondusers 81 | """ 82 | And STDOUT should not contain: 83 | """ 84 | wp_users 85 | """ 86 | 87 | # TODO: Requires investigation for SQLite support. 88 | # See https://github.com/wp-cli/core-command/issues/244 89 | @require-mysql 90 | Scenario: Two WordPress installs sharing the same user table will create new user 91 | Given an empty directory 92 | And WP files 93 | And a WP install in 'second' 94 | And a extra-config file: 95 | """ 96 | define( 'CUSTOM_USER_TABLE', 'secondusers' ); 97 | define( 'CUSTOM_USER_META_TABLE', 'secondusermeta' ); 98 | """ 99 | 100 | When I run `wp --path=second db tables` 101 | Then STDOUT should contain: 102 | """ 103 | secondposts 104 | """ 105 | And STDOUT should contain: 106 | """ 107 | secondusers 108 | """ 109 | 110 | When I run `wp --path=second user list --field=user_login` 111 | Then STDOUT should be: 112 | """ 113 | admin 114 | """ 115 | 116 | When I run `wp config create {CORE_CONFIG_SETTINGS} --skip-check --extra-php < extra-config` 117 | Then STDOUT should be: 118 | """ 119 | Success: Generated 'wp-config.php' file. 120 | """ 121 | 122 | When I run `wp core install --url=example.org --title=Test --admin_user=testadmin --admin_email=testadmin@example.com --admin_password=newpassword` 123 | Then STDOUT should contain: 124 | """ 125 | Success: WordPress installed successfully. 126 | """ 127 | 128 | When I run `wp user list --field=user_login` 129 | Then STDOUT should be: 130 | """ 131 | admin 132 | testadmin 133 | """ 134 | 135 | When I run `wp --path=second user list --field=user_login` 136 | Then STDOUT should be: 137 | """ 138 | admin 139 | testadmin 140 | """ 141 | 142 | When I run `wp user get testadmin --field=email` 143 | Then STDOUT should be: 144 | """ 145 | testadmin@example.com 146 | """ 147 | 148 | When I run `wp db tables` 149 | Then STDOUT should contain: 150 | """ 151 | wp_posts 152 | """ 153 | And STDOUT should contain: 154 | """ 155 | secondusers 156 | """ 157 | And STDOUT should not contain: 158 | """ 159 | wp_users 160 | """ 161 | 162 | Scenario: Install WordPress without specifying the admin password 163 | Given an empty directory 164 | And WP files 165 | And wp-config.php 166 | And a database 167 | 168 | # Old versions of WP can generate wpdb database errors if the WP tables don't exist, so STDERR may or may not be empty 169 | When I try `wp core install --url=localhost:8001 --title=Test --admin_user=wpcli --admin_email=wpcli@example.org` 170 | Then STDOUT should contain: 171 | """ 172 | Admin password: 173 | """ 174 | And STDOUT should contain: 175 | """ 176 | Success: WordPress installed successfully. 177 | """ 178 | And the return code should be 0 179 | 180 | @less-than-php-7 181 | Scenario: Install WordPress with locale set to de_DE on WP < 4.0 182 | Given an empty directory 183 | And an empty cache 184 | And a database 185 | 186 | When I run `wp core download --version=3.7 --locale=de_DE` 187 | And save STDOUT 'Downloading WordPress ([\d\.]+)' as {VERSION} 188 | And I run `echo {VERSION}` 189 | Then STDOUT should contain: 190 | """ 191 | 3.7 192 | """ 193 | And the wp-settings.php file should exist 194 | And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-de_DE.tar.gz file should exist 195 | 196 | When I run `wp config create --dbname={DB_NAME} --dbuser={DB_USER} --dbpass={DB_PASSWORD} --dbhost={DB_HOST} --locale=de_DE --skip-check` 197 | Then STDOUT should be: 198 | """ 199 | Success: Generated 'wp-config.php' file. 200 | """ 201 | 202 | # Old versions of WP can generate wpdb database errors if the WP tables don't exist, so STDERR may or may not be empty 203 | When I try `wp core install --url=example.org --title=Test --admin_user=testadmin --admin_email=testadmin@example.com --admin_password=newpassword --locale=de_DE --skip-email` 204 | Then STDERR should contain: 205 | """ 206 | Warning: The flag --locale=de_DE is being ignored as it requires WordPress 4.0+. 207 | """ 208 | And STDOUT should contain: 209 | """ 210 | Success: WordPress installed successfully. 211 | """ 212 | 213 | When I run `wp core version` 214 | Then STDOUT should contain: 215 | """ 216 | 3.7 217 | """ 218 | 219 | When I run `wp taxonomy list` 220 | Then STDOUT should contain: 221 | """ 222 | Kategorien 223 | """ 224 | 225 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+ 226 | @require-mysql 227 | Scenario: Install WordPress with locale set to de_DE on WP >= 4.0 228 | Given an empty directory 229 | And an empty cache 230 | And a database 231 | 232 | When I run `wp core download --version=5.6 --locale=de_DE` 233 | And save STDOUT 'Downloading WordPress ([\d\.]+)' as {VERSION} 234 | And I run `echo {VERSION}` 235 | Then STDOUT should contain: 236 | """ 237 | 5.6 238 | """ 239 | And the wp-settings.php file should exist 240 | And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-de_DE.tar.gz file should exist 241 | 242 | When I run `wp config create --dbname={DB_NAME} --dbuser={DB_USER} --dbpass={DB_PASSWORD} --dbhost={DB_HOST} --locale=de_DE --skip-check` 243 | Then STDOUT should be: 244 | """ 245 | Success: Generated 'wp-config.php' file. 246 | """ 247 | 248 | # Old versions of WP can generate wpdb database errors if the WP tables don't exist, so STDERR may or may not be empty 249 | When I run `wp core install --url=example.org --title=Test --admin_user=testadmin --admin_email=testadmin@example.com --admin_password=newpassword --locale=de_DE --skip-email` 250 | Then STDOUT should contain: 251 | """ 252 | Success: WordPress installed successfully. 253 | """ 254 | 255 | When I run `wp core version` 256 | Then STDOUT should contain: 257 | """ 258 | 5.6 259 | """ 260 | 261 | When I run `wp taxonomy list` 262 | Then STDOUT should contain: 263 | """ 264 | Kategorien 265 | """ 266 | 267 | Scenario: Install WordPress multisite without specifying the password 268 | Given an empty directory 269 | And WP files 270 | And wp-config.php 271 | And a database 272 | 273 | # Old versions of WP can generate wpdb database errors if the WP tables don't exist, so STDERR may or may not be empty 274 | When I try `wp core multisite-install --url=foobar.org --title=Test --admin_user=wpcli --admin_email=admin@example.com` 275 | Then STDOUT should contain: 276 | """ 277 | Admin password: 278 | """ 279 | And STDOUT should contain: 280 | """ 281 | Success: Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache). 282 | """ 283 | And the return code should be 0 284 | 285 | Scenario: Install WordPress multisite without adding multisite constants to wp-config file 286 | Given an empty directory 287 | And WP files 288 | And wp-config.php 289 | And a database 290 | 291 | When I run `wp core multisite-install --url=foobar.org --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=password --skip-config` 292 | Then STDOUT should contain: 293 | """ 294 | Addition of multisite constants to 'wp-config.php' skipped. You need to add them manually: 295 | """ 296 | 297 | @require-mysql 298 | Scenario: Install WordPress multisite with existing multisite constants in wp-config file 299 | Given an empty directory 300 | And WP files 301 | And a database 302 | And a extra-config file: 303 | """ 304 | define( 'WP_ALLOW_MULTISITE', true ); 305 | define( 'MULTISITE', true ); 306 | define( 'SUBDOMAIN_INSTALL', true ); 307 | $base = '/'; 308 | define( 'DOMAIN_CURRENT_SITE', 'foobar.org' ); 309 | define( 'PATH_CURRENT_SITE', '/' ); 310 | define( 'SITE_ID_CURRENT_SITE', 1 ); 311 | define( 'BLOG_ID_CURRENT_SITE', 1 ); 312 | """ 313 | 314 | When I run `wp config create {CORE_CONFIG_SETTINGS} --extra-php < extra-config` 315 | Then STDOUT should be: 316 | """ 317 | Success: Generated 'wp-config.php' file. 318 | """ 319 | 320 | When I run `wp core multisite-install --url=foobar.org --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=password --skip-config` 321 | Then STDOUT should be: 322 | """ 323 | Created single site database tables. 324 | Set up multisite database tables. 325 | Success: Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache). 326 | """ 327 | 328 | When I run `wp db query "select * from wp_sitemeta where meta_key = 'site_admins' and meta_value = ''"` 329 | Then STDOUT should be: 330 | """ 331 | """ 332 | -------------------------------------------------------------------------------- /features/core-update-db.feature: -------------------------------------------------------------------------------- 1 | Feature: Update core's database 2 | 3 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+ 4 | @require-mysql 5 | Scenario: Update db on a single site 6 | Given a WP install 7 | And a disable_sidebar_check.php file: 8 | """ 9 | = 3.9) 71 | Given a WP install 72 | And I try `wp theme install twentytwenty --activate` 73 | 74 | When I run `wp core download --version=4.1.30 --force` 75 | Then STDOUT should contain: 76 | """ 77 | Success: WordPress downloaded. 78 | """ 79 | 80 | # This version of WP throws a PHP notice 81 | When I try `wp core update --minor` 82 | Then STDOUT should contain: 83 | """ 84 | Updating to version {WP_VERSION-4.1-latest} 85 | """ 86 | And STDOUT should contain: 87 | """ 88 | Success: WordPress updated successfully. 89 | """ 90 | And the return code should be 0 91 | 92 | When I run `wp core update --minor` 93 | Then STDOUT should be: 94 | """ 95 | Success: WordPress is at the latest minor release. 96 | """ 97 | 98 | When I run `wp core version` 99 | Then STDOUT should be: 100 | """ 101 | {WP_VERSION-4.1-latest} 102 | """ 103 | 104 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+ 105 | @require-mysql 106 | Scenario: Core update from cache 107 | Given a WP install 108 | And I try `wp theme install twentytwenty --activate` 109 | And an empty cache 110 | 111 | When I run `wp core update --version=3.9.1 --force` 112 | Then STDOUT should not contain: 113 | """ 114 | Using cached file 115 | """ 116 | And STDOUT should contain: 117 | """ 118 | Downloading 119 | """ 120 | 121 | When I run `wp core update --version=4.0 --force` 122 | Then STDOUT should not be empty 123 | 124 | When I run `wp core update --version=3.9.1 --force` 125 | Then STDOUT should contain: 126 | """ 127 | Using cached file '{SUITE_CACHE_DIR}/core/wordpress-3.9.1-en_US.zip'... 128 | """ 129 | And STDOUT should not contain: 130 | """ 131 | Downloading 132 | """ 133 | 134 | @require-php-7.0 135 | Scenario: Don't run update when up-to-date 136 | Given a WP install 137 | And I run `wp core update` 138 | 139 | When I run `wp core update` 140 | Then STDOUT should contain: 141 | """ 142 | WordPress is up to date 143 | """ 144 | And STDOUT should not contain: 145 | """ 146 | Updating 147 | """ 148 | 149 | When I run `wp core update --force` 150 | Then STDOUT should contain: 151 | """ 152 | Updating 153 | """ 154 | 155 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+ 156 | @require-mysql 157 | Scenario: Ensure cached partial upgrades aren't used in full upgrade 158 | Given a WP install 159 | And I try `wp theme install twentytwenty --activate` 160 | And an empty cache 161 | And a wp-content/mu-plugins/upgrade-override.php file: 162 | """ 163 | array( 167 | (object) array( 168 | 'response' => 'autoupdate', 169 | 'download' => 'https://downloads.wordpress.org/release/wordpress-4.2.4.zip', 170 | 'locale' => 'en_US', 171 | 'packages' => (object) array( 172 | 'full' => 'https://downloads.wordpress.org/release/wordpress-4.2.4.zip', 173 | 'no_content' => 'https://downloads.wordpress.org/release/wordpress-4.2.4-no-content.zip', 174 | 'new_bundled' => 'https://downloads.wordpress.org/release/wordpress-4.2.4-new-bundled.zip', 175 | 'partial' => 'https://downloads.wordpress.org/release/wordpress-4.2.4-partial-1.zip', 176 | 'rollback' => 'https://downloads.wordpress.org/release/wordpress-4.2.4-rollback-1.zip', 177 | ), 178 | 'current' => '4.2.4', 179 | 'version' => '4.2.4', 180 | 'php_version' => '5.2.4', 181 | 'mysql_version' => '5.0', 182 | 'new_bundled' => '4.1', 183 | 'partial_version' => '4.2.1', 184 | 'support_email' => 'updatehelp42@wordpress.org', 185 | 'new_files' => '', 186 | ), 187 | ), 188 | 'version_checked' => '4.2.4', // Needed to avoid PHP notice in `wp_version_check()`. 189 | ); 190 | }); 191 | """ 192 | 193 | When I run `wp core download --version=4.2.1 --force` 194 | And I run `wp core update` 195 | Then STDOUT should contain: 196 | """ 197 | Success: WordPress updated successfully. 198 | """ 199 | And the {SUITE_CACHE_DIR}/core directory should contain: 200 | """ 201 | wordpress-4.2.1-en_US.tar.gz 202 | wordpress-4.2.4-partial-1-en_US.zip 203 | """ 204 | 205 | When I run `wp core download --version=4.1.1 --force` 206 | And I run `wp core update` 207 | Then STDOUT should contain: 208 | """ 209 | Success: WordPress updated successfully. 210 | """ 211 | 212 | # Allow for warnings to be produced. 213 | When I try `wp core verify-checksums` 214 | Then STDOUT should be: 215 | """ 216 | Success: WordPress installation verifies against checksums. 217 | """ 218 | And the {SUITE_CACHE_DIR}/core directory should contain: 219 | """ 220 | wordpress-4.1.1-en_US.tar.gz 221 | wordpress-4.2.1-en_US.tar.gz 222 | wordpress-4.2.4-no-content-en_US.zip 223 | wordpress-4.2.4-partial-1-en_US.zip 224 | """ 225 | 226 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.0+ 227 | @less-than-php-7.3 @require-mysql 228 | Scenario: Make sure files are cleaned up 229 | Given a WP install 230 | And I try `wp theme install twentytwenty --activate` 231 | 232 | When I run `wp core update --version=4.4 --force` 233 | Then the wp-includes/rest-api.php file should exist 234 | And the wp-includes/class-wp-comment.php file should exist 235 | And STDOUT should not contain: 236 | """ 237 | File removed: wp-content 238 | """ 239 | 240 | When I run `wp core update --version=4.3.2 --force` 241 | Then the wp-includes/rest-api.php file should not exist 242 | And the wp-includes/class-wp-comment.php file should not exist 243 | And STDOUT should contain: 244 | """ 245 | File removed: wp-includes/class-walker-comment.php 246 | File removed: wp-includes/class-wp-network.php 247 | File removed: wp-includes/embed-template.php 248 | File removed: wp-includes/class-wp-comment.php 249 | File removed: wp-includes/class-wp-http-response.php 250 | File removed: wp-includes/class-walker-category-dropdown.php 251 | File removed: wp-includes/rest-api.php 252 | """ 253 | And STDOUT should not contain: 254 | """ 255 | File removed: wp-content 256 | """ 257 | 258 | When I run `wp option add str_opt 'bar'` 259 | Then STDOUT should not be empty 260 | When I run `wp post create --post_title='Test post' --porcelain` 261 | Then STDOUT should be a number 262 | 263 | # This test downgrades to an older WordPress version, but the SQLite plugin requires 6.4+ 264 | @require-mysql 265 | Scenario: Make sure files are cleaned up with mixed case 266 | Given a WP install 267 | And I try `wp theme install twentytwenty --activate` 268 | 269 | When I run `wp core update --version=5.8 --force` 270 | Then the wp-includes/Requests/Transport/cURL.php file should exist 271 | And the wp-includes/Requests/Exception/Transport/cURL.php file should exist 272 | And the wp-includes/Requests/Exception/HTTP/502.php file should exist 273 | And the wp-includes/Requests/IRI.php file should exist 274 | And the wp-includes/Requests/src/Transport/Curl.php file should not exist 275 | And the wp-includes/Requests/src/Exception/Transport/Curl.php file should not exist 276 | And the wp-includes/Requests/src/Exception/Http/Status502.php file should not exist 277 | And the wp-includes/Requests/src/Iri.php file should not exist 278 | And STDOUT should contain: 279 | """ 280 | Cleaning up files... 281 | """ 282 | And STDOUT should contain: 283 | """ 284 | Success: WordPress updated successfully. 285 | """ 286 | 287 | When I run `wp core update --version=6.2 --force` 288 | Then the wp-includes/Requests/Transport/cURL.php file should not exist 289 | And the wp-includes/Requests/Exception/Transport/cURL.php file should not exist 290 | And the wp-includes/Requests/Exception/HTTP/502.php file should not exist 291 | And the wp-includes/Requests/IRI.php file should not exist 292 | And the wp-includes/Requests/src/Transport/Curl.php file should exist 293 | And the wp-includes/Requests/src/Exception/Transport/Curl.php file should exist 294 | And the wp-includes/Requests/src/Exception/Http/Status502.php file should exist 295 | And the wp-includes/Requests/src/Iri.php file should exist 296 | And STDOUT should contain: 297 | """ 298 | Cleaning up files... 299 | """ 300 | 301 | When I run `wp option add str_opt 'bar'` 302 | Then STDOUT should not be empty 303 | When I run `wp post create --post_title='Test post' --porcelain` 304 | Then STDOUT should be a number 305 | 306 | @require-php-7.2 307 | Scenario Outline: Use `--version=(nightly|trunk)` to update to the latest nightly version 308 | Given a WP install 309 | 310 | When I run `wp core update --version=` 311 | Then STDOUT should contain: 312 | """ 313 | Updating to version nightly (en_US)... 314 | Downloading update from https://wordpress.org/nightly-builds/wordpress-latest.zip... 315 | """ 316 | And STDOUT should contain: 317 | """ 318 | Success: WordPress updated successfully. 319 | """ 320 | 321 | Examples: 322 | | version | 323 | | trunk | 324 | | nightly | 325 | 326 | @require-php-7.2 327 | Scenario: Installing latest nightly build should skip cache 328 | Given a WP install 329 | 330 | # May produce warnings if checksums cannot be retrieved. 331 | When I try `wp core upgrade --force http://wordpress.org/nightly-builds/wordpress-latest.zip` 332 | Then STDOUT should contain: 333 | """ 334 | Success: 335 | """ 336 | And STDOUT should not contain: 337 | """ 338 | Using cached 339 | """ 340 | 341 | # May produce warnings if checksums cannot be retrieved. 342 | When I try `wp core upgrade --force http://wordpress.org/nightly-builds/wordpress-latest.zip` 343 | Then STDOUT should contain: 344 | """ 345 | Success: 346 | """ 347 | And STDOUT should not contain: 348 | """ 349 | Using cached 350 | """ 351 | 352 | Scenario: Allow installing major version with trailing zero 353 | Given a WP install 354 | 355 | When I run `wp core update --version=6.2.0 --force` 356 | Then STDOUT should contain: 357 | """ 358 | Success: 359 | """ 360 | -------------------------------------------------------------------------------- /features/core-version.feature: -------------------------------------------------------------------------------- 1 | Feature: Find version for WordPress install 2 | 3 | Scenario: Verify core version 4 | Given a WP install 5 | And I run `wp core download --version=4.4.2 --force` 6 | 7 | When I run `wp core version` 8 | Then STDOUT should be: 9 | """ 10 | 4.4.2 11 | """ 12 | 13 | When I run `wp core version --extra` 14 | Then STDOUT should be: 15 | """ 16 | WordPress version: 4.4.2 17 | Database revision: 35700 18 | TinyMCE version: 4.208 (4208-20151113) 19 | Package language: en_US 20 | """ 21 | 22 | Scenario: Installing WordPress for a non-default locale and verify core extended version information. 23 | Given an empty directory 24 | And an empty cache 25 | 26 | When I run `wp core download --version=4.4.2 --locale=de_DE` 27 | Then STDOUT should contain: 28 | """ 29 | Success: WordPress downloaded. 30 | """ 31 | 32 | When I run `wp core version --extra` 33 | Then STDOUT should be: 34 | """ 35 | WordPress version: 4.4.2 36 | Database revision: 35700 37 | TinyMCE version: 4.208 (4208-20151113) 38 | Package language: de_DE 39 | """ 40 | -------------------------------------------------------------------------------- /features/core.feature: -------------------------------------------------------------------------------- 1 | Feature: Manage WordPress installation 2 | 3 | # `wp db create` does not yet work on SQLite, 4 | # See https://github.com/wp-cli/db-command/issues/234 5 | @require-mysql 6 | Scenario: Database doesn't exist 7 | Given an empty directory 8 | And WP files 9 | And wp-config.php 10 | 11 | When I try `wp core is-installed` 12 | Then the return code should be 1 13 | And STDERR should not be empty 14 | 15 | When I run `wp db create` 16 | Then STDOUT should not be empty 17 | 18 | Scenario: Database tables not installed 19 | Given an empty directory 20 | And WP files 21 | And wp-config.php 22 | And a database 23 | 24 | When I try `wp core is-installed` 25 | Then the return code should be 1 26 | 27 | When I try `wp core is-installed --network` 28 | Then the return code should be 1 29 | 30 | When I try `wp core install` 31 | Then the return code should be 1 32 | And STDERR should contain: 33 | """ 34 | missing --url parameter (The address of the new site.) 35 | """ 36 | 37 | When I run `wp core install --url='localhost:8001' --title='Test' --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 38 | Then STDOUT should not be empty 39 | 40 | When I run `wp eval 'echo home_url();'` 41 | Then STDOUT should be: 42 | """ 43 | http://localhost:8001 44 | """ 45 | 46 | When I try `wp core is-installed --network` 47 | Then the return code should be 1 48 | 49 | Scenario: Install WordPress by prompting 50 | Given an empty directory 51 | And WP files 52 | And wp-config.php 53 | And a database 54 | And a session file: 55 | """ 56 | localhost:8001 57 | Test 58 | wpcli 59 | wpcli 60 | admin@example.com 61 | """ 62 | 63 | When I run `wp core install --prompt < session` 64 | Then STDOUT should not be empty 65 | 66 | When I run `wp eval 'echo home_url();'` 67 | Then STDOUT should be: 68 | """ 69 | https://localhost:8001 70 | """ 71 | 72 | Scenario: Install WordPress by prompting for the admin email and password 73 | Given an empty directory 74 | And WP files 75 | And wp-config.php 76 | And a database 77 | And a session file: 78 | """ 79 | wpcli 80 | admin@example.com 81 | """ 82 | 83 | When I run `wp core install --url=localhost:8001 --title=Test --admin_user=wpcli --prompt=admin_password,admin_email < session` 84 | Then STDOUT should not be empty 85 | 86 | When I run `wp eval 'echo home_url();'` 87 | Then STDOUT should be: 88 | """ 89 | http://localhost:8001 90 | """ 91 | 92 | Scenario: Install WordPress with an https scheme 93 | Given an empty directory 94 | And WP files 95 | And wp-config.php 96 | And a database 97 | 98 | When I run `wp core install --url='https://localhost' --title='Test' --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 99 | Then the return code should be 0 100 | 101 | When I run `wp eval 'echo home_url();'` 102 | Then STDOUT should be: 103 | """ 104 | https://localhost 105 | """ 106 | 107 | Scenario: Install WordPress with an https scheme and non-standard port 108 | Given an empty directory 109 | And WP files 110 | And wp-config.php 111 | And a database 112 | 113 | When I run `wp core install --url='https://localhost:8443' --title='Test' --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 114 | Then the return code should be 0 115 | 116 | When I run `wp eval 'echo home_url();'` 117 | Then STDOUT should be: 118 | """ 119 | https://localhost:8443 120 | """ 121 | 122 | Scenario: Full install 123 | Given a WP install 124 | 125 | When I run `wp core is-installed` 126 | Then STDOUT should be empty 127 | And the wp-content/uploads directory should exist 128 | 129 | When I run `wp eval 'var_export( is_admin() );'` 130 | Then STDOUT should be: 131 | """ 132 | false 133 | """ 134 | 135 | When I run `wp eval 'var_export( function_exists( "media_handle_upload" ) );'` 136 | Then STDOUT should be: 137 | """ 138 | true 139 | """ 140 | 141 | # Can complain that it's already installed, but don't exit with an error code 142 | When I try `wp core install --url='localhost:8001' --title='Test' --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 143 | Then the return code should be 0 144 | 145 | Scenario: Convert install to multisite 146 | Given a WP install 147 | 148 | When I run `wp eval 'var_export( is_multisite() );'` 149 | Then STDOUT should be: 150 | """ 151 | false 152 | """ 153 | 154 | When I try `wp core is-installed --network` 155 | Then the return code should be 1 156 | 157 | When I run `wp core install-network --title='test network'` 158 | Then STDOUT should be: 159 | """ 160 | Set up multisite database tables. 161 | Added multisite constants to 'wp-config.php'. 162 | Success: Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache). 163 | """ 164 | And STDERR should be empty 165 | 166 | When I run `wp eval 'var_export( is_multisite() );'` 167 | Then STDOUT should be: 168 | """ 169 | true 170 | """ 171 | 172 | When I run `wp core is-installed --network` 173 | Then the return code should be 0 174 | 175 | When I try `wp core install-network --title='test network'` 176 | Then the return code should be 1 177 | 178 | When I run `wp network meta get 1 upload_space_check_disabled` 179 | Then STDOUT should be: 180 | """ 181 | 1 182 | """ 183 | 184 | Scenario: Install multisite from scratch 185 | Given an empty directory 186 | And WP files 187 | And wp-config.php 188 | And a database 189 | 190 | When I run `wp core multisite-install --url=foobar.org --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 191 | Then STDOUT should be: 192 | """ 193 | Created single site database tables. 194 | Set up multisite database tables. 195 | Added multisite constants to 'wp-config.php'. 196 | Success: Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache). 197 | """ 198 | And STDERR should be empty 199 | 200 | When I run `wp eval 'echo $GLOBALS["current_site"]->domain;'` 201 | Then STDOUT should be: 202 | """ 203 | foobar.org 204 | """ 205 | 206 | # Can complain that it's already installed, but don't exit with an error code 207 | When I try `wp core multisite-install --url=foobar.org --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 208 | Then the return code should be 0 209 | 210 | When I run `wp network meta get 1 upload_space_check_disabled` 211 | Then STDOUT should be: 212 | """ 213 | 1 214 | """ 215 | 216 | # `wp db reset` does not yet work on SQLite, 217 | # See https://github.com/wp-cli/db-command/issues/234 218 | @require-mysql 219 | Scenario: Install multisite from scratch, with MULTISITE already set in wp-config.php 220 | Given a WP multisite install 221 | And I run `wp db reset --yes` 222 | 223 | When I try `wp core is-installed` 224 | Then the return code should be 1 225 | # WP will produce wpdb database errors in `get_sites()` on loading if the WP tables don't exist 226 | And STDERR should contain: 227 | """ 228 | WordPress database error Table 229 | """ 230 | 231 | When I run `wp core multisite-install --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 232 | Then STDOUT should not be empty 233 | 234 | When I run `wp eval 'echo $GLOBALS["current_site"]->domain;'` 235 | Then STDOUT should be: 236 | """ 237 | example.com 238 | """ 239 | 240 | Scenario: Install multisite with subdomains on localhost 241 | Given an empty directory 242 | And WP files 243 | And wp-config.php 244 | And a database 245 | 246 | When I try `wp core multisite-install --url=http://localhost/ --title=Test --admin_user=wpcli --admin_email=admin@example.com --admin_password=1 --subdomains` 247 | Then STDERR should contain: 248 | """ 249 | Error: Multisite with subdomains cannot be configured when domain is 'localhost'. 250 | """ 251 | And the return code should be 1 252 | 253 | # SQLite compat blocked by https://github.com/wp-cli/wp-cli-tests/pull/188. 254 | @require-mysql 255 | Scenario: Custom wp-content directory 256 | Given a WP install 257 | And a custom wp-content directory 258 | 259 | When I run `wp plugin status hello` 260 | Then STDOUT should not be empty 261 | 262 | Scenario: User defined in wp-cli.yml 263 | Given an empty directory 264 | And WP files 265 | And wp-config.php 266 | And a database 267 | And a wp-cli.yml file: 268 | """ 269 | user: wpcli 270 | """ 271 | 272 | When I run `wp core install --url='localhost:8001' --title='Test' --admin_user=wpcli --admin_email=admin@example.com --admin_password=1` 273 | Then STDOUT should not be empty 274 | 275 | When I run `wp eval 'echo home_url();'` 276 | Then STDOUT should be: 277 | """ 278 | http://localhost:8001 279 | """ 280 | 281 | Scenario: Test output in a multisite install with custom base path 282 | Given a WP install 283 | 284 | When I run `wp core multisite-convert --title=Test --base=/test/` 285 | And I run `wp post list` 286 | Then STDOUT should contain: 287 | """ 288 | Hello world! 289 | """ 290 | 291 | Scenario: Download WordPress 292 | Given an empty directory 293 | 294 | When I run `wp core download` 295 | Then STDOUT should contain: 296 | """ 297 | Success: WordPress downloaded. 298 | """ 299 | And the wp-settings.php file should exist 300 | 301 | Scenario: Don't download WordPress when files are already present 302 | Given an empty directory 303 | And WP files 304 | 305 | When I try `wp core download` 306 | Then STDERR should be: 307 | """ 308 | Error: WordPress files seem to already be present here. 309 | """ 310 | And the return code should be 1 311 | 312 | # `wp db create` does not yet work on SQLite, 313 | # See https://github.com/wp-cli/db-command/issues/234 314 | @require-php-7.0 @require-mysql 315 | Scenario: Install WordPress in a subdirectory 316 | Given an empty directory 317 | And a wp-config.php file: 318 | """ 319 | 2 | 3 | Custom ruleset for WP-CLI core-command 4 | 5 | 12 | 13 | 14 | . 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 38 | 39 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | */src/Core_Command\.php$ 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/Core_Command.php: -------------------------------------------------------------------------------- 1 | ] 51 | * : Prints the value of a single field for each update. 52 | * 53 | * [--fields=] 54 | * : Limit the output to specific object fields. Defaults to version,update_type,package_url. 55 | * 56 | * [--format=] 57 | * : Render output in a particular format. 58 | * --- 59 | * default: table 60 | * options: 61 | * - table 62 | * - csv 63 | * - count 64 | * - json 65 | * - yaml 66 | * --- 67 | * 68 | * ## EXAMPLES 69 | * 70 | * $ wp core check-update 71 | * +---------+-------------+-------------------------------------------------------------+ 72 | * | version | update_type | package_url | 73 | * +---------+-------------+-------------------------------------------------------------+ 74 | * | 4.5.2 | major | https://downloads.wordpress.org/release/wordpress-4.5.2.zip | 75 | * +---------+-------------+-------------------------------------------------------------+ 76 | * 77 | * @subcommand check-update 78 | */ 79 | public function check_update( $_, $assoc_args ) { 80 | $format = Utils\get_flag_value( $assoc_args, 'format', 'table' ); 81 | 82 | $updates = $this->get_updates( $assoc_args ); 83 | 84 | if ( $updates || 'table' !== $format ) { 85 | $updates = array_reverse( $updates ); 86 | $formatter = new Formatter( 87 | $assoc_args, 88 | [ 'version', 'update_type', 'package_url' ] 89 | ); 90 | $formatter->display_items( $updates ); 91 | } else { 92 | WP_CLI::success( 'WordPress is at the latest version.' ); 93 | } 94 | } 95 | 96 | /** 97 | * Downloads core WordPress files. 98 | * 99 | * Downloads and extracts WordPress core files to the specified path. Uses 100 | * current directory when no path is specified. Downloaded build is verified 101 | * to have the correct md5 and then cached to the local filesystem. 102 | * Subsequent uses of command will use the local cache if it still exists. 103 | * 104 | * ## OPTIONS 105 | * 106 | * [] 107 | * : Download directly from a provided URL instead of fetching the URL from the wordpress.org servers. 108 | * 109 | * [--path=] 110 | * : Specify the path in which to install WordPress. Defaults to current 111 | * directory. 112 | * 113 | * [--locale=] 114 | * : Select which language you want to download. 115 | * 116 | * [--version=] 117 | * : Select which version you want to download. Accepts a version number, 'latest' or 'nightly'. 118 | * 119 | * [--skip-content] 120 | * : Download WP without the default themes and plugins. 121 | * 122 | * [--force] 123 | * : Overwrites existing files, if present. 124 | * 125 | * [--insecure] 126 | * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 127 | * 128 | * [--extract] 129 | * : Whether to extract the downloaded file. Defaults to true. 130 | * 131 | * ## EXAMPLES 132 | * 133 | * $ wp core download --locale=nl_NL 134 | * Downloading WordPress 4.5.2 (nl_NL)... 135 | * md5 hash verified: c5366d05b521831dd0b29dfc386e56a5 136 | * Success: WordPress downloaded. 137 | * 138 | * @when before_wp_load 139 | */ 140 | public function download( $args, $assoc_args ) { 141 | $download_dir = ! empty( $assoc_args['path'] ) 142 | ? ( rtrim( $assoc_args['path'], '/\\' ) . '/' ) 143 | : ABSPATH; 144 | 145 | // Check for files if WordPress already present or not. 146 | $wordpress_present = is_readable( $download_dir . 'wp-load.php' ) 147 | || is_readable( $download_dir . 'wp-mail.php' ) 148 | || is_readable( $download_dir . 'wp-cron.php' ) 149 | || is_readable( $download_dir . 'wp-links-opml.php' ); 150 | 151 | if ( $wordpress_present && ! Utils\get_flag_value( $assoc_args, 'force' ) ) { 152 | WP_CLI::error( 'WordPress files seem to already be present here.' ); 153 | } 154 | 155 | if ( ! is_dir( $download_dir ) ) { 156 | if ( ! is_writable( dirname( $download_dir ) ) ) { 157 | WP_CLI::error( "Insufficient permission to create directory '{$download_dir}'." ); 158 | } 159 | 160 | WP_CLI::log( "Creating directory '{$download_dir}'." ); 161 | if ( ! @mkdir( $download_dir, 0777, true /*recursive*/ ) ) { 162 | $error = error_get_last(); 163 | WP_CLI::error( "Failed to create directory '{$download_dir}': {$error['message']}." ); 164 | } 165 | } 166 | 167 | if ( ! is_writable( $download_dir ) ) { 168 | WP_CLI::error( "'{$download_dir}' is not writable by current user." ); 169 | } 170 | 171 | $locale = (string) Utils\get_flag_value( $assoc_args, 'locale', 'en_US' ); 172 | $skip_content = (bool) Utils\get_flag_value( $assoc_args, 'skip-content', false ); 173 | $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); 174 | $extract = (bool) Utils\get_flag_value( $assoc_args, 'extract', true ); 175 | 176 | if ( $skip_content && ! $extract ) { 177 | WP_CLI::error( 'Cannot use both --skip-content and --no-extract at the same time.' ); 178 | } 179 | 180 | $download_url = array_shift( $args ); 181 | $from_url = ! empty( $download_url ); 182 | 183 | if ( $from_url ) { 184 | $version = null; 185 | if ( isset( $assoc_args['version'] ) ) { 186 | WP_CLI::error( 'Version option is not available for URL downloads.' ); 187 | } 188 | if ( $skip_content || 'en_US' !== $locale ) { 189 | WP_CLI::error( 'Skip content and locale options are not available for URL downloads.' ); 190 | } 191 | } elseif ( isset( $assoc_args['version'] ) && 'latest' !== $assoc_args['version'] ) { 192 | $version = $assoc_args['version']; 193 | if ( in_array( strtolower( $version ), [ 'trunk', 'nightly' ], true ) ) { 194 | $version = 'nightly'; 195 | } 196 | 197 | // Nightly builds and skip content are only available in .zip format. 198 | $extension = ( ( 'nightly' === $version ) || $skip_content ) 199 | ? 'zip' 200 | : 'tar.gz'; 201 | 202 | $download_url = $this->get_download_url( $version, $locale, $extension ); 203 | } else { 204 | try { 205 | $offer = ( new WpOrgApi( [ 'insecure' => $insecure ] ) ) 206 | ->get_core_download_offer( $locale ); 207 | } catch ( Exception $exception ) { 208 | WP_CLI::error( $exception ); 209 | } 210 | if ( ! $offer ) { 211 | WP_CLI::error( "The requested locale ({$locale}) was not found." ); 212 | } 213 | $version = $offer['current']; 214 | $download_url = $offer['download']; 215 | if ( ! $skip_content ) { 216 | $download_url = str_replace( '.zip', '.tar.gz', $download_url ); 217 | } 218 | } 219 | 220 | if ( 'nightly' === $version && 'en_US' !== $locale ) { 221 | WP_CLI::error( 'Nightly builds are only available for the en_US locale.' ); 222 | } 223 | 224 | $from_version = ''; 225 | if ( file_exists( $download_dir . 'wp-includes/version.php' ) ) { 226 | $wp_details = self::get_wp_details( $download_dir ); 227 | $from_version = $wp_details['wp_version']; 228 | } 229 | 230 | if ( $from_url ) { 231 | WP_CLI::log( "Downloading from {$download_url} ..." ); 232 | } else { 233 | WP_CLI::log( "Downloading WordPress {$version} ({$locale})..." ); 234 | } 235 | 236 | $path_parts = pathinfo( $download_url ); 237 | $extension = 'tar.gz'; 238 | if ( 'zip' === $path_parts['extension'] ) { 239 | $extension = 'zip'; 240 | if ( $extract && ! class_exists( 'ZipArchive' ) ) { 241 | WP_CLI::error( 'Extracting a zip file requires ZipArchive.' ); 242 | } 243 | } 244 | 245 | if ( $skip_content && 'zip' !== $extension ) { 246 | WP_CLI::error( 'Skip content is only available for ZIP files.' ); 247 | } 248 | 249 | $cache = WP_CLI::get_cache(); 250 | if ( $from_url ) { 251 | $cache_file = null; 252 | } else { 253 | $cache_key = "core/wordpress-{$version}-{$locale}.{$extension}"; 254 | $cache_file = $cache->has( $cache_key ); 255 | } 256 | 257 | $bad_cache = false; 258 | 259 | if ( $cache_file ) { 260 | WP_CLI::log( "Using cached file '{$cache_file}'..." ); 261 | $skip_content_cache_file = $skip_content ? self::strip_content_dir( $cache_file ) : null; 262 | if ( $extract ) { 263 | try { 264 | Extractor::extract( $skip_content_cache_file ?: $cache_file, $download_dir ); 265 | } catch ( Exception $exception ) { 266 | WP_CLI::warning( 'Extraction failed, downloading a new copy...' ); 267 | $bad_cache = true; 268 | } 269 | } else { 270 | copy( $cache_file, $download_dir . basename( $cache_file ) ); 271 | } 272 | } 273 | 274 | if ( ! $cache_file || $bad_cache ) { 275 | // We need to use a temporary file because piping from cURL to tar is flaky 276 | // on MinGW (and probably in other environments too). 277 | $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; 278 | register_shutdown_function( 279 | function () use ( $temp ) { 280 | if ( file_exists( $temp ) ) { 281 | unlink( $temp ); 282 | } 283 | } 284 | ); 285 | 286 | $headers = [ 'Accept' => 'application/json' ]; 287 | $options = [ 288 | 'timeout' => 600, // 10 minutes ought to be enough for everybody 289 | 'filename' => $temp, 290 | 'insecure' => $insecure, 291 | ]; 292 | 293 | $response = Utils\http_request( 'GET', $download_url, null, $headers, $options ); 294 | 295 | if ( 404 === (int) $response->status_code ) { 296 | WP_CLI::error( 'Release not found. Double-check locale or version.' ); 297 | } elseif ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { 298 | WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." ); 299 | } 300 | 301 | if ( 'nightly' !== $version ) { 302 | unset( $options['filename'] ); 303 | $md5_response = Utils\http_request( 'GET', $download_url . '.md5', null, [], $options ); 304 | if ( $md5_response->status_code >= 200 && $md5_response->status_code < 300 ) { 305 | $md5_file = md5_file( $temp ); 306 | 307 | if ( $md5_file === $md5_response->body ) { 308 | WP_CLI::log( 'md5 hash verified: ' . $md5_file ); 309 | } else { 310 | WP_CLI::error( "md5 hash for download ({$md5_file}) is different than the release hash ({$md5_response->body})." ); 311 | } 312 | } else { 313 | WP_CLI::warning( "Couldn't access md5 hash for release ({$download_url}.md5, HTTP code {$md5_response->status_code})." ); 314 | } 315 | } else { 316 | WP_CLI::warning( 'md5 hash checks are not available for nightly downloads.' ); 317 | } 318 | 319 | $skip_content_temp = $skip_content ? self::strip_content_dir( $temp ) : null; 320 | if ( $extract ) { 321 | try { 322 | Extractor::extract( $skip_content_temp ?: $temp, $download_dir ); 323 | } catch ( Exception $exception ) { 324 | WP_CLI::error( "Couldn't extract WordPress archive. {$exception->getMessage()}" ); 325 | } 326 | } else { 327 | copy( $temp, $download_dir . basename( $temp ) ); 328 | } 329 | 330 | // Do not use the cache for nightly builds or for downloaded URLs 331 | // (the URL could be something like "latest.zip" or "nightly.zip"). 332 | if ( ! $from_url && 'nightly' !== $version ) { 333 | $cache->import( $cache_key, $temp ); 334 | } 335 | } 336 | 337 | if ( $wordpress_present ) { 338 | $this->cleanup_extra_files( $from_version, $version, $locale, $insecure ); 339 | } 340 | 341 | WP_CLI::success( 'WordPress downloaded.' ); 342 | } 343 | 344 | /** 345 | * Checks if WordPress is installed. 346 | * 347 | * Determines whether WordPress is installed by checking if the standard 348 | * database tables are installed. Doesn't produce output; uses exit codes 349 | * to communicate whether WordPress is installed. 350 | * 351 | * ## OPTIONS 352 | * 353 | * [--network] 354 | * : Check if this is a multisite installation. 355 | * 356 | * ## EXAMPLES 357 | * 358 | * # Bash script for checking if WordPress is not installed. 359 | * 360 | * if ! wp core is-installed 2>/dev/null; then 361 | * # WP is not installed. Let's try installing it. 362 | * wp core install 363 | * fi 364 | * 365 | * # Bash script for checking if WordPress is installed, with fallback. 366 | * 367 | * if wp core is-installed 2>/dev/null; then 368 | * # WP is installed. Let's do some things we should only do in a confirmed WP environment. 369 | * wp core verify-checksums 370 | * else 371 | * # Fallback if WP is not installed. 372 | * echo 'Hey Friend, you are in the wrong spot. Move in to your WordPress directory and try again.' 373 | * fi 374 | * 375 | * @subcommand is-installed 376 | */ 377 | public function is_installed( $args, $assoc_args ) { 378 | if ( is_blog_installed() 379 | && ( ! Utils\get_flag_value( $assoc_args, 'network' ) 380 | || is_multisite() ) ) { 381 | WP_CLI::halt( 0 ); 382 | } 383 | 384 | WP_CLI::halt( 1 ); 385 | } 386 | 387 | /** 388 | * Runs the standard WordPress installation process. 389 | * 390 | * Creates the WordPress tables in the database using the URL, title, and 391 | * default admin user details provided. Performs the famous 5 minute install 392 | * in seconds or less. 393 | * 394 | * Note: if you've installed WordPress in a subdirectory, then you'll need 395 | * to `wp option update siteurl` after `wp core install`. For instance, if 396 | * WordPress is installed in the `/wp` directory and your domain is example.com, 397 | * then you'll need to run `wp option update siteurl http://example.com/wp` for 398 | * your WordPress installation to function properly. 399 | * 400 | * Note: When using custom user tables (e.g. `CUSTOM_USER_TABLE`), the admin 401 | * email and password are ignored if the user_login already exists. If the 402 | * user_login doesn't exist, a new user will be created. 403 | * 404 | * ## OPTIONS 405 | * 406 | * --url= 407 | * : The address of the new site. 408 | * 409 | * --title= 410 | * : The title of the new site. 411 | * 412 | * --admin_user= 413 | * : The name of the admin user. 414 | * 415 | * [--admin_password=] 416 | * : The password for the admin user. Defaults to randomly generated string. 417 | * 418 | * --admin_email= 419 | * : The email address for the admin user. 420 | * 421 | * [--locale=] 422 | * : The locale/language for the installation (e.g. `de_DE`). Default is `en_US`. 423 | * 424 | * [--skip-email] 425 | * : Don't send an email notification to the new admin user. 426 | * 427 | * ## EXAMPLES 428 | * 429 | * # Install WordPress in 5 seconds 430 | * $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com 431 | * Success: WordPress installed successfully. 432 | * 433 | * # Install WordPress without disclosing admin_password to bash history 434 | * $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_email=info@example.com --prompt=admin_password < admin_password.txt 435 | */ 436 | public function install( $args, $assoc_args ) { 437 | if ( $this->do_install( $assoc_args ) ) { 438 | WP_CLI::success( 'WordPress installed successfully.' ); 439 | } else { 440 | WP_CLI::log( 'WordPress is already installed.' ); 441 | } 442 | } 443 | 444 | /** 445 | * Transforms an existing single-site installation into a multisite installation. 446 | * 447 | * Creates the multisite database tables, and adds the multisite constants 448 | * to wp-config.php. 449 | * 450 | * For those using WordPress with Apache, remember to update the `.htaccess` 451 | * file with the appropriate multisite rewrite rules. 452 | * 453 | * [Review the multisite documentation](https://wordpress.org/support/article/create-a-network/) 454 | * for more details about how multisite works. 455 | * 456 | * ## OPTIONS 457 | * 458 | * [--title=] 459 | * : The title of the new network. 460 | * 461 | * [--base=] 462 | * : Base path after the domain name that each site url will start with. 463 | * --- 464 | * default: / 465 | * --- 466 | * 467 | * [--subdomains] 468 | * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. 469 | * 470 | * [--skip-config] 471 | * : Don't add multisite constants to wp-config.php. 472 | * 473 | * ## EXAMPLES 474 | * 475 | * $ wp core multisite-convert 476 | * Set up multisite database tables. 477 | * Added multisite constants to wp-config.php. 478 | * Success: Network installed. Don't forget to set up rewrite rules. 479 | * 480 | * @subcommand multisite-convert 481 | * @alias install-network 482 | */ 483 | public function multisite_convert( $args, $assoc_args ) { 484 | if ( is_multisite() ) { 485 | WP_CLI::error( 'This already is a multisite installation.' ); 486 | } 487 | 488 | $assoc_args = self::set_multisite_defaults( $assoc_args ); 489 | if ( ! isset( $assoc_args['title'] ) ) { 490 | // translators: placeholder is blog name 491 | $assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), get_option( 'blogname' ) ); 492 | } 493 | 494 | if ( $this->multisite_convert_( $assoc_args ) ) { 495 | WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." ); 496 | } 497 | } 498 | 499 | /** 500 | * Installs WordPress multisite from scratch. 501 | * 502 | * Creates the WordPress tables in the database using the URL, title, and 503 | * default admin user details provided. Then, creates the multisite tables 504 | * in the database and adds multisite constants to the wp-config.php. 505 | * 506 | * For those using WordPress with Apache, remember to update the `.htaccess` 507 | * file with the appropriate multisite rewrite rules. 508 | * 509 | * ## OPTIONS 510 | * 511 | * [--url=] 512 | * : The address of the new site. 513 | * 514 | * [--base=] 515 | * : Base path after the domain name that each site url in the network will start with. 516 | * --- 517 | * default: / 518 | * --- 519 | * 520 | * [--subdomains] 521 | * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. 522 | * 523 | * --title= 524 | * : The title of the new site. 525 | * 526 | * --admin_user= 527 | * : The name of the admin user. 528 | * --- 529 | * default: admin 530 | * --- 531 | * 532 | * [--admin_password=] 533 | * : The password for the admin user. Defaults to randomly generated string. 534 | * 535 | * --admin_email= 536 | * : The email address for the admin user. 537 | * 538 | * [--skip-email] 539 | * : Don't send an email notification to the new admin user. 540 | * 541 | * [--skip-config] 542 | * : Don't add multisite constants to wp-config.php. 543 | * 544 | * ## EXAMPLES 545 | * 546 | * $ wp core multisite-install --title="Welcome to the WordPress" \ 547 | * > --admin_user="admin" --admin_password="password" \ 548 | * > --admin_email="user@example.com" 549 | * Single site database tables already present. 550 | * Set up multisite database tables. 551 | * Added multisite constants to wp-config.php. 552 | * Success: Network installed. Don't forget to set up rewrite rules. 553 | * 554 | * @subcommand multisite-install 555 | */ 556 | public function multisite_install( $args, $assoc_args ) { 557 | if ( $this->do_install( $assoc_args ) ) { 558 | WP_CLI::log( 'Created single site database tables.' ); 559 | } else { 560 | WP_CLI::log( 'Single site database tables already present.' ); 561 | } 562 | 563 | $assoc_args = self::set_multisite_defaults( $assoc_args ); 564 | // translators: placeholder is user supplied title 565 | $assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), $assoc_args['title'] ); 566 | 567 | // Overwrite runtime args, to avoid mismatches. 568 | $consts_to_args = [ 569 | 'SUBDOMAIN_INSTALL' => 'subdomains', 570 | 'PATH_CURRENT_SITE' => 'base', 571 | 'SITE_ID_CURRENT_SITE' => 'site_id', 572 | 'BLOG_ID_CURRENT_SITE' => 'blog_id', 573 | ]; 574 | 575 | foreach ( $consts_to_args as $const => $arg ) { 576 | if ( defined( $const ) ) { 577 | $assoc_args[ $arg ] = constant( $const ); 578 | } 579 | } 580 | 581 | if ( ! $this->multisite_convert_( $assoc_args ) ) { 582 | return; 583 | } 584 | 585 | // Do the steps that were skipped by populate_network(), 586 | // which checks is_multisite(). 587 | if ( is_multisite() ) { 588 | $site_user = get_user_by( 'email', $assoc_args['admin_email'] ); 589 | self::add_site_admins( $site_user ); 590 | $domain = self::get_clean_basedomain(); 591 | self::create_initial_blog( 592 | $assoc_args['site_id'], 593 | $assoc_args['blog_id'], 594 | $domain, 595 | $assoc_args['base'], 596 | $assoc_args['subdomains'], 597 | $site_user 598 | ); 599 | } 600 | 601 | WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." ); 602 | } 603 | 604 | private static function set_multisite_defaults( $assoc_args ) { 605 | $defaults = [ 606 | 'subdomains' => false, 607 | 'base' => '/', 608 | 'site_id' => 1, 609 | 'blog_id' => 1, 610 | ]; 611 | 612 | return array_merge( $defaults, $assoc_args ); 613 | } 614 | 615 | private function do_install( $assoc_args ) { 616 | if ( is_blog_installed() ) { 617 | return false; 618 | } 619 | 620 | if ( true === Utils\get_flag_value( $assoc_args, 'skip-email' ) ) { 621 | if ( ! function_exists( 'wp_new_blog_notification' ) ) { 622 | function wp_new_blog_notification() { 623 | // Silence is golden 624 | } 625 | } 626 | // WP 4.9.0 - skip "Notice of Admin Email Change" email as well (https://core.trac.wordpress.org/ticket/39117). 627 | add_filter( 'send_site_admin_email_change_email', '__return_false' ); 628 | } 629 | 630 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 631 | 632 | $defaults = [ 633 | 'title' => '', 634 | 'admin_user' => '', 635 | 'admin_email' => '', 636 | 'admin_password' => '', 637 | ]; 638 | 639 | if ( Utils\wp_version_compare( '4.0', '<' ) ) { 640 | if ( array_key_exists( 'locale', $assoc_args ) ) { 641 | WP_CLI::warning( 642 | sprintf( 643 | 'The flag --locale=%s is being ignored as it requires WordPress 4.0+.', 644 | $assoc_args['locale'] 645 | ) 646 | ); 647 | } 648 | } else { 649 | $defaults['locale'] = ''; 650 | } 651 | 652 | $args = wp_parse_args( $assoc_args, $defaults ); 653 | 654 | // Support prompting for the `--url=`, 655 | // which is normally a runtime argument 656 | if ( isset( $assoc_args['url'] ) ) { 657 | WP_CLI::set_url( $assoc_args['url'] ); 658 | } 659 | 660 | $public = true; 661 | $password = $args['admin_password']; 662 | 663 | if ( ! is_email( $args['admin_email'] ) ) { 664 | WP_CLI::error( "The '{$args['admin_email']}' email address is invalid." ); 665 | } 666 | 667 | if ( Utils\wp_version_compare( '4.0', '>=' ) ) { 668 | $result = wp_install( 669 | $args['title'], 670 | $args['admin_user'], 671 | $args['admin_email'], 672 | $public, 673 | '', 674 | $password, 675 | $args['locale'] 676 | ); 677 | } else { 678 | $result = wp_install( 679 | $args['title'], 680 | $args['admin_user'], 681 | $args['admin_email'], 682 | $public, 683 | '', 684 | $password 685 | ); 686 | } 687 | 688 | if ( is_wp_error( $result ) ) { 689 | $reason = WP_CLI::error_to_string( $result ); 690 | WP_CLI::error( "Installation failed ({$reason})." ); 691 | } 692 | 693 | if ( ! empty( $GLOBALS['wpdb']->last_error ) ) { 694 | WP_CLI::error( 'Installation produced database errors, and may have partially or completely failed.' ); 695 | } 696 | 697 | if ( empty( $args['admin_password'] ) ) { 698 | WP_CLI::log( "Admin password: {$result['password']}" ); 699 | } 700 | 701 | // Confirm the uploads directory exists 702 | $upload_dir = wp_upload_dir(); 703 | if ( ! empty( $upload_dir['error'] ) ) { 704 | WP_CLI::warning( $upload_dir['error'] ); 705 | } 706 | 707 | return true; 708 | } 709 | 710 | private function multisite_convert_( $assoc_args ) { 711 | global $wpdb; 712 | 713 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 714 | 715 | $domain = self::get_clean_basedomain(); 716 | if ( 'localhost' === $domain && ! empty( $assoc_args['subdomains'] ) ) { 717 | WP_CLI::error( "Multisite with subdomains cannot be configured when domain is 'localhost'." ); 718 | } 719 | 720 | // need to register the multisite tables manually for some reason 721 | foreach ( $wpdb->tables( 'ms_global' ) as $table => $prefixed_table ) { 722 | $wpdb->$table = $prefixed_table; 723 | } 724 | 725 | install_network(); 726 | 727 | $result = populate_network( 728 | $assoc_args['site_id'], 729 | $domain, 730 | get_option( 'admin_email' ), 731 | $assoc_args['title'], 732 | $assoc_args['base'], 733 | $assoc_args['subdomains'] 734 | ); 735 | 736 | $site_id = $wpdb->get_var( "SELECT id FROM $wpdb->site" ); 737 | $site_id = ( null === $site_id ) ? 1 : (int) $site_id; 738 | 739 | if ( true === $result ) { 740 | WP_CLI::log( 'Set up multisite database tables.' ); 741 | } elseif ( is_wp_error( $result ) ) { 742 | switch ( $result->get_error_code() ) { 743 | 744 | case 'siteid_exists': 745 | WP_CLI::log( $result->get_error_message() ); 746 | return false; 747 | 748 | case 'no_wildcard_dns': 749 | WP_CLI::warning( __( 'Wildcard DNS may not be configured correctly.' ) ); 750 | break; 751 | 752 | default: 753 | WP_CLI::error( $result ); 754 | } 755 | } 756 | 757 | // delete_site_option() cleans the alloptions cache to prevent dupe option 758 | delete_site_option( 'upload_space_check_disabled' ); 759 | update_site_option( 'upload_space_check_disabled', 1 ); 760 | 761 | if ( ! is_multisite() ) { 762 | $subdomain_export = Utils\get_flag_value( $assoc_args, 'subdomains' ) ? 'true' : 'false'; 763 | $ms_config = <<get_results( "SELECT meta_id, site_id FROM {$wpdb->sitemeta} WHERE meta_key = 'site_admins' AND meta_value = ''" ); 787 | 788 | foreach ( $rows as $row ) { 789 | wp_cache_delete( "{$row->site_id}:site_admins", 'site-options' ); 790 | 791 | $wpdb->delete( 792 | $wpdb->sitemeta, 793 | [ 'meta_id' => $row->meta_id ] 794 | ); 795 | } 796 | } 797 | 798 | return true; 799 | } 800 | 801 | // copied from populate_network() 802 | private static function create_initial_blog( 803 | $network_id, 804 | $blog_id, 805 | $domain, 806 | $path, 807 | $subdomain_install, 808 | $site_user 809 | ) { 810 | global $wpdb, $current_site, $wp_rewrite; 811 | 812 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is meant to replace Core functionality. 813 | $current_site = new stdClass(); 814 | $current_site->domain = $domain; 815 | $current_site->path = $path; 816 | $current_site->site_name = ucfirst( $domain ); 817 | $blog_data = [ 818 | 'site_id' => $network_id, 819 | 'domain' => $domain, 820 | 'path' => $path, 821 | 'registered' => current_time( 'mysql' ), 822 | ]; 823 | $wpdb->insert( $wpdb->blogs, $blog_data ); 824 | $current_site->blog_id = $wpdb->insert_id; 825 | $blog_id = $wpdb->insert_id; 826 | update_user_meta( $site_user->ID, 'source_domain', $domain ); 827 | update_user_meta( $site_user->ID, 'primary_blog', $blog_id ); 828 | 829 | if ( $subdomain_install ) { 830 | $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); 831 | } else { 832 | $wp_rewrite->set_permalink_structure( '/blog/%year%/%monthnum%/%day%/%postname%/' ); 833 | } 834 | 835 | flush_rewrite_rules(); 836 | } 837 | 838 | // copied from populate_network() 839 | private static function add_site_admins( $site_user ) { 840 | $site_admins = [ $site_user->user_login ]; 841 | $users = get_users( [ 'fields' => [ 'ID', 'user_login' ] ] ); 842 | if ( $users ) { 843 | foreach ( $users as $user ) { 844 | if ( is_super_admin( $user->ID ) 845 | && ! in_array( $user->user_login, $site_admins, true ) ) { 846 | $site_admins[] = $user->user_login; 847 | } 848 | } 849 | } 850 | 851 | update_site_option( 'site_admins', $site_admins ); 852 | } 853 | 854 | private static function modify_wp_config( $content ) { 855 | $wp_config_path = Utils\locate_wp_config(); 856 | 857 | $token = "/* That's all, stop editing!"; 858 | $config_contents = file_get_contents( $wp_config_path ); 859 | if ( false === strpos( $config_contents, $token ) ) { 860 | return false; 861 | } 862 | 863 | list( $before, $after ) = explode( $token, $config_contents ); 864 | 865 | $content = trim( $content ); 866 | 867 | file_put_contents( 868 | $wp_config_path, 869 | "{$before}\n\n{$content}\n\n{$token}{$after}" 870 | ); 871 | 872 | return true; 873 | } 874 | 875 | private static function get_clean_basedomain() { 876 | $domain = preg_replace( '|https?://|', '', get_option( 'siteurl' ) ); 877 | $slash = strpos( $domain, '/' ); 878 | if ( false !== $slash ) { 879 | $domain = substr( $domain, 0, $slash ); 880 | } 881 | return $domain; 882 | } 883 | 884 | /** 885 | * Displays the WordPress version. 886 | * 887 | * ## OPTIONS 888 | * 889 | * [--extra] 890 | * : Show extended version information. 891 | * 892 | * ## EXAMPLES 893 | * 894 | * # Display the WordPress version 895 | * $ wp core version 896 | * 4.5.2 897 | * 898 | * # Display WordPress version along with other information 899 | * $ wp core version --extra 900 | * WordPress version: 4.5.2 901 | * Database revision: 36686 902 | * TinyMCE version: 4.310 (4310-20160418) 903 | * Package language: en_US 904 | * 905 | * @when before_wp_load 906 | */ 907 | public function version( $args = [], $assoc_args = [] ) { 908 | $details = self::get_wp_details(); 909 | 910 | if ( ! Utils\get_flag_value( $assoc_args, 'extra' ) ) { 911 | WP_CLI::line( $details['wp_version'] ); 912 | return; 913 | } 914 | 915 | $match = []; 916 | $found_version = preg_match( '/(\d)(\d+)-/', $details['tinymce_version'], $match ); 917 | $human_readable_tiny_mce = $found_version ? "{$match[1]}.{$match[2]}" : ''; 918 | 919 | echo Utils\mustache_render( 920 | self::get_template_path( 'versions.mustache' ), 921 | [ 922 | 'wp-version' => $details['wp_version'], 923 | 'db-version' => $details['wp_db_version'], 924 | 'local-package' => empty( $details['wp_local_package'] ) 925 | ? 'en_US' 926 | : $details['wp_local_package'], 927 | 'mce-version' => $human_readable_tiny_mce 928 | ? "{$human_readable_tiny_mce} ({$details['tinymce_version']})" 929 | : $details['tinymce_version'], 930 | ] 931 | ); 932 | } 933 | 934 | /** 935 | * Gets version information from `wp-includes/version.php`. 936 | * 937 | * @return array { 938 | * @type string $wp_version The WordPress version. 939 | * @type int $wp_db_version The WordPress DB revision. 940 | * @type string $tinymce_version The TinyMCE version. 941 | * @type string $wp_local_package The TinyMCE version. 942 | * } 943 | */ 944 | private static function get_wp_details( $abspath = ABSPATH ) { 945 | $versions_path = $abspath . 'wp-includes/version.php'; 946 | 947 | if ( ! is_readable( $versions_path ) ) { 948 | WP_CLI::error( 949 | "This does not seem to be a WordPress installation.\n" . 950 | 'Pass --path=`path/to/wordpress` or run `wp core download`.' 951 | ); 952 | } 953 | 954 | $version_content = file_get_contents( $versions_path, false, null, 6, 2048 ); 955 | 956 | $vars = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ]; 957 | $result = []; 958 | 959 | foreach ( $vars as $var_name ) { 960 | $result[ $var_name ] = self::find_var( $var_name, $version_content ); 961 | } 962 | 963 | return $result; 964 | } 965 | 966 | /** 967 | * Gets the template path based on installation type. 968 | */ 969 | private static function get_template_path( $template ) { 970 | $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); 971 | $template_path = "{$command_root}/templates/{$template}"; 972 | 973 | if ( ! file_exists( $template_path ) ) { 974 | WP_CLI::error( "Couldn't find {$template}" ); 975 | } 976 | 977 | return $template_path; 978 | } 979 | 980 | /** 981 | * Searches for the value assigned to variable `$var_name` in PHP code `$code`. 982 | * 983 | * This is equivalent to matching the `\$VAR_NAME = ([^;]+)` regular expression and returning 984 | * the first match either as a `string` or as an `integer` (depending if it's surrounded by 985 | * quotes or not). 986 | * 987 | * @param string $var_name Variable name to search for. 988 | * @param string $code PHP code to search in. 989 | * 990 | * @return int|string|null 991 | */ 992 | private static function find_var( $var_name, $code ) { 993 | $start = strpos( $code, '$' . $var_name . ' = ' ); 994 | 995 | if ( ! $start ) { 996 | return null; 997 | } 998 | 999 | $start = $start + strlen( $var_name ) + 3; 1000 | $end = strpos( $code, ';', $start ); 1001 | 1002 | $value = substr( $code, $start, $end - $start ); 1003 | 1004 | return trim( $value, " '" ); 1005 | } 1006 | 1007 | /** 1008 | * Security copy of the core function with Requests - Gets the checksums for the given version of WordPress. 1009 | * 1010 | * @param string $version Version string to query. 1011 | * @param string $locale Locale to query. 1012 | * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. 1013 | * @return string|array String message on failure. An array of checksums on success. 1014 | */ 1015 | private static function get_core_checksums( $version, $locale, $insecure ) { 1016 | $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); 1017 | 1018 | try { 1019 | $checksums = $wp_org_api->get_core_checksums( $version, $locale ); 1020 | } catch ( Exception $exception ) { 1021 | return $exception->getMessage(); 1022 | } 1023 | 1024 | if ( false === $checksums ) { 1025 | return "Checksums not available for WordPress {$version}/{$locale}."; 1026 | } 1027 | 1028 | return $checksums; 1029 | } 1030 | 1031 | /** 1032 | * Updates WordPress to a newer version. 1033 | * 1034 | * Defaults to updating WordPress to the latest version. 1035 | * 1036 | * If you see "Error: Another update is currently in progress.", you may 1037 | * need to run `wp option delete core_updater.lock` after verifying another 1038 | * update isn't actually running. 1039 | * 1040 | * ## OPTIONS 1041 | * 1042 | * [] 1043 | * : Path to zip file to use, instead of downloading from wordpress.org. 1044 | * 1045 | * [--minor] 1046 | * : Only perform updates for minor releases (e.g. update from WP 4.3 to 4.3.3 instead of 4.4.2). 1047 | * 1048 | * [--version=] 1049 | * : Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly'. 1050 | * 1051 | * [--force] 1052 | * : Update even when installed WP version is greater than the requested version. 1053 | * 1054 | * [--locale=] 1055 | * : Select which language you want to download. 1056 | * 1057 | * [--insecure] 1058 | * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. 1059 | * 1060 | * ## EXAMPLES 1061 | * 1062 | * # Update WordPress 1063 | * $ wp core update 1064 | * Updating to version 4.5.2 (en_US)... 1065 | * Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip... 1066 | * Unpacking the update... 1067 | * Cleaning up files... 1068 | * No files found that need cleaning up 1069 | * Success: WordPress updated successfully. 1070 | * 1071 | * # Update WordPress using zip file. 1072 | * $ wp core update ../latest.zip 1073 | * Starting update... 1074 | * Unpacking the update... 1075 | * Success: WordPress updated successfully. 1076 | * 1077 | * # Update WordPress to 3.1 forcefully 1078 | * $ wp core update --version=3.1 --force 1079 | * Updating to version 3.1 (en_US)... 1080 | * Downloading update from https://wordpress.org/wordpress-3.1.zip... 1081 | * Unpacking the update... 1082 | * Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually. 1083 | * Success: WordPress updated successfully. 1084 | * 1085 | * @alias upgrade 1086 | */ 1087 | public function update( $args, $assoc_args ) { 1088 | global $wp_version; 1089 | 1090 | $update = null; 1091 | $upgrader = 'WP_CLI\\Core\\CoreUpgrader'; 1092 | 1093 | if ( 'trunk' === Utils\get_flag_value( $assoc_args, 'version' ) ) { 1094 | $assoc_args['version'] = 'nightly'; 1095 | } 1096 | 1097 | if ( ! empty( $args[0] ) ) { 1098 | 1099 | // ZIP path or URL is given 1100 | $upgrader = 'WP_CLI\\Core\\NonDestructiveCoreUpgrader'; 1101 | $version = Utils\get_flag_value( $assoc_args, 'version' ); 1102 | 1103 | $update = (object) [ 1104 | 'response' => 'upgrade', 1105 | 'current' => $version, 1106 | 'download' => $args[0], 1107 | 'packages' => (object) [ 1108 | 'partial' => null, 1109 | 'new_bundled' => null, 1110 | 'no_content' => null, 1111 | 'full' => $args[0], 1112 | ], 1113 | 'version' => $version, 1114 | 'locale' => null, 1115 | ]; 1116 | 1117 | } elseif ( empty( $assoc_args['version'] ) ) { 1118 | 1119 | // Update to next release 1120 | wp_version_check(); 1121 | $from_api = get_site_transient( 'update_core' ); 1122 | 1123 | if ( Utils\get_flag_value( $assoc_args, 'minor' ) ) { 1124 | foreach ( $from_api->updates as $offer ) { 1125 | $sem_ver = Utils\get_named_sem_ver( $offer->version, $wp_version ); 1126 | if ( ! $sem_ver || 'patch' !== $sem_ver ) { 1127 | continue; 1128 | } 1129 | $update = $offer; 1130 | break; 1131 | } 1132 | if ( empty( $update ) ) { 1133 | WP_CLI::success( 'WordPress is at the latest minor release.' ); 1134 | return; 1135 | } 1136 | } elseif ( ! empty( $from_api->updates ) ) { 1137 | list( $update ) = $from_api->updates; 1138 | } 1139 | } elseif ( Utils\wp_version_compare( $assoc_args['version'], '<' ) 1140 | || 'nightly' === $assoc_args['version'] 1141 | || Utils\get_flag_value( $assoc_args, 'force' ) ) { 1142 | 1143 | // Specific version is given 1144 | $version = $assoc_args['version']; 1145 | $locale = Utils\get_flag_value( $assoc_args, 'locale', get_locale() ); 1146 | 1147 | $new_package = $this->get_download_url( $version, $locale ); 1148 | 1149 | $update = (object) [ 1150 | 'response' => 'upgrade', 1151 | 'current' => $assoc_args['version'], 1152 | 'download' => $new_package, 1153 | 'packages' => (object) [ 1154 | 'partial' => null, 1155 | 'new_bundled' => null, 1156 | 'no_content' => null, 1157 | 'full' => $new_package, 1158 | ], 1159 | 'version' => $version, 1160 | 'locale' => $locale, 1161 | ]; 1162 | 1163 | } 1164 | 1165 | if ( ! empty( $update ) 1166 | && ( $update->version !== $wp_version 1167 | || Utils\get_flag_value( $assoc_args, 'force' ) ) ) { 1168 | 1169 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 1170 | 1171 | if ( $update->version ) { 1172 | WP_CLI::log( "Updating to version {$update->version} ({$update->locale})..." ); 1173 | } else { 1174 | WP_CLI::log( 'Starting update...' ); 1175 | } 1176 | 1177 | $from_version = $wp_version; 1178 | $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); 1179 | 1180 | $GLOBALS['wpcli_core_update_obj'] = $update; 1181 | $result = Utils\get_upgrader( $upgrader, $insecure )->upgrade( $update ); 1182 | unset( $GLOBALS['wpcli_core_update_obj'] ); 1183 | 1184 | if ( is_wp_error( $result ) ) { 1185 | $message = WP_CLI::error_to_string( $result ); 1186 | if ( 'up_to_date' !== $result->get_error_code() ) { 1187 | WP_CLI::error( $message ); 1188 | } else { 1189 | WP_CLI::success( $message ); 1190 | } 1191 | } else { 1192 | 1193 | $to_version = ''; 1194 | if ( file_exists( ABSPATH . 'wp-includes/version.php' ) ) { 1195 | $wp_details = self::get_wp_details(); 1196 | $to_version = $wp_details['wp_version']; 1197 | } 1198 | 1199 | $locale = (string) Utils\get_flag_value( $assoc_args, 'locale', get_locale() ); 1200 | $this->cleanup_extra_files( $from_version, $to_version, $locale, $insecure ); 1201 | 1202 | WP_CLI::success( 'WordPress updated successfully.' ); 1203 | } 1204 | } else { 1205 | WP_CLI::success( 'WordPress is up to date.' ); 1206 | } 1207 | } 1208 | 1209 | /** 1210 | * Runs the WordPress database update procedure. 1211 | * 1212 | * ## OPTIONS 1213 | * 1214 | * [--network] 1215 | * : Update databases for all sites on a network 1216 | * 1217 | * [--dry-run] 1218 | * : Compare database versions without performing the update. 1219 | * 1220 | * ## EXAMPLES 1221 | * 1222 | * # Update the WordPress database. 1223 | * $ wp core update-db 1224 | * Success: WordPress database upgraded successfully from db version 36686 to 35700. 1225 | * 1226 | * # Update databases for all sites on a network. 1227 | * $ wp core update-db --network 1228 | * WordPress database upgraded successfully from db version 35700 to 29630 on example.com/ 1229 | * Success: WordPress database upgraded on 123/123 sites. 1230 | * 1231 | * @subcommand update-db 1232 | */ 1233 | public function update_db( $args, $assoc_args ) { 1234 | global $wpdb, $wp_db_version, $wp_current_db_version; 1235 | 1236 | $network = Utils\get_flag_value( $assoc_args, 'network' ); 1237 | if ( $network && ! is_multisite() ) { 1238 | WP_CLI::error( 'This is not a multisite installation.' ); 1239 | } 1240 | 1241 | $dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' ); 1242 | if ( $dry_run ) { 1243 | WP_CLI::log( 'Performing a dry run, with no database modification.' ); 1244 | } 1245 | 1246 | if ( $network ) { 1247 | $iterator_args = [ 1248 | 'table' => $wpdb->blogs, 1249 | 'where' => [ 1250 | 'spam' => 0, 1251 | 'deleted' => 0, 1252 | 'archived' => 0, 1253 | ], 1254 | ]; 1255 | $it = new TableIterator( $iterator_args ); 1256 | $success = 0; 1257 | $total = 0; 1258 | $site_ids = []; 1259 | foreach ( $it as $blog ) { 1260 | ++$total; 1261 | $site_ids[] = $blog->site_id; 1262 | $url = $blog->domain . $blog->path; 1263 | $cmd = "--url={$url} core update-db"; 1264 | if ( $dry_run ) { 1265 | $cmd .= ' --dry-run'; 1266 | } 1267 | $process = WP_CLI::runcommand( 1268 | $cmd, 1269 | [ 1270 | 'return' => 'all', 1271 | 'exit_error' => false, 1272 | ] 1273 | ); 1274 | if ( 0 === (int) $process->return_code ) { 1275 | // See if we can parse the stdout 1276 | if ( preg_match( '#Success: (.+)#', $process->stdout, $matches ) ) { 1277 | $message = rtrim( $matches[1], '.' ); 1278 | $message = "{$message} on {$url}"; 1279 | } else { 1280 | $message = "Database upgraded successfully on {$url}"; 1281 | } 1282 | WP_CLI::log( $message ); 1283 | ++$success; 1284 | } else { 1285 | WP_CLI::warning( "Database failed to upgrade on {$url}" ); 1286 | } 1287 | } 1288 | if ( ! $dry_run && $total && $success === $total ) { 1289 | foreach ( array_unique( $site_ids ) as $site_id ) { 1290 | update_metadata( 'site', $site_id, 'wpmu_upgrade_site', $wp_db_version ); 1291 | } 1292 | } 1293 | WP_CLI::success( "WordPress database upgraded on {$success}/{$total} sites." ); 1294 | } else { 1295 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 1296 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here. 1297 | $wp_current_db_version = (int) __get_option( 'db_version' ); 1298 | if ( $wp_db_version !== $wp_current_db_version ) { 1299 | if ( $dry_run ) { 1300 | WP_CLI::success( "WordPress database will be upgraded from db version {$wp_current_db_version} to {$wp_db_version}." ); 1301 | } else { 1302 | // WP upgrade isn't too fussy about generating MySQL warnings such as "Duplicate key name" during an upgrade so suppress. 1303 | $wpdb->suppress_errors(); 1304 | 1305 | // WP upgrade expects `$_SERVER['HTTP_HOST']` to be set in `wp_guess_url()`, otherwise get PHP notice. 1306 | if ( ! isset( $_SERVER['HTTP_HOST'] ) ) { 1307 | $_SERVER['HTTP_HOST'] = 'example.com'; 1308 | } 1309 | 1310 | wp_upgrade(); 1311 | 1312 | WP_CLI::success( "WordPress database upgraded successfully from db version {$wp_current_db_version} to {$wp_db_version}." ); 1313 | } 1314 | } else { 1315 | WP_CLI::success( "WordPress database already at latest db version {$wp_db_version}." ); 1316 | } 1317 | } 1318 | } 1319 | 1320 | /** 1321 | * Gets download url based on version, locale and desired file type. 1322 | * 1323 | * @param $version 1324 | * @param string $locale 1325 | * @param string $file_type 1326 | * @return string 1327 | */ 1328 | private function get_download_url( $version, $locale = 'en_US', $file_type = 'zip' ) { 1329 | 1330 | if ( 'nightly' === $version ) { 1331 | if ( 'zip' === $file_type ) { 1332 | return 'https://wordpress.org/nightly-builds/wordpress-latest.zip'; 1333 | } else { 1334 | WP_CLI::error( 'Nightly builds are only available in .zip format.' ); 1335 | } 1336 | } 1337 | 1338 | $locale_subdomain = 'en_US' === $locale ? '' : substr( $locale, 0, 2 ) . '.'; 1339 | $locale_suffix = 'en_US' === $locale ? '' : "-{$locale}"; 1340 | // Match 6.7.0 but not 6.0 1341 | if ( substr_count( $version, '.' ) > 1 && substr( $version, -2 ) === '.0' ) { 1342 | $version = substr( $version, 0, -2 ); 1343 | } 1344 | 1345 | return "https://{$locale_subdomain}wordpress.org/wordpress-{$version}{$locale_suffix}.{$file_type}"; 1346 | } 1347 | 1348 | /** 1349 | * Returns update information. 1350 | * 1351 | * @param array $assoc_args Associative array of arguments. 1352 | * @return array List of available updates , or an empty array if no updates are available. 1353 | */ 1354 | private function get_updates( $assoc_args ) { 1355 | $force_check = Utils\get_flag_value( $assoc_args, 'force-check' ); 1356 | wp_version_check( [], $force_check ); 1357 | $from_api = get_site_transient( 'update_core' ); 1358 | if ( ! $from_api ) { 1359 | return []; 1360 | } 1361 | 1362 | $compare_version = str_replace( '-src', '', $GLOBALS['wp_version'] ); 1363 | 1364 | $updates = [ 1365 | 'major' => false, 1366 | 'minor' => false, 1367 | ]; 1368 | foreach ( $from_api->updates as $offer ) { 1369 | 1370 | $update_type = Utils\get_named_sem_ver( $offer->version, $compare_version ); 1371 | if ( ! $update_type ) { 1372 | continue; 1373 | } 1374 | 1375 | // WordPress follow its own versioning which is roughly equivalent to semver 1376 | if ( 'minor' === $update_type ) { 1377 | $update_type = 'major'; 1378 | } elseif ( 'patch' === $update_type ) { 1379 | $update_type = 'minor'; 1380 | } 1381 | 1382 | if ( ! empty( $updates[ $update_type ] ) && ! Comparator::greaterThan( $offer->version, $updates[ $update_type ]['version'] ) ) { 1383 | continue; 1384 | } 1385 | 1386 | $updates[ $update_type ] = [ 1387 | 'version' => $offer->version, 1388 | 'update_type' => $update_type, 1389 | 'package_url' => ! empty( $offer->packages->partial ) ? $offer->packages->partial : $offer->packages->full, 1390 | ]; 1391 | } 1392 | 1393 | foreach ( $updates as $type => $value ) { 1394 | if ( empty( $value ) ) { 1395 | unset( $updates[ $type ] ); 1396 | } 1397 | } 1398 | 1399 | foreach ( [ 'major', 'minor' ] as $type ) { 1400 | if ( true === Utils\get_flag_value( $assoc_args, $type ) ) { 1401 | return ! empty( $updates[ $type ] ) 1402 | ? [ $updates[ $type ] ] 1403 | : []; 1404 | } 1405 | } 1406 | return array_values( $updates ); 1407 | } 1408 | 1409 | /** 1410 | * Clean up extra files. 1411 | * 1412 | * @param string $version_from Starting version that the installation was updated from. 1413 | * @param string $version_to Target version that the installation is updated to. 1414 | * @param string $locale Locale of the installation. 1415 | * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. 1416 | */ 1417 | private function cleanup_extra_files( $version_from, $version_to, $locale, $insecure ) { 1418 | if ( ! $version_from || ! $version_to ) { 1419 | WP_CLI::warning( 'Failed to find WordPress version. Please cleanup files manually.' ); 1420 | return; 1421 | } 1422 | 1423 | $old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure ); 1424 | if ( ! is_array( $old_checksums ) ) { 1425 | WP_CLI::warning( "{$old_checksums} Please cleanup files manually." ); 1426 | return; 1427 | } 1428 | 1429 | $new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure ); 1430 | if ( ! is_array( $new_checksums ) ) { 1431 | WP_CLI::warning( "{$new_checksums} Please cleanup files manually." ); 1432 | 1433 | return; 1434 | } 1435 | 1436 | // Compare the files from the old version and the new version in a case-insensitive manner, 1437 | // to prevent files being incorrectly deleted on systems with case-insensitive filesystems 1438 | // when core changes the case of filenames. 1439 | // The main logic for this was taken from the Joomla project and adapted for WP. 1440 | // See: https://github.com/joomla/joomla-cms/blob/bb5368c7ef9c20270e6e9fcc4b364cd0849082a5/administrator/components/com_admin/script.php#L8158 1441 | 1442 | $old_filepaths = array_keys( $old_checksums ); 1443 | $new_filepaths = array_keys( $new_checksums ); 1444 | 1445 | $new_filepaths = array_combine( array_map( 'strtolower', $new_filepaths ), $new_filepaths ); 1446 | 1447 | $old_filepaths_to_check = array_diff( $old_filepaths, $new_filepaths ); 1448 | 1449 | foreach ( $old_filepaths_to_check as $old_filepath_to_check ) { 1450 | $old_realpath = realpath( ABSPATH . $old_filepath_to_check ); 1451 | 1452 | // On Unix without incorrectly cased file. 1453 | if ( false === $old_realpath ) { 1454 | continue; 1455 | } 1456 | 1457 | $lowercase_old_filepath_to_check = strtolower( $old_filepath_to_check ); 1458 | 1459 | if ( ! array_key_exists( $lowercase_old_filepath_to_check, $new_filepaths ) ) { 1460 | $files_to_remove[] = $old_filepath_to_check; 1461 | continue; 1462 | } 1463 | 1464 | // We are now left with only the files that are similar from old to new except for their case. 1465 | 1466 | $old_basename = basename( $old_realpath ); 1467 | $new_filepath = $new_filepaths[ $lowercase_old_filepath_to_check ]; 1468 | $expected_basename = basename( $new_filepath ); 1469 | $new_realpath = realpath( ABSPATH . $new_filepath ); 1470 | $new_basename = basename( $new_realpath ); 1471 | 1472 | // On Windows or Unix with only the incorrectly cased file. 1473 | if ( $new_basename !== $expected_basename ) { 1474 | WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' ); 1475 | 1476 | rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' ); 1477 | rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath ); 1478 | 1479 | continue; 1480 | } 1481 | 1482 | // There might still be an incorrectly cased file on other OS than Windows. 1483 | if ( basename( $old_filepath_to_check ) === $old_basename ) { 1484 | // Check if case-insensitive file system, eg on OSX. 1485 | if ( fileinode( $old_realpath ) === fileinode( $new_realpath ) ) { 1486 | // Check deeper because even realpath or glob might not return the actual case. 1487 | if ( ! in_array( $expected_basename, scandir( dirname( $new_realpath ) ), true ) ) { 1488 | WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' ); 1489 | 1490 | rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' ); 1491 | rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath ); 1492 | } 1493 | } else { 1494 | // On Unix with both files: Delete the incorrectly cased file. 1495 | $files_to_remove[] = $old_filepath_to_check; 1496 | } 1497 | } 1498 | } 1499 | 1500 | if ( ! empty( $files_to_remove ) ) { 1501 | WP_CLI::log( 'Cleaning up files...' ); 1502 | 1503 | $count = 0; 1504 | foreach ( $files_to_remove as $file ) { 1505 | 1506 | // wp-content should be considered user data 1507 | if ( 0 === stripos( $file, 'wp-content' ) ) { 1508 | continue; 1509 | } 1510 | 1511 | if ( file_exists( ABSPATH . $file ) ) { 1512 | unlink( ABSPATH . $file ); 1513 | WP_CLI::log( "File removed: {$file}" ); 1514 | ++$count; 1515 | } 1516 | } 1517 | 1518 | if ( $count ) { 1519 | WP_CLI::log( number_format( $count ) . ' files cleaned up.' ); 1520 | } else { 1521 | WP_CLI::log( 'No files found that need cleaning up.' ); 1522 | } 1523 | } 1524 | } 1525 | 1526 | private static function strip_content_dir( $zip_file ) { 1527 | $new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip'; 1528 | register_shutdown_function( 1529 | function () use ( $new_zip_file ) { 1530 | if ( file_exists( $new_zip_file ) ) { 1531 | unlink( $new_zip_file ); 1532 | } 1533 | } 1534 | ); 1535 | // Duplicate file to avoid modifying the original, which could be cache. 1536 | if ( ! copy( $zip_file, $new_zip_file ) ) { 1537 | WP_CLI::error( 'Failed to copy ZIP file.' ); 1538 | } 1539 | $zip = new ZipArchive(); 1540 | $res = $zip->open( $new_zip_file ); 1541 | if ( true === $res ) { 1542 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 1543 | for ( $i = 0; $i < $zip->numFiles; $i++ ) { 1544 | $info = $zip->statIndex( $i ); 1545 | // Strip all files in wp-content/themes and wp-content/plugins 1546 | // but leave the directories and index.php files intact. 1547 | if ( in_array( 1548 | $info['name'], 1549 | array( 1550 | 'wordpress/wp-content/plugins/', 1551 | 'wordpress/wp-content/plugins/index.php', 1552 | 'wordpress/wp-content/themes/', 1553 | 'wordpress/wp-content/themes/index.php', 1554 | ), 1555 | true 1556 | ) ) { 1557 | continue; 1558 | } 1559 | 1560 | if ( 0 === stripos( $info['name'], 'wordpress/wp-content/themes/' ) || 0 === stripos( $info['name'], 'wordpress/wp-content/plugins/' ) ) { 1561 | $zip->deleteIndex( $i ); 1562 | } 1563 | } 1564 | $zip->close(); 1565 | return $new_zip_file; 1566 | } else { 1567 | WP_CLI::error( 'ZipArchive failed to open ZIP file.' ); 1568 | } 1569 | } 1570 | } 1571 | -------------------------------------------------------------------------------- /src/WP_CLI/Core/CoreUpgrader.php: -------------------------------------------------------------------------------- 1 | insecure = $insecure; 34 | parent::__construct( $skin ); 35 | } 36 | 37 | /** 38 | * Caches the download, and uses cached if available. 39 | * 40 | * @access public 41 | * 42 | * @param string $package The URI of the package. If this is the full path to an 43 | * existing local file, it will be returned untouched. 44 | * @param bool $check_signatures Whether to validate file signatures. Default false. 45 | * @param array $hook_extra Extra arguments to pass to the filter hooks. Default empty array. 46 | * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object. 47 | */ 48 | public function download_package( $package, $check_signatures = false, $hook_extra = [] ) { 49 | 50 | /** 51 | * Filter whether to return the package. 52 | * 53 | * @since 3.7.0 54 | * 55 | * @param bool $reply Whether to bail without returning the package. Default is false. 56 | * @param string $package The package file name. 57 | * @param object $this The WP_Upgrader instance. 58 | */ 59 | $reply = apply_filters( 60 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Override existing hook from Core. 61 | 'upgrader_pre_download', 62 | false, 63 | $package, 64 | $this, 65 | $hook_extra 66 | ); 67 | if ( false !== $reply ) { 68 | return $reply; 69 | } 70 | 71 | // Check if package is a local or remote file. Bail if it's local. 72 | if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { 73 | return $package; 74 | } 75 | 76 | if ( empty( $package ) ) { 77 | return new WP_Error( 'no_package', $this->strings['no_package'] ); 78 | } 79 | 80 | $filename = pathinfo( $package, PATHINFO_FILENAME ); 81 | $extension = pathinfo( $package, PATHINFO_EXTENSION ); 82 | 83 | $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; 84 | register_shutdown_function( 85 | function () use ( $temp ) { 86 | if ( file_exists( $temp ) ) { 87 | unlink( $temp ); 88 | } 89 | } 90 | ); 91 | 92 | $cache = WP_CLI::get_cache(); 93 | $update = $GLOBALS['wpcli_core_update_obj']; 94 | $cache_key = "core/{$filename}-{$update->locale}.{$extension}"; 95 | $cache_file = $cache->has( $cache_key ); 96 | 97 | if ( $cache_file && false === stripos( $package, 'https://wordpress.org/nightly-builds/' ) 98 | && false === stripos( $package, 'http://wordpress.org/nightly-builds/' ) ) { 99 | WP_CLI::log( "Using cached file '{$cache_file}'..." ); 100 | copy( $cache_file, $temp ); 101 | return $temp; 102 | } 103 | 104 | /* 105 | * Download to a temporary file because piping from cURL to tar is flaky 106 | * on MinGW (and probably in other environments too). 107 | */ 108 | $headers = [ 'Accept' => 'application/json' ]; 109 | $options = [ 110 | 'timeout' => 600, // 10 minutes ought to be enough for everybody. 111 | 'filename' => $temp, 112 | 'halt_on_error' => false, 113 | 'insecure' => $this->insecure, 114 | ]; 115 | 116 | $this->skin->feedback( 'downloading_package', $package ); 117 | 118 | /** @var \Requests_Response|\WpOrg\Requests\Response null $req */ 119 | try { 120 | $response = Utils\http_request( 'GET', $package, null, $headers, $options ); 121 | } catch ( Exception $e ) { 122 | return new WP_Error( 'download_failed', $e->getMessage() ); 123 | } 124 | 125 | if ( ! is_null( $response ) && 200 !== (int) $response->status_code ) { 126 | return new WP_Error( 'download_failed', $this->strings['download_failed'] ); 127 | } 128 | 129 | if ( false === stripos( $package, 'https://wordpress.org/nightly-builds/' ) ) { 130 | $cache->import( $cache_key, $temp ); 131 | } 132 | 133 | return $temp; 134 | } 135 | 136 | /** 137 | * Upgrade WordPress core. 138 | * 139 | * @access public 140 | * 141 | * @global WP_Filesystem_Base $wp_filesystem Subclass 142 | * @global callable $_wp_filesystem_direct_method 143 | * 144 | * @param object $current Response object for whether WordPress is current. 145 | * @param array $args { 146 | * Optional. Arguments for upgrading WordPress core. Default empty array. 147 | * 148 | * @type bool $pre_check_md5 Whether to check the file checksums before 149 | * attempting the upgrade. Default true. 150 | * @type bool $attempt_rollback Whether to attempt to rollback the chances if 151 | * there is a problem. Default false. 152 | * @type bool $do_rollback Whether to perform this "upgrade" as a rollback. 153 | * Default false. 154 | * } 155 | * @return null|false|WP_Error False or WP_Error on failure, null on success. 156 | */ 157 | public function upgrade( $current, $args = [] ) { 158 | set_error_handler( [ __CLASS__, 'error_handler' ], E_USER_WARNING | E_USER_NOTICE ); 159 | 160 | $result = parent::upgrade( $current, $args ); 161 | 162 | restore_error_handler(); 163 | 164 | return $result; 165 | } 166 | 167 | /** 168 | * Error handler to ignore failures on accessing SSL "https://api.wordpress.org/core/checksums/1.0/" in `get_core_checksums()` which seem to occur intermittently. 169 | */ 170 | public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext = null ) { 171 | // If ignoring E_USER_WARNING | E_USER_NOTICE, default. 172 | if ( ! ( error_reporting() & $errno ) ) { 173 | return false; 174 | } 175 | // If not in "wp-admin/includes/update.php", default. 176 | $update_php = 'wp-admin/includes/update.php'; 177 | if ( 0 !== substr_compare( $errfile, $update_php, -strlen( $update_php ) ) ) { 178 | return false; 179 | } 180 | // Else assume it's in `get_core_checksums()` and just ignore it. 181 | return true; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/WP_CLI/Core/NonDestructiveCoreUpgrader.php: -------------------------------------------------------------------------------- 1 |