feat: initial WooAApanel plugin

Full WooCommerce plugin for aaPanel hosting management:
- aaPanel API client (all website + database endpoints)
- Admin: server management, site/DB assignments, full site/DB management panels
- Customer My Account: web hosting tab with sites and databases
- WooDomains PowerDNS integration for DNS management
- WooCommerce order auto-provisioning (product → server linking)
- Permission model: admin has all actions, customers have scoped access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 12:48:06 +01:00
commit 8bb96b9048
10 changed files with 3458 additions and 0 deletions

View File

@@ -0,0 +1,550 @@
<?php
/**
* WooAApanel Account WooCommerce My Account integration.
*
* Adds a "Web Hosting" tab where customers can manage their assigned sites
* and databases. DNS management is provided via WooDomains PowerDNS if
* that plugin is active.
*/
defined( 'ABSPATH' ) || exit;
class WooAApanel_Account {
const ENDPOINT = 'web-hosting';
public function __construct() {
add_action( 'init', [ $this, 'register_endpoint' ] );
add_filter( 'woocommerce_account_menu_items', [ $this, 'add_menu_item' ] );
add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', [ $this, 'render_page' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
$actions = [
// Sites
'wooaapanel_acct_sites',
'wooaapanel_acct_site_domains',
'wooaapanel_acct_site_add_domain',
'wooaapanel_acct_site_del_domain',
'wooaapanel_acct_site_xss_get',
'wooaapanel_acct_site_xss_set',
'wooaapanel_acct_site_php_get',
'wooaapanel_acct_site_php_versions',
'wooaapanel_acct_site_php_set',
'wooaapanel_acct_site_rewrite_get',
'wooaapanel_acct_site_rewrite_set',
'wooaapanel_acct_site_ssl_get',
'wooaapanel_acct_server_info',
// Databases
'wooaapanel_acct_dbs',
'wooaapanel_acct_db_backup',
'wooaapanel_acct_db_backups',
'wooaapanel_acct_db_backup_delete',
'wooaapanel_acct_db_optimize',
'wooaapanel_acct_db_repair',
'wooaapanel_acct_db_password',
];
foreach ( $actions as $action ) {
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
}
}
public function register_endpoint(): void {
add_rewrite_endpoint( self::ENDPOINT, EP_ROOT | EP_PAGES );
}
public function add_menu_item( array $items ): array {
$logout = $items['customer-logout'] ?? null;
unset( $items['customer-logout'] );
$items[ self::ENDPOINT ] = __( 'Web Hosting', 'wooaapanel' );
if ( $logout ) {
$items['customer-logout'] = $logout;
}
return $items;
}
public function enqueue_assets(): void {
if ( ! is_account_page() ) {
return;
}
wp_enqueue_style( 'wooaapanel-account', WOOAAPANEL_PLUGIN_URL . 'assets/css/wooaapanel-account.css', [], WOOAAPANEL_VERSION );
wp_enqueue_script( 'wooaapanel-account', WOOAAPANEL_PLUGIN_URL . 'assets/js/wooaapanel-account.js', [ 'jquery' ], WOOAAPANEL_VERSION, true );
wp_localize_script( 'wooaapanel-account', 'wooaapanelAcct', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wooaapanel_account' ),
'pdns_nonce' => class_exists( 'WooDomains_PowerDNS_API' ) ? wp_create_nonce( 'woodomains_nonce' ) : '',
'pdns_active' => class_exists( 'WooDomains_PowerDNS_API' ) ? 1 : 0,
'ajax_url_woodomains' => admin_url( 'admin-ajax.php' ),
] );
}
// ── My Account page render ────────────────────────────────────────────────
public function render_page(): void {
if ( ! is_user_logged_in() ) {
echo '<p>' . esc_html__( 'Please log in to manage your hosting.', 'wooaapanel' ) . '</p>';
return;
}
$customer_id = get_current_user_id();
$site_assigns = $this->get_site_assignments( $customer_id );
$db_assigns = $this->get_db_assignments( $customer_id );
$pdns_active = class_exists( 'WooDomains_PowerDNS_API' );
if ( empty( $site_assigns ) && empty( $db_assigns ) ) {
echo '<div class="woocommerce-info">' . esc_html__( 'You have no hosting resources assigned yet. Please contact support.', 'wooaapanel' ) . '</div>';
return;
}
?>
<div class="wooaapanel-account" id="wooaapanel-account">
<div id="wap-acct-notices"></div>
<?php if ( ! empty( $site_assigns ) ) : ?>
<h2><?php esc_html_e( 'Websites', 'wooaapanel' ); ?></h2>
<?php foreach ( $site_assigns as $assign ) : ?>
<div class="wap-site-panel"
data-assignment-id="<?php echo esc_attr( $assign->id ); ?>"
data-server-id="<?php echo esc_attr( $assign->server_id ); ?>"
data-site-name="<?php echo esc_attr( $assign->site_name ); ?>"
data-domain="<?php echo esc_attr( $assign->domain ); ?>">
<div class="wap-panel-header">
<span class="wap-site-name"><?php echo esc_html( $assign->site_name ); ?></span>
<span class="wap-domain"><?php echo esc_html( $assign->domain ); ?></span>
<span class="wap-server-label"><?php echo esc_html( $assign->server_name ); ?></span>
<div class="wap-panel-actions">
<button class="button wap-btn-sm wap-load-domains"><?php esc_html_e( 'Domains', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-load-php"><?php esc_html_e( 'PHP', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-load-rewrite"><?php esc_html_e( 'URL Rewrite', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-load-ssl"><?php esc_html_e( 'SSL', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-load-server-info"><?php esc_html_e( 'Server Info', 'wooaapanel' ); ?></button>
<?php if ( $pdns_active && $assign->domain ) : ?>
<button class="button wap-btn-sm wap-load-dns"
data-zone="<?php echo esc_attr( rtrim( $assign->domain, '.' ) . '.' ); ?>">
<?php esc_html_e( 'DNS Records', 'wooaapanel' ); ?>
</button>
<?php endif; ?>
</div>
</div><!-- .wap-panel-header -->
<!-- Domains -->
<div class="wap-section wap-domains-section" style="display:none">
<h4><?php esc_html_e( 'Domain Names', 'wooaapanel' ); ?></h4>
<div class="wap-domains-list"></div>
<div class="wap-add-domain-form">
<input type="text" class="wap-new-domain input-text" placeholder="newdomain.com">
<button class="button wap-add-domain-btn"><?php esc_html_e( 'Add Domain', 'wooaapanel' ); ?></button>
<span class="wap-domain-notice"></span>
</div>
</div>
<!-- PHP Version -->
<div class="wap-section wap-php-section" style="display:none">
<h4><?php esc_html_e( 'PHP Version', 'wooaapanel' ); ?></h4>
<div class="wap-php-current"></div>
<select class="wap-php-select"></select>
<button class="button wap-php-set-btn"><?php esc_html_e( 'Set PHP Version', 'wooaapanel' ); ?></button>
<span class="wap-php-notice"></span>
</div>
<!-- URL Rewrite -->
<div class="wap-section wap-rewrite-section" style="display:none">
<h4><?php esc_html_e( 'URL Rewrite', 'wooaapanel' ); ?></h4>
<textarea class="wap-rewrite-content" rows="10" style="width:100%;font-family:monospace"></textarea>
<button class="button wap-rewrite-save-btn"><?php esc_html_e( 'Save Rewrite', 'wooaapanel' ); ?></button>
<span class="wap-rewrite-notice"></span>
</div>
<!-- SSL Info -->
<div class="wap-section wap-ssl-section" style="display:none">
<h4><?php esc_html_e( 'SSL Certificate', 'wooaapanel' ); ?></h4>
<div class="wap-ssl-info"></div>
</div>
<!-- XSS -->
<div class="wap-section wap-xss-section" style="display:none">
<h4><?php esc_html_e( 'Anti-XSS Protection', 'wooaapanel' ); ?></h4>
<div class="wap-xss-status"></div>
<button class="button wap-xss-toggle-btn"><?php esc_html_e( 'Toggle XSS Protection', 'wooaapanel' ); ?></button>
<span class="wap-xss-notice"></span>
</div>
<!-- Server Info -->
<div class="wap-section wap-server-section" style="display:none">
<h4><?php esc_html_e( 'Server Information', 'wooaapanel' ); ?></h4>
<div class="wap-server-info"></div>
</div>
<!-- DNS (WooDomains PowerDNS integration) -->
<?php if ( $pdns_active && $assign->domain ) : ?>
<div class="wap-section wap-dns-section" style="display:none"
data-zone="<?php echo esc_attr( rtrim( $assign->domain, '.' ) . '.' ); ?>">
<h4><?php esc_html_e( 'DNS Records', 'wooaapanel' ); ?></h4>
<div class="wap-dns-records-list"></div>
<hr>
<h5><?php esc_html_e( 'Add / Update Record', 'wooaapanel' ); ?></h5>
<div class="wap-dns-add-form">
<label><?php esc_html_e( 'Name', 'wooaapanel' ); ?> <input type="text" class="wap-dns-name input-text" placeholder="<?php echo esc_attr( $assign->domain . '.' ); ?>"></label>
<label><?php esc_html_e( 'Type', 'wooaapanel' ); ?>
<select class="wap-dns-type">
<?php foreach ( [ 'A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV', 'CAA' ] as $t ) : ?>
<option><?php echo esc_html( $t ); ?></option>
<?php endforeach; ?>
</select>
</label>
<label><?php esc_html_e( 'TTL', 'wooaapanel' ); ?> <input type="number" class="wap-dns-ttl input-text" value="3600" min="60" style="width:80px"></label>
<label><?php esc_html_e( 'Content (one per line)', 'wooaapanel' ); ?><br>
<textarea class="wap-dns-content input-text" rows="3"></textarea>
</label>
<button class="button wap-dns-save-btn"><?php esc_html_e( 'Save Record', 'wooaapanel' ); ?></button>
<span class="wap-dns-notice"></span>
</div>
</div>
<?php endif; ?>
</div><!-- .wap-site-panel -->
<?php endforeach; ?>
<?php endif; ?>
<?php if ( ! empty( $db_assigns ) ) : ?>
<h2><?php esc_html_e( 'Databases', 'wooaapanel' ); ?></h2>
<?php foreach ( $db_assigns as $assign ) : ?>
<div class="wap-db-panel"
data-assignment-id="<?php echo esc_attr( $assign->id ); ?>"
data-server-id="<?php echo esc_attr( $assign->server_id ); ?>"
data-db-name="<?php echo esc_attr( $assign->db_name ); ?>">
<div class="wap-panel-header">
<span class="wap-db-name"><strong><?php echo esc_html( $assign->db_name ); ?></strong></span>
<span class="wap-server-label"><?php echo esc_html( $assign->server_name ); ?></span>
<div class="wap-panel-actions">
<button class="button wap-btn-sm wap-db-backup-btn"><?php esc_html_e( 'Backup', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-load-db-backups"><?php esc_html_e( 'Backups', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-db-optimize-btn"><?php esc_html_e( 'Optimize', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-db-repair-btn"><?php esc_html_e( 'Repair', 'wooaapanel' ); ?></button>
<button class="button wap-btn-sm wap-db-password-btn"><?php esc_html_e( 'Change Password', 'wooaapanel' ); ?></button>
</div>
</div>
<div class="wap-db-action-notice"></div>
<!-- Backup list -->
<div class="wap-section wap-db-backups-section" style="display:none">
<h4><?php esc_html_e( 'Database Backups', 'wooaapanel' ); ?></h4>
<div class="wap-db-backups-list"></div>
</div>
<!-- Change password form -->
<div class="wap-section wap-db-password-section" style="display:none">
<h4><?php esc_html_e( 'Change Database Password', 'wooaapanel' ); ?></h4>
<input type="text" class="wap-db-user input-text" placeholder="<?php esc_attr_e( 'Database user', 'wooaapanel' ); ?>">
<input type="password" class="wap-db-new-pass input-text" placeholder="<?php esc_attr_e( 'New password', 'wooaapanel' ); ?>">
<button class="button wap-db-pass-save-btn"><?php esc_html_e( 'Save Password', 'wooaapanel' ); ?></button>
<span class="wap-db-pass-notice"></span>
</div>
</div><!-- .wap-db-panel -->
<?php endforeach; ?>
<?php endif; ?>
</div><!-- #wooaapanel-account -->
<?php
}
// ── Helpers ───────────────────────────────────────────────────────────────
private function get_site_assignments( int $customer_id ): array {
global $wpdb;
return $wpdb->get_results( $wpdb->prepare( "
SELECT a.id, a.server_id, a.site_name, a.domain,
s.name AS server_name, s.url AS server_url
FROM {$wpdb->prefix}wooaapanel_site_assignments a
JOIN {$wpdb->prefix}wooaapanel_servers s ON s.id = a.server_id
WHERE a.customer_id = %d AND s.active = 1
ORDER BY a.site_name
", $customer_id ) );
}
private function get_db_assignments( int $customer_id ): array {
global $wpdb;
return $wpdb->get_results( $wpdb->prepare( "
SELECT a.id, a.server_id, a.db_name,
s.name AS server_name
FROM {$wpdb->prefix}wooaapanel_db_assignments a
JOIN {$wpdb->prefix}wooaapanel_servers s ON s.id = a.server_id
WHERE a.customer_id = %d AND s.active = 1
ORDER BY a.db_name
", $customer_id ) );
}
private function get_server( int $id ): ?object {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wooaapanel_servers WHERE id = %d AND active = 1",
$id
) );
}
/** Verify nonce + login. */
private function account_verify(): void {
check_ajax_referer( 'wooaapanel_account', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( 'Not logged in.', 401 );
}
}
/** Confirm the current user owns this site assignment. */
private function verify_site_ownership( int $server_id, string $site_name ): bool {
global $wpdb;
return (bool) $wpdb->get_var( $wpdb->prepare( "
SELECT id FROM {$wpdb->prefix}wooaapanel_site_assignments
WHERE customer_id = %d AND server_id = %d AND site_name = %s
", get_current_user_id(), $server_id, $site_name ) );
}
/** Confirm the current user owns this DB assignment. */
private function verify_db_ownership( int $server_id, string $db_name ): bool {
global $wpdb;
return (bool) $wpdb->get_var( $wpdb->prepare( "
SELECT id FROM {$wpdb->prefix}wooaapanel_db_assignments
WHERE customer_id = %d AND server_id = %d AND db_name = %s
", get_current_user_id(), $server_id, $db_name ) );
}
private function api_for_site( int $server_id, string $site_name ): WooAApanel_API {
if ( ! $this->verify_site_ownership( $server_id, $site_name ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
return WooAApanel_API::from_server( $server );
}
private function api_for_db( int $server_id, string $db_name ): WooAApanel_API {
if ( ! $this->verify_db_ownership( $server_id, $db_name ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
return WooAApanel_API::from_server( $server );
}
// ═════════════════════════════════════════════════════════════════════════
// AJAX: Account Sites
// ═════════════════════════════════════════════════════════════════════════
public function ajax_wooaapanel_acct_sites(): void {
$this->account_verify();
$sites = $this->get_site_assignments( get_current_user_id() );
$dbs = $this->get_db_assignments( get_current_user_id() );
wp_send_json_success( [ 'sites' => $sites, 'databases' => $dbs ] );
}
public function ajax_wooaapanel_acct_site_domains(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_site_domains( $site_name ) );
}
public function ajax_wooaapanel_acct_site_add_domain(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
if ( ! $domain ) {
wp_send_json_error( 'Domain is required.' );
}
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->add_domain( $site_name, $domain ) );
}
public function ajax_wooaapanel_acct_site_del_domain(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$site_id = absint( $_POST['site_id'] ?? 0 );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->delete_domain( $site_name, $domain, $site_id ) );
}
public function ajax_wooaapanel_acct_site_xss_get(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$site_path = sanitize_text_field( $_POST['site_path'] ?? "/www/wwwroot/{$site_name}" );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_xss( $site_name, $site_path ) );
}
public function ajax_wooaapanel_acct_site_xss_set(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$site_path = sanitize_text_field( $_POST['site_path'] ?? "/www/wwwroot/{$site_name}" );
$enable = ! empty( $_POST['enable'] );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->set_xss( $site_name, $site_path, $enable ) );
}
public function ajax_wooaapanel_acct_site_php_get(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_site_php_version( $site_name ) );
}
public function ajax_wooaapanel_acct_site_php_versions(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_php_versions() );
}
public function ajax_wooaapanel_acct_site_php_set(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$version = sanitize_text_field( $_POST['version'] ?? '' );
if ( ! $version ) {
wp_send_json_error( 'PHP version is required.' );
}
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->set_site_php_version( $site_name, $version ) );
}
public function ajax_wooaapanel_acct_site_rewrite_get(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$template = sanitize_text_field( $_POST['template'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_rewrite_content( $site_name, $template ) );
}
public function ajax_wooaapanel_acct_site_rewrite_set(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$content = wp_unslash( $_POST['content'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->set_rewrite( $site_name, $content ) );
}
public function ajax_wooaapanel_acct_site_ssl_get(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
$api = $this->api_for_site( $server_id, $site_name );
wp_send_json( $api->get_ssl( $site_name ) );
}
public function ajax_wooaapanel_acct_server_info(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$site_name = sanitize_text_field( $_POST['site_name'] ?? '' );
// Fetch the server row to expose only safe fields (name, IP visible via panel URL host).
if ( ! $this->verify_site_ownership( $server_id, $site_name ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
wp_send_json_error( 'Server unavailable.' );
}
// Parse IP from URL.
$host = parse_url( $server->url, PHP_URL_HOST );
// External IP via aaPanel status endpoint.
$api = WooAApanel_API::from_server( $server );
$res = $api->get_server_info();
wp_send_json_success( [
'server_name' => $server->name,
'server_host' => $host,
'panel_url' => $server->url,
'panel_status' => $res['data'] ?? [],
] );
}
// ═════════════════════════════════════════════════════════════════════════
// AJAX: Account Databases
// ═════════════════════════════════════════════════════════════════════════
public function ajax_wooaapanel_acct_dbs(): void {
$this->account_verify();
$dbs = $this->get_db_assignments( get_current_user_id() );
wp_send_json_success( $dbs );
}
public function ajax_wooaapanel_acct_db_backup(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->backup_database( $db_name ) );
}
public function ajax_wooaapanel_acct_db_backups(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->get_db_backups( $db_name ) );
}
public function ajax_wooaapanel_acct_db_backup_delete(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$backup_id = absint( $_POST['backup_id'] ?? 0 );
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->delete_db_backup( $backup_id, $db_name ) );
}
public function ajax_wooaapanel_acct_db_optimize(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->optimize_table( $db_name ) );
}
public function ajax_wooaapanel_acct_db_repair(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->repair_table( $db_name ) );
}
public function ajax_wooaapanel_acct_db_password(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$db_name = sanitize_text_field( $_POST['db_name'] ?? '' );
$db_user = sanitize_text_field( $_POST['db_user'] ?? '' );
$password = $_POST['password'] ?? '';
if ( ! $db_user || ! $password ) {
wp_send_json_error( 'Database user and new password are required.' );
}
$api = $this->api_for_db( $server_id, $db_name );
wp_send_json( $api->reset_db_password( $db_name, $db_user, $password ) );
}
}