From 62439c1fbfbb10f68b3911e5fde789b13b904a3f Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 1 Mar 2018 18:22:24 +0000 Subject: [PATCH] Refresh README and test suite prior to 1.1.10 release. --- README.md | 39 ++++-- features/bootstrap/FeatureContext.php | 59 +++++++-- features/bootstrap/Process.php | 4 +- features/bootstrap/utils.php | 169 +++++++++++++++++++++++--- utils/behat-tags.php | 17 ++- 5 files changed, 242 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 427cbca9a..4dd3b9d13 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ See the WordPress [Plugin Handbook](https://developer.wordpress.org/plugins/) de ### wp plugin activate -Activates a plugin. +Activates one or more plugins. ~~~ wp plugin activate [...] [--all] [--network] @@ -87,7 +87,7 @@ wp plugin activate [...] [--all] [--network] ### wp plugin deactivate -Deactivates a plugin. +Deactivates one or more plugins. ~~~ wp plugin deactivate [...] [--uninstall] [--all] [--network] @@ -182,7 +182,7 @@ wp plugin get [--field=] [--fields=] [--format=] ### wp plugin install -Installs a plugin. +Installs one or more plugins. ~~~ wp plugin install ... [--version=] [--force] [--activate] [--activate-network] @@ -191,7 +191,7 @@ wp plugin install ... [--version=] [--force] [--activat **OPTIONS** ... - A plugin slug, the path to a local zip file, or URL to a remote zip file. + One or more plugins to install. Accepts a plugin slug, the path to a local zip file, or a URL to a remote zip file. [--version=] If set, get that particular version from wordpress.org, instead of the @@ -434,11 +434,23 @@ search query. **requires**: Plugin Minimum Requirements **tested**: Plugin Tested Up To **compatibility**: Plugin Compatible With - **rating**: Plugin Rating + **rating**: Plugin Rating in Percent and Total Number + **ratings**: Plugin Ratings for each star (1-5) **num_ratings**: Number of Plugin Ratings **homepage**: Plugin Author's Homepage **description**: Plugin's Description **short_description**: Plugin's Short Description + **sections**: Plugin Readme Sections: description, installation, FAQ, screenshots, other notes, and changelog + **downloaded**: Plugin Download Count + **last_updated**: Plugin's Last Update + **added**: Plugin's Date Added to wordpress.org Repository + **tags**: Plugin's Tags + **versions**: Plugin's Available Versions with D/L Link + **donate_link**: Plugin's Donation Link + **banners**: Plugin's Banner Image Link + **icons**: Plugin's Icon Image Link + **active_installs**: Plugin's Number of Active Installs + **contributors**: Plugin's List of Contributors [--format=] Render output in a particular format. @@ -541,15 +553,15 @@ inactive, then it will be activated. ### wp plugin uninstall -Uninstalls a plugin. +Uninstalls one or more plugins. ~~~ -wp plugin uninstall ... [--deactivate] [--skip-delete] +wp plugin uninstall [...] [--deactivate] [--skip-delete] [--all] ~~~ **OPTIONS** - ... + [...] One or more plugins to uninstall. [--deactivate] @@ -559,6 +571,9 @@ wp plugin uninstall ... [--deactivate] [--skip-delete] If set, the plugin files will not be deleted. Only the uninstall procedure will be run. + [--all] + If set, all plugins will be uninstalled. + **EXAMPLES** $ wp plugin uninstall hello @@ -721,13 +736,13 @@ wp theme activate ### wp theme delete -Deletes a theme. +Deletes one or more themes. ~~~ wp theme delete ... ~~~ -Removes the theme from the filesystem. +Removes the theme or themes from the filesystem. **OPTIONS** @@ -860,7 +875,7 @@ wp theme get [--field=] [--fields=] [--format=] ### wp theme install -Installs a theme. +Installs one or more themes. ~~~ wp theme install ... [--version=] [--force] [--activate] @@ -869,7 +884,7 @@ wp theme install ... [--version=] [--force] [--activate] **OPTIONS** ... - A theme slug, the path to a local zip file, or URL to a remote zip file. + One or more themes to install. Accepts a theme slug, the path to a local zip file, or a URL to a remote zip file. [--version=] If set, get that particular version from wordpress.org, instead of the diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index b74d7668e..6f88ed1bb 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -87,8 +87,9 @@ class FeatureContext extends BehatContext implements ClosuredContextInterface { 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. + * 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. One is set on use: INVOKE_WP_CLI_WITH_PHP_ARGS-args. + * Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario. */ public $variables = array(); @@ -117,8 +118,9 @@ 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' ); + $path_separator = Utils\is_windows() ? ';' : ':'; $env = array( - 'PATH' => $bin_dir . ':' . $vendor_dir . ':' . getenv( 'PATH' ), + 'PATH' => $bin_dir . $path_separator . $vendor_dir . $path_separator . getenv( 'PATH' ), 'BEHAT_RUN' => 1, 'HOME' => sys_get_temp_dir() . '/wp-cli-home', ); @@ -328,20 +330,57 @@ public function getHookDefinitionResources() { } /** - * Replace {VARIABLE_NAME}. Note that variable names can only contain uppercase letters and underscores (no numbers). + * Replace standard {VARIABLE_NAME} variables and the special {INVOKE_WP_CLI_WITH_PHP_ARGS-args} and {WP_VERSION-version-latest} variables. + * Note that standard variable names can only contain uppercase letters, digits and underscores and cannot begin with a digit. */ public function replace_variables( $str ) { - $ret = preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); + if ( false !== strpos( $str, '{INVOKE_WP_CLI_WITH_PHP_ARGS-' ) ) { + $str = $this->replace_invoke_wp_cli_with_php_args( $str ); + } + $str = preg_replace_callback( '/\{([A-Z_][A-Z_0-9]*)\}/', array( $this, 'replace_var' ), $str ); if ( false !== strpos( $str, '{WP_VERSION-' ) ) { - $ret = $this->_replace_wp_versions( $ret ); + $str = $this->replace_wp_versions( $str ); + } + return $str; + } + + /** + * Substitute {INVOKE_WP_CLI_WITH_PHP_ARGS-args} variables. + */ + private function replace_invoke_wp_cli_with_php_args( $str ) { + static $phar_path = null, $shell_path = null; + + if ( null === $phar_path ) { + $phar_path = false; + $phar_begin = '#!/usr/bin/env php'; + $phar_begin_len = strlen( $phar_begin ); + if ( ( $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ) && file_exists( $bin_dir . '/wp' ) && $phar_begin === file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) ) { + $phar_path = $bin_dir . '/wp'; + } else { + $src_dir = dirname( dirname( __DIR__ ) ); + $bin_path = $src_dir . '/bin/wp'; + $vendor_bin_path = $src_dir . '/vendor/bin/wp'; + if ( file_exists( $bin_path ) && is_executable( $bin_path ) ) { + $shell_path = $bin_path; + } elseif ( file_exists( $vendor_bin_path ) && is_executable( $vendor_bin_path ) ) { + $shell_path = $vendor_bin_path; + } else { + $shell_path = 'wp'; + } + } } - return $ret; + + $str = preg_replace_callback( '/{INVOKE_WP_CLI_WITH_PHP_ARGS-([^}]*)}/', function ( $matches ) use ( $phar_path, $shell_path ) { + return $phar_path ? "php {$matches[1]} {$phar_path}" : ( 'WP_CLI_PHP_ARGS=' . escapeshellarg( $matches[1] ) . ' ' . $shell_path ); + }, $str ); + + return $str; } /** * Replace variables callback. */ - private function _replace_var( $matches ) { + private function replace_var( $matches ) { $cmd = $matches[0]; foreach ( array_slice( $matches, 1 ) as $key ) { @@ -352,9 +391,9 @@ private function _replace_var( $matches ) { } /** - * Substitute "{WP_VERSION-version-latest}" variables. + * Substitute {WP_VERSION-version-latest} variables. */ - private function _replace_wp_versions( $str ) { + private function replace_wp_versions( $str ) { static $wp_versions = null; if ( null === $wp_versions ) { $wp_versions = array(); diff --git a/features/bootstrap/Process.php b/features/bootstrap/Process.php index 70c3c9f82..584f679b9 100644 --- a/features/bootstrap/Process.php +++ b/features/bootstrap/Process.php @@ -2,6 +2,8 @@ namespace WP_CLI; +use WP_CLI\Utils; + /** * Run a system process, and learn what happened. */ @@ -67,7 +69,7 @@ private function __construct() {} public function run() { $start_time = microtime( true ); - $proc = proc_open( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); + $proc = Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); $stdout = stream_get_contents( $pipes[1] ); fclose( $pipes[1] ); diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php index bb6e9ad11..26b3dbbed 100644 --- a/features/bootstrap/utils.php +++ b/features/bootstrap/utils.php @@ -383,15 +383,11 @@ function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { $editor = getenv( 'EDITOR' ); if ( ! $editor ) { - $editor = 'vi'; - - if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) { - $editor = 'notepad'; - } + $editor = is_windows() ? 'notepad' : 'vi'; } $descriptorspec = array( STDIN, STDOUT, STDERR ); - $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); + $process = proc_open_compat( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); $r = proc_close( $process ); if ( $r ) { exit( $r ); @@ -453,7 +449,7 @@ function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); - $proc = proc_open( $final_cmd, $descriptors, $pipes ); + $proc = proc_open_compat( $final_cmd, $descriptors, $pipes ); if ( ! $proc ) { exit( 1 ); } @@ -515,14 +511,15 @@ function mustache_render( $template_name, $data = array() ) { * * @param string $message Text to display before the progress bar. * @param integer $count Total number of ticks to be performed. + * @param int $interval Optional. The interval in milliseconds between updates. Default 100. * @return cli\progress\Bar|WP_CLI\NoOp */ -function make_progress_bar( $message, $count ) { +function make_progress_bar( $message, $count, $interval = 100 ) { if ( \cli\Shell::isPiped() ) { return new \WP_CLI\NoOp; } - return new \cli\progress\Bar( $message, $count ); + return new \cli\progress\Bar( $message, $count, $interval ); } function parse_url( $url ) { @@ -775,6 +772,16 @@ function trailingslashit( $string ) { return rtrim( $string, '/\\' ) . '/'; } +/** + * Convert Windows EOLs to *nix. + * + * @param string $str String to convert. + * @return string String with carriage return / newline pairs reduced to newlines. + */ +function normalize_eols( $str ) { + return str_replace( "\r\n", "\n", $str ); +} + /** * Get the system's temp directory. Warns user if it isn't writable. * @@ -790,14 +797,8 @@ function get_temp_dir() { return $temp; } - $temp = '/tmp/'; - - // `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 ); - } + // `sys_get_temp_dir()` introduced PHP 5.2.1. Will always return something. + $temp = trailingslashit( sys_get_temp_dir() ); if ( ! is_writable( $temp ) ) { \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); @@ -1110,7 +1111,9 @@ function glob_brace( $pattern, $dummy_flags = null ) { function get_suggestion( $target, array $options, $threshold = 2 ) { $suggestion_map = array( + 'add' => 'create', 'check' => 'check-update', + 'capability' => 'cap', 'clear' => 'flush', 'decrement' => 'decr', 'del' => 'delete', @@ -1126,10 +1129,11 @@ function get_suggestion( $target, array $options, $threshold = 2 ) { 'regen' => 'regenerate', 'rep' => 'replace', 'repl' => 'replace', + 'trash' => 'delete', 'v' => 'version', ); - if ( array_key_exists( $target, $suggestion_map ) ) { + if ( array_key_exists( $target, $suggestion_map ) && in_array( $suggestion_map[ $target ], $options, true ) ) { return $suggestion_map[ $target ]; } @@ -1318,3 +1322,132 @@ function get_php_binary() { return 'php'; } + +/** + * Windows compatible `proc_open()`. + * Works around bug in PHP, and also deals with *nix-like `ENV_VAR=blah cmd` environment variable prefixes. + * + * @access public + * + * @param string $command Command to execute. + * @param array $descriptorspec Indexed array of descriptor numbers and their values. + * @param array &$pipes Indexed array of file pointers that correspond to PHP's end of any pipes that are created. + * @param string $cwd Initial working directory for the command. + * @param array $env Array of environment variables. + * @param array $other_options Array of additional options (Windows only). + * + * @return string Command stripped of any environment variable settings. + */ +function proc_open_compat( $cmd, $descriptorspec, &$pipes, $cwd = null, $env = null, $other_options = null ) { + if ( is_windows() ) { + // Need to encompass the whole command in double quotes - PHP bug https://bugs.php.net/bug.php?id=49139 + $cmd = '"' . _proc_open_compat_win_env( $cmd, $env ) . '"'; + } + return proc_open( $cmd, $descriptorspec, $pipes, $cwd, $env, $other_options ); +} + +/** + * For use by `proc_open_compat()` only. Separated out for ease of testing. Windows only. + * Turns *nix-like `ENV_VAR=blah command` environment variable prefixes into stripped `cmd` with prefixed environment variables added to passed in environment array. + * + * @access private + * + * @param string $command Command to execute. + * @param array &$env Array of existing environment variables. Will be modified if any settings in command. + * + * @return string Command stripped of any environment variable settings. + */ +function _proc_open_compat_win_env( $cmd, &$env ) { + if ( false !== strpos( $cmd, '=' ) ) { + while ( preg_match( '/^([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|[^ ]*) /', $cmd, $matches ) ) { + $cmd = substr( $cmd, strlen( $matches[0] ) ); + if ( null === $env ) { + $env = array(); + } + $env[ $matches[1] ] = isset( $matches[2][0] ) && '"' === $matches[2][0] ? substr( $matches[2], 1, -1 ) : $matches[2]; + } + } + return $cmd; +} + +/** + * First half of escaping for LIKE special characters % and _ before preparing for MySQL. + * + * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. + * + * Copied from core "wp-includes/wp-db.php". Avoids dependency on WP 4.4 wpdb. + * + * @access public + * + * @param string $text The raw text to be escaped. The input typed by the user should have no + * extra or deleted slashes. + * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() + * or real_escape next. + */ +function esc_like( $text ) { + return addcslashes( $text, '_%\\' ); +} + +/** + * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. + * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html + * + * @param string|array $idents A single identifier or an array of identifiers. + * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. + */ +function esc_sql_ident( $idents ) { + $backtick = function ( $v ) { + // Escape any backticks in the identifier by doubling. + return '`' . str_replace( '`', '``', $v ) . '`'; + }; + if ( is_string( $idents ) ) { + return $backtick( $idents ); + } + return array_map( $backtick, $idents ); +} + +/** + * Check whether a given string is a valid JSON representation. + * + * @param string $argument String to evaluate. + * @param bool $ignore_scalars Optional. Whether to ignore scalar values. + * Defaults to true. + * + * @return bool Whether the provided string is a valid JSON representation. + */ +function is_json( $argument, $ignore_scalars = true ) { + if ( ! is_string( $argument ) || '' === $argument ) { + return false; + } + + if ( $ignore_scalars && ! in_array( $argument[0], array( '{', '[' ), true ) ) { + return false; + } + + json_decode( $argument, $assoc = true ); + + return json_last_error() === JSON_ERROR_NONE; +} + +/** + * Parse known shell arrays included in the $assoc_args array. + * + * @param array $assoc_args Associative array of arguments. + * @param array $array_arguments Array of argument keys that should receive an + * array through the shell. + * + * @return array + */ +function parse_shell_arrays( $assoc_args, $array_arguments ) { + if ( empty( $assoc_args ) || empty( $array_arguments ) ) { + return $assoc_args; + } + + foreach ( $array_arguments as $key ) { + if ( array_key_exists( $key, $assoc_args ) && is_json( $assoc_args[ $key ] ) ) { + $assoc_args[ $key ] = json_decode( $assoc_args[ $key ], $assoc = true ); + } + } + + return $assoc_args; +} diff --git a/utils/behat-tags.php b/utils/behat-tags.php index 9c87fb656..f65fb7d75 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -31,17 +31,24 @@ function version_tags( $prefix, $current, $operator = '<' ) { return $skip_tags; } +$wp_version = getenv( 'WP_VERSION' ); $wp_version_reqs = array(); -// Only apply @require-wp tags when WP_VERSION isn't 'latest' or 'nightly' -// 'latest' and 'nightly' are expected to work with all features -if ( ! in_array( getenv( 'WP_VERSION' ), array( 'latest', 'nightly', 'trunk' ), true ) ) { - $wp_version_reqs = version_tags( 'require-wp', getenv( 'WP_VERSION' ), '<' ); +// Only apply @require-wp tags when WP_VERSION isn't 'latest', 'nightly' or 'trunk'. +// 'latest', 'nightly' and 'trunk' are expected to work with all features. +if ( $wp_version && ! in_array( $wp_version, array( 'latest', 'nightly', 'trunk' ), true ) ) { + $wp_version_reqs = array_merge( + version_tags( 'require-wp', $wp_version, '<' ), + version_tags( 'less-than-wp', $wp_version, '>=' ) + ); +} else { + // But make sure @less-than-wp tags always exist for those special cases. (Note: @less-than-wp-latest etc won't work and shouldn't be used). + $wp_version_reqs = array_merge( $wp_version_reqs, version_tags( 'less-than-wp', '9999', '>=' ) ); } $skip_tags = array_merge( $wp_version_reqs, version_tags( 'require-php', PHP_VERSION, '<' ), - version_tags( 'less-than-php', PHP_VERSION, '>' ) + version_tags( 'less-than-php', PHP_VERSION, '>=' ) // Note: this was '>' prior to WP-CLI 1.5.0 but the change is unlikely to cause BC issues as usually compared against major.minor only. ); # Skip Github API tests if `GITHUB_TOKEN` not available because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612