Changeset 3414697
- Timestamp:
- 12/08/2025 08:27:27 PM (5 weeks ago)
- Location:
- opti-behavior
- Files:
-
- 93 added
- 2 deleted
- 23 edited
-
assets/icon-128x128.png (modified) (previous)
-
assets/icon-256x256.gif (deleted)
-
assets/icon-256x256.png (modified) (previous)
-
tags/1.0.8 (added)
-
tags/1.0.8/Opti-Behavior.php (added)
-
tags/1.0.8/THIRD-PARTY-LICENSES.md (added)
-
tags/1.0.8/admin (added)
-
tags/1.0.8/admin/class-opti-behavior-heatmap-admin-settings.php (added)
-
tags/1.0.8/admin/class-opti-behavior-heatmap-ajax-handler.php (added)
-
tags/1.0.8/admin/class-opti-behavior-heatmap-dashboard.php (added)
-
tags/1.0.8/admin/class-opti-behavior-heatmap-list.php (added)
-
tags/1.0.8/admin/class-opti-behavior-heatmap-post-metabox.php (added)
-
tags/1.0.8/admin/recordings-upgrade-page.php (added)
-
tags/1.0.8/assets (added)
-
tags/1.0.8/assets/css (added)
-
tags/1.0.8/assets/css/admin-menu.css (added)
-
tags/1.0.8/assets/css/admin-notices.css (added)
-
tags/1.0.8/assets/css/admin-utilities.css (added)
-
tags/1.0.8/assets/css/ai-insights-page.css (added)
-
tags/1.0.8/assets/css/countries-widget-scroll.css (added)
-
tags/1.0.8/assets/css/dashboard.css (added)
-
tags/1.0.8/assets/css/dashboard_styles.css (added)
-
tags/1.0.8/assets/css/heatmaps.css (added)
-
tags/1.0.8/assets/css/leaflet.min.css (added)
-
tags/1.0.8/assets/css/metabox.css (added)
-
tags/1.0.8/assets/css/recordings-upgrade.css (added)
-
tags/1.0.8/assets/css/sessions.css (added)
-
tags/1.0.8/assets/css/settings-modal.css (added)
-
tags/1.0.8/assets/css/settings.css (added)
-
tags/1.0.8/assets/css/style.css (added)
-
tags/1.0.8/assets/images (added)
-
tags/1.0.8/assets/images/OB.ico (added)
-
tags/1.0.8/assets/images/leaflet (added)
-
tags/1.0.8/assets/images/leaflet/layers-2x.png (added)
-
tags/1.0.8/assets/images/leaflet/layers.png (added)
-
tags/1.0.8/assets/images/leaflet/marker-icon-2x.png (added)
-
tags/1.0.8/assets/images/leaflet/marker-icon.png (added)
-
tags/1.0.8/assets/images/leaflet/marker-shadow.png (added)
-
tags/1.0.8/assets/js (added)
-
tags/1.0.8/assets/js/admin-utilities.js (added)
-
tags/1.0.8/assets/js/chart.umd.min.js (added)
-
tags/1.0.8/assets/js/dashboard-cleaned.js (added)
-
tags/1.0.8/assets/js/dashboard-converted.js (added)
-
tags/1.0.8/assets/js/dashboard-final.js (added)
-
tags/1.0.8/assets/js/dashboard.js (added)
-
tags/1.0.8/assets/js/frontend-mobile-simulator.js (added)
-
tags/1.0.8/assets/js/frontend-page-title.js (added)
-
tags/1.0.8/assets/js/heatmap.min.js (added)
-
tags/1.0.8/assets/js/heatmap.min.js.LICENSE.txt (added)
-
tags/1.0.8/assets/js/heatmaps.js (added)
-
tags/1.0.8/assets/js/leaflet-config.js (added)
-
tags/1.0.8/assets/js/leaflet.min.js (added)
-
tags/1.0.8/assets/js/lucide.min.js (added)
-
tags/1.0.8/assets/js/metabox-analytics.js (added)
-
tags/1.0.8/assets/js/mobile-simulator.js (added)
-
tags/1.0.8/assets/js/notice-cleanup.js (added)
-
tags/1.0.8/assets/js/opti-behavior-debug.js (added)
-
tags/1.0.8/assets/js/opti-behavior-heatmap-simple-fixed.js (added)
-
tags/1.0.8/assets/js/opti-behavior-heatmap-simple.js (added)
-
tags/1.0.8/assets/js/opti-behavior-heatmap.min.js (added)
-
tags/1.0.8/assets/js/settings.js (added)
-
tags/1.0.8/assets/js/support-form.js (added)
-
tags/1.0.8/includes (added)
-
tags/1.0.8/includes/class-autoloader.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-analytics.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-bot-tracker.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-core.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-data-protection.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-database.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-debug-manager.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-file-storage.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-options.php (added)
-
tags/1.0.8/includes/class-opti-behavior-heatmap-session.php (added)
-
tags/1.0.8/includes/class-opti-behavior-license-manager.php (added)
-
tags/1.0.8/includes/class-opti-behavior-performance-optimizer.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-ai-insights-views.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-ajax-handlers.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-assets.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-dashboard-views.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-data-helpers.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-exports.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-heatmaps-views.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-maintenance.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-sessions-views.php (added)
-
tags/1.0.8/includes/trait-opti-behavior-settings-views.php (added)
-
tags/1.0.8/languages (added)
-
tags/1.0.8/languages/opti-behavior-fr_FR.mo (added)
-
tags/1.0.8/languages/opti-behavior-fr_FR.po (added)
-
tags/1.0.8/languages/opti-behavior.pot (added)
-
tags/1.0.8/public (added)
-
tags/1.0.8/public/class-opti-behavior-heatmap-frontend.php (added)
-
tags/1.0.8/readme.txt (added)
-
tags/1.0.8/views (added)
-
tags/1.0.8/views/dashboard-views.php (added)
-
tags/1.0.8/views/heatmaps-views.php (added)
-
tags/1.0.8/views/sessions-views.php (added)
-
trunk/Opti-Behavior.php (modified) (3 diffs)
-
trunk/admin/class-opti-behavior-heatmap-ajax-handler.php (modified) (11 diffs)
-
trunk/admin/class-opti-behavior-heatmap-dashboard.php (modified) (8 diffs)
-
trunk/assets/css/dashboard.css (modified) (1 diff)
-
trunk/assets/css/dashboard_styles.css (modified) (1 diff)
-
trunk/assets/css/style.css (modified) (1 diff)
-
trunk/assets/js/dashboard-cleaned.js (modified) (2 diffs)
-
trunk/assets/js/dashboard-converted.js (modified) (2 diffs)
-
trunk/assets/js/dashboard-final.js (modified) (2 diffs)
-
trunk/assets/js/dashboard.js (modified) (21 diffs)
-
trunk/assets/js/opti-behavior-heatmap-simple.js (modified) (4 diffs)
-
trunk/includes/class-opti-behavior-heatmap-file-storage.php (modified) (1 diff)
-
trunk/includes/class-opti-behavior-license-manager.php (modified) (14 diffs)
-
trunk/includes/trait-opti-behavior-ajax-handlers.php (modified) (4 diffs)
-
trunk/includes/trait-opti-behavior-dashboard-views.php (modified) (15 diffs)
-
trunk/includes/trait-opti-behavior-data-helpers.php (modified) (3 diffs)
-
trunk/includes/trait-opti-behavior-sessions-views.php (modified) (1 diff)
-
trunk/includes/trait-opti-behavior-settings-views.php (modified) (12 diffs)
-
trunk/includes/trait-opti-behavior-settings-views.php.backup (deleted)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/views/dashboard-views.php (modified) (8 diffs)
-
trunk/views/sessions-views.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
opti-behavior/trunk/Opti-Behavior.php
r3408429 r3414697 4 4 * Plugin URI: https://optiuser.com/ 5 5 * Description: Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps and real-time insights. Boost conversions and optimize user experience with data-driven decisions. 6 * Version: 1.0. 76 * Version: 1.0.8 7 7 * Author: OptiUser 8 8 * Author URI: https://optiuser.com/ … … 80 80 81 81 // Define plugin constants. 82 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0. 7' );82 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0.8' ); 83 83 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 84 84 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 87 87 define( 'OPTI_BEHAVIOR_HEATMAP_PUBLIC_DIR', OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR . 'public/' ); 88 88 define( 'OPTI_BEHAVIOR_HEATMAP_ASSETS_URL', OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL . 'assets/' ); 89 90 /** 91 * API Environment Configuration 92 * Set the environment for API connections: 93 * - 'local' => Uses http://localhost/API/ for local development 94 * - 'live' => Uses https://api.optiuser.com/ for production (default) 95 * 96 * Change this value to switch between local and live API servers. 97 */ 98 if ( ! defined( 'OPTI_BEHAVIOR_ENVIRONMENT' ) ) { 99 define( 'OPTI_BEHAVIOR_ENVIRONMENT', 'live' ); // Options: 'local' or 'live' 100 } 89 101 90 102 if ( ! function_exists( 'opti_behavior_heatmap_uninstall' ) ) { -
opti-behavior/trunk/admin/class-opti-behavior-heatmap-ajax-handler.php
r3408429 r3414697 88 88 // Support form email handler 89 89 add_action( 'wp_ajax_opti_behavior_send_support_email', array( $this, 'ajax_send_support_email' ) ); 90 91 // Country data cleanup handler 92 add_action( 'wp_ajax_opti_behavior_reset_country_data', array( $this, 'ajax_reset_country_data' ) ); 90 93 } 91 94 … … 105 108 $debug_manager->log( 'AJAX request failed: invalid nonce', 'warning', 'ajax' ); 106 109 wp_send_json_error( __( 'Invalid nonce', 'opti-behavior' ) ); 110 } 111 112 // Don't track admin users (unless in test mode) 113 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET/COOKIE check for test mode (permission check follows) 114 $is_test_mode = isset( $_GET['opti_behavior_test_tracking'] ) || isset( $_COOKIE['opti_behavior_test_tracking'] ) || isset( $_GET['opti_behavior_test_recording'] ) || isset( $_COOKIE['opti_behavior_test_recording'] ); 115 // phpcs:enable WordPress.Security.NonceVerification.Recommended 116 if ( current_user_can( 'manage_options' ) && ! $is_test_mode ) { 117 $debug_manager->log( 'Session record skipped: Admin user (not in test mode)', 'info', 'ajax' ); 118 wp_send_json_success( array( 'message' => 'Admin tracking disabled' ) ); 119 return; 107 120 } 108 121 … … 816 829 $debug_manager->log( 'Session record failed: Invalid nonce', 'warning', 'ajax' ); 817 830 wp_send_json_error( __( 'Invalid nonce', 'opti-behavior' ) ); 831 } 832 833 // Don't track admin users (unless in test mode) 834 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET/COOKIE check for test mode (permission check follows) 835 $is_test_mode = isset( $_GET['opti_behavior_test_tracking'] ) || isset( $_COOKIE['opti_behavior_test_tracking'] ) || isset( $_GET['opti_behavior_test_recording'] ) || isset( $_COOKIE['opti_behavior_test_recording'] ); 836 // phpcs:enable WordPress.Security.NonceVerification.Recommended 837 if ( current_user_can( 'manage_options' ) && ! $is_test_mode ) { 838 $debug_manager->log( 'Session record skipped: Admin user (not in test mode)', 'info', 'ajax' ); 839 wp_send_json_success( array( 'message' => 'Admin tracking disabled' ) ); 840 return; 818 841 } 819 842 … … 1205 1228 } 1206 1229 1207 // For local/private IPs, use browser language as fallback1230 // For local/private IPs, return unknown (don't use browser language as it's unreliable) 1208 1231 if ( $this->is_private_ip( $ip ) ) { 1209 return $this->get_country_from_browser_language(); 1232 return array( 1233 'country' => null, 1234 'country_name' => null, 1235 'region' => null, 1236 'city' => null, 1237 'timezone' => '', 1238 'source' => 'local', 1239 ); 1210 1240 } 1211 1241 … … 1231 1261 1232 1262 if ( is_wp_error( $response ) ) { 1233 // API request failed, try browser language fallback 1234 $browser_geo = $this->get_country_from_browser_language(); 1235 if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) { 1236 $result = array( 1237 'country' => $browser_geo['country'], 1238 'country_name' => $browser_geo['country_name'], 1239 'region' => 'Unknown', 1240 'city' => 'Unknown', 1241 'timezone' => '', 1242 'source' => 'browser_language_fallback', 1243 ); 1244 // Cache for shorter time on fallback (15 minutes) 1245 set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS ); 1246 return $result; 1247 } 1248 1249 // Last resort: return unknown 1263 // API request failed, return null to avoid storing incorrect data 1250 1264 $result = array( 1251 'country' => 'UN',1252 'country_name' => 'Unknown',1253 'region' => 'Unknown',1254 'city' => 'Unknown',1265 'country' => null, 1266 'country_name' => null, 1267 'region' => null, 1268 'city' => null, 1255 1269 'timezone' => '', 1256 1270 'source' => 'error', 1257 1271 ); 1258 // Cache for shorter time on error (5 minutes) 1272 // Cache for shorter time on error (5 minutes) to allow retry 1259 1273 set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS ); 1260 1274 return $result; … … 1267 1281 if ( isset( $data['status'] ) && $data['status'] === 'success' ) { 1268 1282 $result = array( 1269 'country' => isset( $data['countryCode'] ) ? strtoupper( sanitize_text_field( $data['countryCode'] ) ) : 'UN',1270 'country_name' => isset( $data['country'] ) ? sanitize_text_field( $data['country'] ) : 'Unknown',1271 'region' => isset( $data['regionName'] ) ? sanitize_text_field( $data['regionName'] ) : 'Unknown',1272 'city' => isset( $data['city'] ) ? sanitize_text_field( $data['city'] ) : 'Unknown',1283 'country' => isset( $data['countryCode'] ) ? strtoupper( sanitize_text_field( $data['countryCode'] ) ) : null, 1284 'country_name' => isset( $data['country'] ) ? sanitize_text_field( $data['country'] ) : null, 1285 'region' => isset( $data['regionName'] ) ? sanitize_text_field( $data['regionName'] ) : null, 1286 'city' => isset( $data['city'] ) ? sanitize_text_field( $data['city'] ) : null, 1273 1287 'timezone' => isset( $data['timezone'] ) ? sanitize_text_field( $data['timezone'] ) : '', 1274 1288 'source' => 'ip-api', … … 1279 1293 } 1280 1294 1281 // API returned error or invalid data, try browser language fallback 1282 $browser_geo = $this->get_country_from_browser_language(); 1283 if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) { 1284 $result = array( 1285 'country' => $browser_geo['country'], 1286 'country_name' => $browser_geo['country_name'], 1287 'region' => 'Unknown', 1288 'city' => 'Unknown', 1289 'timezone' => '', 1290 'source' => 'browser_language_fallback', 1291 ); 1292 // Cache for shorter time on fallback (15 minutes) 1293 set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS ); 1294 return $result; 1295 } 1296 1297 // Last resort: return unknown 1295 // API returned error or invalid data, return null to avoid storing incorrect data 1298 1296 $result = array( 1299 'country' => 'UN',1300 'country_name' => 'Unknown',1301 'region' => 'Unknown',1302 'city' => 'Unknown',1297 'country' => null, 1298 'country_name' => null, 1299 'region' => null, 1300 'city' => null, 1303 1301 'timezone' => '', 1304 1302 'source' => 'failed', 1305 1303 ); 1306 // Cache for shorter time on failure (5 minutes) 1304 // Cache for shorter time on failure (5 minutes) to allow retry 1307 1305 set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS ); 1308 1306 return $result; … … 1623 1621 $city = ! empty( $geo_data['city'] ) ? $geo_data['city'] : null; 1624 1622 $timezone = ! empty( $geo_data['timezone'] ) ? $geo_data['timezone'] : $timezone; 1625 } elseif ( $geo_data['source'] === 'local' ) {1626 // For local/private IPs, try to detect from browser Accept-Language header1627 $detected = $this->detect_country_from_browser();1628 if ( ! empty( $detected['country'] ) && $detected['country'] !== 'UN' ) {1629 $country = $detected['country'];1630 $country_name = $detected['country_name'];1631 $region = $detected['region'];1632 $city = $detected['city'];1633 }1634 1623 } 1624 // Note: For local/private IPs, we no longer attempt browser language detection as it's unreliable 1635 1625 } 1636 1626 … … 2412 2402 } 2413 2403 2404 // Don't track admin users (unless in test mode) 2405 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- GET/COOKIE check for test mode (permission check follows) 2406 $is_test_mode = isset( $_GET['opti_behavior_test_tracking'] ) || isset( $_COOKIE['opti_behavior_test_tracking'] ) || isset( $_GET['opti_behavior_test_recording'] ) || isset( $_COOKIE['opti_behavior_test_recording'] ); 2407 // phpcs:enable WordPress.Security.NonceVerification.Recommended 2408 if ( current_user_can( 'manage_options' ) && ! $is_test_mode ) { 2409 $debug_manager->log( 'Session record skipped: Admin user (not in test mode)', 'info', 'ajax' ); 2410 wp_send_json_success( array( 'message' => 'Admin tracking disabled' ) ); 2411 return; 2412 } 2413 2414 2414 global $wpdb; 2415 2415 … … 2420 2420 $duration = isset( $_POST['duration'] ) ? intval( $_POST['duration'] ) : 0; 2421 2421 $scroll_depth = isset( $_POST['scroll_depth'] ) ? intval( $_POST['scroll_depth'] ) : 0; 2422 2423 // VALIDATION: Cap session duration at 1 hour (3600 seconds) to prevent inflated averages 2424 // This protects against sessions left open for hours/days in background tabs 2425 $max_duration = 3600; // 1 hour in seconds 2426 if ( $duration > $max_duration ) { 2427 $debug_manager->log( "Duration capped from {$duration}s to {$max_duration}s for session: {$session_id}", 'warning', 'ajax' ); 2428 $duration = $max_duration; 2429 } 2422 2430 2423 2431 if ( empty( $session_id ) || empty( $visitor_id ) ) { … … 2489 2497 } 2490 2498 2499 /** 2500 * AJAX handler to reset country data for all visitors. 2501 * 2502 * This clears country data that was incorrectly detected from browser language settings. 2503 * After running this, visitors will have their country re-detected on their next visit. 2504 * 2505 * @since 1.0.7 2506 */ 2507 public function ajax_reset_country_data() { 2508 // Security checks 2509 check_ajax_referer( 'opti_behavior_dashboard_nonce', 'nonce' ); 2510 2511 if ( ! current_user_can( 'manage_options' ) ) { 2512 wp_send_json_error( array( 2513 'message' => __( 'You do not have permission to perform this action.', 'opti-behavior' ), 2514 ) ); 2515 } 2516 2517 global $wpdb; 2518 $visitors_table = esc_sql( $wpdb->prefix . 'optibehavior_visitors' ); 2519 2520 // Reset country data to NULL for all visitors 2521 // This allows the system to re-detect countries on next visit 2522 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name from $wpdb->prefix with hardcoded suffix, properly escaped with esc_sql() 2523 $result = $wpdb->query( 2524 "UPDATE {$visitors_table} SET country = NULL, country_name = NULL WHERE country IS NOT NULL" 2525 ); 2526 2527 if ( $result === false ) { 2528 wp_send_json_error( array( 2529 'message' => __( 'Failed to reset country data. Please try again.', 'opti-behavior' ), 2530 ) ); 2531 } 2532 2533 // Clear the geo-location cache 2534 $wpdb->query( 2535 $wpdb->prepare( 2536 "DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s OR option_name LIKE %s", 2537 $wpdb->esc_like( '_transient_opti_geo_' ) . '%', 2538 $wpdb->esc_like( '_transient_timeout_opti_geo_' ) . '%' 2539 ) 2540 ); 2541 2542 // Clear the top users cache 2543 $wpdb->query( 2544 $wpdb->prepare( 2545 "DELETE FROM {$wpdb->prefix}options WHERE option_name LIKE %s OR option_name LIKE %s", 2546 $wpdb->esc_like( '_transient_optibehavior_top_users_' ) . '%', 2547 $wpdb->esc_like( '_transient_timeout_optibehavior_top_users_' ) . '%' 2548 ) 2549 ); 2550 2551 $affected_rows = $result !== false ? $result : 0; 2552 2553 wp_send_json_success( array( 2554 'message' => sprintf( 2555 /* translators: %d: number of visitor records updated */ 2556 __( 'Successfully reset country data for %d visitors. Countries will be re-detected on their next visit.', 'opti-behavior' ), 2557 $affected_rows 2558 ), 2559 'affected_rows' => $affected_rows, 2560 ) ); 2561 } 2562 2491 2563 } -
opti-behavior/trunk/admin/class-opti-behavior-heatmap-dashboard.php
r3408429 r3414697 321 321 // Determine initial filter from querystring 322 322 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- GET parameters for filtering dashboard view (read-only operation) 323 $period = isset($_GET['period']) ? sanitize_text_field( wp_unslash( $_GET['period'] ) ) : 'last 7days';323 $period = isset($_GET['period']) ? sanitize_text_field( wp_unslash( $_GET['period'] ) ) : 'last30days'; 324 324 $start_q = isset($_GET['start_date']) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : null; 325 325 $end_q = isset($_GET['end_date']) ? sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) : null; … … 478 478 <div class="visitor-item grid"> 479 479 <span class="visitor-datetime"><?php echo esc_html($visitor['visited_at'] ?? ''); ?><?php if(!empty($visitor['time_ago'])): ?> - <span class="ago"><?php echo esc_html($visitor['time_ago']); ?></span><?php endif; ?></span> 480 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo esc_html($visitor['flag']);?></span> <span class="visitor-location"><?php echo esc_html($visitor['country']); ?></span></span>480 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo $visitor['flag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> <span class="visitor-location"><?php echo esc_html($visitor['country']); ?></span></span> 481 481 <span class="visitor-pageblock"><span class="visitor-title"><?php echo esc_html($visitor['page_title'] ?? ''); ?></span><a class="visitor-url" href="<?php echo esc_url($visitor['current_url'] ?? ''); ?>" target="_blank" rel="noopener"><?php echo esc_html($visitor['current_url'] ?? ''); ?></a></span> 482 482 <span class="visitor-ip-col"><span class="visitor-ip-pill"><?php $ip_raw = isset($visitor['ip']) ? (string)$visitor['ip'] : ''; $ip_raw = trim($ip_raw); if ($ip_raw === '') { echo '-'; } else if (strpos($ip_raw, ':') !== false) { $start = substr($ip_raw, 0, 7); $end = substr($ip_raw, -7); echo esc_html($start . '…' . $end); } else { echo esc_html($ip_raw); } ?></span></span> … … 662 662 </div> 663 663 <div class="visitor-flag"> 664 <span class="flag-icon"><?php echo esc_html($session['flag']);?></span>664 <span class="flag-icon"><?php echo $session['flag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> 665 665 </div> 666 666 </div> … … 800 800 $order = isset($args['order']) ? $args['order'] : 'desc'; 801 801 $page = isset($args['page']) ? $args['page'] : 1; 802 $period = isset($args['period']) ? $args['period'] : 'last 7days';802 $period = isset($args['period']) ? $args['period'] : 'last30days'; 803 803 $start = isset($args['start']) ? $args['start'] : ''; 804 804 $end = isset($args['end']) ? $args['end'] : ''; … … 1316 1316 ORDER BY last_event_time DESC"; 1317 1317 1318 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query with no user input 1318 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query with no user input, no dynamic parameters 1319 1319 $results = $wpdb->get_results( $query ); 1320 1320 } … … 3084 3084 3085 3085 /** 3086 * Extract main domain from a hostname (removes subdomains) 3087 * Examples: api.pfbaza.website -> pfbaza.website, www.example.com -> example.com 3088 * 3089 * @param string $hostname The hostname to extract from. 3090 * @return string The main domain. 3091 */ 3092 private function extract_main_domain( $hostname ) { 3093 if ( empty( $hostname ) ) { 3094 return ''; 3095 } 3096 3097 // Remove 'www.' prefix if present 3098 $hostname = preg_replace( '/^www\./i', '', $hostname ); 3099 3100 // Split by dots 3101 $parts = explode( '.', $hostname ); 3102 $count = count( $parts ); 3103 3104 // If only 2 parts (e.g., example.com), return as-is 3105 if ( $count <= 2 ) { 3106 return $hostname; 3107 } 3108 3109 // Known two-part TLDs (e.g., .co.uk, .com.au) 3110 $two_part_tlds = array( 'co.uk', 'com.au', 'co.za', 'co.nz', 'com.br', 'co.in', 'co.jp' ); 3111 $last_two = $parts[ $count - 2 ] . '.' . $parts[ $count - 1 ]; 3112 3113 if ( in_array( $last_two, $two_part_tlds, true ) ) { 3114 // Return domain.co.uk format (3 parts) 3115 if ( $count >= 3 ) { 3116 return $parts[ $count - 3 ] . '.' . $parts[ $count - 2 ] . '.' . $parts[ $count - 1 ]; 3117 } 3118 return $hostname; 3119 } 3120 3121 // Standard TLD: return last 2 parts (domain.tld) 3122 return $parts[ $count - 2 ] . '.' . $parts[ $count - 1 ]; 3123 } 3124 3125 /** 3086 3126 * Get top referrers data 3087 3127 * … … 3176 3216 $out = array(); 3177 3217 foreach ( array_slice(array_keys($agg), 0, 10) as $k ) { 3178 $out[] = array( 'referrer' => $k, 'count' => intval($agg[$k]) ); 3218 // Extract domain for favicon 3219 $domain = ''; 3220 $favicon_url = ''; 3221 3222 // Try to extract domain from referrer label 3223 if ( $k !== 'Direct / None' && $k !== 'Paid Ads' && $k !== 'Paid Search' && $k !== 'Email' && $k !== 'Social' ) { 3224 // Check if it's a known service (e.g., "Google", "Facebook") 3225 $known_domains = array( 3226 'Google' => 'google.com', 3227 'Bing' => 'bing.com', 3228 'DuckDuckGo' => 'duckduckgo.com', 3229 'Yahoo' => 'yahoo.com', 3230 'Yandex' => 'yandex.com', 3231 'Baidu' => 'baidu.com', 3232 'Naver' => 'naver.com', 3233 'Ecosia' => 'ecosia.org', 3234 'Startpage' => 'startpage.com', 3235 'Qwant' => 'qwant.com', 3236 'Facebook' => 'facebook.com', 3237 'Instagram' => 'instagram.com', 3238 'Twitter/X' => 'twitter.com', 3239 'LinkedIn' => 'linkedin.com', 3240 'Pinterest' => 'pinterest.com', 3241 'Reddit' => 'reddit.com', 3242 'TikTok' => 'tiktok.com', 3243 'Snapchat' => 'snapchat.com', 3244 'YouTube' => 'youtube.com', 3245 'Google Ads' => 'google.com', 3246 'Microsoft Ads' => 'bing.com', 3247 'Facebook Ads' => 'facebook.com', 3248 'Twitter Ads' => 'twitter.com', 3249 'TikTok Ads' => 'tiktok.com', 3250 ); 3251 3252 if ( isset( $known_domains[ $k ] ) ) { 3253 $domain = $known_domains[ $k ]; 3254 } else { 3255 // It's a domain or subdomain (e.g., "api.pfbaza.website") 3256 // Extract main domain for favicon (api.pfbaza.website -> pfbaza.website) 3257 $domain = $this->extract_main_domain( $k ); 3258 } 3259 3260 // Build favicon URL using the main domain 3261 if ( $domain ) { 3262 $favicon_url = 'https://' . $domain . '/favicon.ico'; 3263 } 3264 } 3265 3266 $out[] = array( 3267 'referrer' => $k, 3268 'count' => intval($agg[$k]), 3269 'domain' => $domain, 3270 'favicon_url' => $favicon_url 3271 ); 3179 3272 } 3180 3273 return $out; … … 3487 3580 private function get_overview_analytics( $date_range ) { 3488 3581 return array( 3489 'summary' => $this->get_dashboard_data( 'last 7days' ),3582 'summary' => $this->get_dashboard_data( 'last30days' ), 3490 3583 'trends' => array(), 3491 3584 ); -
opti-behavior/trunk/assets/css/dashboard.css
r3399182 r3414697 317 317 height: 1rem; 318 318 border-radius: 2px; 319 } 320 321 .visitor-flag-icon { 322 width: 1rem; 323 height: 0.75rem; 324 object-fit: cover; 325 border-radius: 2px; 326 margin-right: 0.5rem; 327 display: inline-block; 328 vertical-align: middle; 319 329 } 320 330 -
opti-behavior/trunk/assets/css/dashboard_styles.css
r3408429 r3414697 198 198 .stat-change.neutral { 199 199 color: #6c757d; 200 } 201 202 /* Stat History Mini Chart */ 203 .stat-history { 204 margin-top: 12px; 205 padding-top: 12px; 206 border-top: 1px solid #e5e7eb; 207 } 208 209 .stat-history canvas { 210 max-height: 50px !important; 211 width: 100% !important; 212 display: block; 200 213 } 201 214 -
opti-behavior/trunk/assets/css/style.css
r3399182 r3414697 44 44 45 45 .optibehavior-heatmap-container .count-bar { 46 font-size: 1 2px;46 font-size: 18px; 47 47 position: absolute; 48 right: 0;49 min-width: 48px;48 left: 0; 49 min-width: 60px; 50 50 height: 40px; 51 51 text-align: center; 52 52 line-height: 40px; 53 background: #ef96; 53 background: #4a90e2; 54 color: #ffffff; 55 font-weight: 700; 56 border-radius: 6px; 57 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); 54 58 } 55 59 -
opti-behavior/trunk/assets/js/dashboard-cleaned.js
r3399182 r3414697 1513 1513 fd.append('end_date', eEl && eEl.value ? eEl.value : ''); 1514 1514 } 1515 function cc2flag(cc){ try{ var s=(cc||'').toString().trim().toUpperCase(); if(/^[A-Z]{2}$/.test(s)){ var base=127397; return String.fromCodePoint(s.charCodeAt(0)+base, s.charCodeAt(1)+base); } }catch(e){} return '🌐'; } 1515 // Function to get flag image HTML for a country code 1516 function getFlagHTML(cc, countryName) { 1517 try { 1518 var code = (cc||'').toString().trim().toUpperCase(); 1519 if(/^[A-Z]{2}$/.test(code)) { 1520 // Use flagcdn.com for high-quality flag images 1521 return '<img src="https://flagcdn.com/w20/' + code.toLowerCase() + '.png" ' + 1522 'srcset="https://flagcdn.com/w40/' + code.toLowerCase() + '.png 2x" ' + 1523 'alt="' + (countryName || code) + '" ' + 1524 'class="tu-flag-img" ' + 1525 'style="width:20px;height:15px;margin-right:6px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);" />'; 1526 } 1527 } catch(e) {} 1528 // Fallback to globe emoji for unknown countries 1529 return '<span style="margin-right:6px;">🌐</span>'; 1530 } 1516 1531 1517 1532 fetch('" . esc_url(admin_url('admin-ajax.php')) . "', {method:'POST', credentials:'same-origin', body: fd}) … … 1545 1560 '<td class=\"tu-num\"><span class=\"tu-badge\">'+ (it.sessions||0) +'</span></td>'+ 1546 1561 '<td class=\"tu-num\">'+ (it.pages_per_session||0).toFixed(2) +'</td>'+ 1547 '<td> <span class=\"tu-flag\">'+ cc2flag(it.country_code) +'</span><span class=\"tu-country-name\">'+ esc(it.country_name||'') +'</span></td>'+1562 '<td>'+ getFlagHTML(it.country_code, it.country_name) +'<span class=\"tu-country-name\">'+ esc(it.country_name||'') +'</span></td>'+ 1548 1563 '<td class=\"tu-last\">'+ esc(it.last_seen_human||'') +'</td>'+ 1549 1564 '</tr>'; -
opti-behavior/trunk/assets/js/dashboard-converted.js
r3399182 r3414697 1514 1514 fd.append('end_date', eEl && eEl.value ? eEl.value : ''); 1515 1515 } 1516 function cc2flag(cc){ try{ var s=(cc||'').toString().trim().toUpperCase(); if(/^[A-Z]{2}$/.test(s)){ var base=127397; return String.fromCodePoint(s.charCodeAt(0)+base, s.charCodeAt(1)+base); } }catch(e){} return '🌐'; } 1516 // Function to get flag image HTML for a country code 1517 function getFlagHTML(cc, countryName) { 1518 try { 1519 var code = (cc||'').toString().trim().toUpperCase(); 1520 if(/^[A-Z]{2}$/.test(code)) { 1521 // Use flagcdn.com for high-quality flag images 1522 return '<img src="https://flagcdn.com/w20/' + code.toLowerCase() + '.png" ' + 1523 'srcset="https://flagcdn.com/w40/' + code.toLowerCase() + '.png 2x" ' + 1524 'alt="' + (countryName || code) + '" ' + 1525 'class="tu-flag-img" ' + 1526 'style="width:20px;height:15px;margin-right:6px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);" />'; 1527 } 1528 } catch(e) {} 1529 // Fallback to globe emoji for unknown countries 1530 return '<span style="margin-right:6px;">🌐</span>'; 1531 } 1517 1532 1518 1533 fetch('ajaxUrl', {method:'POST', credentials:'same-origin', body: fd}) … … 1546 1561 '<td class="tu-num"><span class="tu-badge">'+ (it.sessions||0) +'</span></td>'+ 1547 1562 '<td class="tu-num">'+ (it.pages_per_session||0).toFixed(2) +'</td>'+ 1548 '<td> <span class="tu-flag">'+ cc2flag(it.country_code) +'</span><span class="tu-country-name">'+ esc(it.country_name||'') +'</span></td>'+1563 '<td>'+ getFlagHTML(it.country_code, it.country_name) +'<span class="tu-country-name">'+ esc(it.country_name||'') +'</span></td>'+ 1549 1564 '<td class="tu-last">'+ esc(it.last_seen_human||'') +'</td>'+ 1550 1565 '</tr>'; -
opti-behavior/trunk/assets/js/dashboard-final.js
r3399182 r3414697 1511 1511 fd.append('end_date', eEl && eEl.value ? eEl.value : ''); 1512 1512 } 1513 function cc2flag(cc){ try{ var s=(cc||'').toString().trim().toUpperCase(); if(/^[A-Z]{2}$/.test(s)){ var base=127397; return String.fromCodePoint(s.charCodeAt(0)+base, s.charCodeAt(1)+base); } }catch(e){} return '🌐'; } 1513 // Function to get flag image HTML for a country code 1514 function getFlagHTML(cc, countryName) { 1515 try { 1516 var code = (cc||'').toString().trim().toUpperCase(); 1517 if(/^[A-Z]{2}$/.test(code)) { 1518 // Use flagcdn.com for high-quality flag images 1519 return '<img src="https://flagcdn.com/w20/' + code.toLowerCase() + '.png" ' + 1520 'srcset="https://flagcdn.com/w40/' + code.toLowerCase() + '.png 2x" ' + 1521 'alt="' + (countryName || code) + '" ' + 1522 'class="tu-flag-img" ' + 1523 'style="width:20px;height:15px;margin-right:6px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);" />'; 1524 } 1525 } catch(e) {} 1526 // Fallback to globe emoji for unknown countries 1527 return '<span style="margin-right:6px;">🌐</span>'; 1528 } 1514 1529 1515 1530 fetch('" . esc_url(admin_url('admin-ajax.php')) . "', {method:'POST', credentials:'same-origin', body: fd}) … … 1543 1558 '<td class=\"tu-num\"><span class=\"tu-badge\">'+ (it.sessions||0) +'</span></td>'+ 1544 1559 '<td class=\"tu-num\">'+ (it.pages_per_session||0).toFixed(2) +'</td>'+ 1545 '<td> <span class=\"tu-flag\">'+ cc2flag(it.country_code) +'</span><span class=\"tu-country-name\">'+ esc(it.country_name||'') +'</span></td>'+1560 '<td>'+ getFlagHTML(it.country_code, it.country_name) +'<span class=\"tu-country-name\">'+ esc(it.country_name||'') +'</span></td>'+ 1546 1561 '<td class=\"tu-last\">'+ esc(it.last_seen_human||'') +'</td>'+ 1547 1562 '</tr>'; -
opti-behavior/trunk/assets/js/dashboard.js
r3408429 r3414697 50 50 const startEl = document.getElementById('start-date'); 51 51 const endEl = document.getElementById('end-date'); 52 const period = periodEl ? (periodEl.value || 'last 7days') : 'last7days';52 const period = periodEl ? (periodEl.value || 'last30days') : 'last30days'; 53 53 const startDate = startEl ? startEl.value : ''; 54 54 const endDate = endEl ? endEl.value : ''; … … 667 667 var rl = charts.referrers.map(function(i){return i.referrer||'Direct / None'}); 668 668 var rd = charts.referrers.map(function(i){return i.count||0}); 669 670 // Store favicon URLs for later use 671 window.referrersFaviconData = charts.referrers.map(function(i) { 672 return { 673 referrer: i.referrer || 'Direct / None', 674 domain: i.domain || '', 675 favicon_url: i.favicon_url || '' 676 }; 677 }); 678 669 679 new Chart(rEl, { 670 680 type: 'bar', … … 676 686 maintainAspectRatio: false, 677 687 layout: { padding: { top: 8, right: 64, bottom: 8, left: 12 } }, 678 plugins: { legend: { display: false } }, 688 plugins: { 689 legend: { display: false }, 690 tooltip: { 691 callbacks: { 692 label: function(context) { 693 return context.dataset.label + ': ' + context.parsed.x; 694 } 695 } 696 } 697 }, 679 698 elements: { bar: { borderRadius: 6, borderSkipped: false } }, 680 699 scales: { … … 685 704 y: { 686 705 687 ticks: { color: '#6b7280', font: { size: 12 } }, 706 ticks: { 707 color: '#6b7280', 708 font: { size: 12 }, 709 padding: 8, 710 callback: function(value, index, ticks) { 711 // Return label with extra padding for favicon 712 return ' ' + this.getLabelForValue(value); 713 } 714 }, 688 715 grid: { display: false } 689 716 } … … 691 718 } 692 719 }); 720 721 // Add favicons to the chart after rendering 722 setTimeout(function() { 723 var canvas = rEl; 724 var chartContainer = canvas.parentElement; 725 726 // Remove any existing favicon overlay 727 var existingOverlay = chartContainer.querySelector('.referrer-favicon-overlay'); 728 if (existingOverlay) existingOverlay.parentNode.removeChild(existingOverlay); 729 730 // Create overlay container 731 var overlay = document.createElement('div'); 732 overlay.className = 'referrer-favicon-overlay'; 733 overlay.style.cssText = 'position: absolute; top: 0; left: 0; pointer-events: none;'; 734 735 // Get chart instance to calculate positions 736 var chart = Chart.getChart(rEl); 737 if (chart && window.referrersFaviconData) { 738 var scale = chart.scales.y; 739 var faviconData = window.referrersFaviconData; 740 741 faviconData.forEach(function(item, index) { 742 if (item.favicon_url) { 743 var y = scale.getPixelForValue(index); 744 745 // Create favicon image with fallback 746 var img = document.createElement('img'); 747 img.src = item.favicon_url; 748 img.style.cssText = 'position: absolute; left: 12px; top: ' + (y - 8) + 'px; width: 16px; height: 16px; border-radius: 2px;'; 749 img.onerror = function() { 750 // Fallback to default icon 751 this.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHJ4PSIyIiBmaWxsPSIjRTVFN0VCIi8+CiAgPHBhdGggZD0iTTggM0M1LjIzODU4IDMgMyA1LjIzODU4IDMgOEMzIDEwLjc2MTQgNS4yMzg1OCAxMyA4IDEzQzEwLjc2MTQgMTMgMTMgMTAuNzYxNCAxMyA4QzEzIDUuMjM4NTggMTAuNzYxNCAzIDggM1pNOCA0LjVDOC41NTIyOCA0LjUgOSA0Ljk0NzcyIDkgNS41QzkgNi4wNTIyOCA4LjU1MjI4IDYuNSA4IDYuNUM3LjQ0NzcyIDYuNSA3IDYuMDUyMjggNyA1LjVDNyA0Ljk0NzcyIDcuNDQ3NzIgNC41IDggNC41Wk05LjUgMTAuNUg2LjVWOUg3LjVWOEg4LjVWMTBIOC41VjkuNUg5LjVWMTAuNVoiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+'; 752 }; 753 754 overlay.appendChild(img); 755 } 756 }); 757 758 chartContainer.style.position = 'relative'; 759 chartContainer.appendChild(overlay); 760 } 761 }, 100); 693 762 } 694 763 … … 926 995 if (!hasAnyData){ 927 996 var ajaxUrl = (window.opti_behaviorData && (window.opti_behaviorData.ajaxUrl||window.opti_behaviorData.ajaxurl)) || (window.ajaxurl) || '/wp-admin/admin-ajax.php'; 928 var per = document.getElementById('dashboard-period') ? (document.getElementById('dashboard-period').value||'last 7days') : 'last7days';997 var per = document.getElementById('dashboard-period') ? (document.getElementById('dashboard-period').value||'last30days') : 'last30days'; 929 998 var s = document.getElementById('start-date') ? (document.getElementById('start-date').value||'') : ''; 930 999 var e = document.getElementById('end-date') ? (document.getElementById('end-date').value||'') : ''; … … 1395 1464 const labels = items.map(i=>i.referrer||'Direct / None'); 1396 1465 const data = items.map(i=>i.count||0); 1466 1467 // Store favicon URLs for later use 1468 window.referrersFaviconData = items.map(i => ({ 1469 referrer: i.referrer || 'Direct / None', 1470 domain: i.domain || '', 1471 favicon_url: i.favicon_url || '' 1472 })); 1473 1397 1474 // Destroy existing chart if it exists 1398 1475 const existingReferrersChart = Chart.getChart(rEl); 1399 1476 if (existingReferrersChart) existingReferrersChart.destroy(); 1477 1400 1478 new Chart(rEl, { 1401 1479 plugins: (window.optibehaviorValueLabelsPlugin ? [window.optibehaviorValueLabelsPlugin] : []), … … 1407 1485 maintainAspectRatio: false, 1408 1486 layout: { padding: { top: 8, right: 64, bottom: 8, left: 12 } }, 1409 plugins: { legend: { display: false } }, 1487 plugins: { 1488 legend: { display: false }, 1489 tooltip: { 1490 callbacks: { 1491 label: function(context) { 1492 return context.dataset.label + ': ' + context.parsed.x; 1493 } 1494 } 1495 } 1496 }, 1410 1497 elements: { bar: { borderRadius: 6, borderSkipped: false } }, 1411 1498 scales: { … … 1415 1502 }, 1416 1503 y: { 1417 1418 ticks: { color: '#6b7280', font: { size: 12 } }, 1504 ticks: { 1505 color: '#6b7280', 1506 font: { size: 12 }, 1507 padding: 8, 1508 callback: function(value, index, ticks) { 1509 // Return label with extra padding for favicon 1510 return ' ' + this.getLabelForValue(value); 1511 } 1512 }, 1419 1513 grid: { display: false } 1420 1514 } … … 1422 1516 } 1423 1517 }); 1518 1519 // Add favicons to the chart after rendering 1520 setTimeout(() => { 1521 const canvas = rEl; 1522 const chartContainer = canvas.parentElement; 1523 1524 // Remove any existing favicon overlay 1525 const existingOverlay = chartContainer.querySelector('.referrer-favicon-overlay'); 1526 if (existingOverlay) existingOverlay.remove(); 1527 1528 // Create overlay container 1529 const overlay = document.createElement('div'); 1530 overlay.className = 'referrer-favicon-overlay'; 1531 overlay.style.cssText = 'position: absolute; top: 0; left: 0; pointer-events: none;'; 1532 1533 // Get chart instance to calculate positions 1534 const chart = Chart.getChart(rEl); 1535 if (chart && window.referrersFaviconData) { 1536 const scale = chart.scales.y; 1537 const faviconData = window.referrersFaviconData; 1538 1539 faviconData.forEach((item, index) => { 1540 if (item.favicon_url) { 1541 const y = scale.getPixelForValue(index); 1542 1543 // Create favicon image with fallback 1544 const img = document.createElement('img'); 1545 img.src = item.favicon_url; 1546 img.style.cssText = `position: absolute; left: 12px; top: ${y - 8}px; width: 16px; height: 16px; border-radius: 2px;`; 1547 img.onerror = function() { 1548 // Fallback to default icon 1549 this.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHJ4PSIyIiBmaWxsPSIjRTVFN0VCIi8+CiAgPHBhdGggZD0iTTggM0M1LjIzODU4IDMgMyA1LjIzODU4IDMgOEMzIDEwLjc2MTQgNS4yMzg1OCAxMyA4IDEzQzEwLjc2MTQgMTMgMTMgMTAuNzYxNCAxMyA4QzEzIDUuMjM4NTggMTAuNzYxNCAzIDggM1pNOCA0LjVDOC41NTIyOCA0LjUgOSA0Ljk0NzcyIDkgNS41QzkgNi4wNTIyOCA4LjU1MjI4IDYuNSA4IDYuNUM3LjQ0NzcyIDYuNSA3IDYuMDUyMjggNyA1LjVDNyA0Ljk0NzcyIDcuNDQ3NzIgNC41IDggNC41Wk05LjUgMTAuNUg2LjVWOUg3LjVWOEg4LjVWMTBIOC41VjkuNUg5LjVWMTAuNVoiIGZpbGw9IiM5Q0EzQUYiLz4KPC9zdmc+'; 1550 }; 1551 1552 overlay.appendChild(img); 1553 } 1554 }); 1555 1556 chartContainer.style.position = 'relative'; 1557 chartContainer.appendChild(overlay); 1558 } 1559 }, 100); 1424 1560 } 1425 1561 … … 2072 2208 if (optibehaviorRefreshing) return; 2073 2209 optibehaviorRefreshing = true; 2074 const period = document.getElementById('dashboard-period')?.value || 'last 7days';2210 const period = document.getElementById('dashboard-period')?.value || 'last30days'; 2075 2211 fetch(ajaxurl, { 2076 2212 method: 'POST', … … 3167 3303 const rows = tbody.querySelectorAll('tr'); 3168 3304 rows.forEach(function(row) { 3169 const labelNode = row.querySelector('.referrer-name'); 3170 if (!labelNode) { 3171 return; 3172 } 3173 3174 const label = labelNode.textContent || ''; 3175 const faviconUrl = getFaviconUrlFromReferrer(label); 3176 if (!faviconUrl) { 3305 // Get favicon URL from data attribute (provided by PHP) 3306 const faviconUrl = row.getAttribute('data-favicon-url'); 3307 const domain = row.getAttribute('data-domain'); 3308 3309 if (!faviconUrl || !domain) { 3310 // No favicon URL provided by PHP, skip this row 3177 3311 return; 3178 3312 } … … 3183 3317 } 3184 3318 3319 // Save the original SVG for fallback 3320 const originalSVG = iconContainer.innerHTML; 3321 3322 // Create img element and add to DOM immediately 3185 3323 const img = document.createElement('img'); 3186 3324 img.className = 'ob-referrer-favicon'; 3187 img.src = faviconUrl;3188 3325 img.alt = ''; 3189 3326 img.loading = 'lazy'; 3327 img.style.cssText = 'width: 20px; height: 20px; border-radius: 3px; object-fit: contain;'; 3328 3329 // Fallback chain with multiple sources 3330 let fallbackIndex = 0; 3331 const fallbackUrls = [ 3332 'https://www.google.com/s2/favicons?sz=32&domain=' + domain, // Google favicon service (most reliable) 3333 faviconUrl, // Primary: domain.com/favicon.ico 3334 'https://icons.duckduckgo.com/ip3/' + domain + '.ico' // DuckDuckGo favicon service 3335 ]; 3336 3337 function tryNextFallback() { 3338 if (fallbackIndex < fallbackUrls.length) { 3339 img.src = fallbackUrls[fallbackIndex]; 3340 fallbackIndex++; 3341 } else { 3342 // All fallbacks failed, restore the SVG icon 3343 iconContainer.innerHTML = originalSVG; 3344 } 3345 } 3346 3190 3347 img.addEventListener('error', function() { 3191 img.style.display = 'none'; 3348 // Try next fallback on error 3349 tryNextFallback(); 3192 3350 }); 3193 3351 3194 // Replace existing SVG-only icon content so layout stays consistent 3195 iconContainer.innerHTML = ''; 3352 img.addEventListener('load', function() { 3353 // Successfully loaded, check if it's a valid image 3354 // Some services return 1x1 transparent pixel for missing favicons 3355 if (img.naturalWidth > 1 && img.naturalHeight > 1) { 3356 // Valid favicon loaded - image is already in DOM, just hide SVG 3357 const svg = iconContainer.querySelector('svg'); 3358 if (svg) { 3359 svg.style.display = 'none'; 3360 } 3361 } else { 3362 // Invalid image, try next fallback 3363 tryNextFallback(); 3364 } 3365 }); 3366 3367 // Add image to DOM immediately, then start loading 3196 3368 iconContainer.appendChild(img); 3369 img.src = fallbackUrls[0]; 3197 3370 }); 3198 3371 } … … 3205 3378 const percentage = totalCount > 0 ? Math.round((count / totalCount) * 100) : 0; 3206 3379 const referrerIconSVG = getReferrerIconSVG(referrer); 3380 const faviconUrl = item.favicon_url || ''; 3381 const domain = item.domain || ''; 3207 3382 3208 3383 // Calculate trend (for now, show as new since we don't have previous data) … … 3234 3409 3235 3410 const row = document.createElement('tr'); 3411 // Store favicon data as data attributes 3412 if (faviconUrl) { 3413 row.setAttribute('data-favicon-url', faviconUrl); 3414 } 3415 if (domain) { 3416 row.setAttribute('data-domain', domain); 3417 } 3236 3418 row.innerHTML = ` 3237 3419 <td class="referrer-name-cell"> … … 3307 3489 let html = ''; 3308 3490 visitors.forEach(visitor => { 3491 // Get country code for flag icon 3492 const countryCode = (visitor.country_code || '').toLowerCase(); 3493 const flagUrl = countryCode && countryCode.length === 2 ? `https://flagcdn.com/w40/${countryCode}.png` : ''; 3494 const flagHtml = flagUrl ? `<img src="${flagUrl}" alt="${visitor.country}" class="visitor-flag-icon" />` : '<span class="visitor-flag">${visitor.flag}</span>'; 3495 3309 3496 html += ` 3310 3497 <div class="visitor-item grid"> 3311 3498 <span class="visitor-datetime">${(visitor.visited_at || '') + (visitor.time_ago ? ' - <span class="ago">' + visitor.time_ago + '</span>' : '')}</span> 3312 <span class="visitor-flag-country"> <span class="visitor-flag">${visitor.flag}</span><span class="visitor-location">${visitor.country}</span></span>3499 <span class="visitor-flag-country">${flagHtml} <span class="visitor-location">${visitor.country}</span></span> 3313 3500 <span class="visitor-pageblock"><span class="visitor-title">${visitor.page_title || ''}</span><a class="visitor-url" href="${visitor.current_url || '#'}" target="_blank" rel="noopener">${visitor.current_url || ''}</a></span> 3314 3501 <span class="visitor-ip-col"><span class="visitor-ip-pill">${shortIP(visitor.ip)}<\/span></span> … … 3505 3692 fd.append('end_date', eEl && eEl.value ? eEl.value : ''); 3506 3693 } 3507 function cc2flag(cc){ try{ var s=(cc||'').toString().trim().toUpperCase(); if(/^[A-Z]{2}$/.test(s)){ var base=127397; return String.fromCodePoint(s.charCodeAt(0)+base, s.charCodeAt(1)+base); } }catch(e){} return '🌐'; } 3694 // Function to get flag image HTML for a country code 3695 function getFlagHTML(cc, countryName) { 3696 try { 3697 var code = (cc||'').toString().trim().toUpperCase(); 3698 if(/^[A-Z]{2}$/.test(code)) { 3699 // Use flagcdn.com for high-quality flag images 3700 return '<img src="https://flagcdn.com/w20/' + code.toLowerCase() + '.png" ' + 3701 'srcset="https://flagcdn.com/w40/' + code.toLowerCase() + '.png 2x" ' + 3702 'alt="' + (countryName || code) + '" ' + 3703 'class="tu-flag-img" ' + 3704 'style="width:20px;height:15px;margin-right:6px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);" />'; 3705 } 3706 } catch(e) {} 3707 // Fallback to globe emoji for unknown countries 3708 return '<span style="margin-right:6px;">🌐</span>'; 3709 } 3508 3710 3509 3711 fetch('' + opti_behaviorDashboard.ajaxUrl + '', {method:'POST', credentials:'same-origin', body: fd}) … … 3562 3764 '<td class="tu-num"><span class="tu-badge">'+ (it.sessions||0) +'</span></td>'+ 3563 3765 '<td class="tu-num">'+ (it.pages_per_session||0).toFixed(2) +'</td>'+ 3564 '<td> <span class="tu-flag">'+ cc2flag(it.country_code) +'</span><span class="tu-country-name">'+ esc(it.country_name||'') +'</span></td>'+3766 '<td>'+ getFlagHTML(it.country_code, it.country_name) +'<span class="tu-country-name">'+ esc(it.country_name||'') +'</span></td>'+ 3565 3767 '<td class="tu-last">'+ esc(it.last_seen_human||'') +'</td>'+ 3566 3768 '</tr>'; … … 3619 3821 var startEl = document.getElementById('start-date'); 3620 3822 var endEl = document.getElementById('end-date'); 3621 var period = periodEl ? (periodEl.value || 'last 7days') : 'last7days';3823 var period = periodEl ? (periodEl.value || 'last30days') : 'last30days'; 3622 3824 3623 3825 var ajaxUrl = (window.opti_behaviorData && window.opti_behaviorData.ajaxUrl) || (window.ajaxurl) || '/wp-admin/admin-ajax.php'; … … 4124 4326 } 4125 4327 4328 /** 4329 * Initialize mini bar charts for stat cards history 4330 */ 4331 function initStatHistoryCharts() { 4332 // Check if Chart.js is loaded 4333 if (typeof Chart === 'undefined') { 4334 console.warn('Chart.js not loaded, skipping stat history charts'); 4335 return; 4336 } 4337 4338 // Get daily history data from window object 4339 const data = window.opti_behaviorData; 4340 if (!data || !data.dashboard || !data.dashboard.daily_history) { 4341 console.warn('Daily history data not available'); 4342 return; 4343 } 4344 4345 const dailyHistory = data.dashboard.daily_history; 4346 const dates = dailyHistory.dates || []; 4347 4348 // Format dates for display (e.g., "Dec 1", "Dec 2") 4349 const labels = dates.map(dateStr => { 4350 const date = new Date(dateStr); 4351 return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 4352 }); 4353 4354 // Define color scheme for each metric 4355 const colors = { 4356 visitors: '#3b82f6', // Blue 4357 sessions: '#8b5cf6', // Purple 4358 pageviews: '#f59e0b', // Orange 4359 avg_session_time: '#14b8a6', // Teal 4360 avg_scroll_depth: '#6366f1', // Indigo 4361 bounce_rate: '#ef4444' // Red 4362 }; 4363 4364 // Chart configuration template 4365 const getChartConfig = (metricKey, label, data, color) => ({ 4366 type: 'bar', 4367 data: { 4368 labels: labels, 4369 datasets: [{ 4370 label: label, 4371 data: data, 4372 backgroundColor: color, 4373 borderColor: color, 4374 borderWidth: 0, 4375 borderRadius: 2, 4376 barPercentage: 0.8, 4377 categoryPercentage: 0.9 4378 }] 4379 }, 4380 options: { 4381 responsive: true, 4382 maintainAspectRatio: false, 4383 plugins: { 4384 legend: { display: false }, 4385 tooltip: { 4386 enabled: true, 4387 backgroundColor: 'rgba(0, 0, 0, 0.8)', 4388 padding: 8, 4389 displayColors: false, 4390 callbacks: { 4391 title: (tooltipItems) => tooltipItems[0].label, 4392 label: (context) => { 4393 let value = context.parsed.y; 4394 // Format based on metric type 4395 if (metricKey === 'avg_session_time') { 4396 const minutes = Math.floor(value / 60); 4397 const seconds = value % 60; 4398 return `${minutes}:${seconds.toString().padStart(2, '0')}`; 4399 } else if (metricKey === 'avg_scroll_depth' || metricKey === 'bounce_rate') { 4400 return `${value}%`; 4401 } 4402 return value.toString(); 4403 } 4404 } 4405 } 4406 }, 4407 scales: { 4408 x: { 4409 display: false, 4410 grid: { display: false } 4411 }, 4412 y: { 4413 display: false, 4414 grid: { display: false }, 4415 beginAtZero: true 4416 } 4417 }, 4418 animation: { 4419 duration: 500, 4420 easing: 'easeInOutQuart' 4421 } 4422 } 4423 }); 4424 4425 // Initialize each chart 4426 const metrics = [ 4427 { key: 'visitors', label: 'Visitors', canvasId: 'history-visitors' }, 4428 { key: 'sessions', label: 'Sessions', canvasId: 'history-sessions' }, 4429 { key: 'pageviews', label: 'Page Views', canvasId: 'history-pageviews' }, 4430 { key: 'avg_session_time', label: 'Avg. Session Time', canvasId: 'history-avg-session-time' }, 4431 { key: 'avg_scroll_depth', label: 'Avg. Scroll Depth', canvasId: 'history-avg-scroll-depth' }, 4432 { key: 'bounce_rate', label: 'Bounce Rate', canvasId: 'history-bounce-rate' } 4433 ]; 4434 4435 metrics.forEach(metric => { 4436 const canvas = document.getElementById(metric.canvasId); 4437 if (!canvas) { 4438 console.warn(`Canvas not found: ${metric.canvasId}`); 4439 return; 4440 } 4441 4442 const ctx = canvas.getContext('2d'); 4443 const chartData = dailyHistory[metric.key] || []; 4444 const color = colors[metric.key]; 4445 4446 // Create the chart 4447 new Chart(ctx, getChartConfig(metric.key, metric.label, chartData, color)); 4448 }); 4449 } 4450 4451 // Initialize stat history charts when DOM is ready 4452 if (document.readyState === 'loading') { 4453 document.addEventListener('DOMContentLoaded', initStatHistoryCharts); 4454 } else { 4455 initStatHistoryCharts(); 4456 } 4457 4126 4458 // Expose widget rendering functions to window for async loader callbacks 4127 4459 window.updateSessionsChart = updateSessionsChart; … … 4136 4468 window.initBotTrafficWidget = initBotTrafficWidget; 4137 4469 window.initScreenResolutionChart = initScreenResolutionChart; 4470 window.initStatHistoryCharts = initStatHistoryCharts; 4138 4471 })(); -
opti-behavior/trunk/assets/js/opti-behavior-heatmap-simple.js
r3408429 r3414697 77 77 this.sessionId = null; 78 78 this.visitorId = null; 79 // Track last activity time for inactivity timeout (30 minutes like Pro version) 80 this.lastActivityTime = Date.now(); 81 this.inactivityTimeout = 30 * 60 * 1000; // 30 minutes in milliseconds 82 this.maxSessionDuration = 60 * 60 * 1000; // Cap session at 1 hour max 79 83 } 80 84 … … 123 127 HeatmapTracker.prototype.handleClick = function(event) { 124 128 console.log('[Heatmap] Click detected!', event); 129 130 // Update activity timestamp 131 this.lastActivityTime = Date.now(); 125 132 126 133 // Get click coordinates … … 288 295 289 296 HeatmapTracker.prototype.handleScroll = function() { 297 // Update activity timestamp on scroll 298 this.lastActivityTime = Date.now(); 290 299 this.updateScrollDepth(); 291 300 }; … … 336 345 337 346 HeatmapTracker.prototype.sendHeartbeat = function() { 338 const sessionDuration = Math.round((Date.now() - this.sessionStartTime) / 1000); 347 const now = Date.now(); 348 const timeSinceLastActivity = now - this.lastActivityTime; 349 const totalSessionTime = now - this.sessionStartTime; 350 351 // Stop heartbeat if user has been inactive for 30 minutes 352 if (timeSinceLastActivity > this.inactivityTimeout) { 353 console.log('[Heatmap] Session inactive for 30+ minutes, stopping heartbeat'); 354 clearInterval(this.heartbeatTimer); 355 this.heartbeatTimer = null; 356 return; 357 } 358 359 // Stop heartbeat if session exceeds 1 hour (prevents inflated durations) 360 if (totalSessionTime > this.maxSessionDuration) { 361 console.log('[Heatmap] Session exceeded 1 hour max, stopping heartbeat'); 362 clearInterval(this.heartbeatTimer); 363 this.heartbeatTimer = null; 364 return; 365 } 366 367 const sessionDuration = Math.round(totalSessionTime / 1000); 339 368 340 369 // Use stored IDs instead of reading cookies -
opti-behavior/trunk/includes/class-opti-behavior-heatmap-file-storage.php
r3399182 r3414697 673 673 674 674 return $total_count; 675 } 676 677 /** 678 * List all recordings from file storage with pagination and filtering 679 * 680 * @param int $limit Number of recordings to return (0 = no limit). 681 * @param int $offset Offset for pagination. 682 * @param string $start_date Optional start date filter (Y-m-d format). 683 * @param string $end_date Optional end date filter (Y-m-d format). 684 * @param string $sort_order Sort order ('asc' or 'desc'). 685 * @return array Array with 'recordings' and 'total' count. 686 */ 687 public function list_recordings( $limit = 0, $offset = 0, $start_date = null, $end_date = null, $sort_order = 'desc', $sort_by = 'date' ) { 688 $this->log( '[Opti-FREE] list_recordings START - sort_by=' . $sort_by . ', sort_order=' . $sort_order . ', limit=' . $limit, 'debug' ); 689 $recordings_dir = $this->base_dir . 'recordings/'; 690 691 if ( ! file_exists( $recordings_dir ) ) { 692 return array( 693 'recordings' => array(), 694 'total' => 0, 695 ); 696 } 697 698 $all_recordings = array(); 699 700 // Determine date range to scan 701 if ( $start_date && $end_date ) { 702 $start_timestamp = strtotime( $start_date ); 703 $end_timestamp = strtotime( $end_date . ' 23:59:59' ); 704 } else { 705 // Scan all files if no date range specified 706 $start_timestamp = 0; 707 $end_timestamp = PHP_INT_MAX; 708 } 709 710 // Recursively scan recordings directory 711 $iterator = new RecursiveIteratorIterator( 712 new RecursiveDirectoryIterator( $recordings_dir, RecursiveDirectoryIterator::SKIP_DOTS ) 713 ); 714 715 foreach ( $iterator as $file ) { 716 if ( ! $file->isFile() || $file->getFilename() === 'index.php' || $file->getFilename() === '.htaccess' ) { 717 continue; 718 } 719 720 // Parse filename: session_[id]_[timestamp].json.gz 721 $filename = $file->getFilename(); 722 if ( ! preg_match( '/^session_(.+)_(\d+)\.json(\.gz)?$/', $filename, $matches ) ) { 723 continue; 724 } 725 726 $session_id = $matches[1]; 727 $file_timestamp = intval( $matches[2] ); 728 729 // Check if file is within date range 730 if ( $file_timestamp < $start_timestamp || $file_timestamp > $end_timestamp ) { 731 continue; 732 } 733 734 // Get file path relative to base dir 735 $relative_path = str_replace( $this->base_dir, '', $file->getPathname() ); 736 $relative_path = str_replace( '\\', '/', $relative_path ); // Normalize path separators 737 738 // Read file metadata (we'll load full data only when viewing specific recording) 739 $all_recordings[] = array( 740 'session_id' => $session_id, 741 'file_path' => $relative_path, 742 'file_size' => $file->getSize(), 743 'start_time' => gmdate( 'Y-m-d H:i:s', $file_timestamp ), 744 'timestamp' => $file_timestamp, 745 ); 746 } 747 748 // Sort by timestamp 749 usort( $all_recordings, function( $a, $b ) use ( $sort_order ) { 750 if ( $sort_order === 'asc' ) { 751 return $a['timestamp'] - $b['timestamp']; 752 } else { 753 return $b['timestamp'] - $a['timestamp']; 754 } 755 }); 756 757 $total = count( $all_recordings ); 758 759 // Apply pagination 760 if ( $limit > 0 ) { 761 $all_recordings = array_slice( $all_recordings, $offset, $limit ); 762 } 763 764 // If sorting by duration, read duration from files AFTER pagination (only for displayed items) 765 if ( $sort_by === 'duration' ) { 766 $this->log( '[Opti-FREE] DURATION SORTING ACTIVE - Reading durations...', 'debug' ); 767 foreach ( $all_recordings as &$recording ) { 768 try { 769 $file_data = $this->read_recording( $recording['file_path'] ); 770 if ( $file_data && isset( $file_data['duration'] ) ) { 771 $recording['duration'] = intval( $file_data['duration'] ); 772 $this->log( '[Opti-FREE] Session ' . $recording['session_id'] . ' duration: ' . $recording['duration'] . 's', 'debug' ); 773 } 774 } catch ( Exception $e ) { 775 $this->log( '[Opti-FREE] Error reading duration: ' . $recording['file_path'], 'error' ); 776 } 777 } 778 unset( $recording ); // Break reference 779 780 $this->log( '[Opti-FREE] Sorting by duration (' . $sort_order . ')...', 'debug' ); 781 // Now sort by duration 782 usort( $all_recordings, function( $a, $b ) use ( $sort_order ) { 783 if ( $sort_order === 'asc' ) { 784 return $a['duration'] - $b['duration']; 785 } else { 786 return $b['duration'] - $a['duration']; 787 } 788 }); 789 790 // Log final order after sorting 791 $this->log( '[Opti-FREE] After duration sort - Final order:', 'debug' ); 792 foreach ( $all_recordings as $rec ) { 793 $this->log( '[Opti-FREE] => ' . $rec['session_id'] . ': ' . $rec['duration'] . 's', 'debug' ); 794 } 795 } 796 797 return array( 798 'recordings' => $all_recordings, 799 'total' => $total, 800 ); 675 801 } 676 802 -
opti-behavior/trunk/includes/class-opti-behavior-license-manager.php
r3406294 r3414697 16 16 /** 17 17 * API base URL 18 */ 19 const API_URL = 'https://api.optiuser.com/'; 18 * 19 * Environment is controlled by OPTI_BEHAVIOR_ENVIRONMENT constant in Opti-Behavior.php: 20 * - 'local' => http://localhost/api/ 21 * - 'live' => https://api.optiuser.com/ (default) 22 * 23 * Can also be overridden in wp-config.php using: 24 * define( 'OPTI_BEHAVIOR_API_URL', 'https://custom-api.com/' ); 25 */ 26 private static function get_api_url() { 27 // Check if API URL is manually defined in wp-config.php (highest priority) 28 if ( defined( 'OPTI_BEHAVIOR_API_URL' ) && ! empty( OPTI_BEHAVIOR_API_URL ) ) { 29 return trailingslashit( OPTI_BEHAVIOR_API_URL ); 30 } 31 32 // Use environment-based URL 33 if ( defined( 'OPTI_BEHAVIOR_ENVIRONMENT' ) && OPTI_BEHAVIOR_ENVIRONMENT === 'local' ) { 34 return 'http://localhost/api/'; 35 } 36 37 // Default to production API 38 return 'https://api.optiuser.com/'; 39 } 20 40 21 41 /** … … 85 105 */ 86 106 public function auto_register() { 107 $this->debug_manager->log( '=== Starting Auto-Registration ===' , 'info', 'license' ); 108 $this->debug_manager->log( 'API URL: ' . self::get_api_url(), 'info', 'license' ); 109 87 110 // Generate client keypair 111 $this->debug_manager->log( 'Generating client keypair...', 'debug', 'license' ); 88 112 $keypair = $this->generate_client_keypair(); 89 113 … … 92 116 return false; 93 117 } 94 118 119 $this->debug_manager->log( 'Client keypair generated successfully', 'debug', 'license' ); 120 95 121 // Get domain name 96 122 $domain = $this->get_domain(); 97 123 $this->debug_manager->log( 'Domain: ' . $domain, 'info', 'license' ); 124 125 // Get site information 126 $admin_email = get_option( 'admin_email' ); 127 $site_url = get_site_url(); 128 $site_name = get_bloginfo( 'name' ); 129 130 $this->debug_manager->log( 'Site URL: ' . $site_url, 'info', 'license' ); 131 $this->debug_manager->log( 'Site Name: ' . $site_name, 'info', 'license' ); 132 $this->debug_manager->log( 'Admin Email: ' . $admin_email, 'info', 'license' ); 133 98 134 // Register with API 135 $this->debug_manager->log( 'Calling API /register endpoint...', 'info', 'license' ); 99 136 $response = $this->call_api( 'register', array( 100 137 'domain' => $domain, 101 138 'client_public_key' => $keypair['public_key'], 139 'email' => $admin_email, 140 'site_url' => $site_url, 141 'site_name' => $site_name, 102 142 ), 'POST' ); 103 143 104 144 if ( ! $response || ! isset( $response['success'] ) || ! $response['success'] ) { 105 $this->debug_manager->log( 'Registration failed: ' . wp_json_encode( $response ), 'error', 'license' ); 106 return false; 107 } 108 145 $error_details = $response ? wp_json_encode( $response ) : 'NULL response'; 146 $this->debug_manager->log( 'Registration failed: ' . $error_details, 'error', 'license' ); 147 $this->debug_manager->log( '=== Auto-Registration FAILED ===' , 'error', 'license' ); 148 return false; 149 } 150 151 $this->debug_manager->log( 'Registration successful! Storing installation data...', 'info', 'license' ); 152 $this->debug_manager->log( 'Installation ID: ' . $response['data']['installation_id'], 'info', 'license' ); 153 109 154 // Store installation data 110 155 update_option( self::OPTION_INSTALLATION_ID, $response['data']['installation_id'] ); … … 114 159 update_option( self::OPTION_REGISTRATION_DATE, current_time( 'mysql' ) ); 115 160 161 $this->debug_manager->log( 'Installation options saved to WordPress database', 'debug', 'license' ); 162 116 163 // Store master_secret for API authentication (encrypted) 117 164 if ( isset( $response['data']['master_secret'] ) ) { … … 119 166 update_option( 'opti_behavior_master_secret', $response['data']['master_secret'] ); 120 167 $this->debug_manager->log( 'Master secret stored for API authentication', 'info', 'license' ); 168 } else { 169 $this->debug_manager->log( 'WARNING: No master_secret in API response!', 'warning', 'license' ); 121 170 } 122 171 … … 128 177 } 129 178 179 $this->debug_manager->log( '=== Auto-Registration COMPLETED SUCCESSFULLY ===' , 'info', 'license' ); 130 180 return true; 131 181 } … … 225 275 */ 226 276 public function call_api( $endpoint, $data = array(), $method = 'GET' ) { 227 $url = self::API_URL . $endpoint; 277 $this->debug_manager->log( '--- API Call Start ---', 'debug', 'license' ); 278 $this->debug_manager->log( 'Endpoint: ' . $endpoint, 'info', 'license' ); 279 $this->debug_manager->log( 'Method: ' . $method, 'info', 'license' ); 280 281 $url = self::get_api_url() . $endpoint; 282 $this->debug_manager->log( 'Full URL: ' . $url, 'info', 'license' ); 283 228 284 $installation_id = $this->get_installation_id(); 285 $this->debug_manager->log( 'Installation ID: ' . ( $installation_id ? $installation_id : 'EMPTY' ), 'info', 'license' ); 286 229 287 $timestamp = time(); 230 288 … … 238 296 } 239 297 298 $this->debug_manager->log( 'Request Data: ' . $data_to_sign, 'debug', 'license' ); 299 240 300 // Generate HMAC signature (only if registered) 241 301 $signature = ''; 242 302 if ( ! empty( $installation_id ) && $endpoint !== 'register' ) { 243 303 $signature = $this->generate_api_signature( $data_to_sign, $timestamp ); 304 $this->debug_manager->log( 'HMAC Signature generated: ' . substr( $signature, 0, 20 ) . '...', 'debug', 'license' ); 305 } else { 306 $this->debug_manager->log( 'No signature required (registration or no installation ID)', 'debug', 'license' ); 244 307 } 245 308 … … 259 322 } elseif ( $method === 'GET' && ! empty( $data ) ) { 260 323 $url = add_query_arg( $data, $url ); 261 } 262 324 $this->debug_manager->log( 'URL with query params: ' . $url, 'debug', 'license' ); 325 } 326 327 $this->debug_manager->log( 'Sending wp_remote_request...', 'debug', 'license' ); 263 328 $response = wp_remote_request( $url, $args ); 264 329 265 330 if ( is_wp_error( $response ) ) { 266 $this->debug_manager->log( 'API call failed: ' . $response->get_error_message(), 'error', 'license' ); 267 return false; 268 } 331 $error_message = $response->get_error_message(); 332 $this->debug_manager->log( 'wp_remote_request ERROR: ' . $error_message, 'error', 'license' ); 333 $this->debug_manager->log( '--- API Call FAILED (WP_Error) ---', 'error', 'license' ); 334 return false; 335 } 336 337 $http_code = wp_remote_retrieve_response_code( $response ); 338 $this->debug_manager->log( 'HTTP Response Code: ' . $http_code, 'info', 'license' ); 269 339 270 340 $body = wp_remote_retrieve_body( $response ); 341 $this->debug_manager->log( 'Response Body (first 500 chars): ' . substr( $body, 0, 500 ), 'debug', 'license' ); 342 271 343 $decoded = json_decode( $body, true ); 344 345 if ( json_last_error() !== JSON_ERROR_NONE ) { 346 $this->debug_manager->log( 'JSON decode error: ' . json_last_error_msg(), 'error', 'license' ); 347 $this->debug_manager->log( '--- API Call FAILED (Invalid JSON) ---', 'error', 'license' ); 348 return false; 349 } 350 351 $this->debug_manager->log( 'API Response: ' . wp_json_encode( $decoded ), 'debug', 'license' ); 352 $this->debug_manager->log( '--- API Call End ---', 'debug', 'license' ); 272 353 273 354 return $decoded; … … 304 385 305 386 /** 306 * Get quota information 387 * Get quota information from API with encrypted caching 388 * 389 * SECURITY: Uses encrypted cache with HMAC integrity verification 390 * Cache is invalidated if tampered or every 5 minutes to stay fresh 307 391 */ 308 392 public function get_quota() { 393 $this->debug_manager->log( '--- get_quota() called ---', 'debug', 'license' ); 394 309 395 $installation_id = $this->get_installation_id(); 310 396 311 397 if ( empty( $installation_id ) ) { 312 return false; 313 } 314 398 $this->debug_manager->log( 'get_quota failed: No installation ID found (not registered)', 'error', 'license' ); 399 return false; 400 } 401 402 $this->debug_manager->log( 'Installation ID found: ' . $installation_id, 'debug', 'license' ); 403 404 // Try to get from encrypted cache first 405 $cached_quota = $this->get_cached_quota( $installation_id ); 406 if ( $cached_quota !== false ) { 407 $this->debug_manager->log( 'Using cached quota data', 'debug', 'license' ); 408 return $cached_quota; 409 } 410 411 $this->debug_manager->log( 'No cached quota, fetching from API...', 'debug', 'license' ); 412 413 // Fetch fresh data from API 315 414 $response = $this->call_api( 'quota', array( 316 415 'installation_id' => $installation_id, … … 318 417 319 418 if ( ! $response || ! isset( $response['success'] ) || ! $response['success'] ) { 320 return false; 321 } 419 $error_details = $response ? wp_json_encode( $response ) : 'NULL response'; 420 $this->debug_manager->log( 'Failed to fetch quota from API: ' . $error_details, 'error', 'license' ); 421 return false; 422 } 423 424 $this->debug_manager->log( 'Quota API response successful', 'debug', 'license' ); 322 425 323 426 // Return just the current_month data 324 427 if ( isset( $response['data']['current_month'] ) ) { 325 return $response['data']['current_month']; 326 } 327 428 $quota_data = $response['data']['current_month']; 429 430 // Cache the quota data securely 431 $this->cache_quota( $installation_id, $quota_data ); 432 433 $this->debug_manager->log( 'Quota data cached and returned', 'debug', 'license' ); 434 return $quota_data; 435 } 436 437 $this->debug_manager->log( 'Quota API response missing current_month data', 'error', 'license' ); 328 438 return false; 439 } 440 441 /** 442 * Get cached quota with integrity verification 443 * 444 * @param string $installation_id Installation ID 445 * @return array|false Quota data or false if cache invalid/expired 446 */ 447 private function get_cached_quota( $installation_id ) { 448 $cache_option = 'opti_behavior_quota_cache_' . substr( md5( $installation_id ), 0, 16 ); 449 $cached = get_transient( $cache_option ); 450 451 if ( ! $cached || ! is_array( $cached ) ) { 452 return false; 453 } 454 455 // Verify cache has required fields 456 if ( ! isset( $cached['data'], $cached['hash'], $cached['timestamp'] ) ) { 457 $this->debug_manager->log( 'Cache missing required fields', 'warning', 'license' ); 458 delete_transient( $cache_option ); 459 return false; 460 } 461 462 // Verify integrity hash (prevents tampering) 463 $expected_hash = $this->calculate_quota_hash( $cached['data'], $cached['timestamp'], $installation_id ); 464 if ( ! hash_equals( $expected_hash, $cached['hash'] ) ) { 465 $this->debug_manager->log( 'Cache integrity check failed - possible tampering detected', 'error', 'license' ); 466 delete_transient( $cache_option ); 467 return false; 468 } 469 470 return $cached['data']; 471 } 472 473 /** 474 * Cache quota data with integrity protection 475 * 476 * @param string $installation_id Installation ID 477 * @param array $quota_data Quota data to cache 478 */ 479 private function cache_quota( $installation_id, $quota_data ) { 480 $cache_option = 'opti_behavior_quota_cache_' . substr( md5( $installation_id ), 0, 16 ); 481 $timestamp = time(); 482 483 $cache_data = array( 484 'data' => $quota_data, 485 'timestamp' => $timestamp, 486 'hash' => $this->calculate_quota_hash( $quota_data, $timestamp, $installation_id ), 487 ); 488 489 // Cache for 1 minute - allows admin changes to be visible quickly 490 // Admin can change quota limits in API, so we need short cache 491 set_transient( $cache_option, $cache_data, 60 ); 492 } 493 494 /** 495 * Calculate integrity hash for quota cache 496 * 497 * @param array $data Quota data 498 * @param int $timestamp Cache timestamp 499 * @param string $installation_id Installation ID 500 * @return string HMAC hash 501 */ 502 private function calculate_quota_hash( $data, $timestamp, $installation_id ) { 503 // Use site-specific secrets to prevent cross-site tampering 504 $secret = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'opti-behavior-secret'; 505 $salt = defined( 'NONCE_SALT' ) ? NONCE_SALT : get_option( 'siteurl' ); 506 507 $message = wp_json_encode( $data ) . $timestamp . $installation_id . $salt; 508 return hash_hmac( 'sha256', $message, $secret ); 329 509 } 330 510 … … 341 521 * Check if user can record a new session 342 522 * 343 * @return bool True if user can record, false if quota exceeded 523 * SECURITY: FAIL CLOSED - if we can't verify quota, deny recording 524 * This prevents bypass attempts when API is unreachable 525 * 526 * @return bool True if user can record, false if quota exceeded or cannot verify 344 527 */ 345 528 public function can_record_session() { 346 529 $quota = $this->get_quota(); 347 530 531 // SECURITY: Fail closed - deny if we can't get quota info 348 532 if ( ! $quota ) { 349 // If we can't get quota info, allow recording (fail open)350 return true;533 $this->debug_manager->log( 'Cannot verify quota - denying recording for security', 'warning', 'license' ); 534 return false; 351 535 } 352 536 … … 356 540 } 357 541 542 $this->debug_manager->log( 'Quota exceeded or invalid', 'info', 'license' ); 358 543 return false; 359 544 } … … 388 573 } 389 574 575 // SECURITY: Clear quota cache to force fresh fetch on next check 576 $this->clear_quota_cache( $installation_id ); 577 390 578 $this->debug_manager->log( 'Quota incremented successfully. Session: ' . $session_id, 'info', 'license' ); 391 579 return true; 392 580 } 581 582 /** 583 * Clear quota cache 584 * 585 * @param string $installation_id Installation ID 586 */ 587 private function clear_quota_cache( $installation_id ) { 588 $cache_option = 'opti_behavior_quota_cache_' . substr( md5( $installation_id ), 0, 16 ); 589 delete_transient( $cache_option ); 590 $this->debug_manager->log( 'Quota cache cleared', 'debug', 'license' ); 591 } 393 592 } 394 593 -
opti-behavior/trunk/includes/trait-opti-behavior-ajax-handlers.php
r3401441 r3414697 35 35 $page = isset($_POST['paged']) ? max(1, intval( wp_unslash( $_POST['paged'] ) )) : 1; 36 36 $per_page = isset($_POST['per_page']) ? max(1, intval( wp_unslash( $_POST['per_page'] ) )) : 5; 37 $period = isset($_POST['period']) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last 7days';37 $period = isset($_POST['period']) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last30days'; 38 38 $start = isset($_POST['start_date']) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : ''; 39 39 $end = isset($_POST['end_date']) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : ''; … … 93 93 } 94 94 95 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last 7days';95 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last30days'; 96 96 $start = isset($_POST['start_date']) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : null; 97 97 $end = isset($_POST['end_date']) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : null; … … 210 210 211 211 $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'overview'; 212 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last 7days';212 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last30days'; 213 213 214 214 $data = $this->get_analytics_data( $type, $period ); … … 679 679 680 680 // Get and sanitize parameters 681 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last 7days';681 $period = isset( $_POST['period'] ) ? sanitize_text_field( wp_unslash( $_POST['period'] ) ) : 'last30days'; 682 682 $start_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : null; 683 683 $end_date = isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : null; -
opti-behavior/trunk/includes/trait-opti-behavior-dashboard-views.php
r3408429 r3414697 31 31 * @param string $period Period. 32 32 */ 33 private function render_dashboard_header( $date_range = null, $period = 'last 7days' ) {33 private function render_dashboard_header( $date_range = null, $period = 'last30days' ) { 34 34 $start_val = $date_range && isset($date_range['start']) ? substr($date_range['start'],0,10) : ''; 35 35 $end_val = $date_range && isset($date_range['end']) ? substr($date_range['end'],0,10) : ''; … … 81 81 <div class="stat-label"><?php esc_html_e( 'Visitors', 'opti-behavior' ); ?></div> 82 82 <div class="stat-change <?php echo esc_attr($cls($changes['visitors'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['visitors'] ?? 0)); ?></div> 83 <div class="stat-history"> 84 <canvas id="history-visitors" width="280" height="50"></canvas> 85 </div> 83 86 </div> 84 87 </div> … … 90 93 <div class="stat-label"><?php esc_html_e( 'Sessions', 'opti-behavior' ); ?></div> 91 94 <div class="stat-change <?php echo esc_attr($cls($changes['sessions'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['sessions'] ?? 0)); ?></div> 95 <div class="stat-history"> 96 <canvas id="history-sessions" width="280" height="50"></canvas> 97 </div> 92 98 </div> 93 99 </div> … … 99 105 <div class="stat-label"><?php esc_html_e( 'Page Views', 'opti-behavior' ); ?></div> 100 106 <div class="stat-change <?php echo esc_attr($cls($changes['pageviews'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['pageviews'] ?? 0)); ?></div> 107 <div class="stat-history"> 108 <canvas id="history-pageviews" width="280" height="50"></canvas> 109 </div> 101 110 </div> 102 111 </div> … … 108 117 <div class="stat-label"><?php esc_html_e( 'Avg. Session Time', 'opti-behavior' ); ?></div> 109 118 <div class="stat-change <?php echo esc_attr($cls($changes['avg_session_time'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['avg_session_time'] ?? 0)); ?></div> 119 <div class="stat-history"> 120 <canvas id="history-avg-session-time" width="280" height="50"></canvas> 121 </div> 110 122 </div> 111 123 </div> … … 117 129 <div class="stat-label"><?php esc_html_e( 'Avg. Scroll Depth', 'opti-behavior' ); ?></div> 118 130 <div class="stat-change <?php echo esc_attr($cls($changes['avg_scroll_depth'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['avg_scroll_depth'] ?? 0)); ?></div> 131 <div class="stat-history"> 132 <canvas id="history-avg-scroll-depth" width="280" height="50"></canvas> 133 </div> 119 134 </div> 120 135 </div> … … 126 141 <div class="stat-label"><?php esc_html_e( 'Bounce Rate', 'opti-behavior' ); ?></div> 127 142 <div class="stat-change <?php echo esc_attr($cls($changes['bounce_rate'] ?? 0)); ?>"><?php echo esc_html($fmt($changes['bounce_rate'] ?? 0)); ?></div> 143 <div class="stat-history"> 144 <canvas id="history-bounce-rate" width="280" height="50"></canvas> 145 </div> 128 146 </div> 129 147 </div> … … 178 196 <div class="visitor-item grid"> 179 197 <span class="visitor-datetime"><?php echo esc_html($visitor['visited_at'] ?? ''); ?><?php if(!empty($visitor['time_ago'])): ?> - <span class="ago"><?php echo esc_html($visitor['time_ago']); ?></span><?php endif; ?></span> 180 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo esc_html($visitor['flag']);?></span> <span class="visitor-location"><?php echo esc_html($visitor['country']); ?></span></span>198 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo $visitor['flag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Flag emoji is safe, generated from validated ISO country code ?></span> <span class="visitor-location"><?php echo esc_html($visitor['country']); ?></span></span> 181 199 <span class="visitor-pageblock"><span class="visitor-title"><?php echo esc_html($visitor['page_title'] ?? ''); ?></span><a class="visitor-url" href="<?php echo esc_url($visitor['current_url'] ?? ''); ?>" target="_blank" rel="noopener"><?php echo esc_html($visitor['current_url'] ?? ''); ?></a></span> 182 200 <span class="visitor-ip-col"><span class="visitor-ip-pill"><?php $ip_raw = isset($visitor['ip']) ? (string)$visitor['ip'] : ''; $ip_raw = trim($ip_raw); if ($ip_raw === '') { echo '-'; } else if (strpos($ip_raw, ':') !== false) { $start = substr($ip_raw, 0, 7); $end = substr($ip_raw, -7); echo esc_html($start . '…' . $end); } else { echo esc_html($ip_raw); } ?></span></span> … … 592 610 } 593 611 612 // Get user-defined intent rules from settings 613 $intent_rules = get_option( 'opti_behavior_intent_rules', array( 614 'low_intent' => array( 615 'time_spent' => 10, 616 'clicks' => 1, 617 'scroll_depth' => 25, 618 ), 619 'medium_intent' => array( 620 'time_spent' => 30, 621 'clicks' => 3, 622 'scroll_depth' => 50, 623 ), 624 'high_intent' => array( 625 'time_spent' => 60, 626 'clicks' => 5, 627 'scroll_depth' => 75, 628 ), 629 ) ); 630 631 $low_rules = $intent_rules['low_intent']; 632 $medium_rules = $intent_rules['medium_intent']; 633 $high_rules = $intent_rules['high_intent']; 634 594 635 // Calculate previous period 595 636 $duration = max(1, strtotime($end_date) - strtotime($start_date) + 1); … … 597 638 $prev_start = gmdate('Y-m-d H:i:s', strtotime($start_date) - $duration); 598 639 599 // Get current period session data 640 // Get current period session data with scroll depth 600 641 $sql = $wpdb->prepare( 601 642 "SELECT … … 604 645 COUNT(e.id) as interaction_count, 605 646 COUNT(CASE WHEN e.event IN (16, 17) THEN 1 END) as click_count, 606 COUNT(CASE WHEN e.event IN (32, 33) THEN 1 END) as scroll_count647 MAX(pv.scroll_depth) as max_scroll_depth 607 648 FROM {$wpdb->prefix}optibehavior_sessions s 608 649 LEFT JOIN {$wpdb->prefix}optibehavior_events e ON s.id = e.session_id 650 LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id 609 651 WHERE s.start_time >= %s AND s.start_time <= %s 610 652 GROUP BY s.id", … … 623 665 COUNT(e.id) as interaction_count, 624 666 COUNT(CASE WHEN e.event IN (16, 17) THEN 1 END) as click_count, 625 COUNT(CASE WHEN e.event IN (32, 33) THEN 1 END) as scroll_count667 MAX(pv.scroll_depth) as max_scroll_depth 626 668 FROM {$wpdb->prefix}optibehavior_sessions s 627 669 LEFT JOIN {$wpdb->prefix}optibehavior_events e ON s.id = e.session_id 670 LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id 628 671 WHERE s.start_time >= %s AND s.start_time <= %s 629 672 GROUP BY s.id", … … 635 678 $prev_sessions = $wpdb->get_results($prev_sql); 636 679 637 // Current period classification 680 // Current period classification using user-defined rules 638 681 $low_intent = 0; 639 682 $medium_intent = 0; … … 643 686 foreach ($sessions as $session) { 644 687 $duration = (int) $session->duration; 645 $interactions = (int) $session->interaction_count;646 688 $clicks = (int) $session->click_count; 647 $scrolls = (int) $session->scroll_count; 648 649 // Classify based on Microsoft Clarity criteria 650 if ($duration < 5 || ($duration >= 5 && $interactions == 0)) { 651 $low_intent++; 652 } elseif ($duration >= 5 && ($clicks > 0 || $scrolls > 0) && $interactions < 5) { 653 $medium_intent++; 654 } else { 689 $scroll_depth = (int) ($session->max_scroll_depth ?? 0); 690 691 // Classify based on user-defined rules using scoring system 692 // Count how many high intent criteria are met (need at least 2 out of 3) 693 $high_score = 0; 694 if ($duration >= $high_rules['time_spent']) $high_score++; 695 if ($clicks >= $high_rules['clicks']) $high_score++; 696 if ($scroll_depth >= $high_rules['scroll_depth']) $high_score++; 697 698 // High intent: At least 2 out of 3 criteria meet high thresholds 699 if ($high_score >= 2) { 655 700 $high_intent++; 701 } 702 else { 703 // Count how many medium intent criteria are met (need at least 2 out of 3) 704 $medium_score = 0; 705 if ($duration >= $medium_rules['time_spent']) $medium_score++; 706 if ($clicks >= $medium_rules['clicks']) $medium_score++; 707 if ($scroll_depth >= $medium_rules['scroll_depth']) $medium_score++; 708 709 // Medium intent: At least 2 out of 3 criteria meet medium thresholds 710 if ($medium_score >= 2) { 711 $medium_intent++; 712 } 713 // Low intent: Does not meet at least 2 medium thresholds 714 else { 715 $low_intent++; 716 } 656 717 } 657 718 } … … 664 725 foreach ($prev_sessions as $session) { 665 726 $duration = (int) $session->duration; 666 $interactions = (int) $session->interaction_count;667 727 $clicks = (int) $session->click_count; 668 $scrolls = (int) $session->scroll_count; 669 670 if ($duration < 5 || ($duration >= 5 && $interactions == 0)) { 671 $prev_low_intent++; 672 } elseif ($duration >= 5 && ($clicks > 0 || $scrolls > 0) && $interactions < 5) { 673 $prev_medium_intent++; 674 } else { 728 $scroll_depth = (int) ($session->max_scroll_depth ?? 0); 729 730 // Use same classification logic for previous period 731 // Count how many high intent criteria are met (need at least 2 out of 3) 732 $high_score = 0; 733 if ($duration >= $high_rules['time_spent']) $high_score++; 734 if ($clicks >= $high_rules['clicks']) $high_score++; 735 if ($scroll_depth >= $high_rules['scroll_depth']) $high_score++; 736 737 // High intent: At least 2 out of 3 criteria meet high thresholds 738 if ($high_score >= 2) { 675 739 $prev_high_intent++; 740 } 741 else { 742 // Count how many medium intent criteria are met (need at least 2 out of 3) 743 $medium_score = 0; 744 if ($duration >= $medium_rules['time_spent']) $medium_score++; 745 if ($clicks >= $medium_rules['clicks']) $medium_score++; 746 if ($scroll_depth >= $medium_rules['scroll_depth']) $medium_score++; 747 748 // Medium intent: At least 2 out of 3 criteria meet medium thresholds 749 if ($medium_score >= 2) { 750 $prev_medium_intent++; 751 } 752 // Low intent: Does not meet at least 2 medium thresholds 753 else { 754 $prev_low_intent++; 755 } 676 756 } 677 757 } -
opti-behavior/trunk/includes/trait-opti-behavior-data-helpers.php
r3406294 r3414697 220 220 $new_registered_users = method_exists( $this, 'get_new_registered_users_data_impl' ) ? $this->get_new_registered_users_data_impl( $start_date, $end_date ) : array(); 221 221 222 // Get daily history data for stats cards 223 $daily_history = $this->get_daily_stats_history( $start_date, $end_date ); 224 222 225 return array( 223 226 'stats' => $stats, … … 225 228 'date_range' => $date_range, 226 229 'changes' => $changes, 230 'daily_history' => $daily_history, 227 231 // Empty arrays/structures for widgets - will be loaded async 228 232 'charts' => array( … … 1560 1564 return $result; 1561 1565 } 1566 1567 /** 1568 * Get daily breakdown stats for history bar charts 1569 * 1570 * @param string $start_date Start date (Y-m-d H:i:s). 1571 * @param string $end_date End date (Y-m-d H:i:s). 1572 * @return array Daily stats breakdown for all metrics. 1573 */ 1574 private function get_daily_stats_history( $start_date, $end_date ) { 1575 global $wpdb; 1576 1577 // Initialize result arrays 1578 $daily_data = array( 1579 'visitors' => array(), 1580 'sessions' => array(), 1581 'pageviews' => array(), 1582 'avg_session_time' => array(), 1583 'avg_scroll_depth' => array(), 1584 'bounce_rate' => array(), 1585 'dates' => array(), 1586 ); 1587 1588 // Get all dates in the range 1589 $start_dt = new DateTime( $start_date ); 1590 $end_dt = new DateTime( $end_date ); 1591 $interval = DateInterval::createFromDateString( '1 day' ); 1592 $period = new DatePeriod( $start_dt, $interval, $end_dt->modify( '+1 day' ) ); 1593 1594 // Initialize all dates with zero values 1595 foreach ( $period as $dt ) { 1596 $date_key = $dt->format( 'Y-m-d' ); 1597 $daily_data['dates'][] = $date_key; 1598 $daily_data['visitors'][ $date_key ] = 0; 1599 $daily_data['sessions'][ $date_key ] = 0; 1600 $daily_data['pageviews'][ $date_key ] = 0; 1601 $daily_data['avg_session_time'][ $date_key ] = 0; 1602 $daily_data['avg_scroll_depth'][ $date_key ] = 0; 1603 $daily_data['bounce_rate'][ $date_key ] = 0; 1604 } 1605 1606 // Get daily visitors count 1607 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1608 $visitors_data = $wpdb->get_results( 1609 $wpdb->prepare( 1610 "SELECT 1611 DATE(s.start_time) as date, 1612 COUNT(DISTINCT s.visitor_id) as count 1613 FROM {$wpdb->prefix}optibehavior_sessions s 1614 WHERE s.start_time BETWEEN %s AND %s 1615 GROUP BY DATE(s.start_time) 1616 ORDER BY date ASC", 1617 $start_date, 1618 $end_date 1619 ), 1620 ARRAY_A 1621 ); 1622 1623 foreach ( $visitors_data as $row ) { 1624 $daily_data['visitors'][ $row['date'] ] = intval( $row['count'] ); 1625 } 1626 1627 // Get daily sessions count 1628 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1629 $sessions_data = $wpdb->get_results( 1630 $wpdb->prepare( 1631 "SELECT 1632 DATE(start_time) as date, 1633 COUNT(*) as count 1634 FROM {$wpdb->prefix}optibehavior_sessions 1635 WHERE start_time BETWEEN %s AND %s 1636 GROUP BY DATE(start_time) 1637 ORDER BY date ASC", 1638 $start_date, 1639 $end_date 1640 ), 1641 ARRAY_A 1642 ); 1643 1644 foreach ( $sessions_data as $row ) { 1645 $daily_data['sessions'][ $row['date'] ] = intval( $row['count'] ); 1646 } 1647 1648 // Get daily pageviews count 1649 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1650 $pageviews_data = $wpdb->get_results( 1651 $wpdb->prepare( 1652 "SELECT 1653 DATE(view_time) as date, 1654 COUNT(*) as count 1655 FROM {$wpdb->prefix}optibehavior_pageviews 1656 WHERE view_time BETWEEN %s AND %s 1657 GROUP BY DATE(view_time) 1658 ORDER BY date ASC", 1659 $start_date, 1660 $end_date 1661 ), 1662 ARRAY_A 1663 ); 1664 1665 foreach ( $pageviews_data as $row ) { 1666 $daily_data['pageviews'][ $row['date'] ] = intval( $row['count'] ); 1667 } 1668 1669 // Get daily average session time 1670 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1671 $session_time_data = $wpdb->get_results( 1672 $wpdb->prepare( 1673 "SELECT 1674 DATE(start_time) as date, 1675 AVG(duration) as avg_time 1676 FROM {$wpdb->prefix}optibehavior_sessions 1677 WHERE start_time BETWEEN %s AND %s 1678 GROUP BY DATE(start_time) 1679 ORDER BY date ASC", 1680 $start_date, 1681 $end_date 1682 ), 1683 ARRAY_A 1684 ); 1685 1686 foreach ( $session_time_data as $row ) { 1687 $daily_data['avg_session_time'][ $row['date'] ] = round( floatval( $row['avg_time'] ) ); 1688 } 1689 1690 // Get daily average scroll depth 1691 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1692 $scroll_depth_data = $wpdb->get_results( 1693 $wpdb->prepare( 1694 "SELECT 1695 DATE(pv.view_time) as date, 1696 AVG(pv.scroll_depth) as avg_depth 1697 FROM {$wpdb->prefix}optibehavior_pageviews pv 1698 WHERE pv.view_time BETWEEN %s AND %s 1699 AND pv.scroll_depth > 0 1700 GROUP BY DATE(pv.view_time) 1701 ORDER BY date ASC", 1702 $start_date, 1703 $end_date 1704 ), 1705 ARRAY_A 1706 ); 1707 1708 foreach ( $scroll_depth_data as $row ) { 1709 $daily_data['avg_scroll_depth'][ $row['date'] ] = round( floatval( $row['avg_depth'] ), 1 ); 1710 } 1711 1712 // Get daily bounce rate 1713 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1714 $bounce_rate_data = $wpdb->get_results( 1715 $wpdb->prepare( 1716 "SELECT 1717 DATE(start_time) as date, 1718 SUM(CASE WHEN is_bounce = 1 THEN 1 ELSE 0 END) as bounces, 1719 COUNT(*) as total 1720 FROM {$wpdb->prefix}optibehavior_sessions 1721 WHERE start_time BETWEEN %s AND %s 1722 GROUP BY DATE(start_time) 1723 ORDER BY date ASC", 1724 $start_date, 1725 $end_date 1726 ), 1727 ARRAY_A 1728 ); 1729 1730 foreach ( $bounce_rate_data as $row ) { 1731 $total = intval( $row['total'] ); 1732 $bounces = intval( $row['bounces'] ); 1733 $daily_data['bounce_rate'][ $row['date'] ] = $total > 0 ? round( ( $bounces / $total ) * 100, 1 ) : 0; 1734 } 1735 1736 // Convert associative arrays to indexed arrays for easier JS consumption 1737 $result = array( 1738 'dates' => $daily_data['dates'], 1739 'visitors' => array_values( $daily_data['visitors'] ), 1740 'sessions' => array_values( $daily_data['sessions'] ), 1741 'pageviews' => array_values( $daily_data['pageviews'] ), 1742 'avg_session_time' => array_values( $daily_data['avg_session_time'] ), 1743 'avg_scroll_depth' => array_values( $daily_data['avg_scroll_depth'] ), 1744 'bounce_rate' => array_values( $daily_data['bounce_rate'] ), 1745 ); 1746 1747 return $result; 1748 } 1562 1749 } 1563 1750 } -
opti-behavior/trunk/includes/trait-opti-behavior-sessions-views.php
r3401441 r3414697 176 176 </div> 177 177 <div class="visitor-flag"> 178 <span class="flag-icon"><?php echo esc_html( $session['flag'] );?></span>178 <span class="flag-icon"><?php echo $session['flag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> 179 179 </div> 180 180 </div> -
opti-behavior/trunk/includes/trait-opti-behavior-settings-views.php
r3408429 r3414697 1706 1706 'low_intent' => array( 1707 1707 'time_spent' => isset( $intent_data['low_intent']['time_spent'] ) ? absint( $intent_data['low_intent']['time_spent'] ) : 10, 1708 'clicks' => isset( $intent_data['low_intent']['clicks'] ) ? absint( $intent_data['low_intent']['clicks'] ) : 1,1709 'scroll_depth' => isset( $intent_data['low_intent']['scroll_depth'] ) ? absint( $intent_data['low_intent']['scroll_depth'] ) : 25,1708 'clicks' => isset( $intent_data['low_intent']['clicks'] ) ? absint( $intent_data['low_intent']['clicks'] ) : 0, 1709 'scroll_depth' => isset( $intent_data['low_intent']['scroll_depth'] ) ? absint( $intent_data['low_intent']['scroll_depth'] ) : 0, 1710 1710 ), 1711 1711 'medium_intent' => array( 1712 1712 'time_spent' => isset( $intent_data['medium_intent']['time_spent'] ) ? absint( $intent_data['medium_intent']['time_spent'] ) : 30, 1713 'clicks' => isset( $intent_data['medium_intent']['clicks'] ) ? absint( $intent_data['medium_intent']['clicks'] ) : 3,1714 'scroll_depth' => isset( $intent_data['medium_intent']['scroll_depth'] ) ? absint( $intent_data['medium_intent']['scroll_depth'] ) : 50,1713 'clicks' => isset( $intent_data['medium_intent']['clicks'] ) ? absint( $intent_data['medium_intent']['clicks'] ) : 1, 1714 'scroll_depth' => isset( $intent_data['medium_intent']['scroll_depth'] ) ? absint( $intent_data['medium_intent']['scroll_depth'] ) : 15, 1715 1715 ), 1716 1716 'high_intent' => array( 1717 'time_spent' => isset( $intent_data['high_intent']['time_spent'] ) ? absint( $intent_data['high_intent']['time_spent'] ) : 60,1718 'clicks' => isset( $intent_data['high_intent']['clicks'] ) ? absint( $intent_data['high_intent']['clicks'] ) : 5,1719 'scroll_depth' => isset( $intent_data['high_intent']['scroll_depth'] ) ? absint( $intent_data['high_intent']['scroll_depth'] ) : 75,1717 'time_spent' => isset( $intent_data['high_intent']['time_spent'] ) ? absint( $intent_data['high_intent']['time_spent'] ) : 45, 1718 'clicks' => isset( $intent_data['high_intent']['clicks'] ) ? absint( $intent_data['high_intent']['clicks'] ) : 2, 1719 'scroll_depth' => isset( $intent_data['high_intent']['scroll_depth'] ) ? absint( $intent_data['high_intent']['scroll_depth'] ) : 50, 1720 1720 ), 1721 1721 ); … … 1898 1898 'low_intent' => array( 1899 1899 'time_spent' => 10, 1900 'clicks' => 1,1901 'scroll_depth' => 25,1900 'clicks' => 0, 1901 'scroll_depth' => 0, 1902 1902 ), 1903 1903 'medium_intent' => array( 1904 1904 'time_spent' => 30, 1905 'clicks' => 3,1906 'scroll_depth' => 50,1905 'clicks' => 1, 1906 'scroll_depth' => 15, 1907 1907 ), 1908 1908 'high_intent' => array( 1909 'time_spent' => 60,1910 'clicks' => 5,1911 'scroll_depth' => 75,1909 'time_spent' => 45, 1910 'clicks' => 2, 1911 'scroll_depth' => 50, 1912 1912 ), 1913 1913 ) ); … … 1920 1920 <?php wp_nonce_field( 'opti_behavior_intent_rules_submit', 'opti_behavior_intent_nonce' ); ?> 1921 1921 1922 <p class="category-description"><?php esc_html_e( 'Define rules for classifying user intent levels based on engagement metrics. ', 'opti-behavior' ); ?></p>1922 <p class="category-description"><?php esc_html_e( 'Define rules for classifying user intent levels based on engagement metrics. A visitor is classified as high/medium intent if they meet at least 2 out of 3 criteria below.', 'opti-behavior' ); ?></p> 1923 1923 1924 1924 <!-- Low Intent Rules --> … … 1935 1935 value="<?php echo esc_attr( isset( $low_intent['time_spent'] ) ? $low_intent['time_spent'] : 10 ); ?>" 1936 1936 min="0" max="3600" step="1" class="small-text" /> 1937 <p class="description"><?php esc_html_e( ' Maximum time spent on page for low intent classification.', 'opti-behavior' ); ?></p>1937 <p class="description"><?php esc_html_e( 'Threshold for time spent consideration. Values below this are counted toward low intent.', 'opti-behavior' ); ?></p> 1938 1938 </td> 1939 1939 </tr> … … 1942 1942 <td> 1943 1943 <input type="number" name="opti_behavior_intent[low_intent][clicks]" 1944 value="<?php echo esc_attr( isset( $low_intent['clicks'] ) ? $low_intent['clicks'] : 1); ?>"1944 value="<?php echo esc_attr( isset( $low_intent['clicks'] ) ? $low_intent['clicks'] : 0 ); ?>" 1945 1945 min="0" max="100" step="1" class="small-text" /> 1946 <p class="description"><?php esc_html_e( ' Maximum number of clicks for low intent classification.', 'opti-behavior' ); ?></p>1946 <p class="description"><?php esc_html_e( 'Threshold for clicks consideration. Values below this are counted toward low intent.', 'opti-behavior' ); ?></p> 1947 1947 </td> 1948 1948 </tr> … … 1951 1951 <td> 1952 1952 <input type="number" name="opti_behavior_intent[low_intent][scroll_depth]" 1953 value="<?php echo esc_attr( isset( $low_intent['scroll_depth'] ) ? $low_intent['scroll_depth'] : 25); ?>"1953 value="<?php echo esc_attr( isset( $low_intent['scroll_depth'] ) ? $low_intent['scroll_depth'] : 0 ); ?>" 1954 1954 min="0" max="100" step="1" class="small-text" /> 1955 <p class="description"><?php esc_html_e( ' Maximum scroll depth percentage for low intent classification.', 'opti-behavior' ); ?></p>1955 <p class="description"><?php esc_html_e( 'Threshold for scroll depth consideration. Values below this are counted toward low intent.', 'opti-behavior' ); ?></p> 1956 1956 </td> 1957 1957 </tr> … … 1972 1972 value="<?php echo esc_attr( isset( $medium_intent['time_spent'] ) ? $medium_intent['time_spent'] : 30 ); ?>" 1973 1973 min="0" max="3600" step="1" class="small-text" /> 1974 <p class="description"><?php esc_html_e( ' Minimum time spent on page for medium intent classification.', 'opti-behavior' ); ?></p>1974 <p class="description"><?php esc_html_e( 'Threshold for time spent. Meeting 2 out of 3 medium thresholds = medium intent.', 'opti-behavior' ); ?></p> 1975 1975 </td> 1976 1976 </tr> … … 1979 1979 <td> 1980 1980 <input type="number" name="opti_behavior_intent[medium_intent][clicks]" 1981 value="<?php echo esc_attr( isset( $medium_intent['clicks'] ) ? $medium_intent['clicks'] : 3); ?>"1981 value="<?php echo esc_attr( isset( $medium_intent['clicks'] ) ? $medium_intent['clicks'] : 1 ); ?>" 1982 1982 min="0" max="100" step="1" class="small-text" /> 1983 <p class="description"><?php esc_html_e( ' Minimum number of clicks for medium intent classification.', 'opti-behavior' ); ?></p>1983 <p class="description"><?php esc_html_e( 'Threshold for clicks. Meeting 2 out of 3 medium thresholds = medium intent.', 'opti-behavior' ); ?></p> 1984 1984 </td> 1985 1985 </tr> … … 1988 1988 <td> 1989 1989 <input type="number" name="opti_behavior_intent[medium_intent][scroll_depth]" 1990 value="<?php echo esc_attr( isset( $medium_intent['scroll_depth'] ) ? $medium_intent['scroll_depth'] : 50); ?>"1990 value="<?php echo esc_attr( isset( $medium_intent['scroll_depth'] ) ? $medium_intent['scroll_depth'] : 15 ); ?>" 1991 1991 min="0" max="100" step="1" class="small-text" /> 1992 <p class="description"><?php esc_html_e( ' Minimum scroll depth percentage for medium intent classification.', 'opti-behavior' ); ?></p>1992 <p class="description"><?php esc_html_e( 'Threshold for scroll depth. Meeting 2 out of 3 medium thresholds = medium intent.', 'opti-behavior' ); ?></p> 1993 1993 </td> 1994 1994 </tr> … … 2007 2007 <td> 2008 2008 <input type="number" name="opti_behavior_intent[high_intent][time_spent]" 2009 value="<?php echo esc_attr( isset( $high_intent['time_spent'] ) ? $high_intent['time_spent'] : 60); ?>"2009 value="<?php echo esc_attr( isset( $high_intent['time_spent'] ) ? $high_intent['time_spent'] : 45 ); ?>" 2010 2010 min="0" max="3600" step="1" class="small-text" /> 2011 <p class="description"><?php esc_html_e( ' Minimum time spent on page for high intent classification.', 'opti-behavior' ); ?></p>2011 <p class="description"><?php esc_html_e( 'Threshold for time spent. Meeting 2 out of 3 high thresholds = high intent.', 'opti-behavior' ); ?></p> 2012 2012 </td> 2013 2013 </tr> … … 2016 2016 <td> 2017 2017 <input type="number" name="opti_behavior_intent[high_intent][clicks]" 2018 value="<?php echo esc_attr( isset( $high_intent['clicks'] ) ? $high_intent['clicks'] : 5); ?>"2018 value="<?php echo esc_attr( isset( $high_intent['clicks'] ) ? $high_intent['clicks'] : 2 ); ?>" 2019 2019 min="0" max="100" step="1" class="small-text" /> 2020 <p class="description"><?php esc_html_e( ' Minimum number of clicks for high intent classification.', 'opti-behavior' ); ?></p>2020 <p class="description"><?php esc_html_e( 'Threshold for clicks. Meeting 2 out of 3 high thresholds = high intent.', 'opti-behavior' ); ?></p> 2021 2021 </td> 2022 2022 </tr> … … 2025 2025 <td> 2026 2026 <input type="number" name="opti_behavior_intent[high_intent][scroll_depth]" 2027 value="<?php echo esc_attr( isset( $high_intent['scroll_depth'] ) ? $high_intent['scroll_depth'] : 75); ?>"2027 value="<?php echo esc_attr( isset( $high_intent['scroll_depth'] ) ? $high_intent['scroll_depth'] : 50 ); ?>" 2028 2028 min="0" max="100" step="1" class="small-text" /> 2029 <p class="description"><?php esc_html_e( ' Minimum scroll depth percentage for high intent classification.', 'opti-behavior' ); ?></p>2029 <p class="description"><?php esc_html_e( 'Threshold for scroll depth. Meeting 2 out of 3 high thresholds = high intent.', 'opti-behavior' ); ?></p> 2030 2030 </td> 2031 2031 </tr> -
opti-behavior/trunk/readme.txt
r3408438 r3414697 1 === Opti-Behavior ===1 === Opti-Behavior - Behavior Analytics That Grows Your Business === 2 2 Contributors: optiuser 3 3 Donate link: https://optiuser.com/ … … 5 5 6 6 Requires at least: 5.8 7 Tested up to: 6. 87 Tested up to: 6.9 8 8 Requires PHP: 7.4 9 Stable tag: 1.0. 79 Stable tag: 1.0.8 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 243 243 244 244 == Changelog == 245 246 = 1.0.8 - 2025-12-08 = 247 * Feature: User Intent Rules - Advanced system for analyzing and categorizing user behavior patterns 248 * Enhancement: Analytics Dashboard time filter now defaults to 30 Days for better data overview 249 * Fix: Improved favicon handling for referrer websites with proper fallback support 245 250 246 251 = 1.0.7 - 2025-12-02 = … … 326 331 == Upgrade Notice == 327 332 333 = 1.0.8 = 334 New User Intent Rules feature for behavior analysis, Analytics Dashboard now defaults to 30-day view, and improved favicon handling for referrer websites. 335 328 336 = 1.0.7 = 329 337 Important update with French translations, visitor tracking improvements, and username display in Top Engaged Users. Enhanced coding standards compliance and debug logging controls. -
opti-behavior/trunk/views/dashboard-views.php
r3408429 r3414697 66 66 * @param string $period Selected period. 67 67 */ 68 function opti_behavior_views_render_dashboard_header( $self, $date_range = null, $period = 'last 7days' ) {68 function opti_behavior_views_render_dashboard_header( $self, $date_range = null, $period = 'last30days' ) { 69 69 $start_val = $date_range && isset( $date_range['start'] ) ? substr( $date_range['start'], 0, 10 ) : ''; 70 70 $end_val = $date_range && isset( $date_range['end'] ) ? substr( $date_range['end'], 0, 10 ) : ''; … … 128 128 <div class="stat-label"><?php echo esc_html__( 'Visitors', 'opti-behavior' ); ?></div> 129 129 <div class="stat-change <?php echo esc_attr( $cls( $changes['visitors'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['visitors'] ?? 0 ) ); ?></div> 130 <div class="stat-history"> 131 <canvas id="history-visitors" width="280" height="50"></canvas> 132 </div> 130 133 </div> 131 134 </div> … … 136 139 <div class="stat-label"><?php echo esc_html__( 'Sessions', 'opti-behavior' ); ?></div> 137 140 <div class="stat-change <?php echo esc_attr( $cls( $changes['sessions'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['sessions'] ?? 0 ) ); ?></div> 141 <div class="stat-history"> 142 <canvas id="history-sessions" width="280" height="50"></canvas> 143 </div> 138 144 </div> 139 145 </div> … … 144 150 <div class="stat-label"><?php echo esc_html__( 'Page Views', 'opti-behavior' ); ?></div> 145 151 <div class="stat-change <?php echo esc_attr( $cls( $changes['pageviews'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['pageviews'] ?? 0 ) ); ?></div> 152 <div class="stat-history"> 153 <canvas id="history-pageviews" width="280" height="50"></canvas> 154 </div> 146 155 </div> 147 156 </div> … … 157 166 <div class="stat-label"><?php echo esc_html__( 'Avg. Session Time', 'opti-behavior' ); ?></div> 158 167 <div class="stat-change <?php echo esc_attr( $cls( $changes['avg_session_time'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['avg_session_time'] ?? 0 ) ); ?></div> 168 <div class="stat-history"> 169 <canvas id="history-avg-session-time" width="280" height="50"></canvas> 170 </div> 159 171 </div> 160 172 </div> … … 165 177 <div class="stat-label"><?php echo esc_html__( 'Avg. Scroll Depth', 'opti-behavior' ); ?></div> 166 178 <div class="stat-change <?php echo esc_attr( $cls( $changes['avg_scroll_depth'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['avg_scroll_depth'] ?? 0 ) ); ?></div> 179 <div class="stat-history"> 180 <canvas id="history-avg-scroll-depth" width="280" height="50"></canvas> 181 </div> 167 182 </div> 168 183 </div> … … 173 188 <div class="stat-label"><?php echo esc_html__( 'Bounce Rate', 'opti-behavior' ); ?></div> 174 189 <div class="stat-change <?php echo esc_attr( $cls( $changes['bounce_rate'] ?? 0 ) ); ?>"><?php echo esc_html( $fmt( $changes['bounce_rate'] ?? 0 ) ); ?></div> 190 <div class="stat-history"> 191 <canvas id="history-bounce-rate" width="280" height="50"></canvas> 192 </div> 175 193 </div> 176 194 </div> … … 309 327 <div class="visitor-item grid"> 310 328 <span class="visitor-datetime"><?php echo esc_html( $visitor['visited_at'] ?? '' ); ?><?php if ( ! empty( $visitor['time_ago'] ) ) : ?> - <span class="ago"><?php echo esc_html( $visitor['time_ago'] ); ?></span><?php endif; ?></span> 311 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo esc_html( $visitor['flag'] );?></span> <span class="visitor-location"><?php echo esc_html( $visitor['country'] ); ?></span></span>329 <span class="visitor-flag-country"><span class="visitor-flag"><?php echo $visitor['flag']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span> <span class="visitor-location"><?php echo esc_html( $visitor['country'] ); ?></span></span> 312 330 <span class="visitor-pageblock"><span class="visitor-title"><?php echo esc_html( $visitor['page_title'] ?? '' ); ?></span><a class="visitor-url" href="<?php echo esc_url( $visitor['current_url'] ?? '' ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $visitor['current_url'] ?? '' ); ?></a></span> 313 331 <span class="visitor-ip-col"><span class="visitor-ip-pill"> -
opti-behavior/trunk/views/sessions-views.php
r3399182 r3414697 108 108 <td><code><?php echo esc_html( $r['visitor_short'] ?? $r['visitor_id'] ); ?></code></td> 109 109 <td><?php echo esc_html( $r['device'] ?? '' ); ?></td> 110 <td><?php echo esc_html( ( $r['country_flag'] ?? '' ) . ' ' . ( $r['country'] ?? '' )); ?></td>110 <td><?php echo esc_html( $r['country_flag'] ?? '' ) . ' ' . esc_html( $r['country'] ?? '' ); ?></td> 111 111 <td class="column-numeric"><?php echo esc_html( $r['duration_human'] ?? '' ); ?></td> 112 112 <td class="column-numeric"><?php echo (int) ( $r['pages'] ?? 0 ); ?></td>
Note: See TracChangeset
for help on using the changeset viewer.