PluginProbe
FV Player 8 / 8.0.21
FV Player 8 v8.0.21
trunk 8.0.18 8.0.19 8.0.20 8.0.21 8.0.25 8.0.27 8.1 8.1.3
fv-player / models / stats.php

stats.php in FV Player 8 8.0.21, at models/stats.php

1,734 lines 61.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class FV_Player_Stats {
8
9 var $used = false;
10 var $cache_directory = false;
11
12 public function __construct() {
13 global $fv_fp;
14 $this->cache_directory = WP_CONTENT_DIR."/fv-player-tracking";
15
16 add_action( 'admin_init', array( $this, 'register_meta_boxes' ), 9 );
17
18 add_filter( 'fv_flowplayer_conf', array( $this, 'option' ) );
19
20 add_filter( 'fv_flowplayer_attributes', array( $this, 'shortcode' ), 10, 3 );
21
22 if ( function_exists('wp_next_scheduled') ) {
23 if( !wp_next_scheduled( 'fv_player_stats' ) && $fv_fp->_get_option('video_stats_enable')) {
24 wp_schedule_event( time(), '5minutes', 'fv_player_stats' );
25 } else if( wp_next_scheduled( 'fv_player_stats' ) && !$fv_fp->_get_option('video_stats_enable') ) {
26 wp_clear_scheduled_hook( 'fv_player_stats' );
27 }
28 }
29
30 add_action( 'fv_player_stats', array ( $this, 'parse_cached_files' ) );
31
32 add_action( 'fv_player_update', array( $this, 'db_init' ) );
33
34 // add_action( 'admin_init', array( $this, 'db_init' ) );
35
36 add_action( 'admin_init', array( $this, 'folder_init' ) );
37
38 add_action( 'admin_menu', array( $this, 'stats_link' ), 13 );
39
40 add_filter( 'manage_users_columns', array( $this, 'users_column' ) );
41 add_filter( 'manage_users_custom_column', array( $this, 'users_column_content' ), 10, 3 );
42 add_filter( 'manage_users_sortable_columns', array( $this, 'users_sortable_columns' ) );
43
44 if( is_admin() ) {
45 add_action( 'pre_user_query', array( $this, 'users_sort' ) );
46 add_action( 'wp_ajax_fv_player_stats_users_search', array( $this, 'user_stats_search' ) );
47 }
48
49 }
50
51 function stats_link() {
52 global $fv_fp;
53 if ( $fv_fp->_get_option('video_stats_enable') ) {
54 add_submenu_page( 'fv_player', 'FV Player Stats', 'Stats', 'manage_options', 'fv_player_stats', 'fv_player_stats_page' );
55 add_submenu_page( 'fv_player', 'FV Player User Stats', 'User Stats', 'manage_options', 'fv_player_stats_users', 'fv_player_stats_page' );
56 }
57 }
58
59 function get_stat_columns() {
60 return array( 'play', 'seconds', 'click' );
61 }
62
63 public static function get_table_name() {
64 global $wpdb;
65 return $wpdb->prefix . 'fv_player_stats';
66 }
67
68 function db_init( $force = false ) {
69 global $fv_fp;
70
71 if( !$force && !$fv_fp->_get_option('video_stats_enable') ) {
72 return;
73 }
74
75 global $wpdb;
76 $table_name = $this->get_table_name();
77
78 $sql = "CREATE TABLE `$table_name` (
79 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
80 `id_video` INT(11) NOT NULL,
81 `id_player` INT(11) NOT NULL,
82 `id_post` INT(11) NOT NULL,
83 `user_id` INT(11) NOT NULL,
84 `guest_user_id` INT(11) NOT NULL,
85 `date` DATE NULL DEFAULT NULL,\n";
86
87 foreach( $this->get_stat_columns() AS $column ) {
88 $sql .= "`".$column."` INT(11) NOT NULL,\n";
89 }
90
91 $sql .= "PRIMARY KEY (`id`),
92 INDEX `date` (`date`),
93 INDEX `id_video` (`id_video`),
94 INDEX `id_player` (`id_player`),
95 INDEX `id_post` (`id_post`),
96 INDEX `user_id` (`user_id`),
97 INDEX `guest_user_id` (`guest_user_id`)
98 ) " . $wpdb->get_charset_collate() . ";";
99
100 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
101
102 dbDelta($sql);
103 }
104
105 function folder_init( $force = false ) {
106 if ( !WP_Filesystem() ) {
107 return;
108 }
109
110 global $fv_fp;
111 global $wp_filesystem;
112
113 if( !$force && !$fv_fp->_get_option('video_stats_enable') ) {
114 if( $wp_filesystem->exists( $this->cache_directory ) ) {
115 $wp_filesystem->rmdir( $this->cache_directory, true );
116 }
117
118 return;
119 }
120
121 if( !$wp_filesystem->exists($this->cache_directory) ){
122 $wp_filesystem->mkdir( $this->cache_directory );
123 }
124 }
125
126 function option( $conf ) {
127 global $fv_fp, $blog_id;
128 if( $this->used || $fv_fp->_get_option('js-everywhere') || $fv_fp->_get_option('video_stats_enable') ) { // we want to enable the tracking if it's used, if FV Player JS is enabled globally or if the tracking is enabled globally
129
130 // Do not track admins and editors.
131 if ( current_user_can( 'edit_posts' ) ) {
132 return $conf;
133 }
134
135 $conf['fv_stats'] = array(
136 'url' => flowplayer::get_plugin_url().'/controller/track.php',
137 'blog_id' => $blog_id,
138 'user_id' => get_current_user_id(),
139 'nonce' => wp_create_nonce( 'fv_player_track' ),
140 );
141 if( $fv_fp->_get_option('video_stats_enable') ) $conf['fv_stats']['enabled'] = true;
142
143 }
144 return $conf;
145 }
146
147 function register_meta_boxes() {
148 add_meta_box( 'fv_player_stats' , 'Video Stats', array( $this, 'options_html' ), 'fv_flowplayer_settings', 'normal', 'low' );
149 }
150
151 function options_html() {
152 global $fv_fp;
153 ?>
154 <p><?php esc_html_e( 'Track user activity on your site. Administrators and Editors are excluded. You can see the stats in the FV Player menu.', 'fv-player' ); ?></p>
155 <table class="form-table2">
156 <?php
157 $fv_fp->_get_checkbox(__( 'Enable', 'fv-player' ), 'video_stats_enable', __('Gives you a daily count of video plays.'), __('Uses a simple PHP script with a cron job to make sure these stats don\'t slow down your server too much.'));
158 $fv_fp->_get_checkbox(__( 'Track Guest User IDs', 'fv-player' ), 'video_stats_enable_guest', __('Uses cookies to remember non-logged in users returning to website. Leave disabled to only get summary stats for all non-logged in users.'), '');
159 ?>
160 <tr>
161 <td colspan="4">
162 <a class="fv-wordpress-flowplayer-save button button-primary" href="#"><?php esc_html_e( 'Save', 'fv-player' ); ?></a>
163 <a class="button fv-help-link" href="https://foliovision.com/player/analytics/user-stats" target="_blank">Help</a>
164 </td>
165 </tr>
166 </table>
167 <?php
168 }
169
170 function shortcode( $attributes, $media, $fv_fp ) {
171 if( !empty($fv_fp->aCurArgs['stats']) ) {
172 if( $fv_fp->aCurArgs['stats'] != 'no' ) {
173 $this->used = true;
174 }
175 $attributes['data-fv_stats'] = $fv_fp->aCurArgs['stats'];
176 }
177
178 if( !empty($fv_fp->aCurArgs['stats']) || $fv_fp->_get_option('video_stats_enable') ) {
179 global $post;
180
181 $player_id = 0; // 0 if shortcode
182
183 if( $fv_fp->current_player() ) {
184 $player_id = $fv_fp->current_player()->getId();
185 }
186
187 if( !empty($post->ID ) ) {
188 // TODO: Add signature to avoid faking the stats by users
189 $attributes['data-fv_stats_data'] = wp_json_encode( array(
190 'player_id' => $player_id,
191 'post_id' => $post->ID,
192 ) );
193 }
194 }
195
196 return $attributes;
197 }
198
199 /**
200 * Process post counters from cache file and update post meta
201 * @param resource &$fp file handler
202 * @param string $type Type of stats being parsed
203 * @return void
204 */
205 function process_cached_data( &$fp, $type ) {
206 global $wpdb;
207
208 $table_name = $this->get_table_name();
209
210 if( !in_array($type, $this->get_stat_columns() ) ) return;
211
212 if( flock( $fp, LOCK_EX ) ) {
213 $encoded_data = fgets( $fp );
214 $data = json_decode( $encoded_data, true );
215
216 ftruncate( $fp, 0 );
217 //UNLOCK, process data later
218 flock( $fp, LOCK_UN );
219
220 $json_error = json_last_error();
221 if( $json_error !== JSON_ERROR_NONE ) {
222 //file_put_contents( ABSPATH . 'failed_json_decode.log', gmdate('r')."\n".var_export( array( 'err' => $json_error, 'data' => $encoded_data ), true )."\n", FILE_APPEND );
223 return;
224 }
225
226 if( !is_array( $data ) || empty( $data ) )
227 return;
228
229 if( is_array($data) ) {
230 foreach( $data AS $index => $item ) {
231 $video_id = intval($item['video_id']);
232 $player_id = intval($item['player_id']);
233 $post_id = intval($item['post_id']);
234 $user_id = intval($item['user_id']);
235 $guest_user_id = intval($item['guest_user_id']);
236 $value = intval($item[$type]);
237
238 if( $user_id ) {
239 $meta_key = 'fv_player_stats_'.$type;
240 $meta_value = $value + intval( get_user_meta( $user_id, $meta_key, true ) );
241 if( $meta_value > 0 ) {
242 update_user_meta( $user_id, $meta_key, $meta_value );
243 }
244
245 }
246
247 if( $video_id ) {
248 global $FV_Player_Db;
249 $video = new FV_Player_Db_Video( $video_id, array(), $FV_Player_Db );
250
251 if( $video ) {
252 $meta_value = $value + intval($video->getMetaValue('stats_'.$type,true));
253 if( $meta_value > 0 ) {
254 $video->updateMetaValue( 'stats_'.$type, $meta_value );
255 }
256 }
257 }
258
259 $existing = $wpdb->get_row( $wpdb->prepare("SELECT * FROM `{$wpdb->prefix}fv_player_stats` WHERE date = %s AND id_video = %d AND id_post = %d AND id_player = %d AND user_id = %d AND guest_user_id = %d", date_i18n( 'Y-m-d' ), $video_id, $post_id, $player_id, $user_id, $guest_user_id ) );
260
261 if( $existing ) {
262 $wpdb->update(
263 $table_name,
264 array(
265 $type => $value + $existing->{$type}, // update plays in db
266 ),
267 array( 'id_video' => $video_id , 'date' => date_i18n( 'Y-m-d' ), 'id_player' => $player_id, 'id_post' => $post_id, 'user_id' => $user_id, 'guest_user_id' => $guest_user_id ), // update by video id, date, player id, post id, user ID and guest user ID
268 array(
269 '%d'
270 ),
271 array(
272 '%d',
273 '%s',
274 '%d',
275 '%d',
276 '%d'
277 )
278 );
279 } else { // insert new row
280 $wpdb->insert(
281 $table_name,
282 array(
283 'id_video' => $video_id,
284 'id_player' => $player_id,
285 'id_post' => $post_id,
286 'user_id' => $user_id,
287 'guest_user_id' => $guest_user_id,
288 'date' => date_i18n( 'Y-m-d' ),
289 $type => $value
290 ),
291 array(
292 '%d',
293 '%d',
294 '%d',
295 '%d',
296 '%d',
297 '%s',
298 '%d'
299 )
300 );
301 }
302 }
303 }
304 }
305 else {
306 echo "Error: failed to obtain file lock.";
307 }
308 }
309
310 /**
311 * Loads directory with cache files, and process those, which belongs to current blog
312 * @return void
313 */
314 function parse_cached_files() {
315 // just in case...
316 $this->db_init( true );
317 $this->folder_init( true );
318
319 $cache_files = scandir( $this->cache_directory );
320 foreach( $cache_files as $filename ) {
321 if( preg_match( '/^([^-]+)-([^\.]+)\.data$/', $filename, $matches ) ) {
322 $type = $matches[1];
323 if( !in_array($type, $this->get_stat_columns() ) ) continue;
324
325 $blog_id = intval($matches[2]);
326
327 if( get_current_blog_id() != $blog_id ) continue;
328
329 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
330 $fp = fopen( $this->cache_directory."/".$filename, 'r+');
331 $this->process_cached_data( $fp, $type );
332
333 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
334 fclose( $fp );
335 }
336 }
337 }
338
339 public function top_ten_users_by_plays( $interval, $user_type = 'user' ) {
340 global $wpdb;
341
342 $excluded = $this->get_posts_to_exclude();
343
344 $offset = 0;
345 $limit = 50000;
346 $grouped = array();
347
348 // Determine limit by the amount of PHP memory available
349 if ( intval( ini_get('memory_limit') ) > 32 ) {
350 $limit = intval( ini_get('memory_limit') ) * 800;
351 }
352
353 do {
354 if( $user_type == 'user' ) {
355 $results = $wpdb->get_results(
356 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
357 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
358 $wpdb->prepare(
359 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
360 "SELECT user_id, play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT %d, %d",
361 array_merge(
362 array(
363 $interval[0],
364 $interval[1]
365 ),
366 $excluded['values'],
367 array(
368 $offset,
369 $limit
370 )
371 )
372 )
373 );
374
375 } else {
376 $results = $wpdb->get_results(
377 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
378 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
379 $wpdb->prepare(
380 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
381 "SELECT guest_user_id AS user_id, play FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND guest_user_id > 0 LIMIT %d, %d",
382 array_merge(
383 array(
384 $interval[0],
385 $interval[1]
386 ),
387 $excluded['values'],
388 array(
389 $offset,
390 $limit
391 )
392 )
393 )
394 );
395 }
396
397 // Group by user ID and sum up the plays, it's faster in PHP than MySQL.
398 if ( ! empty( $results ) ) {
399 foreach( $results as $row ) {
400 $user_id = $row->user_id;
401 $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->play : $row->play;
402 }
403 }
404
405 $offset += $limit;
406
407 } while( ! empty( $results ) && count( $results ) >= $limit );
408
409 arsort( $grouped );
410
411 $grouped = array_slice( $grouped, 0, 10, true );
412
413 return array_keys( $grouped );
414 }
415
416 public function top_ten_users_by_watch_time( $interval, $user_type = 'user' ) {
417 global $wpdb;
418
419 $excluded = $this->get_posts_to_exclude();
420
421 $offset = 0;
422 $limit = 50000;
423 $grouped = array();
424
425 // Determine limit by the amount of PHP memory available
426 if ( intval( ini_get('memory_limit') ) > 32 ) {
427 $limit = intval( ini_get('memory_limit') ) * 800;
428 }
429
430 do {
431 if( $user_type == 'user' ) {
432 $results = $wpdb->get_results(
433 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
434 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
435 $wpdb->prepare(
436 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
437 "SELECT user_id, seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT %d, %d",
438 array_merge(
439 array(
440 $interval[0],
441 $interval[1]
442 ),
443 $excluded['values'],
444 array(
445 $offset,
446 $limit
447 )
448 )
449 )
450 );
451
452 } else {
453 $results = $wpdb->get_results(
454 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
455 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
456 $wpdb->prepare(
457 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
458 "SELECT guest_user_id AS user_id, seconds FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND guest_user_id > 0 LIMIT %d, %d",
459 array_merge(
460 array(
461 $interval[0],
462 $interval[1]
463 ),
464 $excluded['values'],
465 array(
466 $offset,
467 $limit
468 )
469 )
470 )
471 );
472 }
473
474 // Group by user ID and sum up the plays, it's faster in PHP than MySQL.
475 if ( ! empty( $results ) ) {
476 foreach( $results as $row ) {
477 $user_id = $row->user_id;
478 $grouped[ $user_id ] = isset( $grouped[ $user_id ] ) ? $grouped[ $user_id ] + $row->seconds : $row->seconds;
479 }
480 }
481
482 $offset += $limit;
483
484 } while( ! empty( $results ) && count( $results ) >= $limit );
485
486 arsort( $grouped );
487
488 $grouped = array_slice( $grouped, 0, 10, true );
489
490 return array_keys( $grouped );
491 }
492
493 public function top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ) {
494 global $wpdb;
495
496 // Sanitize input for SQL
497 if ( ! in_array( $type, array( 'post', 'video' ) ) ) {
498 $type = 'video';
499 }
500
501 $excluded = $this->get_posts_to_exclude();
502
503 if( is_numeric( $user_id ) ) {
504 $results = $wpdb->get_col(
505 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
506 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
507 $wpdb->prepare(
508 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
509 "SELECT id_" . esc_sql( $type ) . " FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d GROUP BY id_" . esc_sql( $type ) . " ORDER BY sum(play) DESC LIMIT 10",
510 array_merge(
511 array(
512 $interval[0],
513 $interval[1]
514 ),
515 $excluded['values'],
516 array(
517 $user_id
518 )
519 )
520 )
521 );
522
523 } else {
524 $results = $wpdb->get_col(
525 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
526 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
527 $wpdb->prepare(
528 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
529 "SELECT id_" . esc_sql( $type ) . " FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_" . esc_sql( $type ) . " ORDER BY sum(play) DESC LIMIT 10",
530 array_merge(
531 array(
532 $interval[0],
533 $interval[1]
534 ),
535 $excluded['values']
536 )
537 )
538 );
539 }
540
541 return $results;
542 }
543
544 public function top_ten_videos_by_watch_time( $interval, $user_id ) {
545 global $wpdb;
546
547 $valid_interval = $this->check_watch_time_in_interval( $interval, $user_id );
548
549 if( !$valid_interval ) {
550 return false;
551 }
552
553 $excluded = $this->get_posts_to_exclude();
554
555 if( is_numeric( $user_id ) ) {
556 $results = $wpdb->get_col(
557 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
558 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
559 $wpdb->prepare(
560 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
561 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d GROUP BY id_video ORDER BY sum(seconds) DESC LIMIT 10",
562 array_merge(
563 array(
564 $interval[0],
565 $interval[1]
566 ),
567 $excluded['values'],
568 array(
569 $user_id
570 )
571 )
572 )
573 );
574
575 } else {
576 $results = $wpdb->get_col(
577 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
578 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
579 $wpdb->prepare(
580 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
581 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_video ORDER BY sum(seconds) DESC LIMIT 10",
582 array_merge(
583 array(
584 $interval[0],
585 $interval[1]
586 ),
587 $excluded['values']
588 )
589 )
590 );
591 }
592
593 return $results;
594 }
595
596 public function get_video_ad_video_ids( $interval ) {
597 global $wpdb;
598
599 $excluded = $this->get_posts_to_exclude();
600
601 $results = $wpdb->get_col(
602 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
603 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
604 $wpdb->prepare(
605 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
606 "SELECT s.id_video as id_video FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videometa` AS m ON m.id_video = s.id_video WHERE m.meta_key = 'is_video_ad' AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY id_video",
607 array_merge(
608 array(
609 $interval[0],
610 $interval[1]
611 ),
612 $excluded['values']
613 )
614 )
615 );
616
617 return $results;
618 }
619
620 public function check_watch_time_in_interval( $interval, $user_id ) {
621 global $wpdb;
622
623 $excluded = $this->get_posts_to_exclude();
624
625 if( is_numeric( $user_id ) ) {
626 $results = $wpdb->get_col(
627 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
628 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
629 $wpdb->prepare(
630 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
631 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d AND seconds > 0 LIMIT 1",
632 array_merge(
633 array(
634 $interval[0],
635 $interval[1]
636 ),
637 $excluded['values'],
638 array(
639 $user_id
640 )
641 )
642 )
643 );
644
645 } else {
646 $results = $wpdb->get_col(
647 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
648 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
649 $wpdb->prepare(
650 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
651 "SELECT id_video FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND seconds > 0 LIMIT 1",
652 array_merge(
653 array(
654 $interval[0],
655 $interval[1]
656 ),
657 $excluded['values']
658 )
659 )
660 );
661 }
662
663 return !empty($results);
664 }
665
666 /**
667 * Get post IDs to exclude for stats
668 *
669 * @return array Array of post IDs with 0 value always included to make sure the query SQL is valid
670 */
671 public function get_posts_to_exclude() {
672
673 // exclude posts with filter
674 $exclude_posts_query_args = apply_filters( 'fv_player_stats_view_exclude_posts_query_args', false );
675 if( $exclude_posts_query_args ) {
676 $exclude_posts_query = new WP_Query( $exclude_posts_query_args );
677 if( !empty($exclude_posts_query->posts) ) {
678 // We count +1 for the 0 value
679 $placeholders = implode( ', ', array_fill( 0, count( $exclude_posts_query->posts ) + 1, '%d' ) );
680 }
681
682 return array(
683 'placeholder' => $placeholders,
684 // We append the 0 value too
685 'values' => array_merge( array( 0 ), wp_list_pluck( $exclude_posts_query->posts, 'ID' ) ),
686 );
687
688 }
689
690 // No posts to exclude? We still return the 0 post ID
691 return array(
692 'placeholder' => '%d',
693 'values' => array( 0 ),
694 );
695 }
696
697 public function get_top_user_stats( $metric, $range ) {
698 global $wpdb, $fv_fp;
699
700 // dynamic interval based on range
701 $interval = self::get_interval_from_range( $range );
702
703 $guest_stats = $fv_fp->_get_option('video_stats_enable_guest');
704
705 $datasets = false;
706 $top_ids_user = array();
707 $top_ids_arr_user = array();
708 $top_ids_guest = array();
709 $top_ids_arr_guest = array();
710 $top_ids_results_user = array();
711 $top_ids_results_guest = array();
712 $results_user = array();
713 $results_guest = array();
714 $datasets_users = array();
715 $datasets_guests = array();
716
717 if( $metric == 'play' ) { // play stats
718 $top_ids_results_user = $this->top_ten_users_by_plays( $interval, 'user' );
719 if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_plays( $interval, 'guest' );
720 } else { // watch time stats
721 $top_ids_results_user = $this->top_ten_users_by_watch_time( $interval, 'user' );
722 if( $guest_stats ) $top_ids_results_guest = $this->top_ten_users_by_watch_time( $interval, 'guest' );
723 }
724
725 // if both empty, return false
726 if ( empty( $top_ids_results_user ) && empty( $top_ids_results_guest ) ) {
727 return false;
728 }
729
730 // regular users
731 if( !empty($top_ids_results_user) ) {
732 $top_ids_arr_user = array_values( $top_ids_results_user );
733 $top_ids_user = array_map( 'intval', array_values( $top_ids_arr_user ) );
734
735 $placeholders = implode( ', ', array_fill( 0, count( $top_ids_user ), '%d' ) );
736
737 if( $metric == 'play' ) {
738 $results_user = $wpdb->get_results(
739 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
740 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
741 $wpdb->prepare(
742 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
743 "SELECT date, user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date",
744 array_merge(
745 array(
746 $interval[0],
747 $interval[1]
748 ),
749 $top_ids_user
750 )
751 ),
752 ARRAY_A
753 );
754
755 } else {
756 $results_user = $wpdb->get_results(
757 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
758 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
759 $wpdb->prepare(
760 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
761 "SELECT date, user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND user_id IN( $placeholders ) GROUP BY user_id, date",
762 array_merge(
763 array(
764 $interval[0],
765 $interval[1]
766 ),
767 $top_ids_user
768 )
769 ),
770 ARRAY_A
771 );
772 }
773 }
774
775 // guest users
776 if( $guest_stats && !empty($top_ids_results_guest) ) {
777 // TODO: Fix if empty, the SQL below will fail
778 $top_ids_arr_guest = array_values( $top_ids_results_guest );
779 $top_ids_guest = array_map( 'intval', array_values( $top_ids_arr_guest ) );
780
781 $placeholders = implode( ', ', array_fill( 0, count( $top_ids_guest ), '%d' ) );
782
783 if( $metric == 'play' ) {
784 $results_guest = $wpdb->get_results(
785 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
786 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
787 $wpdb->prepare(
788 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
789 "SELECT date, guest_user_id, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date",
790 array_merge(
791 array(
792 $interval[0],
793 $interval[1]
794 ),
795 $top_ids_guest
796 )
797 ),
798 ARRAY_A
799 );
800
801 } else {
802 $results_guest = $wpdb->get_results(
803 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
804 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
805 $wpdb->prepare(
806 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
807 "SELECT date, guest_user_id, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND guest_user_id IN( $placeholders ) GROUP BY guest_user_id, date",
808 array_merge(
809 array(
810 $interval[0],
811 $interval[1]
812 ),
813 $top_ids_guest
814 )
815 ),
816 ARRAY_A
817 );
818 }
819 }
820
821 // process data for regular users
822 if( !empty($results_user) ) {
823 $datasets_users = $this->process_graph_data( $results_user, $top_ids_arr_user, $range, 'user', $metric );
824 }
825
826 // process data for guest users
827 if( !empty($results_guest) ) {
828 $datasets_guests = $this->process_graph_data( $results_guest, $top_ids_arr_guest, $range, 'guest', $metric );
829 }
830
831 // merge datasets
832 $datasets = array_merge( $datasets_users, $datasets_guests );
833
834 return $datasets;
835 }
836
837 public function get_top_video_watch_time_stats( $range, $user_id ) {
838 global $wpdb;
839
840 // dynamic interval based on range
841 $interval = self::get_interval_from_range( $range );
842
843 $type = 'video';
844 $datasets = false;
845
846 $top_ids_results = $this->top_ten_videos_by_watch_time( $interval, $user_id ); // get top video ids
847
848 if( !empty($top_ids_results) ) {
849 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
850 $top_ids[] = 0; // add 0 to make sure the SQL is valid
851 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
852 } else {
853 return false;
854 }
855
856 if( is_numeric( $user_id ) ) {
857 $results = $wpdb->get_results(
858 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
859 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
860 $wpdb->prepare(
861 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
862 "SELECT date, id_player, id_video, title, src, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) AND user_id = %d GROUP BY id_video, date",
863 array_merge(
864 array(
865 $interval[0],
866 $interval[1]
867 ),
868 $top_ids,
869 array(
870 $user_id
871 )
872 )
873 ),
874 ARRAY_A
875 );
876
877 } else {
878 $results = $wpdb->get_results(
879 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
880 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
881 $wpdb->prepare(
882 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
883 "SELECT date, id_player, id_video, title, src, SUM(seconds) AS seconds FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
884 array_merge(
885 array(
886 $interval[0],
887 $interval[1]
888 ),
889 $top_ids
890 )
891 ),
892 ARRAY_A
893 );
894 }
895
896 if( !empty($results) ) {
897 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, 'seconds' );
898 }
899
900 return $datasets;
901 }
902
903 public function get_top_video_post_stats( $type, $range, $user_id ) {
904 global $wpdb;
905
906 // dynamic interval based on range
907 $interval = self::get_interval_from_range( $range );
908
909 $datasets = false;
910 $top_ids_results = $this->top_ten_videos_or_posts_by_plays( $type, $interval, $user_id ); // get top video ids
911
912 if( !empty($top_ids_results) ) {
913 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
914 $top_ids[] = 0; // add 0 to make sure the SQL is valid
915 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
916 } else {
917 return false;
918 }
919
920 if( is_numeric( $user_id ) ) {
921 if( $type == 'video' ) { // video stats
922 $results = $wpdb->get_results(
923 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
924 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
925 $wpdb->prepare(
926 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
927 "SELECT date, id_player, id_video, title, src, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) AND user_id = %d GROUP BY id_video, date",
928 array_merge(
929 array(
930 $interval[0],
931 $interval[1]
932 ),
933 $top_ids,
934 array(
935 $user_id
936 )
937 )
938 ),
939 ARRAY_A
940 );
941 } else if( $type == 'post' ) { // post stats
942 $results = $wpdb->get_results(
943 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
944 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
945 $wpdb->prepare(
946 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
947 "SELECT date, id_post, id_video, post_title, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}posts` AS p ON s.id_post = p.ID WHERE date BETWEEN %s AND %s AND id_post IN( $placeholders ) AND user_id = %d GROUP BY id_post, date",
948 array_merge(
949 array(
950 $interval[0],
951 $interval[1]
952 ),
953 $top_ids,
954 array(
955 $user_id
956 )
957 )
958 ),
959 ARRAY_A
960 );
961 }
962
963 } else {
964 if( $type == 'video' ) { // video stats
965 $results = $wpdb->get_results(
966 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
967 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
968 $wpdb->prepare(
969 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
970 "SELECT date, id_player, id_video, title, src, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
971 array_merge(
972 array(
973 $interval[0],
974 $interval[1]
975 ),
976 $top_ids
977 )
978 ),
979 ARRAY_A
980 );
981 } else if( $type == 'post' ) { // post stats
982 $results = $wpdb->get_results(
983 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
984 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
985 $wpdb->prepare(
986 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
987 "SELECT date, id_post, id_video, post_title, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}posts` AS p ON s.id_post = p.ID WHERE date BETWEEN %s AND %s AND id_post IN( $placeholders ) GROUP BY id_post, date",
988 array_merge(
989 array(
990 $interval[0],
991 $interval[1]
992 ),
993 $top_ids
994 )
995 ),
996 ARRAY_A
997 );
998 }
999 }
1000
1001 if( !empty($results) ) {
1002 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type );
1003 }
1004
1005 return $datasets;
1006 }
1007
1008 public function get_top_video_ad_data( $range, $metric ) {
1009 global $wpdb;
1010
1011 // dynamic interval based on range
1012 $interval = self::get_interval_from_range( $range );
1013
1014 $datasets = false;
1015
1016 // we track ads based on video
1017 $type = 'video';
1018
1019 $top_ids_results = $this->get_video_ad_video_ids( $interval );
1020
1021 if( !empty($top_ids_results) ) {
1022 $top_ids = array_map( 'intval', array_values( $top_ids_results ) );
1023 $top_ids[] = 0; // add 0 to make sure the SQL is valid
1024 $placeholders = implode( ', ', array_fill( 0, count( $top_ids ), '%d' ) );
1025 } else {
1026 return false;
1027 }
1028
1029 $results = $wpdb->get_results(
1030 // Explanation: $placeholders is created above and is a string for $wpdb->prepare(), it uses variable number of placements
1031 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1032 $wpdb->prepare(
1033 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1034 "SELECT date, id_player, id_video, title, src, SUM($metric) AS {$metric} FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND id_video IN( $placeholders ) GROUP BY id_video, date",
1035 array_merge(
1036 array(
1037 $interval[0],
1038 $interval[1]
1039 ),
1040 $top_ids
1041 )
1042 ),
1043 ARRAY_A
1044 );
1045
1046 if( !empty($results) ) {
1047 $datasets = $this->process_graph_data( $results, $top_ids, $range, $type, $metric );
1048 }
1049
1050 return $datasets;
1051 }
1052
1053 public function get_player_stats( $player_id, $range) {
1054 global $wpdb;
1055
1056 $interval = self::get_interval_from_range( $range );
1057 $datasets = false;
1058
1059 $results = $wpdb->get_results(
1060 $wpdb->prepare(
1061 "SELECT date, id_video, src, title, player_name, SUM(play) AS play FROM `{$wpdb->prefix}fv_player_stats` AS s JOIN `{$wpdb->prefix}fv_player_players` AS p ON s.id_player = p.id JOIN `{$wpdb->prefix}fv_player_videos` AS v ON s.id_video = v.id WHERE date BETWEEN %s AND %s AND s.id_player IN( %d ) GROUP BY date, id_video",
1062 $interval[0],
1063 $interval[1],
1064 $player_id
1065 ),
1066 ARRAY_A
1067 );
1068
1069 if( !empty($results) ) {
1070 $ids_arr = array();
1071 foreach( $results as $row ) {
1072 $ids_arr[] = $row['id_video'];
1073 }
1074
1075 // Make sure each video is only considered once, otherwise this ends up multiplying the stats is loading for one player only
1076 $ids_arr = array_unique( $ids_arr );
1077
1078 $datasets = $this->process_graph_data( $results, $ids_arr, $range, 'video' );
1079 }
1080
1081 return $datasets;
1082 }
1083
1084 public function get_users_by_time_range( $range, $user_id = false ) {
1085 global $wpdb;
1086
1087 $excluded = $this->get_posts_to_exclude();
1088 $interval = self::get_interval_from_range( $range );
1089
1090 if( $user_id ) {
1091 $result = $wpdb->get_results(
1092 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1093 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1094 $wpdb->prepare(
1095 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1096 "SELECT u.ID, display_name, user_email, SUM( play ) AS play FROM `{$wpdb->users}` AS u LEFT JOIN `{$wpdb->prefix}fv_player_stats` AS s ON u.ID = s.user_id AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) WHERE u.ID = %d GROUP BY u.ID ORDER BY display_name",
1097 array_merge(
1098 array(
1099 $interval[0],
1100 $interval[1]
1101 ),
1102 $excluded['values'],
1103 array(
1104 $user_id
1105 )
1106 )
1107 ),
1108 ARRAY_A
1109 );
1110
1111 } else {
1112 $result = $wpdb->get_results(
1113 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1114 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1115 $wpdb->prepare(
1116 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1117 "SELECT u.ID, display_name, user_email, SUM( play ) AS play FROM `{$wpdb->users}` AS u LEFT JOIN `{$wpdb->prefix}fv_player_stats` AS s ON u.ID = s.user_id AND date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) GROUP BY u.ID ORDER BY display_name",
1118 array_merge(
1119 array(
1120 $interval[0],
1121 $interval[1]
1122 ),
1123 $excluded['values']
1124 )
1125 ),
1126 ARRAY_A
1127 );
1128 }
1129
1130 if ( ! $result ) {
1131 $result = array();
1132 }
1133
1134 return $result;
1135 }
1136
1137 public function get_valid_dates( $user_id ) {
1138 global $wpdb;
1139
1140 $excluded = $this->get_posts_to_exclude();
1141
1142 $dates_all = array( 'this_week' => 'This Week', 'last_week' => 'Last Week', 'this_month' => 'This Month', 'last_month' => 'Last Month' );
1143 $years = $this->get_all_years();
1144 $dates_all = $dates_all + $years; // merge
1145 $dates_valid = array();
1146
1147 $this_year = (int) gmdate( 'Y' );
1148 $last_year = $this_year - 1;
1149
1150 foreach( $dates_all as $key => $value ) {
1151
1152 $interval = self::get_interval_from_range( $key );
1153
1154 if( $user_id ) {
1155 $result = $wpdb->get_results(
1156 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1157 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1158 $wpdb->prepare(
1159 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1160 "SELECT date FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) AND user_id = %d LIMIT 1",
1161 array_merge(
1162 array(
1163 $interval[0],
1164 $interval[1]
1165 ),
1166 $excluded['values'],
1167 array(
1168 $user_id
1169 )
1170 )
1171 ),
1172 ARRAY_A
1173 );
1174
1175 } else {
1176 $result = $wpdb->get_results(
1177 // Explanation: $excluded['placeholder'] comes from get_posts_to_exclude() and is a string for $wpdb->prepare(), it uses variable number of placements
1178 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1179 $wpdb->prepare(
1180 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1181 "SELECT date FROM `{$wpdb->prefix}fv_player_stats` WHERE date BETWEEN %s AND %s AND id_post NOT IN ( {$excluded['placeholder']} ) LIMIT 1",
1182 array_merge(
1183 array(
1184 $interval[0],
1185 $interval[1]
1186 ),
1187 $excluded['values']
1188 )
1189 ),
1190 ARRAY_A
1191 );
1192 }
1193
1194 if( $key == $this_year) {
1195 $key = 'this_year';
1196 $value = 'This Year';
1197 } else if( $key == $last_year ) {
1198 $key = 'last_year';
1199 $value = 'Last Year';
1200 }
1201
1202 $dates_valid[$key] = array();
1203
1204 if( !empty($result) ) {
1205 $dates_valid[$key]['disabled'] = false;
1206 } else {
1207 $dates_valid[$key]['disabled'] = true;
1208 }
1209
1210 $dates_valid[$key]['value'] = $value;
1211 }
1212
1213 return $dates_valid;
1214 }
1215
1216 public function get_valid_interval( $user_id ) {
1217 // we need to check every interval for user to check if there is any data
1218 $intervals = array(
1219 'this_week',
1220 'last_week',
1221 'this_month',
1222 'last_month',
1223 );
1224
1225 $years = $this->get_all_years();
1226
1227 $intervals = $intervals + $years; // merge
1228
1229 // TODO: optimize performance, no need to use SUM or ORDER BY, limit 1 would be enough
1230 foreach( $intervals as $k => $interval ) {
1231 $data = $this->get_top_video_watch_time_stats( $interval, $user_id );
1232
1233 // if there is no data for this interval, remove it from the list
1234 if( empty($data) ) {
1235 unset($intervals[$k]);
1236 }
1237
1238 }
1239
1240 return $intervals;
1241 }
1242
1243 public static function get_interval_from_range( $range ) {
1244
1245 if( strcmp( 'this_week', $range ) === 0 ) { // this week
1246 $start = gmdate('Y-m-d', strtotime('-7 days') );
1247 $end = gmdate('Y-m-d', time() );
1248
1249 } else if( strcmp( 'last_week', $range ) === 0 ) { // last week
1250 $previous_week = strtotime("-1 week +1 day");
1251
1252 // convert to datetime
1253 $previous_week = gmdate('Y-m-d', $previous_week);
1254
1255 // respect the start of week day by wordpress
1256 $start_end_week = get_weekstartend($previous_week);
1257
1258 $start = gmdate('Y-m-d', $start_end_week['start']);
1259 $end = gmdate('Y-m-d', $start_end_week['end']);
1260
1261 } else if( strcmp( 'this_month', $range ) === 0 ) { // this month
1262 $start = gmdate('Y-m-01');
1263 $end = gmdate('Y-m-t');
1264
1265 } else if( strcmp( 'last_month', $range ) === 0 ) { // last month
1266 $first_day_last_month = strtotime('first day of last month');
1267 $last_day_last_month = strtotime('last day of last month');
1268
1269 $start = gmdate('Y-m-01', $first_day_last_month );
1270 $end = gmdate('Y-m-t', $last_day_last_month );
1271
1272 } else if( strcmp( 'this_year', $range ) === 0 ) { // this year
1273 $start = gmdate('Y-01-01');
1274 $end = gmdate('Y-12-31');
1275
1276 } else if( strcmp( 'last_year', $range ) === 0 ) { // last year
1277 $start = gmdate('Y-01-01', strtotime('-1 year'));
1278 $end = gmdate('Y-12-31', strtotime('-1 year'));
1279
1280 } else if( is_numeric($range)) { // specific year like 2021
1281 $start = intval( $range ) . '-01-01';
1282 $end = intval( $range ) . '-12-31';
1283 }
1284
1285 return array( $start, $end);
1286 }
1287
1288 /**
1289 * Get the desired date range
1290 *
1291 * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number
1292 * @param mixed $base_date (optional) The base date to use for this_week
1293 * @return array All the days in the date range in YYYY-MM-DD format.
1294 */
1295 private function get_dates_in_range( $range, $base_date = false ) {
1296 $dates = array();
1297
1298 $time = time();
1299 if ( $base_date ) {
1300 $time = strtotime( $base_date );
1301 }
1302
1303 if( strcmp( 'this_week', $range ) === 0 ) {
1304 $end_day = gmdate('Y-m-d', $time );
1305 $start_day = gmdate('Y-m-d', strtotime( '-7 days', $time ) );
1306 $dates = $this->get_days_between_dates( $start_day, $end_day );
1307 } else if( strcmp( 'last_week', $range ) === 0 ) {
1308 $previous_week = strtotime("-1 week +1 day");
1309
1310 // convert to datetime
1311 $previous_week = gmdate('Y-m-d', $previous_week);
1312
1313 // respect the start of week day by wordpress
1314 $start_end_week = get_weekstartend($previous_week);
1315
1316 $start_week = gmdate('Y-m-d', $start_end_week['start']);
1317 $end_week = gmdate('Y-m-d', $start_end_week['end']);
1318
1319 $dates = $this->get_days_between_dates( $start_week, $end_week );
1320 } else if( strcmp( 'this_month', $range ) === 0 ) {
1321 $start_day = gmdate('Y-m-01');
1322 $end_day = gmdate('Y-m-d');
1323 $dates = $this->get_days_between_dates( $start_day, $end_day );
1324 } else if( strcmp( 'last_month', $range ) === 0 ) {
1325 $first_day_last_month = strtotime('first day of last month');
1326 $last_day_last_month = strtotime('last day of last month');
1327
1328 $start_day = gmdate('Y-m-01', $first_day_last_month );
1329 $end_day = gmdate('Y-m-t', $last_day_last_month );
1330
1331 $dates = $this->get_days_between_dates( $start_day, $end_day );
1332 } else if( strcmp( 'this_year', $range ) === 0 ) {
1333 $start_day = gmdate('Y-01-01');
1334 $end_day = gmdate('Y-m-d');
1335 $dates = $this->get_days_between_dates( $start_day, $end_day );
1336 } else if( strcmp( 'last_year', $range ) === 0 ) {
1337 $start_day = gmdate('Y-01-01', strtotime('-1 year'));
1338 $end_day = gmdate('Y-12-31', strtotime('-1 year'));
1339 $dates = $this->get_days_between_dates( $start_day, $end_day );
1340 } else if( is_numeric($range) ) { // get dates for specific year like 2021
1341 $start_day = intval( $range ) . '-01-01';
1342 $end_day = intval( $range ) . '-12-31';
1343 $dates = $this->get_days_between_dates( $start_day, $end_day );
1344 }
1345
1346 return $dates;
1347 }
1348
1349 function get_all_years() {
1350 global $wpdb;
1351
1352 $years = array();
1353
1354 $oldest_year = (int) $wpdb->get_var("SELECT YEAR(date) FROM {$wpdb->prefix}fv_player_stats ORDER BY id ASC LIMIT 1");
1355
1356 // add every year from oldest to current, when oldest is 2021 and current is 2025, it will add 2021, 2022, 2023, 2024, 2025
1357 for( $i = $oldest_year; $i <= gmdate('Y'); $i++ ) {
1358 $j = strval($i);
1359 $years[$j] = $j;
1360 }
1361
1362 // reorder years from newest to oldest
1363 $years = array_reverse( $years, true );
1364
1365 return $years;
1366 }
1367
1368 private function get_days_between_dates( $start_day, $end_day ) {
1369 $dates = array();
1370
1371 $current = strtotime($start_day);
1372 $end = strtotime($end_day);
1373
1374 while( $current <= $end ) {
1375 $dates[] = gmdate('Y-m-d', $current);
1376 $current = strtotime('+1 day', $current);
1377 }
1378
1379 return $dates;
1380 }
1381
1382 private function get_date_labels( $results ) {
1383 $date_labels = array();
1384
1385 foreach( $results as $row) {
1386 if( !in_array( $row['date'], $date_labels ) ) {
1387 $date_labels[strtotime($row['date'])] = $row['date'];
1388 }
1389 }
1390
1391 ksort($date_labels);
1392
1393 return array_values($date_labels);
1394 }
1395
1396 /**
1397 * Group the database result rows by the video or post ID for the desired date range.
1398 *
1399 * @param array $raw_db_results Each item is array like:
1400 * array(
1401 * 'date' => '2024-09-03',
1402 * 'id_player' => '14',
1403 * 'id_video' => '912',
1404 * 'title' => 'My Video',
1405 * 'play' => '1',
1406 * ),
1407 * array(
1408 * 'date' => '2024-09-05',
1409 * 'id_player' => '171',
1410 * 'id_video' => '912',
1411 * 'title' => 'My Video',
1412 * 'play' => '1',
1413 * ),
1414 * array(
1415 * 'date' => '2024-09-07',
1416 * 'id_player' => '14',
1417 * 'id_video' => '912',
1418 * 'title' => 'My Video',
1419 * 'play' => '1',
1420 * )
1421 *
1422 * @param mixed $top_ids_arr
1423 * @param string|int $range this_week, last_week, this_month, last_month, this_year, last_year or year number
1424 * @param string $type video or post
1425 * @param string $metric play or seconds or clicks
1426 * @param string $base_date (optional) The base date to use for $range
1427 *
1428 * @return array Summary of the daily video plays per video or post (see $type) by id_video or is_post:
1429 * 912 => array(
1430 * '2024-09-02' => array( 'play' => 0 ),
1431 * 'name' => 'My Video',
1432 * '2024-09-03' => array( 'play' => '1' ),
1433 * '2024-09-04' => array( 'play' => 0 ),
1434 * '2024-09-05' => array( 'play' => 1 ),
1435 * '2024-09-06' => array( 'play' => 0 ),
1436 * '2024-09-07' => array( 'play' => 1 ),
1437 * '2024-09-08' => array( 'play' => 0 ),
1438 * '2024-09-09' => array( 'play' => 0 ),
1439 * ),
1440 */
1441 private function process_graph_data( $raw_db_results, $top_ids_arr, $range, $type, $metric = 'play', $base_date = false ) {
1442 $datasets = array();
1443
1444 $date_labels = $this->get_dates_in_range( $range, $base_date );
1445
1446 // order data for graph,
1447 foreach( $top_ids_arr as $id ) {
1448 foreach( $date_labels as $date ) {
1449 foreach( $raw_db_results as $row) {
1450 if( ( ( $type == 'video' || $type == 'player' ) && ( isset($row['id_' . $type ]) && $row['id_' . $type ] == $id ) ) || ( isset($row['user_id']) && $row['user_id'] == $id ) || ( isset($row['guest_user_id']) && $row['guest_user_id'] == $id ) || ( isset($row['id_post']) && $row['id_post'] == $id ) ) {
1451 if( !isset($datasets[$id]) ) {
1452 $datasets[$id] = array();
1453 }
1454
1455 // aggregate data by date
1456 if( strcmp( $date, $row['date'] ) == 0 ) { // date row exists
1457 if( $metric === 'play' && isset($row['play']) ) {
1458 if( isset($datasets[$id][$date]['play']) ) {
1459 $datasets[$id][$date]['play'] += $row['play'];
1460 } else {
1461 $datasets[$id][$date]['play'] = $row['play'];
1462 }
1463 }
1464
1465 if( $metric === 'seconds' && isset($row['seconds']) ) {
1466 if( isset($datasets[$id][$date]['seconds']) ) {
1467 $datasets[$id][$date]['seconds'] += $row['seconds'];
1468 } else {
1469 $datasets[$id][$date]['seconds'] = (int) $row['seconds'];
1470 }
1471 }
1472
1473 if( $metric === 'click' && isset($row['click']) ) {
1474 if( isset($datasets[$id][$date]['click']) ) {
1475 $datasets[$id][$date]['click'] += $row['click'];
1476 } else {
1477 $datasets[$id][$date]['click'] = $row['click'];
1478 }
1479 }
1480
1481 } else { // date row dont exists, add 0 plays/seconds - dont overwrite if value already set
1482 if( $metric === 'play' && !isset( $datasets[$id][$date]['play']) ) $datasets[$id][$date]['play'] = 0;
1483 if( $metric === 'seconds' && !isset( $datasets[$id][$date]['seconds']) ) $datasets[$id][$date]['seconds'] = 0;
1484 if( $metric === 'click' && !isset( $datasets[$id][$date]['click']) ) $datasets[$id][$date]['click'] = 0;
1485 }
1486
1487 // add labels
1488 if( !isset($datasets[$id]['name']) ) {
1489 if( $type == 'video' || $type == 'player' ) {
1490 $datasets[$id]['name'] = $this->get_video_name( $row );
1491 } else if( $type == 'post' ) {
1492 $datasets[$id]['name'] = !empty($row['post_title'] ) ? $row['post_title'] : 'id_post_' . $row['id_post'] ;
1493 } else if( $type == 'user' ) {
1494 $user_data = get_userdata( intval($row['user_id']) );
1495
1496 if( $user_data === false ) {
1497 $datasets[$id]['name'] = 'Guest Users';
1498 } else {
1499 $datasets[$id]['name'] = $user_data->display_name;
1500 }
1501 } else if( $type == 'guest') {
1502 $datasets[$id]['name'] = 'Guest ' . $row['guest_user_id'];
1503 }
1504 }
1505 }
1506 }
1507 }
1508 }
1509
1510 $datasets['date-labels'] = $date_labels; // date will be used as X axis label
1511
1512 return $datasets;
1513 }
1514
1515 function get_video_name( $row ) {
1516 if( ! empty( $row['title'] ) ) {
1517 return $row['title'];
1518 }
1519
1520 $src = $row['src'];
1521
1522 // check if youtube
1523 if( FV_Player_YouTube()->is_youtube( $src ) ) {
1524 // get youtube id
1525 preg_match( '/[\\?\\&]v=([^\\?\\&]+)/', $src, $matches );
1526 if( isset($matches[1]) ) {
1527 $id = $matches[1];
1528 $name = 'Youtube: ' . $id;
1529
1530 return $name;
1531 }
1532 }
1533
1534 // check if vimeo
1535 if( function_exists('FV_Player_Pro_Vimeo') && FV_Player_Pro_Vimeo()->is_vimeo($src) ) {
1536 // get vimeo id
1537 preg_match( '/vimeo\.com\/([0-9]+)/', $src, $matches );
1538 if( isset($matches[1]) ) {
1539 $id = $matches[1];
1540 $name = 'Vimeo: ' . $id;
1541
1542 return $name;
1543 }
1544
1545 }
1546
1547 // parse title
1548 $name = flowplayer::get_title_from_src($src);
1549
1550 return $name;
1551 }
1552
1553 function users_column( $columns ) {
1554 global $fv_fp;
1555 if ( $fv_fp->_get_option('video_stats_enable') ) {
1556 $columns['fv_player_stats_user_play_today'] = "Video Plays Today";
1557 $columns['fv_player_stats_user_seconds_today'] = "Video Minutes Today";
1558 }
1559 return $columns;
1560 }
1561
1562 function users_column_content( $content, $column_name, $user_id ) {
1563 $field = false;
1564
1565 if ( 'fv_player_stats_user_play_today' === $column_name ) {
1566 $field = 'play';
1567 } else if ( 'fv_player_stats_user_seconds_today' === $column_name ) {
1568 $field = 'seconds';
1569 }
1570
1571 if( $field ) {
1572
1573 // TODO: Preload to avoid too many SQL queries
1574 global $wpdb;
1575
1576 if ( 'play' === $field ) {
1577 $val = $wpdb->get_var(
1578 $wpdb->prepare(
1579 "SELECT sum(play) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s",
1580 $user_id,
1581 date_i18n( 'Y-m-d' )
1582 )
1583 );
1584 } else if ( 'seconds' === $field ) {
1585 $val = $wpdb->get_var(
1586 $wpdb->prepare(
1587 "SELECT sum(seconds) FROM {$wpdb->prefix}fv_player_stats WHERE user_id = %d AND date = %s",
1588 $user_id,
1589 date_i18n( 'Y-m-d' )
1590 )
1591 );
1592 }
1593
1594 if ( $val ) {
1595
1596 if( 'seconds' === $field ) {
1597 $val = ceil($val/60) . ' min';
1598 }
1599
1600 $url = add_query_arg(
1601 array(
1602 'page' => 'fv_player_stats_users',
1603 'user_id' => $user_id
1604 ),
1605 admin_url( 'admin.php' )
1606 );
1607 $content = '<a href="' . $url . '">' . $val . '</a>';
1608 }
1609 }
1610
1611 return $content;
1612 }
1613
1614 function users_sortable_columns( $columns ) {
1615 $columns['fv_player_stats_user_play_today'] = 'fv_player_stats_user_play_today';
1616 $columns['fv_player_stats_user_seconds_today'] = 'fv_player_stats_user_seconds_today';
1617 return $columns;
1618 }
1619
1620 function users_sort($userquery) {
1621 global $wpdb;
1622
1623 $field = false;
1624
1625 if ( 'fv_player_stats_user_play_today' === $userquery->query_vars['orderby'] ) {
1626 $field = 'play';
1627 } else if ( 'fv_player_stats_user_seconds_today' === $userquery->query_vars['orderby'] ) {
1628 $field = 'seconds';
1629 }
1630
1631 if ( $field ) {
1632 $userquery->query_fields .= ", sum(" . $field . ") AS " . $field . " ";
1633 $userquery->query_from .= " LEFT OUTER JOIN {$wpdb->prefix}fv_player_stats AS stats ON ($wpdb->users.ID = stats.user_id) ";
1634 $userquery->query_where .= " AND stats.date = '" . date_i18n( 'Y-m-d' ) . "' ";
1635 $userquery->query_orderby = " GROUP BY wp_users.ID ORDER BY " . $field . " ".($userquery->query_vars["order"] == "ASC" ? "ASC " : "DESC ");
1636 }
1637 }
1638
1639 function user_stats_search() {
1640 if( isset($_GET['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'fv-player-stats-users-search' ) && isset($_GET['q']) && isset($_GET['date_range']) ) {
1641 $search = sanitize_text_field( $_GET['q'] );
1642 $date_range = sanitize_text_field( $_GET['date_range'] );
1643
1644 // search for users by login, nicename or email
1645 $users = get_users( array(
1646 'search' => '*' . $search . '*',
1647 'search_columns' => array( 'user_login', 'display_name' ,'user_nicename', 'user_email' ),
1648 ) );
1649
1650 $results = array();
1651 foreach( $users AS $user ) {
1652 $data = $this->get_users_by_time_range( $date_range, $user->ID ); // check if user has any data in the selected date range
1653
1654 if( $data ) {
1655 $plays = $data[0]['play'] ? $data[0]['play'] : 0;
1656
1657 $item = array(
1658 'id' => $user->ID, // used as value for option
1659 'text' => $user->display_name . '-' . $user->user_email . ' ( ' . number_format_i18n( $plays, 0) . ' plays )' // used as label for option
1660 );
1661
1662 if( !$plays ) {
1663 $item['disabled'] = true; // disable option if user has no data in the selected date range
1664 }
1665
1666 $results[] = $item;
1667 }
1668 }
1669
1670 echo wp_json_encode( array( 'results' => $results ) );
1671 }
1672
1673 die();
1674 }
1675
1676 }
1677
1678 global $FV_Player_Stats;
1679 $FV_Player_Stats = new FV_Player_Stats();
1680
1681 function fv_player_stats_top( $args = array() ) {
1682 $args = wp_parse_args( $args, array(
1683 'taxonomy' => false,
1684 'term' => false ) );
1685
1686 extract($args);
1687
1688 global $wpdb;
1689
1690 if( $taxonomy && $term ) {
1691 $raw = $wpdb->get_results(
1692 $wpdb->prepare("
1693 SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id
1694 FROM {$wpdb->prefix}fv_player_videometa AS vm
1695 JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0
1696 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player
1697 INNER JOIN {$wpdb->prefix}term_relationships AS tr ON (pm.meta_value = tr.object_id)
1698 INNER JOIN {$wpdb->prefix}term_taxonomy AS tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)
1699 INNER JOIN {$wpdb->prefix}terms AS t ON (t.term_id = tt.term_id)
1700 WHERE vm.meta_key = 'stats_play'
1701 AND pm.meta_key = 'post_id'
1702 AND tt.taxonomy = %s
1703 AND t.name = %s
1704 ORDER BY CAST(vm.meta_value AS unsigned) DESC",
1705 $taxonomy,
1706 $term
1707 )
1708 );
1709
1710 } else {
1711 $raw = $wpdb->get_results( "
1712 SELECT p.id, vm.id_video, vm.meta_value AS stats_play, pm.meta_value AS post_id
1713 FROM {$wpdb->prefix}fv_player_videometa AS vm
1714 JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(vm.id_video, p.videos) > 0
1715 JOIN {$wpdb->prefix}fv_player_playermeta AS pm ON p.id = pm.id_player
1716 WHERE vm.meta_key = 'stats_play'
1717 AND pm.meta_key = 'post_id'
1718 ORDER BY CAST(vm.meta_value AS unsigned) DESC"
1719 );
1720 }
1721
1722 // sice there might be multiple players for a single post_id we count these together
1723 $top = array();
1724 foreach( $raw AS $record ) {
1725 if( empty($top[$record->post_id]) ) $top[$record->post_id] = 0;
1726 $top[$record->post_id] += $record->stats_play;
1727 }
1728
1729 asort($top);
1730 $top = array_reverse($top,true);
1731
1732 return $top;
1733 }
1734