* @copyright Copyright (C) 2020 WPDeveloper. All rights reserved. * @license GPLv3 or later * @since 1.0.0 */ class Shortcode { /** * The WP_oEmbed class instance. * * @since 1.0.0 * @access private * @static * * @var WP_oEmbed $oEmbedInstance */ private static $oEmbedInstance = null; /** * The DefaultProviderCollection class instance. * * @since 1.0.0 * @access private * @static * * @var DefaultProviderCollection $collection */ private static $collection = null; /** * Register the plugin's shortcode into WordPress. * * @return void * @since 1.0.0 * @static * */ public static function register() { // Register the new shortcode for embeds. add_shortcode( EMBEDPRESS_SHORTCODE, ['\\EmbedPress\\Shortcode', 'do_shortcode'] ); add_shortcode( 'embed_oembed_html', ['\\EmbedPress\\Shortcode', 'do_shortcode'] ); } /** * Method that converts the plugin shortcoded-string into its complex content. * * @param array $attributes Array of attributes * @param string $subject The given string * * @return string * @since 1.0.0 * @static * */ public static function do_shortcode( $attributes = [], $subject = null ) { $plgSettings = Core::getSettings(); $default = []; if ( $plgSettings->enableGlobalEmbedResize ) { $default = [ 'width' => $plgSettings->enableEmbedResizeWidth, 'height' => $plgSettings->enableEmbedResizeHeight ]; } $attributes = wp_parse_args( $attributes, $default ); $embed = self::parseContent( $subject, true, $attributes ); return is_object( $embed ) ? $embed->embed : $embed; } /** * Replace a given content with its embeded HTML code. * * @param string The raw content that will be replaced. * @param bool $stripNewLine * @param array $customAttributes * @return string * @since 1.0.0 * @static */ public static function parseContent( $subject, $stripNewLine = false, $customAttributes = [] ) { if ( !empty( $subject ) ) { if ( empty( $customAttributes ) ) { $customAttributes = self::parseContentAttributesFromString( $subject ); } $content = preg_replace( '/(\[' . EMBEDPRESS_SHORTCODE . '(?:\]|.+?\])|\[\/' . EMBEDPRESS_SHORTCODE . '\])/i', "", $subject ); // Converts any special HTML entities back to characters. $content = htmlspecialchars_decode( $content ); // Check if the WP_oEmbed class is loaded if ( !self::$oEmbedInstance ) { global $wp_version; if ( version_compare( $wp_version, '5.3.0', '>=' ) ) { require_once ABSPATH . 'wp-includes/class-wp-oembed.php'; } else { require_once ABSPATH . 'wp-includes/class-oembed.php'; } self::$oEmbedInstance = _wp_oembed_get_object(); }; if ( !empty( self::get_access_token() ) ) { self::$oEmbedInstance->providers = array_merge( self::$oEmbedInstance->providers, self::get_modified_provider( self::get_access_token() ) ); } $emberaInstanceSettings = [ ]; $content_uid = md5( $content ); $attributes = self::parseContentAttributes( $customAttributes, $content_uid ); if ( isset( $attributes[ 'width' ] ) || isset( $attributes[ 'height' ] ) ) { if ( isset( $attributes[ 'width' ] ) ) { $emberaInstanceSettings[ 'maxwidth' ] = $attributes[ 'width' ]; unset( $attributes[ 'width' ] ); } if ( isset( $attributes[ 'height' ] ) ) { $emberaInstanceSettings[ 'maxheight' ] = $attributes[ 'height' ]; unset( $attributes[ 'height' ] ); } } // Identify what service provider the shortcode's link belongs to $serviceProvider = self::$oEmbedInstance->get_provider( $content ); // Check if OEmbed was unable to detect the url service provider. if ( empty( $serviceProvider ) ) { // Attempt to do the same using Embera. // Add support to the user's custom service providers $additionalServiceProviders = Core::getAdditionalServiceProviders(); if ( !empty( $additionalServiceProviders ) ) { foreach ( $additionalServiceProviders as $serviceProviderClassName => $serviceProviderUrls ) { self::addServiceProvider( $serviceProviderClassName, $serviceProviderUrls ); } unset( $serviceProviderUrls, $serviceProviderClassName ); } // Attempt to fetch more info about the url-embed. $emberaInstance = new Embera( $emberaInstanceSettings, self::$collection ); $urlData = $emberaInstance->getUrlData( $content ); } else { // Attempt to fetch more info about the url-embed. $urlData = self::$oEmbedInstance->fetch( $serviceProvider, $content, $attributes ); } // Sanitize the data $urlData = self::sanitizeUrlData( $urlData ); // Stores the original content if ( is_object( $urlData ) ) { $urlData->originalContent = $content; } $eventResults = apply_filters( 'embedpress:onBeforeEmbed', $urlData ); if ( empty( $eventResults ) ) { // EmbedPress seems unable to embed the url. return $subject; } // Transform all shortcode attributes into html form. I.e.: {foo: "joe"} -> foo="joe" $attributesHtml = []; foreach ( $attributes as $attrName => $attrValue ) { $attributesHtml[] = $attrName . '="' . $attrValue . '"'; } // Define the EmbedPress html template where the generated embed will be injected in $embedTemplate = '
{html}
'; // Check if $content is a google shortened url and tries to extract from it which Google service it refers to. if ( preg_match( '/http[s]?:\/\/goo\.gl\/(?:([a-z]+)\/)?[a-z0-9]+\/?$/i', $content, $matches ) ) { // Fetch all headers from the short-url so we can know how to handle its original content depending on the service. $headers = get_headers( $content ); $supportedServicesHeadersPatterns = [ 'maps' => '/^Location:\s+(http[s]?:\/\/.+)$/i', ]; $service = isset( $matches[ 1 ] ) ? strtolower( $matches[ 1 ] ) : null; // No specific service was found in the url. if ( empty( $service ) ) { // Let's try to guess which service the original url belongs to. foreach ( $headers as $header ) { // Check if the short-url reffers to a Google Maps url. if ( preg_match( $supportedServicesHeadersPatterns[ 'maps' ], $header, $matches ) ) { // Replace the shortened url with its original url. $content = $matches[ 1 ]; break; } } unset( $header ); } else { // Check if the Google service is supported atm. if ( isset( $supportedServicesHeadersPatterns[ $service ] ) ) { // Tries to extract the url based on its headers. $originalUrl = self::extractContentFromHeaderAsArray( $supportedServicesHeadersPatterns[ $service ], $headers ); // Replace the shortened url with its original url if the specific header was found. if ( !empty( $originalUrl ) ) { $content = $originalUrl; } unset( $originalUrl ); } } unset( $service, $supportedServicesHeadersPatterns, $headers, $matches ); } $parsedContent = self::$oEmbedInstance->get_html( $content, $attributes ); $provider_name = ''; if (isset( $urlData->provider_name )) { $provider_name = isset( $urlData->provider_name ); }elseif ( is_array( $urlData ) && isset( $urlData[ $content ][ 'provider_name' ] ) ) { $provider_name = $urlData[ $content ][ 'provider_name' ]; } if ( !$parsedContent ) { // If the embed couldn't be generated, we'll try to use Embera's API if ( !isset( $emberaInstance ) ) { // Add support to the user's custom service providers $additionalServiceProviders = Core::getAdditionalServiceProviders(); if ( !empty( $additionalServiceProviders ) ) { foreach ( $additionalServiceProviders as $serviceProviderClassName => $serviceProviderUrls ) { self::addServiceProvider( $serviceProviderClassName, $serviceProviderUrls); } unset( $serviceProviderUrls, $serviceProviderClassName ); } $emberaInstance = new Embera( $emberaInstanceSettings ); } // Inject the generated code inside the html template $parsedContent = str_replace( '{html}', $emberaInstance->autoEmbed($content), $embedTemplate ); } else { // Inject the generated code inside the html template $parsedContent = str_replace( '{html}', $parsedContent, $embedTemplate ); // Replace all single quotes to double quotes. I.e: foo='joe' -> foo="joe" $parsedContent = str_replace( "'", '"', $parsedContent ); $parsedContent = preg_replace( '/((?:ose-)?\{provider_alias\})/i', "ose-" . strtolower( $provider_name ), $parsedContent ); } if ( !empty($provider_name) ) { // NFB seems to always return their embed code with all HTML entities into their applicable characters string. $PROVIDER_NAME_IN_CAP = strtoupper($provider_name); if ( $PROVIDER_NAME_IN_CAP === "NATIONAL FILM BOARD OF CANADA" ) { $parsedContent = html_entity_decode( $parsedContent ); } elseif ( $PROVIDER_NAME_IN_CAP === "FACEBOOK" ) { $plgSettings = Core::getSettings(); // Check if the user wants to force a certain language into Facebook embeds. $locale = isset( $plgSettings->fbLanguage ) && !empty( $plgSettings->fbLanguage ) ? $plgSettings->fbLanguage : false; if ( !!$locale ) { // Replace the automatically detected language by Facebook's API with the language chosen by the user. $parsedContent = preg_replace( '/\/[a-z]{2}\_[a-z]{2}\/sdk\.js/i', "/{$locale}/sdk.js", $parsedContent ); } // Make sure `adapt_container_width` parameter is set to false. Setting to true, as it is by default, might cause Facebook to render embeds inside editors (in admin) with only 180px wide. if ( is_admin() ) { $parsedContent = preg_replace( '~data\-adapt\-container\-width=\"(?:true|1)\"~i', 'data-adapt-container-width="0"', $parsedContent ); } unset( $locale, $plgSettings ); } } unset( $embedTemplate, $serviceProvider ); // This assure that the iframe has the same dimensions the user wants to if ( isset( $emberaInstanceSettings[ 'width' ] ) || isset( $emberaInstanceSettings[ 'height' ] ) ) { if ( isset( $emberaInstanceSettings[ 'width' ] ) && isset( $emberaInstanceSettings[ 'height' ] ) ) { $customWidth = (int)$emberaInstanceSettings[ 'width' ]; $customHeight = (int)$emberaInstanceSettings[ 'height' ]; } else { if ( preg_match( '~width="(\d+)"|width\s+:\s+(\d+)~i', $parsedContent, $matches ) ) { $iframeWidth = (int)$matches[ 1 ]; } if ( preg_match( '~height="(\d+)"|height\s+:\s+(\d+)~i', $parsedContent, $matches ) ) { $iframeHeight = (int)$matches[ 1 ]; } if ( isset( $iframeWidth ) && isset( $iframeHeight ) && $iframeWidth > 0 && $iframeHeight > 0 ) { $iframeRatio = ceil( $iframeWidth / $iframeHeight ); if ( isset( $emberaInstanceSettings[ 'width' ] ) ) { $customWidth = (int)$emberaInstanceSettings[ 'width' ]; $customHeight = ceil( $customWidth / $iframeRatio ); } else { $customHeight = (int)$emberaInstanceSettings[ 'height' ]; $customWidth = $iframeRatio * $customHeight; } } } if ( isset( $customWidth ) && isset( $customHeight ) ) { if ( preg_match( '~width="(\d+)"~i', $parsedContent ) ) { $parsedContent = preg_replace( '~width="(\d+)"~i', 'width="' . $customWidth . '"', $parsedContent ); } if ( preg_match( '~height="(\d+)"~i', $parsedContent ) ) { error_log( 'hit parsed content custom height'); error_log( print_r( $parsedContent,1)); $parsedContent = preg_replace( '~height="(\d+)"~i', 'height="' . $customHeight . '"', $parsedContent ); } if ( preg_match( '~width\s+:\s+(\d+)~i', $parsedContent ) ) { $parsedContent = preg_replace( '~width\s+:\s+(\d+)~i', 'width: ' . $customWidth, $parsedContent ); } if ( preg_match( '~height\s+:\s+(\d+)~i', $parsedContent ) ) { $parsedContent = preg_replace( '~height\s+:\s+(\d+)~i', 'height: ' . $customHeight, $parsedContent ); } } } if ( $stripNewLine ) { $parsedContent = preg_replace( '/\n/', '', $parsedContent ); } $parsedContent = apply_filters( 'pp_embed_parsed_content', $parsedContent, $urlData, $attributes ); if ( !empty( $parsedContent ) ) { $embed = (object)array_merge( (array)$urlData, [ 'attributes' => (object)$attributes, 'embed' => $parsedContent, 'url' => $content, ] ); $embed = apply_filters( 'embedpress:onAfterEmbed', $embed ); return $embed; } } return $subject; } /** * Method that adds support to a given new service provider (SP). * * @param string $className The new SP class name. * @param string $reference The new SP reference name.* * @return DefaultProviderCollection|bool * @since 1.0.0 * @static * */ public static function addServiceProvider( $className, $reference ) { if ( empty( $className ) || empty( $reference ) ) { return false; } if (is_null(self::$collection)) { self::$collection = new DefaultProviderCollection(); } if ( is_string( $reference ) ) { self::$collection->addProvider( $reference, EMBEDPRESS_NAMESPACE . "\\Providers\\{$className}" ); return self::$collection; } elseif ( is_array( $reference ) ) { foreach ( $reference as $serviceProviderUrl ) { self::addServiceProvider( $className, $serviceProviderUrl); } return self::$collection; } else { return false; } } /** * Method that retrieves all custom parameters from a shortcoded string. * * @param string $subject The given shortcoded string. * * @return array * @since 1.0.0 * @static * */ public static function parseContentAttributesFromString( $subject ) { $customAttributes = []; if ( preg_match( '/\[embed\s*(.*?)\]/i', stripslashes( $subject ), $m ) ) { if ( preg_match_all( '/(\!?\w+-?\w*)(?:="(.+?)")?/i', stripslashes( $m[ 1 ] ), $matches ) ) { $attributes = $matches[ 1 ]; $attrValues = $matches[ 2 ]; foreach ( $attributes as $attrIndex => $attrName ) { $customAttributes[ $attrName ] = $attrValues[ $attrIndex ]; } } } return $customAttributes; } /** * Method that parses and adds the "data-" prefix to the given custom shortcode attributes. * * @param array $customAttributes The array containing the embed attributes. * @param string $content_uid An optional string specifying a unique ID for the embed * * @return array * @since 1.0.0 * @access private * @static * */ private static function parseContentAttributes( array $customAttributes, $content_uid = null ) { $attributes = [ 'class' => ["embedpress-wrapper"], ]; $embedShouldBeResponsive = true; $embedShouldHaveCustomDimensions = false; if ( !empty( $customAttributes ) ) { if ( isset( $customAttributes[ 'class' ] ) ) { if ( !empty( $customAttributes[ 'class' ] ) ) { $customAttributes[ 'class' ] = explode( ' ', $customAttributes[ 'class' ] ); $attributes[ 'class' ] = array_merge( $attributes[ 'class' ], $customAttributes[ 'class' ] ); } unset( $customAttributes[ 'class' ] ); } if ( isset( $customAttributes[ 'width' ] ) ) { if ( !empty( $customAttributes[ 'width' ] ) ) { $attributes[ 'width' ] = (int)$customAttributes[ 'width' ]; $embedShouldHaveCustomDimensions = true; } } if ( isset( $customAttributes[ 'height' ] ) ) { if ( !empty( $customAttributes[ 'height' ] ) ) { $attributes[ 'height' ] = (int)$customAttributes[ 'height' ]; $embedShouldHaveCustomDimensions = true; } } if ( !empty( $customAttributes ) ) { $attrNameDefaultPrefix = "data-"; foreach ( $customAttributes as $attrName => $attrValue ) { if ( is_numeric( $attrName ) ) { $attrName = $attrValue; $attrValue = ""; } $attrName = str_replace( $attrNameDefaultPrefix, "", $attrName ); if ( !strlen( $attrValue ) ) { if ( $attrName[ 0 ] === "!" ) { $attrValue = "false"; $attrName = substr( $attrName, 1 ); } else { $attrValue = "true"; } } $attributes[ $attrNameDefaultPrefix . $attrName ] = $attrValue; } } // Check if there's any "responsive" parameter $responsiveAttributes = ["responsive", "data-responsive"]; foreach ( $responsiveAttributes as $responsiveAttr ) { if ( isset( $attributes[ $responsiveAttr ] ) ) { if ( !strlen( $attributes[ $responsiveAttr ] ) ) { // If the parameter is passed but have no value, it will be true by default $embedShouldBeResponsive = true; } else { $embedShouldBeResponsive = !self::valueIsFalse( $attributes[ $responsiveAttr ] ); } break; } } unset( $responsiveAttr, $responsiveAttributes ); } $attributes[ 'class' ][] = 'ose-{provider_alias}'; if ( !empty( $content_uid ) ) { $attributes[ 'class' ][] = 'ose-uid-' . $content_uid; } if ( $embedShouldBeResponsive && !$embedShouldHaveCustomDimensions ) { $attributes[ 'class' ][] = 'responsive'; } else { $attributes[ 'data-responsive' ] = "false"; } $attributes[ 'class' ] = implode( ' ', array_unique( array_filter( $attributes[ 'class' ] ) ) ); if ( isset( $attributes[ 'width' ] ) ) { $attributes[ 'style' ] = "width:{$attributes['width'] }px;height:{$attributes['height'] }px;"; } return $attributes; } /** * Method that checks if a given value is/can be identified as (bool)false. * * @param mixed $subject The value to be checked. * * @return boolean * @since 1.0.0 * @static * */ public static function valueIsFalse( $subject ) { $subject = strtolower( trim( (string)$subject ) ); switch ( $subject ) { case "0": case "false": case "off": case "no": case "n": case "nil": case "null": return true; default: return false; } } /** * Return the value from a header which is in an array resulted from a get_headers() call. * If the header cannot be found, this method will return null instead. * * @param string $headerPattern Regex pattern the header and its value must match. * @param array $headersList A list of headers resulted from a get_headers() call. * * @return mixed * @since 1.1.0 * @access private * @static * */ private static function extractContentFromHeaderAsArray( $headerPattern, $headersList ) { $headerValue = null; foreach ( $headersList as $header ) { if ( preg_match( $headerPattern, $header, $matches ) ) { $headerValue = $matches[ 1 ]; break; } } return $headerValue; } /** * Sanitize the object returned by the embed source. Sometimes we need to convert * attributes from "dash" separated to "underline" separated to be able to access * those attributes from the object, without having to convert it to an array. * * @param object $data * * @return object * @since 1.6.1 * @access private * @static * */ private static function sanitizeUrlData( $data ) { if ( is_object( $data ) ) { $attributes = get_object_vars( $data ); foreach ( $attributes as $key => $value ) { if ( substr_count( $key, '-' ) ) { unset( $data->$key ); $key = str_replace( '-', '_', $key ); $data->$key = $value; } } } elseif ( is_array( $data ) ) { foreach ( $data as $key => $value ) { if ( substr_count( $key, '-' ) ) { unset( $data[ $key ] ); $key = str_replace( '-', '_', $key ); $data[ $key ] = $value; } } } return $data; } public static function get_modified_provider( $access_token = null ) { if ( empty( $access_token ) ) { return []; } return [ '#https?://www\.facebook\.com/.*/posts/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/.*/activity/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/.*/photos/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/photo(s/|\.php).*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/permalink\.php.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/media/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/questions/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/notes/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_post?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/.*/videos/.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_video?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/video\.php.*#i' => [ 'https://graph.facebook.com/v8.0/oembed_video?access_token=' . $access_token, true ], '#https?://www\.facebook\.com/watch/?\?v=\d+#i' => [ 'https://graph.facebook.com/v8.0/oembed_video?access_token=' . $access_token, true ], '#https?://(www\.)?instagr(\.am|am\.com)/(p|tv)/.*#i' => [ 'https://graph.facebook.com/v8.0/instagram_oembed?access_token=' . $access_token, true ] ]; } public static function get_access_token() { $plgSettings = Core::getSettings(); if ( !empty( $plgSettings->facebook_app_code ) && !empty( $plgSettings->facebook_app_secret ) ) { return urlencode( $plgSettings->facebook_app_code . '|' . $plgSettings->facebook_app_secret ); } else { return null; } } public static function get_collection() { return self::$collection; } }