PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / 3.2.0
GutSlider – All in One Slider and Carousel Blocks for Gutenberg v3.2.0
3.2.0 3.1.0 3.0.0 2.13.2 2.13.1 2.13.0 trunk 1.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.1 2.5.3 2.5.4 2.5.5 All 57 releases
slider-blocks / includes / Blocks / VideoEmbeds.php

VideoEmbeds.php in GutSlider – All in One Slider and Carousel Blocks for Gutenberg 3.2.0, at includes/Blocks/VideoEmbeds.php

295 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare( strict_types=1 );
3
4 namespace GutSlider\Blocks;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 /**
11 * Tunes the players of the videos carousel as the block renders.
12 *
13 * Applied while rendering, so the saved block markup (and block validation)
14 * is untouched and blocks saved long ago benefit too:
15 *
16 * - YouTube rejects embeds whose request carries no Referer header and shows
17 * "Video player configuration error (Error 153)". Sites that send a
18 * site-wide `Referrer-Policy: no-referrer` strip that header, so each
19 * YouTube iframe gets its own `referrerpolicy`.
20 * - "Autoplay on load" saved `autoplay=1` on every player, so all videos tried
21 * to start at once (and browsers block unmuted autoplay). Only the first
22 * video autoplays now, muted; a video behind a custom thumbnail waits for
23 * its play button.
24 * - Players after the first load lazily.
25 * - Every player used the same title ("video-carousel-slide"); screen readers
26 * now hear the video's own name, and thumbnail play buttons get a label.
27 *
28 * @package GutSlider\Blocks
29 * @since 3.1.1
30 */
31 final class VideoEmbeds {
32
33 /**
34 * Constructor.
35 *
36 * @since 3.1.1
37 */
38 public function __construct() {
39 add_filter( 'render_block_gutsliders/videos-carousel', array( $this, 'tune_players' ), 10, 2 );
40 add_filter( 'render_block_gutsliders/videos-carousel', array( $this, 'add_extras' ), 11, 2 );
41 }
42
43 /**
44 * Settings added after the block's markup was saved: the YouTube / Vimeo hover
45 * preview flag (Reels, Video Cards) and VideoObject structured data.
46 *
47 * @since 3.1.1
48 *
49 * @param string $block_content Rendered block HTML.
50 * @param array $block Parsed block.
51 * @return string Block HTML.
52 */
53 public function add_extras( $block_content, $block = array() ): string {
54 $block_content = (string) $block_content;
55 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
56
57 if ( ! empty( $attrs['embedPreview'] ) && in_array( $attrs['preset'] ?? '', array( 'reels', 'cards' ), true ) && class_exists( '\WP_HTML_Tag_Processor' ) ) {
58 $processor = new \WP_HTML_Tag_Processor( $block_content );
59 if ( $processor->next_tag( array( 'class_name' => 'wp-block-gutsliders-videos-carousel' ) ) ) {
60 $processor->set_attribute( 'data-embed-preview', 'true' );
61 $block_content = $processor->get_updated_html();
62 }
63 }
64
65 if ( ! empty( $attrs['videoSchema'] ) ) {
66 $block_content .= $this->get_schema( $attrs );
67 }
68
69 return $block_content;
70 }
71
72 /**
73 * VideoObject structured data for the carousel's videos (Google video results).
74 * Videos without a still image are left out: Google requires one.
75 *
76 * @since 3.1.1
77 *
78 * @param array $attrs Block attributes.
79 * @return string `<script type="application/ld+json">` or ''.
80 */
81 private function get_schema( array $attrs ): string {
82 $items = isset( $attrs['slideItems'] ) && is_array( $attrs['slideItems'] ) ? array_values( $attrs['slideItems'] ) : array();
83 $custom = ! empty( $attrs['enableCustomPoster'] );
84 $privacy = ! empty( $attrs['videoPlayer']['privacy'] );
85 $videos = array();
86
87 foreach ( $items as $index => $item ) {
88 if ( ! is_array( $item ) ) {
89 continue;
90 }
91 $type = $item['videoType'] ?? 'youtube';
92 $id = isset( $item['videoID'] ) ? (string) $item['videoID'] : '';
93 $file = 'file' === $type && ! empty( $item['video']['url'] ) ? (string) $item['video']['url'] : '';
94 $thumb = ( $custom || $file ) && ! empty( $item['thumbnail']['url'] ) ? (string) $item['thumbnail']['url'] : '';
95
96 if ( ! $thumb && 'youtube' === $type && '' !== $id ) {
97 $thumb = 'https://i.ytimg.com/vi/' . rawurlencode( $id ) . '/hqdefault.jpg';
98 }
99 if ( ! $thumb && 'vimeo' === $type && ! empty( $item['remoteThumb'] ) ) {
100 $thumb = (string) $item['remoteThumb'];
101 }
102 if ( ! $thumb || ( '' === $id && '' === $file ) ) {
103 continue;
104 }
105
106 $name = $this->get_video_name( $item, $index );
107 $video = array(
108 '@type' => 'VideoObject',
109 'name' => $name,
110 'description' => ! empty( $item['description'] ) ? wp_strip_all_tags( (string) $item['description'] ) : $name,
111 'thumbnailUrl' => array( $thumb ),
112 'uploadDate' => $this->get_upload_date( $item ),
113 );
114
115 if ( $file ) {
116 $video['contentUrl'] = $file;
117 } elseif ( 'vimeo' === $type ) {
118 $video['embedUrl'] = 'https://player.vimeo.com/video/' . rawurlencode( $id );
119 } else {
120 $video['embedUrl'] = ( $privacy ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com' ) . '/embed/' . rawurlencode( $id );
121 }
122
123 $duration = $this->get_iso_duration( isset( $item['duration'] ) ? (string) $item['duration'] : '' );
124 if ( $duration ) {
125 $video['duration'] = $duration;
126 }
127
128 $videos[] = $video;
129 }
130
131 if ( ! $videos ) {
132 return '';
133 }
134
135 $data = array(
136 '@context' => 'https://schema.org',
137 '@graph' => $videos,
138 );
139
140 return '<script type="application/ld+json">' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>';
141 }
142
143 /**
144 * A video's upload date (ISO 8601): the one set on the video, else the page's date.
145 *
146 * @since 3.1.1
147 *
148 * @param array $item Video item.
149 * @return string Date.
150 */
151 private function get_upload_date( array $item ): string {
152 if ( ! empty( $item['uploadDate'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $item['uploadDate'] ) ) {
153 try {
154 return ( new \DateTimeImmutable( $item['uploadDate'] . ' 00:00:00', wp_timezone() ) )->format( 'c' );
155 } catch ( \Exception $e ) {
156 unset( $e );
157 }
158 }
159 $date = get_the_date( 'c' );
160 return $date ? (string) $date : gmdate( 'c' );
161 }
162
163 /**
164 * "4:12" or "1:02:03" as an ISO 8601 duration (PT4M12S); '' when unreadable.
165 *
166 * @since 3.1.1
167 *
168 * @param string $duration Duration as shown on the card.
169 * @return string ISO duration.
170 */
171 private function get_iso_duration( string $duration ): string {
172 if ( ! preg_match( '/^(?:(\d+):)?(\d{1,2}):(\d{2})$/', trim( $duration ), $match ) ) {
173 return '';
174 }
175 $hours = (int) $match[1];
176 $minutes = (int) $match[2];
177 $seconds = (int) $match[3];
178 return 'PT' . ( $hours ? $hours . 'H' : '' ) . ( $minutes ? $minutes . 'M' : '' ) . $seconds . 'S';
179 }
180
181 /**
182 * Adjust the block's iframes and play buttons.
183 *
184 * @since 3.1.1
185 *
186 * @param string $block_content Rendered block HTML.
187 * @param array $block Parsed block.
188 * @return string Block HTML.
189 */
190 public function tune_players( $block_content, $block = array() ): string {
191 $block_content = (string) $block_content;
192
193 if ( false === stripos( $block_content, '<iframe' ) && false === stripos( $block_content, '<video' ) && false === stripos( $block_content, 'play-btn' ) ) {
194 return $block_content;
195 }
196
197 if ( ! class_exists( '\WP_HTML_Tag_Processor' ) ) {
198 return $block_content;
199 }
200
201 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
202 $items = isset( $attrs['slideItems'] ) && is_array( $attrs['slideItems'] ) ? array_values( $attrs['slideItems'] ) : array();
203 $custom_poster = ! empty( $attrs['enableCustomPoster'] );
204
205 $processor = new \WP_HTML_Tag_Processor( $block_content );
206 $slide = -1;
207 $player = 0;
208
209 while ( $processor->next_tag() ) {
210 $tag = $processor->get_tag();
211
212 // Each slide holds one video; count slides to find its item.
213 if ( 'DIV' === $tag && $processor->has_class( 'swiper-slide' ) ) {
214 ++$slide;
215 continue;
216 }
217
218 $item = isset( $items[ $slide ] ) && is_array( $items[ $slide ] ) ? $items[ $slide ] : array();
219 $name = $this->get_video_name( $item, max( 0, $slide ) );
220
221 if ( 'BUTTON' === $tag && $processor->has_class( 'play-btn' ) && null === $processor->get_attribute( 'aria-label' ) ) {
222 /* translators: %s: video name. */
223 $processor->set_attribute( 'aria-label', sprintf( __( 'Play video: %s', 'slider-blocks' ), $name ) );
224 continue;
225 }
226
227 // Self-hosted players (Classic): a name for screen readers.
228 if ( 'VIDEO' === $tag && $processor->has_class( 'video-carousel-slide' ) ) {
229 if ( null === $processor->get_attribute( 'aria-label' ) ) {
230 $processor->set_attribute( 'aria-label', $name );
231 }
232 continue;
233 }
234
235 if ( 'IFRAME' !== $tag ) {
236 continue;
237 }
238
239 $src = (string) $processor->get_attribute( 'src' );
240
241 if ( 'video-carousel-slide' === $processor->get_attribute( 'title' ) ) {
242 $processor->set_attribute( 'title', $name );
243 }
244
245 // youtube.com or the privacy-enhanced youtube-nocookie.com.
246 if ( preg_match( '#youtube(?:-nocookie)?\.com/embed#i', $src ) && null === $processor->get_attribute( 'referrerpolicy' ) ) {
247 $processor->set_attribute( 'referrerpolicy', 'strict-origin-when-cross-origin' );
248 }
249
250 if ( preg_match( '/[?&]autoplay=1\b/', $src ) ) {
251 $has_thumbnail = $custom_poster && ! empty( $item['thumbnail']['url'] );
252
253 if ( 0 === $player && ! $has_thumbnail ) {
254 // Browsers only start playback by themselves when it's muted.
255 $src = (string) preg_replace( '/([?&])mute=0\b/', '$1mute=1', $src );
256 if ( ! preg_match( '/[?&]mute=1\b/', $src ) ) {
257 $src .= ( false === strpos( $src, '?' ) ? '?' : '&' ) . 'mute=1';
258 }
259 } else {
260 $src = (string) preg_replace( '/([?&])autoplay=1\b/', '$1autoplay=0', $src );
261 }
262 $processor->set_attribute( 'src', $src );
263 }
264
265 if ( $player > 0 && null === $processor->get_attribute( 'loading' ) ) {
266 $processor->set_attribute( 'loading', 'lazy' );
267 }
268
269 ++$player;
270 }
271
272 return $processor->get_updated_html();
273 }
274
275 /**
276 * A human name for a video: its title, its editor name, else "Video N".
277 *
278 * @since 3.1.1
279 *
280 * @param array $item Video item.
281 * @param int $index Position in the carousel.
282 * @return string Name.
283 */
284 private function get_video_name( array $item, int $index ): string {
285 foreach ( array( 'title', 'panelTitle' ) as $key ) {
286 if ( ! empty( $item[ $key ] ) && is_string( $item[ $key ] ) ) {
287 return wp_strip_all_tags( $item[ $key ] );
288 }
289 }
290
291 /* translators: %d: video position in the carousel. */
292 return sprintf( __( 'Video %d', 'slider-blocks' ), $index + 1 );
293 }
294 }
295