PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | extensions/blocks/story/render.php +516 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,516 @@
1 +<?php
2 +/**
3 + * Story block render implementation.
4 + *
5 + * Loaded lazily from story.php only when the block is rendered, to keep the
6 + * render body out of the eager front-end PHP/opcache footprint.
7 + *
8 + * @package automattic/jetpack
9 + */
10 +
11 +namespace Automattic\Jetpack\Extensions\Story;
12 +
13 +use Automattic\Jetpack\Blocks;
14 +use Automattic\Jetpack\Connection\Connection_Assets;
15 +use Automattic\Jetpack\Post_Media\Images;
16 +use Jetpack;
17 +use Jetpack_Gutenberg;
18 +
19 +if ( ! defined( 'ABSPATH' ) ) {
20 + exit( 0 );
21 +}
22 +
23 +const EMBED_SIZE = array( 360, 640 ); // twice as many pixels for retina displays.
24 +const CROP_UP_TO = 0.2;
25 +const MAX_BULLETS = 7;
26 +const IMAGE_BREAKPOINTS = '(max-width: 460px) 576w, (max-width: 614px) 768w, 120vw'; // 120vw to match the 20% CROP_UP_TO ratio
27 +
28 +/**
29 + * Compare 2 urls and return true if they likely correspond to the same resource.
30 + * Ignore scheme, ports, query params and hashes and only compare hostname and pathname.
31 + *
32 + * @param string $url1 - First url used in comparison.
33 + * @param string $url2 - Second url used in comparison.
34 + *
35 + * @return boolean
36 + */
37 +function is_same_resource( $url1, $url2 ) {
38 + $url1_parsed = wp_parse_url( $url1 );
39 + $url2_parsed = wp_parse_url( $url2 );
40 + return isset( $url1_parsed['host'] ) &&
41 + isset( $url2_parsed['host'] ) &&
42 + isset( $url1_parsed['path'] ) &&
43 + isset( $url2_parsed['path'] ) &&
44 + $url1_parsed['host'] === $url2_parsed['host'] &&
45 + $url1_parsed['path'] === $url2_parsed['path'];
46 +}
47 +
48 +/**
49 + * Enrich media files retrieved from the story block attributes
50 + * with extra information we can retrieve from the media library.
51 + *
52 + * @param array $media_files - List of media, each as an array containing the media attributes.
53 + *
54 + * @return array $media_files
55 + */
56 +function enrich_media_files( $media_files ) {
57 + return array_filter(
58 + array_map(
59 + function ( $media_file ) {
60 + if ( ! is_array( $media_file ) || ! isset( $media_file['type'] ) ) {
61 + return null;
62 + }
63 + if ( 'image' === $media_file['type'] ) {
64 + return enrich_image_meta( $media_file );
65 + }
66 +
67 + /*
68 + * VideoPress videos can sometimes have type 'file', and mime 'video/videopress' or 'video/mp4'.
69 + * Let's fix `type` for those.
70 + */
71 + if (
72 + 'file' === $media_file['type']
73 + && isset( $media_file['mime'] )
74 + && str_starts_with( $media_file['mime'], 'video' )
75 + ) {
76 + $media_file['type'] = 'video';
77 + }
78 + if ( 'video' !== $media_file['type'] ) { // we only support images and videos at this point.
79 + return null;
80 + }
81 + return enrich_video_meta( $media_file );
82 + },
83 + $media_files
84 + )
85 + );
86 +}
87 +
88 +/**
89 + * Enrich image information with extra data we can retrieve from the media library.
90 + * Add missing `width`, `height`, `srcset`, `sizes`, `title`, `alt` and `caption` properties to the image.
91 + *
92 + * @param array $media_file - An array containing the media attributes for a specific image.
93 + *
94 + * @return array $media_file_enriched
95 + */
96 +function enrich_image_meta( $media_file ) {
97 + $attachment_id = $media_file['id'] ?? null;
98 + $image = wp_get_attachment_image_src( $attachment_id, 'full', false );
99 + if ( ! $image ) {
100 + return $media_file;
101 + }
102 + list( $src, $width, $height ) = $image;
103 + // Bail if url stored in block attributes is different than the media library one for that id.
104 + if ( isset( $media_file['url'] ) && ! is_same_resource( $media_file['url'], $src ) ) {
105 + return $media_file;
106 + }
107 + $image_meta = wp_get_attachment_metadata( $attachment_id );
108 + if ( ! is_array( $image_meta ) ) {
109 + return $media_file;
110 + }
111 + $size_array = array( absint( $width ), absint( $height ) );
112 + return array_merge(
113 + $media_file,
114 + array(
115 + 'width' => absint( $width ),
116 + 'height' => absint( $height ),
117 + 'srcset' => wp_calculate_image_srcset( $size_array, $src, $image_meta, $attachment_id ),
118 + 'sizes' => IMAGE_BREAKPOINTS,
119 + 'title' => get_the_title( $attachment_id ),
120 + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
121 + 'caption' => wp_get_attachment_caption( $attachment_id ),
122 + )
123 + );
124 +}
125 +
126 +/**
127 + * Enrich video information with extra data we can retrieve from the media library.
128 + * Add missing `width`, `height`, `alt`, `url`, `title`, `caption` and `poster` properties to the image.
129 + *
130 + * @param array $media_file - An array containing the media attributes for a specific video.
131 + *
132 + * @return array $media_file_enriched
133 + */
134 +function enrich_video_meta( $media_file ) {
135 + $attachment_id = $media_file['id'] ?? null;
136 + $video_meta = wp_get_attachment_metadata( $attachment_id );
137 + if ( ! $video_meta ) {
138 + return $media_file;
139 + }
140 +
141 + $video_url = ! empty( $video_meta['original']['url'] ) ? $video_meta['original']['url'] : wp_get_attachment_url( $attachment_id );
142 +
143 + // Set the poster attribute for the video tag if a poster image is available.
144 + $poster_url = null;
145 + if ( ! empty( $video_meta['videopress']['poster'] ) ) {
146 + $poster_url = $video_meta['videopress']['poster'];
147 + } elseif ( ! empty( $video_meta['thumb'] ) ) {
148 + $poster_url = str_replace( wp_basename( $video_url ), $video_meta['thumb'], $video_url );
149 + }
150 +
151 + if ( $poster_url ) {
152 + /*
153 + * Use the global content width for thumbnail resize so we match the `w=` query parameter
154 + * that jetpack is going to add when "Enable site accelerator" is enabled for images.
155 + */
156 + $content_width = (int) Jetpack::get_content_width();
157 + $new_width = $content_width > 0 ? $content_width : EMBED_SIZE[0];
158 + $poster_url = add_query_arg( 'w', $new_width, $poster_url );
159 + }
160 +
161 + return array_merge(
162 + $media_file,
163 + array(
164 + 'width' => absint( ! empty( $video_meta['width'] ) ? $video_meta['width'] : ( $media_file['width'] ?? 0 ) ),
165 + 'height' => absint( ! empty( $video_meta['height'] ) ? $video_meta['height'] : ( $media_file['height'] ?? 0 ) ),
166 + 'alt' => ! empty( $video_meta['videopress']['description'] ) ? $video_meta['videopress']['description'] : ( $media_file['alt'] ?? '' ),
167 + 'url' => $video_url,
168 + 'title' => get_the_title( $attachment_id ),
169 + 'caption' => wp_get_attachment_caption( $attachment_id ),
170 + 'poster' => $poster_url,
171 + )
172 + );
173 +}
174 +
175 +/**
176 + * Render an image inside a slide
177 + *
178 + * @param array $media - Image information.
179 + *
180 + * @return string
181 + */
182 +function render_image( $media ) {
183 + $src = '';
184 + $width = null;
185 + $height = null;
186 +
187 + if ( empty( $media['id'] ) || empty( $media['url'] ) ) {
188 + return __( 'Error retrieving media', 'jetpack' );
189 + }
190 + $image = wp_get_attachment_image_src( $media['id'], 'full', false );
191 + if ( $image ) {
192 + list( $src, $width, $height ) = $image;
193 + }
194 +
195 + // if image does not match.
196 + if ( ! $image || isset( $media['url'] ) && ! is_same_resource( $media['url'], $src ?? '' ) ) {
197 + $width = $media['width'] ?? null;
198 + $height = $media['height'] ?? null;
199 + $title = $media['title'] ?? '';
200 + $alt = $media['alt'] ?? '';
201 + return sprintf(
202 + '<img
203 + title="%1$s"
204 + alt="%2$s"
205 + class="wp-block-jetpack-story_image wp-story-image %3$s"
206 + src="%4$s"
207 + />',
208 + esc_attr( $title ),
209 + esc_attr( $alt ),
210 + $width && $height ? get_image_crop_class( $width, $height ) : '',
211 + esc_attr( $media['url'] )
212 + );
213 + }
214 +
215 + $crop_class = get_image_crop_class( $width, $height );
216 +
217 + /*
218 + * need to specify the size of the embed so it picks an image that is large enough for the `src` attribute
219 + * `sizes` is optimized for 1080x1920 (9:16) images
220 + * Note that the Story block does not have thumbnail support, it will load the right
221 + * image based on the viewport size only.
222 + */
223 + return wp_get_attachment_image(
224 + $media['id'],
225 + EMBED_SIZE,
226 + false,
227 + array(
228 + 'class' => sprintf( 'wp-story-image wp-image-%d %s', $media['id'], $crop_class ),
229 + 'sizes' => IMAGE_BREAKPOINTS,
230 + 'title' => get_the_title( $media['id'] ),
231 + )
232 + );
233 +}
234 +
235 +/**
236 + * Return the css crop class if image width and height requires it
237 + *
238 + * @param int $width - Image width.
239 + * @param int $height - Image height.
240 + *
241 + * @return string The CSS class which will display a cropped image
242 + */
243 +function get_image_crop_class( $width, $height ) {
244 + $crop_class = '';
245 + $width = (int) $width;
246 + $height = (int) $height;
247 + if ( ! $width || ! $height ) {
248 + return $crop_class;
249 + }
250 + $media_aspect_ratio = $width / $height;
251 + $target_aspect_ratio = EMBED_SIZE[0] / EMBED_SIZE[1];
252 + if ( $media_aspect_ratio >= $target_aspect_ratio ) {
253 + // image wider than canvas.
254 + $media_too_wide_to_crop = $media_aspect_ratio > $target_aspect_ratio / ( 1 - CROP_UP_TO );
255 + if ( ! $media_too_wide_to_crop ) {
256 + $crop_class = 'wp-story-crop-wide';
257 + }
258 + } else {
259 + // image narrower than canvas.
260 + $media_too_narrow_to_crop = $media_aspect_ratio < $target_aspect_ratio * ( 1 - CROP_UP_TO );
261 + if ( ! $media_too_narrow_to_crop ) {
262 + $crop_class = 'wp-story-crop-narrow';
263 + }
264 + }
265 + return $crop_class;
266 +}
267 +
268 +/**
269 + * Returns a URL for the site icon.
270 + *
271 + * @param int $size - Size for (square) sitei icon.
272 + * @param string $fallback - Fallback URL to use if no site icon is found.
273 + *
274 + * @return string
275 + */
276 +function get_blavatar_or_site_icon_url( $size, $fallback ) {
277 + $image_array = Images::from_blavatar( get_the_ID(), $size );
278 + if ( ! empty( $image_array ) ) {
279 + return $image_array[0]['src'];
280 + } else {
281 + return $fallback;
282 + }
283 +}
284 +
285 +/**
286 + * Render a video inside a slide
287 + *
288 + * @param array $media - Video information.
289 + *
290 + * @return string
291 + */
292 +function render_video( $media ) {
293 + if ( empty( $media['id'] ) || empty( $media['mime'] ) || empty( $media['url'] ) ) {
294 + return __( 'Error retrieving media', 'jetpack' );
295 + }
296 +
297 + if ( ! empty( $media['poster'] ) ) {
298 + return render_image(
299 + array_merge(
300 + $media,
301 + array(
302 + 'type' => 'image',
303 + 'url' => $media['poster'],
304 + )
305 + )
306 + );
307 + }
308 +
309 + return sprintf(
310 + '<video
311 + title="%1$s"
312 + type="%2$s"
313 + class="wp-story-video intrinsic-ignore wp-video-%3$s"
314 + data-id="%3$d"
315 + src="%4$s">
316 + </video>',
317 + esc_attr( get_the_title( $media['id'] ) ),
318 + esc_attr( $media['mime'] ),
319 + absint( $media['id'] ),
320 + esc_attr( $media['url'] )
321 + );
322 +}
323 +
324 +/**
325 + * Pick a thumbnail to render a static/embedded story
326 + *
327 + * @param array $media_files - list of Media files.
328 + *
329 + * @return string
330 + */
331 +function render_static_slide( $media_files ) {
332 + $media_template = '';
333 + if ( empty( $media_files ) ) {
334 + return '';
335 + }
336 +
337 + // find an image to showcase.
338 + foreach ( $media_files as $media ) {
339 + switch ( $media['type'] ) {
340 + case 'image':
341 + $media_template = render_image( $media );
342 + break 2;
343 + case 'video':
344 + // ignore videos without a poster image.
345 + if ( empty( $media['poster'] ) ) {
346 + continue 2;
347 + }
348 + $media_template = render_video( $media );
349 + break 2;
350 + }
351 + }
352 +
353 + // if no "static" media was found for the thumbnail try to render a video tag without poster.
354 + if ( empty( $media_template ) ) {
355 + // enrich_media_files() may return an array with no zero-index, so use reset()
356 + $media_template = render_video( reset( $media_files ) );
357 + }
358 +
359 + return sprintf(
360 + '<div class="wp-story-slide" style="display: block;">
361 + <figure>%s</figure>
362 + </div>',
363 + $media_template
364 + );
365 +}
366 +
367 +/**
368 + * Render the top right icon on top of the story embed
369 + *
370 + * @param array $settings - The block settings.
371 + *
372 + * @return string
373 + */
374 +function render_top_right_icon( $settings ) {
375 + $show_slide_count = $settings['showSlideCount'] ?? false;
376 + $slide_count = isset( $settings['slides'] ) ? count( $settings['slides'] ) : 0;
377 + if ( $show_slide_count ) {
378 + // Render the story block icon along with the slide count.
379 + return sprintf(
380 + '<div class="wp-story-embed-icon">
381 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false">
382 + <path d="M0 0h24v24H0z" fill="none"></path>
383 + <path fill-rule="evenodd" clip-rule="evenodd" d="M6 3H14V17H6L6 3ZM4 3C4 1.89543 4.89543 1 6 1H14C15.1046 1 16 1.89543 16 3V17C16 18.1046 15.1046 19 14 19H6C4.89543 19 4 18.1046 4 17V3ZM18 5C19.1046 5 20 5.89543 20 7V21C20 22.1046 19.1046 23 18 23H10C8.89543 23 8 22.1046 8 21H18V5Z"></path>
384 + </svg>
385 + <span>%d</span>
386 + </div>',
387 + $slide_count
388 + );
389 + } else {
390 + // Render the Fullscreen Gridicon.
391 + return (
392 + '<div class="wp-story-embed-icon-expand">
393 + <svg class="gridicon gridicons-fullscreen" role="img" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
394 + <g>
395 + <path d="M21 3v6h-2V6.41l-3.29 3.3-1.42-1.42L17.59 5H15V3zM3 3v6h2V6.41l3.29 3.3 1.42-1.42L6.41 5H9V3zm18 18v-6h-2v2.59l-3.29-3.29-1.41 1.41L17.59 19H15v2zM9 21v-2H6.41l3.29-3.29-1.41-1.42L5 17.59V15H3v6z"></path>
396 + </g>
397 + </svg>
398 + </div>'
399 + );
400 + }
401 +}
402 +
403 +/**
404 + * Render a pagination bullet
405 + *
406 + * @param int $slide_index - The slide index it corresponds to.
407 + * @param string $class_name - Optional css class name(s) to customize the bullet element.
408 + *
409 + * @return string
410 + */
411 +function render_pagination_bullet( $slide_index, $class_name = '' ) {
412 + return sprintf(
413 + '<div class="wp-story-pagination-bullet %s" aria-label="%s">
414 + <div class="wp-story-pagination-bullet-bar"></div>
415 + </div>',
416 + esc_attr( $class_name ),
417 + /* translators: %d is the slide number (1, 2, 3...) */
418 + sprintf( __( 'Go to slide %d', 'jetpack' ), $slide_index )
419 + );
420 +}
421 +
422 +/**
423 + * Render pagination on top of the story embed
424 + *
425 + * @param array $settings - The block settings.
426 + *
427 + * @return string
428 + */
429 +function render_pagination( $settings ) {
430 + $show_slide_count = $settings['showSlideCount'] ?? false;
431 + if ( $show_slide_count ) {
432 + return '';
433 + }
434 + $slide_count = isset( $settings['slides'] ) ? count( $settings['slides'] ) : 0;
435 + $bullet_count = min( $slide_count, MAX_BULLETS );
436 + $bullet_ellipsis = $slide_count > $bullet_count
437 + ? render_pagination_bullet( $bullet_count + 1, 'wp-story-pagination-ellipsis' )
438 + : '';
439 + return sprintf(
440 + '<div class="wp-story-pagination wp-story-pagination-bullets">
441 + %s
442 + </div>',
443 + implode( "\n", array_map( __NAMESPACE__ . '\render_pagination_bullet', range( 1, $bullet_count ) ) ) . $bullet_ellipsis
444 + );
445 +}
446 +
447 +/**
448 + * Render story block
449 + *
450 + * @param array $attributes - Block attributes.
451 + *
452 + * @return string
453 + */
454 +function render( $attributes ) {
455 + // Let's use a counter to have a different id for each story rendered in the same context.
456 + static $story_block_counter = 0;
457 +
458 + if ( 0 === $story_block_counter ) {
459 + // @todo Fix the webpack tree shaking so the block's view.js no longer depends on jetpack-connection, then remove this.
460 + Connection_Assets::register_assets();
461 + }
462 +
463 + Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
464 +
465 + $media_files = isset( $attributes['mediaFiles'] ) ? enrich_media_files( $attributes['mediaFiles'] ) : array();
466 + $settings_from_attributes = $attributes['settings'] ?? array();
467 +
468 + $settings = array_merge(
469 + $settings_from_attributes,
470 + array(
471 + 'slides' => $media_files,
472 + )
473 + );
474 +
475 + /* translators: Placehodlder if the Story block can't find a post title to use. */
476 + $story_title = in_the_loop() ? get_the_title() : __( 'Story', 'jetpack' );
477 +
478 + return sprintf(
479 + '<div class="%1$s" data-id="%2$s" data-settings="%3$s">
480 + <div class="wp-story-app">
481 + <div class="wp-story-display-contents" style="display: contents;">
482 + <a class="wp-story-container" href="%4$s" title="%5$s">
483 + <div class="wp-story-meta">
484 + <div class="wp-story-icon">
485 + <img alt="%6$s" src="%7$s" width="40" height="40">
486 + </div>
487 + <div>
488 + <div class="wp-story-title">
489 + %8$s
490 + </div>
491 + </div>
492 + </div>
493 + <div class="wp-story-wrapper">
494 + %9$s
495 + </div>
496 + <div class="wp-story-overlay">
497 + %10$s
498 + </div>
499 + %11$s
500 + </a>
501 + </div>
502 + </div>
503 + </div>',
504 + esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attributes, array( 'wp-story', 'aligncenter' ) ) ),
505 + esc_attr( 'wp-story-' . get_the_ID() . '-' . strval( ++$story_block_counter ) ),
506 + filter_var( wp_json_encode( $settings, JSON_UNESCAPED_SLASHES ), FILTER_SANITIZE_SPECIAL_CHARS ),
507 + get_permalink() . '?wp-story-load-in-fullscreen=true&amp;wp-story-play-on-load=true',
508 + __( 'Play story in new tab', 'jetpack' ),
509 + __( 'Site icon', 'jetpack' ),
510 + esc_attr( get_blavatar_or_site_icon_url( 80, includes_url( 'images/w-logo-blue.png' ) ) ),
511 + esc_html( $story_title ),
512 + render_static_slide( $media_files ),
513 + render_top_right_icon( $settings ),
514 + render_pagination( $settings )
515 + );
516 +}