diff --git a/composer.json b/composer.json index 4aea0938..e033d938 100644 --- a/composer.json +++ b/composer.json @@ -125,6 +125,7 @@ "site activate", "site archive", "site create", + "site generate", "site deactivate", "site delete", "site empty", diff --git a/features/site-generate.feature b/features/site-generate.feature new file mode 100644 index 00000000..c0f0235b --- /dev/null +++ b/features/site-generate.feature @@ -0,0 +1,107 @@ +Feature: Generate new WordPress sites + + Scenario: Generate on single site + Given a WP install + When I try `wp site generate` + Then STDERR should contain: + """ + This is not a multisite installation. + """ + And STDOUT should be empty + And the return code should be 1 + + Scenario: Generate a specific number of sites + Given a WP multisite install + When I run `wp site generate --count=10` + And I run `wp site list --format=count` + Then STDOUT should be: + """ + 11 + """ + + Scenario: Generate sites assigned to a specific network + Given a WP multisite install + When I try `wp site generate --count=4 --network_id=2` + Then STDERR should contain: + """ + Network with id 2 does not exist. + """ + And STDOUT should be empty + And the return code should be 1 + + Scenario: Generate sites and output ids + Given a WP multisite install + When I run `wp site generate --count=3 --format=ids` + When I run `wp site list --format=ids` + Then STDOUT should be: + """ + 1 2 3 4 + """ + And STDERR should be empty + And the return code should be 0 + + Scenario: Generate subdomain sites + Given a WP multisite subdomain install + + When I run `wp site generate --count=1` + Then STDOUT should be empty + + When I run `wp site list --fields=blog_id,url` + Then STDOUT should be a table containing rows: + | blog_id | url | + | 1 | https://example.com/ | + | 2 | http://site1.example.com/ | + When I run `wp site list --format=ids` + Then STDOUT should be: + """ + 1 2 + """ + + Scenario: Generate subdirectory sites + Given a WP multisite subdirectory install + When I run `wp site generate --count=1` + Then STDOUT should be empty + And I run `wp site list --site__in=2 --field=url | sed -e's,^\(.*\)://.*,\1,g'` + And save STDOUT as {SCHEME} + + When I run `wp site list --fields=blog_id,url` + Then STDOUT should be a table containing rows: + | blog_id | url | + | 1 | https://example.com/ | + | 2 | {SCHEME}://example.com/site1/ | + When I run `wp site list --format=ids` + Then STDOUT should be: + """ + 1 2 + """ + + Scenario: Generate sites with a slug + Given a WP multisite subdirectory install + When I run `wp site generate --count=2 --slug=subsite` + Then STDOUT should be empty + And I run `wp site list --site__in=2 --field=url | sed -e's,^\(.*\)://.*,\1,g'` + And save STDOUT as {SCHEME1} + And I run `wp site list --site__in=3 --field=url | sed -e's,^\(.*\)://.*,\1,g'` + And save STDOUT as {SCHEME2} + + When I run `wp site list --fields=blog_id,url` + Then STDOUT should be a table containing rows: + | blog_id | url | + | 1 | https://example.com/ | + | 2 | {SCHEME1}://example.com/subsite1/ | + | 3 | {SCHEME2}://example.com/subsite2/ | + When I run `wp site list --format=ids` + Then STDOUT should be: + """ + 1 2 3 + """ + + Scenario: Generate sites with reserved slug + Given a WP multisite subdirectory install + When I try `wp site generate --count=2 --slug=page` + Then STDERR should contain: + """ + The following words are reserved and cannot be used as blog names: page, comments, blog, files, feed + """ + And STDOUT should be empty + And the return code should be 1 diff --git a/src/Site_Command.php b/src/Site_Command.php index 4205e544..de5451a5 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -424,8 +424,7 @@ public function create( $args, $assoc_args ) { // If not a subdomain install, make sure the domain isn't a reserved word if ( ! is_subdomain_install() ) { - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling WordPress native hook. - $subdirectory_reserved_names = apply_filters( 'subdirectory_reserved_names', [ 'page', 'comments', 'blog', 'files', 'feed' ] ); + $subdirectory_reserved_names = $this->get_subdirectory_reserved_names(); if ( in_array( $base, $subdirectory_reserved_names, true ) ) { WP_CLI::error( 'The following words are reserved and cannot be used as blog names: ' . implode( ', ', $subdirectory_reserved_names ) ); } @@ -487,6 +486,194 @@ public function create( $args, $assoc_args ) { } } + /** + * Generate some sites. + * + * Creates a specified number of new sites. + * + * ## OPTIONS + * + * [--count=] + * : How many sites to generates? + * --- + * default: 100 + * --- + * + * [--slug=] + * : Path for the new site. Subdomain on subdomain installs, directory on subdirectory installs. + * + * [--email=] + * : Email for admin user. User will be created if none exists. Assignment to super admin if not included. + * + * [--network_id=] + * : Network to associate new site with. Defaults to current network (typically 1). + * + * [--private] + * : If set, the new site will be non-public (not indexed) + * + * [--format=] + * : Render output in a particular format. + * --- + * default: progress + * options: + * - progress + * - ids + * --- + * + * ## EXAMPLES + * + * # Generate 10 sites. + * $ wp site generate --count=10 + * Generating sites 100% [================================================] 0:01 / 0:04 + */ + public function generate( $args, $assoc_args ) { + if ( ! is_multisite() ) { + WP_CLI::error( 'This is not a multisite installation.' ); + } + + global $wpdb, $current_site; + + $defaults = [ + 'count' => 100, + 'email' => '', + 'network_id' => 1, + 'slug' => 'site', + ]; + + $assoc_args = array_merge( $defaults, $assoc_args ); + + // Base. + $base = $assoc_args['slug']; + if ( preg_match( '|^([a-zA-Z0-9-])+$|', $base ) ) { + $base = strtolower( $base ); + } + + $is_subdomain_install = is_subdomain_install(); + // If not a subdomain install, make sure the domain isn't a reserved word + if ( ! $is_subdomain_install ) { + $subdirectory_reserved_names = $this->get_subdirectory_reserved_names(); + if ( in_array( $base, $subdirectory_reserved_names, true ) ) { + WP_CLI::error( 'The following words are reserved and cannot be used as blog names: ' . implode( ', ', $subdirectory_reserved_names ) ); + } + } + + // Network. + if ( ! empty( $assoc_args['network_id'] ) ) { + $network = $this->get_network( $assoc_args['network_id'] ); + if ( false === $network ) { + WP_CLI::error( "Network with id {$assoc_args['network_id']} does not exist." ); + } + } else { + $network = $current_site; + } + + // Public. + $public = ! Utils\get_flag_value( $assoc_args, 'private' ); + + // Limit. + $limit = $assoc_args['count']; + + // Email. + $email = sanitize_email( $assoc_args['email'] ); + if ( empty( $email ) || ! is_email( $email ) ) { + $super_admins = get_super_admins(); + $email = ''; + if ( ! empty( $super_admins ) && is_array( $super_admins ) ) { + $super_login = reset( $super_admins ); + $super_user = get_user_by( 'login', $super_login ); + if ( $super_user ) { + $email = $super_user->user_email; + } + } + } + + $user_id = email_exists( $email ); + if ( ! $user_id ) { + $password = wp_generate_password( 24, false ); + $user_id = wpmu_create_user( $base . '-admin', $password, $email ); + + if ( false === $user_id ) { + WP_CLI::error( "Can't create user." ); + } else { + User_Command::wp_new_user_notification( $user_id, $password ); + } + } + + $format = Utils\get_flag_value( $assoc_args, 'format', 'progress' ); + + $notify = false; + if ( 'progress' === $format ) { + $notify = Utils\make_progress_bar( 'Generating sites', $limit ); + } + + for ( $index = 1; $index <= $limit; $index++ ) { + $current_base = $base . $index; + $title = ucfirst( $base ) . ' ' . $index; + + if ( $is_subdomain_install ) { + $new_domain = $current_base . '.' . preg_replace( '|^www\.|', '', $network->domain ); + $path = $network->path; + } else { + $new_domain = $network->domain; + $path = $network->path . $current_base . '/'; + } + + $wpdb->hide_errors(); + $title = wp_slash( $title ); + $id = wpmu_create_blog( $new_domain, $path, $title, $user_id, [ 'public' => $public ], $network->id ); + $wpdb->show_errors(); + if ( ! is_wp_error( $id ) ) { + if ( ! is_super_admin( $user_id ) && ! get_user_option( 'primary_blog', $user_id ) ) { + update_user_option( $user_id, 'primary_blog', $id, true ); + } + } else { + WP_CLI::error( $id->get_error_message() ); + } + + if ( 'progress' === $format ) { + $notify->tick(); + } else { + echo $id; + if ( $index < $limit - 1 ) { + echo ' '; + } + } + } + + if ( 'progress' === $format ) { + $notify->finish(); + } + } + + /** + * Retrieves a list of reserved site on a sub-directory Multisite installation. + * + * Works on older WordPress versions where get_subdirectory_reserved_names() does not exist. + * + * @return string[] Array of reserved names. + */ + private function get_subdirectory_reserved_names() { + if ( function_exists( 'get_subdirectory_reserved_names' ) ) { + return get_subdirectory_reserved_names(); + } + + $names = array( + 'page', + 'comments', + 'blog', + 'files', + 'feed', + 'wp-admin', + 'wp-content', + 'wp-includes', + 'wp-json', + 'embed', + ); + + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling WordPress native hook. + return apply_filters( 'subdirectory_reserved_names', $names ); + } + /** * Gets network data for a given id. *