diff --git a/.actrc b/.actrc deleted file mode 100644 index 99e6b7ecc..000000000 --- a/.actrc +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration file for nektos/act. -# See https://github.com/nektos/act#configuration --P ubuntu-latest=shivammathur/node:latest diff --git a/.distignore b/.distignore index 95b52fb02..b964b40c7 100644 --- a/.distignore +++ b/.distignore @@ -6,8 +6,6 @@ .travis.yml behat.yml circle.yml -phpcs.xml.dist -phpunit.xml.dist bin/ features/ utils/ diff --git a/.editorconfig b/.editorconfig index 84f918ed5..fa483b1bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,8 +4,6 @@ # WordPress Coding Standards # https://make.wordpress.org/core/handbook/coding-standards/ -# From https://github.com/WordPress/wordpress-develop/blob/trunk/.editorconfig with a couple of additions. - root = true [*] @@ -15,12 +13,13 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[{*.yml,*.feature,.jshintrc,*.json}] +[{.jshintrc,*.json,*.yml,*.feature}] indent_style = space indent_size = 2 -[*.md] -trim_trailing_whitespace = false - [{*.txt,wp-config-sample.php}] end_of_line = crlf + +[composer.json] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index d84f4ade2..000000000 --- a/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -/.actrc export-ignore -/.distignore export-ignore -/.editorconfig export-ignore -/.github export-ignore -/.gitignore export-ignore -/.typos.toml export-ignore -/AGENTS.md export-ignore -/behat.yml export-ignore -/features export-ignore -/phpcs.xml.dist export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/wp-cli.yml export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index f69375fb2..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @wp-cli/committers diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 66079d9da..12b980072 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -2,12 +2,10 @@ Thanks for submitting a pull request! -Please review our contributing guidelines if you haven't recently: https://make.wordpress.org/cli/handbook/contributing/#creating-a-pull-request - Here's an overview to our process: -1. One of the project committers will soon provide a code review: https://make.wordpress.org/cli/handbook/code-review/ -2. You are expected to address the code review comments in a timely manner (if we don't hear from you in two weeks, we'll consider your pull request abandoned). +1. One of the project committers will soon provide a code review. +2. You are expected to address the code review comments in a timely manner. 3. Please make sure to include functional tests for your changes. 4. The reviewing committer will merge your pull request as soon as it passes code review (and provided it fits within the scope of the project). diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d6c7b8b04..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: composer - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - labels: - - scope:distribution - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - labels: - - scope:distribution - diff --git a/.github/workflows/check-branch-alias.yml b/.github/workflows/check-branch-alias.yml deleted file mode 100644 index 78da63710..000000000 --- a/.github/workflows/check-branch-alias.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Check Branch Alias - -on: - release: - types: [released] - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - check-branch-alias: - uses: wp-cli/.github/.github/workflows/reusable-check-branch-alias.yml@main diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index e9fe57761..000000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Code Quality Checks - -on: - pull_request: - push: - branches: - - main - - master - schedule: - - cron: '17 2 * * *' # Run every day on a seemly random time. - -jobs: - code-quality: - uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index ffb6f8fd4..000000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -permissions: - contents: read - -jobs: - copilot-setup-steps: - name: Setup environment - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Check existence of composer.json file - id: check_composer_file - run: echo "files_exists=$(test -f composer.json && echo true || echo false)" >> "$GITHUB_OUTPUT" - - - name: Set up PHP environment - if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 - with: - php-version: 'latest' - ini-values: zend.assertions=1, error_reporting=-1, display_errors=On - coverage: 'none' - tools: composer,cs2pr - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Composer dependencies & cache dependencies - if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - env: - COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml deleted file mode 100644 index 68334703a..000000000 --- a/.github/workflows/issue-triage.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Issue and PR Triage - -'on': - issues: - types: [opened] - pull_request_target: - types: [opened] - workflow_dispatch: - inputs: - issue_number: - description: 'Issue/PR number to triage (leave empty to process all)' - required: false - type: string - -permissions: - issues: write - pull-requests: write - actions: write - contents: read - models: read - -jobs: - issue-triage: - uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main - with: - issue_number: >- - ${{ - (github.event_name == 'workflow_dispatch' && inputs.issue_number) || - (github.event_name == 'pull_request_target' && github.event.pull_request.number) || - (github.event_name == 'issues' && github.event.issue.number) || - '' - }} diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml deleted file mode 100644 index 45711bded..000000000 --- a/.github/workflows/manage-labels.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Manage Labels - -'on': - workflow_dispatch: - push: - branches: - - main - - master - paths: - - 'composer.json' - -permissions: - issues: write - contents: read - -jobs: - manage-labels: - uses: wp-cli/.github/.github/workflows/reusable-manage-labels.yml@main diff --git a/.github/workflows/regenerate-readme.yml b/.github/workflows/regenerate-readme.yml deleted file mode 100644 index 6198d6308..000000000 --- a/.github/workflows/regenerate-readme.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Regenerate README file - -on: - workflow_dispatch: - push: - branches: - - main - - master - paths-ignore: - - "features/**" - - "README.md" - -permissions: - contents: write - pull-requests: write - -jobs: - regenerate-readme: - uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index bf67592d8..000000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Testing - -on: - workflow_dispatch: - pull_request: - push: - branches: - - main - - master - schedule: - - cron: '17 1 * * *' # Run every day on a seemly random time. - -jobs: - test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml deleted file mode 100644 index bc01490b3..000000000 --- a/.github/workflows/welcome-new-contributors.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Welcome New Contributors - -on: - pull_request_target: - types: [opened] - branches: - - main - - master - -permissions: - pull-requests: write - -jobs: - welcome: - uses: wp-cli/.github/.github/workflows/reusable-welcome-new-contributors.yml@main diff --git a/.gitignore b/.gitignore index bcf211b32..54f24c800 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,3 @@ node_modules/ vendor/ *.zip *.tar.gz -composer.lock -*.log -phpunit.xml -phpcs.xml -.phpcs.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..0835aa68c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,53 @@ +sudo: false +dist: trusty + +language: php + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - vendor + - $HOME/.composer/cache + +env: + global: + - PATH="$TRAVIS_BUILD_DIR/vendor/bin:$PATH" + - WP_CLI_BIN_DIR="$TRAVIS_BUILD_DIR/vendor/bin" + +matrix: + include: + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=3.7.11 + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.3 + dist: precise + env: WP_VERSION=latest + +before_install: + - phpenv config-rm xdebug.ini + +install: + - composer require wp-cli/wp-cli:dev-master + - composer install + - bash bin/install-package-tests.sh + +before_script: + - composer validate + +script: + - bash bin/test.sh diff --git a/.typos.toml b/.typos.toml deleted file mode 100644 index 965f891fc..000000000 --- a/.typos.toml +++ /dev/null @@ -1,6 +0,0 @@ -[default] -extend-ignore-re = [ - "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", - "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", - "(#|//)\\s*spellchecker:ignore-next-line\\n.*" -] diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 1ff84f6d1..000000000 --- a/AGENTS.md +++ /dev/null @@ -1,121 +0,0 @@ -# Instructions - -This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file. - -## Best Practices for Code Contributions - -When contributing to this package, please adhere to the following guidelines: - -* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns. -* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package. -* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory. -* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation. - -### Building and running - -Before submitting any changes, it is crucial to validate them by running the full suite of static code analysis and tests. To run the full suite of checks, execute the following command: `composer test`. - -This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps separately, it is highly recommended to use this single command to ensure a comprehensive validation. - -### Useful Composer Commands - -The project uses Composer to manage dependencies and run scripts. The following commands are available: - -* `composer install`: Install dependencies. -* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests. -* `composer lint`: Check for syntax errors. -* `composer phpcs`: Check for code style violations. -* `composer phpcbf`: Automatically fix code style violations. -* `composer phpstan`: Run static analysis. -* `composer phpunit`: Run unit tests. -* `composer behat`: Run behavior-driven tests. - -### Coding Style - -The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them. - -## Documentation - -The `README.md` file might be 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)). In that case, changes need to be made against the corresponding part of the codebase. - -### Inline Documentation - -Only write high-value comments if at all. Avoid talking to the user through comments. - -## Testing - -The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis. - -* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`. -* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`. -* **Static analysis** is performed with PHPStan. - -All tests are run on GitHub Actions for every pull request. - -When writing tests, aim to follow existing patterns. Key conventions include: - -* When adding tests, first examine existing tests to understand and conform to established conventions. -* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class. -* For Behat tests, only WP-CLI commands installed in `composer.json` can be run. - -### Behat Steps - -WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests. - -> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` . - -#### Given - -* `Given an empty directory` - Creates an empty directory. -* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory. -* `Given an empty cache` - Clears the WP-CLI cache directory. -* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents. -* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex. -* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL. -* `Given WP files` - Download WordPress files without installing. -* `Given wp-config.php` - Create a wp-config.php file using `wp config create`. -* `Given a database` - Creates an empty database. -* `Given a WP install(ation)` - Installs WordPress. -* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory. -* `Given a WP install(ation) with Composer` - Installs WordPress with Composer. -* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory. -* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite. -* `Given these installed and active plugins:` - Installs and activates one or more plugins. -* `Given a custom wp-content directory` - Configure a custom `wp-content` directory. -* `Given download:` - Download multiple files into the given destinations. -* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable. -* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version. -* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub. -* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable. -* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string. -* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency. -* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory. -* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory. - -#### When - -* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background. -* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command. -* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory. -* `When /^I (run|try) the previous command again$/` - Run or try the previous command again. - -#### Then - -* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command. -* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR. -* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value. -* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value. -* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows. -* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows. -* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT. -* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT. -* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values. -* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content. -* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty. -* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty. -* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version. -* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents. -* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex. -* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex. -* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not). -* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`. diff --git a/LICENSE b/LICENSE index a082e073e..0c6145565 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (C) 2011-2018 WP-CLI Development Group (https://github.com/wp-cli/export-command/contributors) +Copyright (C) 2011-2017 WP-CLI Development Group (https://github.com/wp-cli/export-command/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f64560905..9e898ee4b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ wp-cli/export-command ===================== -Exports WordPress content to a WXR file. +Export WordPress content to a WXR file. -[![Testing](https://github.com/wp-cli/export-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/export-command/actions/workflows/testing.yml) [![Code Coverage](https://codecov.io/gh/wp-cli/export-command/branch/main/graph/badge.svg)](https://codecov.io/gh/wp-cli/export-command/tree/main) +[![Build Status](https://travis-ci.org/wp-cli/export-command.svg?branch=master)](https://travis-ci.org/wp-cli/export-command) Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) ## Using ~~~ -wp export [--dir=] [--stdout] [--skip_comments] [--skip_authors] [--skip_terms] [--max_file_size=] [--filename_format=] [--include_once=] [--allow_orphan_terms] [--start_date=] [--end_date=] [--post_type=] [--post_type__not_in=] [--post__in=] [--with_attachments] [--start_id=] [--max_num_posts=] [--author=] [--category=] [--post_status=] +wp export [--dir=] [--skip_comments] [--max_file_size=] [--start_date=] [--end_date=] [--post_type=] [--post_type__not_in=] [--post__in=] [--start_id=] [--author=] [--category=] [--post_status=] [--filename_format=] ~~~ Generates one or more WXR files containing authors, terms, posts, @@ -23,35 +23,15 @@ comments, and attachments. WXR files do not include site configuration Full path to directory where WXR export files should be stored. Defaults to current working directory. - [--stdout] - Output the whole XML using standard output (incompatible with --dir=) - [--skip_comments] Don't include comments in the WXR export file. - [--skip_authors] - Don't include authors in the WXR export file. - - [--skip_terms] - Don't include terms (categories, tags, custom taxonomy terms and nav menu terms) in the WXR export file. - [--max_file_size=] - A single export file should have this many megabytes. -1 for unlimited. + A single export file should have this many megabytes. --- default: 15 --- - [--filename_format=] - Use a custom format for export filenames. Defaults to '{site}.wordpress.{date}.{n}.xml'. - - [--include_once=] - Include specified export section only in the first export file. Valid options - are categories, tags, nav_menu_items, custom_taxonomies_terms. Separate multiple - sections with a comma. Defaults to none. - - [--allow_orphan_terms] - Export orphaned terms with `parent=0`, instead of throwing an exception. - **FILTERS** [--start_date=] @@ -72,27 +52,23 @@ comments, and attachments. WXR files do not include site configuration with a comma. Defaults to none. [--post__in=] - Export all posts specified as a comma-separated or space-separated list of IDs. - Post's attachments won't be exported unless --with_attachments is specified. - - [--with_attachments] - Force including attachments in case --post__in has been specified. + Export all posts specified as a comma- or space-separated list of IDs. [--start_id=] Export only posts with IDs greater than or equal to this post ID. - [--max_num_posts=] - Export no more than posts (excluding attachments). - [--author=] Export only posts by this author. Can be either user login or user ID. - [--category=] + [--category=] Export only posts in this category. [--post_status=] Export only posts with this status. + [--filename_format=] + Use a custom format for export filenames. Defaults to '{site}.wordpress.{date}.{n}.xml'. + **EXAMPLES** # Export posts published by the user between given start and end date @@ -143,13 +119,9 @@ Want to contribute a new feature? Please first [open a new issue](https://github 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. -### License - -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - ## Support -GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support +Github issues aren't for general support questions, but there are other venues you can try: http://wp-cli.org/#support *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.* diff --git a/behat.yml b/behat.yml deleted file mode 100644 index d6ee86224..000000000 --- a/behat.yml +++ /dev/null @@ -1,7 +0,0 @@ -default: - suites: - default: - contexts: - - WP_CLI\Tests\Context\FeatureContext - paths: - - features diff --git a/bin/install-package-tests.sh b/bin/install-package-tests.sh new file mode 100755 index 000000000..2ff49dd8d --- /dev/null +++ b/bin/install-package-tests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -ex + +install_db() { + mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot + mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot +} + +install_db diff --git a/bin/test.sh b/bin/test.sh new file mode 100644 index 000000000..bd3ae6e91 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -ex + +# Run the unit tests, if they exist +if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] +then + phpunit +fi + +# Run the functional tests +BEHAT_TAGS=$(php utils/behat-tags.php) +behat --format progress $BEHAT_TAGS --strict diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/composer.json b/composer.json index 026479d4e..c9b13aa63 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,11 @@ { "name": "wp-cli/export-command", + "description": "Export WordPress content to a WXR file.", "type": "wp-cli-package", - "description": "Exports WordPress content to a WXR file.", "homepage": "https://github.com/wp-cli/export-command", + "support": { + "issues": "https://github.com/wp-cli/export-command/issues" + }, "license": "MIT", "authors": [ { @@ -11,65 +14,26 @@ "homepage": "https://runcommand.io" } ], + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": {"": "src/"}, + "files": [ "export-command.php" ] + }, "require": { "nb/oxymel": "~0.1.0", - "wp-cli/wp-cli": "^2.13" + "wp-cli/wp-cli": "*" }, "require-dev": { - "wp-cli/db-command": "^1.3 || ^2", - "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/extension-command": "^1.2 || ^2", - "wp-cli/import-command": "^1 || ^2", - "wp-cli/media-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^5" - }, - "config": { - "process-timeout": 7200, - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true, - "phpstan/extension-installer": true - }, - "lock": false + "behat/behat": "~2.5" }, "extra": { "branch-alias": { - "dev-main": "2.x-dev" + "dev-master": "1.x-dev" }, "bundled": true, "commands": [ "export" ] - }, - "autoload": { - "classmap": [ - "src/" - ], - "files": [ - "export-command.php" - ] - }, - "minimum-stability": "dev", - "prefer-stable": true, - "scripts": { - "behat": "run-behat-tests", - "behat-rerun": "rerun-behat-tests", - "lint": "run-linter-tests", - "phpcs": "run-phpcs-tests", - "phpstan": "run-phpstan-tests", - "phpcbf": "run-phpcbf-cleanup", - "phpunit": "run-php-unit-tests", - "prepare-tests": "install-package-tests", - "test": [ - "@lint", - "@phpcs", - "@phpstan", - "@phpunit", - "@behat" - ] - }, - "support": { - "issues": "https://github.com/wp-cli/export-command/issues" } } diff --git a/export-command.php b/export-command.php index 6e3d4211b..4add2dfca 100644 --- a/export-command.php +++ b/export-command.php @@ -4,9 +4,9 @@ return; } -$wpcli_export_autoloader = __DIR__ . '/vendor/autoload.php'; -if ( file_exists( $wpcli_export_autoloader ) ) { - require_once $wpcli_export_autoloader; +$autoload = dirname( __FILE__ ) . '/vendor/autoload.php'; +if ( file_exists( $autoload ) ) { + require_once $autoload; } WP_CLI::add_command( 'export', 'Export_Command' ); diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..33620fa61 --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,617 @@ +autoload->files ) ) { + $contents = 'require:' . PHP_EOL; + foreach( $composer->autoload->files as $file ) { + $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file . PHP_EOL; + } + @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' ); + $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml'; + file_put_contents( $project_config, $contents ); + putenv( 'WP_CLI_CONFIG_PATH=' . $project_config ); + } + } +// Inside WP-CLI +} else { + require_once __DIR__ . '/../../php/utils.php'; + require_once __DIR__ . '/../../php/WP_CLI/Process.php'; + require_once __DIR__ . '/../../php/WP_CLI/ProcessRun.php'; + if ( file_exists( __DIR__ . '/../../vendor/autoload.php' ) ) { + require_once __DIR__ . '/../../vendor/autoload.php'; + } else if ( file_exists( __DIR__ . '/../../../../autoload.php' ) ) { + require_once __DIR__ . '/../../../../autoload.php'; + } +} + +/** + * Features context. + */ +class FeatureContext extends BehatContext implements ClosuredContextInterface { + + /** + * The current working directory for scenarios that have a "Given a WP install" or "Given an empty directory" step. Variable RUN_DIR. Lives until the end of the scenario. + */ + private static $run_dir; + + /** + * Where WordPress core is downloaded to for caching, and which is copied to RUN_DIR during a "Given a WP install" step. Lives until manually deleted. + */ + private static $cache_dir; + + /** + * The directory that the WP-CLI cache (WP_CLI_CACHE_DIR, normally "$HOME/.wp-cli/cache") is set to on a "Given an empty cache" step. + * Variable SUITE_CACHE_DIR. Lives until the end of the scenario (or until another "Given an empty cache" step within the scenario). + */ + private static $suite_cache_dir; + + /** + * Where the current WP-CLI source repository is copied to for Composer-based tests with a "Given a dependency on current wp-cli" step. + * Variable COMPOSER_LOCAL_REPOSITORY. Lives until the end of the suite. + */ + private static $composer_local_repository; + + /** + * The test database settings. All but `dbname` can be set via environment variables. The database is dropped at the start of each scenario and created on a "Given a WP install" step. + */ + private static $db_settings = array( + 'dbname' => 'wp_cli_test', + 'dbuser' => 'wp_cli_test', + 'dbpass' => 'password1', + 'dbhost' => '127.0.0.1', + ); + + /** + * Array of background process ids started by the current scenario. Used to terminate them at the end of the scenario. + */ + private $running_procs = array(); + + /** + * Array of variables available as {VARIABLE_NAME}. Some are always set: CORE_CONFIG_SETTINGS, SRC_DIR, CACHE_DIR, WP_VERSION-version-latest. Some are step-dependent: + * RUN_DIR, SUITE_CACHE_DIR, COMPOSER_LOCAL_REPOSITORY, PHAR_PATH. Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario. + */ + public $variables = array(); + + /** + * The current feature file and scenario line number as '.'. Used in RUN_DIR and SUITE_CACHE_DIR directory names. Set at the start of each scenario. + */ + private static $temp_dir_infix; + + /** + * Get the environment variables required for launched `wp` processes + */ + private static function get_process_env_variables() { + // Ensure we're using the expected `wp` binary + $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . '/../../bin' ); + $vendor_dir = realpath( __DIR__ . '/../../vendor/bin' ); + $env = array( + 'PATH' => $bin_dir . ':' . $vendor_dir . ':' . getenv( 'PATH' ), + 'BEHAT_RUN' => 1, + 'HOME' => '/tmp/wp-cli-home', + ); + if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { + $env['WP_CLI_CONFIG_PATH'] = $config_path; + } + if ( $term = getenv( 'TERM' ) ) { + $env['TERM'] = $term; + } + if ( $php_args = getenv( 'WP_CLI_PHP_ARGS' ) ) { + $env['WP_CLI_PHP_ARGS'] = $php_args; + } + if ( $travis_build_dir = getenv( 'TRAVIS_BUILD_DIR' ) ) { + $env['TRAVIS_BUILD_DIR'] = $travis_build_dir; + } + return $env; + } + + /** + * We cache the results of `wp core download` to improve test performance. + * Ideally, we'd cache at the HTTP layer for more reliable tests. + */ + private static function cache_wp_files() { + self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache'; + + if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) + return; + + $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); + if ( getenv( 'WP_VERSION' ) ) { + $cmd .= Utils\esc_cmd( ' --version=%s', getenv( 'WP_VERSION' ) ); + } + Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); + } + + /** + * @BeforeSuite + */ + public static function prepare( SuiteEvent $event ) { + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); + echo PHP_EOL; + echo $result->stdout; + echo PHP_EOL; + self::cache_wp_files(); + $result = Process::create( Utils\esc_cmd( 'wp core version --path=%s', self::$cache_dir ) , null, self::get_process_env_variables() )->run_check(); + echo 'WordPress ' . $result->stdout; + echo PHP_EOL; + } + + /** + * @AfterSuite + */ + public static function afterSuite( SuiteEvent $event ) { + if ( self::$composer_local_repository ) { + self::remove_dir( self::$composer_local_repository ); + self::$composer_local_repository = null; + } + } + + /** + * @BeforeScenario + */ + public function beforeScenario( $event ) { + $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); + + // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. + self::$temp_dir_infix = null; + if ( $file = self::get_event_file( $event, $line ) ) { + self::$temp_dir_infix = basename( $file ) . '.' . $line; + } + } + + /** + * @AfterScenario + */ + public function afterScenario( $event ) { + + if ( self::$run_dir ) { + // remove altered WP install, unless there's an error + if ( $event->getResult() < 4 ) { + self::remove_dir( self::$run_dir ); + } + self::$run_dir = null; + } + + // Remove WP-CLI package directory if any. Set to `wp package path` by package-command and scaffold-package-command features, and by cli-info.feature. + if ( isset( $this->variables['PACKAGE_PATH'] ) ) { + self::remove_dir( $this->variables['PACKAGE_PATH'] ); + } + + // Remove SUITE_CACHE_DIR if any. + if ( self::$suite_cache_dir ) { + self::remove_dir( self::$suite_cache_dir ); + self::$suite_cache_dir = null; + } + + // Remove any background processes. + foreach ( $this->running_procs as $proc ) { + $status = proc_get_status( $proc ); + self::terminate_proc( $status['pid'] ); + } + } + + /** + * Terminate a process and any of its children. + */ + private static function terminate_proc( $master_pid ) { + + $output = `ps -o ppid,pid,command | grep $master_pid`; + + foreach ( explode( PHP_EOL, $output ) as $line ) { + if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { + $parent = $matches[1]; + $child = $matches[2]; + + if ( $parent == $master_pid ) { + self::terminate_proc( $child ); + } + } + } + + if ( ! posix_kill( (int) $master_pid, 9 ) ) { + $errno = posix_get_last_error(); + // Ignore "No such process" error as that's what we want. + if ( 3 /*ESRCH*/ !== $errno ) { + throw new RuntimeException( posix_strerror( $errno ) ); + } + } + } + + /** + * Create a temporary WP_CLI_CACHE_DIR. Exposed as SUITE_CACHE_DIR in "Given an empty cache" step. + */ + public static function create_cache_dir() { + if ( self::$suite_cache_dir ) { + self::remove_dir( self::$suite_cache_dir ); + } + self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', TRUE ); + mkdir( self::$suite_cache_dir ); + return self::$suite_cache_dir; + } + + /** + * Initializes context. + * Every scenario gets its own context object. + * + * @param array $parameters context parameters (set them up through behat.yml) + */ + public function __construct( array $parameters ) { + if ( getenv( 'WP_CLI_TEST_DBUSER' ) ) { + self::$db_settings['dbuser'] = getenv( 'WP_CLI_TEST_DBUSER' ); + } + + if ( false !== getenv( 'WP_CLI_TEST_DBPASS' ) ) { + self::$db_settings['dbpass'] = getenv( 'WP_CLI_TEST_DBPASS' ); + } + + if ( getenv( 'WP_CLI_TEST_DBHOST' ) ) { + self::$db_settings['dbhost'] = getenv( 'WP_CLI_TEST_DBHOST' ); + } + + $this->drop_db(); + $this->set_cache_dir(); + $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); + } + + public function getStepDefinitionResources() { + return glob( __DIR__ . '/../steps/*.php' ); + } + + public function getHookDefinitionResources() { + return array(); + } + + /** + * Replace {VARIABLE_NAME}. Note that variable names can only contain uppercase letters and underscores (no numbers). + */ + public function replace_variables( $str ) { + $ret = preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); + if ( false !== strpos( $str, '{WP_VERSION-' ) ) { + $ret = $this->_replace_wp_versions( $ret ); + } + return $ret; + } + + /** + * Replace variables callback. + */ + private function _replace_var( $matches ) { + $cmd = $matches[0]; + + foreach ( array_slice( $matches, 1 ) as $key ) { + $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd ); + } + + return $cmd; + } + + /** + * Substitute "{WP_VERSION-version-latest}" variables. + */ + private function _replace_wp_versions( $str ) { + static $wp_versions = null; + if ( null === $wp_versions ) { + $wp_versions = array(); + + $response = Requests::get( 'https://api.wordpress.org/core/version-check/1.7/', null, array( 'timeout' => 30 ) ); + if ( 200 === $response->status_code && ( $body = json_decode( $response->body ) ) && is_object( $body ) && isset( $body->offers ) && is_array( $body->offers ) ) { + // Latest version alias. + $wp_versions["{WP_VERSION-latest}"] = count( $body->offers ) ? $body->offers[0]->version : ''; + foreach ( $body->offers as $offer ) { + $sub_ver = preg_replace( '/(^[0-9]+\.[0-9]+)\.[0-9]+$/', '$1', $offer->version ); + $sub_ver_key = "{WP_VERSION-{$sub_ver}-latest}"; + + $main_ver = preg_replace( '/(^[0-9]+)\.[0-9]+$/', '$1', $sub_ver ); + $main_ver_key = "{WP_VERSION-{$main_ver}-latest}"; + + if ( ! isset( $wp_versions[ $main_ver_key ] ) ) { + $wp_versions[ $main_ver_key ] = $offer->version; + } + if ( ! isset( $wp_versions[ $sub_ver_key ] ) ) { + $wp_versions[ $sub_ver_key ] = $offer->version; + } + } + } + } + return strtr( $str, $wp_versions ); + } + + /** + * Get the file and line number for the current behat event. + */ + private static function get_event_file( $event, &$line ) { + if ( method_exists( $event, 'getScenario' ) ) { + $scenario_feature = $event->getScenario(); + } elseif ( method_exists( $event, 'getFeature' ) ) { + $scenario_feature = $event->getFeature(); + } else { + return null; + } + $line = $scenario_feature->getLine(); + return $scenario_feature->getFile(); + } + + /** + * Create the RUN_DIR directory, unless already set for this scenario. + */ + public function create_run_dir() { + if ( !isset( $this->variables['RUN_DIR'] ) ) { + self::$run_dir = $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', TRUE ); + mkdir( $this->variables['RUN_DIR'] ); + } + } + + public function build_phar( $version = 'same' ) { + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; + + // Test running against a package installed as a WP-CLI dependency + // WP-CLI installed as a project dependency + $make_phar_path = __DIR__ . '/../../../../../utils/make-phar.php'; + if ( ! file_exists( $make_phar_path ) ) { + // Test running against WP-CLI proper + $make_phar_path = __DIR__ . '/../../utils/make-phar.php'; + if ( ! file_exists( $make_phar_path ) ) { + // WP-CLI as a dependency of this project + $make_phar_path = __DIR__ . '/../../vendor/wp-cli/wp-cli/utils/make-phar.php'; + } + } + + $this->proc( Utils\esc_cmd( + 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', + $make_phar_path, + $this->variables['PHAR_PATH'], + $version + ) )->run_check(); + } + + public function download_phar( $version = 'same' ) { + if ( 'same' === $version ) { + $version = WP_CLI_VERSION; + } + + $download_url = sprintf( + 'https://github.com/wp-cli/wp-cli/releases/download/v%1$s/wp-cli-%1$s.phar', + $version + ); + + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' + . uniqid( 'wp-cli-download-', true ) + . '.phar'; + + Process::create( Utils\esc_cmd( + 'curl -sSfL %1$s > %2$s && chmod +x %2$s', + $download_url, + $this->variables['PHAR_PATH'] + ) )->run_check(); + } + + /** + * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. + */ + private function set_cache_dir() { + $path = sys_get_temp_dir() . '/wp-cli-test-cache'; + if ( ! file_exists( $path ) ) { + mkdir( $path ); + } + $this->variables['CACHE_DIR'] = $path; + } + + private static function run_sql( $sql ) { + Utils\run_mysql_command( '/usr/bin/env mysql --no-defaults', array( + 'execute' => $sql, + 'host' => self::$db_settings['dbhost'], + 'user' => self::$db_settings['dbuser'], + 'pass' => self::$db_settings['dbpass'], + ) ); + } + + public function create_db() { + $dbname = self::$db_settings['dbname']; + self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" ); + } + + public function drop_db() { + $dbname = self::$db_settings['dbname']; + self::run_sql( "DROP DATABASE IF EXISTS $dbname" ); + } + + public function proc( $command, $assoc_args = array(), $path = '' ) { + if ( !empty( $assoc_args ) ) + $command .= Utils\assoc_args_to_str( $assoc_args ); + + $env = self::get_process_env_variables(); + if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { + $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; + } + + if ( isset( $this->variables['RUN_DIR'] ) ) { + $cwd = "{$this->variables['RUN_DIR']}/{$path}"; + } else { + $cwd = null; + } + + return Process::create( $command, $cwd, $env ); + } + + /** + * Start a background process. Will automatically be closed when the tests finish. + */ + public function background_proc( $cmd ) { + $descriptors = array( + 0 => STDIN, + 1 => array( 'pipe', 'w' ), + 2 => array( 'pipe', 'w' ), + ); + + $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); + + sleep(1); + + $status = proc_get_status( $proc ); + + if ( !$status['running'] ) { + throw new RuntimeException( stream_get_contents( $pipes[2] ) ); + } else { + $this->running_procs[] = $proc; + } + } + + public function move_files( $src, $dest ) { + rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); + } + + /** + * Remove a directory (recursive). + */ + public static function remove_dir( $dir ) { + Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + } + + /** + * Copy a directory (recursive). Destination directory must exist. + */ + public static function copy_dir( $src_dir, $dest_dir ) { + Process::create( Utils\esc_cmd( "cp -r %s/* %s", $src_dir, $dest_dir ) )->run_check(); + } + + public function add_line_to_wp_config( &$wp_config_code, $line ) { + $token = "/* That's all, stop editing!"; + + $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); + } + + public function download_wp( $subdir = '' ) { + $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; + + if ( $subdir ) { + mkdir( $dest_dir ); + } + + self::copy_dir( self::$cache_dir, $dest_dir ); + + // disable emailing + mkdir( $dest_dir . '/wp-content/mu-plugins' ); + copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); + } + + public function create_config( $subdir = '', $extra_php = false ) { + $params = self::$db_settings; + + // Replaces all characters that are not alphanumeric or an underscore into an underscore. + $params['dbprefix'] = $subdir ? preg_replace( '#[^a-zA-Z\_0-9]#', '_', $subdir ) : 'wp_'; + + $params['skip-salts'] = true; + + if( false !== $extra_php ) { + $params['extra-php'] = $extra_php; + } + + $this->proc( 'wp core config', $params, $subdir )->run_check(); + } + + public function install_wp( $subdir = '' ) { + $subdir = $this->replace_variables( $subdir ); + + $this->create_db(); + $this->create_run_dir(); + $this->download_wp( $subdir ); + $this->create_config( $subdir ); + + $install_args = array( + 'url' => 'http://example.com', + 'title' => 'WP CLI Site', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1' + ); + + $this->proc( 'wp core install', $install_args, $subdir )->run_check(); + } + + public function install_wp_with_composer() { + $this->create_run_dir(); + $this->create_db(); + + $yml_path = $this->variables['RUN_DIR'] . "/wp-cli.yml"; + file_put_contents( $yml_path, 'path: wordpress' ); + + $this->proc( 'composer init --name="wp-cli/composer-test" --type="project" --no-interaction' )->run_check(); + $this->proc( 'composer require johnpbloch/wordpress --optimize-autoloader --no-interaction' )->run_check(); + + $config_extra_php = "require_once dirname(__DIR__) . '/vendor/autoload.php';"; + $this->create_config( 'wordpress', $config_extra_php ); + + $install_args = array( + 'url' => 'http://localhost:8080', + 'title' => 'WP CLI Site with both WordPress and wp-cli as Composer dependencies', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1' + ); + + $this->proc( 'wp core install', $install_args )->run_check(); + } + + public function composer_add_wp_cli_local_repository() { + if ( ! self::$composer_local_repository ) { + self::$composer_local_repository = sys_get_temp_dir() . '/' . uniqid( "wp-cli-composer-local-", TRUE ); + mkdir( self::$composer_local_repository ); + + $env = self::get_process_env_variables(); + $src = isset( $env['TRAVIS_BUILD_DIR'] ) ? $env['TRAVIS_BUILD_DIR'] : realpath( __DIR__ . '/../../' ); + + $dest = self::$composer_local_repository . '/'; + + self::copy_dir( $src, $dest ); + self::remove_dir( $dest . '.git' ); + self::remove_dir( $dest . 'vendor' ); + + $this->proc( "composer config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false}}'" )->run_check(); + } + $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; + } + + public function composer_require_current_wp_cli() { + $this->composer_add_wp_cli_local_repository(); + $this->proc( 'composer require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' )->run_check(); + } + + public function get_php_binary() { + if ( getenv( 'WP_CLI_PHP_USED' ) ) + return getenv( 'WP_CLI_PHP_USED' ); + + if ( getenv( 'WP_CLI_PHP' ) ) + return getenv( 'WP_CLI_PHP' ); + + if ( defined( 'PHP_BINARY' ) ) + return PHP_BINARY; + + return 'php'; + } + + public function start_php_server() { + $cmd = Utils\esc_cmd( '%s -S %s -t %s -c %s %s', + $this->get_php_binary(), + 'localhost:8080', + $this->variables['RUN_DIR'] . '/wordpress/', + get_cfg_var( 'cfg_file_path' ), + $this->variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php' + ); + $this->background_proc( $cmd ); + } + +} diff --git a/features/bootstrap/Process.php b/features/bootstrap/Process.php new file mode 100644 index 000000000..858e1947e --- /dev/null +++ b/features/bootstrap/Process.php @@ -0,0 +1,90 @@ +command = $command; + $proc->cwd = $cwd; + $proc->env = $env; + + return $proc; + } + + private function __construct() {} + + /** + * Run the command. + * + * @return ProcessRun + */ + public function run() { + $cwd = $this->cwd; + + $descriptors = array( + 0 => STDIN, + 1 => array( 'pipe', 'w' ), + 2 => array( 'pipe', 'w' ), + ); + + $proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env ); + + $stdout = stream_get_contents( $pipes[1] ); + fclose( $pipes[1] ); + + $stderr = stream_get_contents( $pipes[2] ); + fclose( $pipes[2] ); + + return new ProcessRun( array( + 'stdout' => $stdout, + 'stderr' => $stderr, + 'return_code' => proc_close( $proc ), + 'command' => $this->command, + 'cwd' => $cwd, + 'env' => $this->env, + ) ); + } + + /** + * Run the command, but throw an Exception on error. + * + * @return ProcessRun + */ + public function run_check() { + $r = $this->run(); + + // $r->STDERR is incorrect, but kept incorrect for backwards-compat + if ( $r->return_code || !empty( $r->STDERR ) ) { + throw new \RuntimeException( $r ); + } + + return $r; + } +} diff --git a/features/bootstrap/ProcessRun.php b/features/bootstrap/ProcessRun.php new file mode 100644 index 000000000..aedc5f65d --- /dev/null +++ b/features/bootstrap/ProcessRun.php @@ -0,0 +1,62 @@ + $value ) { + $this->$key = $value; + } + } + + /** + * Return properties of executed command as a string. + * + * @return string + */ + public function __toString() { + $out = "$ $this->command\n"; + $out .= "$this->stdout\n$this->stderr"; + $out .= "cwd: $this->cwd\n"; + $out .= "exit status: $this->return_code"; + + return $out; + } + +} diff --git a/features/bootstrap/support.php b/features/bootstrap/support.php new file mode 100644 index 000000000..a37a064f6 --- /dev/null +++ b/features/bootstrap/support.php @@ -0,0 +1,194 @@ + $value ) { + if ( ! compareContents( $value, $actual->$name ) ) + return false; + } + } else if ( is_array( $expected ) ) { + foreach ( $expected as $key => $value ) { + if ( ! compareContents( $value, $actual[$key] ) ) + return false; + } + } else { + return $expected === $actual; + } + + return true; +} + +/** + * Compare two strings containing JSON to ensure that @a $actualJson contains at + * least what the JSON string @a $expectedJson contains. + * + * @return whether or not @a $actualJson contains @a $expectedJson + * @retval true @a $actualJson contains @a $expectedJson + * @retval false @a $actualJson does not contain @a $expectedJson + * + * @param[in] $actualJson the JSON string to be tested + * @param[in] $expectedJson the expected JSON string + * + * Examples: + * expected: {'a':1,'array':[1,3,5]} + * + * 1 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: true + * + * 2 ) + * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * element 'a' is missing from the root object + * + * 3 ) + * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * the value of element 'a' is not 1 + * + * 4 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} + * return: false + * the contents of 'array' does not include 3 + */ +function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { + $actualValue = json_decode( $actualJson ); + $expectedValue = json_decode( $expectedJson ); + + if ( !$actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} + +/** + * Compare two strings to confirm $actualCSV contains $expectedCSV + * Both strings are expected to have headers for their CSVs. + * $actualCSV must match all data rows in $expectedCSV + * + * @param string A CSV string + * @param array A nested array of values + * @return bool Whether $actualCSV contains $expectedCSV + */ +function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { + $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); + + if ( empty( $actualCSV ) ) + return false; + + // Each sample must have headers + $actualHeaders = array_values( array_shift( $actualCSV ) ); + $expectedHeaders = array_values( array_shift( $expectedCSV ) ); + + // Each expectedCSV must exist somewhere in actualCSV in the proper column + $expectedResult = 0; + foreach ( $expectedCSV as $expected_row ) { + $expected_row = array_combine( $expectedHeaders, $expected_row ); + foreach ( $actualCSV as $actual_row ) { + + if ( count( $actualHeaders ) != count( $actual_row ) ) + continue; + + $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); + if ( $actual_row == $expected_row ) + $expectedResult++; + } + } + + return $expectedResult >= count( $expectedCSV ); +} + +/** + * Compare two strings containing YAML to ensure that @a $actualYaml contains at + * least what the YAML string @a $expectedYaml contains. + * + * @return whether or not @a $actualYaml contains @a $expectedJson + * @retval true @a $actualYaml contains @a $expectedJson + * @retval false @a $actualYaml does not contain @a $expectedJson + * + * @param[in] $actualYaml the YAML string to be tested + * @param[in] $expectedYaml the expected YAML string + */ +function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { + $actualValue = Mustangostang\Spyc::YAMLLoad( $actualYaml ); + $expectedValue = Mustangostang\Spyc::YAMLLoad( $expectedYaml ); + + if ( !$actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} + diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php new file mode 100644 index 000000000..46796053e --- /dev/null +++ b/features/bootstrap/utils.php @@ -0,0 +1,1069 @@ +config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { + array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); + } + } + return $vendor_paths; +} + +// Using require() directly inside a class grants access to private methods to the loaded code +function load_file( $path ) { + require_once $path; +} + +function load_command( $name ) { + $path = WP_CLI_ROOT . "/php/commands/$name.php"; + + if ( is_readable( $path ) ) { + include_once $path; + } +} + +/** + * Like array_map(), except it returns a new iterator, instead of a modified array. + * + * Example: + * + * $arr = array('Football', 'Socker'); + * + * $it = iterator_map($arr, 'strtolower', function($val) { + * return str_replace('foo', 'bar', $val); + * }); + * + * foreach ( $it as $val ) { + * var_dump($val); + * } + * + * @param array|object Either a plain array or another iterator + * @param callback The function to apply to an element + * @return object An iterator that applies the given callback(s) + */ +function iterator_map( $it, $fn ) { + if ( is_array( $it ) ) { + $it = new \ArrayIterator( $it ); + } + + if ( !method_exists( $it, 'add_transform' ) ) { + $it = new Transform( $it ); + } + + foreach ( array_slice( func_get_args(), 1 ) as $fn ) { + $it->add_transform( $fn ); + } + + return $it; +} + +/** + * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true + * @param string|array The files (or file) to search for + * @param string|null The directory to start searching from; defaults to CWD + * @param callable Function which is passed the current dir each time a directory level is traversed + * @return null|string Null if the file was not found + */ +function find_file_upward( $files, $dir = null, $stop_check = null ) { + $files = (array) $files; + if ( is_null( $dir ) ) { + $dir = getcwd(); + } + while ( @is_readable( $dir ) ) { + // Stop walking up when the supplied callable returns true being passed the $dir + if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { + return null; + } + + foreach ( $files as $file ) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if ( file_exists( $path ) ) { + return $path; + } + } + + $parent_dir = dirname( $dir ); + if ( empty($parent_dir) || $parent_dir === $dir ) { + break; + } + $dir = $parent_dir; + } + return null; +} + +function is_path_absolute( $path ) { + // Windows + if ( isset($path[1]) && ':' === $path[1] ) + return true; + + return $path[0] === '/'; +} + +/** + * Composes positional arguments into a command string. + * + * @param array + * @return string + */ +function args_to_str( $args ) { + return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); +} + +/** + * Composes associative arguments into a command string. + * + * @param array + * @return string + */ +function assoc_args_to_str( $assoc_args ) { + $str = ''; + + foreach ( $assoc_args as $key => $value ) { + if ( true === $value ) { + $str .= " --$key"; + } elseif( is_array( $value ) ) { + foreach( $value as $_ => $v ) { + $str .= assoc_args_to_str( array( $key => $v ) ); + } + } else { + $str .= " --$key=" . escapeshellarg( $value ); + } + } + + return $str; +} + +/** + * Given a template string and an arbitrary number of arguments, + * returns the final command, with the parameters escaped. + */ +function esc_cmd( $cmd ) { + if ( func_num_args() < 2 ) + trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); + + $args = func_get_args(); + + $cmd = array_shift( $args ); + + return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); +} + +function locate_wp_config() { + static $path; + + if ( null === $path ) { + if ( file_exists( ABSPATH . 'wp-config.php' ) ) + $path = ABSPATH . 'wp-config.php'; + elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) + $path = ABSPATH . '../wp-config.php'; + else + $path = false; + + if ( $path ) + $path = realpath( $path ); + } + + return $path; +} + +function wp_version_compare( $since, $operator ) { + return version_compare( str_replace( array( '-src' ), '', $GLOBALS['wp_version'] ), $since, $operator ); +} + +/** + * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. + * + * Given a collection of items with a consistent data structure: + * + * ``` + * $items = array( + * array( + * 'key' => 'foo', + * 'value' => 'bar', + * ) + * ); + * ``` + * + * Render `$items` as an ASCII table: + * + * ``` + * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); + * + * # +-----+-------+ + * # | key | value | + * # +-----+-------+ + * # | foo | bar | + * # +-----+-------+ + * ``` + * + * Or render `$items` as YAML: + * + * ``` + * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); + * + * # --- + * # - + * # key: foo + * # value: bar + * ``` + * + * @access public + * @category Output + * + * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count' + * @param array $items An array of items to output. + * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. + * @return null + */ +function format_items( $format, $items, $fields ) { + $assoc_args = compact( 'format', 'fields' ); + $formatter = new \WP_CLI\Formatter( $assoc_args ); + $formatter->display_items( $items ); +} + +/** + * Write data as CSV to a given file. + * + * @access public + * + * @param resource $fd File descriptor + * @param array $rows Array of rows to output + * @param array $headers List of CSV columns (optional) + */ +function write_csv( $fd, $rows, $headers = array() ) { + if ( ! empty( $headers ) ) { + fputcsv( $fd, $headers ); + } + + foreach ( $rows as $row ) { + if ( ! empty( $headers ) ) { + $row = pick_fields( $row, $headers ); + } + + fputcsv( $fd, array_values( $row ) ); + } +} + +/** + * Pick fields from an associative array or object. + * + * @param array|object Associative array or object to pick fields from + * @param array List of fields to pick + * @return array + */ +function pick_fields( $item, $fields ) { + $item = (object) $item; + + $values = array(); + + foreach ( $fields as $field ) { + $values[ $field ] = isset( $item->$field ) ? $item->$field : null; + } + + return $values; +} + +/** + * Launch system's $EDITOR for the user to edit some text. + * + * @access public + * @category Input + * + * @param string $content Some form of text to edit (e.g. post content) + * @return string|bool Edited text, if file is saved from editor; false, if no change to file. + */ +function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { + + check_proc_available( 'launch_editor_for_input' ); + + $tmpdir = get_temp_dir(); + + do { + $tmpfile = basename( $filename ); + $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); + $tmpfile .= '-' . substr( md5( rand() ), 0, 6 ); + $tmpfile = $tmpdir . $tmpfile . '.tmp'; + $fp = @fopen( $tmpfile, 'x' ); + if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { + $tmpfile = ''; + continue; + } + if ( $fp ) { + fclose( $fp ); + } + } while( ! $tmpfile ); + + if ( ! $tmpfile ) { + \WP_CLI::error( 'Error creating temporary file.' ); + } + + $output = ''; + file_put_contents( $tmpfile, $input ); + + $editor = getenv( 'EDITOR' ); + if ( !$editor ) { + if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) + $editor = 'notepad'; + else + $editor = 'vi'; + } + + $descriptorspec = array( STDIN, STDOUT, STDERR ); + $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); + $r = proc_close( $process ); + if ( $r ) { + exit( $r ); + } + + $output = file_get_contents( $tmpfile ); + + unlink( $tmpfile ); + + if ( $output === $input ) + return false; + + return $output; +} + +/** + * @param string MySQL host string, as defined in wp-config.php + * @return array + */ +function mysql_host_to_cli_args( $raw_host ) { + $assoc_args = array(); + + $host_parts = explode( ':', $raw_host ); + if ( count( $host_parts ) == 2 ) { + list( $assoc_args['host'], $extra ) = $host_parts; + $extra = trim( $extra ); + if ( is_numeric( $extra ) ) { + $assoc_args['port'] = intval( $extra ); + $assoc_args['protocol'] = 'tcp'; + } else if ( $extra !== '' ) { + $assoc_args['socket'] = $extra; + } + } else { + $assoc_args['host'] = $raw_host; + } + + return $assoc_args; +} + +function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { + check_proc_available( 'run_mysql_command' ); + + if ( !$descriptors ) + $descriptors = array( STDIN, STDOUT, STDERR ); + + if ( isset( $assoc_args['host'] ) ) { + $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); + } + + $pass = $assoc_args['pass']; + unset( $assoc_args['pass'] ); + + $old_pass = getenv( 'MYSQL_PWD' ); + putenv( 'MYSQL_PWD=' . $pass ); + + $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); + + $proc = proc_open( $final_cmd, $descriptors, $pipes ); + if ( !$proc ) + exit(1); + + $r = proc_close( $proc ); + + putenv( 'MYSQL_PWD=' . $old_pass ); + + if ( $r ) exit( $r ); +} + +/** + * Render PHP or other types of files using Mustache templates. + * + * IMPORTANT: Automatic HTML escaping is disabled! + */ +function mustache_render( $template_name, $data = array() ) { + if ( ! file_exists( $template_name ) ) + $template_name = WP_CLI_ROOT . "/templates/$template_name"; + + $template = file_get_contents( $template_name ); + + $m = new \Mustache_Engine( array( + 'escape' => function ( $val ) { return $val; }, + ) ); + + return $m->render( $template, $data ); +} + +/** + * Create a progress bar to display percent completion of a given operation. + * + * Progress bar is written to STDOUT, and disabled when command is piped. Progress + * advances with `$progress->tick()`, and completes with `$progress->finish()`. + * Process bar also indicates elapsed time and expected total time. + * + * ``` + * # `wp user generate` ticks progress bar each time a new user is created. + * # + * # $ wp user generate --count=500 + * # Generating users 22 % [=======> ] 0:05 / 0:23 + * + * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); + * for ( $i = 0; $i < $count; $i++ ) { + * // uses wp_insert_user() to insert the user + * $progress->tick(); + * } + * $progress->finish(); + * ``` + * + * @access public + * @category Output + * + * @param string $message Text to display before the progress bar. + * @param integer $count Total number of ticks to be performed. + * @return cli\progress\Bar|WP_CLI\NoOp + */ +function make_progress_bar( $message, $count ) { + if ( \cli\Shell::isPiped() ) + return new \WP_CLI\NoOp; + + return new \cli\progress\Bar( $message, $count ); +} + +function parse_url( $url ) { + $url_parts = \parse_url( $url ); + + if ( !isset( $url_parts['scheme'] ) ) { + $url_parts = parse_url( 'http://' . $url ); + } + + return $url_parts; +} + +/** + * Check if we're running in a Windows environment (cmd.exe). + * + * @return bool + */ +function is_windows() { + return false !== ( $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ) ) ? (bool) $test_is_windows : strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; +} + +/** + * Replace magic constants in some PHP source code. + * + * @param string $source The PHP code to manipulate. + * @param string $path The path to use instead of the magic constants + */ +function replace_path_consts( $source, $path ) { + $replacements = array( + '__FILE__' => "'$path'", + '__DIR__' => "'" . dirname( $path ) . "'", + ); + + $old = array_keys( $replacements ); + $new = array_values( $replacements ); + + return str_replace( $old, $new, $source ); +} + +/** + * Make a HTTP request to a remote URL. + * + * Wraps the Requests HTTP library to ensure every request includes a cert. + * + * ``` + * # `wp core download` verifies the hash for a downloaded WordPress archive + * + * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); + * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { + * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); + * } + * ``` + * + * @access public + * + * @param string $method HTTP method (GET, POST, DELETE, etc.) + * @param string $url URL to make the HTTP request to. + * @param array $headers Add specific headers to the request. + * @param array $options + * @return object + */ +function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { + + $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; + if ( inside_phar() ) { + // cURL can't read Phar archives + $options['verify'] = extract_from_phar( + WP_CLI_VENDOR_DIR . $cert_path ); + } else { + foreach( get_vendor_paths() as $vendor_path ) { + if ( file_exists( $vendor_path . $cert_path ) ) { + $options['verify'] = $vendor_path . $cert_path; + break; + } + } + if ( empty( $options['verify'] ) ){ + WP_CLI::error( "Cannot find SSL certificate." ); + } + } + + try { + $request = \Requests::request( $url, $headers, $data, $method, $options ); + return $request; + } catch( \Requests_Exception $ex ) { + // Handle SSL certificate issues gracefully + \WP_CLI::warning( $ex->getMessage() ); + $options['verify'] = false; + try { + return \Requests::request( $url, $headers, $data, $method, $options ); + } catch( \Requests_Exception $ex ) { + \WP_CLI::error( $ex->getMessage() ); + } + } +} + +/** + * Increments a version string using the "x.y.z-pre" format + * + * Can increment the major, minor or patch number by one + * If $new_version == "same" the version string is not changed + * If $new_version is not a known keyword, it will be used as the new version string directly + * + * @param string $current_version + * @param string $new_version + * @return string + */ +function increment_version( $current_version, $new_version ) { + // split version assuming the format is x.y.z-pre + $current_version = explode( '-', $current_version, 2 ); + $current_version[0] = explode( '.', $current_version[0] ); + + switch ( $new_version ) { + case 'same': + // do nothing + break; + + case 'patch': + $current_version[0][2]++; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'minor': + $current_version[0][1]++; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'major': + $current_version[0][0]++; + $current_version[0][1] = 0; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + default: // not a keyword + $current_version = array( array( $new_version ) ); + break; + } + + // reconstruct version string + $current_version[0] = implode( '.', $current_version[0] ); + $current_version = implode( '-', $current_version ); + + return $current_version; +} + +/** + * Compare two version strings to get the named semantic version. + * + * @access public + * + * @param string $new_version + * @param string $original_version + * @return string $name 'major', 'minor', 'patch' + */ +function get_named_sem_ver( $new_version, $original_version ) { + + if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { + return ''; + } + + $parts = explode( '-', $original_version ); + $bits = explode( '.', $parts[0] ); + $major = $bits[0]; + if ( isset( $bits[1] ) ) { + $minor = $bits[1]; + } + if ( isset( $bits[2] ) ) { + $patch = $bits[2]; + } + + if ( ! is_null( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { + return 'patch'; + } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { + return 'minor'; + } else { + return 'major'; + } +} + +/** + * Return the flag value or, if it's not set, the $default value. + * + * Because flags can be negated (e.g. --no-quiet to negate --quiet), this + * function provides a safer alternative to using + * `isset( $assoc_args['quiet'] )` or similar. + * + * @access public + * @category Input + * + * @param array $assoc_args Arguments array. + * @param string $flag Flag to get the value. + * @param mixed $default Default value for the flag. Default: NULL + * @return mixed + */ +function get_flag_value( $assoc_args, $flag, $default = null ) { + return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; +} + +/** + * Get the home directory. + * + * @access public + * @category System + * + * @return string + */ +function get_home_dir() { + $home = getenv( 'HOME' ); + if ( ! $home ) { + // In Windows $HOME may not be defined + $home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' ); + } + + return rtrim( $home, '/\\' ); +} + +/** + * Appends a trailing slash. + * + * @access public + * @category System + * + * @param string $string What to add the trailing slash to. + * @return string String with trailing slash added. + */ +function trailingslashit( $string ) { + return rtrim( $string, '/\\' ) . '/'; +} + +/** + * Get the system's temp directory. Warns user if it isn't writable. + * + * @access public + * @category System + * + * @return string + */ +function get_temp_dir() { + static $temp = ''; + + if ( $temp ) { + return $temp; + } + + // `sys_get_temp_dir()` introduced PHP 5.2.1. + if ( $try = sys_get_temp_dir() ) { + $temp = trailingslashit( $try ); + } elseif ( $try = ini_get( 'upload_tmp_dir' ) ) { + $temp = trailingslashit( $try ); + } else { + $temp = '/tmp/'; + } + + if ( ! @is_writable( $temp ) ) { + \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); + } + + return $temp; +} + +/** + * Parse a SSH url for its host, port, and path. + * + * Similar to parse_url(), but adds support for defined SSH aliases. + * + * ``` + * host OR host/path/to/wordpress OR host:port/path/to/wordpress + * ``` + * + * @access public + * + * @return mixed + */ +function parse_ssh_url( $url, $component = -1 ) { + preg_match( '#^((docker|docker\-compose|ssh):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); + $bits = array(); + foreach( array( + 2 => 'scheme', + 4 => 'user', + 5 => 'host', + 7 => 'port', + 8 => 'path', + ) as $i => $key ) { + if ( ! empty( $matches[ $i ] ) ) { + $bits[ $key ] = $matches[ $i ]; + } + } + switch ( $component ) { + case PHP_URL_SCHEME: + return isset( $bits['scheme'] ) ? $bits['scheme'] : null; + case PHP_URL_USER: + return isset( $bits['user'] ) ? $bits['user'] : null; + case PHP_URL_HOST: + return isset( $bits['host'] ) ? $bits['host'] : null; + case PHP_URL_PATH: + return isset( $bits['path'] ) ? $bits['path'] : null; + case PHP_URL_PORT: + return isset( $bits['port'] ) ? $bits['port'] : null; + default: + return $bits; + } +} + +/** + * Report the results of the same operation against multiple resources. + * + * @access public + * @category Input + * + * @param string $noun Resource being affected (e.g. plugin) + * @param string $verb Type of action happening to the noun (e.g. activate) + * @param integer $total Total number of resource being affected. + * @param integer $successes Number of successful operations. + * @param integer $failures Number of failures. + */ +function report_batch_operation_results( $noun, $verb, $total, $successes, $failures ) { + $plural_noun = $noun . 's'; + if ( in_array( $verb, array( 'reset' ), true ) ) { + $past_tense_verb = $verb; + } else { + $past_tense_verb = 'e' === substr( $verb, -1 ) ? $verb . 'd' : $verb . 'ed'; + } + $past_tense_verb_upper = ucfirst( $past_tense_verb ); + if ( $failures ) { + if ( $successes ) { + WP_CLI::error( "Only {$past_tense_verb} {$successes} of {$total} {$plural_noun}." ); + } else { + WP_CLI::error( "No {$plural_noun} {$past_tense_verb}." ); + } + } else { + if ( $successes ) { + WP_CLI::success( "{$past_tense_verb_upper} {$successes} of {$total} {$plural_noun}." ); + } else { + $message = $total > 1 ? ucfirst( $plural_noun ) : ucfirst( $noun ); + WP_CLI::success( "{$message} already {$past_tense_verb}." ); + } + } +} + +/** + * Parse a string of command line arguments into an $argv-esqe variable. + * + * @access public + * @category Input + * + * @param string $arguments + * @return array + */ +function parse_str_to_argv( $arguments ) { + preg_match_all ('/(?<=^|\s)([\'"]?)(.+?)(?{ATTACHMENT_ID} - """ - And the {EXPORT_FILE} file should not contain: - """ - attachment - """ + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -203,7 +174,6 @@ Feature: Export content. 2 """ - @require-wp-5.2 Scenario: Export multiple posts, separated by spaces Given a WP install @@ -222,40 +192,7 @@ Feature: Export content. And save STDOUT as {POST_ID_TWO} When I run `wp export --post__in="{POST_ID} {POST_ID_TWO}"` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - - When I run `wp site empty --yes` - Then STDOUT should not be empty - - When I run `wp import {EXPORT_FILE} --authors=skip` - Then STDOUT should not be empty - - When I run `wp post list --post_type=post --format=count` - Then STDOUT should be: - """ - 2 - """ - - @require-wp-5.2 - Scenario: Export multiple posts, separated by comma - Given a WP install - - When I run `wp plugin install wordpress-importer --activate` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp post create --post_title='Test post' --porcelain` - Then STDOUT should be a number - And save STDOUT as {POST_ID} - - When I run `wp post create --post_title='Test post 2' --porcelain` - Then STDOUT should be a number - And save STDOUT as {POST_ID_TWO} - - When I run `wp export --post__in="{POST_ID},{POST_ID_TWO}"` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -269,7 +206,6 @@ Feature: Export content. 2 """ - @require-wp-5.2 Scenario: Export posts within a given date range Given a WP install @@ -290,7 +226,7 @@ Feature: Export content. """ When I run `wp export --post_type=post --start_date=2013-08-02 --end_date=2013-08-02` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -310,172 +246,59 @@ Feature: Export content. 10 """ - @require-wp-5.2 Scenario: Export posts from a given category Given a WP install - And I run `wp site empty --yes` - And I run `wp plugin install wordpress-importer --activate` - - When I run `wp term create category Apple --porcelain` - Then STDOUT should be a number - And save STDOUT as {APPLE_TERM_ID} - - When I run `wp term create category Pear --porcelain` - Then STDOUT should be a number - And save STDOUT as {PEAR_TERM_ID} - - When I run `wp post create --post_type=post --post_title='Apple Post' --post_category={APPLE_TERM_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {APPLE_POST_ID} - - When I run `wp post create --post_type=post --post_title='Pear Post' --post_category={PEAR_TERM_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {PEAR_POST_ID} - When I run `wp export --post_type=post --category=apple` - And save STDOUT 'Writing to file %s' as {EXPORT_FILE} - Then the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: + When I run `wp plugin install wordpress-importer --activate` + Then STDERR should not contain: """ - + Warning: """ - When I run `wp export --post_type=post --category={PEAR_TERM_ID}` - And save STDOUT 'Writing to file %s' as {EXPORT_FILE} - Then the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ + When I run `wp term create category Apple --porcelain` + Then STDOUT should be a number + And save STDOUT as {TERM_ID} When I run `wp site empty --yes` - Then STDOUT should not be empty - - When I run `wp post list --post_type=post --format=count` + And I run `wp post generate --post_type=post --count=10` + And I run `wp post list --post_type=post --format=count` Then STDOUT should be: """ - 0 + 10 """ - When I run `wp import {EXPORT_FILE} --authors=skip` - Then STDOUT should not be empty - - When I run `wp post list --post_type=post` - Then STDOUT should contain: - """ - Pear Post - """ - And STDOUT should not contain: + When I run `for id in $(wp post list --posts_per_page=5 --ids); do wp post term add $id category Apple; done` + And I run `wp post list --post_type=post --cat={TERM_ID} --format=count` + Then STDOUT should be: """ - Apple Post + 5 """ - @require-wp-5.2 - Scenario: Export posts from a given author - Given a WP install - And I run `wp site empty --yes` - And I run `wp plugin install wordpress-importer --activate` - - When I run `wp user create john john.doe@example.com --porcelain` - Then STDOUT should be a number - And save STDOUT as {JOHN_USER_ID} - - When I run `wp user create jane jane.doe@example.com --porcelain` - Then STDOUT should be a number - And save STDOUT as {JANE_USER_ID} - - When I run `wp post create --post_type=post --post_title='Post by John' --post_author={JOHN_USER_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {JOHN_POST_ID} - - When I run `wp post create --post_type=post --post_title='Post by Jane' --post_author={JANE_USER_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {JANE_POST_ID} - - When I run `wp export --post_type=post --author={JANE_USER_ID}` + When I run `wp export --post_type=post --category=apple` And save STDOUT 'Writing to file %s' as {EXPORT_FILE} Then the {EXPORT_FILE} file should contain: """ - jane.doe@example.com - """ - And the {EXPORT_FILE} file should contain: - """ - Post by Jane - """ - And the {EXPORT_FILE} file should not contain: - """ - john.doe@example.com - """ - And the {EXPORT_FILE} file should not contain: - """ - Post by John + """ When I run `wp site empty --yes` Then STDOUT should not be empty - When I run `wp user delete {JOHN_USER_ID} {JANE_USER_ID} --yes` - Then STDOUT should contain: - """ - Success: Removed user {JOHN_USER_ID} from https://example.com. - """ - And STDOUT should contain: - """ - Success: Removed user {JANE_USER_ID} from https://example.com. - """ - When I run `wp post list --post_type=post --format=count` Then STDOUT should be: """ 0 """ - When I run `wp import {EXPORT_FILE} --authors=create` + When I run `wp import {EXPORT_FILE} --authors=skip` Then STDOUT should not be empty - When I run `wp post list --post_type=post` - Then STDOUT should contain: - """ - Post by Jane - """ - And STDOUT should not contain: - """ - Post by John - """ - - When I run `wp user list` - Then STDOUT should contain: - """ - jane.doe@example.com - """ - And STDOUT should not contain: + When I run `wp post list --post_type=post --format=count` + Then STDOUT should be: """ - john.doe@example.com + 5 """ - @require-wp-5.2 Scenario: Export posts should include user information Given a WP install And I run `wp plugin install wordpress-importer --activate` @@ -502,7 +325,6 @@ Feature: Export content. Test User """ - @require-wp-5.2 Scenario: Export posts from a given starting post ID Given a WP install @@ -520,12 +342,8 @@ Feature: Export content. 10 """ - # Read the 6th post's ID instead of hardcoding --start_id=6. SQLite does not reset auto-increment on `wp site empty`, so generated IDs aren't always 1..10. - When I run `wp post list --post_type=post --orderby=ID --order=ASC --posts_per_page=1 --offset=5 --format=ids` - Then save STDOUT as {START_ID} - - When I run `wp export --start_id={START_ID}` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + When I run `wp export --start_id=6` + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -545,22 +363,19 @@ Feature: Export content. 5 """ - @require-wp-5.2 Scenario: Exclude a specific post type from export Given a WP install - And I run `wp site empty --yes` - And I run `wp post create --post_title="Some page" --post_type=page` And I run `wp post generate --post_type=post --count=10` And I run `wp plugin install wordpress-importer --activate` When I run `wp post list --post_type=any --format=count` Then STDOUT should be: """ - 11 + 12 """ When I run `wp export --post_type__not_in=post` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -588,7 +403,7 @@ Feature: Export content. """ When I run `wp export --post_type__not_in=post,page` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -608,35 +423,22 @@ Feature: Export content. 0 """ - Scenario: Export posts using --max_num_posts Given a WP install - And I run `wp site empty --yes` - And a count-instances.php file: - """ - ' . $args[0] . '<\/wp:post_type>#', file_get_contents( 'php://stdin' ), $matches ); - """ When I run `wp post generate --post_type=post --count=10` - And I run `wp export --post_type=post --max_num_posts=1 --stdout | wp --skip-wordpress eval-file count-instances.php post` - Then STDOUT should contain: + And I run `wp export --post_type=post --max_num_posts=1 --stdout | grep -cF 'post'` + Then STDOUT should be: """ - count=1 + 1 """ When I run `wp post generate --post_type=post --count=10` And I run `wp post generate --post_type=attachment --count=10` - And I run `wp export --max_num_posts=1 --stdout | wp --skip-wordpress eval-file count-instances.php "(post|attachment)"` - Then STDOUT should contain: - """ - count=1 - """ - - When I run `wp export --max_num_posts=5 --stdout | wp --skip-wordpress eval-file count-instances.php "(post|attachment)"` - Then STDOUT should contain: + And I run `wp export --max_num_posts=1 --stdout | grep -cP '\(attachment|post)\'` + Then STDOUT should be: """ - count=5 + 1 """ Scenario: Export a site with a custom filename format @@ -652,21 +454,6 @@ Feature: Export content. 000.xml """ - Scenario: Export a site with a very long site name produces a filename within a reasonable length - Given a WP install - And I run `wp option update blogname 'This is a very long site name that exceeds fifty characters and should be truncated in the export filename'` - - When I run `wp export` - Then STDOUT should contain: - """ - thisisaverylongsitenamethatexceedsfiftycharactersa.wordpress. - """ - And STDOUT should not contain: - """ - thisisaverylongsitenamethatexceedsfiftycharactersandshouldbetruncated - """ - - @require-wp-5.2 Scenario: Export a site and skip the comments Given a WP install And I run `wp comment generate --post_id=1 --count=2` @@ -679,7 +466,7 @@ Feature: Export content. """ When I run `wp export --skip_comments` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} + And save STDOUT 'Writing to file %s' as {EXPORT_FILE} When I run `wp site empty --yes` Then STDOUT should not be empty @@ -711,143 +498,6 @@ Feature: Export content. 0 """ - Scenario: Export splitting the dump - Given a WP install - - When I run `wp export --max_file_size=0.0001 --filename_format='{n}.xml'` - Then STDOUT should contain: - """ - 001.xml - """ - And STDERR should be empty - - When I run `cat 000.xml` - Then STDOUT should contain: - """ - - """ - - When I run `cat 001.xml` - Then STDOUT should contain: - """ - - """ - - Scenario: Export splitting the dump with a bad --include_once value - Given a WP install - And I run `wp term generate post_tag --count=1` - - When I try `wp export --max_file_size=0.0001 --include_once=invalid --filename_format='{n}.xml'` - Then STDERR should contain: - """ - Warning: include_once should be comma-separated values for optional before_posts sections - """ - - Scenario: Export splitting the dump with a single --include_once value - Given a WP install - And I run `wp term generate post_tag --count=1` - - When I run `wp export --max_file_size=0.0001 --include_once=categories --filename_format='{n}.xml'` - Then STDOUT should contain: - """ - 001.xml - """ - And STDERR should be empty - - When I run `cat 000.xml` - Then STDOUT should contain: - """ - - """ - And STDOUT should contain: - """ - - """ - - When I run `cat 001.xml` - Then STDOUT should not contain: - """ - - """ - And STDOUT should contain: - """ - - """ - - Scenario: Export splitting the dump with multiple --include_once values - Given a WP install - And I run `wp term generate post_tag --count=1` - - When I run `wp export --max_file_size=0.0001 --include_once=categories,tags --filename_format='{n}.xml'` - Then STDOUT should contain: - """ - 001.xml - """ - And STDERR should be empty - - When I run `cat 000.xml` - Then STDOUT should contain: - """ - - """ - And STDOUT should contain: - """ - - """ - - When I run `cat 001.xml` - Then STDOUT should not contain: - """ - - """ - And STDOUT should not contain: - """ - - """ - - @require-mysql - Scenario: Export without splitting the dump - Given a WP install - # Make export file > 15MB so will split by default. Need to split into 4 * 4MB to stay below 10% of default redo log size of 48MB, otherwise get MySQL error. - And I run `wp db query "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '_dummy', REPEAT( 'A', 4 * 1024 * 1024 ) );"` - And I run `wp db query "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '_dummy', REPEAT( 'A', 4 * 1024 * 1024 ) );"` - And I run `wp db query "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '_dummy', REPEAT( 'A', 4 * 1024 * 1024 ) );"` - And I run `wp db query "INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (1, '_dummy', REPEAT( 'A', 4 * 1024 * 1024 ) );"` - - When I run `wp export` - Then STDOUT should contain: - """ - 000.xml - """ - And STDOUT should contain: - """ - 001.xml - """ - And STDERR should be empty - - When I run `wp export --max_file_size=0` - Then STDOUT should contain: - """ - 000.xml - """ - And STDOUT should contain: - """ - 001.xml - """ - And STDERR should be empty - - When I run `wp export --max_file_size=-1` - Then STDOUT should contain: - """ - 000.xml - """ - And STDOUT should not contain: - """ - 001.xml - """ - And STDERR should be empty - - @require-wp-5.2 Scenario: Export a site to stdout Given a WP install And I run `wp comment generate --post_id=1 --count=1` @@ -899,465 +549,3 @@ Feature: Export content. """ Error: --stdout and --dir cannot be used together. """ - And the return code should be 1 - - @require-wp-5.2 - Scenario: Export individual post with attachments - Given a WP install - And I run `wp plugin install wordpress-importer --activate` - And I run `wp site empty --yes` - - When I run `wp post generate --count=10` - And I run `wp post list --format=count` - Then STDOUT should be: - """ - 10 - """ - - When I run `wp post create --post_title='Post with attachment to export' --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_ATTACHMENT_POST_ID} - - When I run `wp media import 'http://wp-cli.github.io/behat-data/codeispoetry.png' --post_id={EXPORT_ATTACHMENT_POST_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_ATTACHMENT_ID} - - When I run `wp post create --post_title='Post with attachment to ignore' --porcelain` - Then STDOUT should be a number - And save STDOUT as {IGNORE_ATTACHMENT_POST_ID} - - When I run `wp media import 'http://wp-cli.github.io/behat-data/white-150-square.jpg' --post_id={IGNORE_ATTACHMENT_POST_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {IGNORE_ATTACHMENT_ID} - - When I run `wp post list --post_type=post --format=count` - Then STDOUT should be: - """ - 12 - """ - - When I run `wp post list --post_type=attachment --format=count` - Then STDOUT should be: - """ - 2 - """ - - When I run `wp export --post__in={EXPORT_ATTACHMENT_POST_ID} --with_attachments` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_ATTACHMENT_POST_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - attachment - """ - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_ATTACHMENT_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - _wp_attachment_metadata - """ - And the {EXPORT_FILE} file should contain: - """ - codeispoetry.png";s: - """ - And the {EXPORT_FILE} file should contain: - """ - _wp_attached_file - """ - And the {EXPORT_FILE} file should contain: - """ - codeispoetry.png]]> - """ - And the {EXPORT_FILE} file should not contain: - """ - _edit_lock - """ - And the {EXPORT_FILE} file should not contain: - """ - {IGNORE_ATTACHMENT_POST_ID} - """ - And the {EXPORT_FILE} file should not contain: - """ - {IGNORE_ATTACHMENT_ID} - """ - And the {EXPORT_FILE} file should not contain: - """ - white-150-square.jpg]]> - """ - And the {EXPORT_FILE} file should not contain: - """ - white-150-square.jpg";s: - """ - - @require-wp-5.2 - Scenario: Export categories, tags and terms - Given a WP install - And a wp-content/mu-plugins/register-region-taxonomy.php file: - """ - 'Region', - 'rewrite' => [ 'slug' => 'region' ], - 'hierarchical' => true, - ] ); - } - add_action( 'init', 'wp_cli_region_taxonomy' ); - """ - And I run `wp plugin install wordpress-importer --activate` - And I run `wp site empty --yes` - - When I run `wp term create category News --description="A news article" --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_CATEGORY_ID} - - When I run `wp term create category National --parent={EXPORT_CATEGORY_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_SUBCATEGORY_ID} - - When I run `wp term create post_tag Tech --description="Technology-related" --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_TAG_ID} - - When I run `wp term create region Europe --description="Concerns Europe" --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_TERM_ID} - - When I run `wp post create --post_title='Breaking News' --post_category={EXPORT_CATEGORY_ID} --tags_input={EXPORT_TAG_ID} --porcelain` - Then STDOUT should be a number - And save STDOUT as {EXPORT_POST_ID} - - When I run `wp post term add {EXPORT_POST_ID} region {EXPORT_TERM_ID}` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp export --post__in={EXPORT_POST_ID}` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_POST_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_CATEGORY_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - news - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_TAG_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - {EXPORT_TERM_ID} - """ - And the {EXPORT_FILE} file should contain: - """ - - """ - - When I run `wp site empty --yes` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp post list` - Then STDOUT should not contain: - """ - Breaking News - """ - - When I run `wp term list category` - Then STDOUT should not contain: - """ - News - """ - - When I run `wp term list post_tag` - Then STDOUT should not contain: - """ - Tech - """ - - When I run `wp term list region` - Then STDOUT should not contain: - """ - Europe - """ - - When I run `wp import {EXPORT_FILE} --authors=skip` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp post list` - Then STDOUT should contain: - """ - Breaking News - """ - - When I run `wp term list category` - Then STDOUT should contain: - """ - News - """ - And STDOUT should contain: - """ - National - """ - - When I run `wp term get category news --by=slug --field=id` - Then STDOUT should be a number - And save STDOUT as {IMPORT_CATEGORY_ID} - - When I run `wp term get category national --by=slug --field=parent` - Then STDOUT should be: - """ - {IMPORT_CATEGORY_ID} - """ - - When I run `wp term list post_tag` - Then STDOUT should contain: - """ - Tech - """ - - When I run `wp term list region` - Then STDOUT should contain: - """ - Europe - """ - - @require-wp-5.2 - Scenario: Export posts should not include oembed_cache posts user information - Given a WP install - And I run `wp plugin install wordpress-importer --activate` - And I run `wp user create user user@user.com --role=editor --display_name="Test User"` - And I run `wp user create oembed_cache_user oembed_cache@user.com --role=editor --display_name="Oembed User"` - And I run `wp post generate --post_type=post --count=10 --post_author=user` - And I run `wp post generate --post_type=oembed_cache --count=1 --post_author=oembed_cache_user` - - When I run `wp export` - And save STDOUT 'Writing to file %s' as {EXPORT_FILE} - Then the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ - When I run `wp site empty --yes` - And I run `wp user list --field=user_login | xargs -n 1 wp user delete --yes` - Then STDOUT should not be empty - - When I run `wp import {EXPORT_FILE} --authors=create` - Then STDOUT should not be empty - - When I run `wp user get user --field=display_name` - Then STDOUT should be: - """ - Test User - """ - - @require-wp-5.2 - Scenario: Allow export to proceed when orphaned terms are found - Given a WP install - And I run `wp term create category orphan --parent=1` - And I run `wp term create category parent` - And I run `wp term create category child --parent=3` - And I run `wp term create post_tag atag` - And I run `wp term create post_tag btag` - And I run `wp term create post_tag ctag` - And I run `wp db query "DELETE FROM wp_terms WHERE term_id = 1"` - - When I run `wp export --allow_orphan_terms` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - And the {EXPORT_FILE} file should contain: - """ - orphan - """ - And the {EXPORT_FILE} file should contain: - """ - atag - """ - - When I run `wp site empty --yes` - And I run `wp plugin install wordpress-importer --activate` - And I run `wp import {EXPORT_FILE} --authors=skip` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp term get post_tag atag --by=slug --field=id` - Then STDOUT should be a number - - When I run `wp term get post_tag btag --by=slug --field=id` - Then STDOUT should be a number - - When I run `wp term get post_tag ctag --by=slug --field=id` - Then STDOUT should be a number - - When I run `wp term get category parent --by=slug --field=id` - Then STDOUT should be a number - And save STDOUT as {EXPORT_CATEGORY_PARENT_ID} - - When I run `wp term get category child --by=slug --field=parent` - Then STDOUT should be: - """ - {EXPORT_CATEGORY_PARENT_ID} - """ - - When I run `wp term get category orphan --by=slug --field=parent` - Then STDOUT should be: - """ - 0 - """ - - - Scenario: Throw exception when orphaned terms are found - Given a WP install - And I run `wp term create category orphan --parent=1` - And I run `wp db query "DELETE FROM wp_terms WHERE term_id = 1"` - - When I try `wp export` - Then STDERR should contain: - """ - Error: Term is missing a parent - """ - - Scenario: Export a site and skip the authors - Given a WP install - - When I run `wp export --skip_authors` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - And the {EXPORT_FILE} file should not contain: - """ - - """ - - Scenario: Export a site and skip the terms - Given a WP install - - When I run `wp export --skip_terms` - Then save STDOUT 'Writing to file %s' as {EXPORT_FILE} - And the {EXPORT_FILE} file should not contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ - And the {EXPORT_FILE} file should not contain: - """ - - """ - - @require-wp-5.2 - Scenario: Export posts with future status - Given a WP install - And I run `wp plugin install wordpress-importer --activate` - And I run `wp site empty --yes` - - When I run `wp post create --post_title='Future Post 1' --post_status=future --post_date='2050-01-01 12:00:00' --porcelain` - Then STDOUT should be a number - And save STDOUT as {FUTURE_POST_1} - - When I run `wp post create --post_title='Future Post 2' --post_status=future --post_date='2050-01-02 12:00:00' --porcelain` - Then STDOUT should be a number - And save STDOUT as {FUTURE_POST_2} - - When I run `wp post list --post_status=future --format=count` - Then STDOUT should be: - """ - 2 - """ - - When I run `wp export --post_type=post --post_status=future` - And save STDOUT 'Writing to file %s' as {EXPORT_FILE} - Then the {EXPORT_FILE} file should contain: - """ - {FUTURE_POST_1} - """ - And the {EXPORT_FILE} file should contain: - """ - future - """ - And the {EXPORT_FILE} file should contain: - """ - 2050-01-01 12:00:00 - """ - - When I run `wp site empty --yes` - Then STDOUT should not be empty - - When I run `wp post list --post_status=future --format=count` - Then STDOUT should be: - """ - 0 - """ - - When I run `wp import {EXPORT_FILE} --authors=skip` - Then STDOUT should not be empty - - When I run `wp post list --post_status=future --format=count` - Then STDOUT should be: - """ - 2 - """ - - Scenario: Export term meta - Given a WP install - - When I run `wp term meta add 1 term_metakey term_metavalue` - Then STDOUT should contain: - """ - Success: - """ - - When I run `wp export` - And save STDOUT 'Writing to file %s' as {EXPORT_FILE} - Then the {EXPORT_FILE} file should contain: - """ - - """ - And the {EXPORT_FILE} file should contain: - """ - term_metakey - """ - And the {EXPORT_FILE} file should contain: - """ - - """ diff --git a/features/extra/no-mail.php b/features/extra/no-mail.php new file mode 100644 index 000000000..de7a42272 --- /dev/null +++ b/features/extra/no-mail.php @@ -0,0 +1,7 @@ +Given( '/^an empty directory$/', + function ( $world ) { + $world->create_run_dir(); + } +); + +$steps->Given( '/^an? (empty|non-existent) ([^\s]+) directory$/', + function ( $world, $empty_or_nonexistent, $dir ) { + $dir = $world->replace_variables( $dir ); + if ( ! WP_CLI\Utils\is_path_absolute( $dir ) ) { + $dir = $world->variables['RUN_DIR'] . "/$dir"; + } + if ( 0 !== strpos( $dir, sys_get_temp_dir() ) ) { + throw new RuntimeException( sprintf( "Attempted to delete directory '%s' that is not in the temp directory '%s'. " . __FILE__ . ':' . __LINE__, $dir, sys_get_temp_dir() ) ); + } + $world->remove_dir( $dir ); + if ( 'empty' === $empty_or_nonexistent ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + } +); + +$steps->Given( '/^an empty cache/', + function ( $world ) { + $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); + } +); + +$steps->Given( '/^an? ([^\s]+) file:$/', + function ( $world, $path, PyStringNode $content ) { + $content = (string) $content . "\n"; + $full_path = $world->variables['RUN_DIR'] . "/$path"; + $dir = dirname( $full_path ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + file_put_contents( $full_path, $content ); + } +); + +$steps->Given( '/^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/', function( $world, $search, $replace, $path ) { + $full_path = $world->variables['RUN_DIR'] . "/$path"; + $contents = file_get_contents( $full_path ); + $contents = str_replace( $search, $replace, $contents ); + file_put_contents( $full_path, $contents ); +}); + +$steps->Given( '/^WP files$/', + function ( $world ) { + $world->download_wp(); + } +); + +$steps->Given( '/^wp-config\.php$/', + function ( $world ) { + $world->create_config(); + } +); + +$steps->Given( '/^a database$/', + function ( $world ) { + $world->create_db(); + } +); + +$steps->Given( '/^a WP install$/', + function ( $world ) { + $world->install_wp(); + } +); + +$steps->Given( "/^a WP install in '([^\s]+)'$/", + function ( $world, $subdir ) { + $world->install_wp( $subdir ); + } +); + +$steps->Given( '/^a WP install with Composer$/', + function ( $world ) { + $world->install_wp_with_composer(); + } +); + +$steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/', + function ( $world, $type = 'subdirectory' ) { + $world->install_wp(); + $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0; + $world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check(); + } +); + +$steps->Given( '/^these installed and active plugins:$/', + function( $world, $stream ) { + $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) ); + $world->proc( "wp plugin install $plugins --activate" )->run_check(); + } +); + +$steps->Given( '/^a custom wp-content directory$/', + function ( $world ) { + $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; + + $wp_config_code = file_get_contents( $wp_config_path ); + + $world->move_files( 'wp-content', 'my-content' ); + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" ); + + $world->move_files( 'my-content/plugins', 'my-plugins' ); + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" ); + + file_put_contents( $wp_config_path, $wp_config_code ); + } +); + +$steps->Given( '/^download:$/', + function ( $world, TableNode $table ) { + foreach ( $table->getHash() as $row ) { + $path = $world->replace_variables( $row['path'] ); + if ( file_exists( $path ) ) { + // assume it's the same file and skip re-download + continue; + } + + Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); + } + } +); + +$steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/', + function ( $world, $stream, $output_filter, $key ) { + + $stream = strtolower( $stream ); + + if ( $output_filter ) { + $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; + if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) ) + $output = array_pop( $matches ); + else + $output = ''; + } else { + $output = $world->result->$stream; + } + $world->variables[ $key ] = trim( $output, "\n" ); + } +); + +$steps->Given( '/^a new Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { + $world->build_phar( $version ); + } +); + +$steps->Given( '/^a downloaded Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { + $world->download_phar( $version ); + } +); + +$steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/', + function ( $world, $filepath, $output_filter, $key ) { + $full_file = file_get_contents( $world->replace_variables( $filepath ) ); + + if ( $output_filter ) { + $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; + if ( false !== preg_match( $output_filter, $full_file, $matches ) ) + $output = array_pop( $matches ); + else + $output = ''; + } else { + $output = $full_file; + } + $world->variables[ $key ] = trim( $output, "\n" ); + } +); + +$steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/', + function($world) { + $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; + + $wp_config_code = file_get_contents( $wp_config_path ); + + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_CONTENT_DIR', '' );" ); + + file_put_contents( $wp_config_path, $wp_config_code ); + } +); + +$steps->Given( '/^a dependency on current wp-cli$/', + function ( $world ) { + $world->composer_require_current_wp_cli(); + } +); + +$steps->Given( '/^a PHP built-in web server$/', + function ( $world ) { + $world->start_php_server(); + } +); diff --git a/features/steps/then.php b/features/steps/then.php new file mode 100644 index 000000000..887555485 --- /dev/null +++ b/features/steps/then.php @@ -0,0 +1,218 @@ +Then( '/^the return code should be (\d+)$/', + function ( $world, $return_code ) { + if ( $return_code != $world->result->return_code ) { + throw new RuntimeException( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/', + function ( $world, $stream, $action, PyStringNode $expected ) { + + $stream = strtolower( $stream ); + + $expected = $world->replace_variables( (string) $expected ); + + checkString( $world->result->$stream, $expected, $action, $world->result ); + } +); + +$steps->Then( '/^(STDOUT|STDERR) should be a number$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + assertNumeric( trim( $world->result->$stream, "\n" ) ); + } +); + +$steps->Then( '/^(STDOUT|STDERR) should not be a number$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + assertNotNumeric( trim( $world->result->$stream, "\n" ) ); + } +); + +$steps->Then( '/^STDOUT should be a table containing rows:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); + + $expected_rows = array(); + foreach ( $expected->getRows() as $row ) { + $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); + } + + compareTables( $expected_rows, $actual_rows, $output ); + } +); + +$steps->Then( '/^STDOUT should end with a table containing rows:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); + + $expected_rows = array(); + foreach ( $expected->getRows() as $row ) { + $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); + } + + $start = array_search( $expected_rows[0], $actual_rows ); + + if ( false === $start ) + throw new \Exception( $world->result ); + + compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output ); + } +); + +$steps->Then( '/^STDOUT should be JSON containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^STDOUT should be a JSON array containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + $actualValues = json_decode( $output ); + $expectedValues = json_decode( $expected ); + + $missing = array_diff( $expectedValues, $actualValues ); + if ( !empty( $missing ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^STDOUT should be CSV containing:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + + $expected_rows = $expected->getRows(); + foreach ( $expected as &$row ) { + foreach ( $row as &$value ) { + $value = $world->replace_variables( $value ); + } + } + + if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) ) + throw new \Exception( $world->result ); + } +); + +$steps->Then( '/^STDOUT should be YAML containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^(STDOUT|STDERR) should be empty$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + if ( !empty( $world->result->$stream ) ) { + throw new \Exception( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should not be empty$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + if ( '' === rtrim( $world->result->$stream, "\n" ) ) { + throw new Exception( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|!=|<>) ([+\w.{}-]+)$/', + function ( $world, $stream, $operator, $goal_ver ) { + $goal_ver = $world->replace_variables( $goal_ver ); + $stream = strtolower( $stream ); + if ( false === version_compare( trim( $world->result->$stream, "\n" ), $goal_ver, $operator ) ) { + throw new Exception( $world->result ); + } + } +); + +$steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/', + function ( $world, $path, $type, $action, $expected = null ) { + $path = $world->replace_variables( $path ); + + // If it's a relative path, make it relative to the current test dir + if ( '/' !== $path[0] ) + $path = $world->variables['RUN_DIR'] . "/$path"; + + if ( 'file' == $type ) { + $test = 'file_exists'; + } else if ( 'directory' == $type ) { + $test = 'is_dir'; + } + + switch ( $action ) { + case 'exist': + if ( ! $test( $path ) ) { + throw new Exception( $world->result ); + } + break; + case 'not exist': + if ( $test( $path ) ) { + throw new Exception( $world->result ); + } + break; + default: + if ( ! $test( $path ) ) { + throw new Exception( "$path doesn't exist." ); + } + $action = substr( $action, 0, -1 ); + $expected = $world->replace_variables( (string) $expected ); + if ( 'file' == $type ) { + $contents = file_get_contents( $path ); + } else if ( 'directory' == $type ) { + $files = glob( rtrim( $path, '/' ) . '/*' ); + foreach( $files as &$file ) { + $file = str_replace( $path . '/', '', $file ); + } + $contents = implode( PHP_EOL, $files ); + } + checkString( $contents, $expected, $action ); + } + } +); + +$steps->Then( '/^an email should (be sent|not be sent)$/', function( $world, $expected ) { + if ( 'be sent' === $expected ) { + assertNotEquals( 0, $world->email_sends ); + } else if ( 'not be sent' === $expected ) { + assertEquals( 0, $world->email_sends ); + } else { + throw new Exception( 'Invalid expectation' ); + } +}); + +$steps->Then( '/^the HTTP status code should be (\d+)$/', + function ( $world, $return_code ) { + $response = \Requests::request( 'http://localhost:8080' ); + assertEquals( $return_code, $response->status_code ); + } +); diff --git a/features/steps/when.php b/features/steps/when.php new file mode 100644 index 000000000..afe3f7a0d --- /dev/null +++ b/features/steps/when.php @@ -0,0 +1,54 @@ + 'run_check', + 'try' => 'run' + ); + $method = $map[ $mode ]; + + return $proc->$method(); +} + +function capture_email_sends( $stdout ) { + $stdout = preg_replace( '#WP-CLI test suite: Sent email to.+\n?#', '', $stdout, -1, $email_sends ); + return array( $stdout, $email_sends ); +} + +$steps->When( '/^I launch in the background `([^`]+)`$/', + function ( $world, $cmd ) { + $world->background_proc( $cmd ); + } +); + +$steps->When( '/^I (run|try) `([^`]+)`$/', + function ( $world, $mode, $cmd ) { + $cmd = $world->replace_variables( $cmd ); + $world->result = invoke_proc( $world->proc( $cmd ), $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + +$steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/", + function ( $world, $mode, $cmd, $subdir ) { + $cmd = $world->replace_variables( $cmd ); + $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + +$steps->When( '/^I (run|try) the previous command again$/', + function ( $world, $mode ) { + if ( !isset( $world->result ) ) + throw new \Exception( 'No previous command.' ); + + $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env ); + $world->result = invoke_proc( $proc, $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + diff --git a/functions.php b/functions.php index 6560a9d70..492e97e9b 100644 --- a/functions.php +++ b/functions.php @@ -1,23 +1,16 @@ , format?: class-string, writer?: class-string, writer_args?: mixed} $args - */ -function wp_export( $args = array() ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Renaming breaks Phar compat. +function wp_export( $args = array() ) { $defaults = array( - 'filters' => array(), - 'format' => 'WP_Export_WXR_Formatter', - 'writer' => 'WP_Export_Returner', + 'filters' => array(), + 'format' => 'WP_Export_WXR_Formatter', + 'writer' => 'WP_Export_Returner', 'writer_args' => null, ); - - /** - * @var array{filters: array, format: class-string, writer: class-string, writer_args: mixed} $args - */ - $args = wp_parse_args( $args, $defaults ); + $args = wp_parse_args( $args, $defaults ); $export_query = new WP_Export_Query( $args['filters'] ); - $formatter = new $args['format']( $export_query ); - $writer = new $args['writer']( $formatter, $args['writer_args'] ); + $formatter = new $args['format']( $export_query ); + $writer = new $args['writer']( $formatter, $args['writer_args'] ); try { return $writer->export(); } catch ( WP_Export_Exception $e ) { @@ -25,14 +18,24 @@ function wp_export( $args = array() ) { // phpcs:ignore WordPress.NamingConventi } } -// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound,WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid -- Renaming breaks Phar compat. +function wp_export_new_style_args_from_old_style_args( $args ) { + if ( isset( $args['content'] ) ) { + if ( 'all' == $args['content'] ) { + unset( $args['content'] ); + } else { + $args['post_type'] = $args['content']; + } + } + return $args; +} + +// TEMPORARY function _wp_export_build_IN_condition( $column_name, $values, $format = '%s' ) { global $wpdb; - if ( ! is_array( $values ) || empty( $values ) ) { + if ( !is_array( $values ) || empty( $values ) ) { return ''; } $formats = implode( ', ', array_fill( 0, count( $values ), $format ) ); - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_name_sql escaped as ident, $formats hardcoded value. - return $wpdb->prepare( "{$column_name} IN ({$formats})", $values ); + return $wpdb->prepare( "$column_name IN ($formats)", $values ); } diff --git a/phpcs.xml.dist b/phpcs.xml.dist deleted file mode 100644 index f30b8623d..000000000 --- a/phpcs.xml.dist +++ /dev/null @@ -1,71 +0,0 @@ - - - Custom ruleset for WP-CLI export-command - - - - - . - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */src/Export_Command\.php$ - */src/WP_(Map|Post_IDs)_Iterator\.php$ - */src/WP_(Export_Term|Iterator)_Exception\.php$ - */src/WP_Export_WXR_Formatter\.php$ - */src/WP_Export_XML_Over_HTTP\.php$ - */src/WP_Export_(Base|File|Split_Files|Stdout)?_Writer\.php$ - */src/WP_Export_(Exception|Oxymel|Query|Returner)\.php$ - - - - - - */src/Export_Command\.php$ - - - diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 251831731..000000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,16 +0,0 @@ -parameters: - level: 9 - paths: - - src - - export-command.php - - functions.php - scanDirectories: - - vendor/wp-cli/wp-cli/php - scanFiles: - - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - treatPhpDocTypesAsCertain: false - ignoreErrors: - - identifier: missingType.iterableValue - - identifier: missingType.property - - identifier: missingType.parameter - - identifier: missingType.return diff --git a/src/Export_Command.php b/src/Export_Command.php index dc46a2aa2..79e7c21c6 100644 --- a/src/Export_Command.php +++ b/src/Export_Command.php @@ -1,12 +1,7 @@ ] - * : A single export file should have this many megabytes. -1 for unlimited. + * : A single export file should have this many megabytes. * --- * default: 15 * --- * - * [--filename_format=] - * : Use a custom format for export filenames. Defaults to '{site}.wordpress.{date}.{n}.xml'. - * - * [--include_once=] - * : Include specified export section only in the first export file. Valid options - * are categories, tags, nav_menu_items, custom_taxonomies_terms. Separate multiple - * sections with a comma. Defaults to none. - * - * [--allow_orphan_terms] - * : Export orphaned terms with `parent=0`, instead of throwing an exception. - * * ## FILTERS * * [--start_date=] @@ -100,11 +67,7 @@ class Export_Command extends WP_CLI_Command { * with a comma. Defaults to none. * * [--post__in=] - * : Export all posts specified as a comma-separated or space-separated list of IDs. - * Post's attachments won't be exported unless --with_attachments is specified. - * - * [--with_attachments] - * : Force including attachments in case --post__in has been specified. + * : Export all posts specified as a comma- or space-separated list of IDs. * * [--start_id=] * : Export only posts with IDs greater than or equal to this post ID. @@ -115,12 +78,15 @@ class Export_Command extends WP_CLI_Command { * [--author=] * : Export only posts by this author. Can be either user login or user ID. * - * [--category=] + * [--category=] * : Export only posts in this category. * * [--post_status=] * : Export only posts with this status. * + * [--filename_format=] + * : Use a custom format for export filenames. Defaults to '{site}.wordpress.{date}.{n}.xml'. + * * ## EXAMPLES * * # Export posts published by the user between given start and end date @@ -142,69 +108,34 @@ class Export_Command extends WP_CLI_Command { * Success: All done with export. */ public function __invoke( $_, $assoc_args ) { - $defaults = [ - 'dir' => null, - 'stdout' => false, - 'start_date' => null, - 'end_date' => null, - 'post_type' => null, - 'post_type__not_in' => null, - 'max_num_posts' => null, - 'author' => null, - 'category' => null, - 'post_status' => null, - 'post__in' => null, - 'with_attachments' => true, // or FALSE if user requested some post__in - 'start_id' => null, - 'skip_comments' => null, - 'skip_authors' => null, - 'skip_terms' => null, - 'max_file_size' => 15, - 'filename_format' => '{site}.wordpress.{date}.{n}.xml', - 'include_once' => null, - 'allow_orphan_terms' => null, - ]; - - if ( ! empty( $assoc_args['stdout'] ) && ( ! empty( $assoc_args['dir'] ) || ! empty( $assoc_args['filename_format'] ) ) ) { - WP_CLI::error( '--stdout and --dir cannot be used together.' ); - } + $defaults = array( + 'dir' => NULL, + 'stdout' => FALSE, + 'start_date' => NULL, + 'end_date' => NULL, + 'post_type' => NULL, + 'post_type__not_in' => NULL, + 'max_num_posts' => NULL, + 'author' => NULL, + 'category' => NULL, + 'post_status' => NULL, + 'post__in' => NULL, + 'start_id' => NULL, + 'skip_comments' => NULL, + 'max_file_size' => 15, + 'filename_format' => '{site}.wordpress.{date}.{n}.xml', + ); + - if ( ! empty( $assoc_args['post__in'] ) && empty( $assoc_args['with_attachments'] ) ) { - $defaults['with_attachments'] = false; + if (! empty( $assoc_args['stdout'] ) && ( ! empty( $assoc_args['dir'] ) || ! empty( $assoc_args['filename_format'] ) ) ) { + WP_CLI::error( '--stdout and --dir cannot be used together.' ); } $assoc_args = wp_parse_args( $assoc_args, $defaults ); $this->validate_args( $assoc_args ); - $this->export_args['with_attachments'] = Utils\get_flag_value( - $assoc_args, - 'with_attachments', - $defaults['with_attachments'] - ); - - $this->export_args['skip_authors'] = Utils\get_flag_value( - $assoc_args, - 'skip_authors', - $defaults['skip_authors'] - ); - - $this->export_args['skip_terms'] = Utils\get_flag_value( - $assoc_args, - 'skip_terms', - $defaults['skip_terms'] - ); - - // Re-calculate exclusions after validation to ensure consistency. - $this->exclude = []; - if ( $this->export_args['skip_authors'] ) { - $this->exclude[] = 'authors'; - } - if ( $this->export_args['skip_terms'] ) { - $this->exclude = array_merge( $this->exclude, array( 'categories', 'tags', 'nav_menu_terms', 'custom_taxonomies_terms' ) ); - } - - if ( ! function_exists( 'wp_export' ) ) { + if ( !function_exists( 'wp_export' ) ) { self::load_export_api(); } @@ -212,37 +143,28 @@ public function __invoke( $_, $assoc_args ) { WP_CLI::log( 'Starting export process...' ); } - add_action( - 'wp_export_new_file', - static function ( $file_path ) { - WP_CLI::log( sprintf( 'Writing to file %s', $file_path ) ); - Utils\wp_clear_object_cache(); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.wp_clear_object_cacheDeprecatedRemoved @phpstan-ignore-line - } - ); + add_action( 'wp_export_new_file', function( $file_path ) { + WP_CLI::log( sprintf( "Writing to file %s", $file_path ) ); + WP_CLI\Utils\wp_clear_object_cache(); + } ); try { if ( $this->stdout ) { - wp_export( - [ - 'filters' => $this->export_args, - 'writer' => WP_Export_File_Writer::class, - 'writer_args' => 'php://output', - ] - ); + wp_export( array( + 'filters' => $this->export_args, + 'writer' => 'WP_Export_Stdout_Writer', + 'writer_args' => NULL + ) ); } else { - wp_export( - [ - 'filters' => $this->export_args, - 'writer' => WP_Export_Split_Files_Writer::class, - 'writer_args' => [ - 'max_file_size' => $this->max_file_size, - 'destination_directory' => $this->wxr_path, - 'filename_template' => self::get_filename_template( $assoc_args['filename_format'] ), - 'include_once' => $this->include_once, - 'exclude' => $this->exclude, - ], - ] - ); + wp_export( array( + 'filters' => $this->export_args, + 'writer' => 'WP_Export_Split_Files_Writer', + 'writer_args' => array( + 'max_file_size' => $this->max_file_size * MB_IN_BYTES, + 'destination_directory' => $this->wxr_path, + 'filename_template' => self::get_filename_template( $assoc_args['filename_format'] ), + ) + ) ); } } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); @@ -258,46 +180,53 @@ private static function get_filename_template( $filename_format ) { if ( empty( $sitename ) ) { $sitename = 'site'; } - $sitename = function_exists( 'mb_substr' ) ? mb_substr( $sitename, 0, self::MAX_FILENAME_SITENAME_LENGTH ) : substr( $sitename, 0, self::MAX_FILENAME_SITENAME_LENGTH ); - return str_replace( [ '{site}', '{date}', '{n}' ], [ $sitename, date( 'Y-m-d' ), '%03d' ], $filename_format ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + return str_replace( array( '{site}', '{date}', '{n}' ), array( $sitename, date( 'Y-m-d' ), '%03d' ), $filename_format ); } - public static function load_export_api() { - require dirname( __DIR__ ) . '/functions.php'; + private static function load_export_api() { + if ( !defined( 'KB_IN_BYTES' ) ) { + // Constants for expressing human-readable data sizes + // in their respective number of bytes. + define( 'KB_IN_BYTES', 1024 ); + define( 'MB_IN_BYTES', 1024 * KB_IN_BYTES ); + define( 'GB_IN_BYTES', 1024 * MB_IN_BYTES ); + define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES ); + define( 'PB_IN_BYTES', 1024 * TB_IN_BYTES ); + define( 'EB_IN_BYTES', 1024 * PB_IN_BYTES ); + define( 'ZB_IN_BYTES', 1024 * EB_IN_BYTES ); + define( 'YB_IN_BYTES', 1024 * ZB_IN_BYTES ); + } + + require dirname( dirname( __FILE__ ) ) . '/functions.php'; } private function validate_args( $args ) { $has_errors = false; foreach ( $args as $key => $value ) { - if ( is_callable( [ $this, 'check_' . $key ] ) ) { - $result = call_user_func( [ $this, 'check_' . $key ], $value ); - if ( false === $result ) { + if ( is_callable( array( $this, 'check_' . $key ) ) ) { + $result = call_user_func( array( $this, 'check_' . $key ), $value ); + if ( false === $result ) $has_errors = true; - } } } if ( $args['stdout'] ) { - $this->wxr_path = null; - $this->stdout = true; + $this->wxr_path = NULL; + $this->stdout = TRUE; } if ( $has_errors ) { - WP_CLI::halt( 1 ); + WP_CLI::halt(1); } } - /** - * @param string $path - */ private function check_dir( $path ) { if ( empty( $path ) ) { - $path = (string) getcwd(); - } elseif ( ! is_dir( $path ) ) { + $path = getcwd(); + } elseif ( !is_dir( $path ) ) { WP_CLI::error( sprintf( "The directory '%s' does not exist.", $path ) ); - } elseif ( ! is_writable( $path ) ) { - WP_CLI::error( sprintf( "The directory '%s' is not writable.", $path ) ); + return false; } $this->wxr_path = trailingslashit( $path ); @@ -305,60 +234,46 @@ private function check_dir( $path ) { return true; } - /** - * @param string $date - */ private function check_start_date( $date ) { - if ( null === $date ) { + if ( is_null( $date ) ) return true; - } $time = strtotime( $date ); - if ( ! empty( $date ) && false === $time ) { - WP_CLI::warning( sprintf( 'The start_date %s is invalid.', $date ) ); + if ( !empty( $date ) && !$time ) { + WP_CLI::warning( sprintf( "The start_date %s is invalid.", $date ) ); return false; } - $this->export_args['start_date'] = date( 'Y-m-d', (int) $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $this->export_args['start_date'] = date( 'Y-m-d', $time ); return true; } - /** - * @param string $date - */ private function check_end_date( $date ) { - if ( null === $date ) { + if ( is_null( $date ) ) return true; - } $time = strtotime( $date ); - if ( ! empty( $date ) && false === $time ) { - WP_CLI::warning( sprintf( 'The end_date %s is invalid.', $date ) ); + if ( !empty( $date ) && !$time ) { + WP_CLI::warning( sprintf( "The end_date %s is invalid.", $date ) ); return false; } - $this->export_args['end_date'] = date( 'Y-m-d', (int) $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $this->export_args['end_date'] = date( 'Y-m-d', $time ); return true; } - /** - * @param string $post_type - */ private function check_post_type( $post_type ) { - if ( null === $post_type || 'any' === $post_type ) { + if ( is_null( $post_type ) || 'any' === $post_type ) return true; - } - $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); + $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); $post_types = get_post_types(); foreach ( $post_type as $type ) { - if ( ! in_array( $type, $post_types, true ) ) { - WP_CLI::warning( - sprintf( - 'The post type %s does not exist. Choose "any" or any of these existing post types instead: %s', - $type, - implode( ', ', $post_types ) - ) - ); + if ( ! in_array( $type, $post_types ) ) { + WP_CLI::warning( sprintf( + 'The post type %s does not exist. Choose "any" or any of these existing post types instead: %s', + $type, + implode( ", ", $post_types ) + ) ); return false; } } @@ -366,26 +281,22 @@ private function check_post_type( $post_type ) { return true; } - /** - * @param string $post_type - */ private function check_post_type__not_in( $post_type ) { - if ( null === $post_type ) { + if ( is_null( $post_type ) ) { return true; } - $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); + $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); $post_types = get_post_types(); + $keep_post_types = array(); foreach ( $post_type as $type ) { - if ( ! in_array( $type, $post_types, true ) ) { - WP_CLI::warning( - sprintf( - 'The post type %s does not exist. Use any of these existing post types instead: %s', - $type, - implode( ', ', $post_types ) - ) - ); + if ( ! in_array( $type, $post_types ) ) { + WP_CLI::warning( sprintf( + 'The post type %s does not exist. Use any of these existing post types instead: %s', + $type, + implode( ", ", $post_types ) + ) ); return false; } } @@ -393,18 +304,14 @@ private function check_post_type__not_in( $post_type ) { return true; } - /** - * @param string $post__in - */ private function check_post__in( $post__in ) { - if ( null === $post__in ) { + if ( is_null( $post__in ) ) return true; - } $separator = false !== stripos( $post__in, ' ' ) ? ' ' : ','; - $post__in = array_filter( array_unique( array_map( 'intval', explode( $separator, $post__in ) ) ) ); + $post__in = array_unique( array_map( 'intval', explode( $separator, $post__in ) ) ); if ( empty( $post__in ) ) { - WP_CLI::warning( 'post__in should be comma-separated or space-separated post IDs.' ); + WP_CLI::warning( "post__in should be comma-separated post IDs." ); return false; } // New exporter uses a different argument. @@ -412,19 +319,16 @@ private function check_post__in( $post__in ) { return true; } - /** - * @param string $start_id - */ private function check_start_id( $start_id ) { - if ( null === $start_id ) { + if ( is_null( $start_id ) ) { return true; } - $start_id = (int) $start_id; + $start_id = intval( $start_id ); // Post IDs must be greater than 0. if ( 0 >= $start_id ) { - WP_CLI::warning( "Invalid start ID: {$start_id}" ); + WP_CLI::warning( sprintf( __( 'Invalid start ID: %d' ), $start_id ) ); return false; } @@ -432,35 +336,27 @@ private function check_start_id( $start_id ) { return true; } - /** - * @param string $author - */ private function check_author( $author ) { - if ( null === $author ) { + if ( is_null( $author ) ) return true; - } - // phpcs:ignore WordPress.WP.DeprecatedFunctions.get_users_of_blogFound -- Fallback. - $authors = function_exists( 'get_users' ) ? get_users() : get_users_of_blog(); // @phpstan-ignore-line - if ( empty( $authors ) ) { - WP_CLI::warning( 'Could not find any authors in this blog.' ); + $authors = get_users_of_blog(); + if ( empty( $authors ) || is_wp_error( $authors ) ) { + WP_CLI::warning( sprintf( "Could not find any authors in this blog." ) ); return false; } $hit = false; - foreach ( $authors as $user ) { - if ( $hit ) { + foreach( $authors as $user ) { + if ( $hit ) break; - } - if ( (int) $author === $user->ID || $author === $user->user_login ) { + if ( (int) $author == $user->ID || $author == $user->user_login ) $hit = $user->ID; - } } if ( false === $hit ) { - $authors_nice = []; - foreach ( $authors as $_author ) { + $authors_nice = array(); + foreach( $authors as $_author ) $authors_nice[] = sprintf( '%s (%s)', $_author->user_login, $_author->display_name ); - } - WP_CLI::warning( sprintf( 'Could not find a matching author for %s. The following authors exist: %s', $author, implode( ', ', $authors_nice ) ) ); + WP_CLI::warning( sprintf( 'Could not find a matching author for %s. The following authors exist: %s', $author, implode( ", ", $authors_nice ) ) ); return false; } @@ -468,34 +364,23 @@ private function check_author( $author ) { return true; } - /** - * @param int|string|null $num - */ private function check_max_num_posts( $num ) { - if ( null !== $num && ( ! is_numeric( $num ) || $num <= 0 ) ) { - WP_CLI::warning( 'max_num_posts should be a positive integer.' ); + if ( ! is_null( $num ) && ( ! is_numeric( $num ) || $num <= 0) ) { + WP_CLI::warning( sprintf( "The --max_num_posts argument should be a positive integer.", $num ) ); return false; } - $this->export_args['max_num_posts'] = (int) $num; + $this->export_args['max_num_posts'] = (int)$num; return true; } - /** - * @param string $category - */ private function check_category( $category ) { - if ( null === $category ) { + if ( is_null( $category ) ) return true; - } - - if ( is_numeric( $category ) ) { - $category = (int) $category; - } $term = category_exists( $category ); - if ( empty( $term ) ) { + if ( empty( $term ) || is_wp_error( $term ) ) { WP_CLI::warning( sprintf( 'Could not find a category matching %s.', $category ) ); return false; } @@ -503,38 +388,29 @@ private function check_category( $category ) { return true; } - /** - * @param string $status - */ private function check_post_status( $status ) { - if ( null === $status ) { + if ( is_null( $status ) ) return true; - } - // spellchecker:ignore-next-line - $statuses = get_post_stati(); - if ( empty( $statuses ) || is_wp_error( $statuses ) ) { - WP_CLI::warning( 'Could not find any post statuses.' ); + $stati = get_post_statuses(); + if ( empty( $stati ) || is_wp_error( $stati ) ) { + WP_CLI::warning( 'Could not find any post stati.' ); return false; } - if ( ! isset( $statuses[ $status ] ) ) { - WP_CLI::warning( sprintf( 'Could not find a post_status matching %s. Here is a list of available statuses: %s', $status, implode( ', ', array_keys( $statuses ) ) ) ); + if ( !isset( $stati[$status] ) ) { + WP_CLI::warning( sprintf( 'Could not find a post_status matching %s. Here is a list of available stati: %s', $status, implode( ", ", array_keys( $stati ) ) ) ); return false; } $this->export_args['status'] = $status; return true; } - /** - * @param string|null $skip - */ private function check_skip_comments( $skip ) { - if ( null === $skip ) { + if ( is_null( $skip ) ) return true; - } - if ( 0 !== (int) $skip && 1 !== (int) $skip ) { + if ( (int) $skip <> 0 && (int) $skip <> 1 ) { WP_CLI::warning( 'skip_comments needs to be 0 (no) or 1 (yes).' ); return false; } @@ -542,44 +418,9 @@ private function check_skip_comments( $skip ) { return true; } - /** - * @param string|null $skip - */ - private function check_skip_authors( $skip ) { - if ( null === $skip ) { - return true; - } - - if ( 0 !== (int) $skip && 1 !== (int) $skip ) { - WP_CLI::warning( 'skip_authors needs to be 0 (no) or 1 (yes).' ); - return false; - } - $this->export_args['skip_authors'] = $skip; - return true; - } - - /** - * @param string|null $skip - */ - private function check_skip_terms( $skip ) { - if ( null === $skip ) { - return true; - } - - if ( 0 !== (int) $skip && 1 !== (int) $skip ) { - WP_CLI::warning( 'skip_terms needs to be 0 (no) or 1 (yes).' ); - return false; - } - $this->export_args['skip_terms'] = $skip; - return true; - } - - /** - * @param string $size - */ private function check_max_file_size( $size ) { - if ( ! is_numeric( $size ) ) { - WP_CLI::warning( 'max_file_size should be numeric.' ); + if ( !is_numeric( $size ) ) { + WP_CLI::warning( sprintf( "max_file_size should be numeric.", $size ) ); return false; } @@ -587,41 +428,4 @@ private function check_max_file_size( $size ) { return true; } - - /** - * @param string $once - */ - private function check_include_once( $once ) { - if ( null === $once ) { - return true; - } - - $separator = false !== stripos( $once, ' ' ) ? ' ' : ','; - $once = array_filter( array_unique( array_map( 'strtolower', explode( $separator, $once ) ) ) ); - $once = array_intersect( $once, array( 'categories', 'tags', 'nav_menu_terms', 'custom_taxonomies_terms' ) ); - if ( empty( $once ) ) { - WP_CLI::warning( 'include_once should be comma-separated values for optional before_posts sections.' ); - return false; - } - - $this->include_once = $once; - - return true; - } - - /** - * @param string $allow_orphan_terms - */ - private function check_allow_orphan_terms( $allow_orphan_terms ) { - if ( null === $allow_orphan_terms ) { - return true; - } - - if ( 0 !== (int) $allow_orphan_terms && 1 !== (int) $allow_orphan_terms ) { - WP_CLI::warning( 'allow_orphan_terms needs to be 0 (no) or 1 (yes).' ); - return false; - } - $this->export_args['allow_orphan_terms'] = $allow_orphan_terms; - return true; - } } diff --git a/src/WP_Export_Base_Writer.php b/src/WP_Export_Base_Writer.php index 28e6583d0..21f7fd35e 100644 --- a/src/WP_Export_Base_Writer.php +++ b/src/WP_Export_Base_Writer.php @@ -3,13 +3,13 @@ abstract class WP_Export_Base_Writer { protected $formatter; - public function __construct( $formatter ) { + function __construct( $formatter ) { $this->formatter = $formatter; } public function export() { $this->write( $this->formatter->before_posts() ); - foreach ( $this->formatter->posts() as $post_in_wxr ) { + foreach( $this->formatter->posts() as $post_in_wxr ) { $this->write( $post_in_wxr ); } $this->write( $this->formatter->after_posts() ); diff --git a/src/WP_Export_File_Writer.php b/src/WP_Export_File_Writer.php index c6dd42473..82be6fae5 100644 --- a/src/WP_Export_File_Writer.php +++ b/src/WP_Export_File_Writer.php @@ -11,17 +11,15 @@ public function __construct( $formatter, $file_name ) { public function export() { $this->f = fopen( $this->file_name, 'w' ); - if ( ! $this->f ) { - throw new WP_Export_Exception( "WP Export: error opening {$this->file_name} for writing." ); + if ( !$this->f ) { + throw new WP_Export_Exception( sprintf( __( 'WP Export: error opening %s for writing.' ), $this->file_name ) ); } - try { + try { parent::export(); } catch ( WP_Export_Exception $e ) { - fclose( $this->f ); throw $e; } catch ( WP_Export_Term_Exception $e ) { - fclose( $this->f ); throw $e; } @@ -29,9 +27,9 @@ public function export() { } protected function write( $xml ) { - $res = fwrite( $this->f, $xml ); + $res = fwrite( $this->f, $xml); if ( false === $res ) { - throw new WP_Export_Exception( 'WP Export: error writing to export file.' ); + throw new WP_Export_Exception( __( 'WP Export: error writing to export file.' ) ); } } } diff --git a/src/WP_Export_Oxymel.php b/src/WP_Export_Oxymel.php index d985cc415..74c1a7cf1 100644 --- a/src/WP_Export_Oxymel.php +++ b/src/WP_Export_Oxymel.php @@ -16,16 +16,10 @@ public function optional_cdata( $tag_name, $contents ) { } public function cdata( $text ) { - if ( is_string( $text ) ) { - if ( function_exists( 'wp_is_valid_utf8' ) ) { - if ( ! wp_is_valid_utf8( $text ) ) { - $text = mb_convert_encoding( $text, 'UTF-8' ); - } - // @phpstan-ignore function.deprecated - } elseif ( ! seems_utf8( $text ) ) { // phpcs:ignore WordPress.WP.DeprecatedFunctions.seems_utf8Found - $text = mb_convert_encoding( $text, 'UTF-8' ); - } + if ( !seems_utf8( $text ) ) { + $text = utf8_encode( $text ); } return parent::cdata( $text ); } } + diff --git a/src/WP_Export_Query.php b/src/WP_Export_Query.php index 0d9c79258..aac007183 100644 --- a/src/WP_Export_Query.php +++ b/src/WP_Export_Query.php @@ -1,6 +1,4 @@ null, - 'post_type' => null, - 'status' => null, - 'author' => null, - 'start_date' => null, - 'end_date' => null, - 'start_id' => null, - 'max_num_posts' => null, - 'category' => null, - 'allow_orphan_terms' => null, - ]; + private static $defaults = array( + 'post_ids' => null, + 'post_type' => null, + 'status' => null, + 'author' => null, + 'start_date' => null, + 'end_date' => null, + 'start_id' => null, + 'max_num_posts' => NULL, + 'category' => null, + ); private $post_ids; private $filters; + private $xml_gen; - private $where_clauses = []; - - private $joins = []; + private $wheres = array(); + private $joins = array(); private $author; private $category; public $missing_parents = false; - public function __construct( $filters = [] ) { + public function __construct( $filters = array() ) { $this->filters = wp_parse_args( $filters, self::$defaults ); - - $user = $this->find_user_from_any_object( $this->filters['author'] ); - if ( $user && ! is_wp_error( $user ) ) { - $this->author = $user; - } - $this->post_ids = $this->calculate_post_ids(); } @@ -54,35 +45,27 @@ public function charset() { } public function site_metadata() { - $metadata = [ - 'name' => $this->bloginfo_rss( 'name' ), - 'url' => $this->bloginfo_rss( 'url' ), - 'language' => $this->bloginfo_rss( 'language' ), + $metadata = array( + 'name' => $this->bloginfo_rss( 'name' ), + 'url' => $this->bloginfo_rss( 'url' ), + 'language' => $this->bloginfo_rss( 'language' ), 'description' => $this->bloginfo_rss( 'description' ), - 'pubDate' => date( 'D, d M Y H:i:s +0000' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - 'site_url' => is_multisite() ? network_home_url() : $this->bloginfo_rss( 'url' ), - 'blog_url' => $this->bloginfo_rss( 'url' ), - ]; + 'pubDate' => date( 'D, d M Y H:i:s +0000' ), + 'site_url' => is_multisite()? network_home_url() : $this->bloginfo_rss( 'url' ), + 'blog_url' => $this->bloginfo_rss( 'url' ), + ); return $metadata; } public function wp_generator_tag() { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return apply_filters( 'the_generator', get_the_generator( 'export' ), 'export' ); } public function authors() { global $wpdb; - - // If we're filtering by a specific author, we only need to include that - // author's user object, and no other users. - if ( is_object( $this->author ) && property_exists( $this->author, 'ID' ) ) { - return [ $this->author ]; - } - - $authors = []; - $author_ids = (array) $wpdb->get_col( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft' AND post_type != 'oembed_cache'" ); - foreach ( $author_ids as $author_id ) { + $authors = array(); + $author_ids = $wpdb->get_col( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft'" ); + foreach ( (array) $author_ids as $author_id ) { $authors[] = get_userdata( $author_id ); } $authors = array_filter( $authors ); @@ -91,14 +74,14 @@ public function authors() { public function categories() { if ( $this->category ) { - return [ $this->category ]; + return array( $this->category ); } if ( $this->filters['post_type'] ) { - return []; + return array(); } - $categories = (array) get_categories( [ 'get' => 'all' ] ); + $categories = (array) get_categories( array( 'get' => 'all' ) ); - $categories = $this->process_orphaned_terms( $categories ); + $this->check_for_orphaned_terms( $categories ); $categories = self::topologically_sort_terms( $categories ); @@ -107,75 +90,57 @@ public function categories() { public function tags() { if ( $this->filters['post_type'] ) { - return []; + return array(); } - $tags = (array) get_tags( [ 'get' => 'all' ] ); + $tags = (array) get_tags( array( 'get' => 'all' ) ); - $tags = $this->process_orphaned_terms( $tags ); + $this->check_for_orphaned_terms( $tags ); return $tags; } public function custom_taxonomies_terms() { if ( $this->filters['post_type'] ) { - return []; + return array(); } - $custom_taxonomies = get_taxonomies( [ '_builtin' => false ] ); - $custom_terms = (array) get_terms( - [ - 'taxonomy' => $custom_taxonomies, - 'get' => 'all', - ] - ); - $custom_terms = $this->process_orphaned_terms( $custom_terms ); - $custom_terms = self::topologically_sort_terms( $custom_terms ); + $custom_taxonomies = get_taxonomies( array( '_builtin' => false ) ); + $custom_terms = (array) get_terms( $custom_taxonomies, array( 'get' => 'all' ) ); + $this->check_for_orphaned_terms( $custom_terms ); + $custom_terms = self::topologically_sort_terms( $custom_terms ); return $custom_terms; } public function nav_menu_terms() { $nav_menus = wp_get_nav_menus(); - foreach ( $nav_menus as $term ) { + foreach( $nav_menus as &$term ) { $term->description = ''; } return $nav_menus; } public function exportify_post( $post ) { - /** - * @var \WP_Query $wp_query - */ - global $wp_query; - - $wp_query->in_the_loop = true; - $previous_global_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null; - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Temporary override. + $GLOBALS['wp_query']->in_the_loop = true; + $previous_global_post = \WP_CLI\Utils\get_flag_value( $GLOBALS, 'post' ); $GLOBALS['post'] = $post; setup_postdata( $post ); - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $post->post_content = apply_filters( 'the_content_export', $post->post_content ); - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $post->post_excerpt = apply_filters( 'the_excerpt_export', $post->post_excerpt ); - $post->is_sticky = is_sticky( $post->ID ) ? 1 : 0; - $post->terms = self::get_terms_for_post( $post ); - $post->meta = self::get_meta_for_post( $post ); - $post->comments = $this->get_comments_for_post( $post ); - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Revert back to original. + $post->is_sticky = is_sticky( $post->ID ) ? 1 : 0; + $post->terms = self::get_terms_for_post( $post ); + $post->meta = self::get_meta_for_post( $post ); + $post->comments = $this->get_comments_for_post( $post ); $GLOBALS['post'] = $previous_global_post; return $post; } public function posts() { $posts_iterator = new WP_Post_IDs_Iterator( $this->post_ids, self::QUERY_CHUNK ); - return new WP_Map_Iterator( $posts_iterator, [ $this, 'exportify_post' ] ); + return new WP_Map_Iterator( $posts_iterator, array( $this, 'exportify_post' ) ); } private function calculate_post_ids() { global $wpdb; if ( is_array( $this->filters['post_ids'] ) ) { - if ( $this->filters['with_attachments'] ) { - $attachment_post_ids = $this->include_attachment_ids( $this->filters['post_ids'] ); - $this->filters['post_ids'] = array_merge( $this->filters['post_ids'], $attachment_post_ids ); - } return $this->filters['post_ids']; } $this->post_type_where(); @@ -186,22 +151,18 @@ private function calculate_post_ids() { $this->start_id_where(); $this->category_where(); - $where = implode( ' AND ', array_filter( $this->where_clauses ) ); - if ( $where ) { - $where = "WHERE $where"; - } + $where = implode( ' AND ', array_filter( $this->wheres ) ); + if ( $where ) $where = "WHERE $where"; $join = implode( ' ', array_filter( $this->joins ) ); - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Individual where clauses run through $wpdb->prepare(). - $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} AS p {$join} {$where} {$this->max_num_posts()}" ); - if ( $this->filters['post_type'] ) { - $post_ids = array_merge( $post_ids, $this->include_attachment_ids( $post_ids ) ); - } + $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} AS p $join $where {$this->max_num_posts()}" ); + $post_ids = array_merge( $post_ids, $this->attachments_for_specific_post_types( $post_ids ) ); return $post_ids; } private function post_type_where() { - $post_types_filters = [ 'can_export' => true ]; + global $wpdb; + $post_types_filters = array( 'can_export' => true ); if ( $this->filters['post_type'] ) { $post_types = $this->filters['post_type']; @@ -209,16 +170,13 @@ private function post_type_where() { if ( is_array( $post_types ) && 1 === count( $post_types ) ) { $post_types = array_shift( $post_types ); } - $post_types_filters = array_merge( $post_types_filters, [ 'name' => $post_types ] ); + $post_types_filters = array_merge( $post_types_filters, array( 'name' => $post_types ) ); } // Multiple post types if ( isset( $post_types_filters['name'] ) && is_array( $post_types_filters['name'] ) ) { - $post_types = []; + $post_types = array(); foreach ( $post_types_filters['name'] as $post_type ) { - /** - * @var string $post_type - */ if ( post_type_exists( $post_type ) ) { $post_types[] = $post_type; } @@ -228,56 +186,51 @@ private function post_type_where() { } if ( ! $post_types ) { - $this->where_clauses[] = 'p.post_type IS NULL'; + $this->wheres[] = 'p.post_type IS NULL'; return; } - - if ( false === $this->filters['with_attachments'] && ( ! $this->filters['post_type'] || ! in_array( 'attachment', $this->filters['post_type'], true ) ) ) { - unset( $post_types['attachment'] ); - } - - $this->where_clauses[] = _wp_export_build_IN_condition( 'p.post_type', $post_types ); + $this->wheres[] = _wp_export_build_IN_condition( 'p.post_type', $post_types ); } private function status_where() { global $wpdb; - if ( ! $this->filters['status'] ) { - $this->where_clauses[] = "p.post_status != 'auto-draft'"; + if ( !$this->filters['status'] ) { + $this->wheres[] = "p.post_status != 'auto-draft'"; return; } - $this->where_clauses[] = $wpdb->prepare( 'p.post_status = %s', $this->filters['status'] ); + $this->wheres[] = $wpdb->prepare( 'p.post_status = %s', $this->filters['status'] ); } private function author_where() { global $wpdb; - if ( is_object( $this->author ) && property_exists( $this->author, 'ID' ) ) { - $this->where_clauses[] = $wpdb->prepare( 'p.post_author = %d', $this->author->ID ); + $user = $this->find_user_from_any_object( $this->filters['author'] ); + if ( !$user || is_wp_error( $user ) ) { + return; } + $this->author = $user; + $this->wheres[] = $wpdb->prepare( 'p.post_author = %d', $user->ID ); } private function start_date_where() { global $wpdb; - $timestamp = $this->filters['start_date'] ? strtotime( $this->filters['start_date'] ) : null; - if ( ! $timestamp ) { + $timestamp = strtotime( $this->filters['start_date'] ); + if ( !$timestamp ) { return; } - $this->where_clauses[] = $wpdb->prepare( 'p.post_date >= %s', date( 'Y-m-d 00:00:00', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $this->wheres[] = $wpdb->prepare( 'p.post_date >= %s', date( 'Y-m-d 00:00:00', $timestamp ) ); } private function end_date_where() { global $wpdb; - if ( ! $this->filters['end_date'] ) { - return; - } if ( preg_match( '/^\d{4}-\d{2}$/', $this->filters['end_date'] ) ) { $timestamp = $this->get_timestamp_for_the_last_day_of_a_month( $this->filters['end_date'] ); } else { $timestamp = strtotime( $this->filters['end_date'] ); } - if ( ! $timestamp ) { + if ( !$timestamp ) { return; } - $this->where_clauses[] = $wpdb->prepare( 'p.post_date <= %s', date( 'Y-m-d 23:59:59', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + $this->wheres[] = $wpdb->prepare( 'p.post_date <= %s', date( 'Y-m-d 23:59:59', $timestamp ) ); } private function start_id_where() { @@ -287,7 +240,7 @@ private function start_id_where() { if ( 0 === $start_id ) { return; } - $this->where_clauses[] = $wpdb->prepare( 'p.ID >= %d', $start_id ); + $this->wheres[] = $wpdb->prepare( 'p.ID >= %d', $start_id ); } private function get_timestamp_for_the_last_day_of_a_month( $yyyy_mm ) { @@ -296,49 +249,47 @@ private function get_timestamp_for_the_last_day_of_a_month( $yyyy_mm ) { private function category_where() { global $wpdb; - if ( 'post' !== $this->filters['post_type'] && ! in_array( 'post', (array) $this->filters['post_type'], true ) ) { + if ( 'post' != $this->filters['post_type'] && ! in_array( 'post', (array) $this->filters['post_type'] ) ) { return; } $category = $this->find_category_from_any_object( $this->filters['category'] ); - if ( ! $category ) { + if ( !$category ) { return; } - $this->category = $category; - $this->joins[] = "INNER JOIN {$wpdb->term_relationships} AS tr ON (p.ID = tr.object_id)"; - $this->where_clauses[] = $wpdb->prepare( 'tr.term_taxonomy_id = %d', $category->term_taxonomy_id ); + $this->category = $category; + $this->joins[] = "INNER JOIN {$wpdb->term_relationships} AS tr ON (p.ID = tr.object_id)"; + $this->wheres[] = $wpdb->prepare( 'tr.term_taxonomy_id = %d', $category->term_taxonomy_id ); } private function max_num_posts() { if ( $this->filters['max_num_posts'] > 0 ) { return "LIMIT {$this->filters['max_num_posts']}"; - } else { - return ''; + } + else { + return ""; } } - private function include_attachment_ids( $post_ids ) { + private function attachments_for_specific_post_types( $post_ids ) { global $wpdb; - if ( ! $post_ids ) { - return []; + if ( !$this->filters['post_type'] ) { + return array(); } - $attachment_ids = []; - // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Assignment is part of the break condition. + $attachment_ids = array(); while ( $batch_of_post_ids = array_splice( $post_ids, 0, self::QUERY_CHUNK ) ) { $post_parent_condition = _wp_export_build_IN_condition( 'post_parent', $batch_of_post_ids ); - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Escaped in wpcli_export_build_in_condition() function. - $attachment_ids = array_merge( $attachment_ids, (array) $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND {$post_parent_condition}" ) ); + $attachment_ids = array_merge( $attachment_ids, (array)$wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND $post_parent_condition" ) ); } return array_map( 'intval', $attachment_ids ); } private function bloginfo_rss( $section ) { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return apply_filters( 'bloginfo_rss', get_bloginfo_rss( $section ), $section ); } private function find_user_from_any_object( $user ) { if ( is_numeric( $user ) ) { - return get_user_by( 'id', (int) $user ); + return get_user_by( 'id', $user ); } elseif ( is_string( $user ) ) { return get_user_by( 'login', $user ); } elseif ( isset( $user->ID ) ) { @@ -349,10 +300,10 @@ private function find_user_from_any_object( $user ) { private function find_category_from_any_object( $category ) { if ( is_numeric( $category ) ) { - return get_term( (int) $category, 'category' ); + return get_term( $category, 'category' ); } elseif ( is_string( $category ) ) { $term = term_exists( $category, 'category' ); - return isset( $term['term_id'] ) ? get_term( (int) $term['term_id'], 'category' ) : false; + return isset( $term['term_id'] )? get_term( $term['term_id'], 'category' ) : false; } elseif ( isset( $category->term_id ) ) { return get_term( $category->term_id, 'category' ); } @@ -360,77 +311,51 @@ private function find_category_from_any_object( $category ) { } private static function topologically_sort_terms( $terms ) { - $sorted = []; - // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- assignment is used as break condition. + $sorted = array(); while ( $term = array_shift( $terms ) ) { - if ( 0 === (int) $term->parent || isset( $sorted[ $term->parent ] ) ) { - $sorted[ $term->term_id ] = $term; - } else { + if ( $term->parent == 0 || isset( $sorted[$term->parent] ) ) + $sorted[$term->term_id] = $term; + else $terms[] = $term; - } } return $sorted; } - private function process_orphaned_terms( $terms ) { - - $term_ids = []; - $have_parent = []; - $orphans = []; + private function check_for_orphaned_terms( $terms ) { + $term_ids = array(); + $have_parent = array(); foreach ( $terms as $term ) { $term_ids[ $term->term_id ] = true; - if ( 0 !== (int) $term->parent ) { + if ( $term->parent != 0 ) $have_parent[] = $term; - } } foreach ( $have_parent as $has_parent ) { if ( ! isset( $term_ids[ $has_parent->parent ] ) ) { - if ( $this->filters['allow_orphan_terms'] ) { - $orphans[ $has_parent->term_id ] = true; - } else { - $this->missing_parents = $has_parent; - throw new WP_Export_Term_Exception( "Term is missing a parent: {$has_parent->slug} ({$has_parent->term_taxonomy_id})" ); - } - } - } - - if ( ! $this->filters['allow_orphan_terms'] ) { - return $terms; - } - - if ( count( $orphans ) > 0 ) { - $terms_return = []; - foreach ( $terms as $term ) { - if ( isset( $orphans[ $term->term_id ] ) ) { - $term->parent = 0; - } - $terms_return[] = $term; + $this->missing_parents = $has_parent; + throw new WP_Export_Term_Exception( sprintf( __( 'Term is missing a parent: %s (%d)' ), $has_parent->slug, $has_parent->term_taxonomy_id ) ); } - $terms = $terms_return; } - return $terms; } private static function get_terms_for_post( $post ) { $taxonomies = get_object_taxonomies( $post->post_type ); - if ( empty( $taxonomies ) ) { - return []; - } - return wp_get_object_terms( $post->ID, $taxonomies ) ?: []; + if ( empty( $taxonomies ) ) + return array(); + $terms = wp_get_object_terms( $post->ID, $taxonomies ); + $terms = $terms? $terms : array(); + return $terms; } private static function get_meta_for_post( $post ) { global $wpdb; - $meta_for_export = []; - $meta_from_db = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE post_id = %d", $post->ID ) ); + $meta_for_export = array(); + $meta_from_db = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE post_id = %d", $post->ID ) ); foreach ( $meta_from_db as $meta ) { - if ( '_edit_lock' === $meta->meta_key ) { + if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) continue; - } - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. - if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) { + if ( in_array( $meta->meta_key, array( '_edit_lock', '_wp_attachment_metadata', '_wp_attached_file' ) ) ) { continue; } $meta_for_export[] = $meta; @@ -442,13 +367,16 @@ private function get_comments_for_post( $post ) { global $wpdb; if ( isset( $this->filters['skip_comments'] ) ) { - return []; + return array(); } $comments = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved <> 'spam'", $post->ID ) ); - foreach ( $comments as $comment ) { - $comment->meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ) ?: []; + foreach( $comments as $comment ) { + $meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); + $meta = $meta? $meta : array(); + $comment->meta = $meta; } return $comments; } } + diff --git a/src/WP_Export_Returner.php b/src/WP_Export_Returner.php index 234e1b5b0..24baadfab 100644 --- a/src/WP_Export_Returner.php +++ b/src/WP_Export_Returner.php @@ -4,17 +4,15 @@ class WP_Export_Returner extends WP_Export_Base_Writer { private $result = ''; public function export() { - try { + $this->private = ''; + try { parent::export(); } catch ( WP_Export_Exception $e ) { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_error_message', $e->getMessage() ); return new WP_Error( 'wp-export-error', $message ); - + } catch ( WP_Export_Term_Exception $e ) { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extensions. do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_term_error_message', $e->getMessage() ); return new WP_Error( 'wp-export-error', $message ); } diff --git a/src/WP_Export_Split_Files_Writer.php b/src/WP_Export_Split_Files_Writer.php index 34f0962cd..360e722d0 100644 --- a/src/WP_Export_Split_Files_Writer.php +++ b/src/WP_Export_Split_Files_Writer.php @@ -1,70 +1,25 @@ max_file_size = 15 * MB_IN_BYTES; - } elseif ( WP_CLI_EXPORT_COMMAND_NO_SPLIT === (string) $writer_args['max_file_size'] ) { - $this->max_file_size = WP_CLI_EXPORT_COMMAND_NO_SPLIT; - } else { - $this->max_file_size = $writer_args['max_file_size'] * MB_IN_BYTES; - } - - // Filter and handle condition where subsequent export files should not contain - // all of the export sections - if ( is_array( $writer_args['include_once'] ) && ! empty( $writer_args['include_once'] ) ) { - $this->subsequent_sections = array_diff( $this->available_sections, $writer_args['include_once'] ); - } - - if ( ! empty( $writer_args['exclude'] ) ) { - if ( ! empty( $writer_args['exclude'] ) ) { - $this->available_sections = array_diff( $this->available_sections, $writer_args['exclude'] ); - } - } - + $this->max_file_size = is_null( $writer_args['max_file_size'] ) ? 15 * MB_IN_BYTES : $writer_args['max_file_size']; $this->destination_directory = $writer_args['destination_directory']; - $this->filename_template = $writer_args['filename_template']; - $this->before_posts_xml = $this->formatter->before_posts( $this->available_sections ); - $this->after_posts_xml = $this->formatter->after_posts(); + $this->filename_template = $writer_args['filename_template']; + $this->before_posts_xml = $this->formatter->before_posts(); + $this->after_posts_xml = $this->formatter->after_posts(); } public function export() { $this->start_new_file(); - foreach ( $this->formatter->posts() as $post_xml ) { - if ( WP_CLI_EXPORT_COMMAND_NO_SPLIT !== $this->max_file_size && $this->current_file_size + strlen( $post_xml ) > $this->max_file_size ) { + foreach( $this->formatter->posts() as $post_xml ) { + if ( $this->current_file_size + strlen( $post_xml ) > $this->max_file_size ) { $this->start_new_file(); } $this->write( $post_xml ); @@ -73,9 +28,9 @@ public function export() { } protected function write( $xml ) { - $res = fwrite( $this->f, $xml ); + $res = fwrite( $this->f, $xml); if ( false === $res ) { - throw new WP_Export_Exception( 'WP Export: error writing to export file.' ); + throw new WP_Export_Exception( __( 'WP Export: error writing to export file.' ) ); } $this->current_file_size += strlen( $xml ); } @@ -85,22 +40,17 @@ private function start_new_file() { $this->close_current_file(); } $file_path = $this->next_file_path(); - $this->f = fopen( $file_path, 'w' ); - if ( ! $this->f ) { - throw new WP_Export_Exception( "WP Export: error opening {$file_path} for writing." ); + $this->f = fopen( $file_path, 'w' ); + if ( !$this->f ) { + throw new WP_Export_Exception( sprintf( __( 'WP Export: error opening %s for writing.' ), $file_path ) ); } - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. do_action( 'wp_export_new_file', $file_path ); $this->current_file_size = 0; - $this->write( $this->before_posts_xml ); - if ( 1 === $this->next_file_number && ! empty( $this->subsequent_sections ) ) { - $this->before_posts_xml = $this->formatter->before_posts( $this->subsequent_sections ); - } } private function close_current_file() { - if ( ! $this->f ) { + if ( !$this->f ) { return; } $this->write( $this->after_posts_xml ); @@ -109,11 +59,12 @@ private function close_current_file() { private function next_file_name() { $next_file_name = sprintf( $this->filename_template, $this->next_file_number ); - ++$this->next_file_number; + $this->next_file_number++; return $next_file_name; } private function next_file_path() { return untrailingslashit( $this->destination_directory ) . DIRECTORY_SEPARATOR . $this->next_file_name(); } + } diff --git a/src/WP_Export_Stdout_Writer.php b/src/WP_Export_Stdout_Writer.php new file mode 100644 index 000000000..6bb81cf7e --- /dev/null +++ b/src/WP_Export_Stdout_Writer.php @@ -0,0 +1,20 @@ +before_posts_xml = $this->formatter->before_posts(); + $this->after_posts_xml = $this->formatter->after_posts(); + } + + public function export() { + fwrite( STDOUT, $this->before_posts_xml ); + foreach( $this->formatter->posts() as $post_xml ) { + fwrite( STDOUT, $post_xml ); + } + fwrite( STDOUT, $this->after_posts_xml ); + } + + protected function write( $xml ) { } +} diff --git a/src/WP_Export_WXR_Formatter.php b/src/WP_Export_WXR_Formatter.php index 870eeb74e..ca874c6ff 100644 --- a/src/WP_Export_WXR_Formatter.php +++ b/src/WP_Export_WXR_Formatter.php @@ -6,55 +6,32 @@ * * @since 2.5.0 */ -define( 'WXR_VERSION', '1.2' ); //phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress constant. +define( 'WXR_VERSION', '1.2' ); /** * Responsible for formatting the data in WP_Export_Query to WXR */ class WP_Export_WXR_Formatter { - /** - * @var WP_Export_Query - */ - private $export; - - /** - * @var string - */ - private $wxr_version; - public function __construct( $export ) { - $this->export = $export; + $this->export = $export; $this->wxr_version = WXR_VERSION; } - /** - * @param array $requested_sections - */ - public function before_posts( $requested_sections = [] ) { - $available_sections = [ - 'header', - 'site_metadata', - 'authors', - 'categories', - 'tags', - 'nav_menu_terms', - 'custom_taxonomies_terms', - 'rss2_head_action', - ]; - + public function before_posts() { $before_posts_xml = ''; - - foreach ( $available_sections as $section ) { - if ( ! $requested_sections || in_array( $section, $requested_sections, true ) ) { - $before_posts_xml .= $this->$section(); - } - } - + $before_posts_xml .= $this->header(); + $before_posts_xml .= $this->site_metadata(); + $before_posts_xml .= $this->authors(); + $before_posts_xml .= $this->categories(); + $before_posts_xml .= $this->tags(); + $before_posts_xml .= $this->nav_menu_terms(); + $before_posts_xml .= $this->custom_taxonomies_terms(); + $before_posts_xml .= $this->rss2_head_action(); return $before_posts_xml; } public function posts() { - return new WP_Map_Iterator( $this->export->posts(), [ $this, 'post' ] ); + return new WP_Map_Iterator( $this->export->posts(), array( $this, 'post' ) ); } public function after_posts() { @@ -62,9 +39,10 @@ public function after_posts() { } public function header() { - $oxymel = new Oxymel(); + $oxymel = new Oxymel; + $charset = $this->export->charset(); $wp_generator_tag = $this->export->wp_generator_tag(); - $comment = <<<'COMMENT' + $comment = <<xml ->comment( $comment ) ->raw( $wp_generator_tag ) - ->open_rss( - [ - 'version' => '2.0', - 'xmlns:excerpt' => "http://wordpress.org/export/{$this->wxr_version}/excerpt/", - 'xmlns:content' => 'http://purl.org/rss/1.0/modules/content/', - 'xmlns:wfw' => 'http://wellformedweb.org/CommentAPI/', - 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', - 'xmlns:wp' => "http://wordpress.org/export/{$this->wxr_version}/", - ] - ) + ->open_rss( array( + 'version' => '2.0', + 'xmlns:excerpt' => "http://wordpress.org/export/{$this->wxr_version}/excerpt/", + 'xmlns:content' => "http://purl.org/rss/1.0/modules/content/", + 'xmlns:wfw' => "http://wellformedweb.org/CommentAPI/", + 'xmlns:dc' => "http://purl.org/dc/elements/1.1/", + 'xmlns:wp' => "http://wordpress.org/export/{$this->wxr_version}/", + ) ) ->open_channel ->to_string(); + } public function site_metadata() { - $oxymel = new Oxymel(); + $oxymel = new Oxymel; $metadata = $this->export->site_metadata(); return $oxymel - // @phpstan-ignore method.notFound ->title( $metadata['name'] ) ->link( $metadata['url'] ) ->description( $metadata['description'] ) @@ -120,7 +95,7 @@ public function site_metadata() { } public function authors() { - $oxymel = new Oxymel(); + $oxymel = new Oxymel; $authors = $this->export->authors(); foreach ( $authors as $author ) { $oxymel @@ -137,26 +112,25 @@ public function authors() { } public function categories() { - $oxymel = new WP_Export_Oxymel(); + $oxymel = new WP_Export_Oxymel; $categories = $this->export->categories(); - foreach ( $categories as $category ) { - $category->parent_slug = $category->parent ? $categories[ $category->parent ]->slug : ''; + foreach( $categories as $term_id => $category ) { + $category->parent_slug = $category->parent? $categories[$category->parent]->slug : ''; $oxymel->tag( 'wp:category' )->contains ->tag( 'wp:term_id', $category->term_id ) ->tag( 'wp:category_nicename', $category->slug ) ->tag( 'wp:category_parent', $category->parent_slug ) ->optional_cdata( 'wp:cat_name', $category->name ) ->optional_cdata( 'wp:category_description', $category->description ) - ->oxymel( $this->term_meta( $category ) ) ->end; } return $oxymel->to_string(); } public function tags() { - $oxymel = new WP_Export_Oxymel(); - $tags = $this->export->tags(); - foreach ( $tags as $tag ) { + $oxymel = new WP_Export_Oxymel; + $tags = $this->export->tags(); + foreach( $tags as $tag ) { $oxymel->tag( 'wp:tag' )->contains ->tag( 'wp:term_id', $tag->term_id ) ->tag( 'wp:tag_slug', $tag->slug ) @@ -177,37 +151,29 @@ public function custom_taxonomies_terms() { public function rss2_head_action() { ob_start(); - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. do_action( 'rss2_head' ); - return ob_get_clean(); + $action_output = ob_get_clean(); + return $action_output; } public function post( $post ) { - /** - * @var \WP_Query $wp_query - */ - global $wp_query; - $oxymel = new WP_Export_Oxymel(); - $wp_query->in_the_loop = true; - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional. + $oxymel = new WP_Export_Oxymel; + $GLOBALS['wp_query']->in_the_loop = true; $GLOBALS['post'] = $post; setup_postdata( $post ); - // @phpstan-ignore property.notFound $oxymel->item->contains - ->tag( 'title' )->contains->cdata( apply_filters( 'the_title_export', $post->post_title ) )->end // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. - ->link( esc_url( apply_filters( 'the_permalink_rss', get_permalink() ) ) ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. - ->pubDate( mysql2date( 'D, d M Y H:i:s +0000', (string) get_post_time( 'Y-m-d H:i:s', true ), false ) ) + ->title( apply_filters( 'the_title_rss', $post->post_title ) ) + ->link( esc_url( apply_filters('the_permalink_rss', get_permalink() ) ) ) + ->pubDate( mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ) ) ->tag( 'dc:creator', get_the_author_meta( 'login' ) ) - ->guid( esc_url( get_the_guid() ), [ 'isPermaLink' => 'false' ] ) + ->guid( esc_url( get_the_guid() ), array( 'isPermaLink' => 'false' ) ) ->description( '' ) ->tag( 'content:encoded' )->contains->cdata( $post->post_content )->end ->tag( 'excerpt:encoded' )->contains->cdata( $post->post_excerpt )->end ->tag( 'wp:post_id', $post->ID ) ->tag( 'wp:post_date', $post->post_date ) ->tag( 'wp:post_date_gmt', $post->post_date_gmt ) - ->tag( 'wp:post_modified', $post->post_modified ) - ->tag( 'wp:post_modified_gmt', $post->post_modified_gmt ) ->tag( 'wp:comment_status', $post->comment_status ) ->tag( 'wp:ping_status', $post->ping_status ) ->tag( 'wp:post_name', $post->post_name ) @@ -218,24 +184,18 @@ public function post( $post ) { ->tag( 'wp:post_password', $post->post_password ) ->tag( 'wp:is_sticky', $post->is_sticky ) ->optional( 'wp:attachment_url', wp_get_attachment_url( $post->ID ) ); - foreach ( $post->terms as $term ) { + foreach( $post->terms as $term ) { $oxymel - // @phpstan-ignore method.notFound - ->category( - [ - 'domain' => $term->taxonomy, - 'nicename' => $term->slug, - ] - )->contains->cdata( $term->name )->end; + ->category( array( 'domain' => $term->taxonomy, 'nicename' => $term->slug ) )->contains->cdata( $term->name )->end; } - foreach ( $post->meta as $meta ) { + foreach( $post->meta as $meta ) { $oxymel ->tag( 'wp:postmeta' )->contains ->tag( 'wp:meta_key', $meta->meta_key ) ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end ->end; } - foreach ( $post->comments as $comment ) { + foreach( $post->comments as $comment ) { $oxymel ->tag( 'wp:comment' )->contains ->tag( 'wp:comment_id', $comment->comment_ID ) @@ -253,34 +213,31 @@ public function post( $post ) { ->oxymel( $this->comment_meta( $comment ) ) ->end; } - - // @phpstan-ignore property.notFound, expr.resultUnused - $oxymel->end; + $oxymel + ->end; return $oxymel->to_string(); } public function footer() { - $oxymel = new Oxymel(); - // @phpstan-ignore property.notFound + $oxymel = new Oxymel; return $oxymel->close_channel->close_rss->to_string(); } protected function terms( $terms ) { - $oxymel = new WP_Export_Oxymel(); - foreach ( $terms as $term ) { - $term->parent_slug = $term->parent ? $terms[ $term->parent ]->slug : ''; + $oxymel = new WP_Export_Oxymel; + foreach( $terms as $term ) { + $term->parent_slug = $term->parent? $terms[$term->parent]->slug : ''; $oxymel->tag( 'wp:term' )->contains ->tag( 'wp:term_id', $term->term_id ) ->tag( 'wp:term_taxonomy', $term->taxonomy ) ->tag( 'wp:term_slug', $term->slug ); - if ( 'nav_menu' !== $term->taxonomy ) { + if ( 'nav_menu' != $term->taxonomy ) { $oxymel ->tag( 'wp:term_parent', $term->parent_slug ); } $oxymel ->optional_cdata( 'wp:term_name', $term->name ) ->optional_cdata( 'wp:term_description', $term->description ) - ->oxymel( $this->term_meta( $term ) ) ->end; } return $oxymel->to_string(); @@ -289,11 +246,11 @@ protected function terms( $terms ) { protected function comment_meta( $comment ) { global $wpdb; $metas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); - if ( ! $metas ) { - return new Oxymel(); + if ( !$metas ) { + return new Oxymel; } - $oxymel = new WP_Export_Oxymel(); - foreach ( $metas as $meta ) { + $oxymel = new WP_Export_Oxymel; + foreach( $metas as $meta ) { $oxymel->tag( 'wp:commentmeta' )->contains ->tag( 'wp:meta_key', $meta->meta_key ) ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end @@ -301,20 +258,4 @@ protected function comment_meta( $comment ) { } return $oxymel; } - - protected function term_meta( $term ) { - global $wpdb; - $metas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->termmeta WHERE term_id = %d", $term->term_id ) ); - if ( ! $metas ) { - return new Oxymel(); - } - $oxymel = new WP_Export_Oxymel(); - foreach ( $metas as $meta ) { - $oxymel->tag( 'wp:termmeta' )->contains - ->tag( 'wp:meta_key', $meta->meta_key ) - ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end - ->end; - } - return $oxymel; - } } diff --git a/src/WP_Export_XML_Over_HTTP.php b/src/WP_Export_XML_Over_HTTP.php index 5c174721a..1a7ee37a5 100644 --- a/src/WP_Export_XML_Over_HTTP.php +++ b/src/WP_Export_XML_Over_HTTP.php @@ -1,21 +1,10 @@ file_name = $file_name; } @@ -26,14 +15,12 @@ public function export() { $this->send_headers(); echo $export; } catch ( WP_Export_Exception $e ) { - // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_error_message', $e->getMessage() ); - wp_die( $message, __( 'Export Error' ), [ 'back_link' => true ] ); + wp_die( $message, __( 'Export Error' ), array( 'back_link' => true ) ); } catch ( WP_Export_Term_Exception $e ) { do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); $message = apply_filters( 'export_term_error_message', $e->getMessage() ); - // phpcs:enable - wp_die( $message, __( 'Export Error' ), [ 'back_link' => true ] ); + wp_die( $message, __( 'Export Error' ), array( 'back_link' => true ) ); } } @@ -48,12 +35,8 @@ protected function get_export() { } protected function send_headers() { - /** - * @var string $charset - */ - $charset = get_option( 'blog_charset' ); header( 'Content-Description: File Transfer' ); header( 'Content-Disposition: attachment; filename=' . $this->file_name ); - header( 'Content-Type: text/xml; charset=' . $charset, true ); + header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); } } diff --git a/src/WP_Map_Iterator.php b/src/WP_Map_Iterator.php index b68a618e5..74daf2986 100644 --- a/src/WP_Map_Iterator.php +++ b/src/WP_Map_Iterator.php @@ -1,25 +1,12 @@ > - */ class WP_Map_Iterator extends IteratorIterator { - /** - * @var callable - */ - private $callback; - - /** - * @param \Iterator $iterator - * @param callable $callback - */ - public function __construct( $iterator, $callback ) { + function __construct( $iterator, $callback ) { $this->callback = $callback; parent::__construct( $iterator ); } - #[\ReturnTypeWillChange] - public function current() { + function current() { $original_current = parent::current(); return call_user_func( $this->callback, $original_current ); } diff --git a/src/WP_Post_IDs_Iterator.php b/src/WP_Post_IDs_Iterator.php index 83b1279c8..49ad84d38 100644 --- a/src/WP_Post_IDs_Iterator.php +++ b/src/WP_Post_IDs_Iterator.php @@ -1,92 +1,49 @@ - */ class WP_Post_IDs_Iterator implements Iterator { - /** - * @var \wpdb - */ - private $db; - - /** - * @var int - */ private $limit = 100; - - /** - * @var int[] - */ private $post_ids; - - /** - * @var int[] - */ private $ids_left; - - /** - * @var object[] - */ private $results = array(); - /** - * @var int - */ - private $index_in_results; - - /** - * @var int - */ - private $global_index; - public function __construct( $post_ids, $limit = null ) { - /** - * @var \wpdb $wpdb - */ - global $wpdb; - - $this->db = $wpdb; + $this->db = $GLOBALS['wpdb']; $this->post_ids = $post_ids; $this->ids_left = $post_ids; - if ( ! is_null( $limit ) ) { + if ( !is_null( $limit ) ) { $this->limit = $limit; } } - #[\ReturnTypeWillChange] public function current() { - return $this->results[ $this->index_in_results ]; + return $this->results[$this->index_in_results]; } - #[\ReturnTypeWillChange] public function key() { return $this->global_index; } - #[\ReturnTypeWillChange] public function next() { - ++$this->index_in_results; - ++$this->global_index; + $this->index_in_results++; + $this->global_index++; } - #[\ReturnTypeWillChange] public function rewind() { - $this->results = array(); - $this->global_index = 0; + $this->results = array(); + $this->global_index = 0; $this->index_in_results = 0; - $this->ids_left = $this->post_ids; + $this->ids_left = $this->post_ids; } - #[\ReturnTypeWillChange] public function valid() { - if ( isset( $this->results[ $this->index_in_results ] ) ) { + if ( isset( $this->results[$this->index_in_results] ) ) { return true; } if ( empty( $this->ids_left ) ) { return false; } $has_posts = $this->load_next_posts_from_db(); - if ( ! $has_posts ) { + if ( !$has_posts ) { return false; } $this->index_in_results = 0; @@ -95,12 +52,11 @@ public function valid() { private function load_next_posts_from_db() { $next_batch_post_ids = array_splice( $this->ids_left, 0, $this->limit ); - $in_post_ids_sql = _wp_export_build_IN_condition( 'ID', $next_batch_post_ids ); - $results = $this->db->get_results( "SELECT * FROM {$this->db->posts} WHERE {$in_post_ids_sql}" ); - $this->results = is_array( $results ) ? $results : array(); - if ( ! $this->results ) { + $in_post_ids_sql = _wp_export_build_IN_condition( 'ID', $next_batch_post_ids ); + $this->results = $this->db->get_results( "SELECT * FROM {$this->db->posts} WHERE $in_post_ids_sql" ); + if ( !$this->results ) { if ( $this->db->last_error ) { - throw new WP_Iterator_Exception( "Database error: {$this->db->last_error}" ); + throw new WP_Iterator_Exception( 'Database error: ' . $this->db->last_error ); } else { return false; } diff --git a/utils/behat-tags.php b/utils/behat-tags.php new file mode 100644 index 000000000..ee51fc91e --- /dev/null +++ b/utils/behat-tags.php @@ -0,0 +1,76 @@ +' ) +); + +# Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612 +$skip_tags[] = '@github-api'; + +# Skip tests known to be broken. +$skip_tags[] = '@broken'; + +# Require PHP extension, eg 'imagick'. +function extension_tags() { + $extension_tags = array(); + exec( "grep '@require-extension-[A-Za-z_]*' -h -o features/*.feature | uniq", $extension_tags ); + + $skip_tags = array(); + + $substr_start = strlen( '@require-extension-' ); + foreach ( $extension_tags as $tag ) { + $extension = substr( $tag, $substr_start ); + if ( ! extension_loaded( $extension ) ) { + $skip_tags[] = $tag; + } + } + + return $skip_tags; +} + +$skip_tags = array_merge( $skip_tags, extension_tags() ); + +if ( !empty( $skip_tags ) ) { + echo '--tags=~' . implode( '&&~', $skip_tags ); +} +