PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.1-a.3
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / extensions / blocks / story / render.php
render.php
517 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
517