Headline
CVE-2023-0447: Changeset 2844200 for youtube-channel – WordPress Plugin Repository
The My YouTube Channel plugin for WordPress is vulnerable to authorization bypass due to a missing capability check on the clear_all_cache function in versions up to, and including, 3.0.12.1. This makes it possible for authenticated attackers, with subscriber-level permissions and above, to clear the plugin’s cache.
19 if ( ! class_exists( ‘WPAU_YOUTUBE_CHANNEL’ ) ) { 20 class WPAU_YOUTUBE_CHANNEL { 19define( 'YTC_VER’, ‘3.23.0’ ); 20define( 'YTC_VER_DB’, 24 ); 21define( 'YTC_PLUGIN_FILE’, __FILE__ ); 22define( 'YTC_PLUGIN’, plugin_basename( __FILE__ ) ); 23define( 'YTC_DIR’, dirname( __FILE__ ) ); 24define( 'YTC_DIR_INC’, YTC_DIR . ‘/inc’ ); 25define( 'YTC_DIR_CLASSES’, YTC_DIR . ‘/classes’ ); 26define( 'YTC_DIR_TEMPLATES’, YTC_DIR . ‘/templates’ ); 27define( 'YTC_URL’, plugin_dir_url( __FILE__ ) ); 28define( 'YTC_URL_ASSETS’, YTC_URL . ‘assets’ );
22 const DB_VER = 23; 23 const VER = '3.0.12.1’; 24 25 public $plugin_name = 'YouTube Channel’; 26 public $plugin_slug = 'youtube-channel’; 27 public $plugin_option = 'youtube_channel_defaults’; 28 public $plugin_url; 29 public $ytc_html5_js = '’; 30 public $defaults; 31 32 /** 33 * Construct class 34 */ 35 function __construct() { 36 37 $this->plugin_url = plugin_dir_url( __FILE__ ); 38 load_plugin_textdomain( $this->plugin_slug, false, dirname( plugin_basename( __FILE__ ) ) . ‘/languages’ ); 39 40 // Generate debug JSON 41 if ( ! empty( $_GET[‘ytc_debug_json_for’] ) ) { 42 $this->generate_debug_json(); 43 } 44 45 // Clear all YTC cache 46 add_action( 'wp_ajax_ytc_clear_all_cache’, [ &$this, ‘clear_all_cache’ ] ); 47 48 // Activation hook and maybe update trigger 49 register_activation_hook( __FILE__, [ $this, ‘activate’ ] ); 50 add_action( 'plugins_loaded’, [ $this, ‘maybe_update’ ] ); 51 52 $this->defaults = self::defaults(); 53 54 // TinyMCE AddOn 55 if ( ! empty( $this->defaults[‘tinymce’] ) ) { 56 add_filter( 'mce_external_plugins’, [ $this, ‘mce_external_plugins’ ], 998 ); 57 add_filter( 'mce_buttons’, [ $this, ‘mce_buttons’ ], 999 ); 58 } 59 60 if ( is_admin() ) { 61 62 // Initialize Plugin Settings Magic 63 add_action( 'init’, [ $this, ‘admin_init’ ] ); 64 65 // Add various Dashboard notices (if needed) 66 add_action( 'admin_notices’, [ $this, ‘admin_notices’ ] ); 67 68 // Enqueue scripts and styles for Widgets page 69 add_action( 'admin_enqueue_scripts’, [ $this, ‘admin_scripts’ ] ); 70 71 } else { // ELSE if ( is_admin() ) 72 73 // Enqueue frontend scripts 74 add_action( 'wp_enqueue_scripts’, [ $this, ‘enqueue_scripts’ ] ); 75 add_action( 'wp_footer’, [ $this, ‘footer_scripts’ ] ); 76 77 } // END if ( is_admin() ) 78 79 // Load widget 80 require_once( ‘inc/widget.php’ ); 81 82 // Register shortcodes `youtube_channel` and `ytc` 83 add_shortcode( 'youtube_channel’, [ $this, ‘shortcode’ ] ); 84 add_shortcode( 'ytc’, [ $this, ‘shortcode’ ] ); 85 86 } // END function __construct() 87 88 /** 89 * Activate the plugin 90 * Credits: http://solislab.com/blog/plugin-activation-checklist/#update-routines 91 */ 92 public static function activate() { 93 94 global $wpau_youtube_channel; 95 $wpau_youtube_channel->init_options(); 96 $wpau_youtube_channel->maybe_update(); 97 98 } // END public static function activate() 99 100 /** 101 * Return initial options 102 * @return array Global defaults for current plugin version 103 */ 104 public function init_options() { 105 106 $init = [ 107 ‘vanity’ => '’, // $this->vanity_id, 108 ‘channel’ => '’, // $this->channel_id, 109 ‘username’ => '’, // $this->username_id, 110 ‘playlist’ => '’, // $this->playlist_id, 111 ‘resource’ => 0, // ex use_res 112 ‘cache’ => 300, // 5 minutes // ex cache_time 113 ‘fetch’ => 25, // ex maxrnd 114 ‘num’ => 1, // ex vidqty 115 ‘skip’ => 0, 116 ‘privacy’ => 0, 117 118 ‘ratio’ => 3, // 3 - 16:9, 1 - 4:3 (deprecated: 2 - 16:10) 119 ‘width’ => 306, 120 ‘responsive’ => 1, 121 ‘display’ => 'thumbnail’, // thumbnail, iframe, iframe2, playlist (deprecated: chromeless, object) 122 ‘thumb_quality’ => 'hqdefault’, // default, mqdefault, hqdefault, sddefault, maxresdefault 123 ‘fullscreen’ => 0, 124 ‘controls’ => 0, 125 ‘autoplay’ => 0, 126 ‘autoplay_mute’ => 0, 127 ‘norel’ => 0, 128 ‘playsinline’ => 0, // play video on mobile devices inline instead in native device player 129 ‘showtitle’ => 'none’, // above, below, inside, inside_b 130 ‘linktitle’ => 0, 131 ‘titletag’ => 'h3’, 132 ‘showdesc’ => 0, 133 ‘desclen’ => 0, 134 ‘modestbranding’ => 0, 135 ‘hideanno’ => 0, 136 137 ‘goto_txt’ => 'Visit our channel’, 138 ‘popup_goto’ => 0, // 0 same window, 1 new window JS, 2 new window target 139 ‘link_to’ => 'none’, // 0 legacy username, 1 channel, 2 vanity 140 ‘tinymce’ => 1, // show TinyMCE button by default 141 ‘nolightbox’ => 0, // do not use lightbox global setting 142 ‘timeout’ => 5, // timeout for wp_remote_get() 143 ‘sslverify’ => true, 144 ‘js_ev_listener’ => false, 145 ]; 146 147 add_option( 'youtube_channel_version’, self::VER, '’, ‘no’ ); 148 add_option( 'youtube_channel_db_ver’, self::DB_VER, '’, ‘no’ ); 149 add_option( $this->plugin_option, $init, '’, ‘no’ ); 150 151 return $init; 152 153 } // END public function init_options() 154 155 /** 156 * Check do we need to migrate options 157 */ 158 public function maybe_update() { 159 160 // bail if this plugin data doesn’t need updating 161 if ( get_option( ‘youtube_channel_db_ver’ ) >= self::DB_VER ) { 162 return; 163 } 164 165 require_once( dirname( __FILE__ ) . ‘/update.php’ ); 166 au_youtube_channel_update(); 167 168 } // END public function maybe_update() 169 170 /** 171 * Initialize Settings link for Plugins page and create Settings page 172 */ 173 function admin_init() { 174 175 add_filter( ‘plugin_action_links_’ . plugin_basename( __FILE__ ), [ $this, ‘add_settings_link’ ] ); 176 // Add row on Plugins page 177 add_filter( 'plugin_row_meta’, [ $this, ‘add_plugin_meta_links’ ], 10, 2 ); 178 179 require_once( ‘inc/settings.php’ ); 180 181 global $wpau_youtube_channel_settings; 182 if ( empty( $wpau_youtube_channel_settings ) ) { 183 $wpau_youtube_channel_settings = new WPAU_YOUTUBE_CHANNEL_SETTINGS(); 184 } 185 186 } // END function admin_init_settings() 187 188 /** 189 * Append Settings link for Plugins page 190 * @param array $links array of links on plugins page 191 */ 192 function add_settings_link( $links ) { 193 194 $settings_title = __( ‘Settings’ ); 195 $settings_link = “<a href=\"options-general.php?page={$this->plugin_slug}\">{$settings_title}</a>"; 196 array_unshift( $links, $settings_link ); 197 198 // Free some memory 199 unset( $settings_title, $settings_link ); 200 201 // Return updated array of links 202 return $links; 203 204 } // END function add_settings_link() 205 206 /** 207 * Add link to official plugin page 208 */ 209 function add_plugin_meta_links( $links, $file ) { 210 211 if ( ‘youtube-channel/youtube-channel.php’ === $file ) { 212 return array_merge( 213 $links, 214 [ 215 sprintf( 216 '<a href="https://wordpress.org/support/plugin/youtube-channel/” target="_blank">%s</a>’, 217 __( ‘Support’ ) 218 ), 219 ] 220 ); 221 } 222 return $links; 223 224 } // END function add_plugin_meta_links() 225 226 /** 227 * Enqueue admin scripts and styles for widget customization 228 */ 229 function admin_scripts() { 230 231 global $pagenow; 232 233 // Enqueue only on widget or post pages 234 if ( ! in_array( $pagenow, [ 'widgets.php’, 'customize.php’, 'options-general.php’, 'post.php’, ‘post-new.php’ ] ) ) { 235 return; 236 } 237 238 // Enqueue on post page only if tinymce is enabled 239 if ( in_array( $pagenow, [ 'post.php’, ‘post-new.php’ ] ) && empty( $this->defaults[‘tinymce’] ) ) { 240 return; 241 } 242 243 wp_enqueue_style( 244 $this->plugin_slug . '-admin’, 245 plugins_url( 'assets/css/admin.css’, __FILE__ ), 246 [], 247 self::VER 248 ); 249 250 // Enqueue script for widget in admin 251 if ( in_array( $pagenow, [ 'widgets.php’, ‘customize.php’ ] ) ) { 252 wp_enqueue_script( 253 $this->plugin_slug . '-admin’, 254 plugins_url( 'assets/js/admin.min.js’, __FILE__ ), 255 [ ‘jquery’ ], 256 self::VER, 257 true 258 ); 259 } 260 } // END function admin_scripts() 261 262 /** 263 * Print dashboard notice 264 * @return string Formatted notice with usefull explanation 265 */ 266 function admin_notices() { 267 268 // Get array of dismissed notices 269 $dismissed_notices = get_option( ‘youtube_channel_dismissed_notices’ ); 270 271 // Dismiss notices if requested and then update option in DB 272 if ( ! empty( $_GET[‘ytc_dismiss_notice_old_php’] ) ) { 273 $dismissed_notices[‘old_php’] = 1; 274 update_option( 'youtube_channel_dismissed_notices’, $dismissed_notices ); 275 } 276 if ( ! empty( $_GET[‘ytc_dismiss_notice_apikey_wpconfig’] ) ) { 277 $dismissed_notices[‘apikey_wpconfig’] = 1; 278 update_option( 'youtube_channel_dismissed_notices’, $dismissed_notices ); 279 } 280 if ( ! empty( $_GET[‘ytc_dismiss_notice_vanity_option’] ) ) { 281 $dismissed_notices[‘vanity_option’] = 1; 282 update_option( 'youtube_channel_dismissed_notices’, $dismissed_notices ); 283 } 284 if ( ! empty( $_GET[‘ytc_dismiss_notice_changed_shortcode_308’] ) ) { 285 $dismissed_notices[‘changed_shortcode_308’] = 1; 286 update_option( 'youtube_channel_dismissed_notices’, $dismissed_notices ); 287 } 288 289 // Prepare vars for notices 290 $settings_page = 'options-general.php?page=youtube-channel’; 291 $notice = [ 292 ‘error’ => '’, 293 ‘warning’ => '’, 294 ‘info’ => '’, 295 ]; 296 297 // Inform if PHP version is lower than 5.3 298 if ( 299 version_compare( PHP_VERSION, '5.3’, ‘<’ ) && 300 ( 301 empty( $dismissed_notices ) || 302 ( ! empty( $dismissed_notices ) && empty( $dismissed_notices[‘old_php’] ) ) 303 ) 304 ) { 305 $notice[‘info’] .= sprintf( 306 __( '<p>Your website running on web server with PHP version %1$s. Please note that <strong>%2$s</strong> requires PHP at least 5.3 or newer to work properly. <a href="%3$s" class="dismiss">Dismiss</a></p>’, ‘youtube-channel’ ), 307 PHP_VERSION, 308 $this->plugin_name, 309 ‘?ytc_dismiss_notice_old_php=1’ 310 ); 311 } 312 313 // Inform if YOUTUBE_DATA_API_KEY is still in wp-config.php 314 if ( 315 defined( ‘YOUTUBE_DATA_API_KEY’ ) && 316 empty( $dismissed_notices[‘apikey_wpconfig’] ) 317 ) { 318 $notice[‘info’] .= sprintf( 319 __( '<p>Since <strong>%1$s</strong> v3.0.6 we store <strong>YouTube Data API Key</strong> in plugin settings. So, you can safely remove %2$s define line from your <strong>wp-config.php</strong> file. <a href="%3$s" class="dismiss">Dismiss</a></p>’, ‘youtube-channel’ ), 320 $this->plugin_name, 321 'YOUTUBE_DATA_API_KEY’, 322 ‘?ytc_dismiss_notice_apikey_wpconfig=1’ 323 ); 324 } 325 326 // No YouTube DATA Api Key? 327 if ( empty( $this->defaults[‘apikey’] ) ) { 328 $notice[‘error’] .= sprintf( 329 wp_kses( 330 __( 331 '<p><strong>%1$s v3</strong> require <strong>%2$s</strong> set on plugin <a href="%6$s">%7$s</a>. You can generate your own key on <a href="%3$s" target="_blank">%4$s</a> by following <a href="%5$s" target="_blank">this tutorial</a>.</p>’, 332 ‘youtube-channel’ 333 ), 334 [ 335 ‘a’ => [ 336 ‘href’ => [], 337 ‘target’ => [ ‘_blank’ ], 338 ], 339 ‘p’ => [], 340 ‘strong’ => [], 341 ‘br’ => [], 342 ] 343 ), 344 $this->plugin_name, 345 __( 'YouTube Data API Key’, ‘youtube-channel’ ), 346 esc_url( ‘https://console.developers.google.com/project’ ), 347 __( 'Google Developers Console’, ‘youtube-channel’ ), 348 esc_url( ‘https://urosevic.net/wordpress/plugins/youtube-channel/#youtube_data_api_key’ ), 349 esc_url( ‘options-general.php?page=youtube-channel&tab=general’ ), 350 __( 'General Settings’, ‘youtube-channel’ ) 351 ); 352 } 353 354 // v3.0.8.1 shortcode changes since v3.0.8 355 if ( ! empty( $dismissed_notices ) && empty( $dismissed_notices[‘changed_shortcode_308’] ) ) { 356 $notice[‘warning’] .= sprintf( 357 __( '<p><strong>%1$s</strong> changed shortcode parameters by removing <code>only_pl</code> and <code>showgoto</code>, and combining with parameters <code>display</code> and <code>link_to</code> respectively. Please check out <a href="%2$s&tab=help">%3$s</a> and update your shortcodes. <a href="%4$s" class="dismiss">Dismiss</a>’, ‘youtube-channel’ ), 358 $this->plugin_name, 359 $settings_page, 360 'Help: How to use shortcode’, 361 ‘?ytc_dismiss_notice_changed_shortcode_308=1’ 362 ); 363 } elseif ( empty( $dismissed_notices ) ) { 364 // First time install? auto dismiss this notice 365 $dismissed_notices[‘changed_shortcode_308’] = 1; 366 update_option( 'youtube_channel_dismissed_notices’, $dismissed_notices ); 367 } 368 369 foreach ( $notice as $type => $message ) { 370 if ( ! empty( $message ) ) { 371 echo "<div class=\"notice notice-{$type}\">{$message}</div>"; 372 } 373 } 374 375 } // END function admin_notices() 376 377 /** 378 * Get default options from DB 379 * @return array Latest global defaults 380 */ 381 public function defaults() { 382 383 $defaults = get_option( $this->plugin_option ); 384 if ( empty( $defaults ) ) { 385 $defaults = $this->init_options(); 386 } 387 388 return $defaults; 389 390 } // END public function defaults() 391 392 /** 393 * Enqueue frontend scripts and styles 394 */ 395 function enqueue_scripts() { 396 397 // Check do we need our own lightbox? 398 if ( empty( $this->defaults[‘nolightbox’] ) ) { 399 wp_enqueue_style( 400 'magnific-popup-au’, 401 plugins_url( 'assets/lib/magnific-popup/magnific-popup.min.css’, __FILE__ ), 402 [], 403 self::VER 404 ); 405 wp_enqueue_script( 406 'magnific-popup-au’, 407 plugins_url( 'assets/lib/magnific-popup/jquery.magnific-popup.min.js’, __FILE__ ), 408 [ ‘jquery’ ], 409 self::VER, 410 true 411 ); 412 } 413 414 wp_enqueue_style( 415 'youtube-channel’, 416 plugins_url( 'assets/css/youtube-channel.css’, __FILE__ ), 417 [], 418 self::VER 419 ); 420 421 } // END function enqueue_scripts() 422 423 /** 424 * Generate comlete inline JavaScript code that conains 425 * Async video load and lightbox init for thumbnails 426 * @return string Compressed JavaScript code 427 */ 428 function footer_scripts() { 429 430 $js = '’; 431 432 // Print YT API only if we have set ytc_html5_js in $_SESSION 433 // if ( ! empty( $_SESSION[‘ytc_html5_js’] ) ) { 434 if ( ! empty( $this->ytc_html5_js ) ) { 435 $js .= " 436 if (!window[‘YT’]) { 437 var tag=document.createElement(‘script’); 438 tag.src=\"//www.youtube.com/iframe_api\"; 439 var firstScriptTag=document.getElementsByTagName(‘script’)[0]; 440 firstScriptTag.parentNode.insertBefore(tag,firstScriptTag); 441 } 442 function ytc_create_ytplayers(){ 443 {$this->ytc_html5_js} 444 } 445 try { 446 ytc_create_ytplayers(); 447 } catch (e) {} 448 function onYouTubeIframeAPIReady(){ 449 ytc_create_ytplayers(); 450 } 451 function ytc_mute(event){event.target.mute();} 452 "; 453 } // END if ( ! empty($_SESSION[‘ytc_html5_js’]) ) 454 455 // Print Magnific Popup if not disabled 456 if ( empty( $this->defaults[‘nolightbox’] ) ) { 457 $js .= " 458 function ytc_init_MPAU() { 459 jQuery(‘.ytc-lightbox’).magnificPopupAU({ 460 disableOn:320, 461 type:’iframe’, 462 mainClass:’ytc-mfp-lightbox’, 463 removalDelay:160, 464 preloader:false, 465 fixedContentPos:false 466 }); 467 } 468 jQuery(window).on('load’,function(){ 469 ytc_init_MPAU(); 470 }); 471 jQuery(document).ajaxComplete(function(){ 472 ytc_init_MPAU(); 473 }); 474 "; 475 } // END if ( empty($this->defaults[‘nolightbox’]) ) 476 477 if ( ! empty( $js ) ) { 478 $js = sprintf( 479 ' 480 <!-- YouTube Channel 3 --> 481 <script type="text/javascript">%2$s%1$s%3$s</script> 482 ', 483 $js, 484 $this->defaults[‘js_ev_listener’] ? “window.addEventListener('DOMContentLoaded’, function() {” : '’, 485 $this->defaults[‘js_ev_listener’] ? ‘});’ : ‘’ 486 ); 487 488 if ( WP_DEBUG ) { 489 // Uncompressed code if WP debug is enabled 490 $js = str_replace( ';var’, ";\nvar", $js ); 491 $js = str_replace( "\t", '’, $js ); 492 // $js = str_replace(',’, “,\n\t", $js); 493 echo $js; 494 } else { 495 echo trim( preg_replace( '/[\t\r\n]+/’, '’, $js ) ); 496 } 497 } 498 499 } // END function footer_scripts() 500 501 public function shortcode( $atts ) { 502 503 // Get general default settings 504 $instance = $this->defaults(); 505 506 // Extract shortcode parameters 507 $atts = shortcode_atts( 508 [ 509 ‘vanity’ => $instance[‘vanity’], 510 ‘channel’ => $instance[‘channel’], 511 ‘username’ => $instance[‘username’], 512 ‘playlist’ => $instance[‘playlist’], 513 ‘res’ => '’, // (deprecated, but leave for back compatibility) ex res 514 ‘use_res’ => '’, // (deprecated, but leave for back compatibility) ex use_res 515 ‘resource’ => $instance[‘resource’], // ex use_res 516 ‘only_pl’ => 0, // disabled by default (was: $instance[‘only_pl’],) 517 ‘cache’ => $instance[‘cache’], // ex cache_time 518 ‘privacy’ => $instance[‘privacy’], // ex showvidesc 519 ‘fetch’ => $instance[‘fetch’], // ex maxrnd 520 ‘num’ => $instance[‘num’], // ex vidqty 521 522 ‘random’ => 0, // ex getrnd 523 524 ‘ratio’ => $instance[‘ratio’], 525 ‘width’ => $instance[‘width’], 526 ‘responsive’ => ! empty( $instance[‘responsive’] ) ? $instance[‘responsive’] : '0’, 527 528 ‘show’ => $instance[‘display’], // (deprecated, but keep for back compatibility) ex to_show 529 ‘display’ => $instance[‘display’], 530 ‘thumb_quality’ => $instance[‘thumb_quality’], 531 ‘no_thumb_title’ => 0, 532 ‘controls’ => $instance[‘controls’], 533 ‘autoplay’ => $instance[‘autoplay’], 534 ‘mute’ => $instance[‘autoplay_mute’], 535 ‘norel’ => $instance[‘norel’], 536 ‘playsinline’ => $instance[‘playsinline’], // play video on mobile devices inline instead in native device player 537 538 ‘showtitle’ => $instance[‘showtitle’], // none, above, below, inside, inside_b 539 ‘linktitle’ => ! empty( $instance[‘linktitle’] ) ? $instance[‘linktitle’] : '0’, 540 ‘titletag’ => $instance[‘titletag’], // h3, h4, h5, div, span, strong 541 ‘showdesc’ => $instance[‘showdesc’], // ex showvidesc 542 ‘nobrand’ => ! empty( $instance[‘modestbranding’] ) ? $instance[‘modestbranding’] : '0’, 543 ‘desclen’ => $instance[‘desclen’], // ex videsclen 544 ‘noanno’ => $instance[‘hideanno’], 545 546 ‘goto_txt’ => $instance[‘goto_txt’], 547 ‘popup’ => $instance[‘popup_goto’], 548 ‘link_to’ => $instance[‘link_to’], // none, vanity, channel, legacy 549 550 ‘class’ => ! empty( $instance[‘class’] ) ? $instance[‘class’] : '’, 551 552 ‘nolightbox’ => ! empty( $instance[‘nolightbox’] ) ? $instance[‘nolightbox’] : '0’, 553 ‘target’ => '’, 554 ‘skip’ => 0, // how many items to skip 555 ], 556 $atts 557 ); 558 559 // backward compatibility for show -> display shortcode parameter 560 if ( ! empty( $atts[‘show’] ) && $atts[‘show’] !== $atts[‘display’] && $atts[‘show’] !== $instance[‘display’] ) { 561 $atts[‘display’] = $atts[‘show’]; 562 } 563 // backward compatibility for use_res -> resource shortcode parameter 564 if ( ! empty( $atts[‘use_res’] ) ) { 565 $atts[‘resource’] = $atts[‘use_res’]; 566 } elseif ( ! empty( $atts[‘res’] ) ) { 567 $atts[‘resource’] = $atts[‘res’]; 568 } 569 570 // prepare instance for output 571 $instance[‘vanity’] = $atts[‘vanity’]; 572 $instance[‘channel’] = $atts[‘channel’]; 573 $instance[‘username’] = $atts[‘username’]; 574 $instance[‘playlist’] = $atts[‘playlist’]; 575 $instance[‘resource’] = $atts[‘resource’]; // resource: 0 channel, 1 favorites, 2 playlist, 3 liked 576 $instance[‘cache’] = $atts[‘cache’]; // in seconds, def 5min - settings? 577 $instance[‘privacy’] = $atts[‘privacy’]; // enhanced privacy 578 579 $instance[‘fetch’] = (int) $atts[‘fetch’]; 580 $instance[‘num’] = (int) $atts[‘num’]; // num: 1 581 582 $instance[‘random’] = $atts[‘random’]; // use embedded playlist - false by default 583 584 // Video Settings 585 $instance[‘ratio’] = $atts[‘ratio’]; // aspect ratio: 3 - 16:9, 2 - 16:10, 1 - 4:3 586 $instance[‘width’] = (int) $atts[‘width’]; // 306 587 $instance[‘responsive’] = $atts[‘responsive’]; // enable responsivenes? 588 $instance[‘display’] = $atts[‘display’]; // thumbnail, iframe, iframe2, playlist 589 $instance[‘thumb_quality’] = $atts[‘thumb_quality’]; // default, mqdefault, hqdefault, sddefault, maxresdefault 590 $instance[‘no_thumb_title’] = $atts[‘no_thumb_title’]; // hide tooltip for thumbnails 591 592 $instance[‘controls’] = $atts[‘controls’]; // hide controls, false by default 593 $instance[‘autoplay’] = $atts[‘autoplay’]; // autoplay disabled by default 594 $instance[‘autoplay_mute’] = $atts[‘mute’]; // mute sound on autoplay - disabled by default 595 $instance[‘norel’] = $atts[‘norel’]; // hide related videos 596 $instance[‘playsinline’] = $atts[‘playsinline’]; // inline plaer for iOS 597 598 // Content Layout 599 $instance[‘showtitle’] = $atts[‘showtitle’]; // show video title, disabled by default 600 $instance[‘linktitle’] = $atts[‘linktitle’]; // link title to video, disabled by default 601 $instance[‘titletag’] = $atts[‘titletag’]; // title HTML tag wrapper, h3 by default 602 $instance[‘showdesc’] = $atts[‘showdesc’]; // show video description, disabled by default 603 $instance[‘modestbranding’] = $atts[‘nobrand’]; // hide YT logo 604 $instance[‘desclen’] = (int) $atts[‘desclen’]; // cut video description, number of characters 605 $instance[‘hideanno’] = $atts[‘noanno’]; // hide annotations, false by default 606 607 // Link to Channel 608 $instance[‘goto_txt’] = $atts[‘goto_txt’]; // text for goto link - use settings 609 $instance[‘popup_goto’] = $atts[‘popup’]; // open channel in: 0 same window, 1 javascript new, 2 target new 610 $instance[‘link_to’] = $atts[‘link_to’]; // link to: none, vanity, legacy, channel 611 612 // Customization 613 $instance[‘class’] = $atts[‘class’]; // custom additional class for container 614 615 $instance[‘nolightbox’] = $atts[‘nolightbox’]; // custom usage of lightbox 616 $instance[‘target’] = $atts[‘target’]; // custom target for thumbnails w/o lightbox (empty, _blank or custom) 617 618 $instance[‘skip’] = (int) $atts[‘skip’]; 619 // return implode( array_values( $this->output( $instance ) ) ); 620 return $this->output( $instance ); 621 } // END public function shortcode() 622 623 // Print out YTC block 624 public function output( $instance ) { 625 626 // Error message if no YouTube Data API Key 627 if ( empty( $this->defaults[‘apikey’] ) ) { 628 629 $error_msg = sprintf( 630 __( '<strong>%1$s v3</strong> requires <strong>YouTube DATA API Key</strong> to work. <a href="%2$s” target="_blank">Learn more here</a>.’, ‘youtube-channel’ ), 631 $this->plugin_name, 632 ‘https://urosevic.net/wordpress/plugins/youtube-channel/#youtube_data_api_key’ 633 ); 634 635 return $this->front_debug( $error_msg ); 636 637 } 638 639 // 1) Get resource from widget/shortcode 640 // 2) If not set, get global default 641 // 3) if no global, get plugin’s default 642 if ( ! isset( $instance[‘resource’] ) ) { 643 $instance[‘resource’] = $this->defaults[‘resource’]; 644 } 645 $resource = intval( $instance[‘resource’] ); 646 if ( empty( $resource ) && 0 !== $resource ) { 647 $resource = intval( $this->defaults[‘resource’] ); 648 if ( empty( $resource ) ) { 649 $resource = 0; 650 } 651 } 652 653 // Get Channel or Playlist ID based on requested resource 654 switch ( $resource ) { 655 656 // Playlist 657 case '2’: 658 // 1) Get Playlist from shortcode/widget 659 // 2) If not set, use global default 660 // 3) If no global, throw error 661 if ( ! empty( $instance[‘playlist’] ) ) { 662 $playlist = trim( $instance[‘playlist’] ); 663 } else { 664 $playlist = trim( $this->defaults[‘playlist’] ); 665 } 666 // Now check has Playlist ID set or throw error 667 if ( ‘’ == $playlist ) { 668 return $this->front_debug( ‘Playlist selected as resource but no Playlist ID provided!’ ); 669 } 670 break; 671 672 // Channel, Favourites, Liked 673 default: 674 /* Channel */ 675 // 1) Get channel from shortcode/widget 676 // 2) If not set, use global default 677 // 3) If no global, throw error 678 if ( ! empty( $instance[‘channel’] ) ) { 679 $channel = trim( $instance[‘channel’] ); 680 } else { 681 $channel = trim( $this->defaults[‘channel’] ); 682 } 683 // Now check is Channel ID set or throw error 684 if ( ‘’ == $channel ) { 685 if ( 1 == $resource ) { 686 $resource_name = 'Favourited videos’; 687 } elseif ( 3 == $resource ) { 688 $resource_name = 'Liked videos’; 689 } else { 690 $resource_name = 'Channel (User uploads)'; 691 } 692 $error_msg = sprintf( '%s selected as resource but no Channel ID provided!’, $resource_name ); 693 return $this->front_debug( $error_msg ); 694 } 695 } // END switch ($resource) 696 697 /* OK, we have required resource (Playlist or Channel ID), so we can proceed to real job */ 698 699 // Set custom class and responsive if needed 700 $class = ( ! empty( $instance[‘class’] ) ) ? $instance[‘class’] : 'default’; 701 if ( ! empty( $instance[‘responsive’] ) ) { 702 $class .= ' responsive’; 703 } 704 if ( ! empty( $instance[‘display’] ) ) { 705 $class .= " ytc_display_{$instance[‘display’]}"; 706 } 707 708 switch ( $resource ) { 709 case 1: // Favourites 710 $resource_name = 'favourites’; 711 $resource_id = preg_replace( '/^UC/’, 'FL’, $channel ); 712 break; 713 case 2: // Playlist 714 $resource_name = 'playlist’; 715 $resource_id = $playlist; 716 break; 717 case 3: // Liked 718 $resource_name = 'liked’; 719 $resource_id = preg_replace( '/^UC/’, 'LL’, $channel ); 720 break; 721 default: // Channel 722 $resource_name = 'channel’; 723 $resource_id = preg_replace( '/^UC/’, 'UU’, $channel ); 724 } 725 726 // Start output string 727 $output = ‘’; 728 729 $output .= “<div class=\"youtube_channel {$class}\">"; 730 731 if ( empty( $instance[‘display’] ) ) { 732 $instance[‘display’] = $this->defaults[‘display’]; 733 } 734 if ( ‘playlist’ == $instance[‘display’] ) { // Insert as Embedded playlist 735 736 $output .= self::embed_playlist( $resource_id, $instance ); 737 738 } else { // Individual videos from channel, favourites, liked or playlist 739 740 // Get max items for random video 741 $fetch = ( empty( $instance[‘fetch’] ) ) ? $this->defaults[‘fetch’] : $instance[‘fetch’]; 742 if ( $fetch < 1 ) { 743 $fetch = 10; 744 } elseif ( $fetch > 50 ) { 745 $fetch = 50; 746 } 747 748 // How many items to skip? 749 $skip = 0; 750 if ( ! empty( $instance[‘skip’] ) ) { 751 $skip = intval( $instance[‘skip’] ) > 49 ? 49 : intval( $instance[‘skip’] ); 752 } 753 // If we have to skip more items than we have in fetch, set skip to $fetch-1 754 if ( $skip >= $fetch ) { 755 $skip = $fetch - 1; 756 } 757 758 $resource_key = “{$resource_id}_{$fetch}"; 759 760 // Do we need cache? Let we define cache fallback key 761 $cache_key_fallback = ‘ytc_’ . md5( $resource_key ) . '_fallback’; 762 763 // Do cache magic 764 if ( ! empty( $instance[‘cache’] ) && $instance[‘cache’] > 0 ) { 765 766 // generate feed cache key for caching time 767 $cache_key = ‘ytc_’ . md5( $resource_key ) . ‘_’ . $instance[‘cache’]; 768 769 if ( ! empty( $_GET[‘ytc_force_recache’] ) ) { 770 delete_transient( $cache_key ); 771 } 772 773 // get/set transient cache 774 $json = get_transient( $cache_key ); 775 if ( false === $json || empty( $json ) ) { 776 777 // no cached JSON, get new 778 $json = $this->fetch_youtube_feed( $resource_id, $fetch ); 779 780 // set decoded JSON to transient cache_key 781 set_transient( $cache_key, base64_encode( $json ), $instance[‘cache’] ); 782 783 } else { 784 785 // we already have cached feed JSON, get it encoded 786 $json = base64_decode( $json ); 787 788 } 789 } else { 790 791 // just get fresh feed if cache disabled 792 $json = $this->fetch_youtube_feed( $resource_id, $fetch ); 793 794 } 795 796 // free some memory 797 unset( $response ); 798 799 // decode JSON data 800 $json_output = json_decode( $json ); 801 802 // YTC 3.0.7: Do we need this, still? 803 // if current feed is messed up, try to get it from fallback cache 804 if ( is_wp_error( $json_output ) && ! is_object( $json_output ) && empty( $json_output->items ) ) { 805 // do we have fallback cache?! 806 $json_fallback = get_transient( $cache_key_fallback ); 807 if ( true === $json_fallback && ! empty( $json_fallback ) ) { 808 $json_output = json_decode( base64_decode( $json_fallback ) ); 809 // and free memory 810 unset( $json_fallback ); 811 } 812 } 813 814 // Get resource nice name based on selected resource 815 $resource_nice_name = $this->resource_nice_name( $resource ); 816 817 // Prevent further checks if we have WP Error or empty record even after fallback 818 if ( is_wp_error( $json_output ) ) { 819 $output .= $this->front_debug( $json_output->get_error_message() ); 820 return $output; 821 } elseif ( isset( $json_output->items ) && 0 == sizeof( $json_output->items ) ) { 822 $output .= $this->front_debug( sprintf( __( ‘You have set to display videos from %1$s [resource list ID: %2$s], but there have no public videos in that resouce.’ ), $resource_nice_name, $resource_id ) ); 823 return $output; 824 } elseif ( empty( $json_output ) ) { 825 $output .= $this->front_debug( sprintf( __( 'We have empty record for this feed. Please read <a href="%1$s” target="_blank">FAQ</a> and if that does not help, contact <a href="%2$s” target="_blank">support</a>.’ ), 'https://wordpress.org/plugins/youtube-channel#faq’, ‘https://wordpress.org/support/plugin/youtube-channel/’ ) ); 826 return $output; 827 } 828 829 // Predefine `max_items` to prevent undefined notices 830 $max_items = 0; 831 if ( is_object( $json_output ) && ! empty( $json_output->items ) ) { 832 833 // Sort by date uploaded 834 $json_entry = $json_output->items; 835 836 $num = ( empty( $instance[‘num’] ) ) ? $this->defaults[‘num’] : $instance[‘num’]; 837 if ( $num > $fetch ) { 838 $fetch = $num; 839 } 840 $max_items = ( $fetch > sizeof( $json_entry ) ) ? sizeof( $json_entry ) : $fetch; 841 842 if ( ! empty( $instance[‘random’] ) ) { 843 $items = array_slice( $json_entry, 0, $max_items ); 844 } else { 845 if ( ! $num ) { 846 $num = 1; 847 } 848 $items = array_slice( $json_entry, $skip, $num ); 849 } 850 } 851 852 if ( 0 == $max_items ) { 853 854 // Set default error message 855 $error_msg = ‘Unrecognized error experienced.’; 856 857 // Append YouTube DATA API error reason as comment 858 if ( ! empty( $json_output ) && is_object( $json_output ) && ! empty( $json_output->error->errors ) ) { 859 860 // Error went in fetch_youtube_feed() 861 if ( ‘wpError’ == $json_output->error->errors[0]->reason ) { 862 $error_msg = $json_output->error->errors[0]->message; 863 } elseif ( ‘playlistNotFound’ == $json_output->error->errors[0]->reason ) { 864 // Playlist error from Google API 865 if ( ‘playlist’ == $resource_name ) { 866 $error_msg = “Please check did you set existing <em>Playlist ID</em>. You set to show videos from {$resource_nice_name}, but YouTube does not recognize <strong>{$resource_id}</strong> as existing and public playlist."; 867 } else { 868 $error_msg = “Please check did you set the proper <em>Channel ID</em>. You set to show videos from {$resource_nice_name}, but YouTube does not recognize <strong>{$channel}</strong> as an existing or public channel."; 869 } 870 } elseif ( ‘keyInvalid’ == $json_output->error->errors[0]->reason ) { 871 // Invalid YouTube Data API Key 872 $error_msg = sprintf( __( “Double check <em>YouTube Data API Key</em> on <em>General</em> plugin tab and make sure it’s correct. Read <a href=\"%s\” target=\"_blank\">Installation</a> document.” ), ‘https://wordpress.org/plugins/youtube-channel/installation/’ ); 873 } elseif ( ‘ipRefererBlocked’ == $json_output->error->errors[0]->reason ) { 874 // Restricted access YouTube Data API Key 875 $error_msg = 'Check <em>YouTube Data API Key</em> restrictions, empty cache if enabled by appending in the browser address bar parameter <em>?ytc_force_recache=1</em>’; 876 } elseif ( ‘invalidChannelId’ == $json_output->error->errors[0]->reason ) { 877 // (deprecated?) Non existing Channel ID set 878 $error_msg = sprintf( __( 'You have set wrong Channel ID. Fix that in General plugin settings, Widget and/or shortcode. Read <a href="%s” target="_blank">FAQ</a> document.’ ), ‘https://wordpress.org/plugins/youtube-channel/faq/’ ); 879 } elseif ( ‘playlistItemsNotAccessible’ == $json_output->error->errors[0]->reason ) { 880 // Forbidden access to resource 881 $error_msg = sprintf( __( "You do not have permission to access ressource <strong>%s</strong> (it’s maybe set to private or even does not exists!)" ), $resource_id ); 882 } else { 883 $error_msg = sprintf( 884 'Reason: %1$s; Domain: %2$s; Message: %3$s’, 885 $json_output->error->errors[0]->reason, 886 $json_output->error->errors[0]->domain, 887 $json_output->error->errors[0]->message 888 ); 889 } 890 } // END ! empty($json_output->error->errors) 891 892 $output .= $this->front_debug( $error_msg ); 893 894 } else { // ELSE if ($max_items == 0) 895 896 // looks that feed is OK, let we update fallback that never expire 897 set_transient( $cache_key_fallback, base64_encode( $json ), 0 ); 898 899 // and now free some memory 900 unset( $json, $json_output, $json_entry ); 901 902 // set array for unique random item 903 if ( ! empty( $instance[‘random’] ) ) { 904 $random_used = []; 905 } 906 907 /* AU:20141230 reduce number of videos if requested > available */ 908 if ( $num > sizeof( $items ) ) { 909 $num = sizeof( $items ); 910 } 911 912 for ( $y = 1; $y <= $num; ++$y ) { 913 if ( ! empty( $instance[‘random’] ) ) { 914 915 $random_item = mt_rand( 0, ( count( $items ) - 1 ) ); 916 while ( $y > 1 && in_array( $random_item, $random_used ) ) { 917 $random_item = mt_rand( 0, ( count( $items ) - 1 ) ); 918 } 919 $random_used[] = $random_item; 920 $item = $items[ $random_item ]; 921 } else { 922 $item = $items[ $y - 1 ]; 923 } 924 925 // Generate single video block 926 $video_block = $this->ytc_print_video( $item, $instance, $y ); 927 // Allow plugins/themes to override the default video block template. 928 $video_block = apply_filters( 'ytc_print_video’, $video_block, $item, $instance, $y ); 929 // Append video block to final output 930 $output .= $video_block; 931 } 932 // Free some memory 933 unset( $random_used, $random_item, $json ); 934 935 } // END if ($max_items == 0) 936 } // single playlist or ytc way 937 938 // Append link to channel on bootom of the widget 939 $output .= $this->ytc_footer( $instance ); 940 941 $output .= '</div><!-- .youtube_channel -->’; 942 943 // fix overflow on crappy themes 944 $output .= '<div class="clearfix"></div>’; 945 946 return $output; 947 948 } // END public function output($instance) 949 950 // — HELPER FUNCTIONS — 951 952 /** 953 * Download YouTube video feed through API 3.0 954 * @param string $id ID of resource 955 * @param integer $items Number of items to fetch (min 2, max 50) 956 * @return array JSON with videos 957 */ 958 function fetch_youtube_feed( $resource_id, $items ) { 959 960 $feed_url = 'https://www.googleapis.com/youtube/v3/playlistItems?’; 961 $feed_url .= 'part=snippet’; 962 $feed_url .= "&playlistId={$resource_id}"; 963 $feed_url .= '&fields=items(snippet(title%2Cdescription%2CpublishedAt%2CresourceId(videoId)))'; 964 $feed_url .= "&maxResults={$items}"; 965 $feed_url .= "&key={$this->defaults[‘apikey’]}"; 966 967 $wparg = [ 968 ‘timeout’ => $this->defaults[‘timeout’], 969 ‘sslverify’ => $this->defaults[‘sslverify’] ? true : false, 970 ‘headers’ => [ ‘referer’ => site_url() ], 971 ]; 972 973 $response = wp_remote_get( $feed_url, $wparg ); 974 975 // If we have WP error, make JSON with error 976 if ( is_wp_error( $response ) ) { 977 978 $json = sprintf( '{"error":{"errors":[{"reason":"wpError","message":"%s","domain":"wpRemoteGet"}]}}’, $response->get_error_message() ); 979 980 } else { 981 982 $json = wp_remote_retrieve_body( $response ); 983 984 } 985 986 // Free some memory 987 unset( $response ); 988 989 return $json; 990 991 } // END function fetch_youtube_feed($resource_id, $items) 992 993 /** 994 * Print explanation of error for administrators (users with capability manage_options) 995 * and hidden message for lower users and visitors 996 * @param string $message Error message 997 * @return string FOrmatted message for error 998 */ 999 function front_debug( $message ) { 1000 1001 // Show visible error to admin, Oops message to visitors and lower members 1002 if ( is_user_logged_in() && current_user_can( ‘manage_options’ ) ) { 1003 1004 $output = "<p class=\"ytc_error\"><strong>YTC ERROR:</strong> $message</p>"; 1005 1006 } else { 1007 1008 $output = __( 'Oops, something went wrong.’, ‘youtube-channel’ ); 1009 $output .= “<!-- YTC ERROR:\n"; 1010 $output .= strip_tags( $message ); 1011 $output .= “\n–>\n"; 1012 1013 } 1014 1015 return $output; 1016 1017 } // END function debug( $message ) 1018 1019 /** 1020 * Calculate height by provided width and aspect ratio 1021 * @param integer $width Width in pixels 1022 * @param integer $ratio Selected aspect ratio (1 for 4:3, other for 16:9) 1023 * @return integer Calculated height in pixels 1024 */ 1025 function height_ratio( $width = 306, $ratio = 2 ) { 1026 1027 switch ( $ratio ) { 1028 case 1: 1029 $height = round( ( $width / 4 ) * 3 ); 1030 break; 1031 default: 1032 $height = round( ( $width / 16 ) * 9 ); 1033 } 1034 return $height; 1035 } // END function height_ratio($width=306, $ratio) 1036 1037 /** 1038 * Generate link to YouTube channel/user 1039 * @param array $instance widget or shortcode settings 1040 * @return array components prepared for output 1041 */ 1042 function ytc_footer( $instance ) { 1043 1044 // Initialize output string 1045 $output = '’; 1046 1047 // Get link to channel part 1048 $output .= $this->ytc_channel_link( $instance ); 1049 1050 // Wrap content, if we have it 1051 if ( ! empty( $output ) ) { 1052 $output = $this->clearfix() . ‘<div class="ytc_link"><p>’ . $output . '</p></div>’; 1053 } 1054 1055 return $output; 1056 } // end function ytc_footer 1057 1058 function clearfix() { 1059 return '<div class="clearfix"></div>’; 1060 } 1061 /** 1062 * Generate link to YouTube channel/user 1063 * @param array $instance widget or shortcode settings 1064 * @return array components prepared for output 1065 */ 1066 function ytc_channel_link( $instance ) { 1067 1068 // Initialize output string 1069 $output = '’; 1070 1071 // do we need to show goto link? 1072 if ( ! empty( $instance[‘link_to’] ) && ‘none’ != $instance[‘link_to’] ) { 1073 1074 $goto_url = 'https://www.youtube.com/’; 1075 1076 switch ( $instance[‘link_to’] ) { 1077 case 'vanity’: 1078 $vanity = trim( $instance[‘vanity’] ); 1079 if ( empty( $vanity ) ) { 1080 if ( empty( $this->defaults[‘vanity’] ) ) { 1081 return '<!-- YTC ERROR: Selected Vanity custom URL to be linked but no Vanity Name provided! -->’; 1082 } 1083 // Get vanity from defaults if not set in instance 1084 $vanity = $this->defaults[‘vanity’]; 1085 } 1086 // sanity vanity content (strip all in front of last slash to cleanup vanity ID only) 1087 if ( ! empty( $vanity ) && false !== strpos( $vanity, ‘youtube.com’ ) ) { 1088 $vanity = preg_replace( '/^.*\//’, '’, $vanity ); 1089 } 1090 $goto_url .= “c/$vanity"; 1091 break; 1092 1093 case 'legacy’: 1094 $username = trim( $instance[‘username’] ); 1095 if ( empty( $username ) ) { 1096 if ( empty( $this->defaults[‘username’] ) ) { 1097 return '<!-- YTC ERROR: Selected Legacy username to be linked but no Legacy username provided! -->’; 1098 } 1099 $username = $this->defaults[‘username’]; 1100 } 1101 $goto_url .= “user/$username"; 1102 break; 1103 1104 case 'channel’: 1105 $channel = trim( $instance[‘channel’] ); 1106 if ( empty( $channel ) ) { 1107 if ( empty( $this->defaults[‘channel’] ) ) { 1108 return '<!-- YTC ERROR: Selected Channel page to be linked but no Channel ID provided! -->’; 1109 } 1110 $channel = $this->defaults[‘channel’]; 1111 } 1112 $goto_url .= “channel/$channel"; 1113 break; 1114 } 1115 1116 $goto_txt = trim( $instance[‘goto_txt’] ); 1117 if ( ‘’ == $goto_txt ) { 1118 $goto_txt = __( 'Visit our YouTube channel’, ‘youtube-channel’ ); 1119 } 1120 1121 $newtab = __( 'in new window/tab’, ‘youtube-channel’ ); 1122 1123 switch ( $instance[‘popup_goto’] ) { 1124 case 1: 1125 $output .= “<a href=\"javascript: window.open(‘{$goto_url}’); void 0;\” title=\"{$goto_txt} {$newtab}\">{$goto_txt}</a>"; 1126 break; 1127 case 2: 1128 $output .= “<a href=\"{$goto_url}\” target=\"_blank\” title=\"{$goto_txt} {$newtab}\">{$goto_txt}</a>"; 1129 break; 1130 default: 1131 $output .= “<a href=\"{$goto_url}\” title=\"{$goto_txt}\">$goto_txt</a>"; 1132 } // switch popup_goto 1133 1134 } // END if ( ! empty( $instance[‘link_to’] ) && ‘none’ != $instance[‘link_to’] ) 1135 1136 return $output; 1137 } // END function ytc_channel_link 1138 1139 1140 /** 1141 * Generate output for single video block 1142 * @param object $item Video object from JSON 1143 * @param array $instance Settings from widget or shortcode 1144 * @param int $y Order number of video 1145 * @return array Prepared single video block as array to concatenate 1146 */ 1147 function ytc_print_video( $item, $instance, $y ) { 1148 1149 // Start output string 1150 $output = '’; 1151 1152 // Calculate width and height 1153 if ( empty( $instance[‘width’] ) ) { 1154 $instance[‘width’] = $this->defaults[‘width’]; 1155 } 1156 if ( empty( $instance[‘ratio’] ) ) { 1157 $instance[‘ratio’] = $this->defaults[‘ratio’]; 1158 } 1159 $height = $this->height_ratio( $instance[‘width’], $instance[‘ratio’] ); 1160 1161 // How to display videos? 1162 if ( empty( $instance[‘display’] ) ) { 1163 $instance[‘display’] = 'thumbnail’; 1164 } 1165 1166 // Extract details about video from Resource 1167 $yt_id = $item->snippet->resourceId->videoId; 1168 $yt_title = $item->snippet->title; 1169 $yt_date = $item->snippet->publishedAt; 1170 1171 // Enhanced privacy? 1172 $youtube_domain = $this->youtube_domain( $instance ); 1173 1174 switch ( $y ) { 1175 case 1: 1176 $vnumclass = 'first’; 1177 break; 1178 case $instance[‘num’]: 1179 $autoplay = false; 1180 $vnumclass = 'last’; 1181 break; 1182 default: 1183 $vnumclass = 'mid’; 1184 $autoplay = false; 1185 break; 1186 } 1187 1188 // Set proper class for responsive thumbs per selected aspect ratio 1189 $arclass = $this->arclass( $instance ); 1190 1191 // Prepare title_tag (H3, etc) 1192 $title_tag = isset( $instance[‘titletag’] ) ? $instance[‘titletag’] : $this->defaults[‘titletag’]; 1193 1194 $output .= “<div class=\"ytc_video_container ytc_video_{$y} ytc_video_{$vnumclass} ${arclass}\” style=\"width:{$instance[‘width’]}px\">"; 1195 1196 // Show video title above video? 1197 if ( ! empty( $instance[‘showtitle’] ) ) { 1198 if ( 1199 // for non-thumbnail for `above` and `inside` 1200 ( ‘thumbnail’ != $instance[‘display’] && in_array( $instance[‘showtitle’], [ 'above’, ‘inside’ ] ) ) || 1201 // for thubmanil only if it’s `below` 1202 ( ‘thumbnail’ == $instance[‘display’] && ‘above’ == $instance[‘showtitle’] ) 1203 ) { 1204 if ( ! empty( $instance[‘linktitle’] ) ) { 1205 $output .= sprintf( 1206 '<%1$s class="ytc_title ytc_title_above"><a href="https://%3$s/watch/?v=%4$s” target="youtube">%2$s</a></%1$s>’, 1207 $title_tag, 1208 $yt_title, 1209 $youtube_domain, 1210 $yt_id 1211 ); 1212 } else { 1213 $output .= sprintf( 1214 '<%1$s class="ytc_title ytc_title_above">%2$s</%1$s>’, 1215 $title_tag, 1216 $yt_title 1217 ); 1218 } 1219 } 1220 } 1221 1222 // Print out video 1223 if ( ‘iframe’ == $instance[‘display’] ) { 1224 1225 // Start wrapper for responsive item 1226 if ( $instance[‘responsive’] ) { 1227 $output .= '<div class="fluid-width-video-wrapper">’; 1228 } 1229 1230 $output .= “<iframe title=\"YouTube Video Player\” width=\"{$instance[‘width’]}\” height=\"{$height}\” src=\"//{$youtube_domain}/embed/{$yt_id}?wmode=opaque"; 1231 1232 // disable related vides 1233 if ( ! empty( $instance[‘norel’] ) ) { 1234 $output .= '&rel=0’; 1235 } 1236 if ( ! empty( $instance[‘controls’] ) ) { 1237 $output .= '&controls=0’; 1238 } 1239 if ( ! empty( $instance[‘autoplay’] ) && 1 == $y ) { 1240 $output .= '&autoplay=1’; 1241 } 1242 if ( ! empty( $instance[‘hideanno’] ) ) { 1243 $output .= '&iv_load_policy=3’; 1244 } 1245 if ( ! empty( $instance[‘modestbranding’] ) ) { 1246 $output .= '&modestbranding=1’; 1247 } 1248 if ( ! empty( $instance[‘playsinline’] ) ) { 1249 $output .= '&playsinline=1’; 1250 } 1251 1252 $output .= “\” style=\"border:0;\” allowfullscreen id=\"ytc_{$yt_id}\"></iframe>"; 1253 1254 // Close wrapper for responsive item 1255 if ( $instance[‘responsive’] ) { 1256 $output .= '</div>’; 1257 } 1258 } elseif ( ‘iframe2’ == $instance[‘display’] ) { 1259 1260 // youtube API async 1261 $js_vars = '’; 1262 $js_vars .= ( ! empty( $instance[‘norel’] ) ) ? ‘rel:0,’ : '’; 1263 $js_vars .= ( ! empty( $instance[‘autoplay’] ) && 1 == $y ) ? ‘autoplay:1,’ : '’; 1264 $js_vars .= ( ! empty( $instance[‘controls’] ) ) ? ‘controls:0,’ : '’; 1265 $js_vars .= ( ! empty( $instance[‘modestbranding’] ) ) ? ‘modestbranding:1,’ : '’; 1266 $js_vars .= ( ! empty( $instance[‘playsinline’] ) ) ? ‘playsinline:1,’ : '’; 1267 $js_vars .= "wmmode:’opaque’"; 1268 $js_vars = rtrim( $js_vars, ‘,’ ); 1269 1270 $js_end = '’; 1271 $js_end .= ( ! empty( $instance[‘hideanno’] ) ) ? ‘iv_load_policy:3,’ : '’; 1272 $js_end .= ( ! empty( $instance[‘autoplay’] ) && ( 1 == $y ) && ! empty( $instance[‘autoplay_mute’] ) ) ? “events:{’onReady’:ytc_mute},” : '’; 1273 $js_end = rtrim( $js_end, ‘,’ ); 1274 1275 $js_player_id = str_replace( '-', '_’, $yt_id ); 1276 1277 // Start wrapper for responsive item 1278 if ( $instance[‘responsive’] ) { 1279 $output .= '<div class="fluid-width-video-wrapper">’; 1280 } 1281 1282 $output .= "<div id=\"ytc_player_{$js_player_id}\"></div>"; 1283 1284 // Close wrapper for responsive item 1285 if ( $instance[‘responsive’] ) { 1286 $output .= '</div>’; 1287 } 1288 1289 $site_domain = $_SERVER[‘HTTP_HOST’]; 1290 $ytc_html5_js = "var ytc_player_{$js_player_id};"; 1291 $ytc_html5_js .= “ytc_player_{$js_player_id}=new YT.Player('ytc_player_{$js_player_id}’,{height:’{$height}’,width:’{$instance[‘width’]}’,"; 1292 $ytc_html5_js .= “videoId:’{$yt_id}’,enablejsapi:1,playerVars:{{$js_vars}},origin:’{$site_domain}’,{$js_end}});"; 1293 1294 // prepare JS for footer 1295 if ( empty( $this->ytc_html5_js ) ) { 1296 $this->ytc_html5_js = $ytc_html5_js; 1297 } else { 1298 $this->ytc_html5_js .= $ytc_html5_js; 1299 } 1300 } else { // default is thumbnail 1301 1302 // Do we need tooltip for thumbnail? 1303 if ( empty( $instance[‘no_thumb_title’] ) ) { 1304 $title = sprintf( __( 'Watch video %1$s published on %2$s’, ‘youtube-channel’ ), $yt_title, $yt_date ); 1305 } 1306 1307 $p = '’; 1308 $target = '’; 1309 if ( empty( $instance[‘nolightbox’] ) ) { 1310 if ( ! empty( $instance[‘norel’] ) ) { 1311 $p .= '&rel=0’; 1312 } 1313 if ( ! empty( $instance[‘modestbranding’] ) ) { 1314 $p .= '&modestbranding=1’; 1315 } 1316 if ( ! empty( $instance[‘controls’] ) ) { 1317 $p .= '&controls=0’; 1318 } 1319 if ( ! empty( $instance[‘playsinline’] ) ) { 1320 $p .= '&playsinline=1’; 1321 } 1322 if ( ! empty( $instance[‘privacy’] ) ) { 1323 $p .= '&enhanceprivacy=1’; 1324 } 1325 $lightbox_class = 'ytc-lightbox’; 1326 } else { 1327 $lightbox_class = 'ytc-nolightbox’; 1328 if ( ! empty( $instance[‘target’] ) ) { 1329 $target = ‘target="’ . sanitize_key( $instance[‘target’] ) . '"’; 1330 } 1331 } 1332 1333 // Do we need thumbnail w/ or w/o tooltip 1334 $tag_title = ( empty( $instance[‘no_thumb_title’] ) ) ? $tag_title = “title=\"{$yt_title}\"” : '’; 1335 1336 // Define video thumbnail 1337 switch ( $instance[‘thumb_quality’] ) { 1338 case 'default’: 1339 case 'mqdefault’: 1340 case 'hqdefault’: 1341 case 'sddefault’: 1342 case 'maxresdefault’: 1343 $thumb_quality = $instance[‘thumb_quality’]; 1344 break; 1345 default: 1346 $thumb_quality = 'hqdefault’; 1347 } 1348 $yt_thumb = “//img.youtube.com/vi/${yt_id}/${thumb_quality}.jpg"; // zero for HD thumb 1349 1350 // Show video title inside video? 1351 $title_inside = '’; 1352 if ( ! empty( $instance[‘showtitle’] ) && in_array( $instance[‘showtitle’], [ 'inside’, ‘inside_b’ ] ) ) { 1353 $title_inside = sprintf( 1354 '<%1$s class="ytc_title ytc_title_inside %3$s">%2$s</%1$s>’, 1355 $title_tag, 1356 $yt_title, 1357 ‘inside_b’ == $instance[‘showtitle’] ? ‘ytc_title_inside_bottom’ : ‘’ 1358 ); 1359 } 1360 1361 $output .= “<a href=\"//www.youtube.com/watch?v=${yt_id}${p}\” ${tag_title} class=\"ytc_thumb ${lightbox_class} ${arclass}\” ${target}><span style=\"background-image: url(${yt_thumb});\” ${tag_title} id=\"ytc_{$yt_id}\">{$title_inside}</span></a>"; 1362 1363 } // what to show conditions 1364 1365 // Show video title below video? 1366 if ( ! empty( $instance[‘showtitle’] ) ) { 1367 if ( 1368 // for non-thumbnail for `below` and `inside_b` 1369 ( ‘thumbnail’ != $instance[‘display’] && in_array( $instance[‘showtitle’], [ 'below’, ‘inside_b’ ] ) ) || 1370 // for thubmanil only if it’s `below` 1371 ( ‘thumbnail’ == $instance[‘display’] && ‘below’ == $instance[‘showtitle’] ) 1372 ) { 1373 if ( ! empty( $instance[‘linktitle’] ) ) { 1374 1375 $output .= sprintf( 1376 '<%1$s class="ytc_title ytc_title_below"><a href="https://%3$s/watch/?v=%4$s” target="youtube">%2$s</a></%1$s>’, 1377 $title_tag, 1378 $yt_title, 1379 $youtube_domain, 1380 $yt_id 1381 ); 1382 } else { 1383 $output .= sprintf( 1384 '<%1$s class="ytc_title ytc_title_below">%2$s</%1$s>’, 1385 $title_tag, 1386 $yt_title 1387 ); 1388 } 1389 } 1390 } 1391 1392 // do we need to show video description? 1393 if ( ! empty( $instance[‘showdesc’] ) ) { 1394 1395 $video_description = $item->snippet->description; 1396 $etcetera = '’; 1397 ##TODO: If description should note be shortened, print HTML formatted desc 1398 if ( $instance[‘desclen’] > 0 ) { 1399 if ( function_exists( ‘mb_strlen’ ) && function_exists( ‘mb_substr’ ) ) { 1400 if ( mb_strlen( $video_description ) > $instance[‘desclen’] ) { 1401 $video_description = mb_substr( $video_description, 0, $instance[‘desclen’] ); 1402 $etcetera = '…’; 1403 } 1404 } else { 1405 if ( strlen( $video_description ) > $instance[‘desclen’] ) { 1406 $video_description = substr( $video_description, 0, $instance[‘desclen’] ); 1407 $etcetera = '…’; 1408 } 1409 } 1410 } 1411 1412 if ( ! empty( $video_description ) ) { 1413 $output .= “<p class=\"ytc_description\">{$video_description}{$etcetera}</p>"; 1414 } 1415 } 1416 1417 $output .= '</div><!-- .ytc_video_container -->’; 1418 1419 return $output; 1420 } // end function ytc_print_video 1421 1422 /* function to print standard playlist embed code */ 1423 function embed_playlist( $resource_id, $instance ) { 1424 1425 $width = empty( $instance[‘width’] ) ? 306 : $instance[‘width’]; 1426 $height = self::height_ratio( $width, $instance[‘ratio’] ); 1427 $autoplay = empty( $instance[‘autoplay’] ) ? ‘’ : '&autoplay=1’; 1428 $modestbranding = empty( $instance[‘modestbranding’] ) ? ‘’ : '&modestbranding=1’; 1429 $rel = empty( $instance[‘norel’] ) ? ‘’ : '&rel=0’; 1430 $playsinline = empty( $instance[‘playsinline’] ) ? ‘’ : '&playsinline=1’; 1431 1432 // enhanced privacy 1433 $youtube_domain = $this->youtube_domain( $instance ); 1434 $arclass = $this->arclass( $instance ); 1435 1436 // Start output string 1437 $output = '’; 1438 1439 $output .= “<div class=\"ytc_video_container ytc_video_1 ytc_video_single ytc_playlist_only {$arclass}\">"; 1440 $output .= '<div class="fluid-width-video-wrapper">’; 1441 $output .= “<iframe src=\"//{$youtube_domain}/embed/videoseries?list={$resource_id}{$autoplay}{$modestbranding}{$rel}\""; 1442 if ( ! empty( $instance[‘fullscreen’] ) ) { 1443 $output .= ' allowfullscreen’; 1444 } 1445 $output .= " width=\"{$width}\” height=\"{$height}\” frameborder=\"0\"></iframe>"; 1446 $output .= '</div><!-- .fluid-width-video-wrapper -->’; 1447 $output .= '</div><!-- .ytc_video_container -->’; 1448 1449 return $output; 1450 1451 } // END function embed_playlist($resource_id, $instance) 1452 1453 // Helper function cache_time() 1454 function cache_time( $cache_time ) { 1455 1456 $out = '’; 1457 $times = self::cache_times_arr(); 1458 foreach ( $times as $sec => $title ) { 1459 $out .= ‘<option value="’ . $sec . '” ' . selected( $cache_time, $sec, 0 ) . ‘>’ . $title . '</option>’; 1460 unset( $sec ); 1461 } 1462 1463 return $out; 1464 1465 } // END function cache_time() 1466 1467 /** 1468 * Define cache times array 1469 */ 1470 public static function cache_times_arr() { 1471 1472 return [ 1473 ‘0’ => __( 'Do not cache’, ‘youtube-channel’ ), 1474 ‘60’ => __( '1 minute’, ‘youtube-channel’ ), 1475 ‘300’ => __( '5 minutes’, ‘youtube-channel’ ), 1476 ‘900’ => __( '15 minutes’, ‘youtube-channel’ ), 1477 ‘1800’ => __( '30 minutes’, ‘youtube-channel’ ), 1478 ‘3600’ => __( '1 hour’, ‘youtube-channel’ ), 1479 ‘7200’ => __( '2 hours’, ‘youtube-channel’ ), 1480 ‘18000’ => __( '5 hours’, ‘youtube-channel’ ), 1481 ‘36000’ => __( '10 hours’, ‘youtube-channel’ ), 1482 ‘43200’ => __( '12 hours’, ‘youtube-channel’ ), 1483 ‘64800’ => __( '18 hours’, ‘youtube-channel’ ), 1484 ‘86400’ => __( '1 day’, ‘youtube-channel’ ), 1485 ‘172800’ => __( '2 days’, ‘youtube-channel’ ), 1486 ‘259200’ => __( '3 days’, ‘youtube-channel’ ), 1487 ‘345600’ => __( '4 days’, ‘youtube-channel’ ), 1488 ‘432000’ => __( '5 days’, ‘youtube-channel’ ), 1489 ‘518400’ => __( '6 days’, ‘youtube-channel’ ), 1490 ‘604800’ => __( '1 week’, ‘youtube-channel’ ), 1491 ‘1209600’ => __( '2 weeks’, ‘youtube-channel’ ), 1492 ‘1814400’ => __( '3 weeks’, ‘youtube-channel’ ), 1493 ‘2419200’ => __( '1 month’, ‘youtube-channel’ ), 1494 ]; 1495 1496 } // END public static function cache_times_arr() 1497 1498 /** 1499 * Method to delete all YTC transient caches 1500 * @return string Report message about success or failed purge cache 1501 */ 1502 function clear_all_cache() { 1503 1504 global $wpdb; 1505 1506 $ret = $wpdb->query( 1507 $wpdb->prepare( 1508 "DELETE FROM $wpdb->options 1509 WHERE option_name LIKE %s 1510 OR option_name LIKE %s 1511 ", 1512 '_transient_timeout_ytc_%’, 1513 ‘_transient_ytc_%’ 1514 ) 1515 ); 1516 1517 if ( false === $ret ) { 1518 echo 'Oops, we did not cleared any YouTube Channel cache because some error occured’; 1519 } else { 1520 if ( 0 == $ret ) { 1521 echo 'Congratulations! You can chill, there is no YouTube Channel caches.’; 1522 } else { 1523 echo "Success! We cleared $ret row/s with YouTube Channel caches."; 1524 } 1525 } 1526 exit(); 1527 1528 } // END function clear_all_cache() 1529 1530 /** 1531 * Return nice name for resource by provided resource ID 1532 * @param integer $resource_id Resource ID 1533 * @return string Resource nice name 1534 */ 1535 function resource_nice_name( $resource_id ) { 1536 if ( 0 == $resource_id ) { 1537 $resource_nice_name = 'Channel (User uploads)'; 1538 } elseif ( 1 == $resource_id ) { 1539 $resource_nice_name = 'Favourited videos’; 1540 } elseif ( 2 == $resource_id ) { 1541 $resource_nice_name = 'Liked videos’; 1542 } elseif ( 3 == $resource_id ) { 1543 $resource_nice_name = 'Liked videos’; 1544 } else { 1545 $resource_nice_name = 'Unknown resource’; 1546 } 1547 return $resource_nice_name; 1548 } // END function resource_nice_name( $resource_id ) 1549 1550 function youtube_domain( $instance ) { 1551 return empty( $instance[‘privacy’] ) ? ‘www.youtube.com’ : 'www.youtube-nocookie.com’; 1552 } // END function youtube_domain 1553 1554 function arclass( $instance ) { 1555 return ! empty( $instance[‘ratio’] ) && 1 == $instance[‘ratio’] ? ‘ar4_3’ : 'ar16_9’; 1556 } // END function arclass() 1557 1558 /** 1559 * Register TinyMCE button for YTC 1560 * @param array $plugins Unmodified set of plugins 1561 * @return array Set of TinyMCE plugins with YTC addition 1562 */ 1563 function mce_external_plugins( $plugins ) { 1564 1565 $plugins[‘youtube_channel’] = plugin_dir_url( __FILE__ ) . 'inc/tinymce/plugin.min.js’; 1566 1567 return $plugins; 1568 1569 } // END function mce_external_plugins() 1570 1571 /** 1572 * Append TinyMCE button for YTC at the end of row 1 1573 * @param array $buttons Unmodified set of buttons 1574 * @return array Set of TinyMCE buttons with YTC addition 1575 */ 1576 function mce_buttons( $buttons ) { 1577 1578 $buttons[] = 'youtube_channel_shortcode’; 1579 return $buttons; 1580 1581 } // END function mce_buttons() 1582 1583 function generate_debug_json() { 1584 global $wp_version; 1585 1586 // get widget ID from parameter 1587 $for = trim( $_GET[‘ytc_debug_json_for’] ); 1588 1589 if ( ‘global’ == $for ) { 1590 // global settings 1591 $options = get_option( ‘youtube_channel_defaults’ ); 1592 1593 if ( ! is_array( $options ) ) { 1594 return; 1595 } 1596 1597 // Remove YouTube Data API Key from config JSON 1598 unset( $options[‘apikey’] ); 1599 1600 } else { 1601 // widget 1602 $widget_id = (int) $for; 1603 $for = "youtube-channel-{$for}"; 1604 1605 // get YTC widgets options 1606 $widget_options = get_option( ‘widget_youtube-channel’ ); 1607 1608 if ( ! is_array( $widget_options[ $widget_id ] ) ) { 1609 return; 1610 } 1611 1612 $options = $widget_options[ $widget_id ]; 1613 unset( $widget_options ); 1614 } 1615 1616 // prepare debug data with settings of current widget 1617 $data = array_merge( 1618 [ 1619 ‘date’ => date( ‘r’ ), 1620 ‘server’ => $_SERVER[‘SERVER_SOFTWARE’], 1621 ‘php’ => PHP_VERSION, 1622 ‘wp’ => $wp_version, 1623 ‘ytc’ => self::VER, 1624 ‘url’ => get_site_url(), 1625 ‘for’ => $for, 1626 ], 1627 $options 1628 ); 1629 1630 // Construct descriptive filename 1631 $date = date( ‘ymdHis’ ); 1632 $json_filename = "ytc3_{$_SERVER[‘HTTP_HOST’]}_{$for}_{$date}.json"; 1633 // Return JSON file 1634 header( “Content-disposition: attachment; filename={$json_filename}” ); 1635 header( ‘Content-Type: application/json’ ); 1636 echo json_encode( $data ); 1637 1638 // Destroy vars 1639 unset( $data, $options, $widget_id, $option_name, $for, $date, $json_filename ); 1640 1641 // Exit now, because we need only debug data in JSON file, not settings or any other page 1642 exit; 1643 } // End function generate_debug_json() 1644 1645 } // End class 1646 } // End class check 30require_once YTC_DIR_INC . '/helper.php’; 31require_once YTC_DIR_CLASSES . '/class-wpau-youtube-channel.php’;