added additional plugins

This commit is contained in:
Rodolfo Martinez 2025-12-12 19:05:48 -05:00
parent c85895d306
commit 00e60ec1b7
132 changed files with 27514 additions and 0 deletions

View file

@ -0,0 +1,73 @@
<?php
/**
* Fired during plugin activation.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Activator {
/**
* Activate the plugin.
*/
public static function activate() {
// Security check: verify user can activate plugins
if (!current_user_can('activate_plugins')) {
return;
}
global $wpdb;
// Create error log table
$table_name = $wpdb->prefix . 'wpfmj_error_log';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
automation_id bigint(20) UNSIGNED NOT NULL,
form_entry_id bigint(20) UNSIGNED NOT NULL,
error_type varchar(50) NOT NULL,
error_message text NOT NULL,
retry_count int(11) NOT NULL DEFAULT 0,
resolved tinyint(1) NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY automation_id (automation_id),
KEY created_at (created_at),
KEY resolved (resolved)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// Register custom post type temporarily for flush_rewrite_rules
require_once WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-cpt.php';
$cpt = new WPFMJ_CPT();
$cpt->register_post_type();
// Flush rewrite rules
flush_rewrite_rules();
// Set plugin version
add_option('wpfmj_version', WPFMJ_VERSION);
// Generate encryption key if it doesn't exist
// CRITICAL: This key is used to encrypt/decrypt API credentials
// If this key is lost or changed, all saved automations will fail to decrypt credentials
// ENTERPRISE DEPLOYMENT: Back up this key before any WordPress migration or database restore
if (!get_option('wpfmj_encryption_key')) {
$key = base64_encode(random_bytes(32));
add_option('wpfmj_encryption_key', $key, '', false); // Don't autoload
// Log key generation for audit trail
error_log('WPFMJ: New encryption key generated on activation. Ensure database backups include wp_options table.');
}
// Schedule cleanup cron job (weekly cleanup of old error logs)
if (!wp_next_scheduled('wpfmj_cleanup_error_logs')) {
wp_schedule_event(time(), 'weekly', 'wpfmj_cleanup_error_logs');
}
}
}

View file

@ -0,0 +1,168 @@
<?php
/**
* The core plugin class.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Core {
/**
* The loader that's responsible for maintaining and registering all hooks.
*/
protected $loader;
/**
* The unique identifier of this plugin.
*/
protected $plugin_name;
/**
* The current version of the plugin.
*/
protected $version;
/**
* Initialize the plugin.
*/
public function __construct() {
$this->version = WPFMJ_VERSION;
$this->plugin_name = 'wpforms-mailjet-automation';
$this->load_dependencies();
$this->define_admin_hooks();
$this->define_public_hooks();
}
/**
* Load the required dependencies for this plugin.
*/
private function load_dependencies() {
// Define required files
$required_files = array(
// Core classes
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-loader.php',
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-cpt.php',
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-encryption.php',
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-mailjet-api.php',
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-form-handler.php',
WPFMJ_PLUGIN_DIR . 'includes/class-wpfmj-error-logger.php',
);
// Add admin files if in admin context
if (is_admin()) {
$required_files[] = WPFMJ_PLUGIN_DIR . 'admin/class-wpfmj-admin.php';
$required_files[] = WPFMJ_PLUGIN_DIR . 'admin/class-wpfmj-dashboard.php';
}
// Check for missing files
$missing_files = array();
foreach ($required_files as $file) {
if (!file_exists($file)) {
$missing_files[] = basename($file);
}
}
// If any files are missing, show error and stop loading
if (!empty($missing_files)) {
add_action('admin_notices', function() use ($missing_files) {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e('WPForms to Mailjet Automation Error:', 'wpforms-mailjet-automation'); ?></strong>
<?php
echo esc_html(
sprintf(
__('Missing required files: %s. Please reinstall the plugin.', 'wpforms-mailjet-automation'),
implode(', ', $missing_files)
)
);
?>
</p>
</div>
<?php
});
return;
}
// All files exist, load them
foreach ($required_files as $file) {
require_once $file;
}
$this->loader = new WPFMJ_Loader();
}
/**
* Register all of the hooks related to the admin area functionality.
*/
private function define_admin_hooks() {
if (!is_admin()) {
return;
}
$admin = new WPFMJ_Admin($this->get_plugin_name(), $this->get_version());
$dashboard = new WPFMJ_Dashboard();
$cpt = new WPFMJ_CPT();
// Admin assets
$this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_styles');
$this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_scripts');
// Dashboard
$this->loader->add_action('admin_menu', $dashboard, 'add_menu_page');
// Custom post type
$this->loader->add_action('init', $cpt, 'register_post_type');
// AJAX endpoints
$this->loader->add_action('wp_ajax_wpfmj_get_forms', $admin, 'ajax_get_forms');
$this->loader->add_action('wp_ajax_wpfmj_get_form_fields', $admin, 'ajax_get_form_fields');
$this->loader->add_action('wp_ajax_wpfmj_test_mailjet', $admin, 'ajax_test_mailjet');
$this->loader->add_action('wp_ajax_wpfmj_get_mailjet_lists', $admin, 'ajax_get_mailjet_lists');
$this->loader->add_action('wp_ajax_wpfmj_save_automation', $admin, 'ajax_save_automation');
$this->loader->add_action('wp_ajax_wpfmj_get_automation', $admin, 'ajax_get_automation');
$this->loader->add_action('wp_ajax_wpfmj_toggle_automation', $admin, 'ajax_toggle_automation');
$this->loader->add_action('wp_ajax_wpfmj_delete_automation', $admin, 'ajax_delete_automation');
$this->loader->add_action('wp_ajax_wpfmj_get_dashboard_data', $admin, 'ajax_get_dashboard_data');
}
/**
* Register all of the hooks related to the public-facing functionality.
*/
private function define_public_hooks() {
$form_handler = new WPFMJ_Form_Handler();
// WPForms submission hook
$this->loader->add_action('wpforms_process_complete', $form_handler, 'handle_submission', 10, 4);
}
/**
* Run the loader to execute all of the hooks with WordPress.
*/
public function run() {
$this->loader->run();
}
/**
* The name of the plugin.
*/
public function get_plugin_name() {
return $this->plugin_name;
}
/**
* The reference to the class that orchestrates the hooks with the plugin.
*/
public function get_loader() {
return $this->loader;
}
/**
* Retrieve the version number of the plugin.
*/
public function get_version() {
return $this->version;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Custom Post Type registration.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_CPT {
/**
* Register the custom post type for automations.
*/
public function register_post_type() {
$labels = array(
'name' => _x('Automations', 'Post Type General Name', 'wpforms-mailjet-automation'),
'singular_name' => _x('Automation', 'Post Type Singular Name', 'wpforms-mailjet-automation'),
'menu_name' => __('Mailjet Automations', 'wpforms-mailjet-automation'),
'name_admin_bar' => __('Automation', 'wpforms-mailjet-automation'),
'archives' => __('Automation Archives', 'wpforms-mailjet-automation'),
'attributes' => __('Automation Attributes', 'wpforms-mailjet-automation'),
'parent_item_colon' => __('Parent Automation:', 'wpforms-mailjet-automation'),
'all_items' => __('All Automations', 'wpforms-mailjet-automation'),
'add_new_item' => __('Add New Automation', 'wpforms-mailjet-automation'),
'add_new' => __('Add New', 'wpforms-mailjet-automation'),
'new_item' => __('New Automation', 'wpforms-mailjet-automation'),
'edit_item' => __('Edit Automation', 'wpforms-mailjet-automation'),
'update_item' => __('Update Automation', 'wpforms-mailjet-automation'),
'view_item' => __('View Automation', 'wpforms-mailjet-automation'),
'view_items' => __('View Automations', 'wpforms-mailjet-automation'),
'search_items' => __('Search Automation', 'wpforms-mailjet-automation'),
'not_found' => __('Not found', 'wpforms-mailjet-automation'),
'not_found_in_trash' => __('Not found in Trash', 'wpforms-mailjet-automation'),
);
$args = array(
'label' => __('Automation', 'wpforms-mailjet-automation'),
'description' => __('WPForms to Mailjet Automations', 'wpforms-mailjet-automation'),
'labels' => $labels,
'supports' => array('title'),
'hierarchical' => false,
'public' => false,
'show_ui' => false,
'show_in_menu' => false,
'menu_position' => 5,
'show_in_admin_bar' => false,
'show_in_nav_menus' => false,
'can_export' => false,
'has_archive' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'capability_type' => 'post',
'capabilities' => array(
'create_posts' => 'manage_options',
),
'map_meta_cap' => true,
);
register_post_type('wpfmj_automation', $args);
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Fired during plugin deactivation.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Deactivator {
/**
* Deactivate the plugin.
*/
public static function deactivate() {
// Clear scheduled cron jobs
$timestamp = wp_next_scheduled('wpfmj_cleanup_error_logs');
if ($timestamp) {
wp_unschedule_event($timestamp, 'wpfmj_cleanup_error_logs');
}
// Flush rewrite rules
flush_rewrite_rules();
// Note: We don't delete data on deactivation.
// Data is only removed if the user explicitly uninstalls the plugin.
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* Encryption/Decryption for sensitive data like API keys.
*
* ENTERPRISE SECURITY NOTICE:
* This class manages encryption of API credentials using AES-256-CBC.
* The encryption key is stored in wp_options as 'wpfmj_encryption_key'.
*
* CRITICAL FOR PRODUCTION:
* - The encryption key MUST be backed up with your database
* - If the key is lost/changed, ALL saved automations will fail
* - During site migrations, ensure wp_options table includes this key
* - For multi-environment setups, consider using wp-config.php constants
* - Monitor error logs for decryption failures
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Encryption {
/**
* Get encryption method (configurable via filter).
*
* @return string Encryption method
*/
private static function get_method() {
$method = apply_filters('wpfmj_encryption_method', 'AES-256-CBC');
// Validate that the method is available
$available_methods = openssl_get_cipher_methods();
if (!in_array($method, $available_methods, true)) {
error_log("WPFMJ: Invalid encryption method '{$method}', falling back to AES-256-CBC");
$method = 'AES-256-CBC';
}
return $method;
}
/**
* Get the encryption key.
*/
private static function get_key() {
$key = get_option('wpfmj_encryption_key');
if (!$key) {
// Generate a new key if it doesn't exist
$key = base64_encode(random_bytes(32));
add_option('wpfmj_encryption_key', $key, '', false);
}
return base64_decode($key);
}
/**
* Encrypt data.
*/
public static function encrypt($data) {
if (empty($data)) {
return '';
}
$key = self::get_key();
$method = self::get_method();
$iv_length = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($iv_length);
$encrypted = openssl_encrypt($data, $method, $key, 0, $iv);
// Combine IV and encrypted data
return base64_encode($iv . $encrypted);
}
/**
* Decrypt data.
*/
public static function decrypt($data) {
if (empty($data)) {
return '';
}
try {
$key = self::get_key();
$method = self::get_method();
$decoded_data = base64_decode($data);
if ($decoded_data === false) {
error_log('WPFMJ Decryption Error: Invalid base64 data');
return false;
}
$iv_length = openssl_cipher_iv_length($method);
if (strlen($decoded_data) <= $iv_length) {
error_log('WPFMJ Decryption Error: Data too short');
return false;
}
$iv = substr($decoded_data, 0, $iv_length);
$encrypted = substr($decoded_data, $iv_length);
$decrypted = openssl_decrypt($encrypted, $method, $key, 0, $iv);
if ($decrypted === false) {
error_log('WPFMJ Decryption Error: Decryption failed - possibly corrupted data or wrong key');
return false;
}
return $decrypted;
} catch (Exception $e) {
error_log('WPFMJ Decryption Error: ' . $e->getMessage());
return false;
}
}
}

View file

@ -0,0 +1,231 @@
<?php
/**
* Error logging system.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Error_Logger {
/**
* Table name.
*/
private $table_name;
/**
* Constructor.
*/
public function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'wpfmj_error_log';
}
/**
* Log an error.
*/
public function log($automation_id, $form_entry_id, $error_type, $error_message, $retry_count = 0) {
global $wpdb;
return $wpdb->insert(
$this->table_name,
array(
'automation_id' => intval($automation_id),
'form_entry_id' => intval($form_entry_id),
'error_type' => sanitize_text_field($error_type),
'error_message' => sanitize_textarea_field($error_message),
'retry_count' => intval($retry_count),
'resolved' => 0
),
array('%d', '%d', '%s', '%s', '%d', '%d')
);
}
/**
* Get errors for an automation.
*
* IMPORTANT: Results contain user-generated error messages.
* Always escape output when displaying: esc_html(), esc_attr(), etc.
*/
public function get_errors($automation_id, $limit = 50, $resolved = null) {
global $wpdb;
// Build query with proper parameter binding
if ($resolved !== null) {
$sql = $wpdb->prepare(
"SELECT * FROM {$this->table_name}
WHERE automation_id = %d AND resolved = %d
ORDER BY created_at DESC
LIMIT %d",
$automation_id,
$resolved ? 1 : 0,
$limit
);
} else {
$sql = $wpdb->prepare(
"SELECT * FROM {$this->table_name}
WHERE automation_id = %d
ORDER BY created_at DESC
LIMIT %d",
$automation_id,
$limit
);
}
$results = $wpdb->get_results($sql, ARRAY_A);
// Sanitize error messages for safe output
if (is_array($results)) {
foreach ($results as &$result) {
$result['error_message'] = sanitize_text_field($result['error_message']);
$result['error_type'] = sanitize_text_field($result['error_type']);
}
}
return $results;
}
/**
* Get error count for an automation.
*/
public function get_error_count($automation_id, $resolved = null) {
global $wpdb;
// Build query with proper parameter binding
if ($resolved !== null) {
$sql = $wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE automation_id = %d AND resolved = %d",
$automation_id,
$resolved ? 1 : 0
);
} else {
$sql = $wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table_name} WHERE automation_id = %d",
$automation_id
);
}
return (int) $wpdb->get_var($sql);
}
/**
* Get error counts for multiple automations in a single query.
* Prevents N+1 query problems when displaying dashboard.
*
* @param array $automation_ids Array of automation IDs
* @param bool|null $resolved Filter by resolved status (null = all)
* @return array Associative array of automation_id => count
*/
public function get_error_counts_bulk($automation_ids, $resolved = null) {
global $wpdb;
if (empty($automation_ids)) {
return array();
}
// Sanitize all IDs
$automation_ids = array_map('absint', $automation_ids);
$placeholders = implode(',', array_fill(0, count($automation_ids), '%d'));
// Build query
if ($resolved !== null) {
$sql = $wpdb->prepare(
"SELECT automation_id, COUNT(*) as error_count
FROM {$this->table_name}
WHERE automation_id IN ($placeholders) AND resolved = %d
GROUP BY automation_id",
array_merge($automation_ids, array($resolved ? 1 : 0))
);
} else {
$sql = $wpdb->prepare(
"SELECT automation_id, COUNT(*) as error_count
FROM {$this->table_name}
WHERE automation_id IN ($placeholders)
GROUP BY automation_id",
$automation_ids
);
}
$results = $wpdb->get_results($sql, ARRAY_A);
// Convert to associative array
$counts = array();
foreach ($results as $row) {
$counts[(int)$row['automation_id']] = (int)$row['error_count'];
}
return $counts;
}
/**
* Mark error as resolved.
*/
public function mark_resolved($error_id) {
global $wpdb;
return $wpdb->update(
$this->table_name,
array('resolved' => 1),
array('id' => $error_id),
array('%d'),
array('%d')
);
}
/**
* Clean up old error logs.
*
* @param int $days Number of days to retain resolved errors. Default 90 days.
* Can be filtered using 'wpfmj_error_log_retention_days'.
*/
public function cleanup_old_logs($days = null) {
global $wpdb;
// Allow filtering of retention period
if ($days === null) {
$days = apply_filters('wpfmj_error_log_retention_days', 90);
}
// Ensure positive integer
$days = absint($days);
if ($days < 1) {
$days = 90; // Fallback to default
}
$date = gmdate('Y-m-d H:i:s', strtotime("-{$days} days"));
$deleted = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$this->table_name} WHERE created_at < %s AND resolved = 1",
$date
)
);
// Log cleanup activity
if ($deleted > 0) {
error_log("WPFMJ: Cleaned up {$deleted} error log entries older than {$days} days");
}
return $deleted;
}
/**
* Get recent errors across all automations.
*/
public function get_recent_errors($limit = 20) {
global $wpdb;
$sql = "SELECT * FROM {$this->table_name}
ORDER BY created_at DESC
LIMIT %d";
return $wpdb->get_results($wpdb->prepare($sql, $limit), ARRAY_A);
}
}
// Register cleanup cron job handler
add_action('wpfmj_cleanup_error_logs', function() {
$logger = new WPFMJ_Error_Logger();
// Retention period can be customized via filter
$logger->cleanup_old_logs();
});

View file

@ -0,0 +1,302 @@
<?php
/**
* Handle WPForms submissions and trigger Mailjet actions.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Form_Handler {
/**
* Maximum retry attempts (configurable via filter).
*/
private $max_retries;
/**
* Constructor.
*/
public function __construct() {
// Allow filtering of max retry attempts (default 3, min 1, max 5)
$this->max_retries = apply_filters('wpfmj_max_retry_attempts', 3);
$this->max_retries = max(1, min(5, absint($this->max_retries)));
}
/**
* Handle form submission.
*/
public function handle_submission($fields, $entry, $form_data, $entry_id) {
$form_id = $form_data['id'];
// Find all active automations for this form
$automations = $this->get_active_automations($form_id);
if (empty($automations)) {
return;
}
foreach ($automations as $automation) {
$this->process_automation($automation, $fields, $entry_id);
}
}
/**
* Get active automations for a form.
*/
private function get_active_automations($form_id) {
$args = array(
'post_type' => 'wpfmj_automation',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_wpfmj_form_id',
'value' => $form_id,
'compare' => '='
)
)
);
return get_posts($args);
}
/**
* Process a single automation.
*/
private function process_automation($automation, $fields, $entry_id) {
$automation_id = $automation->ID;
// Get automation settings
$config = get_post_meta($automation_id, '_wpfmj_config', true);
if (empty($config)) {
$this->log_error($automation_id, $entry_id, 'missing_config', 'Automation configuration is empty');
return;
}
// Validate config structure for data integrity
$required_keys = array('field_mapping', 'trigger_field_id', 'api_key', 'api_secret', 'list_mappings');
foreach ($required_keys as $key) {
if (!isset($config[$key])) {
$this->log_error($automation_id, $entry_id, 'invalid_config', "Missing required config key: {$key}");
return;
}
}
if (!isset($config['field_mapping']['email'])) {
$this->log_error($automation_id, $entry_id, 'invalid_config', 'Email field mapping is missing');
return;
}
if (!is_array($config['list_mappings']) || empty($config['list_mappings'])) {
$this->log_error($automation_id, $entry_id, 'invalid_config', 'List mappings are empty or invalid');
return;
}
// Extract form data
$email = $this->get_field_value($fields, $config['field_mapping']['email']);
$firstname = $this->get_field_value($fields, $config['field_mapping']['firstname']);
$lastname = $this->get_field_value($fields, $config['field_mapping']['lastname']);
$trigger_value = $this->get_field_value($fields, $config['trigger_field_id']);
// Validate email address
if (empty($email)) {
$this->log_error($automation_id, $entry_id, 'missing_email', 'Email field is empty');
return;
}
// Sanitize and validate email format
$email = sanitize_email($email);
if (!is_email($email)) {
$this->log_error($automation_id, $entry_id, 'invalid_email', 'Email address is not valid: ' . sanitize_text_field($email));
return;
}
// Determine which lists to add the contact to based on trigger value
$lists_to_add = $this->get_lists_for_trigger_value($trigger_value, $config['list_mappings']);
if (empty($lists_to_add)) {
// No matching lists, this is normal behavior
return;
}
// Prepare contact properties
$properties = array();
if (!empty($firstname)) {
$properties['firstname'] = $firstname;
}
if (!empty($lastname)) {
$properties['lastname'] = $lastname;
}
// Add to Mailjet with retry logic
$this->add_to_mailjet_with_retry($automation_id, $entry_id, $email, $lists_to_add, $properties, $config['api_key'], $config['api_secret']);
}
/**
* Get field value from fields array.
*/
private function get_field_value($fields, $field_id) {
if (!isset($fields[$field_id])) {
return '';
}
$value = $fields[$field_id]['value'];
// Handle arrays (checkboxes, multi-select)
if (is_array($value)) {
return $value;
}
return sanitize_text_field($value);
}
/**
* Get lists for trigger value.
*/
private function get_lists_for_trigger_value($trigger_value, $mappings) {
$lists = array();
// Handle array values (checkboxes, multi-select)
if (is_array($trigger_value)) {
foreach ($trigger_value as $value) {
if (isset($mappings[$value])) {
$lists[] = $mappings[$value];
}
}
} else {
// Single value (radio, dropdown)
if (isset($mappings[$trigger_value])) {
$lists[] = $mappings[$trigger_value];
}
}
return array_unique(array_filter($lists));
}
/**
* Add to Mailjet with retry logic.
*/
private function add_to_mailjet_with_retry($automation_id, $entry_id, $email, $lists, $properties, $api_key, $api_secret) {
$decrypted_key = WPFMJ_Encryption::decrypt($api_key);
$decrypted_secret = WPFMJ_Encryption::decrypt($api_secret);
// Check for decryption failures (can happen if encryption key changed)
if ($decrypted_key === false || $decrypted_secret === false || empty($decrypted_key) || empty($decrypted_secret)) {
$this->log_error(
$automation_id,
$entry_id,
'decryption_failed',
'Failed to decrypt API credentials. The encryption key may have changed. Please re-save this automation with valid API credentials.'
);
$this->notify_admin_of_failure($automation_id, $entry_id, 'API credential decryption failed');
return;
}
$api = new WPFMJ_Mailjet_API($decrypted_key, $decrypted_secret);
$attempt = 0;
$success = false;
while ($attempt < $this->max_retries && !$success) {
$result = $api->add_contact_to_lists($email, $lists, $properties);
if (!is_wp_error($result)) {
$success = true;
// Log success
do_action('wpfmj_automation_success', $automation_id, $entry_id, $email, $lists);
} else {
$attempt++;
if ($attempt < $this->max_retries) {
// Exponential backoff: 1s, 2s, 4s
sleep(pow(2, $attempt - 1));
} else {
// Max retries reached, log error
$this->log_error(
$automation_id,
$entry_id,
'mailjet_api_error',
$result->get_error_message(),
$attempt
);
// Notify admin
$this->notify_admin_of_failure($automation_id, $entry_id, $result->get_error_message());
}
}
}
}
/**
* Log error.
*/
private function log_error($automation_id, $entry_id, $error_type, $error_message, $retry_count = 0) {
$logger = new WPFMJ_Error_Logger();
$logger->log($automation_id, $entry_id, $error_type, $error_message, $retry_count);
}
/**
* Notify admin of failure.
*/
private function notify_admin_of_failure($automation_id, $entry_id, $error_message) {
// Check if notifications are disabled
if (apply_filters('wpfmj_disable_failure_notifications', false)) {
return;
}
// Get notification recipients (filterable)
$default_email = get_option('admin_email');
$recipients = apply_filters('wpfmj_failure_notification_emails', array($default_email));
// Ensure recipients is an array
if (!is_array($recipients)) {
$recipients = array($default_email);
}
// Remove invalid emails
$recipients = array_filter($recipients, function($email) {
return is_email($email);
});
if (empty($recipients)) {
error_log('WPFMJ: No valid email recipients for failure notification');
return;
}
$automation_title = get_the_title($automation_id);
$site_name = get_bloginfo('name');
// Sanitize all data for email
$automation_title = sanitize_text_field($automation_title);
$site_name = sanitize_text_field($site_name);
$error_message = sanitize_textarea_field($error_message);
$entry_id = intval($entry_id);
$subject = sprintf(
'[%s] Mailjet Automation Failed',
$site_name
);
$message = sprintf(
"A Mailjet automation has failed after %d retry attempts.\n\nAutomation: %s\nEntry ID: %d\nError: %s\n\nPlease check the error logs in your WordPress admin.",
$this->max_retries,
$automation_title,
$entry_id,
$error_message
);
// Use wp_mail with proper headers
$headers = array('Content-Type: text/plain; charset=UTF-8');
// Send to all recipients
foreach ($recipients as $recipient) {
$sent = wp_mail($recipient, $subject, $message, $headers);
if (!$sent) {
error_log("WPFMJ: Failed to send notification email to {$recipient}");
}
}
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* Register all actions and filters for the plugin.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Loader {
/**
* The array of actions registered with WordPress.
*/
protected $actions;
/**
* The array of filters registered with WordPress.
*/
protected $filters;
/**
* Initialize the collections used to maintain the actions and filters.
*/
public function __construct() {
$this->actions = array();
$this->filters = array();
}
/**
* Add a new action to the collection to be registered with WordPress.
*/
public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
$this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
}
/**
* Add a new filter to the collection to be registered with WordPress.
*/
public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
$this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
}
/**
* A utility function that is used to register the actions and hooks into a single collection.
*/
private function add($hooks, $hook, $component, $callback, $priority, $accepted_args) {
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args
);
return $hooks;
}
/**
* Register the filters and actions with WordPress.
*/
public function run() {
foreach ($this->filters as $hook) {
add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
}
foreach ($this->actions as $hook) {
add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
}
}
}

View file

@ -0,0 +1,218 @@
<?php
/**
* Mailjet API wrapper.
*
* @package WPFMJ
* @subpackage WPFMJ/includes
*/
class WPFMJ_Mailjet_API {
/**
* API base URL.
*/
private $api_url = 'https://api.mailjet.com/v3';
/**
* API key.
*/
private $api_key;
/**
* API secret.
*/
private $api_secret;
/**
* Constructor.
*/
public function __construct($api_key = '', $api_secret = '') {
$this->api_key = $api_key;
$this->api_secret = $api_secret;
}
/**
* Test API connection.
*/
public function test_connection() {
$response = $this->request('GET', '/contact');
return !is_wp_error($response);
}
/**
* Get all contact lists.
*/
public function get_lists() {
$response = $this->request('GET', '/contactslist?Limit=1000');
if (is_wp_error($response)) {
return $response;
}
return isset($response['Data']) ? $response['Data'] : array();
}
/**
* Add contact to list(s).
*/
public function add_contact_to_lists($email, $lists, $properties = array()) {
// Validate inputs
if (empty($email) || !is_email($email)) {
return new WP_Error('invalid_email', 'Invalid email address provided');
}
if (!is_array($lists) || empty($lists)) {
return new WP_Error('invalid_lists', 'Lists must be a non-empty array');
}
// First, ensure contact exists
$contact = $this->get_or_create_contact($email, $properties);
if (is_wp_error($contact)) {
return $contact;
}
// Validate contact response structure
if (!isset($contact['ID'])) {
return new WP_Error('invalid_contact_response', 'Contact response missing ID field');
}
$contact_id = $contact['ID'];
$results = array();
// Add contact to each list
foreach ($lists as $list_id) {
// Validate list ID
$list_id = absint($list_id);
if ($list_id === 0) {
$results['invalid'] = new WP_Error('invalid_list_id', 'Invalid list ID');
continue;
}
$result = $this->add_contact_to_list($contact_id, $list_id);
$results[$list_id] = $result;
}
return $results;
}
/**
* Get or create a contact.
*/
private function get_or_create_contact($email, $properties = array()) {
// Try to get existing contact
$response = $this->request('GET', '/contact/' . rawurlencode($email));
if (!is_wp_error($response) && isset($response['Data'][0])) {
// Contact exists, update properties if provided
if (!empty($properties)) {
$this->update_contact($email, $properties);
}
return $response['Data'][0];
}
// Create new contact
$data = array('Email' => $email);
if (!empty($properties)) {
$data['Properties'] = $properties;
}
$response = $this->request('POST', '/contact', $data);
if (is_wp_error($response)) {
return $response;
}
return isset($response['Data'][0]) ? $response['Data'][0] : new WP_Error('contact_creation_failed', 'Failed to create contact');
}
/**
* Update contact properties.
*/
private function update_contact($email, $properties) {
$data = array('Data' => $properties);
return $this->request('PUT', '/contactdata/' . rawurlencode($email), $data);
}
/**
* Add contact to a specific list.
*/
private function add_contact_to_list($contact_id, $list_id) {
$data = array(
'ContactID' => $contact_id,
'ListID' => $list_id,
'IsActive' => true
);
return $this->request('POST', '/contactslist/' . $list_id . '/managecontact', $data);
}
/**
* Make API request with rate limiting.
*/
private function request($method, $endpoint, $data = array()) {
// Rate limiting: configurable via filter, default 60 requests per minute
$rate_limit = apply_filters('wpfmj_api_rate_limit', 60);
$rate_limit = max(10, min(300, absint($rate_limit))); // Clamp between 10-300
$rate_limit_key = 'wpfmj_api_rate_' . md5($this->api_key);
$requests = get_transient($rate_limit_key);
if ($requests === false) {
$requests = 0;
}
if ($requests >= $rate_limit) {
return new WP_Error('rate_limit_exceeded', sprintf(
'API rate limit exceeded (%d requests per minute). Please wait a moment and try again.',
$rate_limit
));
}
// Increment request count
set_transient($rate_limit_key, $requests + 1, 60);
$url = $this->api_url . $endpoint;
$args = array(
'method' => $method,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Basic ' . base64_encode($this->api_key . ':' . $this->api_secret)
),
'timeout' => 30,
'sslverify' => true
);
if ($method === 'POST' || $method === 'PUT') {
$args['body'] = wp_json_encode($data);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return $response;
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
// Validate JSON response
$json = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error(
'mailjet_json_error',
sprintf('Invalid JSON response from Mailjet API: %s', json_last_error_msg()),
array('status' => $code, 'body' => substr($body, 0, 200))
);
}
if ($code >= 400) {
$message = isset($json['ErrorMessage']) ? sanitize_text_field($json['ErrorMessage']) : 'API request failed';
return new WP_Error('mailjet_api_error', $message, array('status' => $code));
}
return $json;
}
}

View file

@ -0,0 +1,2 @@
<?php
// Silence is golden.