/** * @package Crockidialer * @version 1.0.1 */ /* Plugin Name: Crockidialer Plugin URI: Description: CRM and Dialing System Version: 1.0.1 Author: Crockidialer Author URI: Text Domain: crockidialer Domain Path: /languages Requires at least: 5.0 Requires PHP: 7.4 */ // Absolute first - Prevent direct access if (!defined('ABSPATH')) { if (!headers_sent()) { header('HTTP/1.1 403 Forbidden'); } die('Access Denied. No direct script access allowed.'); } // Critical constants that must exist if (!defined('WPINC')) { die('WordPress environment not loaded properly'); } // Prevent double loading if (defined('CROCKIDIALER_VERSION')) { return; } // Define plugin constants with unique prefixes define('CROCKIDIALER_VERSION', '1.0.1'); define('CROCKIDIALER_FILE', __FILE__); define('CROCKIDIALER_PATH', plugin_dir_path(__FILE__)); define('CROCKIDIALER_URL', plugin_dir_url(__FILE__)); define('CROCKIDIALER_BASENAME', plugin_basename(__FILE__)); define('CROCKIDIALER_DEBUG', false); // Minimum PHP version check if (version_compare(PHP_VERSION, '7.4', '<')) { add_action('admin_notices', function() { $message = sprintf( 'Crockidialer requires PHP version 7.4 or higher. Your current version is %s. Please upgrade PHP.', PHP_VERSION ); printf('

%s

', esc_html($message)); }); return; } // WordPress version compatibility check global $wp_version; if (version_compare($wp_version, '5.0', '<')) { add_action('admin_notices', function() use ($wp_version) { $message = sprintf( 'Crockidialer requires WordPress 5.0 or higher. Your current version is %s.', $wp_version ); printf('

%s

', esc_html($message)); }); return; } // Required WordPress files require_once(ABSPATH . 'wp-admin/includes/plugin.php'); // Custom exception handler that won't break WordPress class CrockidialerException extends Exception { public function __construct($message = "", $code = 0, Throwable $previous = null) { error_log("Crockidialer Exception: {$message}"); parent::__construct($message, $code, $previous); } } // Enhanced error handler that logs but never crashes function crockidialer_error_handler($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { return false; } $error_message = sprintf( "Crockidialer Error [%s]: %s in %s on line %d", $errno, $errstr, str_replace(CROCKIDIALER_PATH, '', $errfile), $errline ); error_log($error_message); if (CROCKIDIALER_DEBUG && current_user_can('manage_options')) { add_action('admin_notices', function() use ($error_message) { printf('

%s

', esc_html($error_message)); }); } return true; } set_error_handler('crockidialer_error_handler'); // Bulletproof autoloader with multiple safety checks spl_autoload_register(function ($class) { try { // Early return if not our namespace if (strpos($class, 'Crockidialer\\') !== 0) { return; } // Sanitize class name to prevent directory traversal $class = str_replace(['../', '..\\'], '', $class); $prefix = 'Crockidialer\\'; $base_dir = CROCKIDIALER_PATH . 'src/'; $relative_class = substr($class, strlen($prefix)); $file = $base_dir . str_replace('\\', DIRECTORY_SEPARATOR, $relative_class) . '.php'; // Multiple validation checks if ( !is_readable($file) || !is_file($file) || pathinfo($file, PATHINFO_EXTENSION) !== 'php' ) { throw new CrockidialerException("Invalid class file: {$relative_class}"); } require_once $file; if (!class_exists($class, false) && !interface_exists($class, false) && !trait_exists($class, false)) { throw new CrockidialerException("Class {$class} not found in {$file}"); } } catch (Throwable $e) { error_log("Crockidialer Autoloader Error: " . $e->getMessage()); if (CROCKIDIALER_DEBUG && current_user_can('manage_options')) { add_action('admin_notices', function() use ($e) { printf( '

Autoloader Error: %s

', esc_html($e->getMessage()) ); }); } } }); // Core initialization with comprehensive error handling function crockidialer_init() { // Check if plugin is properly initialized if (get_option('crockidialer_initialized') !== '1') { add_action('admin_notices', function() { printf( '

%s

', esc_html__('Crockidialer plugin is not properly initialized. Please deactivate and reactivate the plugin.', 'crockidialer') ); }); return; } if (!current_user_can('activate_plugins')) { return; } try { // Verify autoloader is available if (!file_exists(CROCKIDIALER_PATH . 'vendor/autoload.php')) { throw new CrockidialerException('Composer dependencies not installed'); } // Required files with detailed error reporting $required_files = [ 'src/Controllers/LeadController.php', 'src/Controllers/ClientController.php', 'src/Controllers/EmailController.php', 'src/Controllers/TwilioController.php', 'src/Controllers/AnalyticsController.php', 'src/Controllers/MassComController.php', 'src/Controllers/AjaxController.php', 'src/Controllers/LeadIntegrationController.php', 'src/Controllers/TemplateController.php', 'src/Controllers/CommunicationController.php', 'src/Controllers/EmailTemplateController.php' ]; // Verify directory exists first if (!is_dir(CROCKIDIALER_PATH . 'src/Controllers')) { throw new CrockidialerException('Controllers directory not found'); } // Check file permissions and readability $missing_files = array_filter($required_files, function($file) { $full_path = CROCKIDIALER_PATH . $file; return !is_readable($full_path) || !is_file($full_path); }); if (!empty($missing_files)) { throw new CrockidialerException( 'Missing or unreadable files: ' . implode(', ', $missing_files) ); } // Admin-specific initialization with safety checks if (is_admin()) { if (!function_exists('add_menu_page')) { throw new CrockidialerException('WordPress admin functions not available'); } add_action('admin_menu', 'crockidialer_admin_menu'); // Controller initialization with individual error handling $controllers = [ 'LeadController', 'ClientController', 'EmailController', 'TwilioController', 'AnalyticsController', 'MassComController', 'AjaxController', 'LeadIntegrationController', 'TemplateController', 'CommunicationController', 'EmailTemplateController' ]; // Verify namespace exists if (!interface_exists('\\Crockidialer\\Controllers\\ControllerInterface')) { throw new CrockidialerException('Controller interface not found'); } foreach ($controllers as $controller) { try { $class = "\\Crockidialer\\Controllers\\{$controller}"; // Multiple validation checks if (!class_exists($class)) { throw new CrockidialerException("Controller class not found: {$controller}"); } $reflection = new ReflectionClass($class); if (!$reflection->implementsInterface('\\Crockidialer\\Controllers\\ControllerInterface')) { throw new CrockidialerException("Controller {$controller} must implement ControllerInterface"); } if (!$reflection->hasMethod('init')) { throw new CrockidialerException("Controller missing init method: {$controller}"); } $instance = new $class(); // Verify instance if (!($instance instanceof \Crockidialer\Controllers\ControllerInterface)) { throw new CrockidialerException("Invalid controller instance: {$controller}"); } $instance->init(); } catch (Throwable $e) { error_log("Crockidialer Controller Error ({$controller}): " . $e->getMessage()); // Only show to admins in debug mode if (CROCKIDIALER_DEBUG && current_user_can('manage_options')) { add_action('admin_notices', function() use ($e, $controller) { printf( '

Controller Error (%s): %s

', esc_html($controller), esc_html($e->getMessage()) ); }); } continue; // Skip failed controller but continue with others } } } } catch (Throwable $e) { error_log("Crockidialer Critical Error: " . $e->getMessage()); if (current_user_can('manage_options')) { add_action('admin_notices', function() use ($e) { printf( '

%s

', esc_html($e->getMessage()) ); }); } // Deactivate plugin on critical error require_once(ABSPATH . 'wp-admin/includes/plugin.php'); deactivate_plugins(plugin_basename(__FILE__)); if (isset($_GET['activate'])) { unset($_GET['activate']); } } } // Admin menu setup with validation function crockidialer_admin_menu() { if (!current_user_can('manage_options')) { return; } add_menu_page( esc_html__('Crockidialer', 'crockidialer'), esc_html__('Crockidialer', 'crockidialer'), 'manage_options', 'crockidialer', 'crockidialer_admin_page', 'dashicons-phone', 30 ); } // Admin page with comprehensive security function crockidialer_admin_page() { if (!current_user_can('manage_options')) { wp_die(__('You do not have sufficient permissions to access this page.', 'crockidialer')); } $dashboard_file = CROCKIDIALER_PATH . 'src/Views/admin/dashboard.php'; if (is_readable($dashboard_file) && is_file($dashboard_file)) { include $dashboard_file; } else { printf( '

%s

%s

', esc_html__('Crockidialer', 'crockidialer'), esc_html__('Dashboard template not found.', 'crockidialer') ); } } function crockidialer_pre_activation_check(): array { $requirements = []; $errors = []; // Check PHP version $requirements['php'] = [ 'required' => '7.4', 'current' => PHP_VERSION, 'pass' => version_compare(PHP_VERSION, '7.4', '>=') ]; if (!$requirements['php']['pass']) { $errors[] = sprintf('PHP version %s or higher is required. Your version: %s', '7.4', PHP_VERSION); } // Check WordPress version global $wp_version; $requirements['wordpress'] = [ 'required' => '5.0', 'current' => $wp_version, 'pass' => version_compare($wp_version, '5.0', '>=') ]; if (!$requirements['wordpress']['pass']) { $errors[] = sprintf('WordPress version %s or higher is required. Your version: %s', '5.0', $wp_version); } // Check PHP extensions $required_extensions = ['curl', 'json', 'mbstring', 'openssl', 'pdo']; foreach ($required_extensions as $ext) { $requirements['extensions'][$ext] = [ 'required' => true, 'current' => extension_loaded($ext), 'pass' => extension_loaded($ext) ]; if (!$requirements['extensions'][$ext]['pass']) { $errors[] = sprintf('PHP extension %s is required but not installed.', $ext); } } // Check write permissions $required_writable_paths = [ CROCKIDIALER_PATH . 'logs', WP_CONTENT_DIR . '/uploads' ]; foreach ($required_writable_paths as $path) { $is_writable = wp_mkdir_p($path) && is_writable($path); $requirements['writable'][$path] = [ 'required' => true, 'current' => $is_writable, 'pass' => $is_writable ]; if (!$requirements['writable'][$path]['pass']) { $errors[] = sprintf('Directory %s must be writable.', $path); } } // Check MySQL version global $wpdb; $mysql_version = $wpdb->db_version(); $requirements['mysql'] = [ 'required' => '5.6', 'current' => $mysql_version, 'pass' => version_compare($mysql_version, '5.6', '>=') ]; if (!$requirements['mysql']['pass']) { $errors[] = sprintf('MySQL version %s or higher is required. Your version: %s', '5.6', $mysql_version); } return [ 'requirements' => $requirements, 'errors' => $errors, 'pass' => empty($errors) ]; } function crockidialer_activate() { if (!current_user_can('activate_plugins')) { return; } try { // Run pre-activation checks $check_results = crockidialer_pre_activation_check(); if (!$check_results['pass']) { throw new CrockidialerException( "Pre-activation checks failed:\n" . implode("\n", $check_results['errors']) ); } // Check for Composer autoloader if (!file_exists(CROCKIDIALER_PATH . 'vendor/autoload.php')) { throw new CrockidialerException('Composer dependencies not installed. Please run composer install.'); } // Load Composer autoloader require_once CROCKIDIALER_PATH . 'vendor/autoload.php'; // Verify required directories exist and are writable $required_dirs = [ CROCKIDIALER_PATH . 'src/Controllers', CROCKIDIALER_PATH . 'src/Services', CROCKIDIALER_PATH . 'src/Templates', CROCKIDIALER_PATH . 'logs' ]; foreach ($required_dirs as $dir) { if (!is_dir($dir) && !wp_mkdir_p($dir)) { throw new CrockidialerException("Failed to create directory: $dir"); } if (!is_writable($dir)) { throw new CrockidialerException("Directory not writable: $dir"); } } // Verify required files exist $required_files = [ CROCKIDIALER_PATH . 'src/Controllers/ControllerInterface.php', CROCKIDIALER_PATH . 'src/Services/BaseService.php' ]; foreach ($required_files as $file) { if (!is_readable($file)) { throw new CrockidialerException("Required file not readable: $file"); } } // Set up database tables with transaction support global $wpdb; $wpdb->show_errors(); require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // Start transaction if supported $has_transaction = method_exists($wpdb, 'query') && $wpdb->query('START TRANSACTION'); try { // Set up database tables $charset_collate = $wpdb->get_charset_collate(); // Create leads table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_leads ( id bigint(20) NOT NULL AUTO_INCREMENT, client_name varchar(255) NOT NULL, phone_number varchar(20) NOT NULL, email varchar(255), source varchar(50), status varchar(50) DEFAULT 'new', disposition varchar(50), family_members int(11), date_of_birth date, address text, meta_data longtext, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_disposition (disposition), KEY idx_created_at (created_at) ) $charset_collate;"; dbDelta($sql); // Create templates table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_templates ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, type varchar(50) NOT NULL, subject varchar(255), content longtext NOT NULL, variables text, category varchar(50), created_by bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type (type), KEY idx_category (category) ) $charset_collate;"; dbDelta($sql); // Create campaigns table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_campaigns ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, type varchar(50) NOT NULL, message text, email_subject varchar(255), email_template varchar(50), schedule_type varchar(50), schedule_time datetime, target_segment varchar(50), status varchar(50) DEFAULT 'draft', last_run datetime, created_by bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_schedule_time (schedule_time) ) $charset_collate;"; dbDelta($sql); // Create communications log table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_communications ( id bigint(20) NOT NULL AUTO_INCREMENT, lead_id bigint(20) NOT NULL, type varchar(50) NOT NULL, message text NOT NULL, metadata text, created_by bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_lead_id (lead_id), KEY idx_type (type) ) $charset_collate;"; dbDelta($sql); // Create activity log table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_activity_log ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, action varchar(50) NOT NULL, data text, ip varchar(45), timestamp datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_action (action) ) $charset_collate;"; dbDelta($sql); // Create email log table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_email_log ( id bigint(20) NOT NULL AUTO_INCREMENT, lead_id bigint(20) NOT NULL, template_type varchar(50), sent_at datetime DEFAULT CURRENT_TIMESTAMP, sent_by bigint(20), PRIMARY KEY (id), KEY idx_lead_id (lead_id), KEY idx_template_type (template_type) ) $charset_collate;"; dbDelta($sql); // Create calls table $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}crockidialer_calls ( id bigint(20) NOT NULL AUTO_INCREMENT, lead_id bigint(20) NOT NULL, twilio_sid varchar(255), status varchar(50), duration int(11) DEFAULT 0, recording_url text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_lead_id (lead_id), KEY idx_twilio_sid (twilio_sid) ) $charset_collate;"; dbDelta($sql); if ($has_transaction) { $wpdb->query('COMMIT'); } } catch (Throwable $e) { if ($has_transaction) { $wpdb->query('ROLLBACK'); } throw $e; } // Set default options with validation $default_options = [ 'crockidialer_twilio_account_sid' => '', 'crockidialer_twilio_auth_token' => '', 'crockidialer_twilio_phone_number' => '', 'crockidialer_sendgrid_api_key' => '', 'crockidialer_email_from_address' => get_option('admin_email'), 'crockidialer_email_from_name' => get_bloginfo('name'), 'crockidialer_debug_mode' => '0', 'crockidialer_log_level' => 'error', 'crockidialer_initialized' => '0' ]; foreach ($default_options as $option => $value) { if (get_option($option) === false) { add_option($option, $value); } } // Create .htaccess for logs directory $htaccess_content = "Order deny,allow\nDeny from all"; $htaccess_file = CROCKIDIALER_PATH . 'logs/.htaccess'; if (!file_exists($htaccess_file)) { if (file_put_contents($htaccess_file, $htaccess_content) === false) { throw new CrockidialerException('Failed to create .htaccess file for logs directory'); } } // Store activation time and mark as initialized update_option('crockidialer_activated_time', time()); update_option('crockidialer_initialized', '1'); // Flush rewrite rules flush_rewrite_rules(); // Clear any existing error logs $log_file = CROCKIDIALER_PATH . 'logs/crockidialer.log'; if (file_exists($log_file)) { @unlink($log_file); } } catch (Throwable $e) { error_log("Crockidialer Activation Error: " . $e->getMessage()); // Cleanup on activation failure foreach ($default_options as $option => $value) { delete_option($option); } wp_die( esc_html($e->getMessage()), 'Activation Error', ['back_link' => true] ); } } // Careful deactivation that preserves data function crockidialer_deactivate() { if (!current_user_can('activate_plugins')) { return; } try { // Cleanup transients delete_transient('crockidialer_doing_update'); // Log deactivation error_log("Crockidialer: Plugin deactivated successfully"); // Store deactivation time update_option('crockidialer_deactivated_time', time()); } catch (Throwable $e) { error_log("Crockidialer Deactivation Error: " . $e->getMessage()); } } // Uninstall hook for complete cleanup function crockidialer_uninstall() { if (!current_user_can('activate_plugins')) { return; } try { // Clean up all plugin-specific options delete_option('crockidialer_activated_time'); delete_option('crockidialer_deactivated_time'); // Log uninstall error_log("Crockidialer: Plugin uninstalled successfully"); } catch (Throwable $e) { error_log("Crockidialer Uninstall Error: " . $e->getMessage()); } } // Register core hooks with existence checks if (function_exists('register_activation_hook')) { register_activation_hook(CROCKIDIALER_FILE, 'crockidialer_activate'); } if (function_exists('register_deactivation_hook')) { register_deactivation_hook(CROCKIDIALER_FILE, 'crockidialer_deactivate'); } if (function_exists('register_uninstall_hook')) { register_uninstall_hook(CROCKIDIALER_FILE, 'crockidialer_uninstall'); } // Initialize only if all systems are go if ( defined('CROCKIDIALER_VERSION') && function_exists('add_action') && did_action('plugins_loaded') === 0 ) { add_action('plugins_loaded', 'crockidialer_init'); }