Admin
4 months ago
Builder
4 months ago
Helpers
4 months ago
Integrations
4 months ago
CFF_Autolink.php
4 months ago
CFF_Blocks.php
4 months ago
CFF_Cache.php
4 months ago
CFF_Education.php
4 months ago
CFF_Elementor_Base.php
4 months ago
CFF_Elementor_Widget.php
4 months ago
CFF_Error_Reporter.php
4 months ago
CFF_FB_Settings.php
4 months ago
CFF_Feed_Elementor_Control.php
4 months ago
CFF_Feed_Locator.php
4 months ago
CFF_Feed_Pro.php
4 months ago
CFF_GDPR_Integrations.php
4 months ago
CFF_Group_Posts.php
4 months ago
CFF_HTTP_Request.php
4 months ago
CFF_Oembed.php
4 months ago
CFF_Parse.php
4 months ago
CFF_Resizer.php
4 months ago
CFF_Response.php
4 months ago
CFF_Shortcode.php
4 months ago
CFF_Shortcode_Display.php
4 months ago
CFF_SiteHealth.php
4 months ago
CFF_Utils.php
4 months ago
CFF_View.php
4 months ago
Custom_Facebook_Feed.php
4 months ago
Email_Notification.php
4 months ago
Platform_Data.php
4 months ago
SB_Facebook_Data_Encryption.php
4 months ago
SB_Facebook_Data_Manager.php
4 months ago
index.php
4 months ago
CFF_Oembed.php
517 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Class CFF_Oembed |
| 5 | * |
| 6 | * Replaces the native WordPress functionality for Facebook oembed |
| 7 | * to allow authenticated oembeds |
| 8 | * |
| 9 | * @since 2.16/3.16 |
| 10 | */ |
| 11 | |
| 12 | namespace CustomFacebookFeed; |
| 13 | |
| 14 | if (! defined('ABSPATH')) { |
| 15 | die('-1'); |
| 16 | } |
| 17 | |
| 18 | class CFF_Oembed |
| 19 | { |
| 20 | /** |
| 21 | * CFF_Oembed constructor. |
| 22 | * |
| 23 | * If an account has been connected, hooks are added |
| 24 | * to change how Facebook links are handled for oembeds |
| 25 | * |
| 26 | * @since 2.16/3.16 |
| 27 | */ |
| 28 | public function __construct() |
| 29 | { |
| 30 | if (CFF_Oembed::can_do_oembed()) { |
| 31 | if (CFF_Oembed::can_check_for_old_oembeds()) { |
| 32 | add_action('init', array('CustomFacebookFeed\CFF_Oembed', 'clear_checks')); |
| 33 | add_action('admin_init', array($this, 'cffOembedNotice')); |
| 34 | } |
| 35 | add_filter('oembed_providers', array( 'CustomFacebookFeed\CFF_Oembed', 'oembed_providers' ), 10, 1); |
| 36 | add_filter('oembed_fetch_url', array( 'CustomFacebookFeed\CFF_Oembed', 'oembed_set_fetch_url' ), 10, 3); |
| 37 | add_filter('oembed_result', array( 'CustomFacebookFeed\CFF_Oembed', 'oembed_result' ), 10, 3); |
| 38 | } |
| 39 | if (CFF_Oembed::should_extend_ttl()) { |
| 40 | add_filter('oembed_ttl', array( 'CustomFacebookFeed\CFF_Oembed', 'oembed_ttl' ), 10, 4); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /** |
| 45 | * Check to make sure there is a saved access token to |
| 46 | * enable authenticated oembeds |
| 47 | * |
| 48 | * @return bool |
| 49 | * |
| 50 | * @since 2.16/3.16 |
| 51 | */ |
| 52 | public static function can_do_oembed() |
| 53 | { |
| 54 | $oembed_token_settings = get_option('cff_oembed_token', array()); |
| 55 | |
| 56 | if (isset($oembed_token_settings['disabled']) && $oembed_token_settings['disabled'] === true) { |
| 57 | return false; |
| 58 | } |
| 59 | |
| 60 | $access_token = CFF_Oembed::last_access_token(); |
| 61 | if (! $access_token) { |
| 62 | return false; |
| 63 | } |
| 64 | |
| 65 | return true; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * The "time to live" for Instagram oEmbeds is extended if the access token expires. |
| 70 | * Even if new oEmbeds will not use the Instagram Feed system due to an expired token |
| 71 | * the time to live should continue to be extended. |
| 72 | * |
| 73 | * @return bool |
| 74 | * |
| 75 | * @since 2.16/3.16 |
| 76 | */ |
| 77 | public static function should_extend_ttl() |
| 78 | { |
| 79 | $oembed_token_settings = get_option('cff_oembed_token', array()); |
| 80 | |
| 81 | if (isset($oembed_token_settings['disabled']) && $oembed_token_settings['disabled']) { |
| 82 | return false; |
| 83 | } |
| 84 | |
| 85 | $will_expire = CFF_Oembed::oembed_access_token_will_expire(); |
| 86 | if ($will_expire) { |
| 87 | return true; |
| 88 | } |
| 89 | |
| 90 | return false; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Checking for old oembeds makes permanent changes to posts |
| 95 | * so we want the user to turn it off and on |
| 96 | * |
| 97 | * @return bool |
| 98 | * |
| 99 | * @since 2.16/3.16 |
| 100 | */ |
| 101 | public static function can_check_for_old_oembeds() |
| 102 | { |
| 103 | $cff_statuses = get_option('cff_statuses', array()); |
| 104 | if (isset($cff_statuses['oembed_api_change_notice'])) { |
| 105 | return false; |
| 106 | } |
| 107 | return true; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Filters the WordPress list of oembed providers to |
| 112 | * change what url is used for remote requests for the |
| 113 | * oembed data |
| 114 | * |
| 115 | * @param array $providers |
| 116 | * |
| 117 | * @return mixed |
| 118 | * |
| 119 | * @since 2.16/3.16 |
| 120 | */ |
| 121 | public static function oembed_providers($providers) |
| 122 | { |
| 123 | $oembed_url = CFF_Oembed::oembed_url(); |
| 124 | if ($oembed_url) { |
| 125 | $post_embed_providers = CFF_Oembed::post_providers(); |
| 126 | foreach ($post_embed_providers as $post_provider) { |
| 127 | $providers[ $post_provider ] = array( $oembed_url . 'oembed_post', true ); |
| 128 | } |
| 129 | |
| 130 | $video_embed_providers = CFF_Oembed::video_providers(); |
| 131 | foreach ($video_embed_providers as $video_provider) { |
| 132 | $providers[ $video_provider ] = array( $oembed_url . 'oembed_video', true ); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | return $providers; |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Add the access token from a connected account to make an authenticated |
| 141 | * call to get oembed data from Facebook |
| 142 | * |
| 143 | * @param string $provider |
| 144 | * @param string $url |
| 145 | * @param array $args |
| 146 | * |
| 147 | * @return string |
| 148 | * |
| 149 | * @since 2.16/3.16 |
| 150 | */ |
| 151 | public static function oembed_set_fetch_url($provider, $url, $args) |
| 152 | { |
| 153 | $access_token = CFF_Oembed::last_access_token(); |
| 154 | if (! $access_token) { |
| 155 | return $provider; |
| 156 | } |
| 157 | |
| 158 | if ( |
| 159 | strpos($provider, 'oembed_post') !== false |
| 160 | || strpos($provider, 'oembed_video') !== false |
| 161 | ) { |
| 162 | if (strpos($url, '?') !== false) { |
| 163 | $provider = self::get_provider_from_url_with_query_vars($provider, $url); |
| 164 | } |
| 165 | $provider = add_query_arg('access_token', $access_token, $provider); |
| 166 | } |
| 167 | |
| 168 | return $provider; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * URLs with query variables are handled specially |
| 173 | * |
| 174 | * @param $provider |
| 175 | * @param $url |
| 176 | * |
| 177 | * @return array|mixed|string|string[] |
| 178 | */ |
| 179 | public static function get_provider_from_url_with_query_vars($provider, $url) |
| 180 | { |
| 181 | $exploded = explode('?', $url); |
| 182 | if (! empty($exploded[1])) { |
| 183 | if (strpos($url, '?v=') !== false) { |
| 184 | $exploded = explode('&', $url); |
| 185 | $provider = str_replace(urlencode('&' . $exploded[1]), '', $provider); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | return $provider; |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * New oembeds are wrapped in a div for easy detection of older oembeds |
| 194 | * that will need to be updated |
| 195 | * |
| 196 | * @param string $html |
| 197 | * @param string $url |
| 198 | * @param array $args |
| 199 | * |
| 200 | * @return string |
| 201 | * |
| 202 | * @since 2.16/3.16 |
| 203 | */ |
| 204 | public static function oembed_result($html, $url, $args) |
| 205 | { |
| 206 | $post_embed_providers = CFF_Oembed::post_providers(); |
| 207 | foreach ($post_embed_providers as $post_provider) { |
| 208 | if (preg_match($post_provider, $url) === 1) { |
| 209 | if (strpos($html, 'class="fb-post"') !== false) { |
| 210 | $html = '<div class="cff-embed-wrap cff-post-embed-wrap">' . str_replace('class="fb-post"', 'class="fb-post cff-embed cff-post-embed"', $html) . '</div>'; |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | $video_embed_providers = CFF_Oembed::video_providers(); |
| 216 | foreach ($video_embed_providers as $video_provider) { |
| 217 | if (preg_match($video_provider, $url) === 1) { |
| 218 | if (strpos($html, 'class="fb-video"') !== false) { |
| 219 | $html = '<div class="cff-embed-wrap cff-video-embed-wrap">' . str_replace('class="fb-video"', 'class="fb-video cff-embed cff-video-embed"', $html) . '</div>'; |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | return $html; |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Extend the "time to live" for oEmbeds created with access tokens that expire |
| 229 | * |
| 230 | * @param $ttl |
| 231 | * @param $url |
| 232 | * @param $attr |
| 233 | * @param $post_ID |
| 234 | * |
| 235 | * @return float|int |
| 236 | * |
| 237 | * @since 2.16/3.16 |
| 238 | */ |
| 239 | public static function oembed_ttl($ttl, $url, $attr, $post_ID) |
| 240 | { |
| 241 | $providers = CFF_Oembed::post_providers(); |
| 242 | foreach ($providers as $provider) { |
| 243 | if (preg_match($provider, $url) === 1) { |
| 244 | $ttl = 30 * YEAR_IN_SECONDS; |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | $providers = CFF_Oembed::video_providers(); |
| 249 | foreach ($providers as $provider) { |
| 250 | if (preg_match($provider, $url) === 1) { |
| 251 | $ttl = 30 * YEAR_IN_SECONDS; |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | return $ttl; |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Only one api URL for FB |
| 260 | * |
| 261 | * @return bool|string |
| 262 | * |
| 263 | * @since 2.16/3.16 |
| 264 | */ |
| 265 | public static function oembed_url() |
| 266 | { |
| 267 | return 'https://graph.facebook.com/'; |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Any access token will work for oembeds so the access token |
| 272 | * saved in settings is used |
| 273 | * |
| 274 | * @return bool|string |
| 275 | * |
| 276 | * @since 2.16/3.16 |
| 277 | */ |
| 278 | public static function last_access_token() |
| 279 | { |
| 280 | $oembed_token_settings = get_option('cff_oembed_token', array()); |
| 281 | $will_expire = CFF_Oembed::oembed_access_token_will_expire(); |
| 282 | $encryption = new \CustomFacebookFeed\SB_Facebook_Data_Encryption(); |
| 283 | |
| 284 | if ( |
| 285 | ! empty($oembed_token_settings['access_token']) |
| 286 | && (! $will_expire || $will_expire > time()) |
| 287 | ) { |
| 288 | $oembed_token_settings['access_token'] = $encryption->maybe_decrypt($oembed_token_settings['access_token']); |
| 289 | return $oembed_token_settings['access_token']; |
| 290 | } else { |
| 291 | $settings_access_token = trim(get_option('cff_access_token')); |
| 292 | $settings_access_token = $encryption->maybe_decrypt($settings_access_token); |
| 293 | if (! empty($settings_access_token)) { |
| 294 | return $settings_access_token; |
| 295 | } |
| 296 | |
| 297 | if (class_exists('SB_Instagram_Oembed')) { |
| 298 | $sbi_oembed_token_settings = get_option('sbi_oembed_token', array()); |
| 299 | if (! empty($sbi_oembed_token_settings['access_token'])) { |
| 300 | $sbi_oembed_token_settings['access_token'] = $encryption->maybe_decrypt($sbi_oembed_token_settings['access_token']); |
| 301 | return $sbi_oembed_token_settings['access_token']; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | return false; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Access tokens created from FB accounts not connected to an |
| 311 | * FB page expire after 60 days. |
| 312 | * |
| 313 | * @return bool|int |
| 314 | */ |
| 315 | public static function oembed_access_token_will_expire() |
| 316 | { |
| 317 | $oembed_token_settings = get_option('cff_oembed_token', array()); |
| 318 | $will_expire = isset($oembed_token_settings['expiration_date']) && (int)$oembed_token_settings['expiration_date'] > 0 ? (int)$oembed_token_settings['expiration_date'] : false; |
| 319 | |
| 320 | return $will_expire; |
| 321 | } |
| 322 | |
| 323 | |
| 324 | /** |
| 325 | * Loop through post meta data and if it's an oembed and has content |
| 326 | * that looks like a Facebook oembed, delete it |
| 327 | * |
| 328 | * @param $post_ID |
| 329 | * |
| 330 | * @return int number of old oembed caches found |
| 331 | * |
| 332 | * @since 2.16/3.16 |
| 333 | */ |
| 334 | public static function delete_facebook_oembed_caches($post_ID) |
| 335 | { |
| 336 | $post_metas = get_post_meta($post_ID); |
| 337 | if (empty($post_metas)) { |
| 338 | return 0; |
| 339 | } |
| 340 | |
| 341 | $total_found = 0; |
| 342 | foreach ($post_metas as $post_meta_key => $post_meta_value) { |
| 343 | if ('_oembed_' === substr($post_meta_key, 0, 8)) { |
| 344 | if ( |
| 345 | strpos($post_meta_value[0], 'class="fb-post"') !== false |
| 346 | && strpos($post_meta_value[0], 'cff-embed-wrap') === false |
| 347 | ) { |
| 348 | $total_found++; |
| 349 | delete_post_meta($post_ID, $post_meta_key); |
| 350 | if ('_oembed_time_' !== substr($post_meta_key, 0, 13)) { |
| 351 | delete_post_meta($post_ID, str_replace('_oembed_', '_oembed_time_', $post_meta_key)); |
| 352 | } |
| 353 | } elseif ( |
| 354 | strpos($post_meta_value[0], 'class="fb-video"') !== false |
| 355 | && strpos($post_meta_value[0], 'cff-embed-wrap') === false |
| 356 | ) { |
| 357 | $total_found++; |
| 358 | delete_post_meta($post_ID, $post_meta_key); |
| 359 | if ('_oembed_time_' !== substr($post_meta_key, 0, 13)) { |
| 360 | delete_post_meta($post_ID, str_replace('_oembed_', '_oembed_time_', $post_meta_key)); |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | return $total_found; |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Current list of regex to identify FB URLs that could become oembeds using |
| 371 | * the 'oembed_post' endpoint. |
| 372 | * |
| 373 | * @return array |
| 374 | * |
| 375 | * @since 2.16/3.16 |
| 376 | */ |
| 377 | public static function post_providers() |
| 378 | { |
| 379 | $post_embed_providers = array( |
| 380 | '#https?://www\.facebook\.com/.*/posts/.*#i', |
| 381 | '#https?://www\.facebook\.com/.*/activity/.*#i', |
| 382 | '#https?://www\.facebook\.com/.*/photos/.*#i', |
| 383 | '#https?://www\.facebook\.com/photo(s/|\.php).*#i', |
| 384 | '#https?://www\.facebook\.com/permalink\.php.*#i', |
| 385 | '#https?://www\.facebook\.com/media/.*#i', |
| 386 | '#https?://www\.facebook\.com/questions/.*#i', |
| 387 | '#https?://www\.facebook\.com/notes/.*#i', |
| 388 | ); |
| 389 | |
| 390 | return $post_embed_providers; |
| 391 | } |
| 392 | |
| 393 | /** |
| 394 | * Current list of regex to identify FB URLs that could become oembeds using |
| 395 | * the 'oembed_video' endpoint. |
| 396 | * |
| 397 | * @return array |
| 398 | * |
| 399 | * @since 2.16/3.16 |
| 400 | */ |
| 401 | public static function video_providers() |
| 402 | { |
| 403 | $video_embed_providers = array( |
| 404 | '#https?://www\.facebook\.com/.*/videos/.*#i', |
| 405 | '#https?://www\.facebook\.com/video\.php.*#i', |
| 406 | '#https?://www\.facebook\.com/watch/.*#i', |
| 407 | '#https?://fb\.watch/.*#i' |
| 408 | ); |
| 409 | |
| 410 | return $video_embed_providers; |
| 411 | } |
| 412 | |
| 413 | /** |
| 414 | * Used for clearing the oembed update check flag for all posts |
| 415 | * |
| 416 | * @since 2.16/3.16 |
| 417 | */ |
| 418 | public static function clear_checks() |
| 419 | { |
| 420 | global $wpdb; |
| 421 | $table_name = esc_sql($wpdb->prefix . "postmeta"); |
| 422 | $result = $wpdb->query(" |
| 423 | DELETE |
| 424 | FROM $table_name |
| 425 | WHERE meta_key = '_cff_oembed_done_checking';"); |
| 426 | } |
| 427 | |
| 428 | /** |
| 429 | * Display oembed notice in the plugin's pages |
| 430 | * |
| 431 | * @since 6.3.7 |
| 432 | */ |
| 433 | public function cffOembedNotice() |
| 434 | { |
| 435 | $allowed_screens = array( |
| 436 | 'cff-feed-builder', |
| 437 | 'cff-settings', |
| 438 | 'cff-oembeds-manager', |
| 439 | 'cff-extensions-manager', |
| 440 | 'cff-about-us', |
| 441 | 'cff-support', |
| 442 | ); |
| 443 | $current_screen = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; |
| 444 | $is_allowed = in_array($current_screen, $allowed_screens); |
| 445 | |
| 446 | // We will display the notice only on those allowed screens. |
| 447 | if (!$current_screen || ! $is_allowed) { |
| 448 | return; |
| 449 | } |
| 450 | |
| 451 | // Only display notice to admins. |
| 452 | $cap = current_user_can('manage_custom_facebook_feed_options') ? 'manage_custom_facebook_feed_options' : 'manage_options'; |
| 453 | $cap = apply_filters('cff_settings_pages_capability', $cap); |
| 454 | if (!current_user_can($cap)) { |
| 455 | return; |
| 456 | } |
| 457 | |
| 458 | $cff_statuses = get_option('cff_statuses', array()); |
| 459 | if (isset($cff_statuses['oembed_api_change_notice'])) { |
| 460 | return; |
| 461 | } |
| 462 | |
| 463 | global $cff_notices; |
| 464 | $title = __('Account reconnection needed for Facebook and Instagram oEmbeds', 'custom-facebook-feed'); |
| 465 | $message = '<p>' . __('Starting May of 2024, Facebook is making some changes to their API that will affect your oEmbeds. Make sure to connect to our oEmbed specific Smash Balloon Tools app to avoid disruption.', 'custom-facebook-feed') . '</p>'; |
| 466 | |
| 467 | $error_args = array( |
| 468 | 'class' => 'cff-admin-notices', |
| 469 | 'title' => array( |
| 470 | 'text' => $title, |
| 471 | 'class' => 'sb-notice-title', |
| 472 | 'tag' => 'h4', |
| 473 | ), |
| 474 | 'message' => $message, |
| 475 | 'buttons' => array( |
| 476 | array( |
| 477 | 'text' => __('Reconnect', 'custom-facebook-feed'), |
| 478 | 'class' => 'sb-btn sb-reconnect-oembed', |
| 479 | 'tag' => 'button', |
| 480 | ), |
| 481 | ), |
| 482 | 'buttons_wrap_start' => '<div class="buttons">', |
| 483 | 'buttons_wrap_end' => '</div>', |
| 484 | 'priority' => 1, |
| 485 | 'page' => array( |
| 486 | 'cff-feed-builder', |
| 487 | 'cff-settings', |
| 488 | 'cff-oembeds-manager', |
| 489 | 'cff-extensions-manager', |
| 490 | 'cff-about-us', |
| 491 | 'cff-support', |
| 492 | ), |
| 493 | 'icon' => array( |
| 494 | 'src' => CFF_PLUGIN_URL . 'admin/assets/img/cff-exclamation.svg', |
| 495 | 'wrap' => '<span class="sb-notice-icon sb-error-icon"><img {src}></span>', |
| 496 | ), |
| 497 | 'styles' => array( |
| 498 | 'display' => 'flex', |
| 499 | 'justify-content' => 'space-between', |
| 500 | 'gap' => '2rem', |
| 501 | ), |
| 502 | 'wrap_schema' => '<div {id} {class}>{icon}<div class="cff-notice-wrap" {styles}><div class="cff-notice-body">{title}{message}</div>{buttons}</div></div>', |
| 503 | ); |
| 504 | |
| 505 | $cff_notices->add_notice('oembed_api_change', 'information', $error_args); |
| 506 | $cff_statuses['oembed_api_change_notice'] = true; |
| 507 | update_option('cff_statuses', $cff_statuses); |
| 508 | } |
| 509 | } |
| 510 | |
| 511 | /* |
| 512 | function cffOembedInit() { |
| 513 | return new CFF_Oembed(); |
| 514 | } |
| 515 | cffOembedInit(); |
| 516 | */ |
| 517 |