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 / db.php

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

2,553 lines 89.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /* FV Player - HTML5 video player
3 Copyright (C) 2013 Foliovision
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 // class handling database shortcode generation and saving
20 class FV_Player_Db {
21
22 private
23 $edit_lock_timeout_seconds = 120,
24 // TODO: Some of the sorting disabled due to poor performance
25 $valid_order_by = array('id', 'player_name', 'date_created', 'author', /*'subtitles_count', 'chapters_count', 'transcript_count'*/ ),
26 $videos_cache = array(),
27 $video_atts_cache = array(),
28 $video_meta_cache = array(),
29 $players_cache = array(),
30 //$player_atts_cache = array(),
31 $player_meta_cache = array(),
32 $player_ids_when_searching,
33 $stopwords, // used in get_search_stopwords method
34 $database_upgrade_queries = false;
35
36 public function __construct() {
37 add_action( 'toplevel_page_fv_player', array($this, 'init_tables') );
38 add_action( 'load-settings_page_fvplayer', array($this, 'init_tables') );
39
40 add_filter( 'fv_flowplayer_args_pre', array($this, 'getPlayerAttsFromDb'), 5, 1 );
41 add_filter( 'fv_player_item_pre', array($this, 'setCurrentVideoAndPlayer' ), 1, 3 );
42 add_action( 'wp_head', array($this, 'cache_players_and_videos' ) );
43
44 add_action( 'save_post', array($this, 'store_post_ids' ) );
45
46 add_action( 'wp_ajax_fv_player_db_load', array($this, 'open_player_for_editing') );
47 add_action( 'wp_ajax_fv_player_db_export', array($this, 'export_player_data') );
48 add_action( 'wp_ajax_fv_player_db_import', array($this, 'import_player_data') );
49 add_action( 'wp_ajax_fv_player_db_clone', array($this, 'clone_player') );
50 add_action( 'wp_ajax_fv_player_db_remove', array($this, 'remove_player') );
51 add_action( 'wp_ajax_fv_player_db_retrieve_all_players_for_dropdown', array($this, 'retrieve_all_players_for_dropdown') );
52 add_action( 'wp_ajax_fv_player_db_save', array($this, 'db_store_player_data') );
53 }
54
55 public function init_tables() {
56 global $wpdb;
57 $wpdb->queries = array();
58
59 FV_Player_Db_Player::initDB(true);
60 FV_Player_Db_Player_Meta::initDB(true);
61 FV_Player_Db_Video::initDB(true);
62 FV_Player_Db_Video_Meta::initDB(true);
63
64 $this->database_upgrade_queries = $wpdb->queries;
65 }
66
67 public function getDatabaseUpgradeStatus() {
68 return $this->database_upgrade_queries;
69 }
70
71 public function getVideosCache() {
72 return $this->videos_cache;
73 }
74
75 public function setVideosCache($cache) {
76 return $this->videos_cache = $cache;
77 }
78
79 public function isVideoCached($id) {
80 return isset($this->videos_cache[$id]);
81 }
82
83 public function getVideoMetaCache() {
84 return $this->video_meta_cache;
85 }
86
87 public function setVideoMetaCache($cache) {
88 return $this->video_meta_cache = $cache;
89 }
90
91 public function isVideoMetaCached($id_video, $id_meta = null) {
92 return ($id_meta !== null ? isset($this->video_meta_cache[$id_video][$id_meta]) : isset($this->video_meta_cache[$id_video]));
93 }
94
95 public function getPlayersCache() {
96 return $this->players_cache;
97 }
98
99 public function setPlayersCache($cache) {
100 return $this->players_cache = $cache;
101 }
102
103 public function isPlayerCached($id) {
104 return isset($this->players_cache[$id]);
105 }
106
107 public function getPlayerMetaCache() {
108 return $this->player_meta_cache;
109 }
110
111 public function setPlayerMetaCache($cache) {
112 return $this->player_meta_cache = $cache;
113 }
114
115 public function isPlayerMetaCached($id_player, $id_meta = null) {
116 return ($id_meta !== null ? isset($this->player_meta_cache[$id_player][$id_meta]) : isset($this->player_meta_cache[$id_player]));
117 }
118
119 public function setCurrentVideoAndPlayer($aItem, $index, $aArgs) {
120 global $fv_fp;
121
122 if (!empty($aArgs['video_objects'][$index])) {
123 $vid_obj = $aArgs['video_objects'][$index];
124 $fv_fp->currentVideoObject = $vid_obj;
125
126 if( !empty($aItem['sources'][0]['src']) && ( is_numeric($aItem['sources'][0]['src']) ) || stripos($aItem['sources'][0]['src'],'preview-') === 0 ) {
127
128 $new = array( 'sources' => array() );
129 if( $src = $vid_obj->getSrc() ) {
130 $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src,array()), 'type' => $fv_fp->get_mime_type($src) );
131 }
132
133 if( $vid_obj->getToggleAdvancedSettings() ) { // check if advanced settings are enabled
134 if( $src1 = $vid_obj->getSrc1() ) {
135 $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src1,array()), 'type' => $fv_fp->get_mime_type($src1) );
136 }
137 if( $src2 = $vid_obj->getSrc2() ) {
138 $new['sources'][] = array( 'src' => apply_filters('fv_flowplayer_video_src',$src2,array()), 'type' => $fv_fp->get_mime_type($src2));
139 }
140 if( $rtmp = $vid_obj->getRtmp() ) {
141 $new['rtmp'] = $rtmp;
142 }
143 if( $rtmp_path = $vid_obj->getRtmpPath() ) {
144 $ext = $fv_fp->get_mime_type($rtmp_path,false,true) ? $fv_fp->get_mime_type($rtmp_path,false,true).':' : false;
145 $new['sources'][] = array( 'src' => $ext.$rtmp_path, 'type' => 'video/flash' );
146 }
147 }
148
149 if( count($new['sources']) ) {
150 $aItem = $new;
151 }
152 }
153
154 if ( count($vid_obj->getMetaData())) {
155 foreach ($vid_obj->getMetaData() as $meta) {
156 if ($meta->getMetaKey() == 'live' && $meta->getMetaValue() == 'true') {
157 $aItem['live'] = 'true';
158 }
159 if ($meta->getMetaKey() == 'dvr' && $meta->getMetaValue() == 'true') {
160 $aItem['dvr'] = 'true';
161 }
162 if ($meta->getMetaKey() == 'audio' && $meta->getMetaValue() == 'true') {
163 $aItem['is_audio_stream'] = 'true';
164 }
165 }
166 }
167
168 if( $id = $vid_obj->getId() ) {
169 $aItem['id'] = $id;
170 }
171 if( $id = $vid_obj->getLive() ) {
172 $aItem['live'] = 'true';
173 }
174 if( $start = $vid_obj->getStart() ) {
175 $aItem['fv_start'] = $start;
176 }
177 if( $end = $vid_obj->getEnd() ) {
178 $aItem['fv_end'] = $end;
179 }
180
181 } else {
182 $fv_fp->currentVideoObject = null;
183 $fv_fp->currentPlayerObject = null;
184 }
185
186 return $aItem;
187 }
188
189 public function cache_players_and_videos() {
190 global $posts;
191 if( !empty($posts) && is_array($posts) ) {
192 $player_ids = array();
193 foreach( $posts AS $post ) {
194 if (isset($post->post_content)) {
195 preg_match_all( '/\[fvplayer id="(\d+)"[^\]]*\]/m', $post->post_content, $matches, PREG_SET_ORDER, 0 );
196 if ( $matches && count( $matches ) ) {
197 foreach ( $matches as $match ) {
198 $player_ids[] = $match[1];
199 }
200 }
201 }
202 }
203
204 if (count($player_ids)) {
205 $this->cache_players_and_videos_do( $player_ids );
206 }
207 }
208 }
209
210 public function cache_players_and_videos_do( $player_ids ) {
211 // load all players at once
212 $this->query_players( array( 'ids' => $player_ids ) );
213
214 // load all player meta
215 new FV_Player_Db_Player_Meta( null, array( 'id_player' => $player_ids ), $this );
216
217 // pre load all videos and their meta for these players
218 $video_ids = array();
219 foreach( $this->players_cache as $player ) {
220 $video_ids = array_merge( $video_ids, explode( ',', $player->getVideoIds() ) );
221 }
222
223 if( count( $video_ids ) ) {
224 new FV_Player_Db_Video( $video_ids, array(), $this );
225 new FV_Player_Db_Video_Meta( null, array( 'id_video' => $video_ids ), $this );
226 }
227 }
228
229 /**
230 * Retrieves total number of players in the database.
231 *
232 * @return int Returns the total number of players in database.
233 */
234 public function getListPageCount() {
235 global $wpdb;
236
237 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
238 $author_id = get_current_user_id();
239
240 // make total the number of players cached, if we've used search
241
242 // Core WordPress does not use nonce for searches in wp-admin -> Posts either
243 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
244 if ( !empty( $_GET['s'] ) ) {
245 if( $this->player_ids_when_searching ) {
246 $db_options = array(
247 'select_fields' => 'player_name, date_created, videos, author, status',
248 'count' => true,
249 'search_by_video_ids' => $this->player_ids_when_searching
250 );
251
252 if( $cannot_edit_other_posts ) {
253 $db_options['author_id'] = $author_id;
254 }
255
256 $total = $this->query_players( $db_options );
257 } else {
258 $total = 0;
259 }
260 } else {
261 if( $cannot_edit_other_posts ) {
262 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->prefix}fv_player_players` WHERE author = %d", $author_id ) );
263
264 } else {
265 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->prefix}fv_player_players` WHERE %d = %d", 1, 1 ) );
266 }
267
268 }
269
270 if ($total) {
271 return $total;
272 } else {
273 return 0;
274 }
275 }
276
277 /**
278 * Adds data for all players table shown in admin to cache, the returns the cache.
279 *
280 * @param array $args {
281 * @param string $order_by If set, data will be ordered by this column.
282 * @param string $order If set, data will be ordered in this order.
283 * @param int $offset If set, data will returned will be limited, starting at this offset.
284 * @param int $per_page If set, data will returned will be limited, ending at this offset.
285 * @param int|array $player_id If set, data will be restricted to a single player ID or array of player IDs.
286 * @param string $search If set, results will be searched for using the GET search parameter.
287 * }
288 *
289 * @return array Returns an array of all cached list page results to be displayed.
290 * @throws Exception When the underlying FV_Player_Db_Video class generates an error.
291 */
292 public function getListPageData( $args ) {
293 $args = wp_parse_args( $args, array(
294 'offset' => false,
295 'order' => 'asc',
296 'order_by' => 'id',
297 'player_id' => null,
298 'per_page' => false,
299 'post_type' => false,
300 'search' => false,
301 ) );
302
303 extract( $args );
304
305 // sanitize variables
306 $order = (in_array($order, array('asc', 'desc')) ? $order : 'asc');
307 $order_by = (in_array($order_by, $this->valid_order_by) ? $order_by : 'id');
308 $author_id = get_current_user_id();
309 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
310
311 // load single player, as requested by the user
312 if ($player_id) {
313 if( is_array($player_id) ) {
314 if( count($player_id) > 0 ) {
315 $this->cache_players_and_videos_do( $player_id );
316 }
317
318 } else {
319 new FV_Player_Db_Player( $player_id, array(), $this );
320 }
321
322 } else if ($search) {
323
324 $direct_hit_cache = false;
325
326 // Try to load the player which ID matches the search query it it's a number
327 if( is_numeric($search) ) {
328 new FV_Player_Db_Player( $search, array(), $this );
329
330 $direct_hit_cache = $this->getPlayersCache();
331 }
332
333 // search for videos that are consistent with the search text
334 // and load their players only
335 $query_videos = array(
336 'fields_to_search' => array('src', 'src1', 'src2', 'title', 'splash', 'splash_text'),
337 'search_string' => $search,
338 'like' => true,
339 'and_or' => 'OR'
340 );
341
342 $vids = $this->query_videos($query_videos);
343
344 // if we have any data, assemble video IDs and load their players
345 if ($vids !== false) {
346 $player_video_ids = array();
347
348 foreach ($vids as $video) {
349 $player_video_ids[] = $video->getId();
350 }
351
352 // cache this, so we can use this in the FV_Player_Db_Player::getListPageCount() method
353 $this->player_ids_when_searching = $player_video_ids;
354
355 $db_options = array(
356 'select_fields' => 'player_name, date_created, videos, author, status',
357 'order_by' => $order_by,
358 'order' => $order,
359 'offset' => $offset,
360 'per_page' => $per_page,
361 'post_type' => $post_type,
362 'search_by_video_ids' => $player_video_ids,
363 'search_string' => $search,
364 );
365
366 if( $cannot_edit_other_posts ) {
367 $db_options['author_id'] = $author_id;
368 }
369
370 $this->query_players( $db_options );
371 }
372
373 if( is_array($direct_hit_cache) ) {
374 $cache = $this->getPlayersCache();
375 $this->setPlayersCache( array_merge( $direct_hit_cache, $cache ) );
376 }
377
378 } else {
379 // load all players, which will put them into the cache automatically
380
381 $db_options = $args;
382 $db_options['select_fields'] = 'player_name, date_created, videos, author, status';
383
384 if( $cannot_edit_other_posts ) {
385 $db_options['author_id'] = $author_id;
386 }
387
388 $this->query_players( $db_options );
389 }
390
391 global $fv_fp;
392 $stats_enabled = $fv_fp->_get_option('video_stats_enable');
393
394 $players = $this->getPlayersCache();
395
396 // get all video IDs used in all players
397 if ($players && count($players)) {
398 $videos = array();
399 $result = array();
400
401 foreach ($players as $player) {
402 /* @var FV_Player_Db_Player $player */
403 $videos = array_merge($videos, explode(',', $player->getVideoIds()));
404 }
405
406 // load all videos data at once
407 if (count($videos)) {
408 // TODO: This class should not provide search
409 $vids_data = new FV_Player_Db_Video( $videos, array(), $this );
410
411 // build the result
412 foreach ($players as $player) {
413 // player data first
414 $result_row = new stdClass();
415 $result_row->id = $player->getId();
416 $result_row->player_name = $player->getPlayerName();
417 $result_row->date_created = $player->getDateCreated();
418 $result_row->thumbs = array();
419 $result_row->author = $player->getAuthor();
420 $result_row->subtitles_count = $player->getCount('subtitles');
421 $result_row->chapters_count = $player->getCount('chapters');
422 $result_row->transcript_count = $player->getCount('transcript');
423 $result_row->status = $player->getStatus();
424
425 $videos = array();
426
427 // put in the videos which belong to the player
428 if ($vids_data) {
429 if (count($this->getVideosCache())) {
430 foreach ( $this->getVideosCache() as $video_object ) {
431 if ( in_array( $video_object->getId(), explode(',', $player->getVideoIds() ) ) ) {
432 $videos[ $video_object->getId() ] = $video_object;
433 }
434 }
435 }
436 }
437
438 $result_row->video_objects = $videos;
439
440 $result_row->player_name = $player->getPlayerName();
441
442 $embeds = array();
443 if( $posts = $player->getMetaValue('post_id') ) {
444 $post_ids = array();
445 foreach( $posts AS $post_id ) {
446 $post_ids[] = $post_id;
447 }
448
449 // Load enough to allow get_permalink() to work with the data
450 global $wpdb;
451 $post_ids = implode( ',', array_map( 'intval', $post_ids ) );
452 $embeds = $wpdb->get_results( "SELECT ID, post_name, post_title, post_status, post_type, post_date FROM {$wpdb->posts} WHERE ID IN ( {$post_ids} ) AND post_status != 'inherit' ORDER BY post_date_gmt DESC", OBJECT_K );
453
454 foreach( $embeds AS $post_id => $post_data ) {
455
456 // Get taxonomies for each post...
457 // But hold on, what columns is the FV Player list page actually showing? We may not need all of this extra processing.
458 $list_page_coluns = array_keys( apply_filters( 'manage_toplevel_page_fv_player_columns', array() ) );
459 $taxonomies = array();
460
461 // Code from core WordPress get_the_taxonomies()
462 foreach ( get_object_taxonomies( $post_data->post_type ) as $taxonomy ) {
463
464 // Is the columns for that taxonomy showing?
465 if ( in_array( 'tax_' . $taxonomy, $list_page_coluns) ) {
466
467 $t = (array) get_taxonomy( $taxonomy );
468 if ( empty( $t['label'] ) ) {
469 $t['label'] = $taxonomy;
470 }
471
472 $terms = get_object_term_cache( $post_data->ID, $taxonomy );
473 if ( false === $terms ) {
474 $terms = wp_get_object_terms( $post_data->ID, $taxonomy );
475 }
476
477 if ( $terms ) {
478 $taxonomies[ $taxonomy ] = array(
479 'label' => $t['label'],
480 'terms' => $terms
481 );
482 }
483 }
484 }
485
486 $embeds[ $post_id ]->taxonomies = $taxonomies;
487 }
488
489 $embeds = apply_filters( 'fv_player_editor_embeds', $embeds, $player );
490 }
491 $result_row->embeds = $embeds;
492
493 $titles = array();
494 foreach (explode(',', $player->getVideoIds()) as $video_id) {
495 if( empty($videos[ $video_id ]) ) { // the videos field might point to a missing video
496 continue;
497 }
498
499 $video = $videos[ $video_id ];
500
501 $title = $video->getTitle();
502 if( !$title ) {
503 $title = $video->getTitleFromSrc();
504 }
505
506 $titles[] = $title;
507
508 // assemble video splash
509 if (isset($videos[ $video_id ]) && $videos[ $video_id ]->getSplash()) {
510 // use splash with title / filename in a span
511 $splash = apply_filters( 'fv_flowplayer_playlist_splash', $videos[ $video_id ]->getSplash() );
512 $result_row->thumbs[] = '<div class="fv_player_splash_list_preview"><img src="'.esc_attr($splash).'" width="100" alt="'.esc_attr($title).'" title="'.esc_attr($title).'" loading="lazy" /><span>' . $title . '</span></div>';
513 } else if ( isset($videos[ $video_id ]) && $title ) {
514 // use title
515 $result_row->thumbs[] = '<div class="fv_player_splash_list_preview fv_player_list_preview_no_splash" title="' . esc_attr($title) . '"><span>' . $title . '</span></div>';
516 }
517
518 if( $stats_enabled ) {
519 if( !isset($result_row->stats_play) ) $result_row->stats_play = 0;
520 $result_row->stats_play += intval($video->getMetaValue('stats_play',true)); // todo: lower SQL count
521 }
522 }
523
524 $result_row->video_titles = $titles;
525
526 $result[] = $result_row;
527 }
528
529 return $result;
530 }
531 }
532
533 return array();
534 }
535
536
537
538 /**
539 * Generates a full code for a playlist from one that uses video IDs
540 * stored in the database to one that uses the first video src attribute
541 * Playlist items stay as IDs and are filled in flowplayer::build_playlist_html()
542 *
543 * @param array $atts Player attributes to build the player shortcode from.
544 * @param array $preview_data Alternative data to use instead of the $atts array
545 * when we want to show previews etc.
546 *
547 * @return array Returns augmented array of attributes that get picked up
548 * on the front-end side.
549 * @throws Exception When any of the underlying classes throw an exception.
550 */
551 private function generateFullPlaylistCode($atts, $preview_data = null) {
552 global $fv_fp;
553
554 // check if we should change anything in the playlist code
555 if ($preview_data || (isset($atts['playlist']) && preg_match('/^[\d,]+$/m', $atts['playlist']))) {
556 $new_playlist_tag = array();
557 $first_video_data_cached = false;
558
559 // serve what we can from the cache
560 if (!$preview_data) {
561 $ids = explode( ',', $atts['playlist'] );
562 $newids = array();
563
564 // check the first video, which is the main one for the playlist
565 if ( isset( $this->video_atts_cache[ $ids[0] ] ) ) {
566 $first_video_data_cached = true;
567 $atts = array_merge( $atts, $this->video_atts_cache[ $ids[0] ] );
568 }
569
570 // prepare cached data and IDs that still need loading from DB
571 foreach ( $ids as $id ) {
572 if ( isset( $this->video_atts_cache[ $id ] ) ) {
573 $new_playlist_tag[] = $id;
574 } else {
575 $newids[] = (int) $id;
576 }
577 }
578 }
579
580 if ($preview_data || count($newids)) {
581 if ($preview_data) {
582 $videos = $preview_data['videos'];
583 } else {
584 $videos = $fv_fp->current_player()->getVideos();
585 }
586
587 // cache first vid
588 if (!$first_video_data_cached && $videos) {
589 $vid = $videos[0]->getAllDataValues();
590
591 // we need to keep the player id!
592 $first_video = $vid;
593 unset($first_video['id']);
594 $atts = array_merge($atts, $first_video);
595 $atts['video_objects'] = array($videos[0]);
596
597 // don't cache if we're previewing
598 if (!$preview_data) {
599 $this->video_atts_cache[ $vid['id'] ] = $vid;
600 }
601
602 // remove the first video and keep adding the rest of the videos to the playlist tag
603 array_shift( $videos );
604 }
605
606 // add rest of the videos into the playlist tag
607 if ($videos && count($videos)) {
608 foreach ( $videos as $k => $vid_object ) {
609 $vid = $vid_object->getAllDataValues();
610 $vid_id = isset($vid['id']) ? $vid['id'] : 'preview-'.($k+1);
611 $atts['video_objects'][] = $vid_object;
612 $this->video_atts_cache[ $vid_id ] = $vid;
613 $new_playlist_tag[] = $vid_id;
614 }
615
616 $atts['playlist'] = implode(';', $new_playlist_tag);
617
618 } else if (isset($videos) && is_array($videos)) {
619 // only one video found, therefore this is not a playlist
620 unset($atts['playlist']);
621 }
622 } else {
623 // remove the first video from playlist, since that is
624 // the video in src and would duplicate that video in player
625 // as a result
626 array_shift($new_playlist_tag);
627
628 $atts['playlist'] = implode(';', $new_playlist_tag);
629 }
630 }
631
632 return $atts;
633 }
634
635
636 /**
637 * Maps attributes from database into their respective shortcode names.
638 *
639 * @param $att_name Attribute name from the database to map into shortcode format.
640 *
641 * @return mixed Returns the correct attribute name for shortcode use.
642 */
643 private function mapDbAttributes2Shortcode($att_name) {
644 $atts_map = array(
645 'playlist' => 'liststyle',
646 'video_ads' => 'preroll',
647 'video_ads_post' => 'postroll'
648 );
649
650 return (isset($atts_map[$att_name]) ? $atts_map[$att_name] : $att_name);
651 }
652
653
654 /**
655 * Maps attributes values from database into their respective shortcode values.
656 *
657 * @param $att_name Attribute name from the database.
658 * @param $att_value Attribute value from the database.
659 *
660 * @return mixed Returns the correct attribute value for shortcode use.
661 */
662 private function mapDbAttributeValue2Shortcode($att_name, $att_value, $data) {
663 switch ($att_name) {
664 case 'playlist_advance':
665 if($att_value == 'on' ) return 'true';
666 if($att_value == 'off' ) return 'false';
667 case 'share':
668 if( $att_value == 'custom' && !empty($data['share_title']) && !empty($data['share_url']) ) {
669 return $data['share_title'].';'.$data['share_url'];
670 }
671 case 'liststyle':
672 // there was a bug which caused the Prev/Next Playlist style to save as prev/next rather than prevnext, so this code fixes the display without need to fix the database data
673 if($att_value == 'prev/next' ) return 'prevnext';
674 }
675
676 return $att_value;
677 }
678
679
680 /**
681 * Retrieves player attributes from the database
682 * as opposed to getting them from the old full-text
683 * shortcode format.
684 *
685 * @return array|mixed Returns an array with all player attributes in it.
686 * If the player ID is not found, an empty array is returned.
687 * @throws Exception When the underlying video object throws.
688 */
689 public function getPlayerAttsFromDb( $atts ) {
690 // if we have a programatically-crafted shortcode that loads a player
691 // to show a custom user playlist on the front-end, process it here
692 if (isset( $atts['src'] ) && is_numeric( $atts['src'] ) && intval( $atts['src'] ) > 0 ) {
693 return $this->setPlayerAttsFromNumericSrc( $atts );
694 }
695
696 global $fv_fp, $FV_Player_Db;
697
698 $is_multi_playlist = false;
699
700 if (isset($atts['id'])) {
701
702 // video attributes which can still be set in shortcode
703 // this makes the preview work with YouTube playlists obtained via API
704 // this lets you set the splash screen for Vimeo channel
705 $preserve = array();
706 foreach( array('autoplay', 'preroll', 'postroll', 'splash', 'splash_attachment_id', 'src','splash_text', 'share' ) AS $attr2preserve ) {
707 if( !empty($atts[$attr2preserve]) ) {
708 $preserve[$attr2preserve] = $atts[$attr2preserve];
709 }
710 }
711
712 // numeric ID means we're coming from a shortcode somewhere in a post
713 if (preg_match('/[\d,]+/', $atts['id']) === 1) {
714 $is_multi_playlist = strpos( $atts['id'], ',' ) !== false;
715 $multi_playlist_ids = array_unique( array_map( 'intval', explode( ',', $atts['id'] ) ) );
716 $real_id = $is_multi_playlist ? $multi_playlist_ids[0] : $atts['id'];
717
718 //if ( isset( $this->player_atts_cache[ $real_id ]) && empty($atts['sort']) ) {
719 //return $this->player_atts_cache[ $real_id ];
720 //}
721
722 if ($this->isPlayerCached($real_id)) {
723 $player = $this->getPlayersCache();
724 $player = $player[$real_id];
725 } else {
726 $player = new FV_Player_Db_Player( $real_id, array(), $FV_Player_Db );
727 }
728
729 // even if we have multi-playlist tag, if we cannot find the first player
730 // we don't continue here, since we get all attributes from the first player
731 if (!$player || !$player->getIsValid()) {
732 return $atts;
733 }
734
735 $fv_fp->currentPlayerObject = $player;
736
737 $data = $player->getAllDataValues();
738
739 // did we find the player?
740 if ( $data ) {
741 foreach ( $data AS $k => $v ) {
742 $k = $this->mapDbAttributes2Shortcode( $k );
743 $v = $this->mapDbAttributeValue2Shortcode( $k, $v, $data );
744 if ( $v ) {
745 // we omit empty values and they will get set to defaults if necessary
746 $atts[ $k ] = $v;
747 }
748 }
749
750 // if we have multiple players, load them here
751 // and merge their videos with first player's videos
752 if ($is_multi_playlist) {
753 $ids = $multi_playlist_ids;
754
755 array_shift($ids);
756
757 foreach ($ids as $id_player) {
758 if ($this->isPlayerCached($id_player)) {
759 $additional_player = $this->getPlayersCache();
760 $additional_player = $additional_player[$id_player];
761 } else {
762 $additional_player = new FV_Player_Db_Player( $id_player, array(), $FV_Player_Db );
763 }
764
765 $additional_player->getVideos();
766 $data['videos'] .= ',' . $additional_player->getVideoIds();
767 }
768
769 $player->setVideos($data['videos']);
770 }
771
772 // check if we should change order of videos
773 $ordered_videos = explode(',', $data['videos']);
774 if (!empty($atts['sort']) && in_array($atts['sort'], array('oldest', 'newest', 'reverse', 'title'))) {
775
776 switch ($atts['sort']) {
777 case 'oldest':
778 $ordered_videos_tmp = array();
779 sort($ordered_videos);
780 foreach ( $ordered_videos as $video_index ) {
781 $ordered_videos_tmp['v'.$video_index] = $video_index;
782 }
783
784 ksort($ordered_videos_tmp);
785 $ordered_videos = array_values($ordered_videos_tmp);
786 break;
787
788 case 'newest':
789 $ordered_videos_tmp = array();
790 sort($ordered_videos);
791 $index = count($ordered_videos);
792 while($index) {
793 $ordered_videos_tmp['v'.$ordered_videos[--$index]] = $ordered_videos[$index];
794 }
795
796 $ordered_videos = array_values($ordered_videos_tmp);
797 break;
798
799 case 'reverse':
800 $ordered_videos = array_reverse($ordered_videos);
801 break;
802
803 case 'title':
804 $ordered_videos_tmp = array();
805 foreach ( $FV_Player_Db->getVideosCache() as $video ) {
806 // if this is not one of our videos, bail out
807 if (!in_array($video->getId(), $ordered_videos)) {
808 continue;
809 }
810
811 $title = $video->getTitle();
812
813 if (!$title) {
814 $title = $video->getSplashText();
815 }
816
817 if (!$title) {
818 $title = $video->getSrc();
819 }
820
821 $ordered_videos_tmp[$title] = $video->getId();
822 }
823
824 ksort($ordered_videos_tmp);
825 $ordered_videos = array_values($ordered_videos_tmp);
826 break;
827 }
828
829 $data['videos'] = implode(',', $ordered_videos);
830 $player->setVideos($data['videos']);
831
832 if( !empty($atts['video_objects']) ) {
833 $new_objects = array();
834 foreach( $ordered_videos AS $v ) {
835 foreach( $atts['video_objects'] AS $i ) {
836 if( $i->getId() == $v ) {
837 $new_objects[] = $i;
838 }
839 }
840 }
841 $atts['video_objects'] = $new_objects;
842 }
843
844 }
845
846 /**
847 * In editor only load the video which is being edited
848 */
849 if ( isset( $atts['current_video_to_edit'] ) && $atts['current_video_to_edit'] > -1 ) {
850 $current_video_to_edit = $atts['current_video_to_edit'];
851
852 $videos = explode( ',', $data['videos'] );
853
854 $data['videos'] = $videos[ $current_video_to_edit ];
855
856 $atts['video_objects'] = array( $atts['video_objects'][ $current_video_to_edit ] );
857 }
858
859 // preload all videos
860 $player->getVideos();
861
862 // add playlist / single video data
863 $atts = array_merge( $atts, $this->generateFullPlaylistCode(
864 // we need to prepare the same attributes array here
865 // as is ingested by generateFullPlaylistCode()
866 // when parsing the new playlist code on the front-end
867 array(
868 'playlist' => $data['videos']
869 )
870 ) );
871
872 }
873
874 //$this->player_atts_cache[ $real_id ] = $atts;
875
876 }
877
878 if( count($preserve) > 0 ) {
879 $atts = array_merge( $atts, $preserve );
880 }
881
882 } else {
883 $fv_fp->currentPlayerObject = null;
884 }
885
886 // clear player cache with our player IDs
887 // if we're coming from multi-ID shortcode,
888 // otherwise we'd store player with manually updated
889 // and therefore invalid video IDs
890 if ($is_multi_playlist) {
891 $cache = $FV_Player_Db->getPlayersCache();
892 unset($cache[$player->getId()]);
893 $FV_Player_Db->setPlayersCache($cache);
894 }
895
896 return $atts;
897 }
898
899 /**
900 * Creates an empty default player from a shortcode like [fvplayer src="1" playlist="1;2;3"]
901 * and fills its videos data from the database. Used for custom user front-end playlists.
902 *
903 * The SRC attribute of the above shortcode must be a numeric ID of the first video in the playlist,
904 * then the playlist must follow with all of the videos to be shown (including the first one from the SRC attribute).
905 *
906 * @param $atts Original player attributes coming from the execution point of this method's filter.
907 *
908 * @return array|mixed Returns an array with all player attributes in it.
909 * If the player ID is not found, an empty array is returned.
910 * @throws Exception When the underlying video object throws.
911 */
912 public function setPlayerAttsFromNumericSrc( $atts ) {
913
914 global $fv_fp, $FV_Player_Db;
915
916 if (isset( $atts['src'] ) && is_numeric( $atts['src'] ) && intval( $atts['src'] ) > 0 ) {
917 $player = new FV_Player_Db_Player( false, array(
918 'playlist' => ( !empty($atts['playlist']) ? $atts['playlist'] : $atts['src'] ),
919 ), $FV_Player_Db );
920
921 // fill-in videos data from the "playlist" shortcode parameter
922 $player->setVideos( str_replace( ';', ',', $atts['playlist'] ) );
923 $fv_fp->currentPlayerObject = $player;
924 $data = $player->getAllDataValues();
925
926 // preload all videos
927 $player->getVideos();
928
929 // add playlist / single video data
930 $atts = array_merge( $atts, $this->generateFullPlaylistCode(
931 // we need to prepare the same attributes array here
932 // as is ingested by generateFullPlaylistCode()
933 // when parsing the new playlist code on the front-end
934 array(
935 'playlist' => $data['videos']
936 )
937 ) );
938 }
939
940 return $atts;
941 }
942
943 public function db_load_player_data( $player_id, $current_video_to_edit = -1, $current_editor_tab = false ) {
944 global $fv_fp;
945
946 $this->getPlayerAttsFromDb( array( 'id' => $player_id ) );
947
948 // fill the $out variable with player data
949 $out = $fv_fp->current_player()->getAllDataValues();
950
951 // load player meta data
952 $meta = $fv_fp->current_player()->getMetaData();
953 foreach ($meta as $meta_object) {
954 if (!isset($out['meta'])) {
955 $out['meta'] = array();
956 }
957
958 $out['meta'][] = $meta_object->getAllDataValues();
959 }
960
961 unset($out['video_objects'], $out['videos']);
962
963 // fill the $out variable with video data
964 $out['videos'] = array();
965 foreach ($fv_fp->current_player()->getVideos() as $video) {
966 // load video values
967 $vid = $video->getAllDataValues();
968 $vid['meta'] = array();
969
970 // load all meta data
971 $meta = $video->getMetaData();
972
973 foreach ($meta as $meta_object) {
974 $vid['meta'][] = $meta_object->getAllDataValues();
975 }
976
977 $out['videos'][] = $vid;
978 }
979
980 // load posts where this player is embedded
981 $embeds_html = '';
982 if( $posts = $fv_fp->current_player()->getMetaValue('post_id') ) {
983 foreach( $posts AS $post_id ) {
984 $embeds_html .= '<li><a href="'.get_permalink($post_id).'" target="_blank">'.get_the_title($post_id).'</a></li>';
985 }
986 }
987 if( $embeds_html ) {
988 $out['embeds'] = '<ol>'.$embeds_html.'</ol>';
989 }
990
991 // Allow plugins to add to the loaded data
992 $out = apply_filters( 'fv_player_editor_db_load', $out, $player_id, $current_video_to_edit );
993
994 $args = array( 'id' => $fv_fp->current_player()->getId(), 'lightbox' => false );
995 if( $current_video_to_edit > -1 ) {
996 $args['current_video_to_edit'] = $current_video_to_edit;
997 }
998
999 if ( $current_editor_tab ) {
1000 $args['current_editor_tab'] = $current_editor_tab;
1001 }
1002
1003 $preview_data = $fv_fp->build_min_player( false, $args );
1004 $out['html'] = $preview_data['html'];
1005
1006 foreach( $out['videos'] as $index => $video ) {
1007 if( !empty($video['splash']) ) {
1008 $out['videos'][$index]['splash_display'] = apply_filters( 'fv_flowplayer_playlist_splash', $video['splash'] );
1009 }
1010 }
1011
1012 return $out;
1013 }
1014
1015
1016 /**
1017 * Stored player data in a database from the POST data sent via AJAX
1018 * from the shortcode editor.
1019 *
1020 * @param array $data Alternative data to work with rather than getting these from $_POST.
1021 * Used when previews are being made.
1022 *
1023 * @return void|array Returns nothing when we're saving a new player into the DB,
1024 * otherwise returns a new unsaved player and video instances to be used as needed.
1025 * @throws Exception When any of the underlying objects throw.
1026 */
1027 public function db_store_player_data($data = null) {
1028 global $FV_Player_Db;
1029
1030 $player_options = array();
1031 $video_ids = array();
1032
1033 $json_post = file_get_contents( 'php://input' );
1034
1035 $post_data = null;
1036 if( is_array($data) ) {
1037 $post_data = $data;
1038
1039 } else if( !empty($json_post) ) {
1040 $json_post = json_decode( $json_post, true );
1041
1042 $json_error = json_last_error();
1043
1044 if( $json_error !== JSON_ERROR_NONE ) {
1045 wp_send_json( array(
1046 'error' => 'Error saving: JSON error.',
1047 'fatal_error' => true
1048 ) );
1049 exit;
1050 }
1051
1052 if( !wp_verify_nonce( sanitize_text_field( wp_unslash( $json_post['nonce'] ) ), "fv-player-edit" ) ) {
1053 wp_send_json( array(
1054 'error' => 'Error saving: Nonce error, please ensure you are logged in and try again.',
1055 'fatal_error' => true
1056 ) );
1057 exit;
1058 }
1059
1060 if( !empty( $json_post['data'] ) ) {
1061 $post_data = $json_post['data'];
1062
1063 // check if user can update player
1064 if(!empty($post_data['update']) && !current_user_can('edit_others_posts') ) {
1065 $player_to_check = new FV_Player_Db_Player(intval($post_data['update']), array(), $FV_Player_Db);
1066
1067 if( $player_to_check->getAuthor() !== get_current_user_id() ) {
1068 wp_send_json( array( 'error' => 'Security check failed.' ) );
1069 }
1070 }
1071 }
1072
1073 } else if( !empty( $_REQUEST['action'] ) && 'fv_player_db_save' === sanitize_key( $_REQUEST['action'] ) ) {
1074 wp_send_json( array(
1075 'error' => 'Error saving: JSON POST data missing!',
1076 'fatal_error' => true
1077 ) );
1078 exit;
1079 }
1080
1081 $ignored_player_fields = array(
1082 'fv_wp_flowplayer_field_subtitles_lang', // subtitles languages is a per-video value with global field name,
1083 // so the player should ignore it, as it will be added via video meta
1084 'fv_wp_flowplayer_field_popup', // never used, never shown in the UI, possibly a remnant of old code,
1085 'fv_wp_flowplayer_field_transcript', // transcript is a meta value, so it should not be stored globally per-player anymore
1086 'fv_wp_flowplayer_field_chapters', // chapters is a meta value, so it should not be stored globally per-player anymore
1087 );
1088
1089 if ($post_data) {
1090
1091 $time_save_start = microtime(true);
1092
1093 // parse and resolve deleted videos
1094 if (!$data && !empty($post_data['deleted_videos'])) { // todo: ajax!
1095 $deleted_videos = explode(',', $post_data['deleted_videos']);
1096 foreach ($deleted_videos as $d_id) {
1097 // we don't need to load this video data, just link it to a database
1098 // and then delete it
1099 // ... although we'll need at least 1 item in the data array to consider this
1100 // video data valid for object creation
1101 $d_vid = new FV_Player_Db_Video(null, array('title' => '1'), $this);
1102 $d_vid->link2db($d_id);
1103 $d_vid->delete();
1104 }
1105 }
1106
1107 // parse and resolve deleted meta data
1108 if (!$data && !empty($post_data['deleted_video_meta'])) { // todo: probably not needed with Ajax saving
1109 $deleted_meta = explode(',', $post_data['deleted_video_meta']);
1110 foreach ($deleted_meta as $d_id) {
1111 // we don't need to load this meta data, just link it to a database
1112 // and then delete it
1113 // ... although we'll need at least 1 item in the data array to consider this
1114 // meta data valid for object creation
1115 $d_meta = new FV_Player_Db_Video_Meta(null, array('meta_key' => '1'), $this);
1116 $d_meta->link2db($d_id);
1117 $d_meta->delete();
1118 }
1119 }
1120
1121 // parse and resolve deleted meta data
1122 if (!$data && !empty($post_data['deleted_player_meta'])) { // todo: probably not needed with Ajax saving
1123 $deleted_meta = explode(',', $post_data['deleted_player_meta']);
1124 foreach ($deleted_meta as $d_id) {
1125 // we don't need to load this meta data, just link it to a database
1126 // and then delete it
1127 // ... although we'll need at least 1 item in the data array to consider this
1128 // meta data valid for object creation
1129 $d_meta = new FV_Player_Db_Player_Meta(null, array('meta_key' => '1'), $this);
1130 $d_meta->link2db($d_id);
1131 $d_meta->delete();
1132 }
1133 }
1134
1135 foreach ($post_data as $field_name => $field_value) {
1136 // global player or local video setting field
1137 if (strpos($field_name, 'fv_wp_flowplayer_field_') !== false) {
1138 if (!in_array($field_name, $ignored_player_fields)) {
1139 $option_name = str_replace( 'fv_wp_flowplayer_field_', '', $field_name );
1140 // global player option
1141 $player_options[ $option_name ] = $field_value;
1142 }
1143 } else if ($field_name == 'videos' && is_array($field_value)) {
1144 // iterate over all videos for the player
1145 foreach ($field_value as $video_index => $video_data) {
1146 // width and height are global options but are sent out for shortcode compatibility
1147 unset($video_data['fv_wp_flowplayer_field_width'], $video_data['fv_wp_flowplayer_field_height']);
1148
1149 // remove global player HLS key option, as it's handled as meta data item
1150 // TODO: create proper API!
1151 unset($video_data['fv_wp_flowplayer_hlskey'], $video_data['fv_wp_flowplayer_hlskey_cryptic'], $video_data['fv_wp_flowplayer_field_encoding_job_id']);
1152
1153 // strip video data of the prefix
1154 $new_video_data = array();
1155 foreach ($video_data as $key => $value) {
1156 if ($key === 'id') {
1157 $id = $value;
1158 } else {
1159 $new_video_data[ str_replace( 'fv_wp_flowplayer_field_', '', $key ) ] = $value;
1160 }
1161 }
1162 $video_data = $new_video_data;
1163 unset($new_video_data);
1164
1165 // add any video meta data that we can gather
1166 $video_meta = array();
1167
1168 if (!empty($post_data['video_meta']['video'][$video_index])) {
1169 foreach ($post_data['video_meta']['video'][$video_index] as $video_meta_section => $video_meta_array) {
1170 $meta_data_to_add = array(
1171 'meta_key' => $video_meta_section,
1172 'meta_value' => $video_meta_array['value']
1173 );
1174
1175 if (isset($video_meta_array['id'])) {
1176 $meta_data_to_add['id'] = (int) $video_meta_array['id'];
1177 }
1178
1179 $video_meta[] = $meta_data_to_add;
1180 }
1181 }
1182
1183 // add chapters and transcript
1184 foreach( array(
1185 'chapters',
1186 'transcript'
1187 ) AS $meta_type ) {
1188 if (!empty($post_data['video_meta'][$meta_type][$video_index]['file']['value'])) {
1189 $file = $post_data['video_meta'][$meta_type][$video_index]['file'];
1190 $new_meta = array(
1191 'meta_key' => $meta_type,
1192 'meta_value' => $file['value']
1193 );
1194
1195 if (!empty($file['id'])) {
1196 $new_meta['id'] = $file['id'];
1197 }
1198
1199 $video_meta[] = $new_meta;
1200 }
1201 }
1202
1203 // Video meta with languages
1204 // TODO: How to do this automatically for all the fields registered in editor with language => true?
1205 foreach( array(
1206 'subtitles',
1207 'transcript_src'
1208 ) AS $meta_type ) {
1209 foreach ( $post_data['video_meta'][ $meta_type ][$video_index] as $transcript ) {
1210 if ($transcript['file']) {
1211 $m = array(
1212 'meta_key' => $meta_type . ($transcript['code'] ? '_'.$transcript['code'] : ''),
1213 'meta_value' => $transcript['file']
1214 );
1215
1216 // add ID, if present
1217 if (!empty($transcript['id'])) {
1218 $m['id'] = $transcript['id'];
1219 }
1220
1221 $video_meta[] = $m;
1222 }
1223 }
1224 }
1225
1226 // call a filter which is server by plugins to augment
1227 // the $video_meta data with all the plugin data for this
1228 // particular video
1229 if (!empty($post_data['video_meta'])) {
1230 $video_meta = apply_filters( 'fv_player_db_video_meta_save', $video_meta, $post_data['video_meta'], $video_index);
1231 }
1232
1233 // save the video
1234 $video = new FV_Player_Db_Video(null, $video_data, $this);
1235
1236 // if we have video ID, link this video to DB
1237 if (isset($id)) {
1238 $video->link2db($id);
1239 unset($id);
1240 }
1241
1242 // Skip video meta check if the save is taking more than 10 seconds.
1243 // We could also rely on the PHP max_execution_time/2 or so.
1244 $skip_video_meta_check = ( microtime(true) - $time_save_start ) > 10;
1245
1246 // save only if we're not requesting new instances for preview purposes
1247 if (!$data) {
1248 $id_video = $video->save( $video_meta, false, $skip_video_meta_check );
1249 if( !$id_video ) {
1250 global $wpdb;
1251 wp_send_json( array( 'fatal_error' => true, 'error' => 'Failed to save the video: '.$wpdb->last_error ) );
1252 exit;
1253 }
1254 } else {
1255 $video->link2meta( $video_meta );
1256 }
1257
1258 // return videos as well as the full player
1259 if (!$data) {
1260 $video_ids[] = $id_video;
1261 } else {
1262 $video_ids[] = $video;
1263 }
1264 }
1265 }
1266 }
1267
1268 // add all videos into this player
1269 if (!$data) {
1270 $player_options['videos'] = implode( ',', $video_ids );
1271 }
1272
1273 // add any player meta data that we can gather
1274 $player_meta = array();
1275
1276 if (!empty($post_data['player_meta']['player'])) {
1277 foreach ($post_data['player_meta']['player'] as $player_meta_section => $player_meta_array) {
1278 $meta_data_to_add = array(
1279 'meta_key' => $player_meta_section,
1280 'meta_value' => $player_meta_array['value']
1281 );
1282
1283 if (isset($player_meta_array['id'])) {
1284 $meta_data_to_add['id'] = (int) $player_meta_array['id'];
1285 }
1286
1287 $player_meta[] = $meta_data_to_add;
1288 }
1289 }
1290
1291 // call a filter which is served by plugins to augment
1292 // the $player_meta data with all the plugin data for this
1293 // particular player
1294 if (!empty($post_data['player_meta'])) {
1295 $player_meta = apply_filters( 'fv_player_db_player_meta_save', $player_meta, $post_data['player_meta'], $post_data );
1296 }
1297
1298 // create and save the player
1299 $player = new FV_Player_Db_Player(null, $player_options, $FV_Player_Db);
1300
1301 // if this player should have a "published" status, add it here
1302 if ( !empty( $post_data['status'] ) && $post_data['status'] == 'published' ) {
1303 $player->setStatus('published');
1304 }
1305
1306 // Save only if we're not requesting new instances for preview purposes
1307 if (!$data) {
1308 // link to DB, if we're doing an update
1309 if (!empty($post_data['update'])) {
1310 $player->link2db($post_data['update']);
1311 }
1312
1313 $id = $player->save($player_meta);
1314
1315 if ($id) {
1316 do_action('fv_player_db_save', $id);
1317
1318 // Process Video Custom Fields
1319 if ( !empty($post_data['current_post_id']) ) {
1320 $post = get_post( $post_data['current_post_id'] );
1321
1322 // Verify if the FV Player Video Custom Field for such meta_key exists
1323 if( $post && !empty( FV_Player_Custom_Videos_Master()->aMetaBoxes[ $post->post_type ][ $post_data['current_post_meta_key'] ] ) ) {
1324 update_post_meta( $post_data['current_post_id'], $post_data['current_post_meta_key'], '[fvplayer id="' . $id. '"]' );
1325
1326 $this->store_post_ids( $post->ID );
1327 }
1328 }
1329
1330 $current_video_to_edit = isset($post_data['current_video_to_edit']) ? $post_data['current_video_to_edit'] : -1;
1331
1332 $current_editor_tab = ! empty( $post_data['current_editor_tab'] ) ? $post_data['current_editor_tab'] : false;
1333
1334 wp_send_json( $this->db_load_player_data( $id, $current_video_to_edit, $current_editor_tab ) );
1335
1336 } else {
1337 global $wpdb;
1338 wp_send_json( array( 'fatal_error' => true, 'error' => 'Failed to save player: '.$wpdb->last_error ) );
1339 }
1340 } else {
1341 // Used for player preview
1342 $player->link2meta( $player_meta );
1343 return array(
1344 'player' => $player,
1345 'videos' => $video_ids
1346 );
1347 }
1348 }
1349
1350 if (!$data) {
1351 die();
1352 }
1353 }
1354
1355
1356
1357 /**
1358 * AJAX method to return database data for the player ID given
1359 */
1360 public function open_player_for_editing() {
1361 global $fv_fp;
1362
1363 if ( isset( $_POST['playerID'] ) && is_numeric( $_POST['playerID'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-load" ) ) {
1364
1365 $load_player_id = absint( $_POST['playerID'] );
1366
1367 // load player and its videos from DB
1368 if ( !$this->getPlayerAttsFromDb( array( 'id' => $load_player_id ) ) ) {
1369 header("HTTP/1.0 404 Not Found");
1370 die();
1371 }
1372
1373 $userID = get_current_user_id();
1374 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
1375
1376 if( $cannot_edit_other_posts && $fv_fp->current_player() ) {
1377 $author = $fv_fp->current_player()->getAuthor();
1378 if( $userID !== $author ) {
1379 wp_send_json( array( 'error' => 'You don\'t have permission to edit this player.' ) );
1380 die();
1381 }
1382 }
1383
1384 // check player's meta data for an edit lock
1385 if ($fv_fp->current_player() && count($fv_fp->current_player()->getMetaData())) {
1386 $edit_lock_found = false;
1387 foreach ($fv_fp->current_player()->getMetaData() as $meta_object) {
1388 $key = $meta_object->getMetaKey();
1389 $user_locked = str_replace('edit_lock_', '', $key);
1390 if ( strstr($key, 'edit_lock_') !== false ) {
1391 $edit_lock_found = true;
1392
1393 if ( $user_locked != $userID) {
1394 // someone else is editing this video, first check the timestamp
1395 $last_tick = $meta_object->getMetaValue();
1396 if (time() - $last_tick > $this->edit_lock_timeout_seconds) {
1397 // timeout, remove lock, add lock for this user
1398 $meta_object->delete();
1399
1400 $meta = new FV_Player_Db_Player_Meta(null, array(
1401 'id_player' => $fv_fp->current_player()->getId(),
1402 'meta_key' => 'edit_lock_'.$userID,
1403 'meta_value' => time()
1404 ), $this);
1405
1406 $meta->save();
1407 } else {
1408 $user = get_userdata($user_locked);
1409 $name = 'Somebody else';
1410 if( $user ) {
1411 if( !empty($user->display_name) ) $name = $user->display_name;
1412 if( !empty($user->user_nicename) ) $name = $user->user_nicename;
1413 }
1414 wp_send_json( array( 'error' => $name." is editing this player at the moment. Please try again later." ) );
1415 die();
1416 }
1417 } else {
1418 // same user, extend the lock
1419 $meta_object->setMetaValue(time());
1420 $meta_object->save();
1421 }
1422 }
1423 }
1424
1425 // no edit lock meta record - create new one
1426 if (!$edit_lock_found) {
1427 $meta = new FV_Player_Db_Player_Meta( null, array(
1428 'id_player' => $fv_fp->current_player()->getId(),
1429 'meta_key' => 'edit_lock_' . $userID,
1430 'meta_value' => time()
1431 ), $this );
1432
1433 $meta->save();
1434 }
1435 } else {
1436 // add player edit lock if none was found
1437 if ($fv_fp->current_player()) {
1438 $meta = new FV_Player_Db_Player_Meta( null, array(
1439 'id_player' => $fv_fp->current_player()->getId(),
1440 'meta_key' => 'edit_lock_' . $userID,
1441 'meta_value' => time()
1442 ), $this );
1443
1444 $meta->save();
1445 }
1446 }
1447
1448 // intval() below is important as -1 means no particular video is being edited
1449 $out = $this->db_load_player_data( $load_player_id, intval( $_POST['current_video_to_edit'] ) );
1450
1451 if( empty($out['videos']) ) {
1452 wp_send_json( array( 'error' => "Failed to load videos for this player." ) );
1453 exit;
1454 }
1455
1456 /**
1457 * Detect bug with duplicate players
1458 */
1459 if ( $fv_fp->current_player() ) {
1460 $video_ids = explode( ',', $fv_fp->current_player()->getVideoIds() );
1461
1462 $db_options = array(
1463 'select_fields' => 'player_name, date_created, videos, author, status',
1464 'search_by_video_ids' => $video_ids,
1465 );
1466
1467 $this->query_players( $db_options );
1468
1469 $players = $this->getPlayersCache();
1470
1471 if ( $players && count($players) ) {
1472 $out['debug_duplicate_players'] = array();
1473
1474 foreach ($players as $player) {
1475 if ( $player->getId() != $load_player_id ) {
1476 $out['debug_duplicate_players'][] = $player->getId();
1477 }
1478 }
1479 }
1480 }
1481
1482 /**
1483 * Output JSON
1484 */
1485 header('Content-Type: application/json');
1486 if (version_compare(phpversion(), '5.3', '<')) {
1487 echo wp_json_encode($out);
1488 } else {
1489 echo wp_json_encode($out, true);
1490 }
1491 die();
1492
1493 } else {
1494 wp_send_json( array( 'error' => 'Security check failed.' ) );
1495 die();
1496 }
1497 }
1498
1499 /**
1500 * Search for players, set internal cache or return
1501 * count if $args['count'] is true
1502 *
1503 * @param array $args
1504 *
1505 * @return void|int
1506 */
1507 public function query_players( $args ) {
1508 global $wpdb;
1509
1510 $args = wp_parse_args( $args, array(
1511 'author_id' => false,
1512 'ids' => false, // should not be used together with count
1513 'offset' => false,
1514 'order' => false,
1515 'order_by' => false,
1516 'per_page' => false,
1517 'post_type' => false,
1518 'search_by_video_ids' => false,
1519 'search_string' => false,
1520 'select_fields' => false,
1521 'count' => false
1522 ) );
1523
1524 $ids = array();
1525 if( is_array($args['ids']) ) {
1526 $ids = $args['ids'];
1527 } else if( $args['ids'] ) {
1528 $ids = explode( ',', $args['ids'] );
1529 }
1530
1531 $query_ids = array();
1532 foreach ( $ids as $id_key => $id_value ) {
1533 // check if this player is not cached yet
1534 if (!$this->isPlayerCached($id_value)) {
1535 $query_ids[ $id_key ] = (int) $id_value;
1536 }
1537 }
1538
1539 // Are we querying players by IDs, but is it all already cached?
1540 if( count($ids) > 0 && count($query_ids) == 0 ) {
1541 return;
1542 }
1543
1544 // load multiple players via their IDs but a single query and return their values
1545 $select = 'p.*';
1546 if( !empty($args['select_fields']) ) {
1547 $select = 'p.id,'.esc_sql($args['select_fields']);
1548 }
1549
1550 if($args['count']) {
1551 $select = 'count(*) as row_count';
1552 }
1553
1554 $where = ' WHERE 1=1 ';
1555 if( count($query_ids) ) {
1556 $where .= ' AND p.id IN('. implode(',', $query_ids).') ';
1557
1558 // if we have multiple video IDs to load players for, let's prepare a like statement here
1559 } else if( is_array($args['search_by_video_ids']) ) {
1560 $where_like_part = array();
1561
1562 if ( !empty( $args['search_string'] ) ) {
1563 // TODO: Escape in some better way
1564 $where_like_part[] = 'player_name LIKE "%' . esc_sql( $args['search_string'] ) . '%"';
1565 }
1566
1567 foreach ($args['search_by_video_ids'] as $player_video_id) {
1568 $where_like_part[] = "FIND_IN_SET( " . intval( $player_video_id ) .", videos ) > 0";
1569 }
1570
1571 $where .= ' AND (' . implode(' OR ', $where_like_part) . ') ';
1572 }
1573
1574 if( !empty( $args['author_id']) ) {
1575 $where .= ' AND author ='.intval($args['author_id']).' ';
1576 }
1577
1578 $order = '';
1579 if( !empty($args['order_by']) ) {
1580
1581 // Verify that each order by is valid
1582 $order_by_items = explode( ',', $args['order_by'] );
1583 $order_by_items = array_map( 'trim', $order_by_items );
1584
1585 foreach( $order_by_items AS $k => $v ) {
1586 if( !in_array($v, $this->valid_order_by ) ) {
1587 unset($order_by_items[$k]);
1588 }
1589 }
1590
1591 if( count($order_by_items) > 0 ) {
1592 $order = ' ORDER BY '.implode( ', ', array_map( 'esc_sql', $order_by_items ) );
1593 if( !empty($args['order']) ) {
1594 if( in_array($args['order'], array( 'asc', 'desc' ) ) ) {
1595 $order .= ' '.esc_sql($args['order']);
1596 }
1597 }
1598 }
1599 }
1600
1601 $limit = '';
1602 if( $args['offset'] !== false && $args['per_page'] !== false ) {
1603 $limit = $wpdb->prepare( ' LIMIT %d, %d', $args['offset'], $args['per_page'] );
1604 }
1605
1606 $post_type_join = '';
1607 $tax_join = '';
1608 if( $args['post_type'] ) {
1609
1610 // Get players which are not embedded in any post = no post_id playermeta
1611 if ( 'none' === $args['post_type'] ) {
1612 $post_type_join = 'LEFT JOIN `'.FV_Player_Db_Player_Meta::get_db_table_name().'` AS pm ON p.id = pm.id_player AND pm.meta_key = "post_id" ';
1613
1614 $where .= ' AND pm.id IS NULL';
1615
1616 } else {
1617 $post_type_join = 'JOIN `'.FV_Player_Db_Player_Meta::get_db_table_name().'` AS pm ON p.id = pm.id_player JOIN `'.$wpdb->posts.'` AS posts ON posts.ID = pm.meta_value ';
1618
1619 $where .= $wpdb->prepare( ' AND pm.meta_key = "post_id" AND posts.post_type = %s', $args['post_type'] );
1620 }
1621
1622 // Is there any known taxonomy in $args ?
1623 $post_type_taxonomies = fv_player_get_post_type_taxonomies( $args['post_type'] );
1624
1625 foreach( $post_type_taxonomies AS $tax) {
1626 if ( !empty( $args[ 'tax_' . $tax ] ) ) {
1627 $tax_join = "
1628 INNER JOIN {$wpdb->term_relationships} AS tr ON posts.ID = tr.object_id
1629 INNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
1630 INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id";
1631
1632 $where .= ' AND t.slug = "' . esc_sql( $args[ 'tax_' . $tax ] ) . '"';
1633 $where .= ' AND tt.taxonomy = "' . esc_sql( $tax ) . '"';
1634 }
1635 }
1636 }
1637
1638 if($args['count']) {
1639 $group_order = '';
1640 } else {
1641 $group_order = ' GROUP BY p.id'.$order.$limit;
1642 }
1643
1644 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1645 $player_data = $wpdb->get_results( "SELECT {$select} FROM `{$wpdb->prefix}fv_player_players` AS p {$post_type_join} {$tax_join} {$where} {$group_order}" );
1646
1647 if($args['count']) {
1648 return intval($player_data[0]->row_count);
1649 }
1650
1651 /**
1652 * Also load count of subtitles, cues, chapters and transcripts
1653 *
1654 * If we do this is the original query with JOIN it takes 10x longer
1655 */
1656 if( is_admin() && count( $player_data ) > 0 ) {
1657 $placeholders = implode( ', ', array_fill( 0, count( $player_data ), '%d' ) );
1658
1659 $meta_counts = $wpdb->get_results(
1660 $wpdb->prepare(
1661 // $placeholders is a string of %d created above
1662 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1663 "SELECT p.id,
1664 count(subtitles.id) as subtitles_count,
1665 count(cues.id) as cues_count,
1666 count(chapters.id) as chapters_count,
1667 count(meta_transcript.id) as transcript_count
1668 FROM `{$wpdb->prefix}fv_player_players` AS p
1669 JOIN `{$wpdb->prefix}fv_player_videos` AS v on FIND_IN_SET(v.id, p.videos)
1670 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS subtitles ON v.id = subtitles.id_video AND subtitles.meta_key like 'subtitles%'
1671 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS cues ON v.id = cues.id_video AND cues.meta_key like 'cues%'
1672 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS chapters ON v.id = chapters.id_video AND chapters.meta_key = 'chapters'
1673 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS meta_transcript ON v.id = meta_transcript.id_video AND meta_transcript.meta_key LIKE 'transcript_src%'
1674 WHERE p.id IN( $placeholders )
1675 GROUP BY p.id",
1676 wp_list_pluck( $player_data, 'id' )
1677 ),
1678 OBJECT_K
1679 );
1680
1681 foreach( $player_data as $k => $v ) {
1682 if ( ! empty( $meta_counts[ $v->id ] ) ) {
1683 $meta_count = $meta_counts[ $v->id ];
1684 $player_data[ $k ]->subtitles_count = $meta_count->subtitles_count;
1685 $player_data[ $k ]->cues_count = $meta_count->cues_count;
1686 $player_data[ $k ]->chapters_count = $meta_count->chapters_count;
1687 $player_data[ $k ]->transcript_count = $meta_count->transcript_count;
1688 }
1689 }
1690 }
1691
1692 $cache = array();
1693
1694 foreach( $player_data AS $db_record ) {
1695 // create a new video object and populate it with DB values
1696 $record_id = $db_record->id;
1697 // if we don't unset this, we'll get warnings
1698 unset($db_record->id);
1699
1700 $player_object = new FV_Player_Db_Player( null, get_object_vars( $db_record ), $this );
1701 $player_object->link2db( $record_id );
1702
1703 // cache this player in DB object
1704 $cache[$record_id] = $player_object;
1705 }
1706
1707 if ( ! empty( $cache ) ) {
1708 $this->setPlayersCache($cache);
1709 }
1710 }
1711
1712 /**
1713 * Receive Heartbeat data and checks for DB edit lock.
1714 * In case the lock is found and valid, it will be extended.
1715 *
1716 * @param array $response Heartbeat response data to pass back to front end.
1717 * @param array $data Data received from the front end (unslashed).
1718 *
1719 * @return array Returns the same response as received, as we don't need to update it or read it anywhere in JS.
1720 * @throws Exception When the underlying meta object throws an exception.
1721 */
1722 function check_db_edit_lock( $response, $data ) {
1723 global $FV_Player_Db;
1724
1725 $userID = get_current_user_id();
1726
1727 // extend an existing lock
1728 if ( !empty( $data['fv_flowplayer_edit_lock_id'] ) ) {
1729 $player_id = $data['fv_flowplayer_edit_lock_id'];
1730
1731 if ($FV_Player_Db && $FV_Player_Db->isPlayerCached($player_id)) {
1732 $player = $FV_Player_Db->getPlayersCache();
1733 $player = $player[$player_id];
1734 } else {
1735 $player = new FV_Player_Db_Player($player_id, array(), $FV_Player_Db);
1736 }
1737
1738 if ($player->getIsValid()) {
1739 $found = false;
1740 if (count($player->getMetaData())) {
1741 foreach ($player->getMetaData() as $meta_object) {
1742 if ( strstr($meta_object->getMetaKey(), 'edit_lock_') !== false ) {
1743 if (str_replace('edit_lock_', '', $meta_object->getMetaKey()) == $userID) {
1744 $found = true;
1745
1746 // same user, extend the lock
1747 $meta_object->setMetaValue(time());
1748 $meta_object->save();
1749 }
1750 }
1751 }
1752 }
1753
1754 if( !$found ) {
1755 $meta_object = new FV_Player_Db_Player_Meta(null, array(
1756 'id_player' => $player_id,
1757 'meta_key' => 'edit_lock_'.$userID,
1758 'meta_value' => time()
1759 ), $FV_Player_Db);
1760 $meta_object->save();
1761 }
1762 }
1763 }
1764
1765 // remove locks that are no longer being edited
1766 if ( !empty( $data['fv_flowplayer_edit_lock_removal'] ) && count($data['fv_flowplayer_edit_lock_removal']) ) {
1767 // load meta for all players to remove locks for (and to auto-cache them as well)
1768 new FV_Player_Db_Player_Meta(null, array('id_player' => array_keys($data['fv_flowplayer_edit_lock_removal'])), $this);
1769 $meta = $this->getPlayerMetaCache();
1770 $locks_removed = array();
1771
1772 if (count($meta)) {
1773 foreach ( $meta as $player ) {
1774 foreach ($player as $meta_object) {
1775 if ( strstr( $meta_object->getMetaKey(), 'edit_lock_' ) !== false ) {
1776 if ( str_replace( 'edit_lock_', '', $meta_object->getMetaKey() ) == $userID ) {
1777 // correct user, delete the lock
1778 $meta_object->delete();
1779 }
1780
1781 $locks_removed[$meta_object->getIdPlayer()] = 1;
1782 }
1783 }
1784 }
1785
1786 $response['fv_flowplayer_edit_locks_removed'] = $locks_removed;
1787 }
1788 }
1789
1790 return $response;
1791 }
1792
1793 /**
1794 * AJAX function to return JSON-formatted export data
1795 * for a specific player ID.
1796 *
1797 * Works for single player only right now!
1798 *
1799 * @param null $unused Populated by WordPress, not used in this method.
1800 * @param bool $output_result If true, the export data will be returned instead of outputted.
1801 * Used when cloning a player.
1802 *
1803 * @return array Returns the actual export data in an associative array, if $output_result is false.
1804 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
1805 */
1806 public function export_player_data($unused = null, $output_result = true, $id = false ) {
1807
1808 if ( !$id ) {
1809 // The player ID is part of the nonce below, so we need to get it from the POST data
1810 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1811 $id = !empty( $_POST['playerID'] ) ? absint( $_POST['playerID'] ) : false;
1812 }
1813
1814 if( defined('DOING_AJAX') && DOING_AJAX &&
1815 ( empty($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-export-".$id ) )
1816 ) {
1817 die('Security check failed');
1818 }
1819
1820 if ( $id ) {
1821 // first, load the player
1822 $player = new FV_Player_Db_Player($id, array(), $this);
1823 if ($player && $player->getIsValid()) {
1824 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
1825 $author_id = get_current_user_id();
1826
1827 if( $cannot_edit_other_posts ) {
1828 if( $author_id !== $player->getAuthor() ) {
1829 die('You don\'t have permission to export this player.');
1830 }
1831 }
1832
1833 $export_data = $player->export();
1834
1835 // load player meta data
1836 $meta = $player->getMetaData();
1837 if ($meta && count($meta)) {
1838 $export_data['meta'] = array();
1839
1840 foreach ($meta as $meta_data) {
1841 // don't include edit locks
1842 if ( strstr($meta_data->getMetaKey(), 'edit_lock_') === false ) {
1843 $export_data['meta'][] = $meta_data->export();
1844 }
1845 }
1846 }
1847
1848 // load videos and meta for this player
1849 $videos = $player->getVideos();
1850
1851 // this line will load and cache meta for all videos at once
1852 new FV_Player_Db_Video_Meta(null, array('id_video' => explode(',', $player->getVideoIds())), $this);
1853
1854 if ($videos && count($videos)) {
1855 $export_data['videos'] = array();
1856
1857 foreach ($videos as $video) {
1858 $video_export_data = $video->export();
1859
1860 // load all meta data for this video
1861 if ($this->isVideoMetaCached($video->getId())) {
1862 $video_export_data['meta'] = array();
1863
1864 foreach ($this->video_meta_cache[$video->getId()] as $meta) {
1865 $video_export_data['meta'][] = $meta->export();
1866 }
1867 }
1868
1869 $export_data['videos'][] = $video_export_data;
1870 }
1871 }
1872 } else {
1873 if ($output_result) {
1874 die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
1875 } else {
1876 return false;
1877 }
1878 }
1879
1880 if ($output_result) {
1881 if (version_compare(phpversion(), '5.3', '<')) {
1882 echo wp_json_encode($export_data);
1883 } else {
1884 echo wp_json_encode($export_data, true);
1885 }
1886 exit;
1887 } else {
1888 return $export_data;
1889 }
1890 } else {
1891 if ($output_result) {
1892 die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
1893 } else {
1894 return false;
1895 }
1896 }
1897 }
1898
1899 /**
1900 * AJAX function to import JSON-formatted export data.
1901 *
1902 * Works for single player only right now!
1903 *
1904 * @param null $unused Populated by WordPress, not used in this method.
1905 * @param bool $output_result If true, the import result will be returned instead of outputted.
1906 * Used when cloning a player.
1907 * @param array|null $alternative_data If set, this is an alternative source of data to import.
1908 * Used when cloning a player.
1909 *
1910 * @return string Returns the actual player ID, if $output_result is false.
1911 *
1912 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
1913 */
1914 public function import_player_data($unused = null, $output_result = true, $alternative_data = null) {
1915 global $FV_Player_Db;
1916
1917 if ( $alternative_data !== null ) {
1918 $data = $alternative_data;
1919
1920 } else if( isset( $_POST['data'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-import" ) ) {
1921
1922 // TODO: How to better sanitize this?
1923 $data = json_decode( stripslashes( $_POST['data'] ), true );
1924 }
1925
1926 if ( $data ) {
1927 try {
1928
1929 $time_import_start = microtime(true);
1930
1931 // first, create the player
1932 $player_keys = $data;
1933 unset($player_keys['meta'], $player_keys['videos']);
1934
1935 foreach( $player_keys AS $k => $v ) {
1936 if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
1937 $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
1938 $player_keys[$new] = $v;
1939 unset($player_keys[$k]);
1940 }
1941 }
1942
1943 $player = new FV_Player_Db_Player(null, $player_keys, $FV_Player_Db);
1944 $player_video_ids = array();
1945
1946 // create player videos, along with meta data
1947 // ... don't save the player yet, as we need all video IDs to be known
1948 // before doing so
1949 if (isset($data['videos'])) {
1950 foreach ($data['videos'] as $video_data) {
1951 // replace caption for title, remove caption
1952 if( isset($video_data['caption']) && !empty($video_data['caption']) && ( !isset($video_data['title']) || empty($video_data['title']) ) ) {
1953 $video_data['title'] = $video_data['caption'];
1954 unset($video_data['caption']);
1955 }
1956
1957 foreach( $video_data AS $k => $v ) {
1958 if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
1959 $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
1960 $video_data[$new] = $v;
1961 unset($video_data[$k]);
1962 }
1963 }
1964
1965 // check meta first before importing and migrate to new format
1966 if (isset($video_data['meta'])) {
1967 foreach ($video_data['meta'] as $k => $video_meta_data) {
1968
1969 // Note: Video duration is checked during the import anyway, but we keep the conversion routine and it might come handy in the future
1970 if( $video_meta_data['meta_key'] == 'duration') { // duration is now in video data
1971 if( !isset( $video_data['duration']) ) {
1972 $video_data['duration'] = $video_meta_data['meta_value'];
1973 }
1974
1975 unset($video_data['meta'][$k]);
1976 }
1977
1978 // Note: Video live flag is checked during the import anyway, but we keep the conversion routine and it might come handy in the future
1979 if( $video_meta_data['meta_key'] == 'live') { // live is now in video data
1980 if( !isset( $video_data['live']) ) {
1981 $video_data['live'] = $video_meta_data['meta_value'];
1982 }
1983
1984 unset($video_data['meta'][$k]);
1985 }
1986
1987 if( $video_meta_data['meta_key'] == 'transcript' ) { // rename transcript to transcript_src
1988 $new_exists = false;
1989 foreach( $video_data['meta'] as $m2) {
1990 if( $m2['meta_key'] == 'transcript_src' ) {
1991 $new_exists = true;
1992 break;
1993 }
1994 }
1995
1996 if(!$new_exists) {
1997 $video_data['meta'][] = array(
1998 'meta_key' => 'transcript_src',
1999 'meta_value' => $video_meta_data['meta_value']
2000 );
2001 }
2002
2003 unset($video_data['meta'][$k]);
2004 }
2005 }
2006 } else {
2007 $video_data['meta'] = array();
2008 }
2009
2010 // Skip video meta check if the import is taking more than 10 seconds.
2011 // We could also rely on the PHP max_execution_time/2 or so.
2012 $skip_video_meta_check = ( microtime(true) - $time_import_start ) > 10;
2013
2014 $video_object = new FV_Player_Db_Video(null, $video_data, $FV_Player_Db);
2015 $id_video = $video_object->save( $video_data['meta'], false, $skip_video_meta_check );
2016
2017 $player_video_ids[] = $id_video;
2018 }
2019 }
2020
2021 // set video IDs for the player
2022 $player->setVideos(implode(',', $player_video_ids));
2023
2024 // save player
2025 $id_player = $player->save(
2026 isset($data['meta']) ? $data['meta'] : array(),
2027 true
2028 );
2029
2030 } catch (Exception $e) {
2031 if ($output_result) {
2032 die( $e );
2033 } else {
2034 return $e;
2035 }
2036 }
2037
2038 if ($output_result) {
2039 die( (string) $id_player );
2040 } else {
2041 return (string) $id_player;
2042 }
2043 } else {
2044 if ($output_result) {
2045 die('No valid import data found, import unsuccessful');
2046 } else {
2047 return 'No valid import data found, import unsuccessful';
2048 }
2049 }
2050 }
2051
2052 public static function has_table_column( $table, $column ) {
2053 global $wpdb;
2054 return $wpdb->get_results(
2055 $wpdb->prepare(
2056 "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
2057 DB_NAME,
2058 $table,
2059 $column
2060 )
2061 );
2062 }
2063
2064 /**
2065 * AJAX function to remove a player from database.
2066 *
2067 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
2068 */
2069 public function remove_player() {
2070 if (isset($_POST['playerID']) && is_numeric($_POST['playerID']) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-remove-".$_POST['playerID'] ) ) {
2071
2072 // first, load the player
2073 $player = new FV_Player_Db_Player( absint( $_POST['playerID'] ), array(), $this);
2074 if ($player && $player->getIsValid()) {
2075 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
2076 $author_id = get_current_user_id();
2077
2078 // check if user can delete player
2079 if( $cannot_edit_other_posts ) {
2080 if( $author_id !== $player->getAuthor() ) {
2081 die('You don\'t have permission to delete this player.');
2082 }
2083 }
2084
2085 // remove the player
2086 if ($player->delete()) {
2087 echo 1;
2088 exit;
2089 } else {
2090 die( 'Could not remove player' );
2091 }
2092 } else {
2093 die( 'Invalid player ID' );
2094 }
2095 } else {
2096 die( 'Invalid player ID' );
2097 }
2098 }
2099
2100 /**
2101 * AJAX function to clone a player in the database.
2102 *
2103 * Works for single player only right now!
2104 *
2105 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
2106 */
2107 public function clone_player() {
2108 if ( isset( $_POST['playerID'] ) && is_numeric( $_POST['playerID'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv-player-db-export-' . absint( $_POST['playerID'] ) ) ) {
2109 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
2110 $author_id = get_current_user_id();
2111
2112 $player = new FV_Player_Db_Player( intval($_POST['playerID']), array(), $this );
2113
2114 if( $cannot_edit_other_posts ) {
2115 if( $author_id !== $player->getAuthor() ) {
2116 die('You don\'t have permission to clone this player.');
2117 }
2118 }
2119
2120 $export_data = $this->export_player_data(null, false);
2121
2122 // do not clone information about where the player is embeded
2123 if (isset($export_data['meta'])) {
2124 foreach($export_data['meta'] as $h => $v){
2125 if($v['meta_key'] == 'post_id'){
2126 unset($export_data['meta'][$h]);
2127 }
2128 }
2129 }
2130
2131 echo esc_html( $this->import_player_data(null, false, $export_data) );
2132 exit;
2133 } else {
2134 die('no valid player ID found, cloning unsuccessful');
2135 }
2136 }
2137
2138 /**
2139 * AJAX method to retrieve IDs and names of all players to be populated
2140 * into a dropdown in the front-end.
2141 */
2142 public function retrieve_all_players_for_dropdown() {
2143 if( !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv-player-editor-search-nonce' ) ) {
2144 wp_send_json_error( 'Nonce verification failed! Please reload the page.' );
2145 }
2146
2147 $search = !empty( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : false;
2148
2149 $players = $this->getListPageData( array(
2150 'order' => 'desc',
2151 'order_by' => 'date_created',
2152 'search' => $search
2153 ) );
2154
2155 $json_data = array(
2156 'success' => true,
2157 'players' => array()
2158 );
2159
2160 foreach ($players as $player) {
2161 $json_data['players'][] = array(
2162 'id' => $player->id,
2163 'player_name' => $player->player_name,
2164 'video_titles' => $player->video_titles,
2165 'thumbs' => $player->thumbs,
2166 'date_created' => gmdate( get_option( 'date_format' ), strtotime( $player->date_created ) ),
2167 'embeds' => $player->embeds,
2168 );
2169 }
2170
2171 wp_send_json( $json_data );
2172 }
2173
2174 /**
2175 * Runs on save_post hook and it stored the post ID in player meta. It also checks any player meta which is pointing to this post and if it's no longer found in it the meta is removed.
2176 *
2177 * @param int $post_id Populated by WordPress, the post ID
2178 */
2179 public function store_post_ids( $post_id ) {
2180 global $wpdb;
2181
2182 if ( wp_is_post_revision( $post_id ) ) return;
2183
2184 $post = get_post($post_id);
2185
2186 $matches = array();
2187 if( preg_match_all('~\[fvplayer.*?id=[\'"]([0-9,]+)[\'"].*?\]~', $post->post_content, $matches1 ) ) {
2188 $matches = array_merge( $matches, $matches1[1] );
2189 }
2190
2191 // The [fvplayer] shortcode might be stored in plain form, or with the quotes escaped like fvplayer id=\"56\"]
2192 if( preg_match_all('~\[fvplayer.*?id=\\\?[\'"]([0-9,]+)~', implode( array_map( 'implode', get_post_custom($post_id) ) ), $matches2 ) ) {
2193 $matches = array_merge( $matches, $matches2[1] );
2194 }
2195
2196 $ids = array();
2197
2198 if( $matches ) {
2199 foreach( $matches AS $match ) {
2200 foreach( explode(',',$match) AS $match_match ) {
2201 $ids[] = $match_match;
2202 }
2203 }
2204
2205 $ids = array_unique($ids);
2206 foreach( $ids AS $player_id ) {
2207
2208 $player = new FV_Player_Db_Player($player_id);
2209 if( $player->getIsValid() ) {
2210
2211 $add = true;
2212 // TODO: This seems to not work when saving with Elementor, it seems store_post_ids() runs 3 times
2213 // but it's never aware of the player meta added using FV_Player_Db_Player_Meta in the previous run
2214 $metas = $player->getMetaData();
2215 if( count($metas) ) {
2216 foreach( $metas as $meta_object ) {
2217 if( $meta_object->getMetaKey() == 'post_id' ) {
2218 if( $meta_object->getMetaValue() == $post_id ) {
2219 $add = false;
2220 }
2221 }
2222 }
2223 }
2224
2225 // TODO: So here's the temporary work-around which should be removed once FV_Player_Db_Player_Meta()
2226 // does properly register the player meta with getMetaData()
2227 if( $wpdb->get_var( $wpdb->prepare("SELECT meta_value FROM {$wpdb->prefix}fv_player_playermeta WHERE id_player = %d AND meta_key = %s AND meta_value = %d", $player_id, 'post_id', $post_id ) ) ) {
2228 $add = false;
2229 }
2230
2231 if( $add ) {
2232 $meta = new FV_Player_Db_Player_Meta(null, array(
2233 'id_player' => $player_id,
2234 'meta_key' => 'post_id',
2235 'meta_value' => $post_id
2236 ) );
2237
2238 $meta->save();
2239
2240 // Make sure the player is no longer a Draft is used in a post
2241 $player->setStatus('published');
2242 $player->save();
2243 }
2244 }
2245
2246 }
2247 }
2248
2249 /**
2250 * Check if table exists before looking for FV Player that is associated in the post.
2251 * We do this because we would run into issues with this in WP Integration tests.
2252 * The database tables get created by tests like FV_Player_DBTest::setUp() but somehow
2253 * wptests_fv_player_playermetas is not there
2254 */
2255 $table_name = FV_Player_Db_Player_Meta::init_db_name();
2256
2257 if( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) == $table_name ) {
2258
2259 $remove = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE meta_key = 'post_id' AND meta_value = %s ", $post_id ) );
2260 if( $remove ) {
2261 foreach( $remove AS $removal ) {
2262 if( !in_array($removal->id_player,$ids) ) {
2263 $d_meta = new FV_Player_Db_Player_Meta($removal->id);
2264 $d_meta->link2db( $removal->id );
2265 $d_meta->delete();
2266 }
2267 }
2268 }
2269 }
2270 }
2271
2272 public static function get_player_duration( $id ) {
2273 global $wpdb;
2274 return $wpdb->get_var( $wpdb->prepare( "SELECT sum(v.duration) FROM {$wpdb->prefix}fv_player_videos AS v JOIN {$wpdb->prefix}fv_player_players AS p ON FIND_IN_SET(v.id, p.videos) WHERE p.id = %d", $id ) );
2275 }
2276
2277 /**
2278 * Searches for a player video via custom query.
2279 *
2280 * @param array $args Array with search arguments.
2281 *
2282 * @return array|bool Returns array of FV_Player_Db_Video if any data were loaded, false otherwise.
2283 */
2284 public function query_videos($args) {
2285 global $wpdb;
2286
2287 $args = wp_parse_args( $args,
2288 array(
2289 'fields_to_search' => array(
2290 'src'
2291 ),
2292 'search_string' => '',
2293 'like' => false,
2294 'and_or' => 'OR'
2295 )
2296 );
2297
2298 // assemble where part
2299 $where = array();
2300
2301 /*
2302 * Inspired by core WP WP_Query::parse_search() but adjusted to make it fit our SQL query
2303 */
2304 if ( $args['like'] ) {
2305 $search_terms_count = 1;
2306 $search_terms = '';
2307
2308 $args['search_string'] = stripslashes( $args['search_string'] );
2309
2310 if( (substr($args['search_string'], 0,1) == "'" && substr( $args['search_string'],-1) == "'") || (substr($args['search_string'], 0,1) == '"' && substr( $args['search_string'],-1) == '"') ) { // Dont break term if in '' or ""
2311 $args['search_string'] = substr($args['search_string'], 1, -1);
2312 $search_terms = array( $args['search_string'] );
2313 } else {
2314 if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $args['search_string'], $matches ) ) {
2315 $search_terms_count = count( $matches[0] );
2316 $search_terms = self::parse_search_terms( $matches[0] );
2317 // If the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
2318 if ( empty( $search_terms ) || count( $search_terms ) > 9 ) {
2319 $search_terms = array( $args['search_string'] );
2320 }
2321 } else {
2322 $search_terms = array( $args['search_string'] );
2323 }
2324 }
2325
2326 $search_terms_encoded = array();
2327
2328 foreach( $search_terms as $term ) {
2329 $search_terms_encoded[] = $term;
2330 $search_terms_encoded[] = urlencode($term);
2331 $search_terms_encoded[] = rawurlencode($term);
2332 }
2333
2334 $search_terms = array_unique( $search_terms_encoded );
2335
2336 $search_terms = array_unique( $search_terms );
2337
2338 unset($search_terms_encoded);
2339
2340 $exclusion_prefix = apply_filters( 'wp_query_search_exclusion_prefix', '-' );
2341
2342 foreach ($args['fields_to_search'] as $field_name) {
2343 $field_name = sanitize_key($field_name);
2344 $searchlike = '';
2345 $first = true;
2346 foreach ( $search_terms as $term ) {
2347 // If there is an $exclusion_prefix, terms prefixed with it should be excluded.
2348 $exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix );
2349
2350 if( ! $first ) {
2351 if ( $exclude ) {
2352 $searchlike .= ' AND ';
2353 } else {
2354 $searchlike .= ' OR ';
2355 }
2356 }
2357
2358 if ( $exclude ) {
2359 $term = substr( $term, 1 );
2360 $searchlike .= $wpdb->prepare( "(v.{$field_name} NOT LIKE %s)", '%' . $wpdb->esc_like( substr( $term, 1 ) ) . '%' );
2361 } else {
2362 $searchlike .= $wpdb->prepare( "(v.{$field_name} LIKE %s)", '%' . $wpdb->esc_like( $term ) . '%' );
2363 }
2364
2365 $first = false;
2366 }
2367 $where[] = "(". $searchlike .")";
2368 }
2369
2370 } else { // TODO same as like
2371 foreach ($args['fields_to_search'] as $field_name) {
2372 $field_name = sanitize_key($field_name);
2373 $where[] = "v.$field_name ='" . esc_sql($args['search_string']) . "'";
2374 }
2375 }
2376
2377 $where = implode(' '.esc_sql($args['and_or']).' ', $where);
2378
2379 // TODO: Sort by subtitles_count, 'chapters_count and transcript_count should be added here
2380 // TODO: Search the meta values too
2381 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2382 $video_data = $wpdb->get_results(
2383 "SELECT v.* FROM `{$wpdb->prefix}fv_player_videos` AS v JOIN `{$wpdb->prefix}fv_player_players` AS p ON FIND_IN_SET(v.id, p.videos) WHERE {$where} ORDER BY v.id DESC"
2384 );
2385
2386 if (!$video_data) {
2387 return false;
2388 }
2389
2390 $videos = array();
2391
2392 foreach( $video_data AS $db_record ) {
2393 // create a new video object and populate it with DB values
2394 $record_id = $db_record->id;
2395 // if we don't unset this, we'll get warnings
2396 unset($db_record->id);
2397
2398 $video_object = new FV_Player_Db_Video( null, get_object_vars( $db_record ), $this );
2399 $video_object->link2db( $record_id );
2400
2401 // cache this player in DB object
2402 $videos[] = $video_object;
2403 }
2404
2405 return $videos;
2406 }
2407
2408 /**
2409 * Copy of core WordPress WP_Query::parse_search_terms() for our purposes without any changes
2410 *
2411 * Check if the terms are suitable for searching.
2412 *
2413 * Uses an array of stopwords (terms) that are excluded from the separate
2414 * term matching when searching for posts. The list of English stopwords is
2415 * the approximate search engines list, and is translatable. ( from class-wp-query.php )
2416 *
2417 * @since 3.7.0
2418 *
2419 * @param string[] $terms Array of terms to check.
2420 * @return string[] Terms that are not stopwords.
2421 */
2422 public function parse_search_terms( $terms ) {
2423 $strtolower = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';
2424 $checked = array();
2425
2426 $stopwords = $this->get_search_stopwords();
2427
2428 foreach ( $terms as $term ) {
2429 // Keep before/after spaces when term is for exact match.
2430 if ( preg_match( '/^".+"$/', $term ) ) {
2431 $term = trim( $term, "\"'" );
2432 } else {
2433 $term = trim( $term, "\"' " );
2434 }
2435
2436 // Avoid single A-Z and single dashes.
2437 if ( ! $term || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
2438 continue;
2439 }
2440
2441 if ( in_array( call_user_func( $strtolower, $term ), $stopwords, true ) ) {
2442 continue;
2443 }
2444
2445 $checked[] = $term;
2446 }
2447
2448 return $checked;
2449 }
2450
2451 /**
2452 * Copy of core WordPress WP_Query::get_search_stopwords() for our purposes without any changes
2453 *
2454 * Retrieve stopwords used when parsing search terms. ( from class-wp-query.php )
2455 *
2456 * @since 3.7.0
2457 *
2458 * @return string[] Stopwords.
2459 */
2460 public function get_search_stopwords() {
2461 if ( isset( $this->stopwords ) ) {
2462 return $this->stopwords;
2463 }
2464
2465 /*
2466 * translators: This is a comma-separated list of very common words that should be excluded from a search,
2467 * like a, an, and the. These are usually called "stopwords". You should not simply translate these individual
2468 * words into your language. Instead, look for and provide commonly accepted stopwords in your language.
2469 */
2470 $words = explode(
2471 ',',
2472 _x(
2473 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www',
2474 'Comma-separated list of search stopwords in your language'
2475 )
2476 );
2477
2478 $stopwords = array();
2479 foreach ( $words as $word ) {
2480 $word = trim( $word, "\r\n\t " );
2481 if ( $word ) {
2482 $stopwords[] = $word;
2483 }
2484 }
2485
2486 /**
2487 * Filters stopwords used when parsing search terms.
2488 *
2489 * @since 3.7.0
2490 *
2491 * @param string[] $stopwords Array of stopwords.
2492 */
2493 $this->stopwords = apply_filters( 'wp_search_stopwords', $stopwords );
2494 return $this->stopwords;
2495 }
2496
2497 /**
2498 * Sanitizes the value for DB class attributes.
2499 *
2500 * TODO: We got a report of PHP warning where the $value was an object in FV_Player_Db_Player_Meta.
2501 * How could that happen and should be sanitize objects and arrays recursively?
2502 *
2503 * @param mixed $value
2504 * @return mixed
2505 */
2506 public static function sanitize( $value ) {
2507
2508 /**
2509 * Avoid issues if the import JSON sets a null value for what's expected to be string "toggle_end_action":null
2510 */
2511 if ( is_string( $value ) ) {
2512 return stripslashes( $value );
2513 } else {
2514 return $value;
2515 }
2516 }
2517
2518 /**
2519 * Strips tags from the value for DB class attributes.
2520 *
2521 * Only "overlay" is allowed to have limited HTML.
2522 *
2523 * @param mixed $value
2524 * @param string $key
2525 *
2526 * @return mixed
2527 */
2528 public static function strip_tags( $value, $key ) {
2529 global $fv_fp;
2530
2531 /**
2532 * Avoid issues if the import JSON sets a null value for what's expected to be string "toggle_end_action":null
2533 */
2534 if ( is_string( $value ) ) {
2535 if( 'overlay' === $key ) {
2536 add_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit' ), 999, 2 );
2537 add_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit_settings' ), 999, 2 );
2538
2539 $value = wp_kses( $value, 'post' );
2540
2541 remove_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit' ), 999, 2 );
2542 remove_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit_settings' ), 999, 2 );
2543
2544 } else {
2545 $value = wp_strip_all_tags( $value );
2546 }
2547 }
2548
2549 return $value;
2550 }
2551
2552 }
2553