Files
WooCow/includes/class-woocow-account.php
Malin 2ee81efacf 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>
2026-02-27 08:06:22 +01:00

459 lines
18 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 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,
] );
}
}
}
}