Files
WooCow/includes/class-woocow-account.php

586 lines
25 KiB
PHP
Raw Normal View History

<?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',
'woocow_acct_quarantine',
'woocow_acct_quarantine_delete',
'woocow_acct_spam_score',
];
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' ),
'i18n' => [
'load_mailboxes' => __( 'Load Mailboxes', 'woocow' ),
'refresh' => __( 'Refresh', 'woocow' ),
'loading' => __( 'Loading…', 'woocow' ),
'no_mailboxes' => __( 'No mailboxes yet. Create one below.', 'woocow' ),
'change_password' => __( 'Change Password', 'woocow' ),
'aliases_forwarders' => __( 'Aliases & Forwarders', 'woocow' ),
'spam_filter' => __( 'Spam Filter', 'woocow' ),
'webmail' => __( 'Webmail', 'woocow' ),
'add_alias' => __( 'Add Alias / Forwarder', 'woocow' ),
'no_aliases' => __( 'No aliases for this domain yet.', 'woocow' ),
'add_mailbox' => __( 'Add Mailbox', 'woocow' ),
'create_mailbox' => __( 'Create Mailbox', 'woocow' ),
'cancel' => __( 'Cancel', 'woocow' ),
'save' => __( 'Save', 'woocow' ),
'delete' => __( 'Delete', 'woocow' ),
'quarantine' => __( 'Quarantine', 'woocow' ),
'no_quarantine' => __( 'No quarantined messages for this domain.', 'woocow' ),
'q_release_note' => __( 'To release a message to your inbox, use the link in your quarantine notification email or via Webmail.', 'woocow' ),
'q_delete_confirm' => __( 'Permanently delete this quarantined message?', 'woocow' ),
'spam_threshold' => __( 'Spam Filter Threshold', 'woocow' ),
'spam_help' => __( 'Lower = stricter. Default is 5. Emails above this score go to spam/quarantine.', 'woocow' ),
'update_password' => __( 'Update Password', 'woocow' ),
'passwords_mismatch' => __( 'Passwords do not match or are empty.', 'woocow' ),
'unlimited' => __( 'Unlimited', 'woocow' ),
],
] );
}
// ── 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"><?php esc_html_e( 'Load Mailboxes', 'woocow' ); ?></button>
<button class="woocow-btn woocow-btn-sm woocow-btn-ghost woocow-load-quarantine"><?php esc_html_e( 'Quarantine', 'woocow' ); ?></button>
</div>
<!-- Quarantine panel -->
<div class="woocow-quarantine-wrap" style="display:none">
<div class="woocow-quarantine-list"></div>
</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();
}
// ── AJAX: Quarantine ──────────────────────────────────────────────────────
public function ajax_woocow_acct_quarantine(): 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_quarantine();
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Could not load quarantine.' );
}
// Filter to messages where the recipient belongs to this domain.
$messages = array_values( array_filter( (array) $result['data'], function ( $m ) use ( $domain ) {
return isset( $m['rcpt'] ) && str_ends_with( $m['rcpt'], '@' . $domain );
} ) );
wp_send_json_success( $messages );
}
public function ajax_woocow_acct_quarantine_delete(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$qid = absint( $_POST['qid'] ?? 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_quarantine( $qid );
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Failed to delete quarantine message.' );
}
wp_send_json_success();
}
// ── AJAX: Spam score ──────────────────────────────────────────────────────
public function ajax_woocow_acct_spam_score(): void {
$this->account_verify();
$server_id = absint( $_POST['server_id'] ?? 0 );
$domain = sanitize_text_field( $_POST['domain'] ?? '' );
$email = sanitize_email( $_POST['email'] ?? '' );
$spam_score = floatval( $_POST['spam_score'] ?? 5 );
if ( ! $this->verify_ownership( $server_id, $domain ) ) {
wp_send_json_error( 'Access denied.', 403 );
}
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->request( 'POST', '/api/v1/edit/spam-score/' . rawurlencode( $email ), [
'spam_score' => $spam_score,
] );
if ( ! $result['success'] ) {
wp_send_json_error( $result['error'] ?? 'Failed to update spam score.' );
}
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,
] );
}
}
}
}