feat: initial WooCow plugin — Mailcow/WooCommerce integration
- Mailcow API client wrapping domains, mailboxes, aliases endpoints - Admin backend: server management, customer-domain assignments, mailbox overview - WooCommerce My Account: email hosting tab with mailbox/alias management - Per-mailbox password change (independent of WP account password) - Optional WP account password sync to all customer mailboxes - Installer creates wp_woocow_servers and wp_woocow_assignments DB tables - Full nonce + capability + ownership verification on all AJAX endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
458
includes/class-woocow-account.php
Normal file
458
includes/class-woocow-account.php
Normal file
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCow Account – WooCommerce My Account integration.
|
||||
*
|
||||
* Registers the "email-hosting" endpoint and handles all customer-facing AJAX.
|
||||
*/
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCow_Account {
|
||||
|
||||
const ENDPOINT = 'email-hosting';
|
||||
|
||||
public function __construct() {
|
||||
// Endpoint registration
|
||||
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' ] );
|
||||
|
||||
// Assets
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
||||
|
||||
// Password reset integration
|
||||
add_action( 'woocommerce_save_account_details', [ $this, 'maybe_sync_password' ], 20 );
|
||||
|
||||
// AJAX
|
||||
$actions = [
|
||||
'woocow_acct_domains',
|
||||
'woocow_acct_mailboxes',
|
||||
'woocow_acct_mailbox_create',
|
||||
'woocow_acct_mailbox_password',
|
||||
'woocow_acct_aliases',
|
||||
'woocow_acct_alias_create',
|
||||
'woocow_acct_alias_delete',
|
||||
];
|
||||
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 {
|
||||
// Insert before logout
|
||||
$logout = $items['customer-logout'] ?? null;
|
||||
unset( $items['customer-logout'] );
|
||||
$items[ self::ENDPOINT ] = __( 'Email Hosting', 'woocow' );
|
||||
if ( $logout ) {
|
||||
$items['customer-logout'] = $logout;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function enqueue_assets(): void {
|
||||
if ( ! is_account_page() ) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
|
||||
wp_enqueue_script( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/js/woocow-account.js', [ 'jquery' ], WOOCOW_VERSION, true );
|
||||
wp_localize_script( 'woocow-account', 'woocowAcct', [
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'woocow_account' ),
|
||||
] );
|
||||
}
|
||||
|
||||
// ── My Account page render ────────────────────────────────────────────────
|
||||
|
||||
public function render_page(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
echo '<p>' . esc_html__( 'Please log in to manage your email hosting.', 'woocow' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_id = get_current_user_id();
|
||||
$assignments = $this->get_customer_assignments( $customer_id );
|
||||
|
||||
if ( empty( $assignments ) ) {
|
||||
echo '<div class="woocommerce-info">' . esc_html__( 'You have no email domains assigned yet. Please contact support.', 'woocow' ) . '</div>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="woocow-account" id="woocow-account">
|
||||
<div id="woocow-acct-notices"></div>
|
||||
|
||||
<?php foreach ( $assignments as $assignment ) : ?>
|
||||
<div class="woocow-domain-panel" data-assignment-id="<?php echo esc_attr( $assignment->id ); ?>"
|
||||
data-server-id="<?php echo esc_attr( $assignment->server_id ); ?>"
|
||||
data-domain="<?php echo esc_attr( $assignment->domain ); ?>">
|
||||
|
||||
<div class="woocow-domain-header">
|
||||
<span class="woocow-domain-name"><?php echo esc_html( $assignment->domain ); ?></span>
|
||||
<span class="woocow-domain-server"><?php echo esc_html( $assignment->server_name ); ?></span>
|
||||
<a href="<?php echo esc_url( $assignment->webmail_url ); ?>" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm">
|
||||
Open Webmail
|
||||
</a>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes">Load Mailboxes</button>
|
||||
</div>
|
||||
|
||||
<div class="woocow-mailboxes-wrap" style="display:none">
|
||||
<div class="woocow-mailboxes-list"></div>
|
||||
|
||||
<!-- Create mailbox form -->
|
||||
<div class="woocow-create-mbox-form" style="display:none">
|
||||
<h4>Create Mailbox</h4>
|
||||
<div class="woocow-field-row">
|
||||
<div class="woocow-flex-inline">
|
||||
<input type="text" class="wc-mbox-local woocow-input" placeholder="username">
|
||||
<span class="woocow-at">@</span>
|
||||
<strong><?php echo esc_html( $assignment->domain ); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="text" class="wc-mbox-name woocow-input" placeholder="Full Name">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" class="wc-mbox-pass woocow-input" placeholder="Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" class="wc-mbox-pass2 woocow-input" placeholder="Confirm Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<label>Quota (MB): <input type="number" class="wc-mbox-quota woocow-input-sm" value="1024" min="1"></label>
|
||||
</div>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="woocow-btn woocow-btn-primary wc-mbox-submit">Create Mailbox</button>
|
||||
<button class="woocow-btn woocow-btn-outline wc-mbox-cancel">Cancel</button>
|
||||
<span class="wc-mbox-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="woocow-btn woocow-btn-outline woocow-create-mbox-btn">+ Add Mailbox</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div id="woocow-pw-modal" class="woocow-modal" style="display:none">
|
||||
<div class="woocow-modal-box">
|
||||
<h3>Change Mailbox Password</h3>
|
||||
<p class="woocow-modal-subtitle" id="woocow-pw-email"></p>
|
||||
<input type="hidden" id="woocow-pw-server-id">
|
||||
<input type="hidden" id="woocow-pw-mailbox">
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" id="woocow-pw-new" class="woocow-input" placeholder="New Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" id="woocow-pw-new2" class="woocow-input" placeholder="Confirm New Password">
|
||||
</div>
|
||||
<div class="woocow-modal-actions">
|
||||
<button class="woocow-btn woocow-btn-primary" id="woocow-pw-save">Update Password</button>
|
||||
<button class="woocow-btn woocow-btn-outline" id="woocow-pw-cancel">Cancel</button>
|
||||
<span id="woocow-pw-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function get_customer_assignments( int $customer_id ): array {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results( $wpdb->prepare( "
|
||||
SELECT a.id, a.server_id, a.domain, s.name AS server_name, s.url AS server_url
|
||||
FROM {$wpdb->prefix}woocow_assignments a
|
||||
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
|
||||
WHERE a.customer_id = %d AND s.active = 1
|
||||
ORDER BY a.domain
|
||||
", $customer_id ) );
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
$row->webmail_url = rtrim( $row->server_url, '/' ) . '/SOGo';
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function get_server( int $id ): ?object {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d AND active = 1",
|
||||
$id
|
||||
) );
|
||||
}
|
||||
|
||||
/** Verify that the current user owns the given assignment. */
|
||||
private function verify_ownership( int $server_id, string $domain ): bool {
|
||||
global $wpdb;
|
||||
$found = $wpdb->get_var( $wpdb->prepare( "
|
||||
SELECT id FROM {$wpdb->prefix}woocow_assignments
|
||||
WHERE customer_id = %d AND server_id = %d AND domain = %s
|
||||
", get_current_user_id(), $server_id, $domain ) );
|
||||
return (bool) $found;
|
||||
}
|
||||
|
||||
private function account_verify(): void {
|
||||
check_ajax_referer( 'woocow_account', 'nonce' );
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( 'Not logged in.', 401 );
|
||||
}
|
||||
}
|
||||
|
||||
// ── AJAX: Account ─────────────────────────────────────────────────────────
|
||||
|
||||
public function ajax_woocow_acct_domains(): void {
|
||||
$this->account_verify();
|
||||
$assignments = $this->get_customer_assignments( get_current_user_id() );
|
||||
wp_send_json_success( $assignments );
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_mailboxes(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->get_domain_mailboxes( $domain );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Could not load mailboxes.' );
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
'mailboxes' => $result['data'] ?? [],
|
||||
'webmail_url' => $api->get_webmail_url(),
|
||||
] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_mailbox_create(): void {
|
||||
$this->account_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 ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
if ( ! $local_part || ! $password ) {
|
||||
wp_send_json_error( 'Username and password are required.' );
|
||||
}
|
||||
if ( $password !== $password2 ) {
|
||||
wp_send_json_error( 'Passwords do not match.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$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'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to create mailbox.' );
|
||||
}
|
||||
|
||||
wp_send_json_success( [ 'email' => $local_part . '@' . $domain ] );
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_mailbox_password(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$email = sanitize_email( $_POST['email'] ?? '' );
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password2 = $_POST['password2'] ?? '';
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
if ( ! $password || $password !== $password2 ) {
|
||||
wp_send_json_error( 'Passwords do not match or are empty.' );
|
||||
}
|
||||
|
||||
// Validate that email belongs to the customer's domain.
|
||||
if ( ! str_ends_with( $email, '@' . $domain ) ) {
|
||||
wp_send_json_error( 'Email does not belong to your domain.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->edit_mailbox( [ $email ], [
|
||||
'password' => $password,
|
||||
'password2' => $password2,
|
||||
] );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to update password.' );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_aliases(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->get_all_aliases();
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Could not load aliases.' );
|
||||
}
|
||||
|
||||
// Filter to this domain only.
|
||||
$aliases = array_filter( (array) $result['data'], function ( $a ) use ( $domain ) {
|
||||
return isset( $a['address'] ) && str_ends_with( $a['address'], '@' . $domain );
|
||||
} );
|
||||
|
||||
wp_send_json_success( array_values( $aliases ) );
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_alias_create(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$address = sanitize_email( $_POST['address'] ?? '' );
|
||||
$goto = sanitize_email( $_POST['goto'] ?? '' );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
if ( ! $address || ! $goto ) {
|
||||
wp_send_json_error( 'Alias address and destination are required.' );
|
||||
}
|
||||
if ( ! str_ends_with( $address, '@' . $domain ) ) {
|
||||
wp_send_json_error( 'Alias address must belong to your domain.' );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->create_alias( [
|
||||
'address' => $address,
|
||||
'goto' => $goto,
|
||||
'active' => 1,
|
||||
] );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to create alias.' );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
public function ajax_woocow_acct_alias_delete(): void {
|
||||
$this->account_verify();
|
||||
|
||||
$server_id = absint( $_POST['server_id'] ?? 0 );
|
||||
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
|
||||
$alias_id = absint( $_POST['alias_id'] ?? 0 );
|
||||
|
||||
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
|
||||
wp_send_json_error( 'Access denied.', 403 );
|
||||
}
|
||||
|
||||
$server = $this->get_server( $server_id );
|
||||
if ( ! $server ) {
|
||||
wp_send_json_error( 'Server unavailable.' );
|
||||
}
|
||||
|
||||
$api = WooCow_API::from_server( $server );
|
||||
$result = $api->delete_alias( $alias_id );
|
||||
|
||||
if ( ! $result['success'] ) {
|
||||
wp_send_json_error( $result['error'] ?? 'Failed to delete alias.' );
|
||||
}
|
||||
|
||||
wp_send_json_success();
|
||||
}
|
||||
|
||||
// ── WP password change sync ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* When a customer saves their WooCommerce account details with a new password,
|
||||
* offer them the option to sync it to all their mailboxes.
|
||||
*
|
||||
* NOTE: This hook fires after the WP password has been updated.
|
||||
* We only sync if the customer explicitly checked the option.
|
||||
*/
|
||||
public function maybe_sync_password( int $user_id ): void {
|
||||
// Only proceed if the "sync to mailcow" checkbox was checked and a new password given.
|
||||
if ( empty( $_POST['woocow_sync_pw'] ) || empty( $_POST['password_1'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $_POST['password_1'];
|
||||
global $wpdb;
|
||||
|
||||
$assignments = $wpdb->get_results( $wpdb->prepare( "
|
||||
SELECT a.server_id, a.domain, s.url, s.api_key
|
||||
FROM {$wpdb->prefix}woocow_assignments a
|
||||
JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id
|
||||
WHERE a.customer_id = %d AND s.active = 1
|
||||
", $user_id ) );
|
||||
|
||||
foreach ( $assignments as $assignment ) {
|
||||
$api = new WooCow_API( $assignment->url, $assignment->api_key );
|
||||
$mboxes = $api->get_domain_mailboxes( $assignment->domain );
|
||||
if ( ! $mboxes['success'] ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( (array) ( $mboxes['data'] ?? [] ) as $mbox ) {
|
||||
$api->edit_mailbox( [ $mbox['username'] ], [
|
||||
'password' => $password,
|
||||
'password2' => $password,
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user