PluginProbe
FV Player 8 / trunk
FV Player 8 vtrunk
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 trunk, at models/stats.php

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