├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── bin └── install-package-tests.sh ├── command.php ├── composer.json ├── features ├── bootstrap │ ├── FeatureContext.php │ ├── Process.php │ ├── support.php │ └── utils.php ├── db-ack.feature ├── extra │ └── no-mail.php └── steps │ ├── given.php │ ├── then.php │ └── when.php └── utils ├── behat-tags.php └── get-package-require-from-composer.php /.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 | indent_size = 4 16 | 17 | [{.jshintrc,*.json,*.yml}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [{*.txt,wp-config-sample.php}] 22 | end_of_line = crlf 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | wp-cli.local.yml 3 | node_modules/ 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | php: 13 | - 5.3 14 | - 5.6 15 | 16 | env: 17 | global: 18 | - WP_CLI_BIN_DIR=/tmp/wp-cli-phar 19 | - WP_CLI_CONFIG_PATH=/tmp/wp-cli-phar/config.yml 20 | 21 | before_script: 22 | - bash bin/install-package-tests.sh 23 | 24 | script: ./vendor/bin/behat 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | runcommand/db-ack 2 | ================= 3 | 4 | # DEPRECATED 5 | 6 | You should use `wp db search` instead: https://developer.wordpress.org/cli/commands/db/search/ 7 | 8 | # DEPRECATED 9 | 10 | Find a specific string in the database. 11 | 12 | [![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/db-ack.svg?branch=master)](https://travis-ci.org/runcommand/db-ack) 13 | 14 | Quick links: [Using](#using) | [Installing](#installing) | [Support](#support) 15 | 16 | ## Using 17 | 18 | ~~~ 19 | wp db ack [...] [--network] [--all-tables-with-prefix] [--all-tables] [--before_context=] [--after_context=] 20 | ~~~ 21 | 22 | Like [ack](http://beyondgrep.com/), but for your WordPress database. 23 | Searches through all or a selection of database tables for a given 24 | string. Outputs colorized references to the string. 25 | 26 | ![Example of search for 'wordpress-development'](https://cloud.githubusercontent.com/assets/36432/14318557/4577836a-fbc2-11e5-9b2d-1c84f03a7c02.png) 27 | 28 | Defaults to searching through all tables registered to `$wpdb`. On 29 | multisite, this default is limited to the tables for the current site. 30 | 31 | **OPTIONS** 32 | 33 | 34 | String to search for. 35 | 36 | [...] 37 | One or more tables to search through for the string. 38 | 39 | [--network] 40 | Search through all the tables registered to $wpdb in a multisite 41 | install. 42 | 43 | [--all-tables-with-prefix] 44 | Search through all tables that match the registered table prefix, even 45 | if not registered on $wpdb. On one hand, sometimes plugins use tables 46 | without registering them to $wpdb. On another hand, this could return 47 | tables you don't expect. 48 | 49 | [--all-tables] 50 | Search through ALL tables in the database, regardless of the prefix, 51 | and even if not registered on $wpdb. Overrides --network and 52 | --all-tables-with-prefix. 53 | 54 | [--before_context=] 55 | Number of characters to display before the match (for large blobs). 56 | --- 57 | default: 40 58 | --- 59 | 60 | [--after_context=] 61 | Number of characters to display after the match (for large blobs). 62 | --- 63 | default: 40 64 | --- 65 | 66 | **EXAMPLES** 67 | 68 | # Search through database for the 'wordpress-develop' string 69 | $ wp db ack wordpress-develop 70 | wp_options:option_value 71 | 1:http://wordpress-develop.dev 72 | wp_options:option_value 73 | 2:http://wordpress-develop.dev 74 | 75 | ## Installing 76 | 77 | Installing this package requires WP-CLI v0.23.0 or greater. Update to the latest stable release with `wp cli update`. 78 | 79 | Once you've done so, you can install this package with `wp package install runcommand/db-ack`. 80 | 81 | ## Support 82 | 83 | 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/). 84 | 85 | 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. 86 | 87 | 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/). 88 | 89 | runcommand customers can also email [support@runcommand.io](mailto:support@runcommand.io) for private support. 90 | 91 | 92 | -------------------------------------------------------------------------------- /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 | set_package_context() { 25 | 26 | touch $WP_CLI_CONFIG_PATH 27 | printf 'require:' > $WP_CLI_CONFIG_PATH 28 | requires=$(php $PACKAGE_DIR/utils/get-package-require-from-composer.php composer.json) 29 | for require in "${requires[@]}" 30 | do 31 | printf "\n%2s-%1s$PACKAGE_DIR/$require" >> $WP_CLI_CONFIG_PATH 32 | done 33 | printf "\n" >> $WP_CLI_CONFIG_PATH 34 | 35 | } 36 | 37 | download_behat() { 38 | 39 | cd $PACKAGE_DIR 40 | download https://getcomposer.org/installer installer 41 | php installer 42 | php composer.phar require --dev behat/behat='~2.5' 43 | 44 | } 45 | 46 | install_db() { 47 | mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot 48 | mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot 49 | } 50 | 51 | install_wp_cli 52 | set_package_context 53 | download_behat 54 | install_db 55 | -------------------------------------------------------------------------------- /command.php: -------------------------------------------------------------------------------- 1 | 24 | * : String to search for. 25 | * 26 | * [...] 27 | * : One or more tables to search through for the string. 28 | * 29 | * [--network] 30 | * : Search through all the tables registered to $wpdb in a multisite 31 | * install. 32 | * 33 | * [--all-tables-with-prefix] 34 | * : Search through all tables that match the registered table prefix, even 35 | * if not registered on $wpdb. On one hand, sometimes plugins use tables 36 | * without registering them to $wpdb. On another hand, this could return 37 | * tables you don't expect. 38 | * 39 | * [--all-tables] 40 | * : Search through ALL tables in the database, regardless of the prefix, 41 | * and even if not registered on $wpdb. Overrides --network and 42 | * --all-tables-with-prefix. 43 | * 44 | * [--before_context=] 45 | * : Number of characters to display before the match (for large blobs). 46 | * --- 47 | * default: 40 48 | * --- 49 | * 50 | * [--after_context=] 51 | * : Number of characters to display after the match (for large blobs). 52 | * --- 53 | * default: 40 54 | * --- 55 | * 56 | * ## EXAMPLES 57 | * 58 | * # Search through database for the 'wordpress-develop' string 59 | * $ wp db ack wordpress-develop 60 | * wp_options:option_value 61 | * 1:http://wordpress-develop.dev 62 | * wp_options:option_value 63 | * 2:http://wordpress-develop.dev 64 | */ 65 | public function __invoke( $args, $assoc_args ) { 66 | global $wpdb; 67 | 68 | $search = array_shift( $args ); 69 | $before_context = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before_context', 40 ); 70 | $after_context = \WP_CLI\Utils\get_flag_value( $assoc_args, 'after_context', 40 ); 71 | 72 | // Avoid constant redefinition in wp-config 73 | @WP_CLI::get_runner()->load_wordpress(); 74 | 75 | $tables = WP_CLI\Utils\wp_get_table_names( $args, $assoc_args ); 76 | foreach( $tables as $table ) { 77 | list( $primary_keys, $text_columns, $all_columns ) = self::get_columns( $table ); 78 | $primary_key = array_shift( $primary_keys ); 79 | foreach( $text_columns as $column ) { 80 | $results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_key}, {$column} FROM {$table} WHERE {$column} LIKE %s;", '%' . self::esc_like( $search ) . '%' ) ); 81 | foreach( $results as $result ) { 82 | WP_CLI::log( WP_CLI::colorize( "%G{$table}:{$column}%n" ) ); 83 | $pk_val = WP_CLI::colorize( '%Y' . $result->$primary_key . '%n' ); 84 | $col_val = $result->$column; 85 | $safe_search = preg_quote( $search, '#' ); 86 | $before_context = '' === $before_context ? $before_context : (int) $before_context; 87 | $after_context = '' === $after_context ? $after_context : (int) $after_context; 88 | $search_regex = '#(.{0,' . $before_context . '})(' . $safe_search .')(.{0,' . $after_context . '})#i'; 89 | preg_match_all( $search_regex , $col_val, $matches ); 90 | $bits = array(); 91 | foreach( $matches[0] as $key => $value ) { 92 | $bits[] = $matches[1][ $key ] . WP_CLI::colorize( '%3%k' . $matches[2][ $key ] . '%n' ) . $matches[3][ $key ]; 93 | } 94 | $col_val = implode( ' [...] ', $bits ); 95 | WP_CLI::log( "{$pk_val}:{$col_val}" ); 96 | } 97 | } 98 | } 99 | 100 | } 101 | 102 | private static function get_columns( $table ) { 103 | global $wpdb; 104 | 105 | $primary_keys = $text_columns = $all_columns = array(); 106 | foreach ( $wpdb->get_results( "DESCRIBE $table" ) as $col ) { 107 | if ( 'PRI' === $col->Key ) { 108 | $primary_keys[] = $col->Field; 109 | } 110 | if ( self::is_text_col( $col->Type ) ) { 111 | $text_columns[] = $col->Field; 112 | } 113 | $all_columns[] = $col->Field; 114 | } 115 | return array( $primary_keys, $text_columns, $all_columns ); 116 | } 117 | 118 | private static function esc_like( $old ) { 119 | global $wpdb; 120 | 121 | // Remove notices in 4.0 and support backwards compatibility 122 | if( method_exists( $wpdb, 'esc_like' ) ) { 123 | // 4.0 124 | $old = $wpdb->esc_like( $old ); 125 | } else { 126 | // 3.9 or less 127 | $old = like_escape( esc_sql( $old ) ); 128 | } 129 | 130 | return $old; 131 | } 132 | 133 | private static function is_text_col( $type ) { 134 | foreach ( array( 'text', 'varchar' ) as $token ) { 135 | if ( false !== strpos( $type, $token ) ) 136 | return true; 137 | } 138 | 139 | return false; 140 | } 141 | 142 | } 143 | WP_CLI::add_command( 'db ack', 'Run_DB_Ack_Command' ); 144 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runcommand/db-ack", 3 | "description": "Find a specific string in the database.", 4 | "type": "wp-cli-package", 5 | "homepage": "https://runcommand.io/wp/db-ack/", 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 | "extra": { 20 | "commands": [ 21 | "db ack" 22 | ], 23 | "readme": { 24 | "shields": [ 25 | "[![runcommand open source](https://runcommand.io/wp-content/themes/runcommand-theme/bin/shields/runcommand-open-source.svg)](https://runcommand.io/pricing/)", 26 | "[![Build Status](https://travis-ci.org/runcommand/db-ack.svg?branch=master)](https://travis-ci.org/runcommand/db-ack)" 27 | ], 28 | "sections": [ 29 | "Using", 30 | "Installing", 31 | "Support" 32 | ], 33 | "support": { 34 | "body": "https://raw.githubusercontent.com/runcommand/runcommand-theme/master/bin/readme-partials/support-open-source.md" 35 | }, 36 | "show_powered_by": false 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | autoload->files ) ) { 19 | $contents = 'require:' . PHP_EOL; 20 | foreach( $composer->autoload->files as $file ) { 21 | $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file; 22 | } 23 | @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' ); 24 | $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml'; 25 | file_put_contents( $project_config, $contents ); 26 | putenv( 'WP_CLI_CONFIG_PATH=' . $project_config ); 27 | } 28 | } 29 | // Inside WP-CLI 30 | } else { 31 | require_once __DIR__ . '/../../php/utils.php'; 32 | require_once __DIR__ . '/../../php/WP_CLI/Process.php'; 33 | } 34 | 35 | /** 36 | * Features context. 37 | */ 38 | class FeatureContext extends BehatContext implements ClosuredContextInterface { 39 | 40 | private static $cache_dir, $suite_cache_dir; 41 | 42 | private static $db_settings = array( 43 | 'dbname' => 'wp_cli_test', 44 | 'dbuser' => 'wp_cli_test', 45 | 'dbpass' => 'password1', 46 | 'dbhost' => '127.0.0.1', 47 | ); 48 | 49 | private $running_procs = array(); 50 | 51 | public $variables = array(); 52 | 53 | /** 54 | * Get the environment variables required for launched `wp` processes 55 | * @beforeSuite 56 | */ 57 | private static function get_process_env_variables() { 58 | // Ensure we're using the expected `wp` binary 59 | $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . "/../../bin" ); 60 | $env = array( 61 | 'PATH' => $bin_dir . ':' . getenv( 'PATH' ), 62 | 'BEHAT_RUN' => 1, 63 | 'HOME' => '/tmp/wp-cli-home', 64 | ); 65 | if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { 66 | $env['WP_CLI_CONFIG_PATH'] = $config_path; 67 | } 68 | return $env; 69 | } 70 | 71 | // We cache the results of `wp core download` to improve test performance 72 | // Ideally, we'd cache at the HTTP layer for more reliable tests 73 | private static function cache_wp_files() { 74 | self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test core-download-cache'; 75 | 76 | if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) 77 | return; 78 | 79 | $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); 80 | Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); 81 | } 82 | 83 | /** 84 | * @BeforeSuite 85 | */ 86 | public static function prepare( SuiteEvent $event ) { 87 | self::cache_wp_files(); 88 | } 89 | 90 | /** 91 | * @AfterSuite 92 | */ 93 | public static function afterSuite( SuiteEvent $event ) { 94 | if ( self::$suite_cache_dir ) { 95 | Process::create( Utils\esc_cmd( 'rm -r %s', self::$suite_cache_dir ), null, self::get_process_env_variables() )->run(); 96 | } 97 | } 98 | 99 | /** 100 | * @BeforeScenario 101 | */ 102 | public function beforeScenario( $event ) { 103 | $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); 104 | } 105 | 106 | /** 107 | * @AfterScenario 108 | */ 109 | public function afterScenario( $event ) { 110 | if ( isset( $this->variables['RUN_DIR'] ) ) { 111 | // remove altered WP install, unless there's an error 112 | if ( $event->getResult() < 4 ) { 113 | $this->proc( Utils\esc_cmd( 'rm -r %s', $this->variables['RUN_DIR'] ) )->run(); 114 | } 115 | } 116 | 117 | foreach ( $this->running_procs as $proc ) { 118 | self::terminate_proc( $proc ); 119 | } 120 | } 121 | 122 | /** 123 | * Terminate a process and any of its children. 124 | */ 125 | private static function terminate_proc( $proc ) { 126 | $status = proc_get_status( $proc ); 127 | 128 | $master_pid = $status['pid']; 129 | 130 | $output = `ps -o ppid,pid,command | grep $master_pid`; 131 | 132 | foreach ( explode( PHP_EOL, $output ) as $line ) { 133 | if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { 134 | $parent = $matches[1]; 135 | $child = $matches[2]; 136 | 137 | if ( $parent == $master_pid ) { 138 | if ( ! posix_kill( (int) $child, 9 ) ) { 139 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 140 | } 141 | } 142 | } 143 | } 144 | 145 | if ( ! posix_kill( (int) $master_pid, 9 ) ) { 146 | throw new RuntimeException( posix_strerror( posix_get_last_error() ) ); 147 | } 148 | } 149 | 150 | public static function create_cache_dir() { 151 | self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-suite-cache-", TRUE ); 152 | mkdir( self::$suite_cache_dir ); 153 | return self::$suite_cache_dir; 154 | } 155 | 156 | /** 157 | * Initializes context. 158 | * Every scenario gets it's own context object. 159 | * 160 | * @param array $parameters context parameters (set them up through behat.yml) 161 | */ 162 | public function __construct( array $parameters ) { 163 | $this->drop_db(); 164 | $this->set_cache_dir(); 165 | $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); 166 | } 167 | 168 | public function getStepDefinitionResources() { 169 | return glob( __DIR__ . '/../steps/*.php' ); 170 | } 171 | 172 | public function getHookDefinitionResources() { 173 | return array(); 174 | } 175 | 176 | public function replace_variables( $str ) { 177 | return preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); 178 | } 179 | 180 | private function _replace_var( $matches ) { 181 | $cmd = $matches[0]; 182 | 183 | foreach ( array_slice( $matches, 1 ) as $key ) { 184 | $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd ); 185 | } 186 | 187 | return $cmd; 188 | } 189 | 190 | public function create_run_dir() { 191 | if ( !isset( $this->variables['RUN_DIR'] ) ) { 192 | $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( "wp-cli-test-run-", TRUE ); 193 | mkdir( $this->variables['RUN_DIR'] ); 194 | } 195 | } 196 | 197 | public function build_phar( $version = 'same' ) { 198 | $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; 199 | 200 | $this->proc( Utils\esc_cmd( 201 | 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', 202 | __DIR__ . '/../../utils/make-phar.php', 203 | $this->variables['PHAR_PATH'], 204 | $version 205 | ) )->run_check(); 206 | } 207 | 208 | private function set_cache_dir() { 209 | $path = sys_get_temp_dir() . '/wp-cli-test-cache'; 210 | $this->proc( Utils\esc_cmd( 'mkdir -p %s', $path ) )->run_check(); 211 | $this->variables['CACHE_DIR'] = $path; 212 | } 213 | 214 | private static function run_sql( $sql ) { 215 | Utils\run_mysql_command( 'mysql --no-defaults', array( 216 | 'execute' => $sql, 217 | 'host' => self::$db_settings['dbhost'], 218 | 'user' => self::$db_settings['dbuser'], 219 | 'pass' => self::$db_settings['dbpass'], 220 | ) ); 221 | } 222 | 223 | public function create_db() { 224 | $dbname = self::$db_settings['dbname']; 225 | self::run_sql( "CREATE DATABASE IF NOT EXISTS $dbname" ); 226 | } 227 | 228 | public function drop_db() { 229 | $dbname = self::$db_settings['dbname']; 230 | self::run_sql( "DROP DATABASE IF EXISTS $dbname" ); 231 | } 232 | 233 | public function proc( $command, $assoc_args = array(), $path = '' ) { 234 | if ( !empty( $assoc_args ) ) 235 | $command .= Utils\assoc_args_to_str( $assoc_args ); 236 | 237 | $env = self::get_process_env_variables(); 238 | if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { 239 | $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; 240 | } 241 | 242 | if ( isset( $this->variables['RUN_DIR'] ) ) { 243 | $cwd = "{$this->variables['RUN_DIR']}/{$path}"; 244 | } else { 245 | $cwd = null; 246 | } 247 | 248 | return Process::create( $command, $cwd, $env ); 249 | } 250 | 251 | /** 252 | * Start a background process. Will automatically be closed when the tests finish. 253 | */ 254 | public function background_proc( $cmd ) { 255 | $descriptors = array( 256 | 0 => STDIN, 257 | 1 => array( 'pipe', 'w' ), 258 | 2 => array( 'pipe', 'w' ), 259 | ); 260 | 261 | $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); 262 | 263 | sleep(1); 264 | 265 | $status = proc_get_status( $proc ); 266 | 267 | if ( !$status['running'] ) { 268 | throw new RuntimeException( stream_get_contents( $pipes[2] ) ); 269 | } else { 270 | $this->running_procs[] = $proc; 271 | } 272 | } 273 | 274 | public function move_files( $src, $dest ) { 275 | rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); 276 | } 277 | 278 | public function add_line_to_wp_config( &$wp_config_code, $line ) { 279 | $token = "/* That's all, stop editing!"; 280 | 281 | $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); 282 | } 283 | 284 | public function download_wp( $subdir = '' ) { 285 | $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; 286 | 287 | if ( $subdir ) { 288 | mkdir( $dest_dir ); 289 | } 290 | 291 | $this->proc( Utils\esc_cmd( "cp -r %s/* %s", self::$cache_dir, $dest_dir ) )->run_check(); 292 | 293 | // disable emailing 294 | mkdir( $dest_dir . '/wp-content/mu-plugins' ); 295 | copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); 296 | } 297 | 298 | public function create_config( $subdir = '' ) { 299 | $params = self::$db_settings; 300 | $params['dbprefix'] = $subdir ?: 'wp_'; 301 | 302 | $params['skip-salts'] = true; 303 | $this->proc( 'wp core config', $params, $subdir )->run_check(); 304 | } 305 | 306 | public function install_wp( $subdir = '' ) { 307 | $this->create_db(); 308 | $this->create_run_dir(); 309 | $this->download_wp( $subdir ); 310 | 311 | $this->create_config( $subdir ); 312 | 313 | $install_args = array( 314 | 'url' => 'http://example.com', 315 | 'title' => 'WP CLI Site', 316 | 'admin_user' => 'admin', 317 | 'admin_email' => 'admin@example.com', 318 | 'admin_password' => 'password1' 319 | ); 320 | 321 | $this->proc( 'wp core install', $install_args, $subdir )->run_check(); 322 | } 323 | } 324 | 325 | -------------------------------------------------------------------------------- /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 | 77 | /** 78 | * Results of an executed command. 79 | */ 80 | class ProcessRun { 81 | 82 | /** 83 | * @var array $props Properties of executed command. 84 | */ 85 | public function __construct( $props ) { 86 | foreach ( $props as $key => $value ) { 87 | $this->$key = $value; 88 | } 89 | } 90 | 91 | /** 92 | * Return properties of executed command as a string. 93 | * 94 | * @return string 95 | */ 96 | public function __toString() { 97 | $out = "$ $this->command\n"; 98 | $out .= "$this->stdout\n$this->stderr"; 99 | $out .= "cwd: $this->cwd\n"; 100 | $out .= "exit status: $this->return_code"; 101 | 102 | return $out; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /features/bootstrap/support.php: -------------------------------------------------------------------------------- 1 | $value ) { 71 | if ( ! compareContents( $value, $actual->$name ) ) 72 | return false; 73 | } 74 | } else if ( is_array( $expected ) ) { 75 | foreach ( $expected as $key => $value ) { 76 | if ( ! compareContents( $value, $actual[$key] ) ) 77 | return false; 78 | } 79 | } else { 80 | return $expected === $actual; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | /** 87 | * Compare two strings containing JSON to ensure that @a $actualJson contains at 88 | * least what the JSON string @a $expectedJson contains. 89 | * 90 | * @return whether or not @a $actualJson contains @a $expectedJson 91 | * @retval true @a $actualJson contains @a $expectedJson 92 | * @retval false @a $actualJson does not contain @a $expectedJson 93 | * 94 | * @param[in] $actualJson the JSON string to be tested 95 | * @param[in] $expectedJson the expected JSON string 96 | * 97 | * Examples: 98 | * expected: {'a':1,'array':[1,3,5]} 99 | * 100 | * 1 ) 101 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} 102 | * return: true 103 | * 104 | * 2 ) 105 | * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} 106 | * return: false 107 | * element 'a' is missing from the root object 108 | * 109 | * 3 ) 110 | * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} 111 | * return: false 112 | * the value of element 'a' is not 1 113 | * 114 | * 4 ) 115 | * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} 116 | * return: false 117 | * the contents of 'array' does not include 3 118 | */ 119 | function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { 120 | $actualValue = json_decode( $actualJson ); 121 | $expectedValue = json_decode( $expectedJson ); 122 | 123 | if ( !$actualValue ) { 124 | return false; 125 | } 126 | 127 | return compareContents( $expectedValue, $actualValue ); 128 | } 129 | 130 | /** 131 | * Compare two strings to confirm $actualCSV contains $expectedCSV 132 | * Both strings are expected to have headers for their CSVs. 133 | * $actualCSV must match all data rows in $expectedCSV 134 | * 135 | * @param string A CSV string 136 | * @param array A nested array of values 137 | * @return bool Whether $actualCSV contains $expectedCSV 138 | */ 139 | function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { 140 | $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); 141 | 142 | if ( empty( $actualCSV ) ) 143 | return false; 144 | 145 | // Each sample must have headers 146 | $actualHeaders = array_values( array_shift( $actualCSV ) ); 147 | $expectedHeaders = array_values( array_shift( $expectedCSV ) ); 148 | 149 | // Each expectedCSV must exist somewhere in actualCSV in the proper column 150 | $expectedResult = 0; 151 | foreach ( $expectedCSV as $expected_row ) { 152 | $expected_row = array_combine( $expectedHeaders, $expected_row ); 153 | foreach ( $actualCSV as $actual_row ) { 154 | 155 | if ( count( $actualHeaders ) != count( $actual_row ) ) 156 | continue; 157 | 158 | $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); 159 | if ( $actual_row == $expected_row ) 160 | $expectedResult++; 161 | } 162 | } 163 | 164 | return $expectedResult >= count( $expectedCSV ); 165 | } 166 | 167 | /** 168 | * Compare two strings containing YAML to ensure that @a $actualYaml contains at 169 | * least what the YAML string @a $expectedYaml contains. 170 | * 171 | * @return whether or not @a $actualYaml contains @a $expectedJson 172 | * @retval true @a $actualYaml contains @a $expectedJson 173 | * @retval false @a $actualYaml does not contain @a $expectedJson 174 | * 175 | * @param[in] $actualYaml the YAML string to be tested 176 | * @param[in] $expectedYaml the expected YAML string 177 | */ 178 | function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { 179 | $actualValue = spyc_load( $actualYaml ); 180 | $expectedValue = spyc_load( $expectedYaml ); 181 | 182 | if ( !$actualValue ) { 183 | return false; 184 | } 185 | 186 | return compareContents( $expectedValue, $actualValue ); 187 | } 188 | 189 | -------------------------------------------------------------------------------- /features/bootstrap/utils.php: -------------------------------------------------------------------------------- 1 | {'vendor-dir'} ) ) { 66 | array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->{'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 to edit text 336 | * 337 | * @param str $content Text to edit (eg post content) 338 | * @return str|bool Edited text, if file is saved from editor 339 | * False, if no change to file 340 | */ 341 | function launch_editor_for_input( $input, $title = 'WP-CLI' ) { 342 | 343 | $tmpfile = wp_tempnam( $title ); 344 | 345 | if ( !$tmpfile ) 346 | \WP_CLI::error( 'Error creating temporary file.' ); 347 | 348 | $output = ''; 349 | file_put_contents( $tmpfile, $input ); 350 | 351 | $editor = getenv( 'EDITOR' ); 352 | if ( !$editor ) { 353 | if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) 354 | $editor = 'notepad'; 355 | else 356 | $editor = 'vi'; 357 | } 358 | 359 | $descriptorspec = array( STDIN, STDOUT, STDERR ); 360 | $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); 361 | $r = proc_close( $process ); 362 | if ( $r ) { 363 | exit( $r ); 364 | } 365 | 366 | $output = file_get_contents( $tmpfile ); 367 | 368 | unlink( $tmpfile ); 369 | 370 | if ( $output === $input ) 371 | return false; 372 | 373 | return $output; 374 | } 375 | 376 | /** 377 | * @param string MySQL host string, as defined in wp-config.php 378 | * @return array 379 | */ 380 | function mysql_host_to_cli_args( $raw_host ) { 381 | $assoc_args = array(); 382 | 383 | $host_parts = explode( ':', $raw_host ); 384 | if ( count( $host_parts ) == 2 ) { 385 | list( $assoc_args['host'], $extra ) = $host_parts; 386 | $extra = trim( $extra ); 387 | if ( is_numeric( $extra ) ) { 388 | $assoc_args['port'] = intval( $extra ); 389 | $assoc_args['protocol'] = 'tcp'; 390 | } else if ( $extra !== '' ) { 391 | $assoc_args['socket'] = $extra; 392 | } 393 | } else { 394 | $assoc_args['host'] = $raw_host; 395 | } 396 | 397 | return $assoc_args; 398 | } 399 | 400 | function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { 401 | if ( !$descriptors ) 402 | $descriptors = array( STDIN, STDOUT, STDERR ); 403 | 404 | if ( isset( $assoc_args['host'] ) ) { 405 | $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); 406 | } 407 | 408 | $pass = $assoc_args['pass']; 409 | unset( $assoc_args['pass'] ); 410 | 411 | $old_pass = getenv( 'MYSQL_PWD' ); 412 | putenv( 'MYSQL_PWD=' . $pass ); 413 | 414 | $final_cmd = $cmd . assoc_args_to_str( $assoc_args ); 415 | 416 | $proc = proc_open( $final_cmd, $descriptors, $pipes ); 417 | if ( !$proc ) 418 | exit(1); 419 | 420 | $r = proc_close( $proc ); 421 | 422 | putenv( 'MYSQL_PWD=' . $old_pass ); 423 | 424 | if ( $r ) exit( $r ); 425 | } 426 | 427 | /** 428 | * Render PHP or other types of files using Mustache templates. 429 | * 430 | * IMPORTANT: Automatic HTML escaping is disabled! 431 | */ 432 | function mustache_render( $template_name, $data ) { 433 | if ( ! file_exists( $template_name ) ) 434 | $template_name = WP_CLI_ROOT . "/templates/$template_name"; 435 | 436 | $template = file_get_contents( $template_name ); 437 | 438 | $m = new \Mustache_Engine( array( 439 | 'escape' => function ( $val ) { return $val; } 440 | ) ); 441 | 442 | return $m->render( $template, $data ); 443 | } 444 | 445 | /** 446 | * Create a progress bar to display percent completion of a given operation. 447 | * 448 | * Progress bar is written to STDOUT, and disabled when command is piped. Progress 449 | * advances with `$progress->tick()`, and completes with `$progress->finish()`. 450 | * Process bar also indicates elapsed time and expected total time. 451 | * 452 | * ``` 453 | * # `wp user generate` ticks progress bar each time a new user is created. 454 | * # 455 | * # $ wp user generate --count=500 456 | * # Generating users 22 % [=======> ] 0:05 / 0:23 457 | * 458 | * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); 459 | * for ( $i = 0; $i < $count; $i++ ) { 460 | * // uses wp_insert_user() to insert the user 461 | * $progress->tick(); 462 | * } 463 | * $progress->finish(); 464 | * ``` 465 | * 466 | * @access public 467 | * @category Output 468 | * 469 | * @param string $message Text to display before the progress bar. 470 | * @param integer $count Total number of ticks to be performed. 471 | * @return cli\progress\Bar|WP_CLI\NoOp 472 | */ 473 | function make_progress_bar( $message, $count ) { 474 | if ( \cli\Shell::isPiped() ) 475 | return new \WP_CLI\NoOp; 476 | 477 | return new \cli\progress\Bar( $message, $count ); 478 | } 479 | 480 | function parse_url( $url ) { 481 | $url_parts = \parse_url( $url ); 482 | 483 | if ( !isset( $url_parts['scheme'] ) ) { 484 | $url_parts = parse_url( 'http://' . $url ); 485 | } 486 | 487 | return $url_parts; 488 | } 489 | 490 | /** 491 | * Check if we're running in a Windows environment (cmd.exe). 492 | */ 493 | function is_windows() { 494 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 495 | } 496 | 497 | /** 498 | * Replace magic constants in some PHP source code. 499 | * 500 | * @param string $source The PHP code to manipulate. 501 | * @param string $path The path to use instead of the magic constants 502 | */ 503 | function replace_path_consts( $source, $path ) { 504 | $replacements = array( 505 | '__FILE__' => "'$path'", 506 | '__DIR__' => "'" . dirname( $path ) . "'" 507 | ); 508 | 509 | $old = array_keys( $replacements ); 510 | $new = array_values( $replacements ); 511 | 512 | return str_replace( $old, $new, $source ); 513 | } 514 | 515 | /** 516 | * Make a HTTP request to a remote URL. 517 | * 518 | * Wraps the Requests HTTP library to ensure every request includes a cert. 519 | * 520 | * ``` 521 | * # `wp core download` verifies the hash for a downloaded WordPress archive 522 | * 523 | * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); 524 | * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { 525 | * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); 526 | * } 527 | * ``` 528 | * 529 | * @access public 530 | * 531 | * @param string $method HTTP method (GET, POST, DELETE, etc.) 532 | * @param string $url URL to make the HTTP request to. 533 | * @param array $headers Add specific headers to the request. 534 | * @param array $options 535 | * @return object 536 | */ 537 | function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { 538 | 539 | $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; 540 | if ( inside_phar() ) { 541 | // cURL can't read Phar archives 542 | $options['verify'] = extract_from_phar( 543 | WP_CLI_ROOT . '/vendor' . $cert_path ); 544 | } else { 545 | foreach( get_vendor_paths() as $vendor_path ) { 546 | if ( file_exists( $vendor_path . $cert_path ) ) { 547 | $options['verify'] = $vendor_path . $cert_path; 548 | break; 549 | } 550 | } 551 | if ( empty( $options['verify'] ) ){ 552 | WP_CLI::error_log( "Cannot find SSL certificate." ); 553 | } 554 | } 555 | 556 | try { 557 | $request = \Requests::request( $url, $headers, $data, $method, $options ); 558 | return $request; 559 | } catch( \Requests_Exception $ex ) { 560 | // Handle SSL certificate issues gracefully 561 | \WP_CLI::warning( $ex->getMessage() ); 562 | $options['verify'] = false; 563 | try { 564 | return \Requests::request( $url, $headers, $data, $method, $options ); 565 | } catch( \Requests_Exception $ex ) { 566 | \WP_CLI::error( $ex->getMessage() ); 567 | } 568 | } 569 | } 570 | 571 | /** 572 | * Increments a version string using the "x.y.z-pre" format 573 | * 574 | * Can increment the major, minor or patch number by one 575 | * If $new_version == "same" the version string is not changed 576 | * If $new_version is not a known keyword, it will be used as the new version string directly 577 | * 578 | * @param string $current_version 579 | * @param string $new_version 580 | * @return string 581 | */ 582 | function increment_version( $current_version, $new_version ) { 583 | // split version assuming the format is x.y.z-pre 584 | $current_version = explode( '-', $current_version, 2 ); 585 | $current_version[0] = explode( '.', $current_version[0] ); 586 | 587 | switch ( $new_version ) { 588 | case 'same': 589 | // do nothing 590 | break; 591 | 592 | case 'patch': 593 | $current_version[0][2]++; 594 | 595 | $current_version = array( $current_version[0] ); // drop possible pre-release info 596 | break; 597 | 598 | case 'minor': 599 | $current_version[0][1]++; 600 | $current_version[0][2] = 0; 601 | 602 | $current_version = array( $current_version[0] ); // drop possible pre-release info 603 | break; 604 | 605 | case 'major': 606 | $current_version[0][0]++; 607 | $current_version[0][1] = 0; 608 | $current_version[0][2] = 0; 609 | 610 | $current_version = array( $current_version[0] ); // drop possible pre-release info 611 | break; 612 | 613 | default: // not a keyword 614 | $current_version = array( array( $new_version ) ); 615 | break; 616 | } 617 | 618 | // reconstruct version string 619 | $current_version[0] = implode( '.', $current_version[0] ); 620 | $current_version = implode( '-', $current_version ); 621 | 622 | return $current_version; 623 | } 624 | 625 | /** 626 | * Compare two version strings to get the named semantic version. 627 | * 628 | * @access public 629 | * 630 | * @param string $new_version 631 | * @param string $original_version 632 | * @return string $name 'major', 'minor', 'patch' 633 | */ 634 | function get_named_sem_ver( $new_version, $original_version ) { 635 | 636 | if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { 637 | return ''; 638 | } 639 | 640 | $parts = explode( '-', $original_version ); 641 | list( $major, $minor, $patch ) = explode( '.', $parts[0] ); 642 | 643 | if ( Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { 644 | return 'patch'; 645 | } else if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { 646 | return 'minor'; 647 | } else { 648 | return 'major'; 649 | } 650 | } 651 | 652 | /** 653 | * Return the flag value or, if it's not set, the $default value. 654 | * 655 | * Because flags can be negated (e.g. --no-quiet to negate --quiet), this 656 | * function provides a safer alternative to using 657 | * `isset( $assoc_args['quiet'] )` or similar. 658 | * 659 | * @access public 660 | * @category Input 661 | * 662 | * @param array $assoc_args Arguments array. 663 | * @param string $flag Flag to get the value. 664 | * @param mixed $default Default value for the flag. Default: NULL 665 | * @return mixed 666 | */ 667 | function get_flag_value( $assoc_args, $flag, $default = null ) { 668 | return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; 669 | } 670 | 671 | /** 672 | * Get the system's temp directory. Warns user if it isn't writable. 673 | * 674 | * @access public 675 | * @category System 676 | * 677 | * @return string 678 | */ 679 | function get_temp_dir() { 680 | static $temp = ''; 681 | 682 | $trailingslashit = function( $path ) { 683 | return rtrim( $path ) . '/'; 684 | }; 685 | 686 | if ( $temp ) 687 | return $trailingslashit( $temp ); 688 | 689 | if ( function_exists( 'sys_get_temp_dir' ) ) { 690 | $temp = sys_get_temp_dir(); 691 | } else if ( ini_get( 'upload_tmp_dir' ) ) { 692 | $temp = ini_get( 'upload_tmp_dir' ); 693 | } else { 694 | $temp = '/tmp/'; 695 | } 696 | 697 | if ( ! @is_writable( $temp ) ) { 698 | WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); 699 | } 700 | 701 | return $trailingslashit( $temp ); 702 | } 703 | -------------------------------------------------------------------------------- /features/db-ack.feature: -------------------------------------------------------------------------------- 1 | Feature: Ack through the database 2 | 3 | Scenario: Search on a single site install 4 | Given a WP install 5 | 6 | When I run `wp db ack example.com wp_options` 7 | Then STDOUT should contain: 8 | """ 9 | wp_options:option_value 10 | 1:http://example.com 11 | """ 12 | 13 | Scenario: Long result strings are truncated 14 | Given a WP install 15 | And I run `wp option update searchtest '11111111searchstring11111111'` 16 | 17 | When I run `wp db ack searchstring --before_context=0 --after_context=0` 18 | Then STDOUT should contain: 19 | """ 20 | :searchstring 21 | """ 22 | 23 | When I run `wp db ack searchstring --before_context=3 --after_context=3` 24 | Then STDOUT should contain: 25 | """ 26 | :111searchstring111 27 | """ 28 | 29 | When I run `wp db ack searchstring` 30 | Then STDOUT should contain: 31 | """ 32 | :11111111searchstring11111111 33 | """ 34 | 35 | Scenario: Search against all tables on a multisite install 36 | Given a WP multisite install 37 | And I run `wp site create --slug=foo` 38 | 39 | When I run `wp db ack example.com` 40 | Then STDOUT should contain: 41 | """ 42 | wp_options:option_value 43 | 1:http://example.com 44 | """ 45 | And STDOUT should not contain: 46 | """ 47 | wp_2_options:option_value 48 | 1:http://example.com/foo 49 | """ 50 | 51 | When I run `wp db ack example.com --network` 52 | Then STDOUT should contain: 53 | """ 54 | wp_options:option_value 55 | 1:http://example.com 56 | """ 57 | And STDOUT should contain: 58 | """ 59 | wp_2_options:option_value 60 | 1:http://example.com/foo 61 | """ 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /features/steps/when.php: -------------------------------------------------------------------------------- 1 | 'run_check', 10 | 'try' => 'run' 11 | ); 12 | $method = $map[ $mode ]; 13 | 14 | return $proc->$method(); 15 | } 16 | 17 | $steps->When( '/^I launch in the background `([^`]+)`$/', 18 | function ( $world, $cmd ) { 19 | $world->background_proc( $cmd ); 20 | } 21 | ); 22 | 23 | $steps->When( '/^I (run|try) `([^`]+)`$/', 24 | function ( $world, $mode, $cmd ) { 25 | $cmd = $world->replace_variables( $cmd ); 26 | $world->result = invoke_proc( $world->proc( $cmd ), $mode ); 27 | } 28 | ); 29 | 30 | $steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/", 31 | function ( $world, $mode, $cmd, $subdir ) { 32 | $cmd = $world->replace_variables( $cmd ); 33 | $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode ); 34 | } 35 | ); 36 | 37 | $steps->When( '/^I (run|try) the previous command again$/', 38 | function ( $world, $mode ) { 39 | if ( !isset( $world->result ) ) 40 | throw new \Exception( 'No previous command.' ); 41 | 42 | $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env ); 43 | $world->result = invoke_proc( $proc, $mode ); 44 | } 45 | ); 46 | 47 | -------------------------------------------------------------------------------- /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); --------------------------------------------------------------------------------