diff --git a/inc/functions/product.php b/inc/functions/product.php index 09f3a65f..a65671f5 100644 --- a/inc/functions/product.php +++ b/inc/functions/product.php @@ -116,25 +116,29 @@ function wu_get_product_by($column, $value) { */ function wu_create_product($product_data) { + // Note: empty-string sentinels (instead of `false`) for fields that + // have model-level `default:` rules. rakit's validator treats `false` + // as a non-empty value (`!is_null(false)` is true), so model defaults + // would never fire if we used `false` here. $product_data = wp_parse_args( $product_data, [ - 'name' => false, - 'description' => false, - 'currency' => false, - 'pricing_type' => false, - 'setup_fee' => false, + 'name' => '', + 'description' => '', + 'currency' => '', + 'pricing_type' => '', + 'setup_fee' => '', 'parent_id' => 0, - 'slug' => false, - 'recurring' => false, + 'slug' => '', + 'recurring' => '', 'trial_duration' => 0, 'trial_duration_unit' => 'day', 'duration' => 1, 'duration_unit' => 'day', - 'amount' => false, - 'billing_cycles' => false, - 'active' => false, - 'type' => false, + 'amount' => '', + 'billing_cycles' => '', + 'active' => '', + 'type' => '', 'featured_image_id' => 0, 'list_order' => 0, 'date_created' => wu_get_current_time('mysql', true), diff --git a/inc/functions/site.php b/inc/functions/site.php index 1069aed4..33ead7cb 100644 --- a/inc/functions/site.php +++ b/inc/functions/site.php @@ -228,10 +228,29 @@ function wu_create_site($site_data) { $current_site = get_current_site(); + $network_domain = $current_site->domain; + + // Mode-aware domain/path normalisation. When the caller passes a path + // but no explicit domain (or only the network root domain) on a + // subdomain multisite install, the path is meaningless — WordPress + // won't route to it. Convert the supplied path into a subdomain + // prefix instead so the site is reachable. Callers that pass an + // explicit non-root domain are respected as-is so subdomain/subdir + // mixing still works. + $path_supplied = isset($site_data['path']) && '' !== $site_data['path'] && '/' !== $site_data['path']; + $domain_supplied = isset($site_data['domain']) && '' !== $site_data['domain'] && $site_data['domain'] !== $network_domain; + + if (is_multisite() && is_subdomain_install() && $path_supplied && ! $domain_supplied) { + $slug = trim((string) $site_data['path'], '/'); + $bare_network_domain = preg_replace('/^www\./i', '', (string) $network_domain); + $site_data['domain'] = "{$slug}.{$bare_network_domain}"; + $site_data['path'] = '/'; + } + $site_data = wp_parse_args( $site_data, [ - 'domain' => $current_site->domain, + 'domain' => $network_domain, 'path' => '/', 'title' => false, 'type' => false, diff --git a/inc/models/class-broadcast.php b/inc/models/class-broadcast.php index 59de6032..90db41f6 100644 --- a/inc/models/class-broadcast.php +++ b/inc/models/class-broadcast.php @@ -129,7 +129,8 @@ public function validation_rules() { 'name' => 'default:title', 'title' => 'required|min:2', 'content' => 'required|min:3', - 'type' => 'required|in:broadcast_email,broadcast_notice|default:broadcast_notice', + // Has a default — `required` is redundant. + 'type' => 'in:broadcast_email,broadcast_notice|default:broadcast_notice', ]; } diff --git a/inc/models/class-customer.php b/inc/models/class-customer.php index 736fce0c..e93f338a 100644 --- a/inc/models/class-customer.php +++ b/inc/models/class-customer.php @@ -176,8 +176,11 @@ public function validation_rules() { return [ 'user_id' => "required|integer|unique:\WP_Ultimo\Models\Customer,user_id,{$id}", - 'email_verification' => 'required|in:none,pending,verified', - 'type' => 'required|in:customer', + // Defaults to "none" so callers don't have to choose a verification + // state on creation — they can update it later if needed. + 'email_verification' => 'in:none,pending,verified|default:none', + // Currently the only valid value, so default it. + 'type' => 'in:customer|default:customer', 'last_login' => 'default:', 'has_trialed' => 'boolean|default:0', 'vip' => 'boolean|default:0', diff --git a/inc/models/class-domain.php b/inc/models/class-domain.php index d30f2ac8..cac48875 100644 --- a/inc/models/class-domain.php +++ b/inc/models/class-domain.php @@ -128,7 +128,8 @@ public function validation_rules() { return [ 'blog_id' => 'required|integer', 'domain' => "required|domain|unique:\WP_Ultimo\Models\Domain,domain,{$id}", - 'stage' => 'required|in:checking-dns,checking-ssl-cert,done-without-ssl,done,failed,ssl-failed|default:checking-dns', + // Has a default — `required` is redundant. + 'stage' => 'in:checking-dns,checking-ssl-cert,done-without-ssl,done,failed,ssl-failed|default:checking-dns', 'active' => 'default:1', 'secure' => 'default:0', 'primary_domain' => 'default:0', diff --git a/inc/models/class-payment.php b/inc/models/class-payment.php index 1455b445..5274c90d 100644 --- a/inc/models/class-payment.php +++ b/inc/models/class-payment.php @@ -244,7 +244,9 @@ public function validation_rules(): array { 'tax_total' => 'numeric', 'discount_code' => 'alpha_dash', 'total' => 'required|numeric', - 'status' => "required|in:{$payment_types}", + // Defaults to "pending" so callers can record a payment without + // having to choose a status up front; gateways update it later. + 'status' => "in:{$payment_types}|default:pending", 'gateway' => 'default:', 'gateway_payment_id' => 'default:', 'discount_total' => 'integer', diff --git a/inc/models/class-product.php b/inc/models/class-product.php index ddf7218d..13c2312d 100644 --- a/inc/models/class-product.php +++ b/inc/models/class-product.php @@ -417,8 +417,11 @@ public function validation_rules() { return [ 'featured_image_id' => 'integer', - 'currency' => "required|default:{$currency}", - 'pricing_type' => 'required|in:free,paid,contact_us,pay_what_you_want', + // Has a default — `required` is redundant. + 'currency' => "default:{$currency}", + // Defaults to "free" so a caller can create a basic product + // without having to choose a pricing model up front. + 'pricing_type' => 'in:free,paid,contact_us,pay_what_you_want|default:free', 'trial_duration' => 'integer', 'trial_duration_unit' => 'in:day,week,month,year|default:month', 'parent_id' => 'integer', @@ -430,7 +433,8 @@ public function validation_rules() { 'billing_cycles' => 'integer|default:0', 'active' => 'default:1', 'price_variations' => "price_variations:{$duration},{$duration_unit}", - 'type' => "required|default:plan|in:{$allowed_types}", + // Has a default — `required` is redundant. + 'type' => "default:plan|in:{$allowed_types}", 'slug' => "required|unique:\WP_Ultimo\Models\Product,slug,{$id}|min:2", 'taxable' => 'boolean|default:0', 'tax_category' => 'default:', diff --git a/inc/models/class-site.php b/inc/models/class-site.php index cd22c17d..27f958a2 100644 --- a/inc/models/class-site.php +++ b/inc/models/class-site.php @@ -331,10 +331,15 @@ public function validation_rules() { return [ 'categories' => 'default:', 'featured_image_id' => 'integer|default:', - 'site_id' => 'required|integer', + // site_id is the network id; defaults to 1 (the main network) in + // the model, so it's optional for a regular WordPress subsite. + 'site_id' => 'integer|default:1', 'title' => 'required', 'name' => 'required', - 'description' => 'required|min:2', + // description is optional metadata. A regular WordPress subsite + // doesn't have one at the network level — it lives in + // blogdescription per blog and can be set later. + 'description' => 'default:', 'domain' => 'domain', 'path' => 'required|default:', 'registered' => "default:{$date}", @@ -346,8 +351,12 @@ public function validation_rules() { 'deleted' => 'boolean|default:0', 'is_publishing' => 'boolean|default:0', 'land_id' => 'integer|default:', - 'customer_id' => 'required|integer|exists:\WP_Ultimo\Models\Customer,id', - 'membership_id' => 'required|integer|exists:\WP_Ultimo\Models\Membership,id', + // customer_id and membership_id are WaaS-specific. A vanilla + // WordPress subsite (type=default) does not need a paying + // customer or a membership. Keep the foreign-key existence + // check so customer-owned sites still validate when supplied. + 'customer_id' => 'integer|default:|exists:\WP_Ultimo\Models\Customer,id', + 'membership_id' => 'integer|default:|exists:\WP_Ultimo\Models\Membership,id', 'template_id' => 'integer|default:', 'type' => "required|in:{$site_types}", 'signup_options' => 'default:', @@ -631,7 +640,7 @@ public function get_domain() { * Set domain name used by this site.. * * @since 2.0.0 - * @param string $domain The site domain. You don't need to put http or https in front of your domain in this field. e.g: example.com. + * @param string $domain Full hostname for the site, no protocol. On a subdomain multisite install supply the full subdomain you want (e.g. "blog.example.com"). On a subdirectory install leave empty to inherit the network root domain. wu_create_site() will auto-derive this from `path` on subdomain installs when you only supply a slug. * @return void */ public function set_domain($domain): void { @@ -653,7 +662,7 @@ public function get_path(): string { * Set path of the site. Used when in sub-directory mode.. * * @since 2.0.0 - * @param string $path Path of the site. Used when in sub-directory mode. + * @param string $path URL path for the site. On a subdirectory multisite install this is the URL prefix (e.g. "/blog/"). On a subdomain install supply just the slug (e.g. "blog") and wu_create_site() will convert it into the full subdomain "blog." automatically. * @return void */ public function set_path($path): void { diff --git a/inc/models/class-webhook.php b/inc/models/class-webhook.php index 95d7d363..dcd7d3ee 100644 --- a/inc/models/class-webhook.php +++ b/inc/models/class-webhook.php @@ -120,7 +120,10 @@ public function validation_rules() { 'event_count' => 'default:0', 'active' => 'default:1', 'hidden' => 'default:0', - 'integration' => 'required|min:2', + // Defaults to "manual" so a caller can register a webhook + // without specifying which integration owns it. Plugins that + // register webhooks set their own integration tag. + 'integration' => 'min:2|default:manual', 'date_last_failed' => 'default:', ]; }