diff --git a/inc/integrations/class-integration-registry.php b/inc/integrations/class-integration-registry.php index 02d5a1bf..c5a91038 100644 --- a/inc/integrations/class-integration-registry.php +++ b/inc/integrations/class-integration-registry.php @@ -128,10 +128,13 @@ private function register_core_integrations(): void { $this->register(new Providers\Cloudflare\Cloudflare_Integration()); $this->register(new Providers\Hestia\Hestia_Integration()); $this->register(new Providers\Enhance\Enhance_Integration()); + $this->register(new Providers\Plesk\Plesk_Integration()); $this->register(new Providers\Rocket\Rocket_Integration()); $this->register(new Providers\WPEngine\WPEngine_Integration()); $this->register(new Providers\WPMUDEV\WPMUDEV_Integration()); - $this->register(new Providers\FrankenPHP\FrankenPHP_Integration()); + $this->register(new Providers\BunnyNet\BunnyNet_Integration()); + $this->register(new Providers\LaravelForge\LaravelForge_Integration()); + $this->register(new Providers\Amazon_SES\Amazon_SES_Integration()); } /** @@ -174,10 +177,13 @@ private function register_core_capabilities(): void { $this->add_capability('cloudflare', new Providers\Cloudflare\Cloudflare_Domain_Mapping()); $this->add_capability('hestia', new Providers\Hestia\Hestia_Domain_Mapping()); $this->add_capability('enhance', new Providers\Enhance\Enhance_Domain_Mapping()); + $this->add_capability('plesk', new Providers\Plesk\Plesk_Domain_Mapping()); $this->add_capability('rocket', new Providers\Rocket\Rocket_Domain_Mapping()); $this->add_capability('wpengine', new Providers\WPEngine\WPEngine_Domain_Mapping()); $this->add_capability('wpmudev', new Providers\WPMUDEV\WPMUDEV_Domain_Mapping()); - $this->add_capability('frankenphp', new Providers\FrankenPHP\FrankenPHP_Domain_Mapping()); + $this->add_capability('bunnynet', new Providers\BunnyNet\BunnyNet_Domain_Mapping()); + $this->add_capability('laravel-forge', new Providers\LaravelForge\LaravelForge_Domain_Mapping()); + $this->add_capability('amazon-ses', new Providers\Amazon_SES\Amazon_SES_Transactional_Email()); } /** diff --git a/inc/integrations/providers/frankenphp/class-frankenphp-domain-mapping.php b/inc/integrations/providers/frankenphp/class-frankenphp-domain-mapping.php deleted file mode 100644 index a64edc53..00000000 --- a/inc/integrations/providers/frankenphp/class-frankenphp-domain-mapping.php +++ /dev/null @@ -1,234 +0,0 @@ - [ - __('Automatically provision Let\'s Encrypt SSL certificates for mapped domains via Caddy.', 'ultimate-multisite'), - __('Validate domain ownership via an internal "ask" endpoint before issuing certificates.', 'ultimate-multisite'), - ], - 'will_not' => [ - __('This integration does not manage DNS records. Domains must already point to this server.', 'ultimate-multisite'), - ], - ]; - } - - /** - * {@inheritdoc} - */ - public function register_hooks(): void { - - add_action('wu_add_domain', [$this, 'on_add_domain'], 10, 2); - add_action('wu_remove_domain', [$this, 'on_remove_domain'], 10, 2); - add_action('wu_add_subdomain', [$this, 'on_add_subdomain'], 10, 2); - add_action('wu_remove_subdomain', [$this, 'on_remove_subdomain'], 10, 2); - - // Register the "ask" endpoint for Caddy's on-demand TLS validation. - add_action('rest_api_init', [$this, 'register_ask_endpoint']); - } - - /** - * Register the REST endpoint that Caddy calls to validate domains - * before issuing on-demand TLS certificates. - * - * @return void - */ - public function register_ask_endpoint(): void { - - register_rest_route('wu-caddy/v1', '/ask', [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [$this, 'handle_ask_request'], - 'permission_callback' => [$this, 'validate_ask_request'], - ]); - } - - /** - * Only allow requests from localhost (Caddy admin API). - * - * @param \WP_REST_Request $request The request. - * @return bool - */ - public function validate_ask_request(\WP_REST_Request $request): bool { - - $ip = $_SERVER['REMOTE_ADDR'] ?? ''; - - return in_array($ip, ['127.0.0.1', '::1', ''], true); - } - - /** - * Handle the "ask" request from Caddy's on-demand TLS. - * - * Caddy sends GET /wp-json/wu-caddy/v1/ask?domain=example.com - * Return 200 if the domain is valid, 403 otherwise. - * - * @param \WP_REST_Request $request The request. - * @return \WP_REST_Response - */ - public function handle_ask_request(\WP_REST_Request $request): \WP_REST_Response { - - $domain = sanitize_text_field($request->get_param('domain')); - - if (empty($domain)) { - return new \WP_REST_Response(['error' => 'missing domain'], 400); - } - - if ($this->is_valid_multisite_domain($domain)) { - return new \WP_REST_Response(['ok' => true], 200); - } - - return new \WP_REST_Response(['error' => 'unknown domain'], 403); - } - - /** - * Check if a domain belongs to this WordPress multisite. - * - * Checks both the wp_blogs table (subdomains) and the Ultimate Multisite - * domain mapping table. - * - * @param string $domain The domain to check. - * @return bool - */ - private function is_valid_multisite_domain(string $domain): bool { - - global $wpdb; - - // Check wp_blogs table (covers subdomains and primary domains). - $blog_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT blog_id FROM {$wpdb->blogs} WHERE domain = %s LIMIT 1", - $domain - ) - ); - - if ($blog_id) { - return true; - } - - // Check Ultimate Multisite domain mapping table. - $table = $wpdb->base_prefix . 'wu_domain_mappings'; - - if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table) { - $mapping_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT id FROM {$table} WHERE domain = %s AND active = 1 LIMIT 1", - $domain - ) - ); - - if ($mapping_id) { - return true; - } - } - - return false; - } - - /** - * Called when a new domain is mapped. - * - * Pre-provisions a Let's Encrypt certificate via Caddy's admin API - * so the first visitor doesn't experience a TLS handshake delay. - * - * @param string $domain The domain name being mapped. - * @param int $site_id ID of the site receiving the mapping. - * On-demand TLS handles certificate provisioning automatically on first - * TLS handshake. The ask endpoint validates the domain. No explicit - * provisioning needed — just log for observability. - * - * @return void - */ - public function on_add_domain(string $domain, int $site_id): void { - - if (function_exists('wu_log_add')) { - wu_log_add('integration-frankenphp', "Domain added: {$domain} (cert will be provisioned on first visit via on-demand TLS)"); - } - } - - /** - * {@inheritdoc} - * - * Caddy automatically stops renewing certs for domains that fail the - * ask endpoint check, so no explicit cleanup is needed. - */ - public function on_remove_domain(string $domain, int $site_id): void { - - if (function_exists('wu_log_add')) { - wu_log_add('integration-frankenphp', "Domain removed: {$domain} (cert will expire naturally)"); - } - } - - /** - * {@inheritdoc} - */ - public function on_add_subdomain(string $subdomain, int $site_id): void { - } - - /** - * {@inheritdoc} - */ - public function on_remove_subdomain(string $subdomain, int $site_id): void { - } - - /** - * {@inheritdoc} - */ - public function test_connection() { - - return $this->get_integration()->test_connection(); - } -} diff --git a/inc/integrations/providers/frankenphp/class-frankenphp-integration.php b/inc/integrations/providers/frankenphp/class-frankenphp-integration.php deleted file mode 100644 index 7a452309..00000000 --- a/inc/integrations/providers/frankenphp/class-frankenphp-integration.php +++ /dev/null @@ -1,139 +0,0 @@ -set_description( - __('FrankenPHP (Caddy) integration with automatic Let\'s Encrypt SSL for mapped domains.', 'ultimate-multisite') - ); - $this->set_logo(function_exists('wu_get_asset') ? wu_get_asset('frankenphp.svg', 'img/hosts') : ''); - $this->set_supports(['autossl', 'no-instructions', 'no-config']); - } - - /** - * Auto-detect FrankenPHP by checking for the FRANKENPHP_WORKER constant - * or the frankenphp SAPI name. - * - * @return bool - */ - public function detect(): bool { - - // FrankenPHP sets this in worker mode. - if (defined('FRANKENPHP_WORKER') || php_sapi_name() === 'frankenphp') { - return true; - } - - // Fallback: check if Caddy admin API is reachable. - $response = wp_remote_get($this->admin_api . '/config/', [ - 'timeout' => 2, - 'sslverify' => false, - ]); - - return ! is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200; - } - - /** - * Test connection to the Caddy admin API. - * - * @return true|\WP_Error - */ - public function test_connection() { - - $response = wp_remote_get($this->admin_api . '/config/', [ - 'timeout' => 5, - 'sslverify' => false, - ]); - - if (is_wp_error($response)) { - return $response; - } - - $code = wp_remote_retrieve_response_code($response); - - if ($code !== 200) { - return new \WP_Error( - 'caddy_api_error', - sprintf(__('Caddy admin API returned HTTP %d', 'ultimate-multisite'), $code) - ); - } - - return true; - } - - /** - * Get the Caddy admin API base URL. - * - * @return string - */ - public function get_admin_api(): string { - - return $this->admin_api; - } - - /** - * Send a request to the Caddy admin API. - * - * @param string $endpoint API endpoint path. - * @param array $body Request body (will be JSON-encoded). - * @param string $method HTTP method. - * @return array|\WP_Error Decoded response or error. - */ - public function api_call(string $endpoint, array $body = [], string $method = 'POST') { - - $args = [ - 'method' => $method, - 'timeout' => 30, - 'sslverify' => false, - 'headers' => ['Content-Type' => 'application/json'], - ]; - - if (! empty($body)) { - $args['body'] = wp_json_encode($body); - } - - $response = wp_remote_request($this->admin_api . $endpoint, $args); - - if (is_wp_error($response)) { - return $response; - } - - $decoded = json_decode(wp_remote_retrieve_body($response), true); - - return is_array($decoded) ? $decoded : []; - } -}