diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79528a0..77798af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,7 @@ name: 'Release' on: + workflow_dispatch: release: types: [published] @@ -17,7 +18,7 @@ jobs: uses: shivammathur/setup-php@2.22.0 with: tools: composer, wp - php-version: 8.0 + php-version: 8.3 - name: Checkout Repo uses: actions/checkout@v3 @@ -31,7 +32,7 @@ jobs: composer install --no-dev composer dump-autoload - name: NPM Setup - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version-file: "${{ env.BUILD_FOLDER }}/.nvmrc" cache: 'npm' @@ -47,7 +48,7 @@ jobs: - name: Configure WP-CLI dist-archive-command run: | cd ${{ env.BUILD_FOLDER }} - wp package install wp-cli/dist-archive-command + wp package install wp-cli/dist-archive-command:^3.1 - name: Build Plugin Zip run: | wp dist-archive ${{ env.BUILD_FOLDER }} --plugin-dirname=${{ env.PLUGIN_SLUG }} diff --git a/.lando.yml b/.lando.yml index 4aac18a..6334ebf 100644 --- a/.lando.yml +++ b/.lando.yml @@ -9,7 +9,7 @@ excludes: - .github config: - php: '8.0' + php: '8.3' via: nginx database: mysql webroot: dev/public diff --git a/assets/js/embed.js b/assets/js/embed.js index 8b7d0ae..9c2e732 100644 --- a/assets/js/embed.js +++ b/assets/js/embed.js @@ -37,6 +37,28 @@ function createCaptionEl( caption ) { return captionEl; } +/** + * Configure and trigger play for (Aurora web component). + * Uses the element's autoplay attribute and api-ready event; window._wq does not apply. + * See https://docs.wistia.com/docs/player-attributes-and-properties#autoplay + * and https://docs.wistia.com/docs/player-events + * + * @param {Element} playerEl The DOM element (in the document). + */ +function playWistiaPlayer( playerEl ) { + if ( ! playerEl || playerEl.tagName !== 'WISTIA-PLAYER' ) return; + playerEl.setAttribute( 'autoplay', '' ); + playerEl.addEventListener( + 'api-ready', + () => { + if ( typeof playerEl.play === 'function' ) { + playerEl.play(); + } + }, + { once: true } + ); +} + /** * Setup event handlers * @@ -53,6 +75,10 @@ function setupEventHandlers( embed, template ) { if ( clickEls.length === 0 ) return; + const embedContent = template.content.children[ 0 ]; + const isWistia = + embedContent && embedContent.querySelector( 'wistia-player' ); + // loop through each click event - play button and thumbnail. clickEls.forEach( ( clickEl ) => { // when the element is clicked. @@ -63,6 +89,14 @@ function setupEventHandlers( embed, template ) { // grab just the first child of the template - this is the figure block element which wraps the iframe. const content = contentOuter.children[ 0 ]; + // Wistia: set autoplay on in the clone so it plays once injected (web component uses attributes, not _wq). + if ( isWistia ) { + const playerInClone = content.querySelector( 'wistia-player' ); + if ( playerInClone ) { + playerInClone.setAttribute( 'autoplay', '' ); + } + } + // add the iframe embed content before the embed wrapper. embed.before( content ); @@ -71,6 +105,14 @@ function setupEventHandlers( embed, template ) { // remove the template item which holds the iframe. template.remove(); + + // Wistia: ensure play when API is ready (handles async script load / custom element upgrade). + if ( isWistia ) { + const playerEl = content.querySelector( 'wistia-player' ); + if ( playerEl ) { + playWistiaPlayer( playerEl ); + } + } } ); } ); } @@ -81,15 +123,24 @@ function setupEventHandlers( embed, template ) { function updateEmbeds() { embedBlocks.forEach( ( embed ) => { // get the associated template element which holds the embed code. - // it is the next element after the wrapper. + // it is the next element after the wrapper (only used for providers that use template, e.g. YouTube and Wistia). const template = embed.nextElementSibling; + if ( ! template || template.tagName !== 'TEMPLATE' ) { + return; + } - const iframe = template.content.children[ 0 ].querySelector( 'iframe' ); - setIframeAttributes( iframe ); + const embedContent = template.content.children[ 0 ]; + if ( ! embedContent ) { + return; + } + + const iframe = embedContent.querySelector( 'iframe' ); + if ( iframe ) { + setIframeAttributes( iframe ); + } - // get the first child of the figure and add after tumbnail if present - const caption = - template.content.children[ 0 ].querySelector( 'figcaption' ); + // get the first child of the figure and add after thumbnail if present + const caption = embedContent.querySelector( 'figcaption' ); if ( caption ) { const captionEl = createCaptionEl( caption ); diff --git a/composer.json b/composer.json index 4f12f90..0eaa5f6 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "phpunit/phpunit": "^9.0", "assertwell/wp-core-test-framework": "^0.2.0", "phpstan/phpstan": "^1.10", - "php-stubs/wordpress-stubs": "^5.9", + "php-stubs/wordpress-stubs": "^6.6", "szepeviktor/phpstan-wordpress": "^1.1", "phpstan/extension-installer": "^1.1", "php-stubs/acf-pro-stubs": "^5.12", diff --git a/composer.lock b/composer.lock index 03178ea..ce13826 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "57248f8129bae7499b7a240a82c99292", + "content-hash": "412d2fb427af068bf60a5bbc69e2b468", "packages": [], "packages-dev": [ { @@ -564,29 +564,35 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v5.9.6", + "version": "v6.8.2", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "6a18d938d0aef39d091505a4a35b025fb6c10098" + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6a18d938d0aef39d091505a4a35b025fb6c10098", - "reference": "6a18d938d0aef39d091505a4a35b025fb6c10098", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", "shasum": "" }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, "require-dev": { - "nikic/php-parser": "< 4.12.0", - "php": "~7.3 || ~8.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.10.12", - "phpunit/phpunit": "^9.5" + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "symfony/polyfill-php73": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" }, "type": "library", @@ -603,9 +609,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.6" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" }, - "time": "2023-05-18T04:34:27+00:00" + "time": "2025-07-16T06:41:00+00:00" }, { "name": "phpcompatibility/php-compatibility", diff --git a/readme.md b/readme.md index 682ee27..3d4bd4e 100644 --- a/readme.md +++ b/readme.md @@ -32,39 +32,117 @@ If you need to rebuild the lando environment you will need to delete the `./dev/ This repo is setup to use the [WP CLI dist-archive](https://developer.wordpress.org/cli/commands/dist-archive/) command. To build the zip file for the make sure you have the dist-archive command package installed and run `wp dist-archive .` form the root folder. The zip file will be created one folder back form the root folder. +### Vimeo -### Providers +In order to make Vimeo provider thumbs work correctly create an access token https://help.vimeo.com/hc/en-us/articles/12427789081745-How-to-generate-a-personal-access-token +Use Tribe Embeds Settings page to store token via DB or set `VIMEO_ACCESS_TOKEN` in you wp-config.php -Each provider represents separate service(YouTube, Vimeo, etc). In order to provide ability extend list of providers use `tribe-embeds_video_provider` hook. -For proper use add new class in your theme or plugin. Each provider should extend `Tribe\Tribe_Embed\Provider` class -Usage example: + +## Hooks (filters & actions) + +These are the **public** extension points intended for themes/plugins to customize behavior. Names and arguments are considered part of the API. + +### Filters + +#### `tribe-embeds_allowed_provider_hosts` + +Expand or restrict the whitelist of hostnames that can be handled by built-in or custom providers. + +- **Signature:** `apply_filters( 'tribe-embeds_allowed_provider_hosts', $allowed_hosts, $host )` +- **Args:** + - `$allowed_hosts` — array of allowed host strings (e.g. ['youtube.com', 'youtu.be', 'vimeo.com', 'dailymotion.com']) + - `$host` — the currently detected host +- **Return:** Modified array of allowed hosts. +- **Example:** ```php -class TestProvider extends \Tribe\Tribe_Embed\Provider { -.... -} - -function is_allowed_provider(): bool { -... -} - -/** - * @var mixed|null $provider - * @var array $video_url_data Video url parsed with parse_url - * @var array $block The full block, including name and attributes. - */ -add_filter( 'tribe-embeds_video_provider', function( $provider, $video_url_data, $block ) { - if ( is_allowed_provider( $video_url_data['host'] ) ) { - return $provider; - } - return ( new TestProvider( $video_url_data ) ); -}, 10, 3 ); +add_filter( 'tribe-embeds_allowed_provider_hosts', function ( array $hosts ) { + $hosts[] = 'videos.example.com'; + return $hosts; +}, 10 ); ``` -A list of allowed providers can be updated via `tribe-embeds_allowed_provider_hosts` hook + +#### `tribe_embeds_video_provider` + +Allow short-circuit with a ready-made provider instance + +- **Signature:** `apply_filters( 'tribe_embeds_video_provider', null, $video_url_data, $block )` +- **Args:** + - `null|` — if object provided resolves immediately and return provided object + - `$video_url_data` — embed video ulr + - `$block` — current embed block data +- **Return:** `null` or provided provider class +- **Example:** ```php -/** - * Allows to inject custom provider hosts - * @var array $allowed_hosts List of allowed hosts - * @var string $host Current video hostname - */ -$allowed_hosts = apply_filters( 'tribe-embeds_allowed_provider_hosts', $allowed_hosts, $host ); +add_filter( 'tribe_embeds_video_provider', function ( $obj, $video_url_data, $block ) { + // Note: $video_url_data has parsed video url. Provider accepts url string + $provider = new CustomProvider( $video_url ); + + return $provider; +}, 10 ); ``` + +#### `tribe_embeds_allowed_provider_hosts_` + +Adjust allowed hosts for provider + +- **Signature:** `apply_filters( 'tribe_embeds_allowed_provider_hosts_' . $slug, $base, $provider_class );` +- **Args:** + - `$base` — list of allowed hosts + - `$provider_class` — current provider class + +#### `tribe_embeds_allowed_provider_hosts` + +Adjust allowed hosts for provider + +- **Signature:** `apply_filters( 'tribe_embeds_allowed_provider_hosts', $by_provider, $provider_class );` +- **Args:** + - `$by_provider` — List of hosts returned from `tribe_embeds_allowed_provider_hosts_` + - `$provider_class` — current provider class + +#### `tribe_embeds_image_sizes_` + +Get image sizes for a provider + +- **Signature:** `apply_filters( 'tribe_embeds_image_sizes_' . $slug, $base, $provider_class );` +- **Args:** + - `$base` — list of image sizes + - `$provider_class` — current provider class + +#### `tribe_embeds_image_sizes` + +Get image sizes for a provider + +- **Signature:** `apply_filters( 'tribe_embeds_image_sizes', $by_provider, $provider_class );` +- **Args:** + - `$by_provider` — list of image sizes `tribe_embeds_image_sizes` + - `$provider_class` — current provider class + +#### `tribe_embeds_provider_classes` + +Allow external override of provider class list + +- **Signature:** `apply_filters( 'tribe_embeds_provider_classes', $provider_classes ?: $defaults );` +- **Args:** + - `$provider_classes` — list of existing providers classes + +#### `tribe_embed__video_thumbnail_url` + +Allows adjusting image data for each provider. Use slug instead of `` e.g `tribe_embed_wistia_video_thumbnail_url` + +- **Signature:** `apply_filters( 'tribe_embed_wistia_video_thumbnail_url', $image_data, $video_id )` +- **Args:** + - `$image_data` — Thumbnail image data + - `$video_id` — current video id +- **Return:** Video thumbnail image data. + +#### `tribe_embeds_facade_html` + +Fires an action with the new block markup attached. + +- **Signature:** `apply_filters( 'tribe_embeds_facade_html', $facade_html, $provider, $block, $html )` +- **Args:** + - `$facade_html` — Resulting html + - `$provider` — provider class + - `$block` — current block + - `$html` — original block html +- **Return:** Embed block markup diff --git a/src/Admin/Credentials.php b/src/Admin/Credentials.php new file mode 100644 index 0000000..a51c4dc --- /dev/null +++ b/src/Admin/Credentials.php @@ -0,0 +1,31 @@ + 'sanitize_text_field' ] + ); + + register_setting( + 'tribe_embeds_settings', + 'tribe_embeds_wistia_api_key', + [ 'sanitize_callback' => 'sanitize_text_field' ] + ); + } + + /** + * Render the settings page. + */ + public function render_page(): void { + ?> +
+

+ +
+ + + + + + + + + + +
+ + + +
+ + + +
+ +
+
+ get_option( 'tribe_embeds_vimeo_access_token', '' ), + self::WISTIA_TOKEN => get_option( 'tribe_embeds_wistia_api_key', '' ), + ]; + } + +} diff --git a/src/Admin/Support_Providers.php b/src/Admin/Support_Providers.php new file mode 100644 index 0000000..3bcbd73 --- /dev/null +++ b/src/Admin/Support_Providers.php @@ -0,0 +1,26 @@ + $providers + * + * @return array + */ + public function add_wistia_oembed( array $providers ): array { + $providers[ self::WISTIA_REGEX ] = [ self::WISTIA_OEMBED, true ]; + + return $providers; + } + +} diff --git a/src/Core.php b/src/Core.php index f7dfef4..5647652 100644 --- a/src/Core.php +++ b/src/Core.php @@ -2,327 +2,107 @@ namespace Tribe\Tribe_Embed; -use Tribe\Tribe_Embed\Providers\Dailymotion; -use Tribe\Tribe_Embed\Providers\Vimeo; -use Tribe\Tribe_Embed\Providers\YouTube; -use WP_Block; - +use Tribe\Tribe_Embed\Admin\Settings_Page; +use Tribe\Tribe_Embed\Admin\Support_Providers; +use Tribe\Tribe_Embed\Providers\Provider_Factory; +use Tribe\Tribe_Embed\Util\Assets; +use Tribe\Tribe_Embed\Util\Block_Filter; +use Tribe\Tribe_Embed\Util\Facade_Builder; +use Tribe\Tribe_Embed\Util\Thumbnail_Service; +use Tribe\Tribe_Embed\Util\Url_Parser; + +/** + * Builds and shares single service instances. + * Delegates hook registration to Block_Filter. + */ final class Core { - public const VERSION = '1.0.3'; + public const VERSION = '2.0.0'; public const PLUGIN_NAME = 'tribe-embed'; - private static self $instance; + private Provider_Factory|null $factory = null; + private Url_Parser|null $url_parser = null; + private Thumbnail_Service|null $thumbs = null; + private Facade_Builder|null $facade = null; + private Block_Filter|null $block_filter = null; - private function __construct() { - define( 'TRIBE_MP_PATH', trailingslashit( plugin_dir_path( dirname( __FILE__ ) ) ) ); - define( 'TRIBE_MP_URL', plugin_dir_url( TRIBE_MP_PATH . self::PLUGIN_NAME ) ); - define( 'TRIBE_MP_VERSION', self::VERSION ); - } + /** @var self|null Singleton instance */ + private static ?self $instance = null; + /** Get Core singleton */ public static function instance(): self { - if ( ! isset( self::$instance ) ) { + if ( ! self::$instance instanceof self ) { self::$instance = new self(); } return self::$instance; } - public function init( string $file ): void { - add_action( 'admin_enqueue_scripts', [ $this, 'register_admin_scripts' ] ); - add_action( 'wp_enqueue_scripts', [ $this, 'register_public_scripts' ] ); + /** Register WP hooks via Block_Filter */ + public function register_hooks(): void { + $assets = new Assets( self::PLUGIN_NAME, self::VERSION ); + $providers_support = new Support_Providers(); - add_filter( 'render_block_core/embed', [ $this, 'filter_embed_block' ], 10, 3 ); + add_action( 'admin_enqueue_scripts', [ $assets, 'register_admin_scripts' ] ); + add_action( 'wp_enqueue_scripts', [ $assets, 'register_public_scripts' ] ); + add_action( 'init', [ $this, 'register_settings' ] ); - add_action( 'video_thumbnail_markup', [ $this, 'open_markup_figure_element' ], 10, 4 ); - add_action( 'video_thumbnail_markup', [ $this, 'add_video_play_button' ], 20, 4 ); - add_action( 'video_thumbnail_markup', [ $this, 'add_video_thumbnail_markup' ], 30, 4 ); - add_action( 'video_thumbnail_markup', [ $this, 'close_markup_figure_element' ], 40, 4 ); - add_action( 'video_thumbnail_markup', [ $this, 'add_original_embed_template' ], 50, 4 ); + $this->block_filter()->register_hooks(); + $providers_support->register(); } - /** - * Registers the admin scripts - */ - public function register_admin_scripts(): void { - $asset_file = include TRIBE_MP_PATH . 'dist/editor.asset.php'; - wp_enqueue_script( self::PLUGIN_NAME . '-admin', TRIBE_MP_URL . 'dist/editor.js', $asset_file['dependencies'], $asset_file['version'] ); - wp_enqueue_style( self::PLUGIN_NAME . '-admin', TRIBE_MP_URL . 'dist/editor.css', $asset_file['version'] ); + public function register_settings(): void { + ( new Settings_Page() ); } - /** - * Registers the public scripts - */ - public function register_public_scripts(): void { - $asset_file = include TRIBE_MP_PATH . 'dist/index.asset.php'; - wp_enqueue_script( self::PLUGIN_NAME . '-public', TRIBE_MP_URL . 'dist/index.js', $asset_file['dependencies'], $asset_file['version'] ); - wp_enqueue_style( self::PLUGIN_NAME . '-public', TRIBE_MP_URL . 'dist/style-index.css', $asset_file['version'] ); - } - - /** - * Filters the code embed block output for improved performance on Youtube videos. - * - * @param string $block_content The block content. - * @param array $block The full block, including name and attributes. - * @param \Tribe\Tribe_Embed\WP_Block $instance The block instance. - * - * @return string $block_content The block content. - */ - public function filter_embed_block( string $block_content, array $block, WP_Block $instance ): string { - - // if the provider slug name is empty. - if ( empty( $block['attrs']['providerNameSlug'] ) ) { - return $block_content; + /** Get Provider_Factory */ + public function provider_factory(): Provider_Factory { + if ( ! $this->factory instanceof Provider_Factory ) { + $this->factory = new Provider_Factory(); } - // if for some reason there is no embed URL. - if ( empty( $block['attrs']['url'] ) ) { - return $block_content; - } - - // setup some base variables and get the video url - $provider = null; - $thumbnail_data = []; - $parsed_video_url = parse_url( $block['attrs']['url'] ); - - // Only continue for allowed providers - if ( ! $this->is_allowed_host( $parsed_video_url['host'] ) ) { - return $block_content; - } - - // switch based on the host. - switch ( $parsed_video_url['host'] ) { - // for youtube urls - case in_array( $parsed_video_url['host'], YouTube::ALLOWED_HOSTS ): - $provider = new YouTube( $parsed_video_url ); - break; - - // for vimeo urls. - case in_array( $parsed_video_url['host'], Vimeo::ALLOWED_HOSTS ): - $provider = new Vimeo( $parsed_video_url ); - break; - - // for dailymotion urls. - case in_array( $parsed_video_url['host'], Dailymotion::ALLOWED_HOSTS ): - $provider = new Dailymotion( $parsed_video_url ); - break; - - default: - /** - * Returns Custom Provider class object - * - * @var mixed|null $provider Provider object - * @var array $video_url_data Video url parsed with parse_url - * @var array $block The full block, including name and attributes. - */ - $provider = apply_filters( 'tribe-embeds_video_provider', null, $parsed_video_url, $block ); - break; - } - - // Bail if empty/wrong provider is provided - if ( empty( $provider ) ) { - return $block_content; - } - - // get thumbnail data. - $video_id = $provider->get_video_id(); - $thumbnail_data = $provider->get_thumbnail_data(); - - // if we don't have any video thumbnails. - if ( count( $thumbnail_data ) === 0 ) { - return $block_content; - } - - // create an array of classes to add to the placeholder image wrapper. - $wrapper_classes = [ - 'wp-block-image', - 'tribe-embed', - 'is--' . $block['attrs']['providerNameSlug'], - ]; - - // if we have classNames on the embed block. - if ( ! empty( $block['attrs']['className'] ) ) { - // explode the className string into array. - $class_names = explode( ' ', $block['attrs']['className'] ); - - // merge the class names into the figures classes array. - $wrapper_classes = array_merge( $wrapper_classes, $class_names ); - } - - // if the embed block has an alignment. - if ( ! empty( $block['attrs']['align'] ) ) { - // add the alignment class to the figure classes. - $wrapper_classes[] = 'align' . $block['attrs']['align']; - } - - // allow the classes to be filtered. - $wrapper_classes = apply_filters( '', $wrapper_classes, $block, $video_id, $thumbnail_data ); - - // buffer the output as we need to return not echo. - ob_start(); - - // output the registered "block" styles for the thubmnail. - wp_print_styles( 'tribe-embeds-styles' ); - - /** - * Fires and action to which the new block markup is added too. - * - * @hooked open_markup_figure_element - 10 - * @hooked add_video_play_button - 20 - * @hooked add_video_thumbnail_markup - 30 - * @hooked hd_bvce_close_markup_figure_element - 40 - * @hooked add_original_embed_template - 50 - */ - do_action( 'video_thumbnail_markup', $block, $video_id, $thumbnail_data, $wrapper_classes ); - - // return the new block markup. - return ob_get_clean(); - } - - /** - * Creates a escaping function to allowed certain HTML for embed content. - * Needed for when echoing the innerblock HTML. - * - * @param array An array of HTML elements allowed. - */ - public function allowed_innerblock_html(): array { - /** - * Return the allowed html - * These are the elements in the rendered embed block for supported videos. - * This also includes everything you can add to an embed caption. - * Therefore we need to allow these to keep the same structure. - */ - return [ - 'iframe' => [ - 'src' => true, - 'height' => true, - 'width' => true, - 'frameborder' => true, - 'allowfullscreen' => true, - ], - 'figure' => [ - 'class' => true, - ], - 'figcaption' => [ - 'class' => true, - ], - 'div' => [ - 'class' => true, - ], - 'a' => [ - 'class' => true, - 'href' => true, - 'data-type' => true, - ], - 'strong' => [], - 'em' => [], - 'sub' => [], - 'sup' => [], - 's' => [], - 'kbd' => [], - 'img' => [ - 'class' => true, - 'style' => true, - 'src' => true, - 'alt' => true, - ], - 'code' => [], - 'mark' => [ - 'style' => true, - 'class' => true, - ], - ]; - } - - /** - * Adds the opening figure element to the thumbnail markup. - * - * @param array $block The block array. - * @param string $video_id The ID of the embedded video. - * @param array $thumbnail_data The URL of the video thumbnail. - * @param array $wrapper_classes An array of CSS classes to add to the wrapper. - */ - public function open_markup_figure_element( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): void { - - ?> -
-
- factory; } - /** - * Adds the play button div to the markup. - * - * @param array $block The block array. - * @param string $video_id The ID of the embedded video. - * @param array $thumbnail_data The URL of the video thumbnail. - * @param array $wrapper_classes An array of CSS classes to add to the wrapper. - */ - public function add_video_play_button( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): void { + /** Get Url_Parser */ + public function url_parser(): Url_Parser { + if ( ! $this->url_parser instanceof Url_Parser ) { + $this->url_parser = new Url_Parser(); + } - ?> - - url_parser; } - /** - * Adds the video thumbnail markup output. - * - * @param array $block The block array. - * @param string $video_id The ID of the embedded video. - * @param array $thumbnail_data The URL of the video thumbnail. - * @param array $wrapper_classes An array of CSS classes to add to the wrapper. - */ - public function add_video_thumbnail_markup( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): void { - - $max_res_image = end( $thumbnail_data ); - $srcset = []; - $sizes = [ '(max-width: ' . $max_res_image['width'] . 'px) 100vw', $max_res_image['width'] . 'px' ]; - - foreach ( $thumbnail_data as $data ) { - $srcset[] = $data['url'] . ' ' . $data['width'] . 'w'; + /** Get Thumbnail_Service */ + public function thumbnail_service(): Thumbnail_Service { + if ( ! $this->thumbs instanceof Thumbnail_Service ) { + $this->thumbs = new Thumbnail_Service( $this->provider_factory() ); } - ?> - height= - class="tribe-embed__thumbnail" alt="" src="" - srcset="" sizes="" /> - thumbs; } - /** - * Adds the closing figure element to the thumbnail markup. - * - * @param array $block The block array. - * @param string $video_id The ID of the embedded video. - * @param array $thumbnail_data The URL of the video thumbnail. - * @param array $wrapper_classes An array of CSS classes to add to the wrapper. - */ - public function close_markup_figure_element( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): void { + /** Get Facade_Builder */ + public function facade_builder(): Facade_Builder { + if ( ! $this->facade instanceof Facade_Builder ) { + $this->facade = new Facade_Builder(); + } - ?> -
-
- facade; } - /** - * Adds the original block markup to the template element. - * This is used when the item is cloned when the thumbnail is clicked. - * - * @param array $block The block array. - * @param string $video_id The ID of the embedded video. - * @param array $thumbnail_data The URL of the video thumbnail. - * @param array $wrapper_classes An array of CSS classes to add to the wrapper. - */ - public function add_original_embed_template( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): void { + /** Get Block_Filter */ + public function block_filter(): Block_Filter { + if ( ! $this->block_filter instanceof Block_Filter ) { + $this->block_filter = new Block_Filter( + $this->url_parser(), + $this->provider_factory(), + $this->thumbnail_service(), + $this->facade_builder() + ); + } - ?> - - block_filter; } /** @@ -339,43 +119,4 @@ public static function deactivate(): void { return; } - /** - * @param string $host - * @param array $allowed_hosts - */ - private function is_allowed_host( string $host, array $allowed_hosts = [] ): bool { - // Use default list of allowed hosts if nothing has been specified - if ( empty( $allowed_hosts ) ) { - $allowed_hosts = array_merge( YouTube::ALLOWED_HOSTS, Vimeo::ALLOWED_HOSTS, Dailymotion::ALLOWED_HOSTS ); - } - - /** - * Allows to inject custom provider hosts - * @var array $allowed_hosts List of allowed hosts - * @var string $host Current video hostname - */ - $allowed_hosts = apply_filters( 'tribe-embeds_allowed_provider_hosts', $allowed_hosts, $host ); - - if ( in_array( $host, $allowed_hosts ) ) { - return true; - } - - /** - * Check if requested host is in the list of allowed ones. This is a wildcard check with regex - * and it is specifically handles problem with provider dynamic urls - * - * Example: - * Wistia video embed: https://tri-4.wistia.com/medias/7c1s0ftfl3?embedType=web_component&seo=true&videoWidth=960 - * The `tri-4` part may vary and be different for different videos or/and accounts - */ - $allowed_hosts = array_filter( $allowed_hosts, static function ( $allowed ) use ( $host ) { - return (bool) preg_match( "/$allowed/i", $host ); - } ); - - return ! empty( $allowed_hosts ); - } - - private function __clone() { - } - } diff --git a/src/Providers/Dailymotion.php b/src/Providers/Dailymotion.php index b13b0db..d17c430 100644 --- a/src/Providers/Dailymotion.php +++ b/src/Providers/Dailymotion.php @@ -26,7 +26,7 @@ final class Dailymotion extends Provider { ]; /** - * Return the vimeo video thumbnail urls. + * Return the Dailymotion video thumbnail URLs. */ public function get_thumbnail_data(): array { @@ -47,8 +47,8 @@ public function get_thumbnail_data(): array { $video_details = wp_remote_get( self::BASE_URL . $this->get_video_id() . '?fields=' . $resolution ); // if the request to the image errors or returns anything other than a http 200 response code. - if ( ( is_wp_error( $video_details )) && ( 200 !== wp_remote_retrieve_response_code( $video_details ) ) ) { - return ''; + if ( is_wp_error( $video_details ) || 200 !== wp_remote_retrieve_response_code( $video_details ) ) { + return []; } // grab the body of the response. @@ -59,7 +59,7 @@ public function get_thumbnail_data(): array { ); if ( $response_body === null ) { - return ''; + return []; } // get the image url from the json. @@ -82,7 +82,7 @@ public function get_thumbnail_data(): array { } // return the url. - return apply_filters( 'tribe-embed_dailymotion_video_thumbnail_url', $image_data, $this->get_video_id() ); + return apply_filters( 'tribe_embed_dailymotion_video_thumbnail_url', $image_data, $this->get_video_id() ); } protected function set_video_id(): string { @@ -97,7 +97,6 @@ protected function set_video_id(): string { // remove the preceeding slash. return str_replace( '/video/', '', $this->video_url['path'] ); - break; case 'dai.ly': // if we have a path. if ( empty( $this->video_url['path'] ) ) { @@ -106,9 +105,9 @@ protected function set_video_id(): string { // remove the preceeding slash. return str_replace( '/', '', $this->video_url['path'] ); - - break; } + + return ''; } } diff --git a/src/Providers/Provider.php b/src/Providers/Provider.php index 96b1f1d..4ce9dc3 100644 --- a/src/Providers/Provider.php +++ b/src/Providers/Provider.php @@ -8,11 +8,15 @@ abstract class Provider { public const IMAGE_SIZES = []; public const ALLOWED_HOSTS = []; + // phpcs:ignore SlevomatCodingStandard.TypeHints.PropertyTypeHint.MissingTraversableTypeHintSpecification protected array $video_url; protected string $video_id; - protected static self $instance; - + /** + * Return thumbnail data for the video. + * + * @return array + */ abstract public function get_thumbnail_data(): array; abstract protected function set_video_id(): string; @@ -22,16 +26,16 @@ public function __construct( array $video_url = [] ) { $this->video_id = $this->set_video_id(); } - public static function instance( array $video_url ): self { - if ( ! isset( self::$instance ) ) { - self::$instance = new self( $video_url ); - } - - return self::$instance; - } - public function get_video_id(): string { return $this->video_id ?? ''; } + /** + * Whether this provider uses inline embed (raw HTML inside facade) instead of a template. + * When true, Facade_Builder will not wrap the embed in a template element. + */ + public function uses_inline_embed(): bool { + return false; + } + } diff --git a/src/Providers/Provider_Factory.php b/src/Providers/Provider_Factory.php new file mode 100644 index 0000000..3ae33d7 --- /dev/null +++ b/src/Providers/Provider_Factory.php @@ -0,0 +1,164 @@ + */ + private array $provider_classes; + + /** + * @param array $provider_classes Optional override list. + */ + public function __construct( array $provider_classes = [] ) { + $defaults = [ + '\\Tribe\\Tribe_Embed\\Providers\\YouTube', + '\\Tribe\\Tribe_Embed\\Providers\\Vimeo', + '\\Tribe\\Tribe_Embed\\Providers\\Dailymotion', + '\\Tribe\\Tribe_Embed\\Providers\\Wistia', + ]; + + /** Allow external override of provider class list */ + $filtered = apply_filters( 'tribe_embeds_provider_classes', $provider_classes ?: $defaults ); + + $this->provider_classes = array_values( array_filter( + array_map( 'strval', (array) $filtered ), + static function ( string $class ): bool { + return class_exists( $class ); + } + ) ); + } + + /** + * Resolve provider instance or null. + * + * @param array $video_url_data Parsed URL parts incl. 'host' + * @param array $block Gutenberg block array + */ + public function resolve( array $video_url_data, array $block ): ?object { + /** Allow short-circuit with a ready-made provider instance */ + $maybe = apply_filters( 'tribe_embeds_video_provider', null, $video_url_data, $block ); + if ( is_object( $maybe ) ) { + return $maybe; + } + + /** Match by per-provider allowed hosts */ + $host = strtolower( (string) ($video_url_data['host'] ?? '') ); + + if ( $host !== '' ) { + foreach ( $this->provider_classes as $class ) { + if ( in_array( $host, $this->allowed_hosts_for( $class ), true ) || $this->is_allowed_hosts_by_regex( $class, $host ) ) { + return $this->instantiate( $class, $video_url_data ); + } + } + } + + return null; + } + + /** + * Get allowed hosts for a provider (constant + filters). + * + * Filters: + * - tribe_embeds_allowed_provider_hosts_{slug} + * - tribe_embeds_allowed_provider_hosts + * + * @return array Lowercased hostnames. + */ + public function allowed_hosts_for( string $provider_class ): array { + $global = $this->get_allowed_rules( $provider_class ); + + return array_values( array_unique( array_map( 'strtolower', array_filter( $global, 'is_string' ) ) ) ); + } + + public function is_allowed_hosts_by_regex( string $provider_class, string $host ): bool { + $global = $this->get_allowed_rules( $provider_class ); + + foreach ( $global as $pattern ) { + if ( ! preg_match( '/' . $pattern . '/i', $host ) ) { + continue; + } + + return true; + } + + return false; + } + + public function get_allowed_rules( string $provider_class ): array { + $base = []; + + if ( defined( $provider_class . '::ALLOWED_HOSTS' ) ) { + /** @var array $base */ + $base = (array) $provider_class::ALLOWED_HOSTS; + } + + $slug = $this->provider_slug( $provider_class ); + + $by_provider = apply_filters( 'tribe_embeds_allowed_provider_hosts_' . $slug, $base, $provider_class ); + + return apply_filters( 'tribe_embeds_allowed_provider_hosts', $by_provider, $provider_class ); + } + + /** + * Get image sizes for a provider (constant + filters). + * + * Filters: + * - tribe_embeds_image_sizes_{slug} + * - tribe_embeds_image_sizes + * + * @return array + */ + public function image_sizes_for( string $provider_class ): array { + $base = []; + + if ( defined( $provider_class . '::IMAGE_SIZES' ) ) { + /** @var array $base */ + $base = (array) $provider_class::IMAGE_SIZES; + } + + $slug = $this->provider_slug( $provider_class ); + + $by_provider = apply_filters( 'tribe_embeds_image_sizes_' . $slug, $base, $provider_class ); + $global = apply_filters( 'tribe_embeds_image_sizes', $by_provider, $provider_class ); + + return (array) $global; + } + + /** + * Slug used in filter names. + * Uses class SLUG constant if present; otherwise derived from class name. + */ + public function provider_slug( string $provider_class ): string { + if ( defined( $provider_class . '::SLUG' ) ) { + $slug = (string) $provider_class::SLUG; + if ( $slug !== '' ) { + return sanitize_key( $slug ); + } + } + + $base = strtolower( trim( (string) strrchr( '\\' . ltrim( $provider_class, '\\' ), '\\' ), '\\' ) ); + $base = preg_replace( '/_provider$|provider$/', '', $base ); + + return sanitize_key( $base ?: 'provider' ); + } + + /** + * Instantiate provider safely. + */ + private function instantiate( string $class, array $video_url_data ): ?object { + try { + return new $class( $video_url_data ); + } catch ( \Throwable $e ) { + try { + return new $class(); + } catch ( \Throwable ) { + return null; + } + } + } + +} diff --git a/src/Providers/Vimeo.php b/src/Providers/Vimeo.php index f33533e..af503a2 100644 --- a/src/Providers/Vimeo.php +++ b/src/Providers/Vimeo.php @@ -2,23 +2,20 @@ namespace Tribe\Tribe_Embed\Providers; -final class Vimeo extends Provider { +use Tribe\Tribe_Embed\Admin\Credentials; - public const BASE_URL = 'https://vimeo.com/api/v2/video/'; +final class Vimeo extends Provider { - public const IMAGE_SIZES = [ - 'thumbnail_small', - 'thumbnail_medium', - 'thumbnail_large', - ]; + public const BASE_URL = 'https://api.vimeo.com/videos/%s/pictures'; public const ALLOWED_HOSTS = [ + 'player.vimeo.com', 'www.vimeo.com', 'vimeo.com', ]; /** - * Return the vimeo video thumbnail urls. + * Return the Vimeo video thumbnail URLs. */ public function get_thumbnail_data(): array { @@ -34,37 +31,22 @@ public function get_thumbnail_data(): array { if ( false === $image_data ) { $image_data = []; - // get the video details from the api. - $video_details = wp_remote_get( - self::BASE_URL . esc_attr( $this->get_video_id() ) . '.json' - ); - - // if the request to the hi res image errors or returns anything other than a http 200 response code. - if ( ( is_wp_error( $video_details )) && ( 200 !== wp_remote_retrieve_response_code( $video_details ) ) ) { - return []; - } - - // grab the body of the response. - $response_body = json_decode( - wp_remote_retrieve_body( - $video_details - ) - ); + $response_body = $this->get_video_pictures(); - if ( $response_body === null ) { + if ( empty( $response_body ) || empty( $response_body['data'] ) || empty( $response_body['data'][0]['sizes'] ) ) { return []; } - foreach ( self::IMAGE_SIZES as $resolution ) { + foreach ( $response_body['data'][0]['sizes'] as $resolution ) { // get the image url from the json. - $image_url = $response_body[0]->$resolution; + $image_url = $resolution['link']; + $width = $resolution['width']; + $height = $resolution['height']; - $image_size = getimagesize( $image_url ); - $width = $image_size[0]; - $height = $image_size[1]; + $resolution_name = sprintf( 'thumbnail_%s_%s', $width, $height ); // set the image data - $image_data[ $resolution ] = [ + $image_data[ $resolution_name ] = [ 'url' => $image_url, 'width' => $width, 'height' => $height, @@ -76,23 +58,75 @@ public function get_thumbnail_data(): array { } // return the url. - return apply_filters( 'tribe-embed_vimeo_video_thumbnail_url', $image_data, $this->get_video_id() ); + return apply_filters( 'tribe_embed_vimeo_video_thumbnail_url', $image_data, $this->get_video_id() ); + } + + protected function get_video_pictures(): array { + $token = $this->get_token(); + + if ( empty( $token ) ) { + return []; + } + + // get the video details from the api. + $video_details = wp_remote_get( + sprintf( self::BASE_URL, $this->get_video_id() ), + [ + 'headers' => [ + 'Authorization' => sprintf( 'Bearer %s', $token ), + 'Accept' => 'application/json', + ], + ] + ); + + // if the request to the hi-res image errors or returns anything other than a http 200 response code. + if ( is_wp_error( $video_details ) || 200 !== wp_remote_retrieve_response_code( $video_details ) ) { + return []; + } + + // grab the body of the response. + $response_body = json_decode( + wp_remote_retrieve_body( + $video_details + ), + true + ); + + if ( $response_body === null ) { + return []; + } + + return $response_body; } protected function set_video_id(): string { switch ( $this->video_url['host'] ) { case 'vimeo.com': case 'www.vimeo.com': - // if we have a path. - if ( $this->video_url['path'] === '' ) { - return $this->video_url['path']; + if ( empty( $this->video_url['path'] ) ) { + return ''; + } + + $maybe_find_correct_id = explode( '/', trim( $this->video_url['path'] ) ); + + if ( is_array( $maybe_find_correct_id ) && isset( $maybe_find_correct_id[2] ) ) { + // urls like https://vimeo.com/1083696811/fd0767701e + return $maybe_find_correct_id[1]; } // remove the preceeding slash. return str_replace( '/', '', $this->video_url['path'] ); - break; + case 'player.vimeo.com': + return str_replace( '/video/', '', $this->video_url['path'] ); + + default: + return ''; } } + protected function get_token(): string { + return Credentials::get_vimeo_token(); + } + } diff --git a/src/Providers/Wistia.php b/src/Providers/Wistia.php new file mode 100644 index 0000000..dc59c4e --- /dev/null +++ b/src/Providers/Wistia.php @@ -0,0 +1,117 @@ +get_token(); + // if we have no video id. + if ( '' === $this->get_video_id() || empty( $token ) ) { + return []; + } + + // get the URL from the transient. + $image_data = get_transient( 'tribe-embed_' . $this->get_video_id() ); + + if ( empty( $image_data ) ) { + $image_data = []; + + $video_details = wp_remote_get(self::BASE_URL . $this->get_video_id(), [ + 'headers' => [ + 'authorization' => 'Bearer ' . $token, + 'accept' => 'application/json', + 'content-type' => 'application/json', + ], + ] ); + + // if the request to the image errors or returns anything other than a http 200 response code. + if ( ( is_wp_error( $video_details ) ) || ( 200 !== wp_remote_retrieve_response_code( $video_details ) ) ) { + return []; + } + + // grab the body of the response. + $response_body = json_decode( + wp_remote_retrieve_body( + $video_details + ) + ); + + if ( $response_body === null ) { + return []; + } + + foreach ( self::IMAGE_SIZES as $resolution ) { + if ( empty( $response_body[0]->thumbnail ) || empty( $response_body[0]->thumbnail->url ) ) { + continue; + } + + $image_url = strtok( $response_body[0]->thumbnail->url, '?' ); + switch ( $resolution ) { + case 'thumbnail_640_url': + $image_url = add_query_arg( [ + 'image_crop_resized' => '640x360', + ], $image_url ); + break; + case 'thumbnail_320_url': + default: + $image_url = add_query_arg( [ + 'image_crop_resized' => '320x260', + ], $image_url ); + break; + } + + + $image_size = getimagesize( $image_url ); + $width = $image_size[0]; + $height = $image_size[1]; + // set the image data + $image_data[ $resolution ] = [ + 'url' => $image_url, + 'width' => $width, + 'height' => $height, + ]; + } + + // set the transient, storing the image url. + set_transient( 'tribe-embed_' . $this->get_video_id(), $image_data, DAY_IN_SECONDS ); + } + + // return the url. + return apply_filters( 'tribe_embed_wistia_video_thumbnail_url', $image_data, $this->get_video_id() ); + } + + public function uses_inline_embed(): bool { + return false; + } + + protected function set_video_id(): string { + if ( empty( $this->video_url['path'] ) ) { + return ''; + } + + // remove the preceeding slash. + return str_replace( '/medias/', '', $this->video_url['path'] ); + } + + protected function get_token(): string { + return Credentials::get_wistia_token(); + } + +} diff --git a/src/Providers/YouTube.php b/src/Providers/YouTube.php index 687d276..8b7ab28 100644 --- a/src/Providers/YouTube.php +++ b/src/Providers/YouTube.php @@ -20,7 +20,7 @@ final class YouTube extends Provider { ]; /** - * Accepts a video id and returns an array of thumbnail data + * Accepts a video id and returns an array of thumbnail data. */ public function get_thumbnail_data(): array { @@ -63,7 +63,7 @@ public function get_thumbnail_data(): array { } // return the thumbnail urls. - return apply_filters( 'tribe-embed_youtube_video_thumbnail_data', $image_data, $this->get_video_id() ); + return apply_filters( 'tribe_embed_youtube_video_thumbnail_data', $image_data, $this->get_video_id() ); } protected function set_video_id(): string { @@ -72,6 +72,22 @@ protected function set_video_id(): string { // for standard youtube URLs case 'www.youtube.com': case 'youtube.com': + // parse the query part of the URL into its arguments. + if ( ! empty( $this->video_url['path'] ) && str_starts_with( $this->video_url['path'], '/shorts/' ) ) { + // Extract video ID from /shorts/VIDEO_ID path + $path_parts = explode( '/', trim( $this->video_url['path'], '/' ) ); + if ( count( $path_parts ) >= 2 && $path_parts[0] === 'shorts' ) { + return $path_parts[1]; + } + + return ''; + } + + // Handle standard YouTube URLs with query parameters + if ( empty( $this->video_url['query'] ) ) { + return ''; + } + // parse the query part of the URL into its arguments. parse_str( $this->video_url['query'], $video_url_query_args ); @@ -83,9 +99,6 @@ protected function set_video_id(): string { // set the video id to the v query arg. return $video_url_query_args['v']; - break; - - // for youtube short urls. case 'youtu.be': // if we have a path. if ( empty( $this->video_url['path'] ) ) { @@ -94,9 +107,9 @@ protected function set_video_id(): string { // remove the preceeding slash. return str_replace( '/', '', $this->video_url['path'] ); - - break; } + + return ''; } } diff --git a/src/Util/Assets.php b/src/Util/Assets.php new file mode 100644 index 0000000..0b2ee02 --- /dev/null +++ b/src/Util/Assets.php @@ -0,0 +1,35 @@ +plugin_name = $plugin_name; + $this->version = $version; + } + + /** + * Registers the admin scripts + */ + public function register_admin_scripts(): void { + $asset_file = include TRIBE_MP_PATH . 'dist/editor.asset.php'; + wp_enqueue_script( $this->plugin_name . '-admin', TRIBE_MP_URL . 'dist/editor.js', $asset_file['dependencies'], (string) $asset_file['version'] ); + wp_enqueue_style( $this->plugin_name . '-admin', TRIBE_MP_URL . 'dist/editor.css', [], (string) $asset_file['version'] ); + } + + /** + * Registers the public scripts + */ + public function register_public_scripts(): void { + $asset_file = include TRIBE_MP_PATH . 'dist/index.asset.php'; + wp_enqueue_script( $this->plugin_name . '-public', TRIBE_MP_URL . 'dist/index.js', $asset_file['dependencies'], (string) $asset_file['version'] ); + wp_enqueue_style( $this->plugin_name . '-public', TRIBE_MP_URL . 'dist/style-index.css', [], (string) $asset_file['version'] ); + } + +} diff --git a/src/Util/Block_Filter.php b/src/Util/Block_Filter.php new file mode 100644 index 0000000..1517552 --- /dev/null +++ b/src/Util/Block_Filter.php @@ -0,0 +1,85 @@ +url_parser = $url_parser; + $this->factory = $factory; + $this->thumbs = $thumbs; + $this->facade = $facade; + } + + /** Attach filter once */ + public function register_hooks(): void { + if ( $this->hooks_added ) { + return; + } + add_filter( 'render_block', [ $this, 'filter_render_block' ], 10, 2 ); + $this->hooks_added = true; + } + + /** + * Filter callback for render_block + * - Only processes core/embed blocks + * - Provider may render fully or fallback to facade + * + * @param string $html Original block HTML + * @param array $block Block data + */ + public function filter_render_block( string $html, array $block ): string { + if ( ! isset( $block['blockName'] ) || $block['blockName'] !== 'core/embed' ) { + return $html; + } + + $attrs = (array) ($block['attrs'] ?? []); + $url = isset( $attrs['url'] ) && is_string( $attrs['url'] ) ? $attrs['url'] : ''; + if ( $url === '' ) { + return $html; + } + + $video_url_data = $this->url_parser->parse_url( $url ); + if ( $video_url_data === null ) { + return $html; + } + + // Fallback: use thumbnails and facade + $result = $this->thumbs->resolve_thumb( $video_url_data, $block ); + + if ( $result === null ) { + return $html; + } + + $facade_html = $this->facade->build( $result['thumb'], $block, $result['video_id'], $result['provider'] ); + + /** Filter the facade HTML before output. */ + $facade_html = (string) apply_filters( 'tribe_embeds_facade_html', $facade_html, $result['provider'], $block, $html ); + + ob_start(); + wp_print_styles( 'tribe-embeds-styles' ); + echo $facade_html; + + return ob_get_clean(); + } + +} diff --git a/src/Util/Facade_Builder.php b/src/Util/Facade_Builder.php new file mode 100644 index 0000000..a6871f9 --- /dev/null +++ b/src/Util/Facade_Builder.php @@ -0,0 +1,235 @@ + for video embeds. + */ +final class Facade_Builder { + + /** + * Build facade image HTML. + * + * @param array $thumb + * @param array $block + * @param string $video_id + */ + public function build( array $thumb, array $block, string $video_id, Provider $provider ): string { + $wrapper_classes = $this->get_wrapper_classes( $block, $video_id, $thumb ); + + $content = $this->open_markup_figure_element( $block, $video_id, $thumb, $wrapper_classes ); + $content .= $this->add_video_play_button( $block, $video_id, $thumb, $wrapper_classes ); + $content .= $this->add_video_thumbnail_markup( $block, $video_id, $thumb, $wrapper_classes ); + + if ( $provider->uses_inline_embed() ) { + $content .= $this->add_raw_original_embed( $block, $video_id, $thumb, $wrapper_classes, $provider ); + $content .= $this->close_markup_figure_element( $block, $video_id, $thumb, $wrapper_classes ); + + return $content; + } + $content .= $this->close_markup_figure_element( $block, $video_id, $thumb, $wrapper_classes ); + $content .= $this->add_original_embed_template( $block, $video_id, $thumb, $wrapper_classes, $provider ); + + return $content; + } + + /** + * Get block wrapper classes + * + * @param array $block + * @param string $video_id + * @param array $thumbnail_data + */ + public function get_wrapper_classes( array $block, string $video_id, array $thumbnail_data ): array { + $wrapper_classes = [ + 'wp-block-image', + 'tribe-embed', + 'is--' . ( $block['attrs']['providerNameSlug'] ?? 'default' ), + ]; + + // if we have classNames on the embed block. + if ( ! empty( $block['attrs']['className'] ) ) { + // explode the className string into array. + $class_names = explode( ' ', $block['attrs']['className'] ); + + // merge the class names into the figures classes array. + $wrapper_classes = array_merge( $wrapper_classes, $class_names ); + } + + // if the embed block has an alignment. + if ( ! empty( $block['attrs']['align'] ) ) { + // add the alignment class to the figure classes. + $wrapper_classes[] = 'align' . $block['attrs']['align']; + } + + // allow the classes to be filtered. + return apply_filters( 'tribe_embeds_video_wrapper_classes', $wrapper_classes, $block, $video_id, $thumbnail_data ); + } + + /** + * Adds the play button div to the markup. + * + * @param array $block + * @param string $video_id + * @param array $thumbnail_data + * @param array $wrapper_classes + */ + public function add_video_play_button( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): string { + $button = sprintf( '', esc_html__( 'Play Video', 'tribe-embeds' ) ); + + return apply_filters( 'tribe_embeds_video_button_html', $button, $block, $video_id, $thumbnail_data, $wrapper_classes ); + } + + /** + * Adds the video thumbnail markup output. + * + * @param array $block The block array. + * @param string $video_id The ID of the embedded video. + * @param array $thumbnail_data The URL of the video thumbnail. + * @param array $wrapper_classes An array of CSS classes to add to the wrapper. + */ + public function add_video_thumbnail_markup( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): string { + + $max_res_image = end( $thumbnail_data ); + $srcset = []; + $sizes = [ '(max-width: ' . $max_res_image['width'] . 'px) 100vw', $max_res_image['width'] . 'px' ]; + + foreach ( $thumbnail_data as $data ) { + $srcset[] = $data['url'] . ' ' . $data['width'] . 'w'; + } + + $classes = apply_filters( 'tribe_embeds_video_thumb_classes', [ + 'tribe-embed__thumbnail', + ], $block, $video_id, $thumbnail_data, $wrapper_classes ); + + $image_tag = sprintf( + '', + $max_res_image['width'], + $max_res_image['height'], + implode( ' ', $classes ), + $max_res_image['url'], + implode( ',', $srcset ), + implode( ',', $sizes ) + ); + + return apply_filters( 'tribe_embeds_video_thumb_markup', $image_tag, $block, $video_id, $thumbnail_data, $wrapper_classes ); + } + + /** + * Adds the closing figure element to the thumbnail markup. + * + * @param array $block The block array. + * @param string $video_id The ID of the embedded video. + * @param array $thumbnail_data The URL of the video thumbnail. + * @param array $wrapper_classes An array of CSS classes to add to the wrapper. + */ + public function close_markup_figure_element( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): string { + $html = ''; + + return apply_filters( 'tribe_embeds_video_thumb_close_markup', $html, $block, $video_id, $thumbnail_data, $wrapper_classes ); + } + + /** + * Adds the opening figure element to the thumbnail markup. + * + * @param array $block The block array. + * @param string $video_id The ID of the embedded video. + * @param array $thumbnail_data The URL of the video thumbnail. + * @param array $wrapper_classes An array of CSS classes to add to the wrapper. + */ + public function open_markup_figure_element( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes ): string { + $html = sprintf( + '
', + esc_attr( implode( ' ', $wrapper_classes ) ), + esc_attr( $video_id ) + ); + + return apply_filters( 'tribe_embeds_video_thumb_open_markup', $html, $block, $video_id, $thumbnail_data, $wrapper_classes ); + } + + /** + * Creates a escaping function to allowed certain HTML for embed content. + * Needed for when echoing the innerblock HTML. + */ + public function allowed_innerblock_html(): array { + /** + * Return the allowed html + * These are the elements in the rendered embed block for supported videos. + * This also includes everything you can add to an embed caption. + * Therefore, we need to allow these to keep the same structure. + */ + return [ + 'iframe' => [ + 'src' => true, + 'height' => true, + 'width' => true, + 'frameborder' => true, + 'allowfullscreen' => true, + ], + 'style' => [], + 'wistia-player' => [ + 'media-id' => true, + 'dnt' => true, + 'aspect' => true, + ], + 'script' => [ + 'src' => true, + 'type' => true, + 'async' => true, + ], + 'figure' => [ + 'class' => true, + ], + 'figcaption' => [ + 'class' => true, + ], + 'div' => [ + 'class' => true, + ], + 'a' => [ + 'class' => true, + 'href' => true, + 'data-type' => true, + ], + 'strong' => [], + 'em' => [], + 'sub' => [], + 'sup' => [], + 's' => [], + 'kbd' => [], + 'img' => [ + 'class' => true, + 'style' => true, + 'src' => true, + 'alt' => true, + ], + 'code' => [], + 'mark' => [ + 'style' => true, + 'class' => true, + ], + ]; + } + + /** + * Adds the original block markup to the template element. + * This is used when the item is cloned when the thumbnail is clicked. + * + * @param array $block The block array. + * @param string $video_id The ID of the embedded video. + * @param array $thumbnail_data The URL of the video thumbnail. + * @param array $wrapper_classes An array of CSS classes to add to the wrapper. + */ + public function add_original_embed_template( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes, Provider $provider ): string { + $html = sprintf( '', esc_attr( $video_id ), wp_kses( $block['innerHTML'], $this->allowed_innerblock_html() ) ); + + return apply_filters( 'tribe_embeds_video_embed_template', $html, $block, $video_id, $thumbnail_data, $wrapper_classes, $provider ); + } + + public function add_raw_original_embed( array $block, string $video_id, array $thumbnail_data, array $wrapper_classes, Provider $provider ): string { + return apply_filters( 'tribe_embeds_video_embed_template', wp_kses( $block['innerHTML'], $this->allowed_innerblock_html() ), $block, $video_id, $thumbnail_data, $wrapper_classes, $provider ); + } + +} diff --git a/src/Util/Thumbnail_Service.php b/src/Util/Thumbnail_Service.php new file mode 100644 index 0000000..c6ae3f2 --- /dev/null +++ b/src/Util/Thumbnail_Service.php @@ -0,0 +1,64 @@ +factory = $factory; + } + + /** + * Get provider, video id, and thumbnails. + * + * @return array{provider:object, video_id:string, thumb:array}|null + */ + public function resolve_thumb( array $video_url_data, array $block ): ?array { + $provider = $this->factory->resolve( $video_url_data, $block ); + if ( ! is_object( $provider ) ) { + return null; + } + + $video_id = $provider->get_video_id(); + + if ( ! $video_id ) { + return null; + } + + $image_sizes = $this->factory->image_sizes_for( $provider::class ); + + $thumb = $this->safe_get_thumbnail_data( $provider, $image_sizes ); + if ( empty( $thumb ) ) { + return null; + } + + return [ + 'provider' => $provider, + 'video_id' => $video_id, + 'thumb' => $thumb, + ]; + } + + /** Call provider get_thumbnail_data and filter results */ + private function safe_get_thumbnail_data( object $provider, array $image_sizes ): array { + if ( ! method_exists( $provider, 'get_thumbnail_data' ) ) { + return apply_filters( 'tribe_embeds_thumbnail_data', [], $provider, $image_sizes ); + } + + try { + $data = $provider->get_thumbnail_data(); + } catch ( \Throwable $e ) { + $data = []; + } + + return apply_filters( 'tribe_embeds_thumbnail_data', is_array( $data ) ? $data : [], $provider, $image_sizes ); + } + +} diff --git a/src/Util/Url_Parser.php b/src/Util/Url_Parser.php new file mode 100644 index 0000000..48af4cf --- /dev/null +++ b/src/Util/Url_Parser.php @@ -0,0 +1,38 @@ +|null + */ + public function parse_url( string $url ): ?array { + $url = trim( $url ); + if ( $url === '' ) { + return null; + } + + if ( str_starts_with( $url, '//' ) ) { + $url = 'https:' . $url; + } elseif ( ! preg_match( '#^https?://#i', $url ) ) { + $url = 'https://' . ltrim( $url, '/' ); + } + + $parts = parse_url( $url ); + if ( ! is_array( $parts ) || empty( $parts['host'] ) ) { + return null; + } + + $parts['host'] = strtolower( (string) $parts['host'] ); + $parts['url'] = $url; + + return $parts; + } + +} diff --git a/tribe-embed.php b/tribe-embed.php index 1662222..18abb84 100644 --- a/tribe-embed.php +++ b/tribe-embed.php @@ -4,14 +4,14 @@ * Plugin Name: Tribe Embed * Plugin URI: https://github.com/moderntribe/tribe-embed * Description: A Tribe Embed Plugin. - * Version: 1.1.0 + * Version: 2.0.0 * Requires at least: 6.3 * Requires PHP: 8.0 * Author: Modern Tribe * Author URI: https://github.com/moderntribe * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html - * Text Domain: tribe + * Text Domain: tribe-embeds * Domain Path: /languages * Update URI: false */ @@ -20,13 +20,25 @@ include dirname( __FILE__ ) . '/vendor/autoload.php'; +if ( ! defined( 'TRIBE_MP_PATH' ) ) { + define( 'TRIBE_MP_PATH', trailingslashit( plugin_dir_path( __FILE__ ) ) ); +} +if ( ! defined( 'TRIBE_MP_URL' ) ) { + define( 'TRIBE_MP_URL', plugin_dir_url( __FILE__ ) ); +} +if ( ! defined( 'TRIBE_MP_VERSION' ) ) { + define( 'TRIBE_MP_VERSION', Core::VERSION ); +} + register_activation_hook( __FILE__, [ Core::class, 'activate' ] ); register_deactivation_hook( __FILE__, [ Core::class, 'deactivate' ] ); add_action( 'plugins_loaded', static function (): void { - tribe_embed_core()->init( __file__ ); + $core = tribe_embed_core(); + $core->register_hooks(); } ); function tribe_embed_core(): Core { return Core::instance(); } +