Skip to content

Conversation

@superdav42
Copy link
Collaborator

@superdav42 superdav42 commented Dec 3, 2025

Summary by CodeRabbit

Release Notes

  • New Features
    • Added BunnyNet integration for DNS and CDN management within the platform
    • Automatic DNS record creation and management for new domains and subdomains
    • Configuration interface to set up BunnyNet Zone ID and API Key credentials
    • Connection testing functionality to validate BunnyNet account settings
    • Comprehensive setup guide with step-by-step integration instructions and best practices

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

Introduces BunnyNet_Host_Provider, a new integration class for BunnyNet CDN/DNS services within WP_Ultimo. Implements domain and subdomain lifecycle hooks for DNS record management, BunnyNet API communication, connection testing, and installation configuration. Includes setup instructions view file.

Changes

Cohort / File(s) Summary
BunnyNet Host Provider Implementation
inc/integrations/host-providers/class-bunnynet-host-provider.php
New host provider class with DNS augmentation (add_bunnynet_dns_entries), API wrapper (bunnynet_api_call), domain/subdomain lifecycle hooks (on_add_domain, on_remove_domain, on_add_subdomain, on_remove_subdomain), connection validation, installation fields for Zone ID and API Key, and UI rendering methods for instructions, description, logo, and explainer content.
Setup Instructions
views/wizards/host-integrations/bunnynet-instructions.php
New view file with translatable, step-by-step BunnyNet integration instructions covering API key retrieval, DNS zone creation, zone ID configuration, DNS setup, and subdomain management.

Sequence Diagram

sequenceDiagram
    actor Admin
    participant WP_Ultimo
    participant BunnyNet_Provider
    participant BunnyNet_API
    participant DNS_System

    Admin->>WP_Ultimo: Add new subdomain
    WP_Ultimo->>BunnyNet_Provider: on_add_subdomain(subdomain, site_id)
    BunnyNet_Provider->>BunnyNet_Provider: Construct domain & API payload
    BunnyNet_Provider->>BunnyNet_API: bunnynet_api_call(endpoint, 'POST', dns_data)
    BunnyNet_API->>DNS_System: Create DNS records
    BunnyNet_API-->>BunnyNet_Provider: Confirm records created
    BunnyNet_Provider->>WP_Ultimo: Log success
    WP_Ultimo-->>Admin: Subdomain configured

    rect rgb(240, 248, 255)
    note right of BunnyNet_Provider: Handles www subdomain<br/>automatically if configured
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • API integration & error handling: Comprehensive error handling in bunnynet_api_call wrapper; validate HTTP status codes and exception management
  • Lifecycle hook logic: Review on_add_subdomain and on_remove_subdomain for correctness of API call construction and subdomain name handling (especially www-prefixing logic)
  • DNS record augmentation: Examine add_bunnynet_dns_entries for proper query filtering and record mapping from BunnyNet zone data
  • Security considerations: Verify Zone ID and API Key are handled securely and not exposed in logs or UI
  • Field validation: Check test_connection implementation and get_fields configuration for proper input sanitization and validation

Poem

🐰 A fluffy new provider hops into view,
BunnyNet DNS magic, records so true!
From zones to subdomains, with APIs that sing,
Domain lifecycle management brings delight to everything! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a complete BunnyNet CDN/DNS integration to the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bunny

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
inc/integrations/host-providers/class-bunnynet-host-provider.php (1)

160-175: Consider adding explicit return for clarity.

While wp_send_json_error() terminates execution internally, adding an explicit return after line 166 would improve code readability and make the control flow clearer.

 		if (! $zone_id) {
 			wp_send_json_error(new \WP_Error('bunnynet-error', __('Zone ID is required.', 'ultimate-multisite')));
+			return;
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 165ab6e and da97ae7.

⛔ Files ignored due to path filters (1)
  • assets/img/hosts/bunnynet.svg is excluded by !**/*.svg
📒 Files selected for processing (2)
  • inc/integrations/host-providers/class-bunnynet-host-provider.php (1 hunks)
  • views/wizards/host-integrations/bunnynet-instructions.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
inc/integrations/host-providers/class-bunnynet-host-provider.php (2)
inc/integrations/host-providers/class-base-host-provider.php (1)
  • supports (337-340)
inc/managers/class-domain-manager.php (1)
  • should_create_www_subdomain (461-484)
🪛 PHPMD (2.15.0)
inc/integrations/host-providers/class-bunnynet-host-provider.php

196-196: Avoid unused parameters such as '$domain'. (undefined)

(UnusedFormalParameter)


196-196: Avoid unused parameters such as '$site_id'. (undefined)

(UnusedFormalParameter)


206-206: Avoid unused parameters such as '$domain'. (undefined)

(UnusedFormalParameter)


206-206: Avoid unused parameters such as '$site_id'. (undefined)

(UnusedFormalParameter)


293-293: Avoid unused parameters such as '$site_id'. (undefined)

(UnusedFormalParameter)


307-307: Avoid unused local variables such as '$original_subdomain'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (6)
views/wizards/host-integrations/bunnynet-instructions.php (1)

1-102: LGTM!

The instructions view is well-structured with proper WordPress i18n escaping using esc_html_e(). The five-step guide covers the essential BunnyNet setup workflow including security guidance for the API key.

inc/integrations/host-providers/class-bunnynet-host-provider.php (5)

21-68: LGTM!

Class declaration follows the established pattern from Base_Host_Provider. The $supports array and $constants definitions are correctly configured for BunnyNet integration.


79-119: LGTM!

The DNS entries augmentation logic correctly handles the BunnyNet API response, filters records by domain, and properly translates the @ symbol for root domain records.


121-152: LGTM!

The detect() stub and get_fields() implementation are appropriate. The field order shows Zone ID first but the $constants array has API Key first - this is fine as field order is for UI while constants are for validation.


196-206: Empty stubs are intentional.

The unused parameters flagged by static analysis are expected here - these are interface methods from Base_Host_Provider that must maintain the signature even when the implementation doesn't use them.


437-453: LGTM!

The get_explainer_lines() method correctly differentiates between subdomain and subdirectory installs, providing appropriate messaging for each context.

Comment on lines +256 to +277
foreach ($domains_to_send as $subdomain) {
$server_addr = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : '';

$data = apply_filters(
'wu_bunnynet_on_add_domain_data',
[
'Type' => 0, // A record type
'Ttl' => 3600,
'Name' => $subdomain,
'Value' => $server_addr,
],
$subdomain,
$site_id
);

$results = $this->bunnynet_api_call("dnszone/$zone_id/records", 'PUT', $data);

if (is_wp_error($results)) {
wu_log_add('integration-bunnynet', sprintf('Failed to add subdomain "%s" to BunnyNet. Reason: %s', $subdomain, $results->get_error_message()), LogLevel::ERROR);

return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Variable shadowing and potential empty IP address.

Two concerns in this loop:

  1. Variable shadowing: Reusing $subdomain in the foreach (line 256) shadows the outer variable, reducing readability.

  2. Empty IP address: $_SERVER['SERVER_ADDR'] may be empty in CLI contexts, load balancer setups, or CGI environments. This would create DNS records pointing to an empty value.

-		foreach ($domains_to_send as $subdomain) {
+		foreach ($domains_to_send as $subdomain_to_add) {
 			$server_addr = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : '';
 
+			if (empty($server_addr)) {
+				wu_log_add('integration-bunnynet', 'SERVER_ADDR is not available. Cannot determine server IP for DNS record.', LogLevel::ERROR);
+				return;
+			}
+
 			$data = apply_filters(
 				'wu_bunnynet_on_add_domain_data',
 				[
 					'Type'  => 0, // A record type
 					'Ttl'   => 3600,
-					'Name'  => $subdomain,
+					'Name'  => $subdomain_to_add,
 					'Value' => $server_addr,
 				],
-				$subdomain,
+				$subdomain_to_add,
 				$site_id
 			);
 
 			$results = $this->bunnynet_api_call("dnszone/$zone_id/records", 'PUT', $data);
 
 			if (is_wp_error($results)) {
-				wu_log_add('integration-bunnynet', sprintf('Failed to add subdomain "%s" to BunnyNet. Reason: %s', $subdomain, $results->get_error_message()), LogLevel::ERROR);
+				wu_log_add('integration-bunnynet', sprintf('Failed to add subdomain "%s" to BunnyNet. Reason: %s', $subdomain_to_add, $results->get_error_message()), LogLevel::ERROR);
 
 				return;
 			}
 
-			wu_log_add('integration-bunnynet', sprintf('Added sub-domain "%s" to BunnyNet.', $subdomain));
+			wu_log_add('integration-bunnynet', sprintf('Added sub-domain "%s" to BunnyNet.', $subdomain_to_add));
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ($domains_to_send as $subdomain) {
$server_addr = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : '';
$data = apply_filters(
'wu_bunnynet_on_add_domain_data',
[
'Type' => 0, // A record type
'Ttl' => 3600,
'Name' => $subdomain,
'Value' => $server_addr,
],
$subdomain,
$site_id
);
$results = $this->bunnynet_api_call("dnszone/$zone_id/records", 'PUT', $data);
if (is_wp_error($results)) {
wu_log_add('integration-bunnynet', sprintf('Failed to add subdomain "%s" to BunnyNet. Reason: %s', $subdomain, $results->get_error_message()), LogLevel::ERROR);
return;
}
foreach ($domains_to_send as $subdomain_to_add) {
$server_addr = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) : '';
if (empty($server_addr)) {
wu_log_add('integration-bunnynet', 'SERVER_ADDR is not available. Cannot determine server IP for DNS record.', LogLevel::ERROR);
return;
}
$data = apply_filters(
'wu_bunnynet_on_add_domain_data',
[
'Type' => 0, // A record type
'Ttl' => 3600,
'Name' => $subdomain_to_add,
'Value' => $server_addr,
],
$subdomain_to_add,
$site_id
);
$results = $this->bunnynet_api_call("dnszone/$zone_id/records", 'PUT', $data);
if (is_wp_error($results)) {
wu_log_add('integration-bunnynet', sprintf('Failed to add subdomain "%s" to BunnyNet. Reason: %s', $subdomain_to_add, $results->get_error_message()), LogLevel::ERROR);
return;
}
🤖 Prompt for AI Agents
inc/integrations/host-providers/class-bunnynet-host-provider.php around lines
256-277: the foreach reuses $subdomain (shadowing an outer variable) and reads
$_SERVER['SERVER_ADDR'] which can be empty; rename the loop variable (e.g.
$domain_to_send) to avoid shadowing, move retrieval and sanitization of the
server IP out of the loop so it's computed once, and if the sanitized
SERVER_ADDR is empty attempt fallbacks such as resolving $_SERVER['SERVER_NAME']
or gethostname() with gethostbyname(), and if still empty log an error and skip
adding that DNS record instead of creating a record with an empty value. Ensure
you update variable names used in the data payload and leave the API call and
error handling logic unchanged otherwise.

Comment on lines +307 to +313
$original_subdomain = $subdomain;

$subdomain = rtrim(str_replace($current_site->domain, '', $subdomain), '.');

if (! $subdomain) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused variable.

$original_subdomain is assigned but never used. This appears to be leftover from development.

-		$original_subdomain = $subdomain;
-
 		$subdomain = rtrim(str_replace($current_site->domain, '', $subdomain), '.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$original_subdomain = $subdomain;
$subdomain = rtrim(str_replace($current_site->domain, '', $subdomain), '.');
if (! $subdomain) {
return;
}
$subdomain = rtrim(str_replace($current_site->domain, '', $subdomain), '.');
if (! $subdomain) {
return;
}
🧰 Tools
🪛 PHPMD (2.15.0)

307-307: Avoid unused local variables such as '$original_subdomain'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In inc/integrations/host-providers/class-bunnynet-host-provider.php around lines
307 to 313, the variable $original_subdomain is assigned but never used; remove
the unused assignment to clean up the code by deleting the line that sets
$original_subdomain = $subdomain so only the remaining logic (normalizing
$subdomain and early return) remains.

Comment on lines +362 to +396
protected function bunnynet_api_call($endpoint = 'dnszone', $method = 'GET', $data = []): object {

$api_url = 'https://api.bunny.net/';

$endpoint_url = $api_url . $endpoint;

$args = [
'method' => $method,
'headers' => [
'AccessKey' => defined('WU_BUNNYNET_API_KEY') ? WU_BUNNYNET_API_KEY : '',
'Content-Type' => 'application/json',
],
];

if ('GET' !== $method && ! empty($data)) {
$args['body'] = wp_json_encode($data);
}

$response = wp_remote_request($endpoint_url, $args);

if (! is_wp_error($response)) {
$body = wp_remote_retrieve_body($response);
$code = wp_remote_retrieve_response_code($response);

if ($code >= 200 && $code < 300) {
return json_decode($body);
} else {
$error_message = wp_remote_retrieve_response_message($response);

$response = new \WP_Error('bunnynet-error', sprintf('%s: %s', $error_message, $body));
}
}

return $response;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Return type mismatch: method returns \WP_Error but declares object.

The return type declaration object is incorrect since the method can return \WP_Error. Additionally, json_decode() on an empty body returns null, which would violate the object return type.

-	protected function bunnynet_api_call($endpoint = 'dnszone', $method = 'GET', $data = []): object {
+	protected function bunnynet_api_call($endpoint = 'dnszone', $method = 'GET', $data = []) {

Alternatively, update to a union type if PHP 8.0+ is required:

protected function bunnynet_api_call($endpoint = 'dnszone', $method = 'GET', $data = []): object|\WP_Error {

Also consider handling empty responses:

 			if ($code >= 200 && $code < 300) {
-				return json_decode($body);
+				$decoded = json_decode($body);
+				return $decoded ?? (object) [];
 			} else {
🤖 Prompt for AI Agents
In inc/integrations/host-providers/class-bunnynet-host-provider.php around lines
362 to 396, the method declares a return type of object but may return a
\WP_Error or null from json_decode; change the method signature to a union that
allows \WP_Error and null (or remove the return type if running on PHP <8.0),
and add handling for empty or unparseable bodies: after json_decode, if the body
is empty return an empty object (e.g. (object)[]), and if json_decode returns
null while the body is non-empty return a \WP_Error with the json error; ensure
all return paths match the updated signature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants