diff --git a/features/site-create.feature b/features/site-create.feature index f068115c3..0b59f9a8f 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 run `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 e1ded0171..f5c41cffe 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -389,8 +389,15 @@ public function delete( $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. @@ -409,8 +416,17 @@ public function delete( $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() ) { @@ -419,7 +435,78 @@ 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, use the path as the base + $base = trim( $custom_path, '/' ); + // Use the last part of the path if there are multiple segments + if ( ! empty( $base ) ) { + $path_parts = explode( '/', $base ); + $last_part = array_pop( $path_parts ); + if ( null !== $last_part && '' !== $last_part ) { + $base = $last_part; + } + } + // 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 @@ -471,9 +558,28 @@ public function create( $args, $assoc_args ) { } if ( is_subdomain_install() ) { - $newdomain = $base . '.' . preg_replace( '|^www\.|', '', $current_site->domain ); - $path = $current_site->path; + if ( null !== $custom_domain ) { + // Use custom domain if provided via --site-url + $newdomain = $custom_domain; + $path = $custom_path; + } else { + // Use default behavior + $newdomain = $base . '.' . preg_replace( '|^www\.|', '', $current_site->domain ); + $path = $current_site->path; + } + } elseif ( null !== $custom_domain ) { + // Use custom domain and path if provided via --site-url + $newdomain = $custom_domain; + $path = $custom_path; + + // Warn if using a different domain in subdirectory 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.' ); + } } else { + // Use default behavior $newdomain = $current_site->domain; $path = $current_site->path . $base . '/'; }