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

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

2,598 lines 90.7 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 if ( ! empty( $data['videos'] ) && ! empty( $atts['sort'] ) && in_array( $atts['sort'], array( 'oldest', 'newest', 'reverse', 'title' ) ) ) {
774 $ordered_videos = explode(',', $data['videos']);
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 * Parse JSON request body, tolerating garbage prepended or appended
1018 * to application/json bodies by some hosts, plugins, or WAFs.
1019 *
1020 * We run into a case there the input looked like this:
1021 *
1022 * {"data":{....","editor_post_id":0},"nonce":"6d3fe1b4a9"}&_nonce=a688a5a94f
1023 *
1024 * So we remove everything before the first { and everything after the last }
1025 *
1026 * @param string $raw Raw php://input.
1027 * @return array|null Decoded array or null on failure.
1028 */
1029 private function parse_json_request_body( $raw ) {
1030 $raw = trim( $raw );
1031
1032 if ( empty( $raw ) ) {
1033 return null;
1034 }
1035
1036 $decoded = json_decode( $raw, true );
1037
1038 if ( JSON_ERROR_NONE === json_last_error() ) {
1039 return $decoded;
1040 }
1041
1042 $start = strpos( $raw, '{' );
1043 $end = strrpos( $raw, '}' );
1044
1045 if ( false !== $start && false !== $end && $end > $start ) {
1046 $decoded = json_decode( substr( $raw, $start, $end - $start + 1 ), true );
1047
1048 if ( JSON_ERROR_NONE === json_last_error() ) {
1049 return $decoded;
1050 }
1051 }
1052
1053 return null;
1054 }
1055
1056 /**
1057 * Stored player data in a database from the POST data sent via AJAX
1058 * from the shortcode editor.
1059 *
1060 * @param array $data Alternative data to work with rather than getting these from $_POST.
1061 * Used when previews are being made.
1062 *
1063 * @return void|array Returns nothing when we're saving a new player into the DB,
1064 * otherwise returns a new unsaved player and video instances to be used as needed.
1065 * @throws Exception When any of the underlying objects throw.
1066 */
1067 public function db_store_player_data($data = null) {
1068 global $FV_Player_Db;
1069
1070 $player_options = array();
1071 $video_ids = array();
1072
1073 $raw_input = file_get_contents( 'php://input' );
1074
1075 $post_data = null;
1076 if( is_array($data) ) {
1077 $post_data = $data;
1078
1079 } else if( !empty($raw_input) ) {
1080 $json_post = $this->parse_json_request_body( $raw_input );
1081
1082 if ( null === $json_post ) {
1083 wp_send_json( array(
1084 'error' => 'Error saving: JSON error: ' . json_last_error() . ' - ' . json_last_error_msg(),
1085 'fatal_error' => true
1086 ) );
1087 exit;
1088 }
1089
1090 if( !wp_verify_nonce( sanitize_text_field( wp_unslash( $json_post['nonce'] ) ), "fv-player-edit" ) ) {
1091 wp_send_json( array(
1092 'error' => 'Error saving: Nonce error, please ensure you are logged in and try again.',
1093 'fatal_error' => true
1094 ) );
1095 exit;
1096 }
1097
1098 if( !empty( $json_post['data'] ) ) {
1099 $post_data = $json_post['data'];
1100
1101 // check if user can update player
1102 if(!empty($post_data['update']) && !current_user_can('edit_others_posts') ) {
1103 $player_to_check = new FV_Player_Db_Player(intval($post_data['update']), array(), $FV_Player_Db);
1104
1105 if( $player_to_check->getAuthor() !== get_current_user_id() ) {
1106 wp_send_json( array( 'error' => 'Security check failed.' ) );
1107 }
1108 }
1109 }
1110
1111 } else if( !empty( $_REQUEST['action'] ) && 'fv_player_db_save' === sanitize_key( $_REQUEST['action'] ) ) {
1112 wp_send_json( array(
1113 'error' => 'Error saving: JSON POST data missing!',
1114 'fatal_error' => true
1115 ) );
1116 exit;
1117 }
1118
1119 $ignored_player_fields = array(
1120 'fv_wp_flowplayer_field_subtitles_lang', // subtitles languages is a per-video value with global field name,
1121 // so the player should ignore it, as it will be added via video meta
1122 'fv_wp_flowplayer_field_popup', // never used, never shown in the UI, possibly a remnant of old code,
1123 'fv_wp_flowplayer_field_transcript', // transcript is a meta value, so it should not be stored globally per-player anymore
1124 'fv_wp_flowplayer_field_chapters', // chapters is a meta value, so it should not be stored globally per-player anymore
1125 );
1126
1127 if ($post_data) {
1128
1129 $time_save_start = microtime(true);
1130
1131 // parse and resolve deleted videos
1132 if (!$data && !empty($post_data['deleted_videos'])) { // todo: ajax!
1133 $deleted_videos = explode(',', $post_data['deleted_videos']);
1134 foreach ($deleted_videos as $d_id) {
1135 // we don't need to load this video data, just link it to a database
1136 // and then delete it
1137 // ... although we'll need at least 1 item in the data array to consider this
1138 // video data valid for object creation
1139 $d_vid = new FV_Player_Db_Video(null, array('title' => '1'), $this);
1140 $d_vid->link2db($d_id);
1141 $d_vid->delete();
1142 }
1143 }
1144
1145 // parse and resolve deleted meta data
1146 if (!$data && !empty($post_data['deleted_video_meta'])) { // todo: probably not needed with Ajax saving
1147 $deleted_meta = explode(',', $post_data['deleted_video_meta']);
1148 foreach ($deleted_meta as $d_id) {
1149 // we don't need to load this meta data, just link it to a database
1150 // and then delete it
1151 // ... although we'll need at least 1 item in the data array to consider this
1152 // meta data valid for object creation
1153 $d_meta = new FV_Player_Db_Video_Meta(null, array('meta_key' => '1'), $this);
1154 $d_meta->link2db($d_id);
1155 $d_meta->delete();
1156 }
1157 }
1158
1159 // parse and resolve deleted meta data
1160 if (!$data && !empty($post_data['deleted_player_meta'])) { // todo: probably not needed with Ajax saving
1161 $deleted_meta = explode(',', $post_data['deleted_player_meta']);
1162 foreach ($deleted_meta as $d_id) {
1163 // we don't need to load this meta data, just link it to a database
1164 // and then delete it
1165 // ... although we'll need at least 1 item in the data array to consider this
1166 // meta data valid for object creation
1167 $d_meta = new FV_Player_Db_Player_Meta(null, array('meta_key' => '1'), $this);
1168 $d_meta->link2db($d_id);
1169 $d_meta->delete();
1170 }
1171 }
1172
1173 foreach ($post_data as $field_name => $field_value) {
1174 // global player or local video setting field
1175 if (strpos($field_name, 'fv_wp_flowplayer_field_') !== false) {
1176 if (!in_array($field_name, $ignored_player_fields)) {
1177 $option_name = str_replace( 'fv_wp_flowplayer_field_', '', $field_name );
1178 // global player option
1179 $player_options[ $option_name ] = $field_value;
1180 }
1181 } else if ($field_name == 'videos' && is_array($field_value)) {
1182 // iterate over all videos for the player
1183 foreach ($field_value as $video_index => $video_data) {
1184 // width and height are global options but are sent out for shortcode compatibility
1185 unset($video_data['fv_wp_flowplayer_field_width'], $video_data['fv_wp_flowplayer_field_height']);
1186
1187 // remove global player HLS key option, as it's handled as meta data item
1188 // TODO: create proper API!
1189 unset($video_data['fv_wp_flowplayer_hlskey'], $video_data['fv_wp_flowplayer_hlskey_cryptic'], $video_data['fv_wp_flowplayer_field_encoding_job_id']);
1190
1191 // strip video data of the prefix
1192 $new_video_data = array();
1193 foreach ($video_data as $key => $value) {
1194 if ($key === 'id') {
1195 $id = $value;
1196 } else {
1197 $new_video_data[ str_replace( 'fv_wp_flowplayer_field_', '', $key ) ] = $value;
1198 }
1199 }
1200 $video_data = $new_video_data;
1201 unset($new_video_data);
1202
1203 // add any video meta data that we can gather
1204 $video_meta = array();
1205
1206 if (!empty($post_data['video_meta']['video'][$video_index])) {
1207 foreach ($post_data['video_meta']['video'][$video_index] as $video_meta_section => $video_meta_array) {
1208 $meta_data_to_add = array(
1209 'meta_key' => $video_meta_section,
1210 'meta_value' => $video_meta_array['value']
1211 );
1212
1213 if (isset($video_meta_array['id'])) {
1214 $meta_data_to_add['id'] = (int) $video_meta_array['id'];
1215 }
1216
1217 $video_meta[] = $meta_data_to_add;
1218 }
1219 }
1220
1221 // add chapters and transcript
1222 foreach( array(
1223 'chapters',
1224 'transcript'
1225 ) AS $meta_type ) {
1226 if (!empty($post_data['video_meta'][$meta_type][$video_index]['file']['value'])) {
1227 $file = $post_data['video_meta'][$meta_type][$video_index]['file'];
1228 $new_meta = array(
1229 'meta_key' => $meta_type,
1230 'meta_value' => $file['value']
1231 );
1232
1233 if (!empty($file['id'])) {
1234 $new_meta['id'] = $file['id'];
1235 }
1236
1237 $video_meta[] = $new_meta;
1238 }
1239 }
1240
1241 // Video meta with languages
1242 // TODO: How to do this automatically for all the fields registered in editor with language => true?
1243 foreach( array(
1244 'subtitles',
1245 'transcript_src'
1246 ) AS $meta_type ) {
1247 foreach ( $post_data['video_meta'][ $meta_type ][$video_index] as $transcript ) {
1248 if ($transcript['file']) {
1249 $m = array(
1250 'meta_key' => $meta_type . ($transcript['code'] ? '_'.$transcript['code'] : ''),
1251 'meta_value' => $transcript['file']
1252 );
1253
1254 // add ID, if present
1255 if (!empty($transcript['id'])) {
1256 $m['id'] = $transcript['id'];
1257 }
1258
1259 $video_meta[] = $m;
1260 }
1261 }
1262 }
1263
1264 // call a filter which is server by plugins to augment
1265 // the $video_meta data with all the plugin data for this
1266 // particular video
1267 if (!empty($post_data['video_meta'])) {
1268 $video_meta = apply_filters( 'fv_player_db_video_meta_save', $video_meta, $post_data['video_meta'], $video_index);
1269 }
1270
1271 // save the video
1272 $video = new FV_Player_Db_Video(null, $video_data, $this);
1273
1274 // if we have video ID, link this video to DB
1275 if (isset($id)) {
1276 $video->link2db($id);
1277 unset($id);
1278 }
1279
1280 // Skip video meta check if the save is taking more than 10 seconds.
1281 // We could also rely on the PHP max_execution_time/2 or so.
1282 $skip_video_meta_check = ( microtime(true) - $time_save_start ) > 10;
1283
1284 // save only if we're not requesting new instances for preview purposes
1285 if (!$data) {
1286 $id_video = $video->save( $video_meta, false, $skip_video_meta_check );
1287 if( !$id_video ) {
1288 global $wpdb;
1289 wp_send_json( array( 'fatal_error' => true, 'error' => 'Failed to save the video: '.$wpdb->last_error ) );
1290 exit;
1291 }
1292 } else {
1293 $video->link2meta( $video_meta );
1294 }
1295
1296 // return videos as well as the full player
1297 if (!$data) {
1298 $video_ids[] = $id_video;
1299 } else {
1300 $video_ids[] = $video;
1301 }
1302 }
1303 }
1304 }
1305
1306 // add all videos into this player
1307 if (!$data) {
1308 $player_options['videos'] = implode( ',', $video_ids );
1309 }
1310
1311 // add any player meta data that we can gather
1312 $player_meta = array();
1313
1314 if (!empty($post_data['player_meta']['player'])) {
1315 foreach ($post_data['player_meta']['player'] as $player_meta_section => $player_meta_array) {
1316 $meta_data_to_add = array(
1317 'meta_key' => $player_meta_section,
1318 'meta_value' => $player_meta_array['value']
1319 );
1320
1321 if (isset($player_meta_array['id'])) {
1322 $meta_data_to_add['id'] = (int) $player_meta_array['id'];
1323 }
1324
1325 $player_meta[] = $meta_data_to_add;
1326 }
1327 }
1328
1329 // call a filter which is served by plugins to augment
1330 // the $player_meta data with all the plugin data for this
1331 // particular player
1332 if (!empty($post_data['player_meta'])) {
1333 $player_meta = apply_filters( 'fv_player_db_player_meta_save', $player_meta, $post_data['player_meta'], $post_data );
1334 }
1335
1336 // create and save the player
1337 $player = new FV_Player_Db_Player(null, $player_options, $FV_Player_Db);
1338
1339 // if this player should have a "published" status, add it here
1340 if ( !empty( $post_data['status'] ) && $post_data['status'] == 'published' ) {
1341 $player->setStatus('published');
1342 }
1343
1344 // Save only if we're not requesting new instances for preview purposes
1345 if (!$data) {
1346 // link to DB, if we're doing an update
1347 if (!empty($post_data['update'])) {
1348 $player->link2db($post_data['update']);
1349 }
1350
1351 $id = $player->save($player_meta);
1352
1353 if ($id) {
1354 do_action('fv_player_db_save', $id);
1355
1356 // Process Video Custom Fields
1357 if ( !empty($post_data['current_post_id']) ) {
1358 $post = get_post( $post_data['current_post_id'] );
1359
1360 // Verify if the FV Player Video Custom Field for such meta_key exists
1361 if( $post && !empty( FV_Player_Custom_Videos_Master()->aMetaBoxes[ $post->post_type ][ $post_data['current_post_meta_key'] ] ) ) {
1362 update_post_meta( $post_data['current_post_id'], $post_data['current_post_meta_key'], '[fvplayer id="' . $id. '"]' );
1363
1364 $this->store_post_ids( $post->ID );
1365 }
1366 }
1367
1368 $current_video_to_edit = isset($post_data['current_video_to_edit']) ? $post_data['current_video_to_edit'] : -1;
1369
1370 $current_editor_tab = ! empty( $post_data['current_editor_tab'] ) ? $post_data['current_editor_tab'] : false;
1371
1372 wp_send_json( $this->db_load_player_data( $id, $current_video_to_edit, $current_editor_tab ) );
1373
1374 } else {
1375 global $wpdb;
1376 wp_send_json( array( 'fatal_error' => true, 'error' => 'Failed to save player: '.$wpdb->last_error ) );
1377 }
1378 } else {
1379 // Used for player preview
1380 $player->link2meta( $player_meta );
1381 return array(
1382 'player' => $player,
1383 'videos' => $video_ids
1384 );
1385 }
1386 }
1387
1388 if (!$data) {
1389 die();
1390 }
1391 }
1392
1393
1394
1395 /**
1396 * AJAX method to return database data for the player ID given
1397 */
1398 public function open_player_for_editing() {
1399 global $fv_fp;
1400
1401 if ( isset( $_POST['playerID'] ) && is_numeric( $_POST['playerID'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-load" ) ) {
1402
1403 $load_player_id = absint( $_POST['playerID'] );
1404
1405 // load player and its videos from DB
1406 if ( !$this->getPlayerAttsFromDb( array( 'id' => $load_player_id ) ) ) {
1407 header("HTTP/1.0 404 Not Found");
1408 die();
1409 }
1410
1411 $userID = get_current_user_id();
1412 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
1413
1414 if( $cannot_edit_other_posts && $fv_fp->current_player() ) {
1415 $author = $fv_fp->current_player()->getAuthor();
1416 if( $userID !== $author ) {
1417 wp_send_json( array( 'error' => 'You don\'t have permission to edit this player.' ) );
1418 die();
1419 }
1420 }
1421
1422 // check player's meta data for an edit lock
1423 if ($fv_fp->current_player() && count($fv_fp->current_player()->getMetaData())) {
1424 $edit_lock_found = false;
1425 foreach ($fv_fp->current_player()->getMetaData() as $meta_object) {
1426 $key = $meta_object->getMetaKey();
1427 $user_locked = str_replace('edit_lock_', '', $key);
1428 if ( strstr($key, 'edit_lock_') !== false ) {
1429 $edit_lock_found = true;
1430
1431 if ( $user_locked != $userID) {
1432 // someone else is editing this video, first check the timestamp
1433 $last_tick = $meta_object->getMetaValue();
1434 if (time() - $last_tick > $this->edit_lock_timeout_seconds) {
1435 // timeout, remove lock, add lock for this user
1436 $meta_object->delete();
1437
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 } else {
1446 $user = get_userdata($user_locked);
1447 $name = 'Somebody else';
1448 if( $user ) {
1449 if( !empty($user->display_name) ) $name = $user->display_name;
1450 if( !empty($user->user_nicename) ) $name = $user->user_nicename;
1451 }
1452 wp_send_json( array( 'error' => $name." is editing this player at the moment. Please try again later." ) );
1453 die();
1454 }
1455 } else {
1456 // same user, extend the lock
1457 $meta_object->setMetaValue(time());
1458 $meta_object->save();
1459 }
1460 }
1461 }
1462
1463 // no edit lock meta record - create new one
1464 if (!$edit_lock_found) {
1465 $meta = new FV_Player_Db_Player_Meta( null, array(
1466 'id_player' => $fv_fp->current_player()->getId(),
1467 'meta_key' => 'edit_lock_' . $userID,
1468 'meta_value' => time()
1469 ), $this );
1470
1471 $meta->save();
1472 }
1473 } else {
1474 // add player edit lock if none was found
1475 if ($fv_fp->current_player()) {
1476 $meta = new FV_Player_Db_Player_Meta( null, array(
1477 'id_player' => $fv_fp->current_player()->getId(),
1478 'meta_key' => 'edit_lock_' . $userID,
1479 'meta_value' => time()
1480 ), $this );
1481
1482 $meta->save();
1483 }
1484 }
1485
1486 // intval() below is important as -1 means no particular video is being edited
1487 $out = $this->db_load_player_data( $load_player_id, intval( $_POST['current_video_to_edit'] ) );
1488
1489 if( empty($out['videos']) ) {
1490 wp_send_json( array( 'error' => "Failed to load videos for this player." ) );
1491 exit;
1492 }
1493
1494 /**
1495 * Detect bug with duplicate players
1496 */
1497 if ( $fv_fp->current_player() ) {
1498 $video_ids = explode( ',', $fv_fp->current_player()->getVideoIds() );
1499
1500 $db_options = array(
1501 'select_fields' => 'player_name, date_created, videos, author, status',
1502 'search_by_video_ids' => $video_ids,
1503 );
1504
1505 $this->query_players( $db_options );
1506
1507 $players = $this->getPlayersCache();
1508
1509 if ( $players && count($players) ) {
1510 $out['debug_duplicate_players'] = array();
1511
1512 foreach ($players as $player) {
1513 if ( $player->getId() != $load_player_id ) {
1514 $out['debug_duplicate_players'][] = $player->getId();
1515 }
1516 }
1517 }
1518 }
1519
1520 /**
1521 * Output JSON
1522 */
1523 header('Content-Type: application/json');
1524 if (version_compare(phpversion(), '5.3', '<')) {
1525 echo wp_json_encode($out);
1526 } else {
1527 echo wp_json_encode($out, true);
1528 }
1529 die();
1530
1531 } else {
1532 wp_send_json( array( 'error' => 'Security check failed.' ) );
1533 die();
1534 }
1535 }
1536
1537 /**
1538 * Search for players, set internal cache or return
1539 * count if $args['count'] is true
1540 *
1541 * @param array $args
1542 *
1543 * @return void|int
1544 */
1545 public function query_players( $args ) {
1546 global $wpdb;
1547
1548 $args = wp_parse_args( $args, array(
1549 'author_id' => false,
1550 'ids' => false, // should not be used together with count
1551 'offset' => false,
1552 'order' => false,
1553 'order_by' => false,
1554 'per_page' => false,
1555 'post_type' => false,
1556 'search_by_video_ids' => false,
1557 'search_string' => false,
1558 'select_fields' => false,
1559 'count' => false
1560 ) );
1561
1562 $ids = array();
1563 if( is_array($args['ids']) ) {
1564 $ids = $args['ids'];
1565 } else if( $args['ids'] ) {
1566 $ids = explode( ',', $args['ids'] );
1567 }
1568
1569 $query_ids = array();
1570 foreach ( $ids as $id_key => $id_value ) {
1571 // check if this player is not cached yet
1572 if (!$this->isPlayerCached($id_value)) {
1573 $query_ids[ $id_key ] = (int) $id_value;
1574 }
1575 }
1576
1577 // Are we querying players by IDs, but is it all already cached?
1578 if( count($ids) > 0 && count($query_ids) == 0 ) {
1579 return;
1580 }
1581
1582 // load multiple players via their IDs but a single query and return their values
1583 $select = 'p.*';
1584 if( !empty($args['select_fields']) ) {
1585 $select = 'p.id,'.esc_sql($args['select_fields']);
1586 }
1587
1588 if($args['count']) {
1589 $select = 'count(*) as row_count';
1590 }
1591
1592 $where = ' WHERE 1=1 ';
1593 if( count($query_ids) ) {
1594 $where .= ' AND p.id IN('. implode(',', $query_ids).') ';
1595
1596 // if we have multiple video IDs to load players for, let's prepare a like statement here
1597 } else if( is_array($args['search_by_video_ids']) ) {
1598 $where_like_part = array();
1599
1600 if ( !empty( $args['search_string'] ) ) {
1601 // TODO: Escape in some better way
1602 $where_like_part[] = 'player_name LIKE "%' . esc_sql( $args['search_string'] ) . '%"';
1603 }
1604
1605 foreach ($args['search_by_video_ids'] as $player_video_id) {
1606 $where_like_part[] = "FIND_IN_SET( " . intval( $player_video_id ) .", videos ) > 0";
1607 }
1608
1609 $where .= ' AND (' . implode(' OR ', $where_like_part) . ') ';
1610 }
1611
1612 if( !empty( $args['author_id']) ) {
1613 $where .= ' AND author ='.intval($args['author_id']).' ';
1614 }
1615
1616 $order = '';
1617 if( !empty($args['order_by']) ) {
1618
1619 // Verify that each order by is valid
1620 $order_by_items = explode( ',', $args['order_by'] );
1621 $order_by_items = array_map( 'trim', $order_by_items );
1622
1623 foreach( $order_by_items AS $k => $v ) {
1624 if( !in_array($v, $this->valid_order_by ) ) {
1625 unset($order_by_items[$k]);
1626 }
1627 }
1628
1629 if( count($order_by_items) > 0 ) {
1630 $order = ' ORDER BY '.implode( ', ', array_map( 'esc_sql', $order_by_items ) );
1631 if( !empty($args['order']) ) {
1632 if( in_array($args['order'], array( 'asc', 'desc' ) ) ) {
1633 $order .= ' '.esc_sql($args['order']);
1634 }
1635 }
1636 }
1637 }
1638
1639 $limit = '';
1640 if( $args['offset'] !== false && $args['per_page'] !== false ) {
1641 $limit = $wpdb->prepare( ' LIMIT %d, %d', $args['offset'], $args['per_page'] );
1642 }
1643
1644 $post_type_join = '';
1645 $tax_join = '';
1646 if( $args['post_type'] ) {
1647
1648 // Get players which are not embedded in any post = no post_id playermeta
1649 if ( 'none' === $args['post_type'] ) {
1650 $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" ';
1651
1652 $where .= ' AND pm.id IS NULL';
1653
1654 } else {
1655 $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 ';
1656
1657 $where .= $wpdb->prepare( ' AND pm.meta_key = "post_id" AND posts.post_type = %s', $args['post_type'] );
1658 }
1659
1660 // Is there any known taxonomy in $args ?
1661 $post_type_taxonomies = fv_player_get_post_type_taxonomies( $args['post_type'] );
1662
1663 foreach( $post_type_taxonomies AS $tax) {
1664 if ( !empty( $args[ 'tax_' . $tax ] ) ) {
1665 $tax_join = "
1666 INNER JOIN {$wpdb->term_relationships} AS tr ON posts.ID = tr.object_id
1667 INNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
1668 INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id";
1669
1670 $where .= ' AND t.slug = "' . esc_sql( $args[ 'tax_' . $tax ] ) . '"';
1671 $where .= ' AND tt.taxonomy = "' . esc_sql( $tax ) . '"';
1672 }
1673 }
1674 }
1675
1676 if($args['count']) {
1677 $group_order = '';
1678 } else {
1679 $group_order = ' GROUP BY p.id'.$order.$limit;
1680 }
1681
1682 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1683 $player_data = $wpdb->get_results( "SELECT {$select} FROM `{$wpdb->prefix}fv_player_players` AS p {$post_type_join} {$tax_join} {$where} {$group_order}" );
1684
1685 if($args['count']) {
1686 return intval($player_data[0]->row_count);
1687 }
1688
1689 /**
1690 * Also load count of subtitles, cues, chapters and transcripts
1691 *
1692 * If we do this is the original query with JOIN it takes 10x longer
1693 */
1694 if( is_admin() && count( $player_data ) > 0 ) {
1695 $placeholders = implode( ', ', array_fill( 0, count( $player_data ), '%d' ) );
1696
1697 $meta_counts = $wpdb->get_results(
1698 $wpdb->prepare(
1699 // $placeholders is a string of %d created above
1700 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1701 "SELECT p.id,
1702 count(subtitles.id) as subtitles_count,
1703 count(cues.id) as cues_count,
1704 count(chapters.id) as chapters_count,
1705 count(meta_transcript.id) as transcript_count
1706 FROM `{$wpdb->prefix}fv_player_players` AS p
1707 JOIN `{$wpdb->prefix}fv_player_videos` AS v on FIND_IN_SET(v.id, p.videos)
1708 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS subtitles ON v.id = subtitles.id_video AND subtitles.meta_key like 'subtitles%'
1709 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS cues ON v.id = cues.id_video AND cues.meta_key like 'cues%'
1710 LEFT JOIN `{$wpdb->prefix}fv_player_videometa` AS chapters ON v.id = chapters.id_video AND chapters.meta_key = 'chapters'
1711 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%'
1712 WHERE p.id IN( $placeholders )
1713 GROUP BY p.id",
1714 wp_list_pluck( $player_data, 'id' )
1715 ),
1716 OBJECT_K
1717 );
1718
1719 foreach( $player_data as $k => $v ) {
1720 if ( ! empty( $meta_counts[ $v->id ] ) ) {
1721 $meta_count = $meta_counts[ $v->id ];
1722 $player_data[ $k ]->subtitles_count = $meta_count->subtitles_count;
1723 $player_data[ $k ]->cues_count = $meta_count->cues_count;
1724 $player_data[ $k ]->chapters_count = $meta_count->chapters_count;
1725 $player_data[ $k ]->transcript_count = $meta_count->transcript_count;
1726 }
1727 }
1728 }
1729
1730 $cache = array();
1731
1732 foreach( $player_data AS $db_record ) {
1733 // create a new video object and populate it with DB values
1734 $record_id = $db_record->id;
1735 // if we don't unset this, we'll get warnings
1736 unset($db_record->id);
1737
1738 $player_object = new FV_Player_Db_Player( null, get_object_vars( $db_record ), $this );
1739 $player_object->link2db( $record_id );
1740
1741 // cache this player in DB object
1742 $cache[$record_id] = $player_object;
1743 }
1744
1745 if ( ! empty( $cache ) ) {
1746 $this->setPlayersCache($cache);
1747 }
1748 }
1749
1750 /**
1751 * Receive Heartbeat data and checks for DB edit lock.
1752 * In case the lock is found and valid, it will be extended.
1753 *
1754 * @param array $response Heartbeat response data to pass back to front end.
1755 * @param array $data Data received from the front end (unslashed).
1756 *
1757 * @return array Returns the same response as received, as we don't need to update it or read it anywhere in JS.
1758 * @throws Exception When the underlying meta object throws an exception.
1759 */
1760 function check_db_edit_lock( $response, $data ) {
1761 global $FV_Player_Db;
1762
1763 $userID = get_current_user_id();
1764
1765 // extend an existing lock
1766 if ( !empty( $data['fv_flowplayer_edit_lock_id'] ) ) {
1767 $player_id = $data['fv_flowplayer_edit_lock_id'];
1768
1769 if ($FV_Player_Db && $FV_Player_Db->isPlayerCached($player_id)) {
1770 $player = $FV_Player_Db->getPlayersCache();
1771 $player = $player[$player_id];
1772 } else {
1773 $player = new FV_Player_Db_Player($player_id, array(), $FV_Player_Db);
1774 }
1775
1776 if ($player->getIsValid()) {
1777 $found = false;
1778 if (count($player->getMetaData())) {
1779 foreach ($player->getMetaData() as $meta_object) {
1780 if ( strstr($meta_object->getMetaKey(), 'edit_lock_') !== false ) {
1781 if (str_replace('edit_lock_', '', $meta_object->getMetaKey()) == $userID) {
1782 $found = true;
1783
1784 // same user, extend the lock
1785 $meta_object->setMetaValue(time());
1786 $meta_object->save();
1787 }
1788 }
1789 }
1790 }
1791
1792 if( !$found ) {
1793 $meta_object = new FV_Player_Db_Player_Meta(null, array(
1794 'id_player' => $player_id,
1795 'meta_key' => 'edit_lock_'.$userID,
1796 'meta_value' => time()
1797 ), $FV_Player_Db);
1798 $meta_object->save();
1799 }
1800 }
1801 }
1802
1803 // remove locks that are no longer being edited
1804 if ( !empty( $data['fv_flowplayer_edit_lock_removal'] ) && count($data['fv_flowplayer_edit_lock_removal']) ) {
1805 // load meta for all players to remove locks for (and to auto-cache them as well)
1806 new FV_Player_Db_Player_Meta(null, array('id_player' => array_keys($data['fv_flowplayer_edit_lock_removal'])), $this);
1807 $meta = $this->getPlayerMetaCache();
1808 $locks_removed = array();
1809
1810 if (count($meta)) {
1811 foreach ( $meta as $player ) {
1812 foreach ($player as $meta_object) {
1813 if ( strstr( $meta_object->getMetaKey(), 'edit_lock_' ) !== false ) {
1814 if ( str_replace( 'edit_lock_', '', $meta_object->getMetaKey() ) == $userID ) {
1815 // correct user, delete the lock
1816 $meta_object->delete();
1817 }
1818
1819 $locks_removed[$meta_object->getIdPlayer()] = 1;
1820 }
1821 }
1822 }
1823
1824 $response['fv_flowplayer_edit_locks_removed'] = $locks_removed;
1825 }
1826 }
1827
1828 return $response;
1829 }
1830
1831 /**
1832 * AJAX function to return JSON-formatted export data
1833 * for a specific player ID.
1834 *
1835 * Works for single player only right now!
1836 *
1837 * @param null $unused Populated by WordPress, not used in this method.
1838 * @param bool $output_result If true, the export data will be returned instead of outputted.
1839 * Used when cloning a player.
1840 *
1841 * @return array Returns the actual export data in an associative array, if $output_result is false.
1842 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
1843 */
1844 public function export_player_data($unused = null, $output_result = true, $id = false ) {
1845
1846 if ( !$id ) {
1847 // The player ID is part of the nonce below, so we need to get it from the POST data
1848 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1849 $id = !empty( $_POST['playerID'] ) ? absint( $_POST['playerID'] ) : false;
1850 }
1851
1852 if( defined('DOING_AJAX') && DOING_AJAX &&
1853 ( empty($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-export-".$id ) )
1854 ) {
1855 die('Security check failed');
1856 }
1857
1858 if ( $id ) {
1859 // first, load the player
1860 $player = new FV_Player_Db_Player($id, array(), $this);
1861 if ($player && $player->getIsValid()) {
1862 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
1863 $author_id = get_current_user_id();
1864
1865 if( $cannot_edit_other_posts ) {
1866 if( $author_id !== $player->getAuthor() ) {
1867 die('You don\'t have permission to export this player.');
1868 }
1869 }
1870
1871 $export_data = $player->export();
1872
1873 // load player meta data
1874 $meta = $player->getMetaData();
1875 if ($meta && count($meta)) {
1876 $export_data['meta'] = array();
1877
1878 foreach ($meta as $meta_data) {
1879 // don't include edit locks
1880 if ( strstr($meta_data->getMetaKey(), 'edit_lock_') === false ) {
1881 $export_data['meta'][] = $meta_data->export();
1882 }
1883 }
1884 }
1885
1886 // load videos and meta for this player
1887 $videos = $player->getVideos();
1888
1889 // this line will load and cache meta for all videos at once
1890 new FV_Player_Db_Video_Meta(null, array('id_video' => explode(',', $player->getVideoIds())), $this);
1891
1892 if ($videos && count($videos)) {
1893 $export_data['videos'] = array();
1894
1895 foreach ($videos as $video) {
1896 $video_export_data = $video->export();
1897
1898 // load all meta data for this video
1899 if ($this->isVideoMetaCached($video->getId())) {
1900 $video_export_data['meta'] = array();
1901
1902 foreach ($this->video_meta_cache[$video->getId()] as $meta) {
1903 $video_export_data['meta'][] = $meta->export();
1904 }
1905 }
1906
1907 $export_data['videos'][] = $video_export_data;
1908 }
1909 }
1910 } else {
1911 if ($output_result) {
1912 die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
1913 } else {
1914 return false;
1915 }
1916 }
1917
1918 if ($output_result) {
1919 if (version_compare(phpversion(), '5.3', '<')) {
1920 echo wp_json_encode($export_data);
1921 } else {
1922 echo wp_json_encode($export_data, true);
1923 }
1924 exit;
1925 } else {
1926 return $export_data;
1927 }
1928 } else {
1929 if ($output_result) {
1930 die( 'invalid player ID, export unsuccessful - please use the close button and try again' );
1931 } else {
1932 return false;
1933 }
1934 }
1935 }
1936
1937 /**
1938 * AJAX function to import JSON-formatted export data.
1939 *
1940 * Works for single player only right now!
1941 *
1942 * @param null $unused Populated by WordPress, not used in this method.
1943 * @param bool $output_result If true, the import result will be returned instead of outputted.
1944 * Used when cloning a player.
1945 * @param array|null $alternative_data If set, this is an alternative source of data to import.
1946 * Used when cloning a player.
1947 *
1948 * @return string Returns the actual player ID, if $output_result is false.
1949 *
1950 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
1951 */
1952 public function import_player_data($unused = null, $output_result = true, $alternative_data = null) {
1953 global $FV_Player_Db;
1954
1955 if ( $alternative_data !== null ) {
1956 $data = $alternative_data;
1957
1958 } else if( isset( $_POST['data'] ) ) {
1959 if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), "fv-player-db-import" ) ) {
1960 die( 'Security check failed.' );
1961 }
1962
1963 if ( ! current_user_can( 'edit_others_posts' ) ) {
1964 die('You don\'t have permission to import players.');
1965 }
1966
1967 // TODO: How to better sanitize this?
1968 $data = json_decode( stripslashes( $_POST['data'] ), true );
1969 }
1970
1971 if ( $data ) {
1972 try {
1973
1974 $time_import_start = microtime(true);
1975
1976 // first, create the player
1977 $player_keys = $data;
1978 unset($player_keys['meta'], $player_keys['videos']);
1979
1980 foreach( $player_keys AS $k => $v ) {
1981 if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
1982 $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
1983 $player_keys[$new] = $v;
1984 unset($player_keys[$k]);
1985 }
1986 }
1987
1988 $player = new FV_Player_Db_Player(null, $player_keys, $FV_Player_Db);
1989 $player_video_ids = array();
1990
1991 // create player videos, along with meta data
1992 // ... don't save the player yet, as we need all video IDs to be known
1993 // before doing so
1994 if (isset($data['videos'])) {
1995 foreach ($data['videos'] as $video_data) {
1996 // replace caption for title, remove caption
1997 if( isset($video_data['caption']) && !empty($video_data['caption']) && ( !isset($video_data['title']) || empty($video_data['title']) ) ) {
1998 $video_data['title'] = $video_data['caption'];
1999 unset($video_data['caption']);
2000 }
2001
2002 foreach( $video_data AS $k => $v ) {
2003 if( stripos($k,'fv_wp_flowplayer_field_') === 0 ) {
2004 $new = str_replace( 'fv_wp_flowplayer_field_', '', $k );
2005 $video_data[$new] = $v;
2006 unset($video_data[$k]);
2007 }
2008 }
2009
2010 // check meta first before importing and migrate to new format
2011 if (isset($video_data['meta'])) {
2012 foreach ($video_data['meta'] as $k => $video_meta_data) {
2013
2014 // Note: Video duration is checked during the import anyway, but we keep the conversion routine and it might come handy in the future
2015 if( $video_meta_data['meta_key'] == 'duration') { // duration is now in video data
2016 if( !isset( $video_data['duration']) ) {
2017 $video_data['duration'] = $video_meta_data['meta_value'];
2018 }
2019
2020 unset($video_data['meta'][$k]);
2021 }
2022
2023 // Note: Video live flag is checked during the import anyway, but we keep the conversion routine and it might come handy in the future
2024 if( $video_meta_data['meta_key'] == 'live') { // live is now in video data
2025 if( !isset( $video_data['live']) ) {
2026 $video_data['live'] = $video_meta_data['meta_value'];
2027 }
2028
2029 unset($video_data['meta'][$k]);
2030 }
2031
2032 if( $video_meta_data['meta_key'] == 'transcript' ) { // rename transcript to transcript_src
2033 $new_exists = false;
2034 foreach( $video_data['meta'] as $m2) {
2035 if( $m2['meta_key'] == 'transcript_src' ) {
2036 $new_exists = true;
2037 break;
2038 }
2039 }
2040
2041 if(!$new_exists) {
2042 $video_data['meta'][] = array(
2043 'meta_key' => 'transcript_src',
2044 'meta_value' => $video_meta_data['meta_value']
2045 );
2046 }
2047
2048 unset($video_data['meta'][$k]);
2049 }
2050 }
2051 } else {
2052 $video_data['meta'] = array();
2053 }
2054
2055 // Skip video meta check if the import is taking more than 10 seconds.
2056 // We could also rely on the PHP max_execution_time/2 or so.
2057 $skip_video_meta_check = ( microtime(true) - $time_import_start ) > 10;
2058
2059 $video_object = new FV_Player_Db_Video(null, $video_data, $FV_Player_Db);
2060 $id_video = $video_object->save( $video_data['meta'], false, $skip_video_meta_check );
2061
2062 $player_video_ids[] = $id_video;
2063 }
2064 }
2065
2066 // set video IDs for the player
2067 $player->setVideos(implode(',', $player_video_ids));
2068
2069 // save player
2070 $id_player = $player->save(
2071 isset($data['meta']) ? $data['meta'] : array(),
2072 true
2073 );
2074
2075 } catch (Exception $e) {
2076 if ($output_result) {
2077 die( $e );
2078 } else {
2079 return $e;
2080 }
2081 }
2082
2083 if ($output_result) {
2084 die( (string) $id_player );
2085 } else {
2086 return (string) $id_player;
2087 }
2088 } else {
2089 if ($output_result) {
2090 die('No valid import data found, import unsuccessful');
2091 } else {
2092 return 'No valid import data found, import unsuccessful';
2093 }
2094 }
2095 }
2096
2097 public static function has_table_column( $table, $column ) {
2098 global $wpdb;
2099 return $wpdb->get_results(
2100 $wpdb->prepare(
2101 "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
2102 DB_NAME,
2103 $table,
2104 $column
2105 )
2106 );
2107 }
2108
2109 /**
2110 * AJAX function to remove a player from database.
2111 *
2112 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
2113 */
2114 public function remove_player() {
2115 if (isset($_POST['playerID']) && is_numeric($_POST['playerID']) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ),"fv-player-db-remove-".$_POST['playerID'] ) ) {
2116
2117 // first, load the player
2118 $player = new FV_Player_Db_Player( absint( $_POST['playerID'] ), array(), $this);
2119 if ($player && $player->getIsValid()) {
2120 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
2121 $author_id = get_current_user_id();
2122
2123 // check if user can delete player
2124 if( $cannot_edit_other_posts ) {
2125 if( $author_id !== $player->getAuthor() ) {
2126 die('You don\'t have permission to delete this player.');
2127 }
2128 }
2129
2130 // remove the player
2131 if ($player->delete()) {
2132 echo 1;
2133 exit;
2134 } else {
2135 die( 'Could not remove player' );
2136 }
2137 } else {
2138 die( 'Invalid player ID' );
2139 }
2140 } else {
2141 die( 'Invalid player ID' );
2142 }
2143 }
2144
2145 /**
2146 * AJAX function to clone a player in the database.
2147 *
2148 * Works for single player only right now!
2149 *
2150 * @throws Exception Thrown if one of the underlying DB classes throws an exception.
2151 */
2152 public function clone_player() {
2153 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'] ) ) ) {
2154 $cannot_edit_other_posts = !current_user_can('edit_others_posts');
2155 $author_id = get_current_user_id();
2156
2157 $player = new FV_Player_Db_Player( intval($_POST['playerID']), array(), $this );
2158
2159 if( $cannot_edit_other_posts ) {
2160 if( $author_id !== $player->getAuthor() ) {
2161 die('You don\'t have permission to clone this player.');
2162 }
2163 }
2164
2165 $export_data = $this->export_player_data(null, false);
2166
2167 // do not clone information about where the player is embeded
2168 if (isset($export_data['meta'])) {
2169 foreach($export_data['meta'] as $h => $v){
2170 if($v['meta_key'] == 'post_id'){
2171 unset($export_data['meta'][$h]);
2172 }
2173 }
2174 }
2175
2176 echo esc_html( $this->import_player_data(null, false, $export_data) );
2177 exit;
2178 } else {
2179 die('no valid player ID found, cloning unsuccessful');
2180 }
2181 }
2182
2183 /**
2184 * AJAX method to retrieve IDs and names of all players to be populated
2185 * into a dropdown in the front-end.
2186 */
2187 public function retrieve_all_players_for_dropdown() {
2188 if( !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'fv-player-editor-search-nonce' ) ) {
2189 wp_send_json_error( 'Nonce verification failed! Please reload the page.' );
2190 }
2191
2192 $search = !empty( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : false;
2193
2194 $players = $this->getListPageData( array(
2195 'order' => 'desc',
2196 'order_by' => 'date_created',
2197 'search' => $search
2198 ) );
2199
2200 $json_data = array(
2201 'success' => true,
2202 'players' => array()
2203 );
2204
2205 foreach ($players as $player) {
2206 $json_data['players'][] = array(
2207 'id' => $player->id,
2208 'player_name' => $player->player_name,
2209 'video_titles' => $player->video_titles,
2210 'thumbs' => $player->thumbs,
2211 'date_created' => gmdate( get_option( 'date_format' ), strtotime( $player->date_created ) ),
2212 'embeds' => $player->embeds,
2213 );
2214 }
2215
2216 wp_send_json( $json_data );
2217 }
2218
2219 /**
2220 * 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.
2221 *
2222 * @param int $post_id Populated by WordPress, the post ID
2223 */
2224 public function store_post_ids( $post_id ) {
2225 global $wpdb;
2226
2227 if ( wp_is_post_revision( $post_id ) ) return;
2228
2229 $post = get_post($post_id);
2230
2231 $matches = array();
2232 if( preg_match_all('~\[fvplayer.*?id=[\'"]([0-9,]+)[\'"].*?\]~', $post->post_content, $matches1 ) ) {
2233 $matches = array_merge( $matches, $matches1[1] );
2234 }
2235
2236 // The [fvplayer] shortcode might be stored in plain form, or with the quotes escaped like fvplayer id=\"56\"]
2237 if( preg_match_all('~\[fvplayer.*?id=\\\?[\'"]([0-9,]+)~', implode( array_map( 'implode', get_post_custom($post_id) ) ), $matches2 ) ) {
2238 $matches = array_merge( $matches, $matches2[1] );
2239 }
2240
2241 $ids = array();
2242
2243 if( $matches ) {
2244 foreach( $matches AS $match ) {
2245 foreach( explode(',',$match) AS $match_match ) {
2246 $ids[] = $match_match;
2247 }
2248 }
2249
2250 $ids = array_unique($ids);
2251 foreach( $ids AS $player_id ) {
2252
2253 $player = new FV_Player_Db_Player($player_id);
2254 if( $player->getIsValid() ) {
2255
2256 $add = true;
2257 // TODO: This seems to not work when saving with Elementor, it seems store_post_ids() runs 3 times
2258 // but it's never aware of the player meta added using FV_Player_Db_Player_Meta in the previous run
2259 $metas = $player->getMetaData();
2260 if( count($metas) ) {
2261 foreach( $metas as $meta_object ) {
2262 if( $meta_object->getMetaKey() == 'post_id' ) {
2263 if( $meta_object->getMetaValue() == $post_id ) {
2264 $add = false;
2265 }
2266 }
2267 }
2268 }
2269
2270 // TODO: So here's the temporary work-around which should be removed once FV_Player_Db_Player_Meta()
2271 // does properly register the player meta with getMetaData()
2272 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 ) ) ) {
2273 $add = false;
2274 }
2275
2276 if( $add ) {
2277 $meta = new FV_Player_Db_Player_Meta(null, array(
2278 'id_player' => $player_id,
2279 'meta_key' => 'post_id',
2280 'meta_value' => $post_id
2281 ) );
2282
2283 $meta->save();
2284
2285 // Make sure the player is no longer a Draft is used in a post
2286 $player->setStatus('published');
2287 $player->save();
2288 }
2289 }
2290
2291 }
2292 }
2293
2294 /**
2295 * Check if table exists before looking for FV Player that is associated in the post.
2296 * We do this because we would run into issues with this in WP Integration tests.
2297 * The database tables get created by tests like FV_Player_DBTest::setUp() but somehow
2298 * wptests_fv_player_playermetas is not there
2299 */
2300 $table_name = FV_Player_Db_Player_Meta::init_db_name();
2301
2302 if( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) == $table_name ) {
2303
2304 $remove = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE meta_key = 'post_id' AND meta_value = %s ", $post_id ) );
2305 if( $remove ) {
2306 foreach( $remove AS $removal ) {
2307 if( !in_array($removal->id_player,$ids) ) {
2308 $d_meta = new FV_Player_Db_Player_Meta($removal->id);
2309 $d_meta->link2db( $removal->id );
2310 $d_meta->delete();
2311 }
2312 }
2313 }
2314 }
2315 }
2316
2317 public static function get_player_duration( $id ) {
2318 global $wpdb;
2319 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 ) );
2320 }
2321
2322 /**
2323 * Searches for a player video via custom query.
2324 *
2325 * @param array $args Array with search arguments.
2326 *
2327 * @return array|bool Returns array of FV_Player_Db_Video if any data were loaded, false otherwise.
2328 */
2329 public function query_videos($args) {
2330 global $wpdb;
2331
2332 $args = wp_parse_args( $args,
2333 array(
2334 'fields_to_search' => array(
2335 'src'
2336 ),
2337 'search_string' => '',
2338 'like' => false,
2339 'and_or' => 'OR'
2340 )
2341 );
2342
2343 // assemble where part
2344 $where = array();
2345
2346 /*
2347 * Inspired by core WP WP_Query::parse_search() but adjusted to make it fit our SQL query
2348 */
2349 if ( $args['like'] ) {
2350 $search_terms_count = 1;
2351 $search_terms = '';
2352
2353 $args['search_string'] = stripslashes( $args['search_string'] );
2354
2355 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 ""
2356 $args['search_string'] = substr($args['search_string'], 1, -1);
2357 $search_terms = array( $args['search_string'] );
2358 } else {
2359 if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $args['search_string'], $matches ) ) {
2360 $search_terms_count = count( $matches[0] );
2361 $search_terms = self::parse_search_terms( $matches[0] );
2362 // If the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
2363 if ( empty( $search_terms ) || count( $search_terms ) > 9 ) {
2364 $search_terms = array( $args['search_string'] );
2365 }
2366 } else {
2367 $search_terms = array( $args['search_string'] );
2368 }
2369 }
2370
2371 $search_terms_encoded = array();
2372
2373 foreach( $search_terms as $term ) {
2374 $search_terms_encoded[] = $term;
2375 $search_terms_encoded[] = urlencode($term);
2376 $search_terms_encoded[] = rawurlencode($term);
2377 }
2378
2379 $search_terms = array_unique( $search_terms_encoded );
2380
2381 $search_terms = array_unique( $search_terms );
2382
2383 unset($search_terms_encoded);
2384
2385 $exclusion_prefix = apply_filters( 'wp_query_search_exclusion_prefix', '-' );
2386
2387 foreach ($args['fields_to_search'] as $field_name) {
2388 $field_name = sanitize_key($field_name);
2389 $searchlike = '';
2390 $first = true;
2391 foreach ( $search_terms as $term ) {
2392 // If there is an $exclusion_prefix, terms prefixed with it should be excluded.
2393 $exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix );
2394
2395 if( ! $first ) {
2396 if ( $exclude ) {
2397 $searchlike .= ' AND ';
2398 } else {
2399 $searchlike .= ' OR ';
2400 }
2401 }
2402
2403 if ( $exclude ) {
2404 $term = substr( $term, 1 );
2405 $searchlike .= $wpdb->prepare( "(v.{$field_name} NOT LIKE %s)", '%' . $wpdb->esc_like( substr( $term, 1 ) ) . '%' );
2406 } else {
2407 $searchlike .= $wpdb->prepare( "(v.{$field_name} LIKE %s)", '%' . $wpdb->esc_like( $term ) . '%' );
2408 }
2409
2410 $first = false;
2411 }
2412 $where[] = "(". $searchlike .")";
2413 }
2414
2415 } else { // TODO same as like
2416 foreach ($args['fields_to_search'] as $field_name) {
2417 $field_name = sanitize_key($field_name);
2418 $where[] = "v.$field_name ='" . esc_sql($args['search_string']) . "'";
2419 }
2420 }
2421
2422 $where = implode(' '.esc_sql($args['and_or']).' ', $where);
2423
2424 // TODO: Sort by subtitles_count, 'chapters_count and transcript_count should be added here
2425 // TODO: Search the meta values too
2426 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2427 $video_data = $wpdb->get_results(
2428 "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"
2429 );
2430
2431 if (!$video_data) {
2432 return false;
2433 }
2434
2435 $videos = array();
2436
2437 foreach( $video_data AS $db_record ) {
2438 // create a new video object and populate it with DB values
2439 $record_id = $db_record->id;
2440 // if we don't unset this, we'll get warnings
2441 unset($db_record->id);
2442
2443 $video_object = new FV_Player_Db_Video( null, get_object_vars( $db_record ), $this );
2444 $video_object->link2db( $record_id );
2445
2446 // cache this player in DB object
2447 $videos[] = $video_object;
2448 }
2449
2450 return $videos;
2451 }
2452
2453 /**
2454 * Copy of core WordPress WP_Query::parse_search_terms() for our purposes without any changes
2455 *
2456 * Check if the terms are suitable for searching.
2457 *
2458 * Uses an array of stopwords (terms) that are excluded from the separate
2459 * term matching when searching for posts. The list of English stopwords is
2460 * the approximate search engines list, and is translatable. ( from class-wp-query.php )
2461 *
2462 * @since 3.7.0
2463 *
2464 * @param string[] $terms Array of terms to check.
2465 * @return string[] Terms that are not stopwords.
2466 */
2467 public function parse_search_terms( $terms ) {
2468 $strtolower = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';
2469 $checked = array();
2470
2471 $stopwords = $this->get_search_stopwords();
2472
2473 foreach ( $terms as $term ) {
2474 // Keep before/after spaces when term is for exact match.
2475 if ( preg_match( '/^".+"$/', $term ) ) {
2476 $term = trim( $term, "\"'" );
2477 } else {
2478 $term = trim( $term, "\"' " );
2479 }
2480
2481 // Avoid single A-Z and single dashes.
2482 if ( ! $term || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) {
2483 continue;
2484 }
2485
2486 if ( in_array( call_user_func( $strtolower, $term ), $stopwords, true ) ) {
2487 continue;
2488 }
2489
2490 $checked[] = $term;
2491 }
2492
2493 return $checked;
2494 }
2495
2496 /**
2497 * Copy of core WordPress WP_Query::get_search_stopwords() for our purposes without any changes
2498 *
2499 * Retrieve stopwords used when parsing search terms. ( from class-wp-query.php )
2500 *
2501 * @since 3.7.0
2502 *
2503 * @return string[] Stopwords.
2504 */
2505 public function get_search_stopwords() {
2506 if ( isset( $this->stopwords ) ) {
2507 return $this->stopwords;
2508 }
2509
2510 /*
2511 * translators: This is a comma-separated list of very common words that should be excluded from a search,
2512 * like a, an, and the. These are usually called "stopwords". You should not simply translate these individual
2513 * words into your language. Instead, look for and provide commonly accepted stopwords in your language.
2514 */
2515 $words = explode(
2516 ',',
2517 _x(
2518 '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',
2519 'Comma-separated list of search stopwords in your language'
2520 )
2521 );
2522
2523 $stopwords = array();
2524 foreach ( $words as $word ) {
2525 $word = trim( $word, "\r\n\t " );
2526 if ( $word ) {
2527 $stopwords[] = $word;
2528 }
2529 }
2530
2531 /**
2532 * Filters stopwords used when parsing search terms.
2533 *
2534 * @since 3.7.0
2535 *
2536 * @param string[] $stopwords Array of stopwords.
2537 */
2538 $this->stopwords = apply_filters( 'wp_search_stopwords', $stopwords );
2539 return $this->stopwords;
2540 }
2541
2542 /**
2543 * Sanitizes the value for DB class attributes.
2544 *
2545 * TODO: We got a report of PHP warning where the $value was an object in FV_Player_Db_Player_Meta.
2546 * How could that happen and should be sanitize objects and arrays recursively?
2547 *
2548 * @param mixed $value
2549 * @return mixed
2550 */
2551 public static function sanitize( $value ) {
2552
2553 /**
2554 * Avoid issues if the import JSON sets a null value for what's expected to be string "toggle_end_action":null
2555 */
2556 if ( is_string( $value ) ) {
2557 return stripslashes( $value );
2558 } else {
2559 return $value;
2560 }
2561 }
2562
2563 /**
2564 * Strips tags from the value for DB class attributes.
2565 *
2566 * Only "overlay" is allowed to have limited HTML.
2567 *
2568 * @param mixed $value
2569 * @param string $key
2570 *
2571 * @return mixed
2572 */
2573 public static function strip_tags( $value, $key ) {
2574 global $fv_fp;
2575
2576 /**
2577 * Avoid issues if the import JSON sets a null value for what's expected to be string "toggle_end_action":null
2578 */
2579 if ( is_string( $value ) ) {
2580 if( 'overlay' === $key ) {
2581 add_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit' ), 999, 2 );
2582 add_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit_settings' ), 999, 2 );
2583
2584 $value = wp_kses( $value, 'post' );
2585
2586 remove_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit' ), 999, 2 );
2587 remove_filter( 'wp_kses_allowed_html', array( $fv_fp, 'wp_kses_permit_settings' ), 999, 2 );
2588
2589 } else {
2590 $value = wp_strip_all_tags( $value );
2591 }
2592 }
2593
2594 return $value;
2595 }
2596
2597 }
2598