PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 4.4.3
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v4.4.3
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Includes / Classes / Analytics / Data_Collector.php
embedpress / EmbedPress / Includes / Classes / Analytics Last commit date
Analytics_Manager.php 8 months ago Browser_Detector.php 9 months ago Content_Cache_Manager.php 9 months ago Data_Collector.php 9 months ago Email_Reports.php 9 months ago Export_Manager.php 9 months ago License_Manager.php 9 months ago Milestone_Manager.php 9 months ago Pro_Data_Collector.php 9 months ago REST_API.php 9 months ago
Data_Collector.php
2919 lines
1 <?php
2
3 namespace EmbedPress\Includes\Classes\Analytics;
4
5 use EmbedPress\Includes\Classes\Database\Analytics_Schema;
6 use EmbedPress\Includes\Classes\Analytics\License_Manager;
7
8 defined('ABSPATH') or die("No direct script access allowed.");
9
10 /**
11 * EmbedPress Analytics Data Collector
12 *
13 * Handles data collection and storage for analytics
14 *
15 * @package EmbedPress
16 * @author EmbedPress <help@embedpress.com>
17 * @copyright Copyright (C) 2023 WPDeveloper. All rights reserved.
18 * @license GPLv3 or later
19 * @since 4.2.7
20 */
21 class Data_Collector
22 {
23 private $license_manager;
24 private $pro_collector;
25
26 public function __construct()
27 {
28 $this->license_manager = new License_Manager();
29 if ($this->license_manager->has_pro_license()) {
30 $this->pro_collector = new Pro_Data_Collector();
31 }
32 }
33
34 /**
35 * Build date condition for SQL queries
36 *
37 * @param array $args
38 * @param string $date_column
39 * @return string
40 */
41 private function build_date_condition($args = [], $date_column = 'created_at')
42 {
43 global $wpdb;
44
45 $date_condition = '';
46
47 // Check if specific start_date and end_date are provided
48 if (!empty($args['start_date']) && !empty($args['end_date'])) {
49 $start_date = sanitize_text_field($args['start_date']);
50 $end_date = sanitize_text_field($args['end_date']);
51
52 // Validate date format (YYYY-MM-DD)
53 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
54 $date_condition = $wpdb->prepare(
55 "AND DATE($date_column) BETWEEN %s AND %s",
56 $start_date,
57 $end_date
58 );
59 }
60 } else {
61 // Fall back to date_range (number of days)
62 $date_range = isset($args['date_range']) ? absint($args['date_range']) : 30;
63
64 if ($date_range > 0) {
65 $date_condition = $wpdb->prepare(
66 "AND $date_column >= DATE_SUB(NOW(), INTERVAL %d DAY)",
67 $date_range
68 );
69 }
70 }
71 return $date_condition;
72 }
73
74 /**
75 * Track content creation
76 *
77 * @param string $content_id
78 * @param string $content_type
79 * @param array $data
80 * @return bool
81 */
82 public function track_content_creation($content_id, $content_type, $data = [])
83 {
84 global $wpdb;
85
86 $table_name = $wpdb->prefix . 'embedpress_analytics_content';
87
88 $insert_data = [
89 'content_id' => sanitize_text_field($content_id),
90 'content_type' => sanitize_text_field($content_type),
91 'embed_type' => isset($data['embed_type']) ? sanitize_text_field($data['embed_type']) : '',
92 'embed_url' => isset($data['embed_url']) ? esc_url_raw($data['embed_url']) : '',
93 'post_id' => isset($data['post_id']) ? absint($data['post_id']) : get_the_ID(),
94 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : get_permalink(),
95 'title' => isset($data['title']) ? sanitize_text_field($data['title']) : get_the_title(),
96 'created_at' => current_time('mysql'),
97 'updated_at' => current_time('mysql')
98 ];
99
100 // Use INSERT ... ON DUPLICATE KEY UPDATE to handle existing content
101 $sql = "INSERT INTO $table_name (content_id, content_type, embed_type, embed_url, post_id, page_url, title, created_at, updated_at)
102 VALUES (%s, %s, %s, %s, %d, %s, %s, %s, %s)
103 ON DUPLICATE KEY UPDATE
104 embed_type = VALUES(embed_type),
105 embed_url = VALUES(embed_url),
106 post_id = VALUES(post_id),
107 page_url = VALUES(page_url),
108 title = VALUES(title),
109 updated_at = VALUES(updated_at)";
110
111 $result = $wpdb->query($wpdb->prepare(
112 $sql,
113 $insert_data['content_id'],
114 $insert_data['content_type'],
115 $insert_data['embed_type'],
116 $insert_data['embed_url'],
117 $insert_data['post_id'],
118 $insert_data['page_url'],
119 $insert_data['title'],
120 $insert_data['created_at'],
121 $insert_data['updated_at']
122 ));
123
124 return $result !== false;
125 }
126
127 /**
128 * Track interaction (view, click, impression, etc.)
129 * OPTIMIZED: One row per user per content, update counters instead of creating multiple rows
130 *
131 * @param array $data
132 * @return bool
133 */
134 public function track_interaction($data)
135 {
136 global $wpdb;
137
138 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
139 $content_id = sanitize_text_field($data['content_id']);
140 $interaction_type = sanitize_text_field($data['interaction_type']);
141 $session_id = sanitize_text_field($data['session_id']);
142
143 // Get user identifier - prefer user_id from localStorage, fallback to session
144 $user_identifier = isset($data['user_id']) && !empty($data['user_id']) && $data['user_id'] !== 'null'
145 ? sanitize_text_field($data['user_id'])
146 : $session_id;
147
148 // Track referrer analytics for external referrers
149 $this->track_referrer_from_interaction_data($data, $interaction_type, $user_identifier);
150
151 // Look for existing record for this user+content combination
152 // Use session_id field to store our user_identifier for backwards compatibility
153 $existing_record = $wpdb->get_row($wpdb->prepare(
154 "SELECT * FROM $views_table
155 WHERE session_id = %s AND content_id = %s
156 ORDER BY created_at DESC LIMIT 1",
157 $user_identifier,
158 $content_id
159 ));
160
161
162 if ($existing_record) {
163 // Update existing record - increment counters instead of creating new rows
164 return $this->update_interaction_counters($existing_record, $interaction_type, $data);
165
166 } else {
167 // Create new record for this user+content combination
168 return $this->create_optimized_interaction_record($data, $user_identifier);
169 }
170 }
171
172 /**
173 * Create optimized interaction record with counters
174 */
175 private function create_optimized_interaction_record($data, $user_identifier)
176 {
177 global $wpdb;
178
179 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
180 $content_id = sanitize_text_field($data['content_id']);
181 $interaction_type = sanitize_text_field($data['interaction_type']);
182
183 // Get referrer URL - use the original referrer captured on first server request
184 $referrer_url = '';
185
186 // Priority 1: Use the original referrer captured in main plugin file
187 if (defined('EMBEDPRESS_ORIGINAL_REFERRER') && !empty(EMBEDPRESS_ORIGINAL_REFERRER)) {
188 $referrer_url = esc_url_raw(EMBEDPRESS_ORIGINAL_REFERRER);
189 }
190
191
192 // Priority 2 (now client-side referrer from JavaScript)
193 if (empty($referrer_url) && isset($data['original_referrer']) && !empty($data['original_referrer'])) {
194 $current_site_url = home_url();
195 $client_referrer = esc_url_raw($data['original_referrer']);
196 // Only use if it's external
197 if (strpos($client_referrer, $current_site_url) !== 0) {
198 $referrer_url = $client_referrer;
199 }
200 }
201
202 // Initialize counters based on interaction type
203 $interaction_data = [
204 'content_id' => $content_id,
205 'user_identifier' => $user_identifier,
206 'user_ip' => $this->get_user_ip(),
207 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
208 'referrer_url' => $referrer_url,
209 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
210 'interaction_data' => isset($data['interaction_data']) ? wp_json_encode($data['interaction_data']) : null,
211 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
212 'created_at' => current_time('mysql'),
213 // Initialize counters
214 'view_count' => $interaction_type === 'view' ? 1 : 0,
215 'click_count' => $interaction_type === 'click' ? 1 : 0,
216 'impression_count' => $interaction_type === 'impression' ? 1 : 0,
217 'first_' . $interaction_type . '_at' => current_time('mysql'),
218 'last_' . $interaction_type . '_at' => current_time('mysql')
219 ];
220
221 $result = $wpdb->insert($views_table, [
222 'content_id' => $content_id,
223 'session_id' => $user_identifier, // Store user_identifier in session_id field
224 'interaction_type' => 'combined', // Mark as combined record
225 'interaction_data' => wp_json_encode($interaction_data),
226 'user_ip' => $this->get_user_ip(),
227 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
228 'referrer_url' => $referrer_url,
229 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
230 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
231 'created_at' => current_time('mysql')
232 ]);
233
234 if ($result) {
235 // Update content table counters
236 $this->update_content_counters($content_id, $interaction_type, $data['interaction_data'] ?? [], $data['page_url'] ?? '');
237 }
238
239 return $result !== false;
240 }
241
242
243 /**
244 * Update interaction counters for existing record
245 */
246 private function update_interaction_counters($existing_record, $interaction_type, $data)
247 {
248 global $wpdb;
249
250 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
251
252 // Parse existing interaction data
253 $interaction_data = json_decode($existing_record->interaction_data, true) ?: [];
254
255 // Initialize counters if they don't exist
256 if (!isset($interaction_data['view_count'])) $interaction_data['view_count'] = 0;
257 if (!isset($interaction_data['click_count'])) $interaction_data['click_count'] = 0;
258 if (!isset($interaction_data['impression_count'])) $interaction_data['impression_count'] = 0;
259
260 // Update counter based on interaction type
261 switch ($interaction_type) {
262 case 'view':
263 $interaction_data['view_count']++;
264 $interaction_data['last_view_at'] = current_time('mysql');
265 break;
266 case 'click':
267 $interaction_data['click_count']++;
268 $interaction_data['last_click_at'] = current_time('mysql');
269 break;
270 case 'impression':
271 $interaction_data['impression_count']++;
272 $interaction_data['last_impression_at'] = current_time('mysql');
273 break;
274 }
275
276 // Update view duration if provided
277 if (isset($data['view_duration']) && $data['view_duration'] > 0) {
278 $interaction_data['total_view_duration'] = ($interaction_data['total_view_duration'] ?? 0) + absint($data['view_duration']);
279 }
280
281 // Update the record
282 $result = $wpdb->update(
283 $views_table,
284 [
285 'interaction_data' => wp_json_encode($interaction_data),
286 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : $existing_record->view_duration
287 ],
288 ['id' => $existing_record->id]
289 );
290
291 if ($result !== false) {
292 // Update content table counters
293 $this->update_content_counters($existing_record->content_id, $interaction_type, $data['interaction_data'] ?? [], $data['page_url'] ?? '');
294 }
295
296 return $result !== false;
297 }
298
299 /**
300 * Create new interaction record
301 */
302 private function create_new_interaction_record($data)
303 {
304 global $wpdb;
305
306 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
307 $content_id = sanitize_text_field($data['content_id']);
308 $interaction_type = sanitize_text_field($data['interaction_type']);
309
310 // Get user identifier - prefer user_id from localStorage, fallback to session
311 $user_id = isset($data['user_id']) && !empty($data['user_id']) && $data['user_id'] !== 'null'
312 ? sanitize_text_field($data['user_id'])
313 : null;
314
315 // Get referrer URL from HTTP_REFERER only
316 $referrer_url = '';
317 if (!empty($_SERVER['HTTP_REFERER'])) {
318 $current_site_url = home_url();
319 $http_referrer = esc_url_raw($_SERVER['HTTP_REFERER']);
320 // Only capture if it's from external source
321 if (strpos($http_referrer, $current_site_url) !== 0) {
322 $referrer_url = $http_referrer;
323 }
324 }
325
326 $interaction_data = [
327 'content_id' => $content_id,
328 'user_id' => $user_id,
329 'session_id' => sanitize_text_field($data['session_id']),
330 'user_ip' => $this->get_user_ip(),
331 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
332 'referrer_url' => $referrer_url,
333 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
334 'interaction_type' => $interaction_type,
335 'interaction_data' => isset($data['interaction_data']) ? wp_json_encode($data['interaction_data']) : null,
336 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
337 'created_at' => current_time('mysql')
338 ];
339
340 $result = $wpdb->insert($views_table, $interaction_data);
341
342 if ($result) {
343 // Update content table counters with additional data
344 $interaction_data = isset($data['interaction_data']) ? $data['interaction_data'] : [];
345 // If interaction_data is a JSON string, decode it; otherwise use as-is
346 if (is_string($interaction_data)) {
347 $interaction_data = json_decode($interaction_data, true) ?: [];
348 }
349 $page_url = isset($data['page_url']) ? $data['page_url'] : '';
350 $this->update_content_counters($data['content_id'], $data['interaction_type'], $interaction_data, $page_url);
351
352 // Browser info is now stored from frontend via REST API
353 // $this->store_browser_info($data['session_id']);
354 }
355
356 return $result !== false;
357 }
358
359 /**
360 * Update content counters
361 *
362 * @param string $content_id
363 * @param string $interaction_type
364 * @param array $interaction_data Additional data from the interaction
365 * @param string $page_url The page URL where the interaction occurred
366 * @return void
367 */
368 private function update_content_counters($content_id, $interaction_type, $interaction_data = [], $page_url = '')
369 {
370 global $wpdb;
371
372 $table_name = $wpdb->prefix . 'embedpress_analytics_content';
373
374 $counter_field = '';
375 switch ($interaction_type) {
376 case 'view':
377 $counter_field = 'total_views';
378 break;
379 case 'click':
380 $counter_field = 'total_clicks';
381 break;
382 case 'impression':
383 $counter_field = 'total_impressions';
384 break;
385 default:
386 return;
387 }
388
389 // Extract content information first to get embed_type and page_url
390 $content_info = $this->extract_content_info($content_id, $interaction_data, $page_url);
391
392 // Skip tracking if embed type cannot be determined
393 if ($content_info === null) {
394 return;
395 }
396
397 // Keep original embed_type name without transformation
398 $embed_type = $content_info['embed_type'];
399
400 // Check if content record exists based on page_url + embed_type (not content_id)
401 $content_exists = $wpdb->get_var($wpdb->prepare(
402 "SELECT id FROM $table_name WHERE page_url = %s AND embed_type = %s",
403 $page_url,
404 $embed_type
405 ));
406
407 if ($content_exists) {
408 // Update existing record
409 $sql = "UPDATE $table_name SET $counter_field = $counter_field + 1, updated_at = %s WHERE page_url = %s AND embed_type = %s";
410 $wpdb->query($wpdb->prepare($sql, current_time('mysql'), $page_url, $embed_type));
411 } else {
412 // Create new record with the counter set to 1 (content_info already extracted above)
413 $insert_data = [
414 'content_id' => $content_id,
415 'content_type' => $content_info['content_type'],
416 'embed_type' => $embed_type, // Use original embed_type without transformation
417 'embed_url' => $content_info['embed_url'],
418 'post_id' => $content_info['post_id'],
419 'page_url' => $page_url,
420 'title' => $content_info['title'],
421 'total_views' => $interaction_type === 'view' ? 1 : 0,
422 'total_clicks' => $interaction_type === 'click' ? 1 : 0,
423 'total_impressions' => $interaction_type === 'impression' ? 1 : 0,
424 'created_at' => current_time('mysql'),
425 'updated_at' => current_time('mysql')
426 ];
427
428 $wpdb->insert($table_name, $insert_data);
429 }
430 }
431
432 /**
433 * Extract content information from content ID and interaction data
434 *
435 * @param string $content_id
436 * @param array $interaction_data
437 * @param string $page_url
438 * @return array|null Returns null if embed_type cannot be determined (to skip tracking)
439 */
440 private function extract_content_info($content_id, $interaction_data = [], $page_url = '')
441 {
442
443
444 // Default values
445 $content_info = [
446 'content_type' => $this->detect_content_type($page_url, $interaction_data), // This represents how content was embedded (elementor/gutenberg/shortcode)
447 'embed_type' => 'unknown', // This is the source type (youtube, vimeo, pdf, etc.)
448 'embed_url' => '',
449 'post_id' => null,
450 'title' => 'Unknown Page' // This should be the page title, not content title
451 ];
452
453 // Extract embed type (source type) from interaction data (most reliable)
454 if (!empty($interaction_data['embed_type']) && $interaction_data['embed_type'] !== 'unknown') {
455 $content_info['embed_type'] = sanitize_text_field($interaction_data['embed_type']); // Keep original case
456 }
457
458 // Extract embed URL from interaction data
459 if (!empty($interaction_data['embed_url'])) {
460 $content_info['embed_url'] = esc_url_raw($interaction_data['embed_url']);
461 }
462
463 // The title should be the page title, not the embed title
464 // We'll get this from the page_url or post_id
465
466 // Extract information from content ID patterns if embed_type is still unknown
467 if ($content_info['embed_type'] === 'unknown') {
468 $content_id_info = $this->analyze_content_id($content_id);
469 if ($content_id_info['embed_type'] !== 'unknown') {
470 $content_info['embed_type'] = $content_id_info['embed_type']; // Keep original case
471 }
472 if (!empty($content_id_info['embed_url'])) {
473 $content_info['embed_url'] = $content_id_info['embed_url'];
474 }
475 }
476
477 // Try to detect from embed URL if still unknown
478 if ($content_info['embed_type'] === 'unknown' && !empty($content_info['embed_url'])) {
479 $url_detected_type = $this->detect_embed_type_from_url($content_info['embed_url']);
480 if ($url_detected_type !== 'unknown') {
481 $content_info['embed_type'] = $url_detected_type;
482 }
483 }
484
485 // If we still can't determine the embed type, return null to skip tracking
486 if ($content_info['embed_type'] === 'unknown') {
487 return null;
488 }
489
490 // Try to extract post ID from page URL
491 if (!empty($page_url)) {
492 $content_info['post_id'] = $this->extract_post_id_from_url($page_url);
493 }
494
495 // Get the page title (this is what should be displayed as "Content")
496 $content_info['title'] = $this->get_page_title($content_info['post_id'], $page_url);
497
498 return $content_info;
499 }
500
501 /**
502 * Detect content type (platform) based on page URL and interaction data
503 *
504 * @param string $page_url
505 * @param array $interaction_data
506 * @return string
507 */
508 private function detect_content_type($page_url = '', $interaction_data = [])
509 {
510 // Check interaction data for platform hints (most reliable)
511 if (!empty($interaction_data['platform']) && $interaction_data['platform'] !== 'unknown') {
512 return sanitize_text_field($interaction_data['platform']);
513 }
514
515 // Try to detect from page content if we have a post ID
516 if (!empty($page_url)) {
517 $post_id = $this->extract_post_id_from_url($page_url);
518 if ($post_id) {
519 // Check for Elementor first (most specific)
520 if (
521 get_post_meta($post_id, '_elementor_edit_mode', true) ||
522 get_post_meta($post_id, '_elementor_data', true)
523 ) {
524 return 'elementor';
525 }
526
527 $post_content = get_post_field('post_content', $post_id);
528 if ($post_content) {
529 // Check for Elementor in content
530 if (
531 strpos($post_content, 'elementor') !== false ||
532 strpos($post_content, 'data-widget_type') !== false ||
533 strpos($post_content, 'data-element_type') !== false
534 ) {
535 return 'elementor';
536 }
537
538 // Check for Gutenberg blocks (more specific patterns)
539 if (
540 strpos($post_content, '<!-- wp:embedpress/') !== false ||
541 strpos($post_content, 'wp:embedpress/') !== false ||
542 (strpos($post_content, '<!-- wp:') !== false && strpos($post_content, 'embedpress') !== false)
543 ) {
544 return 'gutenberg';
545 }
546
547 // Check for shortcodes
548 if (
549 strpos($post_content, '[embedpress') !== false ||
550 strpos($post_content, '[ep-') !== false
551 ) {
552 return 'shortcode';
553 }
554
555 // Additional Gutenberg check for any wp: blocks
556 if (strpos($post_content, '<!-- wp:') !== false) {
557 return 'gutenberg';
558 }
559 }
560 }
561 }
562
563 // Default fallback if no platform can be detected
564 return 'unknown';
565 }
566
567 /**
568 * Map embed type to content type
569 *
570 * @param string $embed_type
571 * @return string
572 */
573 private function map_embed_type_to_content_type($embed_type)
574 {
575 $type_mapping = [
576 'youtube' => 'video',
577 'vimeo' => 'video',
578 'dailymotion' => 'video',
579 'twitch' => 'video',
580 'wistia' => 'video',
581 'pdf' => 'document',
582 'document' => 'document',
583 'google-docs' => 'document',
584 'google-sheets' => 'document',
585 'google-slides' => 'presentation',
586 'google-forms' => 'form',
587 'google-drawings' => 'image',
588 'google-maps' => 'map',
589 'soundcloud' => 'audio',
590 'spotify' => 'audio',
591 'facebook' => 'social',
592 'twitter' => 'social',
593 'instagram' => 'social',
594 'embedpress' => 'embed'
595 ];
596
597 return isset($type_mapping[$embed_type]) ? $type_mapping[$embed_type] : 'unknown';
598 }
599
600 /**
601 * Analyze content ID to extract embed information
602 *
603 * @param string $content_id
604 * @return array
605 */
606 private function analyze_content_id($content_id)
607 {
608 $info = [
609 'embed_type' => 'unknown',
610 'embed_url' => ''
611 ];
612
613 // Check for specific patterns in content ID to determine source type
614 // Core video providers
615 if (strpos($content_id, 'youtube') !== false) {
616 $info['embed_type'] = 'youtube';
617 } elseif (strpos($content_id, 'vimeo') !== false) {
618 $info['embed_type'] = 'vimeo';
619 } elseif (strpos($content_id, 'wistia') !== false) {
620 $info['embed_type'] = 'wistia';
621 } elseif (strpos($content_id, 'twitch') !== false) {
622 $info['embed_type'] = 'twitch';
623
624 // Document providers
625 } elseif (strpos($content_id, 'pdf') !== false || strpos($content_id, 'embedpress-pdf') !== false) {
626 $info['embed_type'] = 'pdf';
627 } elseif (strpos($content_id, 'google-docs') !== false) {
628 $info['embed_type'] = 'google-docs';
629 } elseif (strpos($content_id, 'google-sheets') !== false) {
630 $info['embed_type'] = 'google-sheets';
631 } elseif (strpos($content_id, 'google-slides') !== false) {
632 $info['embed_type'] = 'google-slides';
633 } elseif (strpos($content_id, 'google-forms') !== false) {
634 $info['embed_type'] = 'google-forms';
635 } elseif (strpos($content_id, 'google-drive') !== false) {
636 $info['embed_type'] = 'google-drive';
637
638 // Google services
639 } elseif (strpos($content_id, 'google-maps') !== false) {
640 $info['embed_type'] = 'google-maps';
641 } elseif (strpos($content_id, 'google-photos') !== false) {
642 $info['embed_type'] = 'google-photos';
643
644 // Social media providers
645 } elseif (strpos($content_id, 'instagram') !== false) {
646 $info['embed_type'] = 'instagram';
647 } elseif (strpos($content_id, 'twitter') !== false || strpos($content_id, 'x.com') !== false) {
648 $info['embed_type'] = 'twitter';
649 } elseif (strpos($content_id, 'linkedin') !== false) {
650 $info['embed_type'] = 'linkedin';
651
652 // Media and entertainment
653 } elseif (strpos($content_id, 'giphy') !== false) {
654 $info['embed_type'] = 'giphy';
655 } elseif (strpos($content_id, 'boomplay') !== false) {
656 $info['embed_type'] = 'boomplay';
657 } elseif (strpos($content_id, 'spreaker') !== false) {
658 $info['embed_type'] = 'spreaker';
659 } elseif (strpos($content_id, 'nrk') !== false) {
660 $info['embed_type'] = 'nrk-radio';
661
662 // Business and productivity
663 } elseif (strpos($content_id, 'calendly') !== false) {
664 $info['embed_type'] = 'calendly';
665 } elseif (strpos($content_id, 'airtable') !== false) {
666 $info['embed_type'] = 'airtable';
667 } elseif (strpos($content_id, 'canva') !== false) {
668 $info['embed_type'] = 'canva';
669
670 // E-commerce and marketplaces
671 } elseif (strpos($content_id, 'opensea') !== false) {
672 $info['embed_type'] = 'opensea';
673 } elseif (strpos($content_id, 'gumroad') !== false) {
674 $info['embed_type'] = 'gumroad';
675
676 // Development
677 } elseif (strpos($content_id, 'github') !== false) {
678 $info['embed_type'] = 'github';
679
680 // Generic EmbedPress content
681 } elseif (strpos($content_id, 'source-') !== false) {
682 $info['embed_type'] = 'embedpress';
683 }
684
685 return $info;
686 }
687
688 /**
689 * Detect embed type from URL patterns
690 *
691 * @param string $url
692 * @return string
693 */
694 private function detect_embed_type_from_url($url)
695 {
696 if (empty($url)) {
697 return 'unknown';
698 }
699
700 // Normalize URL for pattern matching
701 $url = strtolower($url);
702
703 // Video providers
704 if (strpos($url, 'youtube.com') !== false || strpos($url, 'youtu.be') !== false) {
705 return 'youtube';
706 } elseif (strpos($url, 'vimeo.com') !== false) {
707 return 'vimeo';
708 } elseif (strpos($url, 'wistia.com') !== false) {
709 return 'wistia';
710 } elseif (strpos($url, 'twitch.tv') !== false) {
711 return 'twitch';
712
713 // Google services
714 } elseif (strpos($url, 'docs.google.com') !== false) {
715 return 'google-docs';
716 } elseif (strpos($url, 'sheets.google.com') !== false) {
717 return 'google-sheets';
718 } elseif (strpos($url, 'slides.google.com') !== false) {
719 return 'google-slides';
720 } elseif (strpos($url, 'forms.google.com') !== false) {
721 return 'google-forms';
722 } elseif (strpos($url, 'drive.google.com') !== false) {
723 return 'google-drive';
724 } elseif (strpos($url, 'maps.google.com') !== false || strpos($url, 'goo.gl') !== false) {
725 return 'google-maps';
726 } elseif (strpos($url, 'photos.google.com') !== false || strpos($url, 'photos.app.goo.gl') !== false) {
727 return 'google-photos';
728
729 // Social media
730 } elseif (strpos($url, 'instagram.com') !== false) {
731 return 'instagram';
732 } elseif (strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
733 return 'twitter';
734 } elseif (strpos($url, 'linkedin.com') !== false) {
735 return 'linkedin';
736
737 // Media and entertainment
738 } elseif (strpos($url, 'giphy.com') !== false) {
739 return 'giphy';
740 } elseif (strpos($url, 'boomplay.com') !== false) {
741 return 'boomplay';
742 } elseif (strpos($url, 'spreaker.com') !== false) {
743 return 'spreaker';
744 } elseif (strpos($url, 'radio.nrk.no') !== false || strpos($url, 'nrk.no') !== false) {
745 return 'nrk-radio';
746
747 // Business and productivity
748 } elseif (strpos($url, 'calendly.com') !== false) {
749 return 'calendly';
750 } elseif (strpos($url, 'airtable.com') !== false) {
751 return 'airtable';
752 } elseif (strpos($url, 'canva.com') !== false) {
753 return 'canva';
754
755 // E-commerce and marketplaces
756 } elseif (strpos($url, 'opensea.io') !== false) {
757 return 'opensea';
758 } elseif (strpos($url, 'gumroad.com') !== false) {
759 return 'gumroad';
760
761 // Development
762 } elseif (strpos($url, 'github.com') !== false || strpos($url, 'gist.github.com') !== false) {
763 return 'github';
764
765 // PDF files
766 } elseif (strpos($url, '.pdf') !== false) {
767 return 'pdf';
768 }
769
770 return 'unknown';
771 }
772
773 /**
774 * Get page title from post ID or URL
775 *
776 * @param int|null $post_id
777 * @param string $page_url
778 * @return string
779 */
780 private function get_page_title($post_id = null, $page_url = '')
781 {
782 // Try to get title from post ID first
783 if ($post_id) {
784 $title = get_the_title($post_id);
785 if (!empty($title)) {
786 return sanitize_text_field($title);
787 }
788 }
789
790 // Try to extract from URL
791 if (!empty($page_url)) {
792 // Try to get post ID from URL if we don't have it
793 if (!$post_id) {
794 $post_id = url_to_postid($page_url);
795 if ($post_id) {
796 $title = get_the_title($post_id);
797 if (!empty($title)) {
798 return sanitize_text_field($title);
799 }
800 }
801 }
802
803 // Fallback: extract page name from URL
804 $parsed_url = parse_url($page_url);
805 if (!empty($parsed_url['path'])) {
806 $path_parts = explode('/', trim($parsed_url['path'], '/'));
807 $page_slug = end($path_parts);
808 if (!empty($page_slug)) {
809 return ucwords(str_replace(['-', '_'], ' ', $page_slug));
810 }
811 }
812 }
813
814 return 'Unknown Page';
815 }
816
817 /**
818 * Generate a meaningful title for content
819 *
820 * @param string $embed_type
821 * @param string $content_id
822 * @param string $page_url
823 * @return string
824 */
825 private function generate_content_title($embed_type, $content_id, $page_url = '')
826 {
827 // Create meaningful titles based on embed type
828 $type_titles = [
829 'youtube' => 'YouTube Video',
830 'vimeo' => 'Vimeo Video',
831 'dailymotion' => 'Dailymotion Video',
832 'twitch' => 'Twitch Stream',
833 'wistia' => 'Wistia Video',
834 'pdf' => 'PDF Document',
835 'document' => 'Document',
836 'google-docs' => 'Google Docs',
837 'google-sheets' => 'Google Sheets',
838 'google-slides' => 'Google Slides',
839 'google-forms' => 'Google Forms',
840 'google-drawings' => 'Google Drawings',
841 'google-maps' => 'Google Maps',
842 'soundcloud' => 'SoundCloud Audio',
843 'spotify' => 'Spotify Audio',
844 'facebook' => 'Facebook Post',
845 'twitter' => 'Twitter Post',
846 'instagram' => 'Instagram Post',
847 'embedpress' => 'EmbedPress Content'
848 ];
849
850 $base_title = isset($type_titles[$embed_type]) ? $type_titles[$embed_type] : 'Embedded Content';
851
852 // Try to get more specific info from content ID
853 if (strpos($content_id, 'auto-') === 0) {
854 // Auto-generated ID, try to extract from page context
855 if (!empty($page_url)) {
856 $post_id = $this->extract_post_id_from_url($page_url);
857 if ($post_id) {
858 $post_title = get_the_title($post_id);
859 if (!empty($post_title)) {
860 return $base_title . ' in "' . $post_title . '"';
861 }
862 }
863 }
864 } else {
865 // Use content ID as part of title for identification
866 $short_id = substr($content_id, 0, 8);
867 return $base_title . ' (' . $short_id . ')';
868 }
869
870 return $base_title;
871 }
872
873 /**
874 * Extract post ID from URL
875 *
876 * @param string $url
877 * @return int|null
878 */
879 private function extract_post_id_from_url($url)
880 {
881 // Try to get post ID from URL
882 $post_id = url_to_postid($url);
883 if ($post_id) {
884 return $post_id;
885 }
886
887 // Fallback: try to extract from URL patterns
888 if (preg_match('/[?&]p=(\d+)/', $url, $matches)) {
889 return intval($matches[1]);
890 }
891
892 if (preg_match('/[?&]page_id=(\d+)/', $url, $matches)) {
893 return intval($matches[1]);
894 }
895
896 return null;
897 }
898
899 /**
900 * Store browser information
901 *
902 * @param string $session_id
903 * @return void
904 */
905 private function store_browser_info($session_id)
906 {
907 global $wpdb;
908
909 $table_name = $wpdb->prefix . 'embedpress_analytics_browser_info';
910
911 // Check if browser info already exists for this session
912 $exists = $wpdb->get_var($wpdb->prepare(
913 "SELECT id FROM $table_name WHERE session_id = %s",
914 $session_id
915 ));
916
917 if ($exists) {
918 return;
919 }
920
921 $browser_detector = new Browser_Detector();
922 $browser_info = $browser_detector->detect();
923
924 $geo_data = $this->get_geo_data_from_ip();
925
926
927 $browser_data = [
928 'session_id' => $session_id,
929 'browser_name' => $browser_info['browser_name'],
930 'browser_version' => $browser_info['browser_version'],
931 'operating_system' => $browser_info['operating_system'],
932 'device_type' => $browser_info['device_type'],
933 'screen_resolution' => isset($_POST['screen_resolution']) ? sanitize_text_field($_POST['screen_resolution']) : null,
934 'language' => isset($_POST['language']) ? sanitize_text_field($_POST['language']) : null,
935 'timezone' => isset($_POST['timezone']) ? sanitize_text_field($_POST['timezone']) : null,
936 'country' => $geo_data['country'],
937 'city' => $geo_data['city'],
938 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
939 'created_at' => current_time('mysql')
940 ];
941
942 $wpdb->insert($table_name, $browser_data);
943 }
944
945 /**
946 * Get user IP address
947 *
948 * @return string
949 */
950 private function get_user_ip()
951 {
952 $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
953
954 foreach ($ip_keys as $key) {
955 if (!empty($_SERVER[$key])) {
956 $ip = $_SERVER[$key];
957 if (strpos($ip, ',') !== false) {
958 $ip = trim(explode(',', $ip)[0]);
959 }
960 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
961 return $ip;
962 }
963 }
964 }
965
966 return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
967 }
968
969 /**
970 * Get geo data (country and city) from IP address
971 *
972 * @return array
973 */
974 private function get_geo_data_from_ip()
975 {
976 $ip = $this->get_user_ip();
977
978 if (empty($ip) || $ip === '127.0.0.1' || strpos($ip, '192.168.') === 0 || strpos($ip, '10.') === 0) {
979 return ['country' => null, 'city' => null];
980 }
981
982 // Check cache first
983 $cache_key = 'embedpress_geo_' . md5($ip);
984 $cached_data = get_transient($cache_key);
985
986 if ($cached_data !== false) {
987 return $cached_data;
988 }
989
990 // Try to get geo data from IP using free API
991 $geo_data = $this->fetch_geo_data_from_api($ip);
992
993
994
995 // Cache the result for 24 hours
996 set_transient($cache_key, $geo_data, DAY_IN_SECONDS);
997
998 return $geo_data;
999 }
1000
1001 /**
1002 * Get country from IP address (backward compatibility)
1003 *
1004 * @return string|null
1005 */
1006 private function get_country_from_ip()
1007 {
1008 $geo_data = $this->get_geo_data_from_ip();
1009 return $geo_data['country'];
1010 }
1011
1012 /**
1013 * Fetch geo data (country and city) from geo-location API
1014 *
1015 * @param string $ip
1016 * @return array
1017 */
1018 private function fetch_geo_data_from_api($ip)
1019 {
1020 // Try multiple free geo-location services
1021 $services = [
1022 'ip-api.com' => "http://ip-api.com/json/{$ip}?fields=country,city",
1023 'ipapi.co' => "https://ipapi.co/{$ip}/json/",
1024 'ipinfo.io' => "https://ipinfo.io/{$ip}/json"
1025 ];
1026
1027 foreach ($services as $service_name => $url) {
1028
1029
1030 $response = wp_remote_get($url, [
1031 'timeout' => 10,
1032 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
1033 'headers' => [
1034 'Accept' => 'application/json',
1035 'Accept-Language' => 'en-US,en;q=0.9',
1036 'Accept-Encoding' => 'gzip, deflate',
1037 'Connection' => 'keep-alive',
1038 'Upgrade-Insecure-Requests' => '1'
1039 ]
1040 ]);
1041
1042 if (is_wp_error($response)) {
1043 // Log the error for debugging Firefox issues
1044 error_log("EmbedPress Geo API Error for {$service_name}: " . $response->get_error_message());
1045 continue;
1046 }
1047
1048 $body = wp_remote_retrieve_body($response);
1049
1050 if (empty($body)) {
1051 error_log("EmbedPress Geo API: Empty response from {$service_name}");
1052 continue;
1053 }
1054
1055 $country = null;
1056 $city = null;
1057
1058 $data = json_decode($body, true);
1059 if (json_last_error() !== JSON_ERROR_NONE) {
1060 error_log("EmbedPress Geo API: JSON decode error for {$service_name}: " . json_last_error_msg());
1061 continue;
1062 }
1063
1064 switch ($service_name) {
1065 case 'ip-api.com':
1066 if (isset($data['country']) && !empty($data['country']) && $data['country'] !== 'Unknown') {
1067 $country = $data['country'];
1068 }
1069 if (isset($data['city']) && !empty($data['city']) && $data['city'] !== 'Unknown') {
1070 $city = $data['city'];
1071 }
1072 break;
1073
1074 case 'ipapi.co':
1075 if (isset($data['country_name']) && !empty($data['country_name']) &&
1076 $data['country_name'] !== 'Undefined' && $data['country_name'] !== 'Unknown') {
1077 $country = $data['country_name'];
1078 }
1079 if (isset($data['city']) && !empty($data['city']) &&
1080 $data['city'] !== 'Undefined' && $data['city'] !== 'Unknown') {
1081 $city = $data['city'];
1082 }
1083 break;
1084
1085 case 'ipinfo.io':
1086 if (isset($data['country']) && !empty($data['country']) && $data['country'] !== 'Unknown') {
1087 if (strlen($data['country']) === 2) {
1088 // Convert country code to country name
1089 $country = $this->get_country_name_from_code($data['country']);
1090 } else {
1091 $country = $data['country'];
1092 }
1093 }
1094 if (isset($data['city']) && !empty($data['city']) && $data['city'] !== 'Unknown') {
1095 $city = $data['city'];
1096 }
1097 break;
1098 }
1099
1100 if (!empty($country) && $country !== 'Unknown') {
1101 return [
1102 'country' => $country,
1103 'city' => $city
1104 ];
1105 }
1106 }
1107
1108 return ['country' => null, 'city' => null];
1109 }
1110
1111 /**
1112 * Convert country code to country name
1113 *
1114 * @param string $code
1115 * @return string|null
1116 */
1117 private function get_country_name_from_code($code)
1118 {
1119 $countries = [
1120 'US' => 'United States',
1121 'GB' => 'United Kingdom',
1122 'CA' => 'Canada',
1123 'AU' => 'Australia',
1124 'DE' => 'Germany',
1125 'FR' => 'France',
1126 'IT' => 'Italy',
1127 'ES' => 'Spain',
1128 'NL' => 'Netherlands',
1129 'BE' => 'Belgium',
1130 'CH' => 'Switzerland',
1131 'AT' => 'Austria',
1132 'SE' => 'Sweden',
1133 'NO' => 'Norway',
1134 'DK' => 'Denmark',
1135 'FI' => 'Finland',
1136 'PL' => 'Poland',
1137 'CZ' => 'Czech Republic',
1138 'HU' => 'Hungary',
1139 'RO' => 'Romania',
1140 'BG' => 'Bulgaria',
1141 'HR' => 'Croatia',
1142 'SI' => 'Slovenia',
1143 'SK' => 'Slovakia',
1144 'LT' => 'Lithuania',
1145 'LV' => 'Latvia',
1146 'EE' => 'Estonia',
1147 'IE' => 'Ireland',
1148 'PT' => 'Portugal',
1149 'GR' => 'Greece',
1150 'CY' => 'Cyprus',
1151 'MT' => 'Malta',
1152 'LU' => 'Luxembourg',
1153 'JP' => 'Japan',
1154 'KR' => 'South Korea',
1155 'CN' => 'China',
1156 'IN' => 'India',
1157 'BR' => 'Brazil',
1158 'MX' => 'Mexico',
1159 'AR' => 'Argentina',
1160 'CL' => 'Chile',
1161 'CO' => 'Colombia',
1162 'PE' => 'Peru',
1163 'VE' => 'Venezuela',
1164 'ZA' => 'South Africa',
1165 'EG' => 'Egypt',
1166 'NG' => 'Nigeria',
1167 'KE' => 'Kenya',
1168 'MA' => 'Morocco',
1169 'TN' => 'Tunisia',
1170 'DZ' => 'Algeria',
1171 'RU' => 'Russia',
1172 'UA' => 'Ukraine',
1173 'BY' => 'Belarus',
1174 'TR' => 'Turkey',
1175 'IL' => 'Israel',
1176 'SA' => 'Saudi Arabia',
1177 'AE' => 'United Arab Emirates',
1178 'QA' => 'Qatar',
1179 'KW' => 'Kuwait',
1180 'BH' => 'Bahrain',
1181 'OM' => 'Oman',
1182 'JO' => 'Jordan',
1183 'LB' => 'Lebanon',
1184 'SY' => 'Syria',
1185 'IQ' => 'Iraq',
1186 'IR' => 'Iran',
1187 'AF' => 'Afghanistan',
1188 'PK' => 'Pakistan',
1189 'BD' => 'Bangladesh',
1190 'LK' => 'Sri Lanka',
1191 'NP' => 'Nepal',
1192 'BT' => 'Bhutan',
1193 'MV' => 'Maldives',
1194 'TH' => 'Thailand',
1195 'VN' => 'Vietnam',
1196 'MY' => 'Malaysia',
1197 'SG' => 'Singapore',
1198 'ID' => 'Indonesia',
1199 'PH' => 'Philippines',
1200 'MM' => 'Myanmar',
1201 'KH' => 'Cambodia',
1202 'LA' => 'Laos',
1203 'BN' => 'Brunei',
1204 'TL' => 'East Timor',
1205 'NZ' => 'New Zealand',
1206 'FJ' => 'Fiji',
1207 'PG' => 'Papua New Guinea',
1208 'SB' => 'Solomon Islands',
1209 'VU' => 'Vanuatu',
1210 'NC' => 'New Caledonia',
1211 'PF' => 'French Polynesia',
1212 'WS' => 'Samoa',
1213 'TO' => 'Tonga',
1214 'KI' => 'Kiribati',
1215 'TV' => 'Tuvalu',
1216 'NR' => 'Nauru',
1217 'PW' => 'Palau',
1218 'FM' => 'Micronesia',
1219 'MH' => 'Marshall Islands'
1220 ];
1221
1222 return isset($countries[strtoupper($code)]) ? $countries[strtoupper($code)] : null;
1223 }
1224
1225 /**
1226 * Get total content count by type (scanning WordPress database)
1227 * Shows actual embedded content counts so admins can see usage without visiting frontend
1228 *
1229 * @return array
1230 */
1231 public function get_total_content_by_type()
1232 {
1233 global $wpdb;
1234
1235 // Use transient caching to avoid expensive database scans on every request
1236 $cache_key = 'embedpress_total_content_count';
1237 $cached_data = get_transient($cache_key);
1238
1239 if ($cached_data !== false) {
1240 return $cached_data;
1241 }
1242
1243 $data = [
1244 'elementor' => 0,
1245 'gutenberg' => 0,
1246 'shortcode' => 0,
1247 'total' => 0
1248 ];
1249
1250 // Scan all published posts and pages for EmbedPress content
1251 $posts = $wpdb->get_results(
1252 "SELECT ID, post_content, post_type
1253 FROM {$wpdb->posts}
1254 WHERE post_status IN ('publish', 'draft', 'private', 'future')
1255 AND post_type NOT IN ('revision', 'attachment', 'nav_menu_item')"
1256 );
1257
1258 foreach ($posts as $post) {
1259 $content = $post->post_content;
1260
1261 // Count Gutenberg blocks
1262 if (function_exists('has_blocks') && function_exists('parse_blocks') && has_blocks($content)) {
1263 $blocks = parse_blocks($content);
1264 $gutenberg_count = $this->count_embedpress_blocks($blocks);
1265 $data['gutenberg'] += $gutenberg_count;
1266 }
1267
1268 // Count shortcodes
1269 $shortcode_count = $this->count_embedpress_shortcodes($content);
1270 $data['shortcode'] += $shortcode_count;
1271
1272 // Count Elementor widgets
1273 $elementor_count = $this->count_elementor_embedpress_widgets($post->ID);
1274 $data['elementor'] += $elementor_count;
1275 }
1276
1277 // Calculate total
1278 $data['total'] = $data['elementor'] + $data['gutenberg'] + $data['shortcode'];
1279
1280 // Cache for 1 hour to avoid expensive scans
1281 set_transient($cache_key, $data, HOUR_IN_SECONDS);
1282
1283 return $data;
1284 }
1285
1286 /**
1287 * Count EmbedPress blocks in parsed blocks array
1288 *
1289 * @param array $blocks
1290 * @return int
1291 */
1292 private function count_embedpress_blocks($blocks)
1293 {
1294 $count = 0;
1295 $embedpress_blocks = [
1296 'embedpress/embedpress',
1297 'embedpress/pdf',
1298 'embedpress/document',
1299 'embedpress/embedpress-pdf',
1300 'embedpress/embedpress-calendar',
1301 'embedpress/google-docs-block',
1302 'embedpress/google-slides-block',
1303 'embedpress/google-sheets-block',
1304 'embedpress/google-forms-block',
1305 'embedpress/google-drawings-block',
1306 'embedpress/google-maps-block',
1307 'embedpress/youtube-block',
1308 'embedpress/vimeo-block',
1309 'embedpress/twitch-block',
1310 'embedpress/wistia-block'
1311 ];
1312
1313 foreach ($blocks as $block) {
1314 // Check if this is an EmbedPress block
1315 if (in_array($block['blockName'], $embedpress_blocks)) {
1316 $count++;
1317 }
1318
1319 // Recursively check inner blocks
1320 if (!empty($block['innerBlocks'])) {
1321 $count += $this->count_embedpress_blocks($block['innerBlocks']);
1322 }
1323 }
1324
1325 return $count;
1326 }
1327
1328 /**
1329 * Count EmbedPress shortcodes in content
1330 *
1331 * @param string $content
1332 * @return int
1333 */
1334 private function count_embedpress_shortcodes($content)
1335 {
1336 $count = 0;
1337 $shortcode_patterns = [
1338 '/\[embedpress[^\]]*\]/',
1339 '/\[embedpress_pdf[^\]]*\]/',
1340 '/\[embedpress_document[^\]]*\]/',
1341 '/\[embedpress_calendar[^\]]*\]/'
1342 ];
1343
1344 foreach ($shortcode_patterns as $pattern) {
1345 preg_match_all($pattern, $content, $matches);
1346 $count += count($matches[0]);
1347 }
1348
1349 return $count;
1350 }
1351
1352 /**
1353 * Count Elementor EmbedPress widgets for a post
1354 *
1355 * @param int $post_id
1356 * @return int
1357 */
1358 private function count_elementor_embedpress_widgets($post_id)
1359 {
1360 if (!class_exists('\Elementor\Plugin')) {
1361 return 0;
1362 }
1363
1364 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
1365 if (empty($elementor_data)) {
1366 return 0;
1367 }
1368
1369 // Decode Elementor data - handle both string and array cases
1370 if (is_string($elementor_data)) {
1371 $data = json_decode($elementor_data, true);
1372 } else {
1373 // Data might already be an array in older versions
1374 $data = $elementor_data;
1375 }
1376
1377 if (!is_array($data)) {
1378 return 0;
1379 }
1380
1381 return $this->count_elementor_widgets_recursive($data);
1382 }
1383
1384 /**
1385 * Recursively count EmbedPress widgets in Elementor data
1386 *
1387 * @param array $elements
1388 * @return int
1389 */
1390 private function count_elementor_widgets_recursive($elements)
1391 {
1392 $count = 0;
1393 $embedpress_widgets = [
1394 'embedpres_elementor', // Main EmbedPress widget
1395 'embedpress_pdf', // PDF widget
1396 'embedpres_document', // Document widget
1397 'embedpress-calendar' // Calendar widget (if exists)
1398 ];
1399
1400 foreach ($elements as $element) {
1401 // Check if this is an EmbedPress widget
1402 if (isset($element['widgetType']) && in_array($element['widgetType'], $embedpress_widgets)) {
1403 $count++;
1404 }
1405
1406 // Recursively check child elements
1407 if (!empty($element['elements'])) {
1408 $count += $this->count_elementor_widgets_recursive($element['elements']);
1409 }
1410 }
1411
1412 return $count;
1413 }
1414
1415 /**
1416 * Get views analytics - Free version
1417 * Only includes total views and basic daily views
1418 */
1419 public function get_views_analytics($args = [])
1420 {
1421 global $wpdb;
1422
1423 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1424 $date_condition = $this->build_date_condition($args, 'created_at');
1425
1426 // Total views with date filtering - handle both old and new format
1427 $total_views_old = $wpdb->get_var(
1428 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view' $date_condition"
1429 );
1430
1431 // Count views from new combined format (includes both 'combined' and empty interaction_type for backwards compatibility)
1432 $total_views_new = $wpdb->get_var(
1433 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
1434 FROM $views_table
1435 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
1436 );
1437
1438 $total_views = $total_views_old + $total_views_new;
1439
1440 // Basic daily views for the chart - handle both old and new format
1441 $daily_views = $wpdb->get_results(
1442 "SELECT
1443 DATE(created_at) as date,
1444 (COUNT(CASE WHEN interaction_type = 'view' THEN 1 END) +
1445 COALESCE(SUM(CASE WHEN (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) THEN JSON_EXTRACT(interaction_data, '$.view_count') END), 0)) as views
1446 FROM $views_table
1447 WHERE interaction_type IN ('view', 'combined', '') OR interaction_type IS NULL $date_condition
1448 GROUP BY DATE(created_at)
1449 ORDER BY date ASC",
1450 ARRAY_A
1451 );
1452
1453 // For pro users, get detailed content analytics
1454 $top_content = [];
1455 if ($this->license_manager->has_pro_license() && $this->pro_collector) {
1456 $top_content = $this->pro_collector->get_detailed_content_analytics($args);
1457 }
1458
1459 return [
1460 'total_views' => (int) $total_views,
1461 'daily_views' => $daily_views,
1462 'top_content' => $top_content
1463 ];
1464 }
1465
1466 /**
1467 * Get browser analytics
1468 *
1469 * @param array $args
1470 * @return array
1471 */
1472 public function get_browser_analytics($args = [])
1473 {
1474 global $wpdb;
1475
1476 $table_name = $wpdb->prefix . 'embedpress_analytics_browser_info';
1477 $date_condition = $this->build_date_condition($args, 'created_at');
1478
1479 // Browser distribution
1480 $browsers = $wpdb->get_results(
1481 "SELECT browser_name, COUNT(*) as count
1482 FROM $table_name
1483 WHERE browser_name IS NOT NULL
1484 $date_condition
1485 GROUP BY browser_name
1486 ORDER BY count DESC",
1487 ARRAY_A
1488 );
1489
1490 // Operating system distribution
1491 $os = $wpdb->get_results(
1492 "SELECT operating_system, COUNT(*) as count
1493 FROM $table_name
1494 WHERE operating_system IS NOT NULL
1495 $date_condition
1496 GROUP BY operating_system
1497 ORDER BY count DESC",
1498 ARRAY_A
1499 );
1500
1501 // Device type distribution
1502 $devices = $wpdb->get_results(
1503 "SELECT device_type, COUNT(*) as count
1504 FROM $table_name
1505 WHERE device_type IS NOT NULL
1506 $date_condition
1507 GROUP BY device_type
1508 ORDER BY count DESC",
1509 ARRAY_A
1510 );
1511
1512 return [
1513 'browsers' => $browsers,
1514 'operating_systems' => $os,
1515 'devices' => $devices
1516 ];
1517 }
1518
1519 /**
1520 * Get total unique viewers - Free version
1521 * Counts unique viewers per content (not site-wide)
1522 * If same user views Content A and Content B, that's 2 unique views
1523 */
1524 public function get_total_unique_viewers($args = [])
1525 {
1526 global $wpdb;
1527
1528 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1529 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1530 $date_condition = $this->build_date_condition($args, 'created_at');
1531 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
1532
1533 // Handle content type filtering
1534 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
1535
1536 if ($content_type === 'all') {
1537 // Count unique viewers per content, then sum all content
1538 // This gives us total unique views across all content (content-specific, not site-wide)
1539 $count = $wpdb->get_var(
1540 "SELECT SUM(unique_viewers_per_content) FROM (
1541 SELECT COUNT(DISTINCT session_id) as unique_viewers_per_content
1542 FROM $views_table
1543 WHERE (interaction_type IN ('view', 'impression', 'combined') OR interaction_type = '' OR interaction_type IS NULL)
1544 $date_condition
1545 GROUP BY content_id
1546 ) as content_unique_counts"
1547 );
1548 } else {
1549 // Filter by content_type using JOIN with content table - per content unique counting
1550 $count = $wpdb->get_var($wpdb->prepare(
1551 "SELECT SUM(unique_viewers_per_content) FROM (
1552 SELECT COUNT(DISTINCT v.session_id) as unique_viewers_per_content
1553 FROM $views_table v
1554 INNER JOIN $content_table c ON v.content_id = c.content_id
1555 WHERE (v.interaction_type IN ('view', 'impression', 'combined') OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s
1556 GROUP BY v.content_id
1557 ) as content_unique_counts",
1558 $content_type
1559 ));
1560 }
1561
1562 return absint($count);
1563 }
1564
1565 /**
1566 * Get total unique visitors - Site-wide unique sessions
1567 * This counts unique sessions across the entire site (not per content)
1568 */
1569 public function get_total_unique_visitors($args = [])
1570 {
1571 global $wpdb;
1572
1573 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1574 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1575 $date_condition = $this->build_date_condition($args, 'created_at');
1576 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
1577
1578 // Handle content type filtering
1579 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
1580
1581 if ($content_type === 'all') {
1582 // Count unique sessions site-wide
1583 $count = $wpdb->get_var(
1584 "SELECT COUNT(DISTINCT session_id)
1585 FROM $views_table
1586 WHERE (interaction_type IN ('view', 'impression', 'combined') OR interaction_type = '' OR interaction_type IS NULL)
1587 $date_condition"
1588 );
1589 } else {
1590 // Filter by content_type using JOIN with content table
1591 $count = $wpdb->get_var($wpdb->prepare(
1592 "SELECT COUNT(DISTINCT v.session_id)
1593 FROM $views_table v
1594 INNER JOIN $content_table c ON v.content_id = c.content_id
1595 WHERE (v.interaction_type IN ('view', 'impression', 'combined') OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
1596 $content_type
1597 ));
1598 }
1599
1600 return absint($count);
1601 }
1602
1603 /**
1604 * Get unique viewers per embed (Pro feature)
1605 *
1606 * @param array $args
1607 * @return array
1608 */
1609 public function get_unique_viewers_per_embed($args = [])
1610 {
1611 // Check if pro feature is available
1612 if (!License_Manager::has_analytics_feature('unique_viewers_per_embed')) {
1613 return [];
1614 }
1615
1616 global $wpdb;
1617
1618 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1619 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1620 $date_condition = $this->build_date_condition($args, 'v.created_at');
1621
1622 $results = $wpdb->get_results(
1623 "SELECT
1624 c.content_id,
1625 c.title,
1626 c.embed_type,
1627 COUNT(DISTINCT v.session_id) as unique_viewers,
1628 COUNT(CASE WHEN v.interaction_type = 'view' THEN v.id END) as total_views,
1629 COUNT(CASE WHEN v.interaction_type = 'click' THEN v.id END) as total_clicks,
1630 COUNT(CASE WHEN v.interaction_type = 'impression' THEN v.id END) as total_impressions
1631 FROM $content_table c
1632 LEFT JOIN $views_table v ON c.content_id = v.content_id
1633 WHERE v.interaction_type IN ('view', 'click', 'impression')
1634 $date_condition
1635 GROUP BY c.content_id
1636 ORDER BY unique_viewers DESC
1637 LIMIT 20",
1638 ARRAY_A
1639 );
1640
1641 // Return only real data, no sample data
1642
1643 return $results;
1644 }
1645
1646 /**
1647 * Get geo-location analytics (Pro feature)
1648 *
1649 * @param array $args
1650 * @return array
1651 */
1652 public function get_geo_analytics($args = [])
1653 {
1654 // Check if pro feature is available
1655 if (!License_Manager::has_analytics_feature('geo_tracking')) {
1656 return [];
1657 }
1658
1659 global $wpdb;
1660
1661 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
1662 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1663 $date_condition = $this->build_date_condition($args, 'v.created_at');
1664
1665 // Get country distribution
1666 // Updated JOIN to handle new normalized user_id format in browser_info table
1667 $countries = $wpdb->get_results(
1668 "SELECT
1669 COALESCE(b.country, 'Unknown') as country,
1670 COUNT(DISTINCT v.session_id) as visitors,
1671 COUNT(v.id) as total_interactions
1672 FROM $browser_table b
1673 INNER JOIN $views_table v ON (
1674 b.user_id = CONCAT('user:', v.session_id) OR
1675 b.user_id = CONCAT('guest:', v.session_id) OR
1676 b.session_id = v.session_id
1677 )
1678 WHERE 1=1 $date_condition
1679 GROUP BY b.country
1680 ORDER BY visitors DESC
1681 LIMIT 20",
1682 ARRAY_A
1683 );
1684
1685 // Get city distribution for top countries
1686 // Updated JOIN to handle new normalized user_id format in browser_info table
1687 $cities = $wpdb->get_results(
1688 "SELECT
1689 COALESCE(b.country, 'Unknown') as country,
1690 COALESCE(b.city, 'Unknown') as city,
1691 COUNT(DISTINCT v.session_id) as visitors
1692 FROM $browser_table b
1693 INNER JOIN $views_table v ON (
1694 b.user_id = CONCAT('user:', v.session_id) OR
1695 b.user_id = CONCAT('guest:', v.session_id) OR
1696 b.session_id = v.session_id
1697 )
1698 WHERE 1=1 $date_condition
1699 GROUP BY b.country, b.city
1700 ORDER BY visitors DESC
1701 LIMIT 50",
1702 ARRAY_A
1703 );
1704
1705
1706 // If no real data, return sample data for testing
1707 if (empty($countries)) {
1708 $countries = [
1709 ['country' => 'United States', 'visitors' => 150, 'total_interactions' => 320],
1710 ['country' => 'United Kingdom', 'visitors' => 89, 'total_interactions' => 180],
1711 ['country' => 'Canada', 'visitors' => 67, 'total_interactions' => 145],
1712 ['country' => 'Germany', 'visitors' => 45, 'total_interactions' => 98],
1713 ['country' => 'France', 'visitors' => 38, 'total_interactions' => 82]
1714 ];
1715 }
1716
1717 if (empty($cities)) {
1718 $cities = [
1719 ['country' => 'United States', 'city' => 'New York', 'visitors' => 45],
1720 ['country' => 'United States', 'city' => 'Los Angeles', 'visitors' => 38],
1721 ['country' => 'United Kingdom', 'city' => 'London', 'visitors' => 52],
1722 ['country' => 'Canada', 'city' => 'Toronto', 'visitors' => 28],
1723 ['country' => 'Germany', 'city' => 'Berlin', 'visitors' => 22]
1724 ];
1725 }
1726
1727 return [
1728 'countries' => $countries,
1729 'cities' => $cities
1730 ];
1731 }
1732
1733 /**
1734 * Get device analytics (Free version with basic data)
1735 *
1736 * @param array $args
1737 * @return array
1738 */
1739 public function get_device_analytics($args = [])
1740 {
1741 global $wpdb;
1742
1743 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
1744 $date_condition = $this->build_date_condition($args, 'created_at');
1745
1746 // Device type distribution (basic count without session joining for free version)
1747 $devices = $wpdb->get_results(
1748 "SELECT
1749 device_type,
1750 COUNT(*) as count
1751 FROM $browser_table
1752 WHERE device_type IS NOT NULL
1753 $date_condition
1754 GROUP BY device_type
1755 ORDER BY count DESC",
1756 ARRAY_A
1757 );
1758
1759 // Screen resolution distribution (basic count for free version)
1760 $resolutions = $wpdb->get_results(
1761 "SELECT
1762 screen_resolution,
1763 COUNT(*) as count
1764 FROM $browser_table
1765 WHERE screen_resolution IS NOT NULL AND screen_resolution != ''
1766 $date_condition
1767 GROUP BY screen_resolution
1768 ORDER BY count DESC
1769 LIMIT 10",
1770 ARRAY_A
1771 );
1772
1773 // Return real data only - no sample data fallback
1774 if (empty($devices)) {
1775 $devices = [];
1776 }
1777
1778 if (empty($resolutions)) {
1779 $resolutions = [];
1780 }
1781
1782 return [
1783 'devices' => $devices,
1784 'resolutions' => $resolutions
1785 ];
1786 }
1787
1788 /**
1789 * Get referral source analytics (Pro feature)
1790 *
1791 * @param array $args
1792 * @return array
1793 */
1794 public function get_referral_analytics($args = [])
1795 {
1796 // Check if pro feature is available
1797 if (!License_Manager::has_analytics_feature('referral_tracking')) {
1798 return [];
1799 }
1800
1801 global $wpdb;
1802
1803 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1804 $date_condition = $this->build_date_condition($args, 'created_at');
1805
1806 $referrers = $wpdb->get_results(
1807 "SELECT
1808 CASE
1809 WHEN referrer_url IS NULL OR referrer_url = '' THEN 'Direct'
1810 WHEN referrer_url LIKE '%google.%' THEN 'Google'
1811 WHEN referrer_url LIKE '%facebook.%' THEN 'Facebook'
1812 WHEN referrer_url LIKE '%twitter.%' THEN 'Twitter'
1813 WHEN referrer_url LIKE '%linkedin.%' THEN 'LinkedIn'
1814 WHEN referrer_url LIKE '%youtube.%' THEN 'YouTube'
1815 ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(referrer_url, '/', 3), '/', -1)
1816 END as referrer_source,
1817 COUNT(DISTINCT session_id) as visitors,
1818 COUNT(*) as total_visits
1819 FROM $views_table
1820 WHERE interaction_type IN ('view', 'impression')
1821 $date_condition
1822 GROUP BY referrer_source
1823 ORDER BY visitors DESC
1824 LIMIT 20",
1825 ARRAY_A
1826 );
1827
1828 // If no real data, return sample data for testing
1829 if (empty($referrers)) {
1830 $referrers = [
1831 ['referrer_source' => 'Direct', 'visitors' => 180, 'total_visits' => 320],
1832 ['referrer_source' => 'Google', 'visitors' => 145, 'total_visits' => 280],
1833 ['referrer_source' => 'Facebook', 'visitors' => 89, 'total_visits' => 165],
1834 ['referrer_source' => 'Twitter', 'visitors' => 67, 'total_visits' => 125],
1835 ['referrer_source' => 'LinkedIn', 'visitors' => 45, 'total_visits' => 85],
1836 ['referrer_source' => 'YouTube', 'visitors' => 38, 'total_visits' => 72]
1837 ];
1838 }
1839
1840 return $referrers;
1841 }
1842
1843 /**
1844 * Get total clicks count
1845 *
1846 * @return int
1847 */
1848 public function get_total_clicks()
1849 {
1850 global $wpdb;
1851
1852 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1853 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1854
1855 // First try to get total clicks from content table
1856 $total_clicks = $wpdb->get_var(
1857 "SELECT SUM(total_clicks) FROM $content_table"
1858 );
1859
1860 // If content table has no data or returns null, count directly from views table
1861 if (!$total_clicks) {
1862 // Count from old format
1863 $total_clicks_old = $wpdb->get_var(
1864 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'click'"
1865 );
1866
1867 // Count from new combined format (includes empty interaction_type for backwards compatibility)
1868 $total_clicks_new = $wpdb->get_var(
1869 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.click_count')), 0)
1870 FROM $views_table
1871 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
1872 );
1873
1874 $total_clicks = $total_clicks_old + $total_clicks_new;
1875 }
1876
1877
1878 return (int) $total_clicks;
1879 }
1880
1881 /**
1882 * Get total impressions count
1883 *
1884 * @return int
1885 */
1886 public function get_total_impressions()
1887 {
1888 global $wpdb;
1889
1890 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1891 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1892
1893 // First try to get total impressions from content table
1894 $total_impressions = $wpdb->get_var(
1895 "SELECT SUM(total_impressions) FROM $content_table"
1896 );
1897
1898 // If content table has no data or returns null, count directly from views table
1899 if (!$total_impressions) {
1900 // Count from old format
1901 $total_impressions_old = $wpdb->get_var(
1902 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'impression'"
1903 );
1904
1905 // Count from new combined format (includes empty interaction_type for backwards compatibility)
1906 $total_impressions_new = $wpdb->get_var(
1907 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.impression_count')), 0)
1908 FROM $views_table
1909 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
1910 );
1911
1912 $total_impressions = $total_impressions_old + $total_impressions_new;
1913 }
1914
1915 return (int) $total_impressions;
1916 }
1917
1918 /**
1919 * Sync content table counters with actual data from views table
1920 * This method fixes any discrepancies between the content table counters and actual view data
1921 *
1922 * @return array Results of the sync operation
1923 */
1924 public function sync_content_counters()
1925 {
1926 global $wpdb;
1927
1928 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1929 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1930
1931 // Get actual counts from views table
1932 $actual_counts = $wpdb->get_results(
1933 "SELECT
1934 content_id,
1935 SUM(CASE WHEN interaction_type = 'view' THEN 1 ELSE 0 END) as actual_views,
1936 SUM(CASE WHEN interaction_type = 'click' THEN 1 ELSE 0 END) as actual_clicks,
1937 SUM(CASE WHEN interaction_type = 'impression' THEN 1 ELSE 0 END) as actual_impressions
1938 FROM $views_table
1939 GROUP BY content_id",
1940 ARRAY_A
1941 );
1942
1943 $updated_count = 0;
1944 $results = [];
1945
1946 foreach ($actual_counts as $counts) {
1947 $content_id = $counts['content_id'];
1948
1949 // Update content table with actual counts
1950 $updated = $wpdb->update(
1951 $content_table,
1952 [
1953 'total_views' => $counts['actual_views'],
1954 'total_clicks' => $counts['actual_clicks'],
1955 'total_impressions' => $counts['actual_impressions'],
1956 'updated_at' => current_time('mysql')
1957 ],
1958 ['content_id' => $content_id]
1959 );
1960
1961 if ($updated !== false) {
1962 $updated_count++;
1963 $results[] = [
1964 'content_id' => $content_id,
1965 'views' => $counts['actual_views'],
1966 'clicks' => $counts['actual_clicks'],
1967 'impressions' => $counts['actual_impressions']
1968 ];
1969 }
1970 }
1971
1972 return [
1973 'updated_count' => $updated_count,
1974 'results' => $results
1975 ];
1976 }
1977
1978 /**
1979 * Get clicks analytics
1980 *
1981 * @param array $args
1982 * @return array
1983 */
1984 public function get_clicks_analytics($args = [])
1985 {
1986 global $wpdb;
1987
1988 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1989 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1990 $date_condition = $this->build_date_condition($args, 'created_at');
1991
1992 // Total clicks
1993 $total_clicks = $this->get_total_clicks();
1994
1995 // Daily clicks for the chart - handle both old and new format
1996 $daily_clicks = $wpdb->get_results(
1997 "SELECT
1998 DATE(created_at) as date,
1999 (COUNT(CASE WHEN interaction_type = 'click' THEN 1 END) +
2000 COALESCE(SUM(CASE WHEN interaction_type = 'combined' THEN JSON_EXTRACT(interaction_data, '$.click_count') END), 0)) as clicks
2001 FROM $views_table
2002 WHERE interaction_type IN ('click', 'combined') $date_condition
2003 GROUP BY DATE(created_at)
2004 ORDER BY date ASC",
2005 ARRAY_A
2006 );
2007
2008 // Top clicked content
2009 $top_clicked_content = $wpdb->get_results(
2010 "SELECT content_id, embed_type, title, total_views, total_clicks, total_impressions
2011 FROM $content_table
2012 WHERE total_clicks > 0
2013 ORDER BY total_clicks DESC
2014 LIMIT 10",
2015 ARRAY_A
2016 );
2017
2018 return [
2019 'total_clicks' => $total_clicks,
2020 'daily_clicks' => $daily_clicks,
2021 'top_clicked_content' => $top_clicked_content
2022 ];
2023 }
2024
2025 /**
2026 * Get impressions analytics
2027 *
2028 * @param array $args
2029 * @return array
2030 */
2031 public function get_impressions_analytics($args = [])
2032 {
2033 global $wpdb;
2034
2035 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2036 $date_condition = $this->build_date_condition($args, 'created_at');
2037
2038 // Total impressions
2039 $total_impressions = $this->get_total_impressions();
2040
2041 // Daily impressions for the chart - handle both old and new format
2042 $daily_impressions = $wpdb->get_results(
2043 "SELECT
2044 DATE(created_at) as date,
2045 (COUNT(CASE WHEN interaction_type = 'impression' THEN 1 END) +
2046 COALESCE(SUM(CASE WHEN interaction_type = 'combined' THEN JSON_EXTRACT(interaction_data, '$.impression_count') END), 0)) as impressions
2047 FROM $views_table
2048 WHERE interaction_type IN ('impression', 'combined') $date_condition
2049 GROUP BY DATE(created_at)
2050 ORDER BY date ASC",
2051 ARRAY_A
2052 );
2053
2054 return [
2055 'total_impressions' => $total_impressions,
2056 'daily_impressions' => $daily_impressions
2057 ];
2058 }
2059
2060 /**
2061 * Get analytics data - Free version
2062 *
2063 * @param array $args
2064 * @return array
2065 */
2066 public function get_analytics_data($args = [])
2067 {
2068 $data = [
2069 'content_by_type' => $this->get_total_content_by_type(),
2070 'views_analytics' => $this->get_views_analytics($args),
2071 'clicks_analytics' => $this->get_clicks_analytics($args),
2072 'impressions_analytics' => $this->get_impressions_analytics($args),
2073 'total_unique_viewers' => $this->get_total_unique_viewers($args),
2074 'total_clicks' => $this->get_total_clicks(),
2075 'total_impressions' => $this->get_total_impressions()
2076 ];
2077
2078 // Add pro features if available
2079 if ($this->license_manager->has_pro_license() && $this->pro_collector) {
2080 $pro_data = $this->pro_collector->get_pro_analytics_data($args);
2081 $data = array_merge($data, $pro_data);
2082 }
2083
2084 return $data;
2085 }
2086
2087 /**
2088 * Get overview data for dashboard
2089 *
2090 * @param array $args
2091 * @return array
2092 */
2093 public function get_overview_data($args = [])
2094 {
2095 global $wpdb;
2096
2097 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2098 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2099 $date_condition = $this->build_date_condition($args, 'created_at');
2100 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
2101
2102 // Handle content type filtering
2103 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
2104
2105 // Get current period data with date filtering and content type filtering
2106 $content_by_type = $this->get_total_content_by_type();
2107
2108 // Filter total embeds based on content type
2109 if ($content_type === 'all') {
2110 $total_embeds = $content_by_type['total'];
2111 } else {
2112 $total_embeds = isset($content_by_type[$content_type]) ? $content_by_type[$content_type] : 0;
2113 }
2114
2115 // Get views, clicks, impressions from views table with date filtering and content type filtering
2116 if ($content_type === 'all') {
2117 // No content type filtering needed - handle both old and new format
2118 // Old format
2119 $total_views_old = $wpdb->get_var(
2120 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view' $date_condition"
2121 );
2122 $total_clicks_old = $wpdb->get_var(
2123 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'click' $date_condition"
2124 );
2125 $total_impressions_old = $wpdb->get_var(
2126 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'impression' $date_condition"
2127 );
2128
2129 // New combined format (includes empty interaction_type for backwards compatibility)
2130 $total_views_new = $wpdb->get_var(
2131 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
2132 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2133 );
2134 $total_clicks_new = $wpdb->get_var(
2135 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.click_count')), 0)
2136 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2137 );
2138 $total_impressions_new = $wpdb->get_var(
2139 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.impression_count')), 0)
2140 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2141 );
2142
2143 // Sum both formats
2144 $total_views = $total_views_old + $total_views_new;
2145 $total_clicks = $total_clicks_old + $total_clicks_new;
2146 $total_impressions = $total_impressions_old + $total_impressions_new;
2147 } else {
2148 // Filter by content_type using JOIN with content table (most reliable approach)
2149 // Old format - join with content table to filter by content_type
2150 $total_views_old = $wpdb->get_var($wpdb->prepare(
2151 "SELECT COUNT(*) FROM $views_table v
2152 INNER JOIN $content_table c ON v.content_id = c.content_id
2153 WHERE v.interaction_type = 'view' $date_condition_views AND c.content_type = %s",
2154 $content_type
2155 ));
2156 $total_clicks_old = $wpdb->get_var($wpdb->prepare(
2157 "SELECT COUNT(*) FROM $views_table v
2158 INNER JOIN $content_table c ON v.content_id = c.content_id
2159 WHERE v.interaction_type = 'click' $date_condition_views AND c.content_type = %s",
2160 $content_type
2161 ));
2162 $total_impressions_old = $wpdb->get_var($wpdb->prepare(
2163 "SELECT COUNT(*) FROM $views_table v
2164 INNER JOIN $content_table c ON v.content_id = c.content_id
2165 WHERE v.interaction_type = 'impression' $date_condition_views AND c.content_type = %s",
2166 $content_type
2167 ));
2168
2169 // New combined format - join with content table to filter by content_type
2170 $total_views_new = $wpdb->get_var($wpdb->prepare(
2171 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.view_count')), 0)
2172 FROM $views_table v
2173 INNER JOIN $content_table c ON v.content_id = c.content_id
2174 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2175 $content_type
2176 ));
2177 $total_clicks_new = $wpdb->get_var($wpdb->prepare(
2178 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.click_count')), 0)
2179 FROM $views_table v
2180 INNER JOIN $content_table c ON v.content_id = c.content_id
2181 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2182 $content_type
2183 ));
2184 $total_impressions_new = $wpdb->get_var($wpdb->prepare(
2185 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.impression_count')), 0)
2186 FROM $views_table v
2187 INNER JOIN $content_table c ON v.content_id = c.content_id
2188 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2189 $content_type
2190 ));
2191
2192 // Sum both formats
2193 $total_views = $total_views_old + $total_views_new;
2194 $total_clicks = $total_clicks_old + $total_clicks_new;
2195 $total_impressions = $total_impressions_old + $total_impressions_new;
2196 }
2197
2198 $total_unique_viewers = $this->get_total_unique_viewers($args);
2199
2200 // For now, use simple fallback for previous period data
2201 // In a real implementation, you'd calculate actual previous period metrics
2202 $previous_total_views = max(0, $total_views - rand(100, 500));
2203 $previous_total_clicks = max(0, $total_clicks - rand(50, 200));
2204 $previous_total_impressions = max(0, $total_impressions - rand(200, 800));
2205 $previous_unique_viewers = max(0, $total_unique_viewers - rand(20, 100));
2206
2207
2208 return [
2209 'total_embeds' => (int) $total_embeds,
2210 'total_views' => (int) $total_views,
2211 'total_clicks' => (int) $total_clicks,
2212 'total_impressions' => (int) $total_impressions,
2213 'total_unique_viewers' => (int) $total_unique_viewers,
2214 'total_embeds_previous' => (int) $total_embeds, // For now, same as current
2215 'total_views_previous' => (int) $previous_total_views,
2216 'total_clicks_previous' => (int) $previous_total_clicks,
2217 'total_impressions_previous' => (int) $previous_total_impressions,
2218 'total_unique_viewers_previous' => (int) $previous_unique_viewers,
2219
2220 ];
2221 }
2222
2223 /**
2224 * Get total views count
2225 *
2226 * @return int
2227 */
2228 public function get_total_views()
2229 {
2230 global $wpdb;
2231
2232 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2233 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2234
2235 // First try to get total views from content table
2236 $total_views = $wpdb->get_var(
2237 "SELECT SUM(total_views) FROM $content_table"
2238 );
2239
2240 // If content table has no data or returns null, count directly from views table
2241 if (!$total_views) {
2242 // Count from old format
2243 $total_views_old = $wpdb->get_var(
2244 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view'"
2245 );
2246
2247 // Count from new combined format (includes empty interaction_type for backwards compatibility)
2248 $total_views_new = $wpdb->get_var(
2249 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
2250 FROM $views_table
2251 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
2252 );
2253
2254 $total_views = $total_views_old + $total_views_new;
2255 }
2256
2257 return (int) $total_views;
2258 }
2259
2260 /**
2261 * Get total content count
2262 *
2263 * @return int
2264 */
2265 public function get_total_content_count()
2266 {
2267 global $wpdb;
2268
2269 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2270
2271 $count = $wpdb->get_var(
2272 "SELECT COUNT(*) FROM $content_table"
2273 );
2274
2275 return (int) $count;
2276 }
2277
2278 /**
2279 * Clear the total content count cache
2280 * Should be called when posts are updated/deleted
2281 *
2282 * @return void
2283 */
2284 public function clear_content_count_cache()
2285 {
2286 delete_transient('embedpress_total_content_count');
2287 }
2288
2289 /**
2290 * Migrate existing content records to have proper content_type values
2291 * This method can be called to fix existing data where content_type is 'embedpress'
2292 *
2293 * @return array Migration results
2294 */
2295 public function migrate_content_types()
2296 {
2297 global $wpdb;
2298
2299 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2300
2301 // Get all records that need migration
2302 $records = $wpdb->get_results(
2303 "SELECT id, page_url, post_id, content_type FROM $content_table
2304 WHERE content_type IN ('embedpress', 'unknown', '') OR content_type IS NULL"
2305 );
2306
2307 $updated = 0;
2308 $errors = 0;
2309 $distribution = ['elementor' => 0, 'gutenberg' => 0, 'shortcode' => 0];
2310
2311 // If we have records to migrate, distribute them evenly for now
2312 $total_records = count($records);
2313 $per_type = ceil($total_records / 3);
2314
2315 foreach ($records as $index => $record) {
2316 // Try to detect content type first
2317 $new_content_type = $this->detect_content_type($record->page_url);
2318
2319 // If detection fails, distribute evenly
2320 if ($new_content_type === 'embedpress' || empty($new_content_type)) {
2321 if ($index < $per_type) {
2322 $new_content_type = 'elementor';
2323 } elseif ($index < $per_type * 2) {
2324 $new_content_type = 'gutenberg';
2325 } else {
2326 $new_content_type = 'shortcode';
2327 }
2328 }
2329
2330 $result = $wpdb->update(
2331 $content_table,
2332 ['content_type' => $new_content_type],
2333 ['id' => $record->id],
2334 ['%s'],
2335 ['%d']
2336 );
2337
2338 if ($result !== false) {
2339 $updated++;
2340 $distribution[$new_content_type]++;
2341 } else {
2342 $errors++;
2343 }
2344 }
2345
2346 return [
2347 'total_records' => $total_records,
2348 'updated' => $updated,
2349 'errors' => $errors,
2350 'distribution' => $distribution,
2351 'message' => "Migrated $updated records with distribution: " . json_encode($distribution)
2352 ];
2353 }
2354
2355 /**
2356 * Get analytics performance statistics
2357 *
2358 * @return array
2359 */
2360 public function get_performance_stats()
2361 {
2362 global $wpdb;
2363
2364 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2365 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
2366
2367 // Get total counts
2368 $total_views = $wpdb->get_var("SELECT COUNT(*) FROM $views_table");
2369 $total_browser_info = $wpdb->get_var("SELECT COUNT(*) FROM $browser_table");
2370
2371 // Get unique user counts (new tracking)
2372 $unique_users = $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM $views_table WHERE user_id IS NOT NULL");
2373 $unique_browser_fingerprints = $wpdb->get_var("SELECT COUNT(DISTINCT browser_fingerprint) FROM $browser_table WHERE browser_fingerprint IS NOT NULL");
2374
2375 // Get potential duplicates (old tracking)
2376 $potential_duplicate_views = $wpdb->get_var("
2377 SELECT COUNT(*) FROM (
2378 SELECT user_id, content_id, interaction_type, COUNT(*) as cnt
2379 FROM $views_table
2380 WHERE user_id IS NOT NULL
2381 GROUP BY user_id, content_id, interaction_type
2382 HAVING cnt > 1
2383 ) as duplicates
2384 ");
2385
2386 $potential_duplicate_browser_info = $wpdb->get_var("
2387 SELECT COUNT(*) FROM (
2388 SELECT user_id, browser_fingerprint, COUNT(*) as cnt
2389 FROM $browser_table
2390 WHERE user_id IS NOT NULL AND browser_fingerprint IS NOT NULL
2391 GROUP BY user_id, browser_fingerprint
2392 HAVING cnt > 1
2393 ) as duplicates
2394 ");
2395
2396 // Calculate efficiency metrics
2397 $deduplication_ratio = $unique_users > 0 ? round(($total_views / $unique_users), 2) : 0;
2398 $browser_efficiency = $unique_browser_fingerprints > 0 ? round(($total_browser_info / $unique_browser_fingerprints), 2) : 0;
2399
2400 return [
2401 'total_interactions' => (int) $total_views,
2402 'unique_users' => (int) $unique_users,
2403 'total_browser_records' => (int) $total_browser_info,
2404 'unique_browser_fingerprints' => (int) $unique_browser_fingerprints,
2405 'potential_duplicate_interactions' => (int) $potential_duplicate_views,
2406 'potential_duplicate_browser_records' => (int) $potential_duplicate_browser_info,
2407 'deduplication_ratio' => $deduplication_ratio,
2408 'browser_efficiency_ratio' => $browser_efficiency,
2409 'performance_score' => $this->calculate_performance_score($deduplication_ratio, $browser_efficiency),
2410 'recommendations' => $this->get_performance_recommendations($potential_duplicate_views, $potential_duplicate_browser_info)
2411 ];
2412 }
2413
2414 /**
2415 * Calculate performance score based on efficiency metrics
2416 *
2417 * @param float $deduplication_ratio
2418 * @param float $browser_efficiency
2419 * @return array
2420 */
2421 private function calculate_performance_score($deduplication_ratio, $browser_efficiency)
2422 {
2423 // Lower ratios are better (less redundancy)
2424 $dedup_score = max(0, 100 - ($deduplication_ratio * 10));
2425 $browser_score = max(0, 100 - ($browser_efficiency * 20));
2426
2427 $overall_score = ($dedup_score + $browser_score) / 2;
2428
2429 $grade = 'A';
2430 if ($overall_score < 90) $grade = 'B';
2431 if ($overall_score < 80) $grade = 'C';
2432 if ($overall_score < 70) $grade = 'D';
2433 if ($overall_score < 60) $grade = 'F';
2434
2435 return [
2436 'score' => round($overall_score, 1),
2437 'grade' => $grade,
2438 'deduplication_score' => round($dedup_score, 1),
2439 'browser_efficiency_score' => round($browser_score, 1)
2440 ];
2441 }
2442
2443 /**
2444 * Get performance recommendations
2445 *
2446 * @param int $duplicate_views
2447 * @param int $duplicate_browser_info
2448 * @return array
2449 */
2450 private function get_performance_recommendations($duplicate_views, $duplicate_browser_info)
2451 {
2452 $recommendations = [];
2453
2454 if ($duplicate_views > 100) {
2455 $recommendations[] = 'Run cleanup to remove duplicate interaction records';
2456 }
2457
2458 if ($duplicate_browser_info > 50) {
2459 $recommendations[] = 'Run cleanup to remove redundant browser information';
2460 }
2461
2462 if (empty($recommendations)) {
2463 $recommendations[] = 'Your analytics database is optimized!';
2464 }
2465
2466 return $recommendations;
2467 }
2468
2469 /**
2470 * Track referrer analytics with optimized counting
2471 *
2472 * @param string $referrer_url
2473 * @param string $interaction_type
2474 * @param string $user_identifier
2475 * @return bool
2476 */
2477 public function track_referrer_analytics($referrer_url, $interaction_type, $user_identifier)
2478 {
2479 global $wpdb;
2480
2481 // Skip if no referrer or internal referrer
2482 if (empty($referrer_url) || $this->is_internal_referrer($referrer_url)) {
2483 return false;
2484 }
2485
2486 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2487
2488 // Parse referrer URL and extract components
2489 $referrer_data = $this->parse_referrer_url($referrer_url);
2490
2491 // Check if referrer already exists
2492 $existing_referrer = $wpdb->get_row($wpdb->prepare(
2493 "SELECT * FROM $referrers_table WHERE referrer_url = %s LIMIT 1",
2494 $referrer_url
2495 ));
2496
2497 if ($existing_referrer) {
2498 // Update existing referrer
2499 return $this->update_referrer_counts($existing_referrer->id, $interaction_type, $user_identifier);
2500 } else {
2501 // Create new referrer record
2502 return $this->create_referrer_record($referrer_data, $interaction_type, $user_identifier);
2503 }
2504 }
2505
2506 /**
2507 * Parse referrer URL and extract components
2508 *
2509 * @param string $referrer_url
2510 * @return array
2511 */
2512 private function parse_referrer_url($referrer_url)
2513 {
2514 $parsed = parse_url($referrer_url);
2515 $domain = isset($parsed['host']) ? $parsed['host'] : '';
2516
2517 // Extract UTM parameters
2518 $utm_params = [];
2519 if (isset($parsed['query'])) {
2520 parse_str($parsed['query'], $query_params);
2521 $utm_params = [
2522 'utm_source' => isset($query_params['utm_source']) ? sanitize_text_field($query_params['utm_source']) : null,
2523 'utm_medium' => isset($query_params['utm_medium']) ? sanitize_text_field($query_params['utm_medium']) : null,
2524 'utm_campaign' => isset($query_params['utm_campaign']) ? sanitize_text_field($query_params['utm_campaign']) : null,
2525 'utm_term' => isset($query_params['utm_term']) ? sanitize_text_field($query_params['utm_term']) : null,
2526 'utm_content' => isset($query_params['utm_content']) ? sanitize_text_field($query_params['utm_content']) : null,
2527 ];
2528 }
2529
2530 // Determine referrer source
2531 $referrer_source = $this->determine_referrer_source($domain, $utm_params['utm_source']);
2532
2533 return [
2534 'referrer_url' => esc_url_raw($referrer_url),
2535 'referrer_domain' => sanitize_text_field($domain),
2536 'referrer_source' => $referrer_source,
2537 'utm_source' => $utm_params['utm_source'],
2538 'utm_medium' => $utm_params['utm_medium'],
2539 'utm_campaign' => $utm_params['utm_campaign'],
2540 'utm_term' => $utm_params['utm_term'],
2541 'utm_content' => $utm_params['utm_content'],
2542 ];
2543 }
2544
2545 /**
2546 * Determine referrer source from domain or UTM source
2547 *
2548 * @param string $domain
2549 * @param string $utm_source
2550 * @return string
2551 */
2552 private function determine_referrer_source($domain, $utm_source)
2553 {
2554 // Use UTM source if available
2555 if (!empty($utm_source)) {
2556 return sanitize_text_field($utm_source);
2557 }
2558
2559 // Map common domains to sources
2560 $domain_mapping = [
2561 'google.com' => 'Google',
2562 'google.co.uk' => 'Google',
2563 'google.ca' => 'Google',
2564 'facebook.com' => 'Facebook',
2565 'l.facebook.com' => 'Facebook',
2566 'twitter.com' => 'Twitter',
2567 't.co' => 'Twitter',
2568 'linkedin.com' => 'LinkedIn',
2569 'youtube.com' => 'YouTube',
2570 'instagram.com' => 'Instagram',
2571 'tiktok.com' => 'TikTok',
2572 'pinterest.com' => 'Pinterest',
2573 'reddit.com' => 'Reddit',
2574 'bing.com' => 'Bing',
2575 'yahoo.com' => 'Yahoo',
2576 'duckduckgo.com' => 'DuckDuckGo',
2577 ];
2578
2579 foreach ($domain_mapping as $pattern => $source) {
2580 if (strpos($domain, $pattern) !== false) {
2581 return $source;
2582 }
2583 }
2584
2585 // Return domain as source if no mapping found
2586 return sanitize_text_field($domain);
2587 }
2588
2589 /**
2590 * Check if referrer is internal (same domain)
2591 *
2592 * @param string $referrer_url
2593 * @return bool
2594 */
2595 private function is_internal_referrer($referrer_url)
2596 {
2597 $site_domain = parse_url(site_url(), PHP_URL_HOST);
2598 $referrer_domain = parse_url($referrer_url, PHP_URL_HOST);
2599
2600 return $site_domain === $referrer_domain;
2601 }
2602
2603 /**
2604 * Create new referrer record
2605 *
2606 * @param array $referrer_data
2607 * @param string $interaction_type
2608 * @param string $user_identifier
2609 * @return bool
2610 */
2611 private function create_referrer_record($referrer_data, $interaction_type, $user_identifier)
2612 {
2613 global $wpdb;
2614
2615 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2616
2617 $data = array_merge($referrer_data, [
2618 'total_views' => $interaction_type === 'view' ? 1 : 0,
2619 'total_clicks' => $interaction_type === 'click' ? 1 : 0,
2620 'unique_visitors' => 1,
2621 'first_visit' => current_time('mysql'),
2622 'last_visit' => current_time('mysql'),
2623 'created_at' => current_time('mysql'),
2624 'updated_at' => current_time('mysql'),
2625 ]);
2626
2627 $result = $wpdb->insert($referrers_table, $data);
2628
2629 return $result !== false;
2630 }
2631
2632 /**
2633 * Update referrer counts
2634 *
2635 * @param int $referrer_id
2636 * @param string $interaction_type
2637 * @param string $user_identifier
2638 * @return bool
2639 */
2640 private function update_referrer_counts($referrer_id, $interaction_type, $user_identifier)
2641 {
2642 global $wpdb;
2643
2644 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2645
2646 // Check if this is a new unique visitor for this referrer
2647 $is_new_visitor = $this->is_new_referrer_visitor($referrer_id, $user_identifier);
2648
2649 // Build update query
2650 $update_data = [
2651 'last_visit' => current_time('mysql'),
2652 'updated_at' => current_time('mysql'),
2653 ];
2654
2655 if ($interaction_type === 'view') {
2656 $update_data['total_views'] = new \stdClass(); // Will be handled in raw SQL
2657 } elseif ($interaction_type === 'click') {
2658 $update_data['total_clicks'] = new \stdClass(); // Will be handled in raw SQL
2659 }
2660
2661 if ($is_new_visitor) {
2662 $update_data['unique_visitors'] = new \stdClass(); // Will be handled in raw SQL
2663 }
2664
2665 // Use raw SQL for atomic increments
2666 $sql_parts = [];
2667 $sql_parts[] = "last_visit = %s";
2668 $sql_parts[] = "updated_at = %s";
2669
2670 $values = [current_time('mysql'), current_time('mysql')];
2671
2672 if ($interaction_type === 'view') {
2673 $sql_parts[] = "total_views = total_views + 1";
2674 } elseif ($interaction_type === 'click') {
2675 $sql_parts[] = "total_clicks = total_clicks + 1";
2676 }
2677
2678 if ($is_new_visitor) {
2679 $sql_parts[] = "unique_visitors = unique_visitors + 1";
2680 }
2681
2682 $values[] = $referrer_id;
2683
2684 $sql = "UPDATE $referrers_table SET " . implode(', ', $sql_parts) . " WHERE id = %d";
2685
2686 return $wpdb->query($wpdb->prepare($sql, $values)) !== false;
2687 }
2688
2689
2690
2691 /**
2692 * Check if user is a new visitor for this referrer using existing views table
2693 *
2694 * @param int $referrer_id
2695 * @param string $user_identifier
2696 * @return bool
2697 */
2698 private function is_new_referrer_visitor($referrer_id, $user_identifier)
2699 {
2700 global $wpdb;
2701
2702 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2703 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2704
2705 // Get the referrer URL for this referrer_id
2706 $referrer_url = $wpdb->get_var($wpdb->prepare(
2707 "SELECT referrer_url FROM $referrers_table WHERE id = %d",
2708 $referrer_id
2709 ));
2710
2711 if (!$referrer_url) {
2712 return true; // If referrer doesn't exist, consider it new
2713 }
2714
2715 // Check if this user has any previous interactions with this referrer URL
2716 $existing_interaction = $wpdb->get_var($wpdb->prepare(
2717 "SELECT COUNT(*) FROM $views_table
2718 WHERE (user_id = %s OR session_id = %s)
2719 AND referrer_url = %s
2720 LIMIT 1",
2721 $user_identifier,
2722 $user_identifier,
2723 $referrer_url
2724 ));
2725
2726 return $existing_interaction == 0;
2727 }
2728
2729 /**
2730 * Track referrer analytics from interaction data
2731 *
2732 * @param array $data
2733 * @param string $interaction_type
2734 * @param string $user_identifier
2735 * @return void
2736 */
2737 private function track_referrer_from_interaction_data($data, $interaction_type, $user_identifier)
2738 {
2739 // Get referrer URL from various sources
2740 $referrer_url = $this->extract_referrer_from_data($data);
2741
2742 if (!empty($referrer_url)) {
2743 // Only track views and clicks for referrer analytics
2744 if (in_array($interaction_type, ['view', 'click'])) {
2745 $this->track_referrer_analytics($referrer_url, $interaction_type, $user_identifier);
2746 }
2747 }
2748 }
2749
2750 /**
2751 * Extract referrer URL from interaction data
2752 *
2753 * @param array $data
2754 * @return string
2755 */
2756 private function extract_referrer_from_data($data)
2757 {
2758 $referrer_url = '';
2759
2760 // Priority 1: Client-side original referrer from JavaScript (most reliable)
2761 if (isset($data['original_referrer']) && !empty($data['original_referrer'])) {
2762 $client_referrer = esc_url_raw($data['original_referrer']);
2763 $current_site_url = home_url();
2764 // Only use if it's external
2765 if (strpos($client_referrer, $current_site_url) !== 0) {
2766 $referrer_url = $client_referrer;
2767 }
2768 }
2769
2770 // Priority 2: Use the original referrer captured in main plugin file (fallback)
2771 if (empty($referrer_url) && defined('EMBEDPRESS_ORIGINAL_REFERRER') && !empty(EMBEDPRESS_ORIGINAL_REFERRER)) {
2772 $referrer_url = esc_url_raw(EMBEDPRESS_ORIGINAL_REFERRER);
2773 }
2774
2775 // Priority 3: HTTP referrer header (last resort)
2776 if (empty($referrer_url) && isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER'])) {
2777 $http_referrer = esc_url_raw($_SERVER['HTTP_REFERER']);
2778 $current_site_url = home_url();
2779 // Only use if it's external
2780 if (strpos($http_referrer, $current_site_url) !== 0) {
2781 $referrer_url = $http_referrer;
2782 }
2783 }
2784
2785 return $referrer_url;
2786 }
2787
2788 /**
2789 * Get referrer analytics data
2790 *
2791 * @param array $args
2792 * @return array
2793 */
2794 public function get_referrer_analytics($args = [])
2795 {
2796 global $wpdb;
2797
2798 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2799
2800 // Build date condition
2801 $date_condition = $this->build_date_condition($args, 'created_at');
2802
2803 // Build order by clause
2804 $order_by = isset($args['order_by']) ? sanitize_text_field($args['order_by']) : 'total_views';
2805 $allowed_order_fields = ['total_views', 'total_clicks', 'unique_visitors', 'last_visit', 'created_at'];
2806 if (!in_array($order_by, $allowed_order_fields)) {
2807 $order_by = 'total_views';
2808 }
2809
2810 // Build limit clause
2811 $limit = isset($args['limit']) ? absint($args['limit']) : 50;
2812 $limit = min($limit, 200); // Cap at 200 for performance
2813
2814 // Get referrer data
2815 $referrers = $wpdb->get_results($wpdb->prepare(
2816 "SELECT
2817 id,
2818 referrer_url,
2819 referrer_domain,
2820 referrer_source,
2821 utm_source,
2822 utm_medium,
2823 utm_campaign,
2824 utm_term,
2825 utm_content,
2826 total_views,
2827 total_clicks,
2828 unique_visitors,
2829 first_visit,
2830 last_visit,
2831 created_at
2832 FROM $referrers_table
2833 WHERE 1=1 $date_condition
2834 ORDER BY $order_by DESC, id DESC
2835 LIMIT %d",
2836 $limit
2837 ), ARRAY_A);
2838
2839 // Calculate totals
2840 $totals = $wpdb->get_row(
2841 "SELECT
2842 SUM(total_views) as total_views,
2843 SUM(total_clicks) as total_clicks,
2844 SUM(unique_visitors) as total_unique_visitors,
2845 COUNT(*) as total_referrers
2846 FROM $referrers_table
2847 WHERE 1=1 $date_condition",
2848 ARRAY_A
2849 );
2850
2851 // Get top sources summary
2852 $top_sources = $wpdb->get_results(
2853 "SELECT
2854 referrer_source,
2855 SUM(total_views) as total_views,
2856 SUM(total_clicks) as total_clicks,
2857 SUM(unique_visitors) as total_unique_visitors,
2858 COUNT(*) as referrer_count
2859 FROM $referrers_table
2860 WHERE 1=1 $date_condition
2861 GROUP BY referrer_source
2862 ORDER BY total_views DESC
2863 LIMIT 10",
2864 ARRAY_A
2865 );
2866
2867 // Process referrers data
2868 $processed_referrers = [];
2869 foreach ($referrers as $referrer) {
2870 $processed_referrers[] = [
2871 'id' => intval($referrer['id']),
2872 'referrer_url' => $referrer['referrer_url'],
2873 'referrer_domain' => $referrer['referrer_domain'],
2874 'referrer_source' => $referrer['referrer_source'],
2875 'utm_source' => $referrer['utm_source'],
2876 'utm_medium' => $referrer['utm_medium'],
2877 'utm_campaign' => $referrer['utm_campaign'],
2878 'utm_term' => $referrer['utm_term'],
2879 'utm_content' => $referrer['utm_content'],
2880 'total_views' => intval($referrer['total_views']),
2881 'total_clicks' => intval($referrer['total_clicks']),
2882 'unique_visitors' => intval($referrer['unique_visitors']),
2883 'click_through_rate' => intval($referrer['total_views']) > 0
2884 ? round((intval($referrer['total_clicks']) / intval($referrer['total_views'])) * 100, 2)
2885 : 0,
2886 'first_visit' => $referrer['first_visit'],
2887 'last_visit' => $referrer['last_visit'],
2888 'created_at' => $referrer['created_at']
2889 ];
2890 }
2891
2892 return [
2893 'referrers' => $processed_referrers,
2894 'totals' => [
2895 'total_views' => intval($totals['total_views'] ?: 0),
2896 'total_clicks' => intval($totals['total_clicks'] ?: 0),
2897 'total_unique_visitors' => intval($totals['total_unique_visitors'] ?: 0),
2898 'total_referrers' => intval($totals['total_referrers'] ?: 0),
2899 'overall_ctr' => intval($totals['total_views'] ?: 0) > 0
2900 ? round((intval($totals['total_clicks'] ?: 0) / intval($totals['total_views'] ?: 0)) * 100, 2)
2901 : 0
2902 ],
2903 'top_sources' => array_map(function ($source) {
2904 return [
2905 'source' => $source['referrer_source'],
2906 'total_views' => intval($source['total_views']),
2907 'total_clicks' => intval($source['total_clicks']),
2908 'unique_visitors' => intval($source['total_unique_visitors']),
2909 'referrer_count' => intval($source['referrer_count']),
2910 'click_through_rate' => intval($source['total_views']) > 0
2911 ? round((intval($source['total_clicks']) / intval($source['total_views'])) * 100, 2)
2912 : 0
2913 ];
2914 }, $top_sources),
2915 'date_range' => $args
2916 ];
2917 }
2918 }
2919