diff --git a/features/site-create.feature b/features/site-create.feature index f068115c3..9c53543dc 100644 --- a/features/site-create.feature +++ b/features/site-create.feature @@ -93,3 +93,160 @@ Feature: Create a new site on a WP multisite | blog_id | url | | 1 | http://localhost/ | | 2 | http://localhost/newsite/ | + + Scenario: Create site with custom URL in subdomain multisite + Given a WP multisite subdomain install + + When I run `wp site create --site-url=http://custom.example.com` + Then STDOUT should contain: + """ + Success: Site 2 created: http://custom.example.com/ + """ + + 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://custom.example.com/ | + + When I run `wp --url=custom.example.com option get home` + Then STDOUT should be: + """ + http://custom.example.com + """ + + Scenario: Create site with custom URL in subdirectory multisite + Given a WP multisite subdirectory install + + When I run `wp site create --site-url=http://example.com/custom/path/` + Then STDOUT should contain: + """ + Success: Site 2 created: + """ + And STDOUT should contain: + """ + ://example.com/custom/path/ + """ + + When I run `wp site list --fields=blog_id,url` + Then STDOUT should contain: + """ + ://example.com/custom/path/ + """ + + Scenario: Create site with custom URL and explicit slug + Given a WP multisite subdomain install + + When I run `wp site create --site-url=http://custom.example.com --slug=myslug` + Then STDOUT should contain: + """ + Success: Site 2 created: http://custom.example.com/ + """ + + Scenario: Error when neither slug nor site-url is provided + Given a WP multisite install + + When I try `wp site create --title="Test Site"` + Then STDERR should be: + """ + Error: Either --slug or --site-url must be provided. + """ + And the return code should be 1 + + Scenario: Error when invalid URL format is provided + Given a WP multisite install + + When I try `wp site create --site-url=not-a-valid-url` + Then STDERR should contain: + """ + Error: Invalid URL format + """ + And the return code should be 1 + + Scenario: Error when invalid scheme is provided + Given a WP multisite install + + When I try `wp site create --site-url=ftp://example.com/site` + Then STDERR should be: + """ + Error: Invalid URL scheme. Only http and https schemes are supported. + """ + And the return code should be 1 + + Scenario: Error when root path provided without explicit slug + Given a WP multisite subdirectory install + + When I try `wp site create --site-url=http://example.com/` + Then STDERR should be: + """ + Error: Could not derive a valid slug from the URL path. Please provide --slug explicitly. + """ + And the return code should be 1 + + Scenario: Create site with URL without trailing slash + Given a WP multisite subdirectory install + + When I run `wp site create --site-url=http://example.com/notrailing` + Then STDOUT should contain: + """ + Success: Site 2 created: + """ + And STDOUT should contain: + """ + ://example.com/notrailing/ + """ + + Scenario: Error when numeric-only domain is provided without slug + Given a WP multisite subdomain install + + When I try `wp site create --site-url=http://123.example.com` + Then STDERR should be: + """ + Error: Could not derive a valid slug from the domain (numeric-only or empty slugs are not allowed). Please provide --slug explicitly. + """ + And the return code should be 1 + + Scenario: Create site with different domain in subdirectory multisite shows warning + Given a WP multisite subdirectory install + + When I try `wp site create --site-url=http://custom.example.com/mypath/` + Then STDERR should contain: + """ + Warning: Using a different domain for a subdirectory multisite install may require additional configuration + """ + And STDOUT should contain: + """ + Success: Site 2 created: + """ + And STDOUT should contain: + """ + ://custom.example.com/mypath/ + """ + + Scenario: Create site with both site-url and slug uses slug for internal operations + Given a WP multisite subdomain install + + When I run `wp site create --site-url=http://custom.example.com --slug=myslug --porcelain` + Then STDOUT should be a number + And save STDOUT as {SITE_ID} + + When I run `wp site list --site__in={SITE_ID} --field=url` + Then STDOUT should contain: + """ + custom.example.com + """ + + Scenario: Preserve existing slug behavior + Given a WP multisite subdomain install + + When I run `wp site create --slug=testsite` + Then STDOUT should contain: + """ + Success: Site 2 created: http://testsite.example.com/ + """ + + 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://testsite.example.com/ | diff --git a/src/Site_Command.php b/src/Site_Command.php index a0d6a9b38..11c0d6ebf 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -522,8 +522,15 @@ public function get( $args, $assoc_args ) { * * ## OPTIONS * - * --slug= + * [--slug=] * : Path for the new site. Subdomain on subdomain installs, directory on subdirectory installs. + * Required if --site-url is not provided. + * + * [--site-url=] + * : Full URL for the new site. Use this to specify a custom domain instead of the auto-generated one. + * For subdomain installs, this allows you to use a different base domain (e.g., 'http://site.example.com' instead of 'http://site.main.example.com'). + * For subdirectory installs, this allows you to use a different path. + * If provided, --slug is optional and will be derived from the URL. If both --slug and --site-url are provided, --slug will be used as the base for internal operations (like user creation), while the domain/path from --site-url will be used for the actual site URL. * * [--title=] * : Title of the new site. Default: prettified slug. @@ -542,8 +549,17 @@ public function get( $args, $assoc_args ) { * * ## EXAMPLES * + * # Create a site with auto-generated domain * $ wp site create --slug=example * Success: Site 3 created: http://www.example.com/example/ + * + * # Create a site with a custom domain (subdomain multisite) + * $ wp site create --site-url=http://site.example.com + * Success: Site 4 created: http://site.example.com/ + * + * # Create a site with a custom subdirectory (subdirectory multisite) + * $ wp site create --site-url=http://example.com/custom/path/ + * Success: Site 5 created: http://example.com/custom/path/ */ public function create( $args, $assoc_args ) { if ( ! is_multisite() ) { @@ -552,7 +568,72 @@ public function create( $args, $assoc_args ) { global $wpdb, $current_site; - $base = $assoc_args['slug']; + // Check if either slug or site-url is provided + $has_slug = isset( $assoc_args['slug'] ); + $has_site_url = isset( $assoc_args['site-url'] ); + + if ( ! $has_slug && ! $has_site_url ) { + WP_CLI::error( 'Either --slug or --site-url must be provided.' ); + } + + // If site URL is provided, parse it to get domain and path + $custom_domain = null; + $custom_path = null; + $base = null; + + if ( $has_site_url ) { + $parsed_url = wp_parse_url( $assoc_args['site-url'] ); + if ( ! isset( $parsed_url['host'] ) ) { + WP_CLI::error( 'Invalid URL format. Please provide a valid URL (e.g., http://site.example.com).' ); + } + + // Validate the scheme if present + if ( isset( $parsed_url['scheme'] ) && ! in_array( $parsed_url['scheme'], [ 'http', 'https' ], true ) ) { + WP_CLI::error( 'Invalid URL scheme. Only http and https schemes are supported.' ); + } + + // Sanitize domain and path + $custom_domain = sanitize_text_field( $parsed_url['host'] ); + $custom_path = isset( $parsed_url['path'] ) ? sanitize_text_field( '/' . ltrim( $parsed_url['path'], '/' ) ) : '/'; + + // Ensure path ends with / + if ( '/' !== substr( $custom_path, -1 ) ) { + $custom_path .= '/'; + } + + // Derive base/slug from the URL if not explicitly provided + if ( ! $has_slug ) { + if ( is_subdomain_install() ) { + // For subdomain installs, use the first part of the domain as the base + $domain_parts = explode( '.', $custom_domain ); + $base = $domain_parts[0]; + + // Validate that the derived base is suitable for use as a slug + if ( empty( $base ) || is_numeric( $base ) ) { + WP_CLI::error( 'Could not derive a valid slug from the domain (numeric-only or empty slugs are not allowed). Please provide --slug explicitly.' ); + } + + // Sanitize and lowercase the derived base + $base = strtolower( $base ); + } else { + // For subdirectory installs, derive slug from the last part of the path. + $path_parts = array_filter( explode( '/', trim( $custom_path, '/' ) ) ); + $base = (string) array_pop( $path_parts ); + + // If base is empty (root path), require explicit slug. + if ( empty( $base ) ) { + WP_CLI::error( 'Could not derive a valid slug from the URL path. Please provide --slug explicitly.' ); + } + + // Sanitize and lowercase the derived base. + $base = strtolower( $base ); + } + } else { + $base = $assoc_args['slug']; + } + } else { + $base = $assoc_args['slug']; + } /** * @var string $title @@ -603,10 +684,25 @@ public function create( $args, $assoc_args ) { } } - if ( is_subdomain_install() ) { + if ( null !== $custom_domain ) { + // A custom site URL was provided. + $newdomain = $custom_domain; + $path = $custom_path; + + // For subdirectory installs, warn if the domain is different from the network's domain. + if ( ! is_subdomain_install() ) { + $network_domain = preg_replace( '|^www\.|', '', $current_site->domain ); + $custom_domain_normalized = preg_replace( '|^www\.|', '', $custom_domain ); + if ( $custom_domain_normalized !== $network_domain ) { + WP_CLI::warning( 'Using a different domain for a subdirectory multisite install may require additional configuration (such as domain mapping) to work properly.' ); + } + } + } elseif ( is_subdomain_install() ) { + // No custom site URL, use the slug to generate the domain/path for subdomain install. $newdomain = $base . '.' . preg_replace( '|^www\.|', '', $current_site->domain ); $path = $current_site->path; } else { + // No custom site URL, use the slug to generate the domain/path for subdirectory install. $newdomain = $current_site->domain; $path = $current_site->path . $base . '/'; }