├── .distignore ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── bin └── install-package-tests.sh ├── command.php ├── composer.json ├── features ├── bootstrap │ ├── FeatureContext.php │ ├── Process.php │ ├── ProcessRun.php │ ├── support.php │ └── utils.php ├── extra │ └── no-mail.php ├── reset-password.feature └── steps │ ├── given.php │ ├── then.php │ └── when.php ├── utils ├── behat-tags.php └── get-package-require-from-composer.php └── wp-cli.yml /.distignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git 3 | .gitignore 4 | .editorconfig 5 | .travis.yml 6 | circle.yml 7 | bin/ 8 | features/ 9 | utils/ 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [{.jshintrc,*.json,*.yml,*.feature}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [{*.txt,wp-config-sample.php}] 21 | end_of_line = crlf 22 | 23 | [composer.json] 24 | indent_style = space 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | wp-cli.local.yml 3 | node_modules/ 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | notifications: 6 | email: 7 | on_success: never 8 | on_failure: change 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | php: 15 | - 5.3 16 | - 5.6 17 | 18 | cache: 19 | - composer 20 | - $HOME/.composer/cache 21 | 22 | env: 23 | global: 24 | - WP_CLI_BIN_DIR=/tmp/wp-cli-phar 25 | 26 | before_script: 27 | - composer validate 28 | - bash bin/install-package-tests.sh 29 | 30 | script: ./vendor/bin/behat --strict 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | runcommand/user-reset-password 2 | ============================== 3 | 4 | Reset passwords for one or more WordPress users. 5 | 6 | [![runcommand open source](https://runcommand.io/wp-content/themes/runcommand-theme/bin/shields/runcommand-open-source.svg)](https://runcommand.io/pricing/) [![Build Status](https://travis-ci.org/runcommand/user-reset-password.svg?branch=master)](https://travis-ci.org/runcommand/user-reset-password) 7 | 8 | Quick links: [Using](#using) | [Installing](#installing) | [Support](#support) 9 | 10 | ## Using 11 | 12 | ~~~ 13 | wp user reset-password ... [--skip-email] 14 | ~~~ 15 | 16 | **OPTIONS** 17 | 18 | ... 19 | Specify one or more user logins or IDs. 20 | 21 | [--skip-email] 22 | Don't send an email notification to the affected user(s). 23 | 24 | ## Installing 25 | 26 | Installing this package requires WP-CLI v0.23.0 or greater. Update to the latest stable release with `wp cli update`. 27 | 28 | Once you've done so, you can install this package with `wp package install runcommand/user-reset-password`. 29 | 30 | ## Support 31 | 32 | This WP-CLI package is free for anyone to use. Support, including usage questions and feature requests, is available to [paying runcommand customers](https://runcommand.io/pricing/). 33 | 34 | Think you’ve found a bug? Before you create a new issue, you should [search existing issues](https://github.com/runcommand/sparks/issues?q=label%3Abug%20) to see if there’s an existing resolution to it, or if it’s already been fixed in a newer version. Once you’ve done a bit of searching and discovered there isn’t an open or fixed issue for your bug, please [create a new issue](https://github.com/runcommand/sparks/issues/new) with description of what you were doing, what you saw, and what you expected to see. 35 | 36 | Want to contribute a new feature? Please first [open a new issue](https://github.com/runcommand/sparks/issues/new) to discuss whether the feature is a good fit for the project. Once you've decided to work on a pull request, please include [functional tests](https://wp-cli.org/docs/pull-requests/#functional-tests) and follow the [WordPress Coding Standards](http://make.wordpress.org/core/handbook/coding-standards/). 37 | 38 | runcommand customers can also email [support@runcommand.io](mailto:support@runcommand.io) for private support. 39 | 40 | 41 | -------------------------------------------------------------------------------- /bin/install-package-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | PACKAGE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../ && pwd )" 6 | 7 | download() { 8 | if [ `which curl` ]; then 9 | curl -s "$1" > "$2"; 10 | elif [ `which wget` ]; then 11 | wget -nv -O "$2" "$1" 12 | fi 13 | } 14 | 15 | install_wp_cli() { 16 | 17 | # the Behat test suite will pick up the executable found in $WP_CLI_BIN_DIR 18 | mkdir -p $WP_CLI_BIN_DIR 19 | download https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar $WP_CLI_BIN_DIR/wp 20 | chmod +x $WP_CLI_BIN_DIR/wp 21 | 22 | } 23 | 24 | download_behat() { 25 | 26 | cd $PACKAGE_DIR 27 | download https://getcomposer.org/installer installer 28 | php installer 29 | php composer.phar require --dev behat/behat='~2.5' 30 | 31 | } 32 | 33 | install_db() { 34 | mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot 35 | mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot 36 | } 37 | 38 | install_wp_cli 39 | download_behat 40 | install_db 41 | -------------------------------------------------------------------------------- /command.php: -------------------------------------------------------------------------------- 1 | ... 13 | * : Specify one or more user logins or IDs. 14 | * 15 | * [--skip-email] 16 | * : Don't send an email notification to the affected user(s). 17 | */ 18 | $reset_password_command = function( $args, $assoc_args ) { 19 | 20 | $skip_email = WP_CLI\Utils\get_flag_value( $assoc_args, 'skip-email' ); 21 | if ( $skip_email ) { 22 | add_filter( 'send_password_change_email', '__return_false' ); 23 | } 24 | 25 | $fetcher = new \WP_CLI\Fetchers\User; 26 | $users = $fetcher->get_many( $args ); 27 | foreach( $users as $user ) { 28 | wp_update_user( array( 'ID' => $user->ID, 'user_pass' => wp_generate_password() ) ); 29 | WP_CLI::log( "Reset password for {$user->user_login}." ); 30 | } 31 | 32 | if ( $skip_email ) { 33 | remove_filter( 'send_password_change_email', '__return_false' ); 34 | } 35 | 36 | WP_CLI::success( 'Passwords reset.' ); 37 | }; 38 | WP_CLI::add_command( 'user reset-password', $reset_password_command ); 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runcommand/user-reset-password", 3 | "description": "Reset passwords for one or more WordPress users.", 4 | "type": "wp-cli-package", 5 | "homepage": "https://runcommand.io/wp/user-reset-password/", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Daniel Bachhuber", 10 | "email": "daniel@runcommand.io", 11 | "homepage": "https://runcommand.io" 12 | } 13 | ], 14 | "minimum-stability": "dev", 15 | "autoload": { 16 | "files": [ "command.php" ] 17 | }, 18 | "require": {}, 19 | "require-dev": { 20 | "behat/behat": "~2.5" 21 | }, 22 | "extra": { 23 | "commands": [ 24 | "user reset-password" 25 | ], 26 | "readme": { 27 | "shields": [ 28 | "[![runcommand open source](https://runcommand.io/wp-content/themes/runcommand-theme/bin/shields/runcommand-open-source.svg)](https://runcommand.io/pricing/)", 29 | "[![Build Status](https://travis-ci.org/runcommand/user-reset-password.svg?branch=master)](https://travis-ci.org/runcommand/user-reset-password)" 30 | ], 31 | "sections": [ 32 | "Using", 33 | "Installing", 34 | "Support" 35 | ], 36 | "support": { 37 | "body": "https://raw.githubusercontent.com/runcommand/runcommand-theme/master/bin/readme-partials/support-open-source.md" 38 | }, 39 | "show_powered_by": false 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | autoload->files ) ) { 20 | $contents = 'require:' . PHP_EOL; 21 | foreach( $composer->autoload->files as $file ) { 22 | $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file; 23 | } 24 | @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' ); 25 | $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml'; 26 | file_put_contents( $project_config, $contents ); 27 | putenv( 'WP_CLI_CONFIG_PATH=' . $project_config ); 28 | } 29 | } 30 | // Inside WP-CLI 31 | } else { 32 | require_once __DIR__ . '/../../php/utils.php'; 33 | require_once __DIR__ . '/../../php/WP_CLI/Process.php'; 34 | require_once __DIR__ . '/../../php/WP_CLI/ProcessRun.php'; 35 | require_once __DIR__ . '/../../vendor/autoload.php'; 36 | } 37 | 38 | /** 39 | * Features context. 40 | */ 41 | class FeatureContext extends BehatContext implements ClosuredContextInterface { 42 | 43 | private static $cache_dir, $suite_cache_dir; 44 | 45 | private static $db_settings = array( 46 | 'dbname' => 'wp_cli_test', 47 | 'dbuser' => 'wp_cli_test', 48 | 'dbpass' => 'password1', 49 | 'dbhost' => '127.0.0.1', 50 | ); 51 | 52 | private $running_procs = array(); 53 | 54 | public $variables = array(); 55 | 56 | /** 57 | * Get the environment variables required for launched `wp` processes 58 | * @beforeSuite 59 | */ 60 | private static function get_process_env_variables() { 61 | // Ensure we're using the expected `wp` binary 62 | $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" ); 63 | $env = array( 64 | 'PATH' => $bin_dir . ':' . getenv( 'PATH' ), 65 | 'BEHAT_RUN' => 1, 66 | 'HOME' => '/tmp/wp-cli-home', 67 | ); 68 | if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { 69 | $env['WP_CLI_CONFIG_PATH'] = $config_path; 70 | } 71 | return $env; 72 | } 73 | 74 | // We cache the results of `wp core download` to improve test performance 75 | // Ideally, we'd cache at the HTTP layer for more reliable tests 76 | private static function cache_wp_files() { 77 | self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache'; 78 | 79 | if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) 80 | return; 81 | 82 | $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); 83 | if ( getenv( 'WP_VERSION' ) ) { 84 | $cmd .= Utils\esc_cmd( ' --version=%s', getenv( 'WP_VERSION' ) ); 85 | } 86 | Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); 87 | } 88 | 89 | /** 90 | * @BeforeSuite 91 | */ 92 | public static function prepare( SuiteEvent $event ) { 93 | $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); 94 | echo PHP_EOL; 95 | echo $result->stdout; 96 | echo PHP_EOL; 97 | self::cache_wp_files(); 98 | $result = Process::create( Utils\esc_cmd( 'wp core version --path=%s', self::$cache_dir ) , null, self::get_process_env_variables() )->run_check(); 99 | echo 'WordPress ' . $result->stdout; 100 | echo PHP_EOL; 101 | } 102 | 103 | /** 104 | * @AfterSuite 105 | */ 106 | public static function afterSuite( SuiteEvent $event ) { 107 | if ( self::$suite_cache_dir ) { 108 | Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run(); 109 | } 110 | } 111 | 112 | /** 113 | * @BeforeScenario 114 | */ 115 | public function beforeScenario( $event ) { 116 | $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); 117 | } 118 | 119 | /** 120 | * @AfterScenario 121 | */ 122 | public function afterScenario( $event ) { 123 | if ( isset( $this->variables['RUN_DIR'] ) ) { 124 | // remove altered WP install, unless there's an error 125 | if ( $event->getResult() < 4 ) { 126 | $this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run(); 127 | } 128 | } 129 | 130 | // Remove WP-CLI package directory 131 | if ( isset( $this->variables['PACKAGE_PATH'] ) ) { 132 | $this->proc( Utils\esc_cmd( 'rm -rf %s', $this->variables['PACKAGE_PATH'] ) )->run(); 133 | } 134 | 135 | foreach ( $this->running_procs as $proc ) { 136 | self::terminate_proc( $proc ); 137 | } 138 | } 139 | 140 | /** 141 | * Terminate a process and any of its children. 142 | */ 143 | private static function terminate_proc( $proc ) { 144 | $status = proc_get_status( $proc ); 145 | 146 | $master_pid = $status['pid']; 147 | 148 | $output = `ps -o ppid,pid,command | grep $master_pid`; 149 | 150 | foreach ( explode( PHP_EOL, $output ) as $line ) { 151 | if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { 152 | $parent = $matches[1]; 153 | $child = $matches[2]; 154 | 155 | if ( $parent == $master_pid ) { 156 | if ( ! posix_kill( (int) $child, 9 ) ) { 157 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 158 | } 159 | } 160 | } 161 | } 162 | 163 | if ( ! posix_kill( (int) $master_pid, 9 ) ) { 164 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 165 | } 166 | } 167 | 168 | public static function create_cache_dir() { 169 | self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE ); 170 | mkdir( self::$suite_cache_dir ); 171 | return self::$suite_cache_dir; 172 | } 173 | 174 | /** 175 | * Initializes context. 176 | * Every scenario gets it's own context object. 177 | * 178 | * @param array $parameters context parameters (set them up through behat.yml) 179 | */ 180 | public function __construct( array $parameters ) { 181 | $this->drop_db(); 182 | $this->set_cache_dir(); 183 | $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); 184 | } 185 | 186 | public function getStepDefinitionResources() { 187 | return glob( __DIR__ . '/../steps/*.php' ); 188 | } 189 | 190 | public function getHookDefinitionResources() { 191 | return array(); 192 | } 193 | 194 | public function replace_variables( $str ) { 195 | return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); 196 | } 197 | 198 | private function _replace_var( $matches ) { 199 | $cmd = $matches[0]; 200 | 201 | foreach ( array_slice( $matches, 1 ) as $key ) { 202 | $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd ); 203 | } 204 | 205 | return $cmd; 206 | } 207 | 208 | public function create_run_dir() { 209 | if ( !isset( $this->variables['RUN_DIR'] ) ) { 210 | $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE ); 211 | mkdir( $this->variables['RUN_DIR'] ); 212 | } 213 | } 214 | 215 | public function build_phar( $version = 'same' ) { 216 | $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; 217 | 218 | $this->proc( Utils\esc_cmd( 219 | 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', 220 | __DIR__ . '/../../utils/make-phar.php', 221 | $this->variables['PHAR_PATH'], 222 | $version 223 | ) )->run_check(); 224 | } 225 | 226 | private function set_cache_dir() { 227 | $path = sys_get_temp_dir() . '/wp-cli-test-cache'; 228 | $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check(); 229 | $this->variables['CACHE_DIR'] = $path; 230 | } 231 | 232 | private static function run_sql( $sql ) { 233 | Utils\run_mysql_command( 'mysql --no-defaults', array( 234 | 'execute' => $sql, 235 | 'host' => self::$db_settings['dbhost'], 236 | 'user' => self::$db_settings['dbuser'], 237 | 'pass' => self::$db_settings['dbpass'], 238 | ) ); 239 | } 240 | 241 | public function create_db() { 242 | $dbname = self::$db_settings['dbname']; 243 | self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" ); 244 | } 245 | 246 | public function drop_db() { 247 | $dbname = self::$db_settings['dbname']; 248 | self::run_sql( "DROP DATABASE IF EXISTS $dbname" ); 249 | } 250 | 251 | public function proc( $command, $assoc_args = array(), $path = '' ) { 252 | if ( !empty( $assoc_args ) ) 253 | $command .= Utils\assoc_args_to_str( $assoc_args ); 254 | 255 | $env = self::get_process_env_variables(); 256 | if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { 257 | $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; 258 | } 259 | 260 | if ( isset( $this->variables['RUN_DIR'] ) ) { 261 | $cwd = "{$this->variables['RUN_DIR']}/{$path}"; 262 | } else { 263 | $cwd = null; 264 | } 265 | 266 | return Process::create( $command, $cwd, $env ); 267 | } 268 | 269 | /** 270 | * Start a background process. Will automatically be closed when the tests finish. 271 | */ 272 | public function background_proc( $cmd ) { 273 | $descriptors = array( 274 | 0 => STDIN, 275 | 1 => array( 'pipe', 'w' ), 276 | 2 => array( 'pipe', 'w' ), 277 | ); 278 | 279 | $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); 280 | 281 | sleep(1); 282 | 283 | $status = proc_get_status( $proc ); 284 | 285 | if ( !$status['running'] ) { 286 | throw new RuntimeException( stream_get_contents( $pipes[2] ) ); 287 | } else { 288 | $this->running_procs[] = $proc; 289 | } 290 | } 291 | 292 | public function move_files( $src, $dest ) { 293 | rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); 294 | } 295 | 296 | public function add_line_to_wp_config( &$wp_config_code, $line ) { 297 | $token = "/* That's all, stop editing!"; 298 | 299 | $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); 300 | } 301 | 302 | public function download_wp( $subdir = '' ) { 303 | $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; 304 | 305 | if ( $subdir ) { 306 | mkdir( $dest_dir ); 307 | } 308 | 309 | $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check(); 310 | 311 | // disable emailing 312 | mkdir( $dest_dir . '/wp-content/mu-plugins' ); 313 | copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); 314 | } 315 | 316 | public function create_config( $subdir = '' ) { 317 | $params = self::$db_settings; 318 | $params['dbprefix'] = $subdir ?: 'wp_'; 319 | 320 | $params['skip-salts'] = true; 321 | $this->proc( 'wp core config', $params, $subdir )->run_check(); 322 | } 323 | 324 | public function install_wp( $subdir = '' ) { 325 | $this->create_db(); 326 | $this->create_run_dir(); 327 | $this->download_wp( $subdir ); 328 | 329 | $this->create_config( $subdir ); 330 | 331 | $install_args = array( 332 | 'url' => 'http://example.com', 333 | 'title' => 'WP CLI Site', 334 | 'admin_user' => 'admin', 335 | 'admin_email' => 'admin@example.com', 336 | 'admin_password' => 'password1' 337 | ); 338 | 339 | $this->proc( 'wp core install', $install_args, $subdir )->run_check(); 340 | } 341 | } 342 | 343 | -------------------------------------------------------------------------------- /features/bootstrap/Process.php: -------------------------------------------------------------------------------- 1 | command = $command; 19 | $proc->cwd = $cwd; 20 | $proc->env = $env; 21 | 22 | return $proc; 23 | } 24 | 25 | private $command, $cwd, $env; 26 | 27 | private function __construct() {} 28 | 29 | /** 30 | * Run the command. 31 | * 32 | * @return ProcessRun 33 | */ 34 | public function run() { 35 | $cwd = $this->cwd; 36 | 37 | $descriptors = array( 38 | 0 => STDIN, 39 | 1 => array( 'pipe', 'w' ), 40 | 2 => array( 'pipe', 'w' ), 41 | ); 42 | 43 | $proc = proc_open( $this->command, $descriptors, $pipes, $cwd, $this->env ); 44 | 45 | $stdout = stream_get_contents( $pipes[1] ); 46 | fclose( $pipes[1] ); 47 | 48 | $stderr = stream_get_contents( $pipes[2] ); 49 | fclose( $pipes[2] ); 50 | 51 | return new ProcessRun( array( 52 | 'stdout' => $stdout, 53 | 'stderr' => $stderr, 54 | 'return_code' => proc_close( $proc ), 55 | 'command' => $this->command, 56 | 'cwd' => $cwd, 57 | 'env' => $this->env 58 | ) ); 59 | } 60 | 61 | /** 62 | * Run the command, but throw an Exception on error. 63 | * 64 | * @return ProcessRun 65 | */ 66 | public function run_check() { 67 | $r = $this->run(); 68 | 69 | if ( $r->return_code || !empty( $r->STDERR ) ) { 70 | throw new \RuntimeException( $r ); 71 | } 72 | 73 | return $r; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /features/bootstrap/ProcessRun.php: -------------------------------------------------------------------------------- 1 | $value ) { 15 | $this->$key = $value; 16 | } 17 | } 18 | 19 | /** 20 | * Return properties of executed command as a string. 21 | * 22 | * @return string 23 | */ 24 | public function __toString() { 25 | $out = "$ $this->command\n"; 26 | $out .= "$this->stdout\n$this->stderr"; 27 | $out .= "cwd: $this->cwd\n"; 28 | $out .= "exit status: $this->return_code"; 29 | 30 | return $out; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /features/bootstrap/support.php: -------------------------------------------------------------------------------- 1 | $value ) { 77 | if ( ! compareContents( $value, $actual->$name ) ) 78 | return false; 79 | } 80 | } else if ( is_array( $expected ) ) { 81 | foreach ( $expected as $key => $value ) { 82 | if ( ! compareContents( $value, $actual[$key] ) ) 83 | return false; 84 | } 85 | } else { 86 | return $expected === $actual; 87 | } 88 | 89 | return true; 90 | } 91 | 92 | /** 93 | * Compare two strings containing JSON to ensure that @a $actualJson contains at 94 | * least what the JSON string @a $expectedJson contains. 95 | * 96 | * @return whether or not @a $actualJson contains @a $expectedJson 97 | * @retval true @a $actualJson contains @a $expectedJson 98 | * @retval false @a $actualJson does not contain @a $expectedJson 99 | * 100 | * @param[in] $actualJson the JSON string to be tested 101 | * @param[in] $expectedJson the expected JSON string 102 | * 103 | * Examples: 104 | * expected: {'a':1,'array':[1,3,5]} 105 | * 106 | * 1 ) 107 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} 108 | * return: true 109 | * 110 | * 2 ) 111 | * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} 112 | * return: false 113 | * element 'a' is missing from the root object 114 | * 115 | * 3 ) 116 | * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} 117 | * return: false 118 | * the value of element 'a' is not 1 119 | * 120 | * 4 ) 121 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} 122 | * return: false 123 | * the contents of 'array' does not include 3 124 | */ 125 | function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { 126 | $actualValue = json_decode( $actualJson ); 127 | $expectedValue = json_decode( $expectedJson ); 128 | 129 | if ( !$actualValue ) { 130 | return false; 131 | } 132 | 133 | return compareContents( $expectedValue, $actualValue ); 134 | } 135 | 136 | /** 137 | * Compare two strings to confirm $actualCSV contains $expectedCSV 138 | * Both strings are expected to have headers for their CSVs. 139 | * $actualCSV must match all data rows in $expectedCSV 140 | * 141 | * @param string A CSV string 142 | * @param array A nested array of values 143 | * @return bool Whether $actualCSV contains $expectedCSV 144 | */ 145 | function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { 146 | $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); 147 | 148 | if ( empty( $actualCSV ) ) 149 | return false; 150 | 151 | // Each sample must have headers 152 | $actualHeaders = array_values( array_shift( $actualCSV ) ); 153 | $expectedHeaders = array_values( array_shift( $expectedCSV ) ); 154 | 155 | // Each expectedCSV must exist somewhere in actualCSV in the proper column 156 | $expectedResult = 0; 157 | foreach ( $expectedCSV as $expected_row ) { 158 | $expected_row = array_combine( $expectedHeaders, $expected_row ); 159 | foreach ( $actualCSV as $actual_row ) { 160 | 161 | if ( count( $actualHeaders ) != count( $actual_row ) ) 162 | continue; 163 | 164 | $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); 165 | if ( $actual_row == $expected_row ) 166 | $expectedResult++; 167 | } 168 | } 169 | 170 | return $expectedResult >= count( $expectedCSV ); 171 | } 172 | 173 | /** 174 | * Compare two strings containing YAML to ensure that @a $actualYaml contains at 175 | * least what the YAML string @a $expectedYaml contains. 176 | * 177 | * @return whether or not @a $actualYaml contains @a $expectedJson 178 | * @retval true @a $actualYaml contains @a $expectedJson 179 | * @retval false @a $actualYaml does not contain @a $expectedJson 180 | * 181 | * @param[in] $actualYaml the YAML string to be tested 182 | * @param[in] $expectedYaml the expected YAML string 183 | */ 184 | function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { 185 | $actualValue = spyc_load( $actualYaml ); 186 | $expectedValue = spyc_load( $expectedYaml ); 187 | 188 | if ( !$actualValue ) { 189 | return false; 190 | } 191 | 192 | return compareContents( $expectedValue, $actualValue ); 193 | } 194 | 195 | -------------------------------------------------------------------------------- /features/bootstrap/utils.php: -------------------------------------------------------------------------------- 1 | config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { 66 | array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); 67 | } 68 | } 69 | return $vendor_paths; 70 | } 71 | 72 | // Using require() directly inside a class grants access to private methods to the loaded code 73 | function load_file( $path ) { 74 | require_once $path; 75 | } 76 | 77 | function load_command( $name ) { 78 | $path = WP_CLI_ROOT . "/php/commands/$name.php"; 79 | 80 | if ( is_readable( $path ) ) { 81 | include_once $path; 82 | } 83 | } 84 | 85 | function load_all_commands() { 86 | $cmd_dir = WP_CLI_ROOT . '/php/commands'; 87 | 88 | $iterator = new \DirectoryIterator( $cmd_dir ); 89 | 90 | foreach ( $iterator as $filename ) { 91 | if ( '.php' != substr( $filename, -4 ) ) 92 | continue; 93 | 94 | include_once "$cmd_dir/$filename"; 95 | } 96 | } 97 | 98 | /** 99 | * Like array_map(), except it returns a new iterator, instead of a modified array. 100 | * 101 | * Example: 102 | * 103 | * $arr = array('Football', 'Socker'); 104 | * 105 | * $it = iterator_map($arr, 'strtolower', function($val) { 106 | * return str_replace('foo', 'bar', $val); 107 | * }); 108 | * 109 | * foreach ( $it as $val ) { 110 | * var_dump($val); 111 | * } 112 | * 113 | * @param array|object Either a plain array or another iterator 114 | * @param callback The function to apply to an element 115 | * @return object An iterator that applies the given callback(s) 116 | */ 117 | function iterator_map( $it, $fn ) { 118 | if ( is_array( $it ) ) { 119 | $it = new \ArrayIterator( $it ); 120 | } 121 | 122 | if ( !method_exists( $it, 'add_transform' ) ) { 123 | $it = new Transform( $it ); 124 | } 125 | 126 | foreach ( array_slice( func_get_args(), 1 ) as $fn ) { 127 | $it->add_transform( $fn ); 128 | } 129 | 130 | return $it; 131 | } 132 | 133 | /** 134 | * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true 135 | * @param string|array The files (or file) to search for 136 | * @param string|null The directory to start searching from; defaults to CWD 137 | * @param callable Function which is passed the current dir each time a directory level is traversed 138 | * @return null|string Null if the file was not found 139 | */ 140 | function find_file_upward( $files, $dir = null, $stop_check = null ) { 141 | $files = (array) $files; 142 | if ( is_null( $dir ) ) { 143 | $dir = getcwd(); 144 | } 145 | while ( @is_readable( $dir ) ) { 146 | // Stop walking up when the supplied callable returns true being passed the $dir 147 | if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { 148 | return null; 149 | } 150 | 151 | foreach ( $files as $file ) { 152 | $path = $dir . DIRECTORY_SEPARATOR . $file; 153 | if ( file_exists( $path ) ) { 154 | return $path; 155 | } 156 | } 157 | 158 | $parent_dir = dirname( $dir ); 159 | if ( empty($parent_dir) || $parent_dir === $dir ) { 160 | break; 161 | } 162 | $dir = $parent_dir; 163 | } 164 | return null; 165 | } 166 | 167 | function is_path_absolute( $path ) { 168 | // Windows 169 | if ( isset($path[1]) && ':' === $path[1] ) 170 | return true; 171 | 172 | return $path[0] === '/'; 173 | } 174 | 175 | /** 176 | * Composes positional arguments into a command string. 177 | * 178 | * @param array 179 | * @return string 180 | */ 181 | function args_to_str( $args ) { 182 | return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); 183 | } 184 | 185 | /** 186 | * Composes associative arguments into a command string. 187 | * 188 | * @param array 189 | * @return string 190 | */ 191 | function assoc_args_to_str( $assoc_args ) { 192 | $str = ''; 193 | 194 | foreach ( $assoc_args as $key => $value ) { 195 | if ( true === $value ) 196 | $str .= " --$key"; 197 | else 198 | $str .= " --$key=" . escapeshellarg( $value ); 199 | } 200 | 201 | return $str; 202 | } 203 | 204 | /** 205 | * Given a template string and an arbitrary number of arguments, 206 | * returns the final command, with the parameters escaped. 207 | */ 208 | function esc_cmd( $cmd ) { 209 | if ( func_num_args() < 2 ) 210 | trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); 211 | 212 | $args = func_get_args(); 213 | 214 | $cmd = array_shift( $args ); 215 | 216 | return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); 217 | } 218 | 219 | function locate_wp_config() { 220 | static $path; 221 | 222 | if ( null === $path ) { 223 | if ( file_exists( ABSPATH . 'wp-config.php' ) ) 224 | $path = ABSPATH . 'wp-config.php'; 225 | elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) 226 | $path = ABSPATH . '../wp-config.php'; 227 | else 228 | $path = false; 229 | 230 | if ( $path ) 231 | $path = realpath( $path ); 232 | } 233 | 234 | return $path; 235 | } 236 | 237 | function wp_version_compare( $since, $operator ) { 238 | return version_compare( str_replace( array( '-src' ), '', $GLOBALS['wp_version'] ), $since, $operator ); 239 | } 240 | 241 | /** 242 | * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. 243 | * 244 | * Given a collection of items with a consistent data structure: 245 | * 246 | * ``` 247 | * $items = array( 248 | * array( 249 | * 'key' => 'foo', 250 | * 'value' => 'bar', 251 | * ) 252 | * ); 253 | * ``` 254 | * 255 | * Render `$items` as an ASCII table: 256 | * 257 | * ``` 258 | * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); 259 | * 260 | * # +-----+-------+ 261 | * # | key | value | 262 | * # +-----+-------+ 263 | * # | foo | bar | 264 | * # +-----+-------+ 265 | * ``` 266 | * 267 | * Or render `$items` as YAML: 268 | * 269 | * ``` 270 | * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); 271 | * 272 | * # --- 273 | * # - 274 | * # key: foo 275 | * # value: bar 276 | * ``` 277 | * 278 | * @access public 279 | * @category Output 280 | * 281 | * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count' 282 | * @param array $items An array of items to output. 283 | * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. 284 | * @return null 285 | */ 286 | function format_items( $format, $items, $fields ) { 287 | $assoc_args = compact( 'format', 'fields' ); 288 | $formatter = new \WP_CLI\Formatter( $assoc_args ); 289 | $formatter->display_items( $items ); 290 | } 291 | 292 | /** 293 | * Write data as CSV to a given file. 294 | * 295 | * @access public 296 | * 297 | * @param resource $fd File descriptor 298 | * @param array $rows Array of rows to output 299 | * @param array $headers List of CSV columns (optional) 300 | */ 301 | function write_csv( $fd, $rows, $headers = array() ) { 302 | if ( ! empty( $headers ) ) { 303 | fputcsv( $fd, $headers ); 304 | } 305 | 306 | foreach ( $rows as $row ) { 307 | if ( ! empty( $headers ) ) { 308 | $row = pick_fields( $row, $headers ); 309 | } 310 | 311 | fputcsv( $fd, array_values( $row ) ); 312 | } 313 | } 314 | 315 | /** 316 | * Pick fields from an associative array or object. 317 | * 318 | * @param array|object Associative array or object to pick fields from 319 | * @param array List of fields to pick 320 | * @return array 321 | */ 322 | function pick_fields( $item, $fields ) { 323 | $item = (object) $item; 324 | 325 | $values = array(); 326 | 327 | foreach ( $fields as $field ) { 328 | $values[ $field ] = isset( $item->$field ) ? $item->$field : null; 329 | } 330 | 331 | return $values; 332 | } 333 | 334 | /** 335 | * Launch system's $EDITOR for the user to edit some text. 336 | * 337 | * @access public 338 | * @category Input 339 | * 340 | * @param string $content Some form of text to edit (e.g. post content) 341 | * @return string|bool Edited text, if file is saved from editor; false, if no change to file. 342 | */ 343 | function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { 344 | 345 | $tmpdir = get_temp_dir(); 346 | 347 | do { 348 | $tmpfile = basename( $filename ); 349 | $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); 350 | $tmpfile .= '-' . substr( md5( rand() ), 0, 6 ); 351 | $tmpfile = $tmpdir . $tmpfile . '.tmp'; 352 | $fp = @fopen( $tmpfile, 'x' ); 353 | if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { 354 | $tmpfile = ''; 355 | continue; 356 | } 357 | if ( $fp ) { 358 | fclose( $fp ); 359 | } 360 | } while( ! $tmpfile ); 361 | 362 | if ( ! $tmpfile ) { 363 | \WP_CLI::error( 'Error creating temporary file.' ); 364 | } 365 | 366 | $output = ''; 367 | file_put_contents( $tmpfile, $input ); 368 | 369 | $editor = getenv( 'EDITOR' ); 370 | if ( !$editor ) { 371 | if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) 372 | $editor = 'notepad'; 373 | else 374 | $editor = 'vi'; 375 | } 376 | 377 | $descriptorspec = array( STDIN, STDOUT, STDERR ); 378 | $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); 379 | $r = proc_close( $process ); 380 | if ( $r ) { 381 | exit( $r ); 382 | } 383 | 384 | $output = file_get_contents( $tmpfile ); 385 | 386 | unlink( $tmpfile ); 387 | 388 | if ( $output === $input ) 389 | return false; 390 | 391 | return $output; 392 | } 393 | 394 | /** 395 | * @param string MySQL host string, as defined in wp-config.php 396 | * @return array 397 | */ 398 | function mysql_host_to_cli_args( $raw_host ) { 399 | $assoc_args = array(); 400 | 401 | $host_parts = explode( ':', $raw_host ); 402 | if ( count( $host_parts ) == 2 ) { 403 | list( $assoc_args['host'], $extra ) = $host_parts; 404 | $extra = trim( $extra ); 405 | if ( is_numeric( $extra ) ) { 406 | $assoc_args['port'] = intval( $extra ); 407 | $assoc_args['protocol'] = 'tcp'; 408 | } else if ( $extra !== '' ) { 409 | $assoc_args['socket'] = $extra; 410 | } 411 | } else { 412 | $assoc_args['host'] = $raw_host; 413 | } 414 | 415 | return $assoc_args; 416 | } 417 | 418 | function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { 419 | if ( !$descriptors ) 420 | $descriptors = array( STDIN, STDOUT, STDERR ); 421 | 422 | if ( isset( $assoc_args['host'] ) ) { 423 | $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); 424 | } 425 | 426 | $pass = $assoc_args['pass']; 427 | unset( $assoc_args['pass'] ); 428 | 429 | $old_pass = getenv( 'MYSQL_PWD' ); 430 | putenv( 'MYSQL_PWD=' . $pass ); 431 | 432 | $final_cmd = $cmd . assoc_args_to_str( $assoc_args ); 433 | 434 | $proc = proc_open( $final_cmd, $descriptors, $pipes ); 435 | if ( !$proc ) 436 | exit(1); 437 | 438 | $r = proc_close( $proc ); 439 | 440 | putenv( 'MYSQL_PWD=' . $old_pass ); 441 | 442 | if ( $r ) exit( $r ); 443 | } 444 | 445 | /** 446 | * Render PHP or other types of files using Mustache templates. 447 | * 448 | * IMPORTANT: Automatic HTML escaping is disabled! 449 | */ 450 | function mustache_render( $template_name, $data = array() ) { 451 | if ( ! file_exists( $template_name ) ) 452 | $template_name = WP_CLI_ROOT . "/templates/$template_name"; 453 | 454 | $template = file_get_contents( $template_name ); 455 | 456 | $m = new \Mustache_Engine( array( 457 | 'escape' => function ( $val ) { return $val; } 458 | ) ); 459 | 460 | return $m->render( $template, $data ); 461 | } 462 | 463 | /** 464 | * Create a progress bar to display percent completion of a given operation. 465 | * 466 | * Progress bar is written to STDOUT, and disabled when command is piped. Progress 467 | * advances with `$progress->tick()`, and completes with `$progress->finish()`. 468 | * Process bar also indicates elapsed time and expected total time. 469 | * 470 | * ``` 471 | * # `wp user generate` ticks progress bar each time a new user is created. 472 | * # 473 | * # $ wp user generate --count=500 474 | * # Generating users 22 % [=======> ] 0:05 / 0:23 475 | * 476 | * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); 477 | * for ( $i = 0; $i < $count; $i++ ) { 478 | * // uses wp_insert_user() to insert the user 479 | * $progress->tick(); 480 | * } 481 | * $progress->finish(); 482 | * ``` 483 | * 484 | * @access public 485 | * @category Output 486 | * 487 | * @param string $message Text to display before the progress bar. 488 | * @param integer $count Total number of ticks to be performed. 489 | * @return cli\progress\Bar|WP_CLI\NoOp 490 | */ 491 | function make_progress_bar( $message, $count ) { 492 | if ( \cli\Shell::isPiped() ) 493 | return new \WP_CLI\NoOp; 494 | 495 | return new \cli\progress\Bar( $message, $count ); 496 | } 497 | 498 | function parse_url( $url ) { 499 | $url_parts = \parse_url( $url ); 500 | 501 | if ( !isset( $url_parts['scheme'] ) ) { 502 | $url_parts = parse_url( 'http://' . $url ); 503 | } 504 | 505 | return $url_parts; 506 | } 507 | 508 | /** 509 | * Check if we're running in a Windows environment (cmd.exe). 510 | */ 511 | function is_windows() { 512 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 513 | } 514 | 515 | /** 516 | * Replace magic constants in some PHP source code. 517 | * 518 | * @param string $source The PHP code to manipulate. 519 | * @param string $path The path to use instead of the magic constants 520 | */ 521 | function replace_path_consts( $source, $path ) { 522 | $replacements = array( 523 | '__FILE__' => "'$path'", 524 | '__DIR__' => "'" . dirname( $path ) . "'" 525 | ); 526 | 527 | $old = array_keys( $replacements ); 528 | $new = array_values( $replacements ); 529 | 530 | return str_replace( $old, $new, $source ); 531 | } 532 | 533 | /** 534 | * Make a HTTP request to a remote URL. 535 | * 536 | * Wraps the Requests HTTP library to ensure every request includes a cert. 537 | * 538 | * ``` 539 | * # `wp core download` verifies the hash for a downloaded WordPress archive 540 | * 541 | * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); 542 | * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { 543 | * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); 544 | * } 545 | * ``` 546 | * 547 | * @access public 548 | * 549 | * @param string $method HTTP method (GET, POST, DELETE, etc.) 550 | * @param string $url URL to make the HTTP request to. 551 | * @param array $headers Add specific headers to the request. 552 | * @param array $options 553 | * @return object 554 | */ 555 | function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { 556 | 557 | $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; 558 | if ( inside_phar() ) { 559 | // cURL can't read Phar archives 560 | $options['verify'] = extract_from_phar( 561 | WP_CLI_ROOT . '/vendor' . $cert_path ); 562 | } else { 563 | foreach( get_vendor_paths() as $vendor_path ) { 564 | if ( file_exists( $vendor_path . $cert_path ) ) { 565 | $options['verify'] = $vendor_path . $cert_path; 566 | break; 567 | } 568 | } 569 | if ( empty( $options['verify'] ) ){ 570 | WP_CLI::error_log( "Cannot find SSL certificate." ); 571 | } 572 | } 573 | 574 | try { 575 | $request = \Requests::request( $url, $headers, $data, $method, $options ); 576 | return $request; 577 | } catch( \Requests_Exception $ex ) { 578 | // Handle SSL certificate issues gracefully 579 | \WP_CLI::warning( $ex->getMessage() ); 580 | $options['verify'] = false; 581 | try { 582 | return \Requests::request( $url, $headers, $data, $method, $options ); 583 | } catch( \Requests_Exception $ex ) { 584 | \WP_CLI::error( $ex->getMessage() ); 585 | } 586 | } 587 | } 588 | 589 | /** 590 | * Increments a version string using the "x.y.z-pre" format 591 | * 592 | * Can increment the major, minor or patch number by one 593 | * If $new_version == "same" the version string is not changed 594 | * If $new_version is not a known keyword, it will be used as the new version string directly 595 | * 596 | * @param string $current_version 597 | * @param string $new_version 598 | * @return string 599 | */ 600 | function increment_version( $current_version, $new_version ) { 601 | // split version assuming the format is x.y.z-pre 602 | $current_version = explode( '-', $current_version, 2 ); 603 | $current_version[0] = explode( '.', $current_version[0] ); 604 | 605 | switch ( $new_version ) { 606 | case 'same': 607 | // do nothing 608 | break; 609 | 610 | case 'patch': 611 | $current_version[0][2]++; 612 | 613 | $current_version = array( $current_version[0] ); // drop possible pre-release info 614 | break; 615 | 616 | case 'minor': 617 | $current_version[0][1]++; 618 | $current_version[0][2] = 0; 619 | 620 | $current_version = array( $current_version[0] ); // drop possible pre-release info 621 | break; 622 | 623 | case 'major': 624 | $current_version[0][0]++; 625 | $current_version[0][1] = 0; 626 | $current_version[0][2] = 0; 627 | 628 | $current_version = array( $current_version[0] ); // drop possible pre-release info 629 | break; 630 | 631 | default: // not a keyword 632 | $current_version = array( array( $new_version ) ); 633 | break; 634 | } 635 | 636 | // reconstruct version string 637 | $current_version[0] = implode( '.', $current_version[0] ); 638 | $current_version = implode( '-', $current_version ); 639 | 640 | return $current_version; 641 | } 642 | 643 | /** 644 | * Compare two version strings to get the named semantic version. 645 | * 646 | * @access public 647 | * 648 | * @param string $new_version 649 | * @param string $original_version 650 | * @return string $name 'major', 'minor', 'patch' 651 | */ 652 | function get_named_sem_ver( $new_version, $original_version ) { 653 | 654 | if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { 655 | return ''; 656 | } 657 | 658 | $parts = explode( '-', $original_version ); 659 | $bits = explode( '.', $parts[0] ); 660 | $major = $bits[0]; 661 | if ( isset( $bits[1] ) ) { 662 | $minor = $bits[1]; 663 | } 664 | if ( isset( $bits[2] ) ) { 665 | $patch = $bits[2]; 666 | } 667 | 668 | if ( ! is_null( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { 669 | return 'patch'; 670 | } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { 671 | return 'minor'; 672 | } else { 673 | return 'major'; 674 | } 675 | } 676 | 677 | /** 678 | * Return the flag value or, if it's not set, the $default value. 679 | * 680 | * Because flags can be negated (e.g. --no-quiet to negate --quiet), this 681 | * function provides a safer alternative to using 682 | * `isset( $assoc_args['quiet'] )` or similar. 683 | * 684 | * @access public 685 | * @category Input 686 | * 687 | * @param array $assoc_args Arguments array. 688 | * @param string $flag Flag to get the value. 689 | * @param mixed $default Default value for the flag. Default: NULL 690 | * @return mixed 691 | */ 692 | function get_flag_value( $assoc_args, $flag, $default = null ) { 693 | return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; 694 | } 695 | 696 | /** 697 | * Get the system's temp directory. Warns user if it isn't writable. 698 | * 699 | * @access public 700 | * @category System 701 | * 702 | * @return string 703 | */ 704 | function get_temp_dir() { 705 | static $temp = ''; 706 | 707 | $trailingslashit = function( $path ) { 708 | return rtrim( $path ) . '/'; 709 | }; 710 | 711 | if ( $temp ) 712 | return $trailingslashit( $temp ); 713 | 714 | if ( function_exists( 'sys_get_temp_dir' ) ) { 715 | $temp = sys_get_temp_dir(); 716 | } else if ( ini_get( 'upload_tmp_dir' ) ) { 717 | $temp = ini_get( 'upload_tmp_dir' ); 718 | } else { 719 | $temp = '/tmp/'; 720 | } 721 | 722 | if ( ! @is_writable( $temp ) ) { 723 | \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); 724 | } 725 | 726 | return $trailingslashit( $temp ); 727 | } 728 | 729 | /** 730 | * Parse a SSH url for its host, port, and path. 731 | * 732 | * Similar to parse_url(), but adds support for defined SSH aliases. 733 | * 734 | * ``` 735 | * host OR host/path/to/wordpress OR host:port/path/to/wordpress 736 | * ``` 737 | * 738 | * @access public 739 | * 740 | * @return mixed 741 | */ 742 | function parse_ssh_url( $url, $component = -1 ) { 743 | preg_match( '#^([^:/~]+)(:([\d]+))?((/|~)(.+))?$#', $url, $matches ); 744 | $bits = array(); 745 | foreach( array( 746 | 1 => 'host', 747 | 3 => 'port', 748 | 4 => 'path', 749 | ) as $i => $key ) { 750 | if ( ! empty( $matches[ $i ] ) ) { 751 | $bits[ $key ] = $matches[ $i ]; 752 | } 753 | } 754 | switch ( $component ) { 755 | case PHP_URL_HOST: 756 | return isset( $bits['host'] ) ? $bits['host'] : null; 757 | case PHP_URL_PATH: 758 | return isset( $bits['path'] ) ? $bits['path'] : null; 759 | case PHP_URL_PORT: 760 | return isset( $bits['port'] ) ? $bits['port'] : null; 761 | default: 762 | return $bits; 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /features/extra/no-mail.php: -------------------------------------------------------------------------------- 1 | Given( '/^an empty directory$/', 8 | function ( $world ) { 9 | $world->create_run_dir(); 10 | } 11 | ); 12 | 13 | $steps->Given( '/^an empty cache/', 14 | function ( $world ) { 15 | $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); 16 | } 17 | ); 18 | 19 | $steps->Given( '/^an? ([^\s]+) file:$/', 20 | function ( $world, $path, PyStringNode $content ) { 21 | $content = (string) $content . "\n"; 22 | $full_path = $world->variables['RUN_DIR'] . "/$path"; 23 | Process::create( \WP_CLI\utils\esc_cmd( 'mkdir -p %s', dirname( $full_path ) ) )->run_check(); 24 | file_put_contents( $full_path, $content ); 25 | } 26 | ); 27 | 28 | $steps->Given( '/^WP files$/', 29 | function ( $world ) { 30 | $world->download_wp(); 31 | } 32 | ); 33 | 34 | $steps->Given( '/^wp-config\.php$/', 35 | function ( $world ) { 36 | $world->create_config(); 37 | } 38 | ); 39 | 40 | $steps->Given( '/^a database$/', 41 | function ( $world ) { 42 | $world->create_db(); 43 | } 44 | ); 45 | 46 | $steps->Given( '/^a WP install$/', 47 | function ( $world ) { 48 | $world->install_wp(); 49 | } 50 | ); 51 | 52 | $steps->Given( "/^a WP install in '([^\s]+)'$/", 53 | function ( $world, $subdir ) { 54 | $world->install_wp( $subdir ); 55 | } 56 | ); 57 | 58 | $steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?install$/', 59 | function ( $world, $type = 'subdirectory' ) { 60 | $world->install_wp(); 61 | $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0; 62 | $world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check(); 63 | } 64 | ); 65 | 66 | $steps->Given( '/^these installed and active plugins:$/', 67 | function( $world, $stream ) { 68 | $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) ); 69 | $world->proc( "wp plugin install $plugins --activate" )->run_check(); 70 | } 71 | ); 72 | 73 | $steps->Given( '/^a custom wp-content directory$/', 74 | function ( $world ) { 75 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; 76 | 77 | $wp_config_code = file_get_contents( $wp_config_path ); 78 | 79 | $world->move_files( 'wp-content', 'my-content' ); 80 | $world->add_line_to_wp_config( $wp_config_code, 81 | "define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" ); 82 | 83 | $world->move_files( 'my-content/plugins', 'my-plugins' ); 84 | $world->add_line_to_wp_config( $wp_config_code, 85 | "define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" ); 86 | 87 | file_put_contents( $wp_config_path, $wp_config_code ); 88 | } 89 | ); 90 | 91 | $steps->Given( '/^download:$/', 92 | function ( $world, TableNode $table ) { 93 | foreach ( $table->getHash() as $row ) { 94 | $path = $world->replace_variables( $row['path'] ); 95 | if ( file_exists( $path ) ) { 96 | // assume it's the same file and skip re-download 97 | continue; 98 | } 99 | 100 | Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); 101 | } 102 | } 103 | ); 104 | 105 | $steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?as \{(\w+)\}$/', 106 | function ( $world, $stream, $output_filter, $key ) { 107 | 108 | $stream = strtolower( $stream ); 109 | 110 | if ( $output_filter ) { 111 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; 112 | if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) ) 113 | $output = array_pop( $matches ); 114 | else 115 | $output = ''; 116 | } else { 117 | $output = $world->result->$stream; 118 | } 119 | $world->variables[ $key ] = trim( $output, "\n" ); 120 | } 121 | ); 122 | 123 | $steps->Given( '/^a new Phar(?: with version "([^"]+)")$/', 124 | function ( $world, $version ) { 125 | $world->build_phar( $version ); 126 | } 127 | ); 128 | 129 | $steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/', 130 | function ( $world, $filepath, $output_filter, $key ) { 131 | $full_file = file_get_contents( $world->replace_variables( $filepath ) ); 132 | 133 | if ( $output_filter ) { 134 | $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; 135 | if ( false !== preg_match( $output_filter, $full_file, $matches ) ) 136 | $output = array_pop( $matches ); 137 | else 138 | $output = ''; 139 | } else { 140 | $output = $full_file; 141 | } 142 | $world->variables[ $key ] = trim( $output, "\n" ); 143 | } 144 | ); 145 | 146 | $steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/', 147 | function($world) { 148 | $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; 149 | 150 | $wp_config_code = file_get_contents( $wp_config_path ); 151 | 152 | $world->add_line_to_wp_config( $wp_config_code, 153 | "define( 'WP_CONTENT_DIR', '' );" ); 154 | 155 | file_put_contents( $wp_config_path, $wp_config_code ); 156 | } 157 | ); -------------------------------------------------------------------------------- /features/steps/then.php: -------------------------------------------------------------------------------- 1 | Then( '/^the return code should be (\d+)$/', 7 | function ( $world, $return_code ) { 8 | if ( $return_code != $world->result->return_code ) { 9 | throw new RuntimeException( $world->result ); 10 | } 11 | } 12 | ); 13 | 14 | $steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/', 15 | function ( $world, $stream, $action, PyStringNode $expected ) { 16 | 17 | $stream = strtolower( $stream ); 18 | 19 | $expected = $world->replace_variables( (string) $expected ); 20 | 21 | checkString( $world->result->$stream, $expected, $action, $world->result ); 22 | } 23 | ); 24 | 25 | $steps->Then( '/^(STDOUT|STDERR) should be a number$/', 26 | function ( $world, $stream ) { 27 | 28 | $stream = strtolower( $stream ); 29 | 30 | assertNumeric( trim( $world->result->$stream, "\n" ) ); 31 | } 32 | ); 33 | 34 | $steps->Then( '/^(STDOUT|STDERR) should not be a number$/', 35 | function ( $world, $stream ) { 36 | 37 | $stream = strtolower( $stream ); 38 | 39 | assertNotNumeric( trim( $world->result->$stream, "\n" ) ); 40 | } 41 | ); 42 | 43 | $steps->Then( '/^STDOUT should be a table containing rows:$/', 44 | function ( $world, TableNode $expected ) { 45 | $output = $world->result->stdout; 46 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); 47 | 48 | $expected_rows = array(); 49 | foreach ( $expected->getRows() as $row ) { 50 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); 51 | } 52 | 53 | compareTables( $expected_rows, $actual_rows, $output ); 54 | } 55 | ); 56 | 57 | $steps->Then( '/^STDOUT should end with a table containing rows:$/', 58 | function ( $world, TableNode $expected ) { 59 | $output = $world->result->stdout; 60 | $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); 61 | 62 | $expected_rows = array(); 63 | foreach ( $expected->getRows() as $row ) { 64 | $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); 65 | } 66 | 67 | $start = array_search( $expected_rows[0], $actual_rows ); 68 | 69 | if ( false === $start ) 70 | throw new \Exception( $world->result ); 71 | 72 | compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output ); 73 | } 74 | ); 75 | 76 | $steps->Then( '/^STDOUT should be JSON containing:$/', 77 | function ( $world, PyStringNode $expected ) { 78 | $output = $world->result->stdout; 79 | $expected = $world->replace_variables( (string) $expected ); 80 | 81 | if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) { 82 | throw new \Exception( $world->result ); 83 | } 84 | }); 85 | 86 | $steps->Then( '/^STDOUT should be a JSON array containing:$/', 87 | function ( $world, PyStringNode $expected ) { 88 | $output = $world->result->stdout; 89 | $expected = $world->replace_variables( (string) $expected ); 90 | 91 | $actualValues = json_decode( $output ); 92 | $expectedValues = json_decode( $expected ); 93 | 94 | $missing = array_diff( $expectedValues, $actualValues ); 95 | if ( !empty( $missing ) ) { 96 | throw new \Exception( $world->result ); 97 | } 98 | }); 99 | 100 | $steps->Then( '/^STDOUT should be CSV containing:$/', 101 | function ( $world, TableNode $expected ) { 102 | $output = $world->result->stdout; 103 | 104 | $expected_rows = $expected->getRows(); 105 | foreach ( $expected as &$row ) { 106 | foreach ( $row as &$value ) { 107 | $value = $world->replace_variables( $value ); 108 | } 109 | } 110 | 111 | if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) ) 112 | throw new \Exception( $world->result ); 113 | } 114 | ); 115 | 116 | $steps->Then( '/^STDOUT should be YAML containing:$/', 117 | function ( $world, PyStringNode $expected ) { 118 | $output = $world->result->stdout; 119 | $expected = $world->replace_variables( (string) $expected ); 120 | 121 | if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) { 122 | throw new \Exception( $world->result ); 123 | } 124 | }); 125 | 126 | $steps->Then( '/^(STDOUT|STDERR) should be empty$/', 127 | function ( $world, $stream ) { 128 | 129 | $stream = strtolower( $stream ); 130 | 131 | if ( !empty( $world->result->$stream ) ) { 132 | throw new \Exception( $world->result ); 133 | } 134 | } 135 | ); 136 | 137 | $steps->Then( '/^(STDOUT|STDERR) should not be empty$/', 138 | function ( $world, $stream ) { 139 | 140 | $stream = strtolower( $stream ); 141 | 142 | if ( '' === rtrim( $world->result->$stream, "\n" ) ) { 143 | throw new Exception( $world->result ); 144 | } 145 | } 146 | ); 147 | 148 | $steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/', 149 | function ( $world, $path, $type, $action, $expected = null ) { 150 | $path = $world->replace_variables( $path ); 151 | 152 | // If it's a relative path, make it relative to the current test dir 153 | if ( '/' !== $path[0] ) 154 | $path = $world->variables['RUN_DIR'] . "/$path"; 155 | 156 | if ( 'file' == $type ) { 157 | $test = 'file_exists'; 158 | } else if ( 'directory' == $type ) { 159 | $test = 'is_dir'; 160 | } 161 | 162 | switch ( $action ) { 163 | case 'exist': 164 | if ( ! $test( $path ) ) { 165 | throw new Exception( $world->result ); 166 | } 167 | break; 168 | case 'not exist': 169 | if ( $test( $path ) ) { 170 | throw new Exception( $world->result ); 171 | } 172 | break; 173 | default: 174 | if ( ! $test( $path ) ) { 175 | throw new Exception( "$path doesn't exist." ); 176 | } 177 | $action = substr( $action, 0, -1 ); 178 | $expected = $world->replace_variables( (string) $expected ); 179 | if ( 'file' == $type ) { 180 | $contents = file_get_contents( $path ); 181 | } else if ( 'directory' == $type ) { 182 | $files = glob( rtrim( $path, '/' ) . '/*' ); 183 | foreach( $files as &$file ) { 184 | $file = str_replace( $path . '/', '', $file ); 185 | } 186 | $contents = implode( PHP_EOL, $files ); 187 | } 188 | checkString( $contents, $expected, $action ); 189 | } 190 | } 191 | ); 192 | 193 | $steps->Then( '/^an email should (be sent|not be sent)$/', function( $world, $expected ) { 194 | if ( 'be sent' === $expected ) { 195 | assertNotEquals( 0, $world->email_sends ); 196 | } else if ( 'not be sent' === $expected ) { 197 | assertEquals( 0, $world->email_sends ); 198 | } else { 199 | throw new Exception( 'Invalid expectation' ); 200 | } 201 | }); 202 | -------------------------------------------------------------------------------- /features/steps/when.php: -------------------------------------------------------------------------------- 1 | 'run_check', 10 | 'try' => 'run' 11 | ); 12 | $method = $map[ $mode ]; 13 | 14 | return $proc->$method(); 15 | } 16 | 17 | function capture_email_sends( $stdout ) { 18 | $stdout = preg_replace( '#WP-CLI test suite: Sent email to.+\n?#', '', $stdout, -1, $email_sends ); 19 | return array( $stdout, $email_sends ); 20 | } 21 | 22 | $steps->When( '/^I launch in the background `([^`]+)`$/', 23 | function ( $world, $cmd ) { 24 | $world->background_proc( $cmd ); 25 | } 26 | ); 27 | 28 | $steps->When( '/^I (run|try) `([^`]+)`$/', 29 | function ( $world, $mode, $cmd ) { 30 | $cmd = $world->replace_variables( $cmd ); 31 | $world->result = invoke_proc( $world->proc( $cmd ), $mode ); 32 | list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); 33 | } 34 | ); 35 | 36 | $steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/", 37 | function ( $world, $mode, $cmd, $subdir ) { 38 | $cmd = $world->replace_variables( $cmd ); 39 | $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode ); 40 | list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); 41 | } 42 | ); 43 | 44 | $steps->When( '/^I (run|try) the previous command again$/', 45 | function ( $world, $mode ) { 46 | if ( !isset( $world->result ) ) 47 | throw new \Exception( 'No previous command.' ); 48 | 49 | $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env ); 50 | $world->result = invoke_proc( $proc, $mode ); 51 | list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); 52 | } 53 | ); 54 | 55 | -------------------------------------------------------------------------------- /utils/behat-tags.php: -------------------------------------------------------------------------------- 1 | ' ) 38 | ); 39 | 40 | # Skip Github API tests by default because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612 41 | $skip_tags[] = '@github-api'; 42 | 43 | if ( !empty( $skip_tags ) ) { 44 | echo '--tags=~' . implode( '&&~', $skip_tags ); 45 | } 46 | 47 | -------------------------------------------------------------------------------- /utils/get-package-require-from-composer.php: -------------------------------------------------------------------------------- 1 | autoload->files ) ) { 18 | echo 'composer.json must specify valid "autoload" => "files"'; 19 | exit(1); 20 | } 21 | 22 | echo implode( PHP_EOL, $composer->autoload->files ); 23 | exit(0); -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - command.php 3 | --------------------------------------------------------------------------------