read_cached_list($transient_key); if (!empty($cached_list)) { return $cached_list; } // A cache marker without a usable file should trigger a refresh. delete_transient($transient_key); } $response = wp_remote_get( $url, [ 'timeout' => 3, 'redirection' => 2, ] ); if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) { set_transient($transient_key, 'aefe_fetch_failed', self::FAILURE_CACHE_TTL); return $this->read_cached_list($transient_key); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ( JSON_ERROR_NONE !== json_last_error() || !is_array($data) || !isset($data['domains']) || !is_array($data['domains']) ) { set_transient($transient_key, 'aefe_fetch_failed', self::FAILURE_CACHE_TTL); return $this->read_cached_list($transient_key); } $list = $this->normalize_domains($data['domains']); if (empty($list)) { set_transient($transient_key, 'aefe_fetch_failed', self::FAILURE_CACHE_TTL); return $this->read_cached_list($transient_key); } if ($this->write_cached_list($transient_key, $list)) { set_transient($transient_key, 'aefe_cache_fresh', self::REMOTE_CACHE_TTL); } return $list; } private function get_cache_file_path($transient_key) { $filenames = [ 'aefe_personal_domains' => 'personal-domains.json', 'aefe_disposable_domains' => 'disposable-domains.json', ]; if (!isset($filenames[$transient_key])) { return ''; } $upload_dir = wp_upload_dir(); if (!empty($upload_dir['error']) || empty($upload_dir['basedir'])) { return ''; } return trailingslashit($upload_dir['basedir']) . 'aefe/' . $filenames[$transient_key]; } private function read_cached_list($transient_key) { $cache_file = $this->get_cache_file_path($transient_key); if ($cache_file && is_readable($cache_file)) { $data = json_decode(file_get_contents($cache_file), true); if ( JSON_ERROR_NONE === json_last_error() && is_array($data) && isset($data['domains']) && is_array($data['domains']) ) { $list = $this->normalize_domains($data['domains']); if (!empty($list)) { return $list; } } } // Migrate the previous database-based fallback once, if it exists. $legacy_list = get_option($transient_key . '_last_good', []); if (is_array($legacy_list) && !empty($legacy_list)) { $legacy_list = $this->normalize_domains($legacy_list); if (!empty($legacy_list) && $this->write_cached_list($transient_key, $legacy_list)) { delete_option($transient_key . '_last_good'); return $legacy_list; } } return []; } private function write_cached_list($transient_key, $list) { $cache_file = $this->get_cache_file_path($transient_key); if (!$cache_file) { return false; } $cache_dir = dirname($cache_file); if (!is_dir($cache_dir) && !wp_mkdir_p($cache_dir)) { return false; } $json = wp_json_encode(['domains' => $list]); if (false === $json) { return false; } $temporary_file = tempnam($cache_dir, 'aefe-'); if (false === $temporary_file) { return false; } $written = file_put_contents($temporary_file, $json, LOCK_EX); if (false === $written || !rename($temporary_file, $cache_file)) { if (file_exists($temporary_file)) { unlink($temporary_file); } return false; } return true; } private function normalize_domains($domains) { $normalized = []; foreach ($domains as $domain) { if (!is_string($domain)) { continue; } $domain = strtolower(trim($domain)); if ('' !== $domain && false !== strpos($domain, '.')) { $normalized[] = $domain; } } return array_values(array_unique($normalized)); } private function load_local_domains($relative_path) { $json_file = AEFE_PLUGIN_PATH . $relative_path; if (!file_exists($json_file) || !is_readable($json_file)) { return []; } $data = json_decode(file_get_contents($json_file), true); if ( JSON_ERROR_NONE !== json_last_error() || !is_array($data) || !isset($data['domains']) || !is_array($data['domains']) ) { return []; } return $this->normalize_domains($data['domains']); } private function init_personal_domains() { $remote_domains = $this->fetch_remote_list(self::PERSONAL_DOMAINS_URL, 'aefe_personal_domains'); if (!empty($remote_domains)) { $this->personal_domains = $remote_domains; } else { $this->personal_domains = $this->load_local_domains('assets/data/personal-domains.json'); } } private function init_disposable_domains() { $remote_domains = $this->fetch_remote_list(self::DISPOSABLE_DOMAINS_URL, 'aefe_disposable_domains'); if (!empty($remote_domains)) { $this->disposable_domains = $remote_domains; } else { $this->disposable_domains = $this->load_local_domains('assets/data/disposable-email-domains.json'); } } public function validate_email($field, $record, $ajax_handler) { $email = strtolower(sanitize_email($field['value'])); $settings = $record->get('form_settings'); if (!is_email($email)) { $ajax_handler->add_error( $field['id'], __('Invalid email format', 'advanced-email-filter-for-elementor-forms') ); return; } // Get global and form-specific lists $global_blacklist = get_option(Filter_Settings::BLACKLIST_OPTION, ''); $global_whitelist = get_option(Filter_Settings::WHITELIST_OPTION, ''); $form_blacklist = $settings['aefe_blacklist'] ?? ''; $form_whitelist = $settings['aefe_whitelist'] ?? ''; // Combine lists $combined_whitelist = $this->combine_lists($global_whitelist, $form_whitelist); $combined_blacklist = $this->combine_lists($global_blacklist, $form_blacklist); // A matching allowlist rule overrides all other filters. if (!empty($combined_whitelist)) { $whitelist_match = $this->check_patterns($email, $combined_whitelist); if ($whitelist_match['matched']) { return; } } // Process blacklist $blacklist_match = $this->check_patterns($email, $combined_blacklist); if ($blacklist_match['matched']) { $this->add_validation_error( sprintf( /* translators: %s: email user input */ __('Error: %s is blocked', 'advanced-email-filter-for-elementor-forms'), $email ), $field, $ajax_handler ); return; } // Check business email if enabled (both global or form-specific) $global_business_only = get_option(Filter_Settings::BUSINESS_ONLY_OPTION, ''); $form_business_only = $settings['aefe_business_email'] ?? ''; if ($global_business_only || $form_business_only === 'yes') { if (empty($this->personal_domains)) { $this->init_personal_domains(); } if (empty($this->personal_domains)) { $this->add_validation_error( __('Email validation is temporarily unavailable. Please try again shortly.', 'advanced-email-filter-for-elementor-forms'), $field, $ajax_handler ); return; } $domain = strtolower(substr(strrchr($email, "@"), 1)); if (in_array($domain, $this->personal_domains, true)) { $this->add_validation_error( __('Personal email domains are not allowed. Please use a company email.', 'advanced-email-filter-for-elementor-forms'), $field, $ajax_handler ); return; } } // Check disposable email if enabled $global_block_disposable = get_option(Filter_Settings::DISPOSABLE_OPTION, ''); $form_block_disposable = $settings['aefe_block_disposable'] ?? ''; if ($global_block_disposable || $form_block_disposable === 'yes') { if (empty($this->disposable_domains)) { $this->init_disposable_domains(); } if (empty($this->disposable_domains)) { $this->add_validation_error( __('Email validation is temporarily unavailable. Please try again shortly.', 'advanced-email-filter-for-elementor-forms'), $field, $ajax_handler ); return; } $domain = strtolower(substr(strrchr($email, "@"), 1)); if (in_array($domain, $this->disposable_domains, true)) { $this->add_validation_error( __('Disposable email domains are not allowed.', 'advanced-email-filter-for-elementor-forms'), $field, $ajax_handler ); return; } } } private function combine_lists($global, $form) { return implode(',', array_filter([$global, $form], 'strlen')); } private function check_patterns($email, $list) { $patterns = $this->prepare_patterns($list); $result = [ 'matched' => false, 'matched_pattern' => '', 'patterns' => $patterns ]; foreach ($patterns as $pattern) { if ($this->match_pattern($email, $pattern)) { $result['matched'] = true; $result['matched_pattern'] = $pattern; break; } } return $result; } private function prepare_patterns($list) { $patterns = array_filter(array_map('trim', explode(',', $list))); return array_map([$this, 'normalize_pattern'], $patterns); } private function normalize_pattern($pattern) { $pattern = strtolower(trim($pattern)); // Add wildcard for domain-only patterns if (strpos($pattern, '@') === 0) { return '*' . $pattern; } // Treat a trailing @ as an email-user prefix. if (substr($pattern, -1) === '@') { return $pattern . '*'; } // Plain text without an address separator is a partial match. if (false === strpos($pattern, '@') && false === strpos($pattern, '*')) { return '*' . $pattern . '*'; } return $pattern; } private function match_pattern($email, $pattern) { $regex = str_replace('\*', '.*', preg_quote($pattern, '/')); return (bool) preg_match("/^{$regex}$/i", $email); } private function add_validation_error($message, $field, $ajax_handler) { $error_message = apply_filters( 'aefe_validation_error', $message, $field['value'] ); $ajax_handler->add_error( $field['id'], esc_html($error_message) ); } }