PluginProbe
Opti-Behavior – Self-Hosted Heatmaps, Session Recordings, Funnels, A/B Testing & Smart Insights / 1.8.0
Opti-Behavior – Self-Hosted Heatmaps, Session Recordings, Funnels, A/B Testing & Smart Insights v1.8.0
1.9.0 1.8.3 1.8.2 1.8.1 1.8.0 1.7.2 1.7.0 1.7.1 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 trunk 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 All 40 releases
opti-behavior / opti-behavior.php

opti-behavior.php in Opti-Behavior – Self-Hosted Heatmaps, Session Recordings, Funnels, A/B Testing & Smart Insights 1.8.0, at opti-behavior.php

480 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Opti-Behavior – Self-Hosted Heatmaps, Session Recordings, Funnels, A/B Testing & Smart Insights
4 * Plugin URI: https://optiuser.com/
5 * Description: Self-hosted heatmaps, funnels, A/B WooCommerce testing, behavior analytics & Smart Insights for WordPress. Own your data and optimize what users do.
6 * Version: 1.8.0
7 * Author: OptiUser
8 * Author URI: https://optiuser.com/
9 * License: GPLv2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 * Text Domain: opti-behavior
12 * Domain Path: /languages
13 * Requires at least: 5.8
14 * Requires PHP: 7.4
15 *
16 * @package opti-behavior
17 * @copyright 2025-2026 OptiUser
18 * @version 1.8.0
19 */
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 // Check PHP version compatibility.
26 if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
27 add_action( 'admin_notices', 'opti_behavior_php_version_notice' );
28 /**
29 * Display admin notice for incompatible PHP version.
30 *
31 * Shows an error message when the server is running an incompatible PHP version
32 * and automatically deactivates the plugin.
33 *
34 * @since 1.0.4
35 */
36 function opti_behavior_php_version_notice() {
37 ?>
38 <div class="notice notice-error">
39 <p>
40 <?php
41 printf(
42 /* translators: 1: Required PHP version, 2: Current PHP version */
43 esc_html__( 'Opti-Behavior requires PHP version %1$s or higher. You are running PHP version %2$s. Please upgrade PHP to activate this plugin.', 'opti-behavior' ),
44 '7.4',
45 esc_html( PHP_VERSION )
46 );
47 ?>
48 </p>
49 </div>
50 <?php
51 // Deactivate the plugin.
52 if ( function_exists( 'deactivate_plugins' ) ) {
53 deactivate_plugins( plugin_basename( __FILE__ ) );
54 }
55 }
56 return;
57 }
58
59 if ( ! function_exists( 'opti_behavior_heatmap_conflict' ) ) {
60 /**
61 * Display error message when plugin conflict is detected.
62 *
63 * Terminates execution and displays a user-friendly error message
64 * when another version of the plugin is already active.
65 *
66 * @since 1.0.0
67 */
68 function opti_behavior_heatmap_conflict() {
69 die( esc_html__( 'Fail to activate. Another version of opti-behavior Heatmap is already active.', 'opti-behavior' ) );
70 }
71 }
72
73 // Check for plugin conflicts.
74 if ( defined( 'OPTI_BEHAVIOR_HEATMAP' ) ) {
75 if ( is_admin() ) {
76 register_activation_hook( __FILE__, 'opti_behavior_heatmap_conflict' );
77 }
78 return;
79 } else {
80 define( 'OPTI_BEHAVIOR_HEATMAP', __FILE__ );
81 }
82
83 // Define plugin constants.
84 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.8.0' );
85 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
86 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
87 define( 'OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR', OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR . 'includes/' );
88 define( 'OPTI_BEHAVIOR_HEATMAP_ADMIN_DIR', OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR . 'admin/' );
89 define( 'OPTI_BEHAVIOR_HEATMAP_PUBLIC_DIR', OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR . 'public/' );
90 define( 'OPTI_BEHAVIOR_HEATMAP_ASSETS_URL', OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL . 'assets/' );
91
92 /**
93 * API Environment Configuration
94 * Set the environment for API connections:
95 * - 'local' => Uses http://localhost/API/ for local development
96 * - 'live' => Uses https://api.optiuser.com/ for production (default)
97 *
98 * Change this value to switch between local and live API servers.
99 */
100 if ( ! defined( 'OPTI_BEHAVIOR_ENVIRONMENT' ) ) {
101 define( 'OPTI_BEHAVIOR_ENVIRONMENT', 'live' ); // Options: 'local' or 'live'
102 }
103
104 // Initialize autoloader.
105 require_once __DIR__ . '/includes/class-autoloader.php';
106 Opti_Behavior_Heatmap_Autoloader::init( __DIR__ );
107
108 // Load tooltip helper functions.
109 require_once __DIR__ . '/includes/tooltip-helper.php';
110
111 // Load dashboard cache helpers (TTL + invalidation).
112 require_once __DIR__ . '/includes/opti-behavior-dashboard-cache.php';
113
114 // Load shared admin contact resolver early for welcome/trial and Pro registration flows.
115 require_once __DIR__ . '/includes/class-opti-behavior-admin-email-resolver.php';
116
117 // Load welcome/consent class early so its activation hook fires on plugin activation.
118 require_once __DIR__ . '/includes/class-opti-behavior-welcome.php';
119
120 // Load onboarding popup class (shown on dashboard after first activation with no data).
121 require_once __DIR__ . '/includes/class-opti-behavior-onboarding.php';
122
123 // Load delayed WordPress.org review reminder banner.
124 require_once __DIR__ . '/includes/class-opti-behavior-review-banner.php';
125
126 // Load the Pro endpoint graceful-degradation shim. Serves cached/CDN visitors
127 // running stale Pro tracker JS a well-formed AJAX success for any Pro-only save
128 // action that has no live handler, so they stop flooding admin-ajax with 400
129 // retries. Registered on `init` priority 999 — after Pro binds its real
130 // handlers on `init` priority 11 — so the per-action has_action() probe is
131 // authoritative (covers Pro off AND Pro active-but-unlicensed).
132 require_once __DIR__ . '/includes/class-opti-behavior-pro-shim.php';
133 add_action( 'init', array( 'Opti_Behavior_Pro_Ajax_Shim', 'maybe_register' ), 999 );
134
135 // Register plugin hooks.
136 register_activation_hook( __FILE__, 'Opti_Behavior_Heatmap_Core::activation' );
137 register_activation_hook( __FILE__, array( 'Opti_Behavior_Welcome', 'on_activation' ) );
138 register_activation_hook( __FILE__, array( 'Opti_Behavior_Review_Banner', 'on_activation' ) );
139 register_deactivation_hook( __FILE__, 'Opti_Behavior_Heatmap_Core::deactivation' );
140
141 if ( ! function_exists( 'opti_behavior_purge_all_page_caches' ) ) {
142 /**
143 * Purge all known full-page caches domain-wide.
144 *
145 * Cached HTML embeds this plugin's tracker bootstrap (script tags, nonces,
146 * inline config). Activating or deactivating either plugin changes what the
147 * page HTML must contain, so stale cached copies would keep serving trackers
148 * that no longer exist (404 JS, dead AJAX endpoints) or omit ones that should
149 * run. Best-effort: every branch is guarded and the whole call never throws.
150 *
151 * @since 1.7.1
152 */
153 function opti_behavior_purge_all_page_caches() {
154 try {
155 // WP Rocket.
156 if ( function_exists( 'rocket_clean_domain' ) ) {
157 rocket_clean_domain();
158 }
159 // LiteSpeed Cache.
160 do_action( 'litespeed_purge_all' );
161 // W3 Total Cache.
162 if ( function_exists( 'w3tc_flush_all' ) ) {
163 w3tc_flush_all();
164 }
165 // WP Super Cache.
166 if ( function_exists( 'wp_cache_clear_cache' ) ) {
167 wp_cache_clear_cache();
168 }
169 // WP Fastest Cache.
170 if ( function_exists( 'wpfc_clear_all_cache' ) ) {
171 wpfc_clear_all_cache( true );
172 }
173 // Cache Enabler.
174 do_action( 'cache_enabler_clear_complete_cache' );
175 // Hummingbird.
176 do_action( 'wphb_clear_page_cache' );
177 // SiteGround Optimizer.
178 if ( function_exists( 'sg_cachepress_purge_cache' ) ) {
179 sg_cachepress_purge_cache();
180 }
181 // Autoptimize.
182 if ( class_exists( 'autoptimizeCache' ) && method_exists( 'autoptimizeCache', 'clearall' ) ) {
183 autoptimizeCache::clearall();
184 }
185 } catch ( \Throwable $e ) {
186 // Never block (de)activation on a cache-purge failure.
187 unset( $e );
188 }
189 }
190 }
191 // Stale cached HTML must never outlive an activation state change (see helper docblock).
192 register_activation_hook( __FILE__, 'opti_behavior_purge_all_page_caches' );
193 register_deactivation_hook( __FILE__, 'opti_behavior_purge_all_page_caches' );
194
195 /**
196 * Override text domain loading to use custom locale for Opti-Behavior pages.
197 *
198 * @since 1.0.0
199 */
200 add_filter(
201 'override_load_textdomain',
202 function( $override, $domain, $mofile ) {
203 // Only apply to opti-behavior and opti-behavior-pro text domains
204 if ( $domain !== 'opti-behavior' && $domain !== 'opti-behavior-pro' ) {
205 return $override;
206 }
207
208 // Only apply on admin pages
209 if ( ! is_admin() ) {
210 return $override;
211 }
212
213 // Check if we're on an Opti-Behavior admin page
214 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET parameter used for page identification (read-only operation)
215 $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
216 // phpcs:enable WordPress.Security.NonceVerification.Recommended
217 global $pagenow;
218 $is_plugins_screen = ( isset( $pagenow ) && 'plugins.php' === $pagenow );
219
220 $opti_behavior_pages = array(
221 'opti-behavior-analytics',
222 'opti-behavior-heatmaps',
223 'opti-behavior-heatmap-detail',
224 'opti-behavior-settings',
225 'opti-behavior-ai-insights',
226 'opti-behavior-funnels',
227 'opti-behavior-ab-testing',
228 );
229
230 // Add PRO pages if PRO version is active
231 if ( opti_behavior_pro_active() ) {
232 $opti_behavior_pages[] = 'opti-behavior-recordings';
233 $opti_behavior_pages[] = 'opti-behavior-errors';
234 $opti_behavior_pages[] = 'opti-behavior-user-journey';
235 }
236
237 // Check if we're doing an AJAX request for Opti-Behavior
238 $is_opti_behavior_ajax = false;
239 if ( wp_doing_ajax() ) {
240 // phpcs:disable WordPress.Security.NonceVerification.Missing -- POST parameter used for action identification only (read-only operation, nonce verified in actual AJAX handlers)
241 $ajax_action = isset( $_POST['action'] ) ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : '';
242 // phpcs:enable WordPress.Security.NonceVerification.Missing
243 if ( strpos( $ajax_action, 'opti_behavior' ) === 0 || strpos( $ajax_action, 'optibehavior' ) === 0 ) {
244 $is_opti_behavior_ajax = true;
245 }
246 }
247
248 if ( ! in_array( $current_page, $opti_behavior_pages, true ) && ! $is_opti_behavior_ajax && ! $is_plugins_screen ) {
249 return $override; // Return original for non-plugin pages
250 }
251
252 // Get the saved language preference
253 $selected_language = get_option( 'opti_behavior_admin_language', 'en_US' );
254
255 // Whitelist of allowed languages
256 $allowed_languages = array( 'en_US', 'fr_FR', 'de_DE', 'es_ES', 'pt_BR', 'it_IT' );
257
258 // Validate against whitelist
259 if ( ! in_array( $selected_language, $allowed_languages, true ) ) {
260 $selected_language = 'en_US'; // Default to English if invalid
261 }
262
263 // If English is selected, prevent loading any translation file
264 if ( $selected_language === 'en_US' ) {
265 // Get debug manager if available
266 if ( class_exists( 'Opti_Behavior_Heatmap_Core' ) ) {
267 $core = Opti_Behavior_Heatmap_Core::get_instance();
268 $debug_manager = $core->get_debug_manager();
269 $debug_manager->log( 'English selected - preventing translation file load', 'debug', 'i18n' );
270 }
271 return true; // Tell WordPress we handled it (by not loading any translations)
272 }
273
274 // If a non-English language is selected, load the custom .mo file
275 // Determine the plugin directory based on the domain
276 if ( $domain === 'opti-behavior-pro' ) {
277 $plugin_dir = dirname( __FILE__ ) . '/../opti-behavior-pro';
278 } else {
279 $plugin_dir = dirname( __FILE__ );
280 }
281
282 $custom_mofile = $plugin_dir . '/languages/' . $domain . '-' . $selected_language . '.mo';
283
284 // Get debug manager if available
285 if ( class_exists( 'Opti_Behavior_Heatmap_Core' ) ) {
286 $core = Opti_Behavior_Heatmap_Core::get_instance();
287 $debug_manager = $core->get_debug_manager();
288 $debug_manager->log( 'Override load textdomain. Domain: ' . $domain . ', Original: ' . $mofile . ', Custom: ' . $custom_mofile . ' (exists: ' . ( file_exists( $custom_mofile ) ? 'yes' : 'no' ) . ')', 'debug', 'i18n' );
289 }
290
291 if ( file_exists( $custom_mofile ) ) {
292 // Use WordPress's MO class to load the custom .mo file
293 global $l10n;
294
295 // Create a new MO object and load the custom .mo file
296 $mo = new MO();
297 if ( $mo->import_from_file( $custom_mofile ) ) {
298 $l10n[ $domain ] = $mo;
299
300 // Log success
301 if ( class_exists( 'Opti_Behavior_Heatmap_Core' ) ) {
302 $core = Opti_Behavior_Heatmap_Core::get_instance();
303 $debug_manager = $core->get_debug_manager();
304 $debug_manager->log( 'Successfully loaded custom .mo file for domain "' . $domain . '" with ' . count( $mo->entries ) . ' translations', 'info', 'i18n' );
305 }
306
307 return true; // Tell WordPress we handled the loading
308 } else {
309 // Log failure
310 if ( class_exists( 'Opti_Behavior_Heatmap_Core' ) ) {
311 $core = Opti_Behavior_Heatmap_Core::get_instance();
312 $debug_manager = $core->get_debug_manager();
313 $debug_manager->log( 'Failed to import .mo file for domain "' . $domain . '": ' . $custom_mofile, 'error', 'i18n' );
314 }
315 }
316 }
317
318 return $override; // Let WordPress handle the default loading
319 },
320 10,
321 3
322 );
323
324 /**
325 * Force reload translations BEFORE admin menus are registered.
326 * This must run on admin_menu with priority 1 (before menus at priority 10+).
327 *
328 * @since 2.3.6
329 */
330 add_action(
331 'admin_menu',
332 function() {
333 // Get the saved language preference
334 $selected_language = get_option( 'opti_behavior_admin_language', 'en_US' );
335
336 // If English, no need to reload
337 if ( $selected_language === 'en_US' ) {
338 return;
339 }
340
341 // Whitelist of allowed languages
342 $allowed_languages = array( 'en_US', 'fr_FR', 'de_DE', 'es_ES', 'pt_BR', 'it_IT' );
343 if ( ! in_array( $selected_language, $allowed_languages, true ) ) {
344 return;
345 }
346
347 // Force reload translations for both domains
348 global $l10n;
349
350 // Reload opti-behavior translations
351 $free_mofile = dirname( __FILE__ ) . '/languages/opti-behavior-' . $selected_language . '.mo';
352 if ( file_exists( $free_mofile ) ) {
353 $mo = new MO();
354 if ( $mo->import_from_file( $free_mofile ) ) {
355 $l10n['opti-behavior'] = $mo;
356 }
357 }
358
359 // Reload opti-behavior-pro translations if PRO is active
360 if ( opti_behavior_pro_active() ) {
361 $pro_mofile = dirname( __FILE__ ) . '/../opti-behavior-pro/languages/opti-behavior-pro-' . $selected_language . '.mo';
362 if ( file_exists( $pro_mofile ) ) {
363 $mo = new MO();
364 if ( $mo->import_from_file( $pro_mofile ) ) {
365 $l10n['opti-behavior-pro'] = $mo;
366 }
367 }
368 }
369 },
370 1 // Run BEFORE menus are registered (priority 10+)
371 );
372
373 /**
374 * Initialize plugin core and performance optimizer.
375 *
376 * @since 1.0.0
377 */
378 add_action(
379 'init',
380 function() {
381 $core = Opti_Behavior_Heatmap_Core::get_instance();
382 $core->maybe_migrate_legacy_default_auto_cleanup_settings();
383
384 // Initialize performance optimizer
385 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-performance-optimizer.php';
386
387 // License manager is now in Pro plugin (opti-behavior-pro)
388 // It will be loaded by the Pro plugin if active
389
390 // Manifest manager is now in Pro plugin (opti-behavior-pro)
391 // It will be loaded by the Pro plugin if active
392 // Free plugin has unlimited features - no need for limit checking
393
394 // Initialize file storage
395 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-heatmap-file-storage.php';
396
397 // Initialize centralized optimizer/cache compatibility layer.
398 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-optimizer-compat.php';
399 Opti_Behavior_Optimizer_Compat::init();
400
401 // Initialize tracker-heartbeat: detects optimizer/cache plugins
402 // silently killing tracking scripts (zero events despite traffic).
403 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-tracker-heartbeat.php';
404 Opti_Behavior_Tracker_Heartbeat::init();
405
406 // Initialize welcome/consent flow (must run before tracker).
407 Opti_Behavior_Welcome::init();
408
409 // Initialize onboarding popup (shown on dashboard when no data exists).
410 Opti_Behavior_Onboarding::init();
411
412 // Initialize delayed WordPress.org review reminder banner.
413 Opti_Behavior_Review_Banner::init();
414
415 // Initialize plugin tracker (works for both Free-only and Free+Pro setups)
416 // Sends installation data with 24h heartbeat, auto-detects plugin type
417 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-free-tracker.php';
418 Opti_Behavior_Free_Tracker::init();
419
420 // Initialize broadcast banner manager (API-driven in-plugin messages)
421 require_once OPTI_BEHAVIOR_HEATMAP_INCLUDES_DIR . 'class-opti-behavior-broadcast.php';
422 Opti_Behavior_Broadcast::init();
423
424 // Initialize deactivation survey AJAX relay (plugins.php flow).
425 Opti_Behavior_Heatmap_Deactivation_Survey::init();
426
427 // Session recording is now a PRO feature
428 // It will be initialized by opti-behavior-pro plugin if active
429 }
430 );
431
432 /**
433 * Check if Pro plugin files are present.
434 * This checks if the Pro plugin code exists, but features are gated by the signed manifest.
435 *
436 * @since 1.0.0
437 * @return bool True if Pro plugin is active, false otherwise.
438 */
439 if ( ! function_exists( 'opti_behavior_pro_active' ) ) {
440 function opti_behavior_pro_active() {
441 return defined( 'OPTI_BEHAVIOR_PRO_VERSION' );
442 }
443 }
444
445 /**
446 * Whether the current visitor's IP is excluded from all tracking.
447 *
448 * Global wrapper so the Pro plugin (and third parties) can gate their
449 * trackers with a simple function_exists() check.
450 *
451 * @since 1.6.0
452 * @param string|null $ip IP to test; null = current request.
453 * @return bool True when tracking scripts must not run.
454 */
455 if ( ! function_exists( 'opti_behavior_ip_is_excluded' ) ) {
456 function opti_behavior_ip_is_excluded( $ip = null ) {
457 return Opti_Behavior_IP_Exclusion::is_excluded( $ip );
458 }
459 }
460
461 /**
462 * Add custom cron intervals for scheduled reports.
463 *
464 * @since 1.1.0
465 * @param array $schedules Existing schedules.
466 * @return array Modified schedules.
467 */
468 add_filter(
469 'cron_schedules',
470 function( $schedules ) {
471 if ( ! isset( $schedules['every_fifteen_minutes'] ) ) {
472 $schedules['every_fifteen_minutes'] = array(
473 'interval' => 15 * MINUTE_IN_SECONDS,
474 'display' => __( 'Every 15 Minutes', 'opti-behavior' ),
475 );
476 }
477 return $schedules;
478 }
479 );
480