added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
Loading…
Add table
Add a link
Reference in a new issue