Files
WooCow/includes/class-woocow-admin.php
Malin 1ea2ed7e74 feat: admin reset password and set quota for existing mailboxes
- Add Reset PW and Set Quota action buttons to each mailbox row
- Shared edit modal that switches between password / quota modes
- New woocow_admin_mailbox_edit AJAX handler in PHP
- Quota reloads mailbox list after save; password closes modal silently
- Customer-facing Change Password was already implemented in account JS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:21:46 +01:00

636 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* WooCow Admin menu pages + AJAX handlers for the backend.
*/
defined( 'ABSPATH' ) || exit;
class WooCow_Admin {
public function __construct() {
add_action( 'admin_menu', [ $this, 'register_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
$ajax_actions = [
'woocow_servers_list',
'woocow_server_save',
'woocow_server_delete',
'woocow_server_test',
'woocow_server_domains',
'woocow_assignments_list',
'woocow_assignment_save',
'woocow_assignment_delete',
'woocow_customers_search',
'woocow_admin_mailboxes',
'woocow_admin_mailbox_create',
'woocow_admin_mailbox_delete',
'woocow_admin_mailbox_edit',
];
foreach ( $ajax_actions as $action ) {
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
}
}
// ── Menu ─────────────────────────────────────────────────────────────────
public function register_menu(): void {
add_menu_page(
'WooCow',
'WooCow',
'manage_woocommerce',
'woocow',
[ $this, 'page_dashboard' ],
'dashicons-email-alt2',
56
);
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
add_submenu_page( 'woocow', 'Servers', 'Servers', 'manage_woocommerce', 'woocow-servers', [ $this, 'page_servers' ] );
add_submenu_page( 'woocow', 'Assignments','Assignments', 'manage_woocommerce', 'woocow-assignments', [ $this, 'page_assignments' ] );
add_submenu_page( 'woocow', 'Mailboxes', 'Mailboxes', 'manage_woocommerce', 'woocow-mailboxes', [ $this, 'page_mailboxes' ] );
}
public function enqueue_assets( string $hook ): void {
if ( strpos( $hook, 'woocow' ) === false ) {
return;
}
wp_enqueue_style( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
wp_enqueue_script( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/js/woocow-admin.js', [ 'jquery' ], WOOCOW_VERSION, true );
wp_localize_script( 'woocow-admin', 'woocow', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocow_admin' ),
] );
}
// ── Page: Dashboard ──────────────────────────────────────────────────────
public function page_dashboard(): void {
global $wpdb;
$servers = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_servers WHERE active=1" );
$assignments = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_assignments" );
$customers = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT customer_id) FROM {$wpdb->prefix}woocow_assignments" );
?>
<div class="wrap woocow-wrap">
<h1>WooCow <span class="woocow-version">v<?php echo esc_html( WOOCOW_VERSION ); ?></span></h1>
<div class="woocow-dashboard-cards">
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $servers ); ?></span>
<span class="woocow-card-label">Active Servers</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-servers' ) ); ?>" class="button button-secondary">Manage</a>
</div>
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $customers ); ?></span>
<span class="woocow-card-label">Customers with Email</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-assignments' ) ); ?>" class="button button-secondary">Manage</a>
</div>
<div class="woocow-card">
<span class="woocow-card-number"><?php echo esc_html( $assignments ); ?></span>
<span class="woocow-card-label">Domain Assignments</span>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-mailboxes' ) ); ?>" class="button button-secondary">View Mailboxes</a>
</div>
</div>
</div>
<?php
}
// ── Page: Servers ────────────────────────────────────────────────────────
public function page_servers(): void {
?>
<div class="wrap woocow-wrap">
<h1>WooCow Servers</h1>
<p>Add your Mailcow server instances here. The API key must be a read-write key from <strong>Configuration &rarr; Access &rarr; Edit administrator details &rarr; API</strong>.</p>
<div class="woocow-toolbar">
<button class="button button-primary" id="wc-add-server">+ Add Server</button>
</div>
<div id="wc-notices"></div>
<!-- Add / Edit form (hidden by default) -->
<div id="wc-server-form" class="woocow-card woocow-form" style="display:none">
<h3 id="wc-server-form-title">Add Server</h3>
<input type="hidden" id="wc-server-id" value="">
<table class="form-table">
<tr>
<th><label for="wc-server-name">Name</label></th>
<td><input type="text" id="wc-server-name" class="regular-text" placeholder="My Mailcow Server"></td>
</tr>
<tr>
<th><label for="wc-server-url">Server URL</label></th>
<td><input type="url" id="wc-server-url" class="regular-text" placeholder="https://mail.example.com"></td>
</tr>
<tr>
<th><label for="wc-server-key">API Key</label></th>
<td><input type="text" id="wc-server-key" class="regular-text" placeholder="Your read-write API key"></td>
</tr>
<tr>
<th><label for="wc-server-active">Active</label></th>
<td><input type="checkbox" id="wc-server-active" checked></td>
</tr>
</table>
<div class="woocow-form-actions">
<button class="button button-primary" id="wc-server-save">Save Server</button>
<button class="button" id="wc-server-test">Test Connection</button>
<button class="button" id="wc-server-cancel">Cancel</button>
<span id="wc-server-test-result"></span>
</div>
</div>
<!-- Servers table -->
<div id="wc-servers-table-wrap">
<p id="wc-servers-loading">Loading servers…</p>
</div>
</div>
<?php
}
// ── Page: Assignments ────────────────────────────────────────────────────
public function page_assignments(): void {
?>
<div class="wrap woocow-wrap">
<h1>WooCow Domain Assignments</h1>
<p>Assign one or more Mailcow domains to a WooCommerce customer. The customer can then manage mailboxes for those domains from <em>My Account &rarr; Email Hosting</em>.</p>
<div class="woocow-card woocow-form" id="wc-assign-form">
<h3>Assign Domain to Customer</h3>
<table class="form-table">
<tr>
<th><label>Customer</label></th>
<td>
<input type="text" id="wc-cust-search" class="regular-text" placeholder="Search by name or email…" autocomplete="off">
<div id="wc-cust-results" class="woocow-autocomplete"></div>
<input type="hidden" id="wc-cust-id">
<span id="wc-cust-selected" class="woocow-selected-badge"></span>
</td>
</tr>
<tr>
<th><label for="wc-assign-server">Server</label></th>
<td>
<select id="wc-assign-server" class="regular-text">
<option value="">— Select a server —</option>
</select>
</td>
</tr>
<tr id="wc-domain-row" style="display:none">
<th><label for="wc-assign-domain">Domain</label></th>
<td>
<select id="wc-assign-domain" class="regular-text">
<option value="">— Loading domains —</option>
</select>
</td>
</tr>
</table>
<div class="woocow-form-actions">
<button class="button button-primary" id="wc-assign-save">Assign Domain</button>
</div>
<div id="wc-assign-notice"></div>
</div>
<h2>Current Assignments</h2>
<div id="wc-assignments-loading">Loading…</div>
<div id="wc-assignments-table-wrap"></div>
</div>
<?php
}
// ── Page: Mailboxes ──────────────────────────────────────────────────────
public function page_mailboxes(): void {
global $wpdb;
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
?>
<div class="wrap woocow-wrap">
<h1>WooCow Mailboxes</h1>
<div class="woocow-toolbar woocow-flex">
<select id="wc-mb-server" class="regular-text">
<option value="">— Select server —</option>
<?php foreach ( $servers as $s ) : ?>
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
<?php endforeach; ?>
</select>
<select id="wc-mb-domain" class="regular-text" style="display:none">
<option value="">— Select domain —</option>
</select>
<button class="button" id="wc-mb-load" disabled>Load Mailboxes</button>
<button class="button button-primary" id="wc-mb-create" style="display:none">+ Create Mailbox</button>
</div>
<div id="wc-mb-notices"></div>
<div id="wc-mb-table-wrap"></div>
<!-- Edit Mailbox Modal (password / quota) -->
<div id="wc-mb-edit-modal" class="woocow-modal" style="display:none">
<div class="woocow-modal-box">
<h3 id="wc-mb-edit-title">Edit Mailbox</h3>
<p id="wc-mb-edit-subtitle" class="woocow-modal-subtitle"></p>
<div id="wc-mb-edit-pw-section" style="display:none">
<table class="form-table">
<tr>
<th><label for="wc-mb-edit-pass">New Password</label></th>
<td><input type="password" id="wc-mb-edit-pass" class="regular-text" autocomplete="new-password"></td>
</tr>
<tr>
<th><label for="wc-mb-edit-pass2">Confirm Password</label></th>
<td><input type="password" id="wc-mb-edit-pass2" class="regular-text" autocomplete="new-password"></td>
</tr>
</table>
</div>
<div id="wc-mb-edit-quota-section" style="display:none">
<table class="form-table">
<tr>
<th><label for="wc-mb-edit-quota">Max Quota (MB)</label></th>
<td>
<input type="number" id="wc-mb-edit-quota" class="small-text" min="1" step="1">
<p class="description">Current usage is shown in the mailbox list. 1024 MB = 1 GB.</p>
</td>
</tr>
</table>
</div>
<div class="woocow-modal-actions">
<button class="button button-primary" id="wc-mb-edit-save">Save</button>
<button class="button" id="wc-mb-edit-cancel">Cancel</button>
<span id="wc-mb-edit-notice"></span>
</div>
</div>
</div>
<!-- Create Mailbox Modal -->
<div id="wc-mb-modal" class="woocow-modal" style="display:none">
<div class="woocow-modal-box">
<h3>Create Mailbox</h3>
<table class="form-table">
<tr>
<th><label for="wc-mb-local">Local Part</label></th>
<td>
<div class="woocow-flex-inline">
<input type="text" id="wc-mb-local" placeholder="user">
<span class="woocow-at">@</span>
<span id="wc-mb-domain-label" class="woocow-domain-label"></span>
</div>
</td>
</tr>
<tr>
<th><label for="wc-mb-fullname">Full Name</label></th>
<td><input type="text" id="wc-mb-fullname" class="regular-text" placeholder="Jane Doe"></td>
</tr>
<tr>
<th><label for="wc-mb-pass">Password</label></th>
<td><input type="password" id="wc-mb-pass" class="regular-text"></td>
</tr>
<tr>
<th><label for="wc-mb-pass2">Confirm Password</label></th>
<td><input type="password" id="wc-mb-pass2" class="regular-text"></td>
</tr>
<tr>
<th><label for="wc-mb-quota">Quota (MB)</label></th>
<td><input type="number" id="wc-mb-quota" value="1024" min="1"></td>
</tr>
</table>
<div class="woocow-modal-actions">
<button class="button button-primary" id="wc-mb-modal-save">Create</button>
<button class="button" id="wc-mb-modal-cancel">Cancel</button>
<span id="wc-mb-modal-notice"></span>
</div>
</div>
</div>
</div>
<?php
}
// ── AJAX helpers ─────────────────────────────────────────────────────────
private function verify(): void {
check_ajax_referer( 'woocow_admin', 'nonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Insufficient permissions.', 403 );
}
}
private function get_server( int $id ): ?object {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d",
$id
) );
}
private function json_ok( $data = null ): void {
wp_send_json_success( $data );
}
private function json_err( string $msg ): void {
wp_send_json_error( $msg );
}
// ── AJAX: Servers ─────────────────────────────────────────────────────────
public function ajax_woocow_servers_list(): void {
$this->verify();
global $wpdb;
$rows = $wpdb->get_results( "SELECT id, name, url, active, created_at FROM {$wpdb->prefix}woocow_servers ORDER BY name" );
$this->json_ok( $rows );
}
public function ajax_woocow_server_save(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$url = esc_url_raw( $_POST['url'] ?? '' );
$key = sanitize_text_field( $_POST['api_key'] ?? '' );
$active = absint( $_POST['active'] ?? 1 );
if ( ! $name || ! $url || ! $key ) {
$this->json_err( 'Name, URL, and API key are required.' );
}
$data = compact( 'name', 'url', 'active' ) + [ 'api_key' => $key ];
if ( $id ) {
$wpdb->update( "{$wpdb->prefix}woocow_servers", $data, [ 'id' => $id ] );
$this->json_ok( [ 'id' => $id ] );
} else {
$wpdb->insert( "{$wpdb->prefix}woocow_servers", $data );
$this->json_ok( [ 'id' => $wpdb->insert_id ] );
}
}
public function ajax_woocow_server_delete(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$wpdb->delete( "{$wpdb->prefix}woocow_servers", [ 'id' => $id ] );
$wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'server_id' => $id ] );
$this->json_ok();
}
public function ajax_woocow_server_test(): void {
$this->verify();
$id = absint( $_POST['id'] ?? 0 );
// Allow testing before saving (pass url+key directly)
if ( $id ) {
$server = $this->get_server( $id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
} else {
$url = esc_url_raw( $_POST['url'] ?? '' );
$key = sanitize_text_field( $_POST['api_key'] ?? '' );
if ( ! $url || ! $key ) {
$this->json_err( 'URL and API key required.' );
}
$api = new WooCow_API( $url, $key );
}
$result = $api->test_connection();
if ( $result['success'] ) {
$version = $result['data']['version'] ?? $result['data'][0]['version'] ?? 'unknown';
$this->json_ok( [ 'version' => $version ] );
} else {
$this->json_err( $result['error'] ?? 'Connection failed.' );
}
}
public function ajax_woocow_server_domains(): void {
$this->verify();
$id = absint( $_POST['server_id'] ?? 0 );
$server = $this->get_server( $id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->get_domains();
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch domains.' );
}
$domains = array_map( fn( $d ) => [ 'domain' => $d['domain_name'], 'active' => $d['active'] ], (array) $result['data'] );
$this->json_ok( $domains );
}
// ── AJAX: Assignments ─────────────────────────────────────────────────────
public function ajax_woocow_assignments_list(): void {
$this->verify();
global $wpdb;
$rows = $wpdb->get_results( "
SELECT a.id, a.customer_id, a.domain, a.created_at,
s.name AS server_name, s.url AS server_url,
u.display_name, u.user_email
FROM {$wpdb->prefix}woocow_assignments a
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
JOIN {$wpdb->users} u ON u.ID = a.customer_id
ORDER BY u.display_name, a.domain
" );
$this->json_ok( $rows );
}
public function ajax_woocow_assignment_save(): void {
$this->verify();
global $wpdb;
$customer_id = absint( $_POST['customer_id'] ?? 0 );
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
if ( ! $customer_id || ! $server_id || ! $domain ) {
$this->json_err( 'Customer, server, and domain are all required.' );
}
$existing = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}woocow_assignments WHERE customer_id=%d AND domain=%s",
$customer_id, $domain
) );
if ( $existing ) {
$this->json_err( 'This domain is already assigned to this customer.' );
}
$wpdb->insert( "{$wpdb->prefix}woocow_assignments", compact( 'customer_id', 'server_id', 'domain' ) );
$this->json_ok( [ 'id' => $wpdb->insert_id ] );
}
public function ajax_woocow_assignment_delete(): void {
$this->verify();
global $wpdb;
$id = absint( $_POST['id'] ?? 0 );
$wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'id' => $id ] );
$this->json_ok();
}
// ── AJAX: Customer search ─────────────────────────────────────────────────
public function ajax_woocow_customers_search(): void {
$this->verify();
$term = sanitize_text_field( $_POST['term'] ?? '' );
if ( strlen( $term ) < 2 ) {
$this->json_ok( [] );
}
$users = get_users( [
'search' => '*' . $term . '*',
'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
'role__in' => [ 'customer', 'subscriber', 'administrator', 'shop_manager' ],
'number' => 15,
] );
$out = array_map( fn( $u ) => [
'id' => $u->ID,
'label' => sprintf( '%s (%s)', $u->display_name, $u->user_email ),
], $users );
$this->json_ok( $out );
}
// ── AJAX: Admin Mailboxes ─────────────────────────────────────────────────
public function ajax_woocow_admin_mailboxes(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $domain
? $api->get_domain_mailboxes( $domain )
: $api->get_all_mailboxes();
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to fetch mailboxes.' );
}
$this->json_ok( [
'mailboxes' => $result['data'] ?? [],
'webmail_url' => $api->get_webmail_url(),
] );
}
public function ajax_woocow_admin_mailbox_create(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$local_part = sanitize_text_field( $_POST['local_part'] ?? '' );
$name = sanitize_text_field( $_POST['name'] ?? '' );
$password = $_POST['password'] ?? '';
$password2 = $_POST['password2'] ?? '';
$quota = absint( $_POST['quota'] ?? 1024 );
if ( ! $domain || ! $local_part || ! $password ) {
$this->json_err( 'Domain, local part, and password are required.' );
}
if ( $password !== $password2 ) {
$this->json_err( 'Passwords do not match.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->create_mailbox( [
'local_part' => $local_part,
'domain' => $domain,
'name' => $name ?: $local_part,
'password' => $password,
'password2' => $password2,
'quota' => $quota,
'active' => 1,
'force_pw_update' => 0,
'tls_enforce_in' => 0,
'tls_enforce_out' => 0,
] );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to create mailbox.' );
}
$this->json_ok( [ 'email' => $local_part . '@' . $domain ] );
}
public function ajax_woocow_admin_mailbox_delete(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$email = sanitize_email( $_POST['email'] ?? '' );
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$result = $api->delete_mailbox( $email );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to delete mailbox.' );
}
$this->json_ok();
}
public function ajax_woocow_admin_mailbox_edit(): void {
$this->verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$email = sanitize_email( $_POST['email'] ?? '' );
$type = sanitize_text_field( $_POST['type'] ?? '' ); // 'password' or 'quota'
if ( ! $email ) {
$this->json_err( 'Email address is required.' );
}
$server = $this->get_server( $server_id );
if ( ! $server ) {
$this->json_err( 'Server not found.' );
}
$api = WooCow_API::from_server( $server );
$attr = [];
if ( $type === 'password' ) {
$pass = $_POST['password'] ?? '';
$pass2 = $_POST['password2'] ?? '';
if ( ! $pass ) {
$this->json_err( 'Password cannot be empty.' );
}
if ( $pass !== $pass2 ) {
$this->json_err( 'Passwords do not match.' );
}
$attr = [ 'password' => $pass, 'password2' => $pass2 ];
} elseif ( $type === 'quota' ) {
$quota = absint( $_POST['quota'] ?? 0 );
if ( $quota < 1 ) {
$this->json_err( 'Quota must be at least 1 MB.' );
}
$attr = [ 'quota' => $quota ];
} else {
$this->json_err( 'Unknown edit type.' );
}
$result = $api->edit_mailbox( [ $email ], $attr );
if ( ! $result['success'] ) {
$this->json_err( $result['error'] ?? 'Failed to update mailbox.' );
}
$this->json_ok();
}
}