diff --git a/README.md b/README.md index ebbc01fb4..7760bf5b4 100644 --- a/README.md +++ b/README.md @@ -5443,18 +5443,19 @@ These fields are optionally available: Lists all sites in a multisite installation. ~~~ -wp site list [--network=] [--=] [--site__in=] [--site_user=] [--site-path=] [--blog_id=] [--site_id=] [--domain=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--field=] [--fields=] [--format=] +wp site list [--network=|site_id|network_id] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--meta_query=] [--date_query=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] ~~~ **OPTIONS** - [--network=] - The network to which the sites belong. + [--network=|site_id|network_id] + The network to which the sites belong. `--site_id` is the name of the + column this filters, `--network_id` is WP_Site_Query's name for it, and + both are accepted as aliases. `--network` wins when more than one is given. [--=] Filter by one or more fields (see "Available Fields" section), or pass any - other argument accepted by WP_Site_Query, such as 'search', 'site__not_in', - 'number', 'offset', 'orderby' or 'order'. However, 'url' isn't an available + other argument accepted by WP_Site_Query. However, 'url' isn't an available filter, as it comes from 'home' in wp_options. Note: '--path' conflicts with the global parameter of the same name; use '--site-path' to filter by path instead. @@ -5462,22 +5463,40 @@ wp site list [--network=] [--=] [--site__in=] [--site_u [--site__in=] Only list the sites with these blog_id values (comma-separated). + [--site__not_in=] + Exclude the sites with these blog_id values (comma-separated). + [--site_user=] Only list the sites with this user. [--site-path=] Filter by path. Avoids conflict with the global `--path` parameter. - [--blog_id=] - Filter by site ID. + [--path__in=] + Only list the sites with these paths (comma-separated). + + [--path__not_in=] + Exclude the sites with these paths (comma-separated). + + [--blog_id=|ID] + Filter by site ID. `--ID` is WP_Site_Query's name for the same filter + and is accepted as an alias. + + [--network__in=] + Only list the sites belonging to these network IDs (comma-separated). - [--site_id=] - Filter by the ID of the network the site belongs to. `--network` is an - alias for this, and takes precedence when both are given. + [--network__not_in=] + Exclude the sites belonging to these network IDs (comma-separated). [--domain=] Filter by domain. + [--domain__in=] + Only list the sites with these domains (comma-separated). + + [--domain__not_in=] + Exclude the sites with these domains (comma-separated). + [--registered=] Filter by the date the site was registered. Accepts a timestamp or a date; a value carrying no time of day matches that whole day. @@ -5504,6 +5523,72 @@ wp site list [--network=] [--=] [--site__in=] [--site_u [--lang_id=] Filter by language ID. + [--lang__in=] + Only list the sites with these language IDs (comma-separated). + + [--lang__not_in=] + Exclude the sites with these language IDs (comma-separated). + + [--search=] + Only list the sites matching this search term. Searches the domain and + path columns unless `--search_columns` narrows it. + + [--search_columns=] + Comma-separated list of columns `--search` looks in. Accepts 'domain' + and 'path'. + + [--meta_key=] + Filter by this site meta key. The site meta arguments need WordPress 5.1 + or later, which is where multisite gained the table they read. + + [--meta_value=] + Filter by this site meta value. Used together with `--meta_key`. + + [--meta_compare=] + Operator to test the meta value against. Accepts the operators + WP_Meta_Query supports, such as '=', '!=', 'LIKE' or 'IN'. + + [--meta_type=] + Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. + + [--meta_query=] + A WP_Meta_Query clause list, as JSON, for conditions the meta_* arguments + above cannot express. + + [--date_query=] + A WP_Date_Query clause list, as JSON, for ranges `--registered` and + `--last_updated` cannot express. Giving those as well narrows this further + rather than replacing it. + + [--number=] + Limit the number of sites returned. + + [--offset=] + Number of sites to skip. Used together with `--number`. + + [--no_found_rows=] + Whether to skip counting the total rows the query matches. Accepts 1 or 0. + + [--update_site_cache=] + Whether to prime the object cache with the sites found. Accepts 1 or 0. + Turning it off saves work when listing a large network once. + + [--update_site_meta_cache=] + Whether to prime the object cache with the site meta of the sites found. + Accepts 1 or 0. + + [--orderby=] + Order the results by this field, such as 'blog_id', 'domain', 'path', + 'registered', 'last_updated' or 'site__in'. + + [--order=] + Whether to order the results ascending or descending. + --- + options: + - asc + - desc + --- + [--field=] Prints the value of a single field for each site. diff --git a/features/site.feature b/features/site.feature index bebd0e0c2..da83f80f7 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1087,6 +1087,199 @@ Feature: Manage sites in a multisite installation 1 """ + Scenario: List sites using the WP_Site_Query arguments that take a list + Given a WP multisite install + + When I run `wp site create --slug=alpha --porcelain` + Then STDOUT should be a number + And save STDOUT as {ALPHA_ID} + + When I run `wp site create --slug=beta --porcelain` + Then STDOUT should be a number + And save STDOUT as {BETA_ID} + + # WP_Site_Query reads these through is_array(), so before they were split + # they were skipped without a word and every site came back. + When I run `wp site list --path__in=/alpha/,/beta/ --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + {BETA_ID} + """ + + When I run `wp site list --path__not_in=/alpha/,/beta/ --field=blog_id` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --domain__in=example.com --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --domain__not_in=example.com --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --search_columns reaches array_intersect(), which is fatal on a string. + When I run `wp site list --search=alpha --search_columns=path --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --search=alpha --search_columns=domain --format=count` + Then STDOUT should be: + """ + 0 + """ + + # The remaining newly documented filters. + When I run `wp site list --network__in=1 --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --network__not_in=1 --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I run `wp site list --lang__in=0 --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --ID={ALPHA_ID} --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --network_id=1 --format=count` + Then STDOUT should be: + """ + 3 + """ + + # --date_query is a nested array, so it is given as JSON. + When I run `wp site list --date_query='[{"column":"registered","after":"1999-01-01"}]' --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --date_query='[{"column":"registered","before":"1999-01-01"}]' --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --registered narrows a --date_query of its own rather than replacing it. The + # date on its own has to match first, or the pair below would prove nothing. + When I run `wp site list --blog_id={ALPHA_ID} --field=registered` + Then save STDOUT as {ALPHA_REGISTERED} + + When I run `wp site list --blog_id={ALPHA_ID} --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --blog_id={ALPHA_ID} --date_query='[{"column":"registered","before":"1999-01-01"}]' --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + + # The relation of a given --date_query governs only its own clauses. Appending + # to the same list would let an 'OR' reach the clause --registered adds and + # match a site that satisfies neither half of what was asked for. + When I run `wp site list --blog_id={ALPHA_ID} --date_query='{"relation":"OR","0":{"column":"registered","before":"1999-01-01"},"1":{"column":"registered","before":"1998-01-01"}}' --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I run `wp site list --blog_id={ALPHA_ID} --field=last_updated` + Then save STDOUT as {ALPHA_UPDATED} + + When I run `wp site list --blog_id={ALPHA_ID} --last_updated='{ALPHA_UPDATED}' --format=count` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --blog_id={ALPHA_ID} --date_query='{"relation":"OR","0":{"column":"last_updated","before":"1999-01-01"},"1":{"column":"last_updated","before":"1998-01-01"}}' --last_updated='{ALPHA_UPDATED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I try `wp site list --meta_query=notjson` + Then STDERR should contain: + """ + Invalid JSON passed to --meta_query. + """ + And the return code should be 1 + + When I try `wp site list --date_query=notjson` + Then STDERR should contain: + """ + Invalid JSON passed to --date_query. + """ + And the return code should be 1 + + # WP_Site_Query gained the meta_* parameters, and multisite gained the site meta + # table they read, in WordPress 5.1. + @require-wp-5.1 + Scenario: Filter the site list by site meta + Given a WP multisite install + + When I run `wp site create --slug=alpha --porcelain` + Then STDOUT should be a number + And save STDOUT as {ALPHA_ID} + + When I run `wp site create --slug=beta --porcelain` + Then STDOUT should be a number + And save STDOUT as {BETA_ID} + + When I run `wp site meta add {ALPHA_ID} colour blue` + Then STDOUT should not be empty + + When I run `wp site list --meta_key=colour --meta_value=blue --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_key=colour --meta_value=red --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --meta_query is a nested array, so it is given as JSON. + When I run `wp site list --meta_query='[{"key":"colour","value":"blue"}]' --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_query='[{"key":"colour","compare":"NOT EXISTS"}]' --field=blog_id` + Then STDOUT should be: + """ + 1 + {BETA_ID} + """ + Scenario: Existing site list filters keep working against WP_Site_Query Given a WP multisite install diff --git a/src/Site_Command.php b/src/Site_Command.php index 2c9834bb9..c8b959d4e 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -982,13 +982,14 @@ private function get_network( $network_id ) { * * ## OPTIONS * - * [--network=] - * : The network to which the sites belong. + * [--network=|site_id|network_id] + * : The network to which the sites belong. `--site_id` is the name of the + * column this filters, `--network_id` is WP_Site_Query's name for it, and + * both are accepted as aliases. `--network` wins when more than one is given. * * [--=] * : Filter by one or more fields (see "Available Fields" section), or pass any - * other argument accepted by WP_Site_Query, such as 'search', 'site__not_in', - * 'number', 'offset', 'orderby' or 'order'. However, 'url' isn't an available + * other argument accepted by WP_Site_Query. However, 'url' isn't an available * filter, as it comes from 'home' in wp_options. * Note: '--path' conflicts with the global parameter of the same name; use * '--site-path' to filter by path instead. @@ -996,22 +997,40 @@ private function get_network( $network_id ) { * [--site__in=] * : Only list the sites with these blog_id values (comma-separated). * + * [--site__not_in=] + * : Exclude the sites with these blog_id values (comma-separated). + * * [--site_user=] * : Only list the sites with this user. * * [--site-path=] * : Filter by path. Avoids conflict with the global `--path` parameter. * - * [--blog_id=] - * : Filter by site ID. + * [--path__in=] + * : Only list the sites with these paths (comma-separated). + * + * [--path__not_in=] + * : Exclude the sites with these paths (comma-separated). + * + * [--blog_id=|ID] + * : Filter by site ID. `--ID` is WP_Site_Query's name for the same filter + * and is accepted as an alias. * - * [--site_id=] - * : Filter by the ID of the network the site belongs to. `--network` is an - * alias for this, and takes precedence when both are given. + * [--network__in=] + * : Only list the sites belonging to these network IDs (comma-separated). + * + * [--network__not_in=] + * : Exclude the sites belonging to these network IDs (comma-separated). * * [--domain=] * : Filter by domain. * + * [--domain__in=] + * : Only list the sites with these domains (comma-separated). + * + * [--domain__not_in=] + * : Exclude the sites with these domains (comma-separated). + * * [--registered=] * : Filter by the date the site was registered. Accepts a timestamp or a * date; a value carrying no time of day matches that whole day. @@ -1038,6 +1057,72 @@ private function get_network( $network_id ) { * [--lang_id=] * : Filter by language ID. * + * [--lang__in=] + * : Only list the sites with these language IDs (comma-separated). + * + * [--lang__not_in=] + * : Exclude the sites with these language IDs (comma-separated). + * + * [--search=] + * : Only list the sites matching this search term. Searches the domain and + * path columns unless `--search_columns` narrows it. + * + * [--search_columns=] + * : Comma-separated list of columns `--search` looks in. Accepts 'domain' + * and 'path'. + * + * [--meta_key=] + * : Filter by this site meta key. The site meta arguments need WordPress 5.1 + * or later, which is where multisite gained the table they read. + * + * [--meta_value=] + * : Filter by this site meta value. Used together with `--meta_key`. + * + * [--meta_compare=] + * : Operator to test the meta value against. Accepts the operators + * WP_Meta_Query supports, such as '=', '!=', 'LIKE' or 'IN'. + * + * [--meta_type=] + * : Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. + * + * [--meta_query=] + * : A WP_Meta_Query clause list, as JSON, for conditions the meta_* arguments + * above cannot express. + * + * [--date_query=] + * : A WP_Date_Query clause list, as JSON, for ranges `--registered` and + * `--last_updated` cannot express. Giving those as well narrows this further + * rather than replacing it. + * + * [--number=] + * : Limit the number of sites returned. + * + * [--offset=] + * : Number of sites to skip. Used together with `--number`. + * + * [--no_found_rows=] + * : Whether to skip counting the total rows the query matches. Accepts 1 or 0. + * + * [--update_site_cache=] + * : Whether to prime the object cache with the sites found. Accepts 1 or 0. + * Turning it off saves work when listing a large network once. + * + * [--update_site_meta_cache=] + * : Whether to prime the object cache with the site meta of the sites found. + * Accepts 1 or 0. + * + * [--orderby=] + * : Order the results by this field, such as 'blog_id', 'domain', 'path', + * 'registered', 'last_updated' or 'site__in'. + * + * [--order=] + * : Whether to order the results ascending or descending. + * --- + * options: + * - asc + * - desc + * --- + * * [--field=] * : Prints the value of a single field for each site. * @@ -1124,6 +1209,26 @@ public function list_( $args, $assoc_args ) { ) ); + // 'meta_query' and 'date_query' are nested arrays, so they are given as JSON. + // The decoded values go straight into the query arguments rather than back + // into $assoc_args, which the rest of this method reads as strings. + // + // parse_shell_arrays() leaves anything that is not JSON alone, and a string + // reaching either of these is ignored without a word. Say so instead. + $decoded_args = Utils\parse_shell_arrays( $assoc_args, [ 'meta_query', 'date_query' ] ); + + foreach ( [ 'meta_query', 'date_query' ] as $json_arg ) { + if ( ! isset( $decoded_args[ $json_arg ] ) ) { + continue; + } + + if ( ! is_array( $decoded_args[ $json_arg ] ) ) { + WP_CLI::error( "Invalid JSON passed to --{$json_arg}." ); + } + + $query_args[ $json_arg ] = $decoded_args[ $json_arg ]; + } + // Arguments this command spells differently to WP_Site_Query. if ( isset( $assoc_args['blog_id'] ) ) { $query_args['site__in'] = [ $assoc_args['blog_id'] ]; @@ -1133,11 +1238,20 @@ public function list_( $args, $assoc_args ) { $query_args['site__in'] = array_map( 'trim', explode( ',', $assoc_args['site__in'] ) ); } - if ( isset( $assoc_args['site_id'] ) ) { - $query_args['network_id'] = $assoc_args['site_id']; + // WP_Site_Query reads these through is_array() or array_intersect(), so a + // comma-separated string reaches them as a value they cannot use: the domain + // and path clauses are skipped without a word, and 'search_columns' is fatal. + // Splitting them here is what makes them mean anything from the command line. + foreach ( [ 'domain__in', 'domain__not_in', 'path__in', 'path__not_in', 'search_columns' ] as $list_arg ) { + if ( isset( $assoc_args[ $list_arg ] ) && ! is_array( $assoc_args[ $list_arg ] ) ) { + $query_args[ $list_arg ] = array_map( 'trim', explode( ',', (string) $assoc_args[ $list_arg ] ) ); + } } - // '--network' has always taken precedence over '--site_id'. + // '--site_id' and '--network_id' are aliases of '--network', so they have + // already been resolved to it by the time the command runs. wp-cli lets the + // canonical name win when more than one is given, which is the precedence + // '--network' has always had over '--site_id'. if ( isset( $assoc_args['network'] ) ) { $query_args['network_id'] = $assoc_args['network']; } @@ -1163,8 +1277,27 @@ public function list_( $args, $assoc_args ) { } } + // A '--date_query' of its own is kept rather than replaced, so '--registered' + // and '--last_updated' narrow it the way every other filter here narrows the + // result instead of quietly winning. + // + // It is nested a level down rather than appended to, because its 'relation' + // governs whatever shares its list: an 'OR' would reach the clauses added + // here and match a site that satisfies neither half of what was asked for. if ( ! empty( $date_query ) ) { - $query_args['date_query'] = $date_query; + $given = isset( $query_args['date_query'] ) && is_array( $query_args['date_query'] ) + ? $query_args['date_query'] + : []; + + $query_args['date_query'] = empty( $given ) + ? $date_query + : array_merge( + [ + 'relation' => 'AND', + $given, + ], + $date_query + ); } if ( isset( $assoc_args['site_user'] ) ) {