PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 | modules/carousel/jetpack-carousel.php +1582 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,1582 @@
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +/**
3 + * Module: Jetpack Carousel
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +use Automattic\Jetpack\Assets;
9 +use Automattic\Jetpack\Stats\Options as Stats_Options;
10 +use Automattic\Jetpack\Status;
11 +use Automattic\Jetpack\Status\Host;
12 +
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit( 0 );
15 +}
16 +
17 +/**
18 + * Jetpack_Carousel class.
19 + *
20 + * @phan-constructor-used-for-side-effects
21 + */
22 +class Jetpack_Carousel {
23 + /**
24 + * Defines Carousel pre-built widths
25 + *
26 + * @var array
27 + */
28 + public $prebuilt_widths = array( 370, 700, 1000, 1200, 1400, 2000 );
29 +
30 + /**
31 + * Localization strings and other data for the JavaScript
32 + *
33 + * @var array
34 + */
35 + public $localize_strings;
36 +
37 + /**
38 + * Represents whether or not this is the first load of Carousel on a page. Default is true.
39 + *
40 + * @var bool
41 + */
42 + public $first_run = true;
43 +
44 + /**
45 + * Determines whether or not to set in the gallery. Default is false.
46 + *
47 + * @deprecated since 10.8
48 + *
49 + * @var bool
50 + */
51 + public $in_gallery = false;
52 +
53 + /**
54 + * Determines whether the module runs in the Jetpack plugin, as opposed to WP.com Simple site environment
55 + *
56 + * @var bool
57 + */
58 + public $in_jetpack = true;
59 +
60 + /**
61 + * Determines whether or not a single image gallery is enabled. Default is false.
62 + *
63 + * @var bool
64 + */
65 + public $single_image_gallery_enabled = false;
66 +
67 + /**
68 + * Determines whether images that link to themselves should be replaced with a one image gallery. Default is false.
69 + *
70 + * @var bool
71 + */
72 + public $single_image_gallery_enabled_media_file = false;
73 +
74 + /**
75 + * Constructor.
76 + */
77 + public function __construct() {
78 + add_action( 'init', array( $this, 'init' ) );
79 + }
80 +
81 + /**
82 + * Initialize class
83 + */
84 + public function init() {
85 + if ( $this->maybe_disable_jp_carousel() ) {
86 + return;
87 + }
88 +
89 + $this->in_jetpack = ! ( new Host() )->is_wpcom_simple();
90 +
91 + $this->single_image_gallery_enabled = ! $this->maybe_disable_jp_carousel_single_images();
92 + $this->single_image_gallery_enabled_media_file = $this->maybe_enable_jp_carousel_single_images_media_file();
93 +
94 + if ( is_admin() ) {
95 + // Register the Carousel-related related settings.
96 + add_action( 'admin_init', array( $this, 'register_settings' ), 5 );
97 + if ( ! $this->in_jetpack ) {
98 + if ( 0 === $this->test_1or0_option( get_option( 'carousel_enable_it' ), true ) ) {
99 + return; // Carousel disabled, abort early, but still register setting so user can switch it back on.
100 + }
101 + }
102 + // If in admin, register the ajax endpoints.
103 + add_action( 'wp_ajax_get_attachment_comments', array( $this, 'get_attachment_comments' ) );
104 + add_action( 'wp_ajax_nopriv_get_attachment_comments', array( $this, 'get_attachment_comments' ) );
105 + add_action( 'wp_ajax_post_attachment_comment', array( $this, 'post_attachment_comment' ) );
106 + add_action( 'wp_ajax_nopriv_post_attachment_comment', array( $this, 'post_attachment_comment' ) );
107 + } else {
108 + if ( ! $this->in_jetpack ) {
109 + if ( 0 === $this->test_1or0_option( get_option( 'carousel_enable_it' ), true ) ) {
110 + return; // Carousel disabled, abort early.
111 + }
112 + }
113 + // If on front-end, do the Carousel thang.
114 + /**
115 + * Filter the array of default prebuilt widths used in Carousel.
116 + *
117 + * @module carousel
118 + *
119 + * @since 1.6.0
120 + *
121 + * @param array $this->prebuilt_widths Array of default widths.
122 + */
123 + $this->prebuilt_widths = apply_filters( 'jp_carousel_widths', $this->prebuilt_widths );
124 + // below: load later than other callbacks hooked it (e.g. 3rd party plugins handling gallery shortcode).
125 + add_filter( 'post_gallery', array( $this, 'check_if_shortcode_processed_and_enqueue_assets' ), 1000, 2 );
126 + add_filter( 'post_gallery', array( $this, 'set_in_gallery' ), -1000 );
127 + add_filter( 'gallery_style', array( $this, 'add_data_to_container' ) );
128 + add_filter( 'wp_get_attachment_image_attributes', array( $this, 'add_data_to_images' ), 10, 2 );
129 + add_filter( 'jetpack_tiled_galleries_block_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
130 + if ( $this->single_image_gallery_enabled ) {
131 + add_filter( 'the_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
132 + }
133 +
134 + add_filter( 'render_block_data', array( $this, 'remove_core_lightbox_in_gallery' ), 10, 3 );
135 +
136 + // `is_amp_request()` can't be called until the 'wp' filter.
137 + add_action( 'wp', array( $this, 'check_amp_support' ) );
138 + }
139 +
140 + if ( $this->in_jetpack ) {
141 + Jetpack::enable_module_configurable( dirname( __DIR__ ) . '/carousel.php' );
142 + }
143 + }
144 +
145 + /**
146 + * Check AMP and add filters.
147 + */
148 + public function check_amp_support() {
149 + if (
150 + ! class_exists( 'Jetpack_AMP_Support' )
151 + || ! Jetpack_AMP_Support::is_amp_request()
152 + ) {
153 + add_filter( 'render_block_core/gallery', array( $this, 'filter_gallery_block_render' ), 10, 2 );
154 + add_filter( 'render_block_jetpack/tiled-gallery', array( $this, 'filter_gallery_block_render' ), 10, 2 );
155 + }
156 + }
157 +
158 + /**
159 + * Returns the value of the applied jp_carousel_maybe_disable filter
160 + *
161 + * @since 1.6.0
162 + *
163 + * @return bool - Should Carousel be disabled? Default to false.
164 +  */
165 + public function maybe_disable_jp_carousel() {
166 + /**
167 + * Allow third-party plugins or themes to disable Carousel.
168 + *
169 + * @module carousel
170 + *
171 + * @since 1.6.0
172 + *
173 + * @param bool false Should Carousel be disabled? Default to false.
174 + */
175 + return apply_filters( 'jp_carousel_maybe_disable', false );
176 + }
177 +
178 + /**
179 + * Returns the value of the applied jp_carousel_maybe_disable_single_images filter
180 + *
181 + * @since 4.5.0
182 + *
183 + * @return bool - Should Carousel be disabled for single images? Default to false.
184 + */
185 + public function maybe_disable_jp_carousel_single_images() {
186 + /**
187 + * Allow third-party plugins or themes to disable Carousel for single images.
188 + *
189 + * @module carousel
190 + *
191 + * @since 4.5.0
192 + *
193 + * @param bool false Should Carousel be disabled for single images? Default to false.
194 + */
195 + return apply_filters( 'jp_carousel_maybe_disable_single_images', false );
196 + }
197 +
198 + /**
199 + * Returns the value of the applied jp_carousel_load_for_images_linked_to_file filter
200 + *
201 + * @since 4.5.0
202 + *
203 + * @return bool - Should Carousel be enabled for single images linking to 'Media File'? Default to false.
204 + */
205 + public function maybe_enable_jp_carousel_single_images_media_file() {
206 + /**
207 + * Allow third-party plugins or themes to enable Carousel
208 + * for single images linking to 'Media File' (full size image).
209 + *
210 + * @module carousel
211 + *
212 + * @since 4.5.0
213 + *
214 + * @param bool false Should Carousel be enabled for single images linking to 'Media File'? Default to false.
215 + */
216 + return apply_filters( 'jp_carousel_load_for_images_linked_to_file', false );
217 + }
218 +
219 + /**
220 + * Returns the value of the applied jp_carousel_asset_version filter
221 + *
222 + * @since 1.6.0
223 + *
224 + * @param string $version Asset version.
225 + *
226 + * @return string
227 + */
228 + public function asset_version( $version ) {
229 + /**
230 + * Filter the version string used when enqueuing Carousel assets.
231 + *
232 + * @module carousel
233 + *
234 + * @since 1.6.0
235 + *
236 + * @param string $version Asset version.
237 + */
238 + return apply_filters( 'jp_carousel_asset_version', $version );
239 + }
240 +
241 + /**
242 + * Displays a message on top of gallery if carousel has bailed.
243 + *
244 + * @param string $output Gallery shortcode output.
245 + *
246 + * @return string Shortcode output with bail message prepended.
247 + */
248 + public function display_bail_message( $output = '' ) {
249 + $message = '<div class="jp-carousel-msg"><p>';
250 + $message .= __( 'Jetpack\'s Carousel has been disabled, because another plugin or your theme is overriding the [gallery] shortcode.', 'jetpack' );
251 + $message .= '</p></div>';
252 + // put before gallery output.
253 + $output = $message . $output;
254 + return $output;
255 + }
256 +
257 + /**
258 + * Determine whether Carousel is enabled, and adjust filters and enqueue assets accordingly.
259 + *
260 + * If no other filter hook produced output for the gallery shortcode or something returns true for
261 + * the `jp_carousel_force_enable` filter, Carousel is enabled and we queue our assets. Otherwise
262 + * it's disabled and we remove some of our subsequent filter hooks.
263 + *
264 + * @since 1.9.0
265 + *
266 + * @param string $output Gallery shortcode output.
267 + *
268 + * @return string Gallery shortcode output.
269 + */
270 + public function check_if_shortcode_processed_and_enqueue_assets( $output ) {
271 + if (
272 + class_exists( 'Jetpack_AMP_Support' )
273 + && Jetpack_AMP_Support::is_amp_request()
274 + ) {
275 + return $output;
276 + }
277 +
278 + if (
279 + ! empty( $output ) &&
280 + /**
281 + * Allow third-party plugins or themes to force-enable Carousel.
282 + *
283 + * @module carousel
284 + *
285 + * @since 1.9.0
286 + *
287 + * @param bool false Should we force enable Carousel? Default to false.
288 + */
289 + ! apply_filters( 'jp_carousel_force_enable', false )
290 + ) {
291 + // Bail because someone is overriding the [gallery] shortcode.
292 + remove_filter( 'gallery_style', array( $this, 'add_data_to_container' ) );
293 + remove_filter( 'wp_get_attachment_image_attributes', array( $this, 'add_data_to_images' ) );
294 + remove_filter( 'the_content', array( $this, 'add_data_img_tags_and_enqueue_assets' ) );
295 + // Display message that carousel has bailed, if user is super_admin, and if we're not on WordPress.com.
296 + if (
297 + is_super_admin() &&
298 + ! ( defined( 'IS_WPCOM' ) && IS_WPCOM )
299 + ) {
300 + add_filter( 'post_gallery', array( $this, 'display_bail_message' ) );
301 + }
302 + return $output;
303 + }
304 +
305 + /**
306 + * Fires when thumbnails are shown in Carousel.
307 + *
308 + * @module carousel
309 + *
310 + * @since 1.6.0
311 + */
312 + do_action( 'jp_carousel_thumbnails_shown' );
313 +
314 + $this->enqueue_assets();
315 +
316 + return $output;
317 + }
318 +
319 + /**
320 + * Check if the content of a post uses gallery blocks. To be used by 'the_content' filter.
321 + *
322 + * @since 6.8.0
323 + * @deprecated since 11.3 We now hook into the 'block_render_{block_name}' hook to add markup.
324 + *
325 + * @param string $content Post content.
326 + *
327 + * @return string $content Post content.
328 + */
329 + public function check_content_for_blocks( $content ) {
330 + _deprecated_function( __METHOD__, 'jetpack-11.3' );
331 +
332 + if (
333 + class_exists( 'Jetpack_AMP_Support' )
334 + && Jetpack_AMP_Support::is_amp_request()
335 + ) {
336 + return $content;
337 + }
338 +
339 + if ( has_block( 'gallery', $content ) || has_block( 'jetpack/tiled-gallery', $content ) ) {
340 + $this->enqueue_assets();
341 + $content = $this->add_data_to_container( $content );
342 + }
343 +
344 + return $content;
345 + }
346 +
347 + /**
348 + * Remove core lightbox settings from images in a gallery, if Carousel is enabled.
349 + *
350 + * @param array $parsed_block An associative array of the block being rendered.
351 + * @param array $source_block An un-modified copy of `$parsed_block`, as it appeared in the source content.
352 + * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
353 + * @return array The modified block data.
354 + */
355 + public function remove_core_lightbox_in_gallery( $parsed_block, $source_block, $parent_block ) {
356 + if (
357 + ! empty( $parsed_block['blockName'] ) &&
358 + 'core/image' === $parsed_block['blockName'] &&
359 + ! empty( $parent_block->name ) &&
360 + 'core/gallery' === $parent_block->name
361 + ) {
362 + unset( $parsed_block['attrs']['lightbox'] );
363 + }
364 + return $parsed_block;
365 + }
366 +
367 + /**
368 + * Enrich the gallery block content using the render_block_{$this->name} filter.
369 + * This function is triggered after block render to make sure we track galleries within
370 + * reusable blocks.
371 + *
372 + * @see https://developer.wordpress.org/reference/hooks/render_block_this-name/
373 + *
374 + * @param string $block_content The rendered HTML for the carousel or gallery block.
375 + * @param array $block The parsed block details for the block.
376 + * @return string The fully-processed HTML for the carousel or gallery block.
377 + *
378 + * @since 11.3
379 + */
380 + public function filter_gallery_block_render( $block_content, $block ) {
381 + global $post;
382 +
383 + if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], array( 'core/gallery', 'jetpack/tiled-gallery' ), true ) ) {
384 + return $block_content;
385 + }
386 +
387 + $this->enqueue_assets();
388 +
389 + if ( ! $post instanceof WP_Post ) {
390 + return $block_content;
391 + }
392 +
393 + $blog_id = (int) get_current_blog_id();
394 +
395 + $extra_data = array(
396 + 'data-carousel-extra' => array(
397 + 'blog_id' => $blog_id,
398 + 'permalink' => get_permalink( $post->ID ),
399 + ),
400 + );
401 +
402 + /**
403 + * Filter the data added to the Gallery container.
404 + *
405 + * @module carousel
406 + *
407 + * @since 1.6.0
408 + *
409 + * @param array $extra_data Array of data about the site and the post.
410 + */
411 + $extra_data = apply_filters( 'jp_carousel_add_data_to_container', $extra_data );
412 + $extra_data = (array) $extra_data;
413 +
414 + if ( empty( $extra_data ) ) {
415 + return $block_content;
416 + }
417 +
418 + $extra_attributes = implode(
419 + ' ',
420 + array_map(
421 + function ( $data_key, $data_values ) {
422 + return esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_UNESCAPED_SLASHES | JSON_HEX_AMP ) ) . "'";
423 + },
424 + array_keys( $extra_data ),
425 + array_values( $extra_data )
426 + )
427 + );
428 +
429 + // Add extra attributes to first HTML element (which may have leading whitespace)
430 + return preg_replace(
431 + '/^(\s*<(div|ul|figure))/',
432 + '$1 ' . $extra_attributes . ' ',
433 + $block_content,
434 + 1
435 + );
436 + }
437 +
438 + /**
439 + * Enqueueing Carousel assets.
440 + */
441 + public function enqueue_assets() {
442 + if ( $this->first_run ) {
443 + wp_enqueue_script(
444 + 'jetpack-carousel',
445 + Assets::get_file_url_for_environment(
446 + '_inc/build/carousel/jetpack-carousel.min.js',
447 + 'modules/carousel/jetpack-carousel.js'
448 + ),
449 + array(),
450 + $this->asset_version( JETPACK__VERSION ),
451 + true
452 + );
453 +
454 + $swiper_library_path = array(
455 + 'url' => plugins_url( '_inc/blocks/swiper.js', JETPACK__PLUGIN_FILE ),
456 + );
457 + wp_localize_script( 'jetpack-carousel', 'jetpackSwiperLibraryPath', $swiper_library_path );
458 + add_action( 'wp_footer', array( $this, 'prefetch_swiper_library' ) );
459 +
460 + // Note: using home_url() instead of admin_url() for ajaxurl to be sure to get same domain on wpcom when using mapped domains (also works on self-hosted).
461 + // Also: not hardcoding path since there is no guarantee site is running on site root in self-hosted context.
462 + $is_logged_in = is_user_logged_in();
463 + $comment_registration = (int) get_option( 'comment_registration' );
464 + $require_name_email = (int) get_option( 'require_name_email' );
465 + $localize_strings = array(
466 + 'widths' => $this->prebuilt_widths,
467 + 'is_logged_in' => $is_logged_in,
468 + 'lang' => strtolower( substr( get_locale(), 0, 2 ) ),
469 + 'ajaxurl' => set_url_scheme( admin_url( 'admin-ajax.php' ) ),
470 + 'nonce' => wp_create_nonce( 'carousel_nonce' ),
471 + 'display_exif' => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_exif', true ) ),
472 + 'display_comments' => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_comments', true ) ),
473 + 'single_image_gallery' => $this->single_image_gallery_enabled,
474 + 'single_image_gallery_media_file' => $this->single_image_gallery_enabled_media_file,
475 + 'background_color' => $this->carousel_background_color_sanitize( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_background_color', '' ) ),
476 + 'comment' => __( 'Comment', 'jetpack' ),
477 + 'post_comment' => __( 'Post Comment', 'jetpack' ),
478 + 'write_comment' => __( 'Write a Comment...', 'jetpack' ),
479 + 'loading_comments' => __( 'Loading Comments...', 'jetpack' ),
480 + 'image_label' => __( 'Open image in full-screen.', 'jetpack' ),
481 + 'download_original' => sprintf(
482 + /* translators: %1s is the full-size image width, and %2s is the height. */
483 + __( 'View full size <span class="photo-size">%1$s<span class="photo-size-times">&times;</span>%2$s</span>', 'jetpack' ),
484 + '{0}',
485 + '{1}'
486 + ),
487 + 'no_comment_text' => __( 'Please be sure to submit some text with your comment.', 'jetpack' ),
488 + 'no_comment_email' => __( 'Please provide an email address to comment.', 'jetpack' ),
489 + 'no_comment_author' => __( 'Please provide your name to comment.', 'jetpack' ),
490 + 'comment_post_error' => __( 'Sorry, but there was an error posting your comment. Please try again later.', 'jetpack' ),
491 + 'comment_approved' => __( 'Your comment was approved.', 'jetpack' ),
492 + 'comment_unapproved' => __( 'Your comment is in moderation.', 'jetpack' ),
493 + 'camera' => __( 'Camera', 'jetpack' ),
494 + 'aperture' => __( 'Aperture', 'jetpack' ),
495 + 'shutter_speed' => __( 'Shutter Speed', 'jetpack' ),
496 + 'focal_length' => __( 'Focal Length', 'jetpack' ),
497 + 'copyright' => __( 'Copyright', 'jetpack' ),
498 + 'comment_registration' => $comment_registration,
499 + 'require_name_email' => $require_name_email,
500 + /** This action is documented in core/src/wp-includes/link-template.php */
501 + 'login_url' => wp_login_url( apply_filters( 'the_permalink', get_permalink() ) ),
502 + 'blog_id' => (int) get_current_blog_id(),
503 + 'meta_data' => array( 'camera', 'aperture', 'shutter_speed', 'focal_length', 'copyright' ),
504 + );
505 +
506 + /**
507 + * Handle WP stats for images in full-screen.
508 + * Build string with tracking info.
509 + */
510 +
511 + /**
512 + * Filter if Jetpack should enable stats collection on carousel views
513 + *
514 + * @module carousel
515 + *
516 + * @since 4.3.2
517 + *
518 + * @param bool Enable Jetpack Carousel stat collection. Default false.
519 + */
520 + if ( apply_filters( 'jetpack_enable_carousel_stats', false ) && in_array( 'stats', Jetpack::get_active_modules(), true ) && ! ( new Status() )->is_offline_mode() ) {
521 + $localize_strings['stats'] = 'blog=' . Jetpack_Options::get_option( 'id' ) . '&host=' . wp_parse_url( get_option( 'home' ), PHP_URL_HOST ) . '&v=ext&j=' . JETPACK__API_VERSION . ':' . JETPACK__VERSION;
522 +
523 + // Set the stats as empty if user is logged in but logged-in users shouldn't be tracked.
524 + if ( is_user_logged_in() ) {
525 + $stats_options = Stats_Options::get_options();
526 + $track_loggedin_users = isset( $stats_options['count_roles'] ) ? (bool) $stats_options['count_roles'] : false;
527 +
528 + if ( ! $track_loggedin_users ) {
529 + $localize_strings['stats'] = '';
530 + }
531 + }
532 + }
533 +
534 + /**
535 + * Filter the strings passed to the Carousel's js file.
536 + *
537 + * @module carousel
538 + *
539 + * @since 1.6.0
540 + *
541 + * @param array $localize_strings Array of strings passed to the Jetpack js file.
542 + */
543 + $localize_strings = apply_filters( 'jp_carousel_localize_strings', $localize_strings );
544 + wp_localize_script( 'jetpack-carousel', 'jetpackCarouselStrings', $localize_strings );
545 + wp_enqueue_style(
546 + 'jetpack-swiper-library',
547 + plugins_url( '_inc/blocks/swiper.css', JETPACK__PLUGIN_FILE ),
548 + array(),
549 + JETPACK__VERSION
550 + );
551 + wp_enqueue_style( 'jetpack-carousel', plugins_url( 'jetpack-carousel.css', __FILE__ ), array(), $this->asset_version( JETPACK__VERSION ) );
552 + wp_style_add_data( 'jetpack-carousel', 'rtl', 'replace' );
553 +
554 + /**
555 + * Fires after carousel assets are enqueued for the first time.
556 + * Allows for adding additional assets to the carousel page.
557 + *
558 + * @module carousel
559 + *
560 + * @since 1.6.0
561 + *
562 + * @param bool $first_run First load if Carousel on the page.
563 + * @param array $localized_strings Array of strings passed to the Jetpack js file.
564 + */
565 + do_action( 'jp_carousel_enqueue_assets', $this->first_run, $localize_strings );
566 +
567 + // Add the carousel skeleton to the page.
568 + $this->localize_strings = $localize_strings;
569 + add_action( 'wp_footer', array( $this, 'add_carousel_skeleton' ) );
570 +
571 + $this->first_run = false;
572 + }
573 + }
574 +
575 + /**
576 + * Hint the browser to fetch the Swiper library while it is idle.
577 + *
578 + * Swiper is only requested when the lightbox is first opened, which puts a network
579 + * round trip in front of that first click. This is deliberately `prefetch` rather than
580 + * `preload`: most visitors never open the lightbox, so the fetch must stay at low
581 + * priority and out of the way of the page's own images. `loadSwiper()` still loads the
582 + * library on demand, since a prefetch is a hint the browser is free to ignore.
583 + */
584 + public function prefetch_swiper_library() {
585 + printf(
586 + '<link rel="prefetch" href="%s" as="script" />' . "\n",
587 + esc_url( plugins_url( '_inc/blocks/swiper.js', JETPACK__PLUGIN_FILE ) )
588 + );
589 + }
590 +
591 + /**
592 + * Generate the HTML skeleton that will be picked up by the Carousel JS and used for showing the carousel.
593 + */
594 + public function add_carousel_skeleton() {
595 + $localize_strings = $this->localize_strings;
596 + $is_light = ( 'white' === $localize_strings['background_color'] );
597 + // Determine whether to fall back to standard local comments.
598 + $use_local_comments = ! isset( $localize_strings['jetpack_comments_iframe_src'] ) || empty( $localize_strings['jetpack_comments_iframe_src'] );
599 + $current_user = wp_get_current_user();
600 + $require_name_email = (int) get_option( 'require_name_email' );
601 + /* translators: %s is replaced with a field name in the form, e.g. "Email" */
602 + $required = ( $require_name_email ) ? __( '%s (Required)', 'jetpack' ) : '%s';
603 + require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-spinner.php';
604 + ?>
605 + <div id="jp-carousel-loading-overlay" style="display: none;">
606 + <div id="jp-carousel-loading-wrapper">
607 + <span id="jp-carousel-library-loading"><?php echo Jetpack_Spinner::render( 40 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?></span>
608 + </div>
609 + </div>
610 + <div class="jp-carousel-overlay<?php echo( $is_light ? ' jp-carousel-light' : '' ); ?>" style="display: none;">
611 +
612 + <div class="jp-carousel-container<?php echo( $is_light ? ' jp-carousel-light' : '' ); ?>">
613 + <!-- The Carousel Swiper -->
614 + <div
615 + class="jp-carousel-wrap swiper jp-carousel-swiper-container jp-carousel-transitions"
616 + itemscope
617 + itemtype="https://schema.org/ImageGallery">
618 + <div class="jp-carousel swiper-wrapper"></div>
619 + <div class="jp-swiper-button-prev swiper-button-prev">
620 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
621 + <mask id="maskPrev" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="6" width="9" height="12">
622 + <path d="M16.2072 16.59L11.6496 12L16.2072 7.41L14.8041 6L8.8335 12L14.8041 18L16.2072 16.59Z" fill="white"/>
623 + </mask>
624 + <g mask="url(#maskPrev)">
625 + <rect x="0.579102" width="23.8823" height="24" fill="#FFFFFF"/>
626 + </g>
627 + </svg>
628 + </div>
629 + <div class="jp-swiper-button-next swiper-button-next">
630 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
631 + <mask id="maskNext" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="6" width="8" height="12">
632 + <path d="M8.59814 16.59L13.1557 12L8.59814 7.41L10.0012 6L15.9718 12L10.0012 18L8.59814 16.59Z" fill="white"/>
633 + </mask>
634 + <g mask="url(#maskNext)">
635 + <rect x="0.34375" width="23.8822" height="24" fill="#FFFFFF"/>
636 + </g>
637 + </svg>
638 + </div>
639 + </div>
640 + <!-- The main close buton -->
641 + <div class="jp-carousel-close-hint">
642 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
643 + <mask id="maskClose" mask-type="alpha" maskUnits="userSpaceOnUse" x="5" y="5" width="15" height="14">
644 + <path d="M19.3166 6.41L17.9135 5L12.3509 10.59L6.78834 5L5.38525 6.41L10.9478 12L5.38525 17.59L6.78834 19L12.3509 13.41L17.9135 19L19.3166 17.59L13.754 12L19.3166 6.41Z" fill="white"/>
645 + </mask>
646 + <g mask="url(#maskClose)">
647 + <rect x="0.409668" width="23.8823" height="24" fill="#FFFFFF"/>
648 + </g>
649 + </svg>
650 + </div>
651 + <!-- Image info, comments and meta -->
652 + <div class="jp-carousel-info">
653 + <div class="jp-carousel-info-footer">
654 + <div class="jp-carousel-pagination-container">
655 + <div class="jp-swiper-pagination swiper-pagination"></div>
656 + <div class="jp-carousel-pagination"></div>
657 + </div>
658 + <div class="jp-carousel-photo-title-container">
659 + <div class="jp-carousel-photo-caption"></div>
660 + </div>
661 + <div class="jp-carousel-photo-icons-container">
662 + <a href="#" class="jp-carousel-icon-btn jp-carousel-icon-info" aria-label="<?php esc_attr_e( 'Toggle photo metadata visibility', 'jetpack' ); ?>">
663 + <span class="jp-carousel-icon">
664 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
665 + <mask id="maskInfo" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="21" height="20">
666 + <path fill-rule="evenodd" clip-rule="evenodd" d="M12.7537 2C7.26076 2 2.80273 6.48 2.80273 12C2.80273 17.52 7.26076 22 12.7537 22C18.2466 22 22.7046 17.52 22.7046 12C22.7046 6.48 18.2466 2 12.7537 2ZM11.7586 7V9H13.7488V7H11.7586ZM11.7586 11V17H13.7488V11H11.7586ZM4.79292 12C4.79292 16.41 8.36531 20 12.7537 20C17.142 20 20.7144 16.41 20.7144 12C20.7144 7.59 17.142 4 12.7537 4C8.36531 4 4.79292 7.59 4.79292 12Z" fill="white"/>
667 + </mask>
668 + <g mask="url(#maskInfo)">
669 + <rect x="0.8125" width="23.8823" height="24" fill="#FFFFFF"/>
670 + </g>
671 + </svg>
672 + </span>
673 + </a>
674 + <?php if ( $localize_strings['display_comments'] ) : ?>
675 + <a href="#" class="jp-carousel-icon-btn jp-carousel-icon-comments" aria-label="<?php esc_attr_e( 'Toggle photo comments visibility', 'jetpack' ); ?>">
676 + <span class="jp-carousel-icon">
677 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
678 + <mask id="maskComments" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="21" height="20">
679 + <path fill-rule="evenodd" clip-rule="evenodd" d="M4.3271 2H20.2486C21.3432 2 22.2388 2.9 22.2388 4V16C22.2388 17.1 21.3432 18 20.2486 18H6.31729L2.33691 22V4C2.33691 2.9 3.2325 2 4.3271 2ZM6.31729 16H20.2486V4H4.3271V18L6.31729 16Z" fill="white"/>
680 + </mask>
681 + <g mask="url(#maskComments)">
682 + <rect x="0.34668" width="23.8823" height="24" fill="#FFFFFF"/>
683 + </g>
684 + </svg>
685 +
686 + <span class="jp-carousel-has-comments-indicator" aria-label="<?php esc_attr_e( 'This image has comments.', 'jetpack' ); ?>"></span>
687 + </span>
688 + </a>
689 + <?php endif; ?>
690 + </div>
691 + </div>
692 + <div class="jp-carousel-info-extra">
693 + <div class="jp-carousel-info-content-wrapper">
694 + <div class="jp-carousel-photo-title-container">
695 + <div class="jp-carousel-photo-title"></div>
696 + </div>
697 + <div class="jp-carousel-comments-wrapper">
698 + <?php if ( $localize_strings['display_comments'] ) : ?>
699 + <div id="jp-carousel-comments-loading">
700 + <span><?php echo esc_html( $localize_strings['loading_comments'] ); ?></span>
701 + </div>
702 + <div class="jp-carousel-comments"></div>
703 + <div id="jp-carousel-comment-form-container">
704 + <span id="jp-carousel-comment-form-spinner"><?php echo Jetpack_Spinner::render( 20 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?></span>
705 + <div id="jp-carousel-comment-post-results"></div>
706 + <?php if ( $use_local_comments ) : ?>
707 + <?php if ( ! $localize_strings['is_logged_in'] && $localize_strings['comment_registration'] ) : ?>
708 + <div id="jp-carousel-comment-form-commenting-as">
709 + <p id="jp-carousel-commenting-as">
710 + <?php
711 + echo wp_kses(
712 + __( 'You must be <a href="#" class="jp-carousel-comment-login">logged in</a> to post a comment.', 'jetpack' ),
713 + array(
714 + 'a' => array(
715 + 'href' => array(),
716 + 'class' => array(),
717 + ),
718 + )
719 + );
720 + ?>
721 + </p>
722 + </div>
723 + <?php else : ?>
724 + <form id="jp-carousel-comment-form">
725 + <label for="jp-carousel-comment-form-comment-field" class="screen-reader-text"><?php echo esc_attr( $localize_strings['write_comment'] ); ?></label>
726 + <textarea
727 + name="comment"
728 + class="jp-carousel-comment-form-field jp-carousel-comment-form-textarea"
729 + id="jp-carousel-comment-form-comment-field"
730 + placeholder="<?php echo esc_attr( $localize_strings['write_comment'] ); ?>"
731 + ></textarea>
732 + <div id="jp-carousel-comment-form-submit-and-info-wrapper">
733 + <div id="jp-carousel-comment-form-commenting-as">
734 + <?php if ( $localize_strings['is_logged_in'] ) : ?>
735 + <p id="jp-carousel-commenting-as">
736 + <?php
737 + printf(
738 + /* translators: %s is replaced with the user's display name */
739 + esc_html__( 'Commenting as %s', 'jetpack' ),
740 + esc_html( $current_user->data->display_name )
741 + );
742 + ?>
743 + </p>
744 + <?php else : ?>
745 + <fieldset>
746 + <label for="jp-carousel-comment-form-email-field"><?php echo esc_html( sprintf( $required, __( 'Email', 'jetpack' ) ) ); ?></label>
747 + <input type="text" name="email" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-email-field" />
748 + </fieldset>
749 + <fieldset>
750 + <label for="jp-carousel-comment-form-author-field"><?php echo esc_html( sprintf( $required, __( 'Name', 'jetpack' ) ) ); ?></label>
751 + <input type="text" name="author" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-author-field" />
752 + </fieldset>
753 + <fieldset>
754 + <label for="jp-carousel-comment-form-url-field"><?php esc_html_e( 'Website', 'jetpack' ); ?></label>
755 + <input type="text" name="url" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-url-field" />
756 + </fieldset>
757 + <?php endif ?>
758 + </div>
759 + <input
760 + type="submit"
761 + name="submit"
762 + class="jp-carousel-comment-form-button"
763 + id="jp-carousel-comment-form-button-submit"
764 + value="<?php echo esc_attr( $localize_strings['post_comment'] ); ?>" />
765 + </div>
766 + </form>
767 + <?php endif ?>
768 + <?php endif ?>
769 + </div>
770 + <?php endif ?>
771 + </div>
772 + <div class="jp-carousel-image-meta">
773 + <div class="jp-carousel-title-and-caption">
774 + <div class="jp-carousel-photo-info">
775 + <div class="jp-carousel-caption" itemprop="caption description"></div>
776 + </div>
777 +
778 + <div class="jp-carousel-photo-description"></div>
779 + </div>
780 + <a class="jp-carousel-image-download" href="#" aria-label="<?php esc_attr_e( 'Download image', 'jetpack' ); ?>" target="_blank" style="display: none;">
781 + <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
782 + <mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="19" height="18">
783 + <path fill-rule="evenodd" clip-rule="evenodd" d="M5.84615 5V19H19.7775V12H21.7677V19C21.7677 20.1 20.8721 21 19.7775 21H5.84615C4.74159 21 3.85596 20.1 3.85596 19V5C3.85596 3.9 4.74159 3 5.84615 3H12.8118V5H5.84615ZM14.802 5V3H21.7677V10H19.7775V6.41L9.99569 16.24L8.59261 14.83L18.3744 5H14.802Z" fill="white"/>
784 + </mask>
785 + <g mask="url(#mask0)">
786 + <rect x="0.870605" width="23.8823" height="24" fill="#FFFFFF"/>
787 + </g>
788 + </svg>
789 + <span class="jp-carousel-download-text"></span>
790 + </a>
791 + <div class="jp-carousel-image-map" style="display: none;"></div>
792 + </div>
793 + </div>
794 + </div>
795 + </div>
796 + </div>
797 +
798 + </div>
799 + <?php
800 + }
801 +
802 + /**
803 + * Sets the "in_gallery" flag when the first gallery is encountered (unless in AMP mode).
804 + *
805 + * @deprecated since 10.8
806 + *
807 + * @param string $output Gallery shortcode output. Passed through unchanged.
808 + *
809 + * @return string
810 + */
811 + public function set_in_gallery( $output ) {
812 + if (
813 + class_exists( 'Jetpack_AMP_Support' )
814 + && Jetpack_AMP_Support::is_amp_request()
815 + ) {
816 + return $output;
817 + }
818 + $this->in_gallery = true;
819 + return $output;
820 + }
821 +
822 + /**
823 + * Adds data-* attributes required by carousel to img tags in post HTML
824 + * content. To be used by 'the_content' filter.
825 + *
826 + * @see add_data_to_images()
827 + * @see wp_make_content_images_responsive() in wp-includes/media.php
828 + *
829 + * @param string $content HTML content of the post.
830 + * @return string
831 + */
832 + public function add_data_img_tags_and_enqueue_assets( $content ) {
833 + if ( ! is_string( $content ) || $content === '' ) {
834 + return '';
835 + }
836 + if (
837 + class_exists( 'Jetpack_AMP_Support' )
838 + && Jetpack_AMP_Support::is_amp_request()
839 + ) {
840 + return $this->maybe_add_amp_lightbox( $content );
841 + }
842 +
843 + if ( ! preg_match_all( '/<img [^>]+>/', $content, $matches ) ) {
844 + return $content;
845 + }
846 + $selected_images = array();
847 + foreach ( $matches[0] as $image_html ) {
848 + // This image already carries the attributes this method adds, so adding
849 + // them again would emit every one of them twice. Tiled Gallery output
850 + // reaches this filter twice: once as 'jetpack_tiled_galleries_block_content'
851 + // from inside the block's render callback, and again as 'the_content' when
852 + // single image galleries are enabled. See JETPACK-1990.
853 + if ( str_contains( $image_html, 'data-attachment-id=' ) ) {
854 + continue;
855 + }
856 + if (
857 + preg_match( '/(wp-image-|data-id=)\"?([0-9]+)\"?/i', $image_html, $class_id )
858 + && ! str_contains( $image_html, 'wp-block-jetpack-slideshow_image' )
859 + ) {
860 + /**
861 + * Allow filtering the attachment ID used to fetch and populate metadata about an image in a gallery.
862 + *
863 + * @module carousel
864 + *
865 + * @since 12.6
866 + *
867 + * @param int $attachment_id Attachment ID pulled from image HTML.
868 + * @param string $image_html Full HTML image tag.
869 + */
870 + $attachment_id = absint(
871 + apply_filters(
872 + 'jetpack_carousel_image_attachment_id',
873 + $class_id[2],
874 + $image_html
875 + )
876 + );
877 +
878 + /**
879 + * The same image tag may be used more than once but with different attribs,
880 + * so save each of them against the attachment id.
881 + */
882 + if ( ! isset( $selected_images[ $attachment_id ] ) || ! in_array( $image_html, $selected_images[ $attachment_id ], true ) ) {
883 + $selected_images[ $attachment_id ][] = $image_html;
884 + }
885 + }
886 + }
887 +
888 + $find = array();
889 + $replace = array();
890 + if ( empty( $selected_images ) ) {
891 + return $content;
892 + }
893 +
894 + $attachments = get_posts(
895 + array(
896 + 'include' => array_keys( $selected_images ),
897 + 'post_type' => 'any',
898 + 'post_status' => 'any',
899 + 'suppress_filters' => false,
900 + )
901 + );
902 +
903 + foreach ( $attachments as $attachment ) {
904 + /*
905 + * If the item from get_posts isn't an attachment, skip. This can occur when copy-pasta from another WP site.
906 + * For example, if one copies "<img class="wp-image-7 size-full" src="https://twentysixteendemo.files.wordpress.com/2015/11/post.png" alt="post" width="1000" height="563" />"
907 + * then, we're going to look up post 7 below, which making sure it is an attachment.
908 + *
909 + * This is meant as a relatively quick fix, as a better fix is likely to update the get_posts call above to only
910 + * include attachments.
911 + */
912 + if (
913 + ! isset( $attachment->ID )
914 + || ! wp_attachment_is_image( $attachment->ID )
915 + || ! isset( $selected_images[ $attachment->ID ] )
916 + ) {
917 + continue;
918 + }
919 + $image_elements = $selected_images[ $attachment->ID ];
920 +
921 + if ( ! is_array( $image_elements ) ) {
922 + continue;
923 + }
924 +
925 + $attributes = $this->add_data_to_images( array(), $attachment );
926 + $attributes_html = '';
927 + foreach ( $attributes as $k => $v ) {
928 + $attributes_html .= esc_attr( $k ) . '="' . esc_attr( $v ) . '" ';
929 + }
930 + foreach ( $image_elements as $image_html ) {
931 + $find[] = $image_html;
932 + $replace[] = str_replace( '<img ', "<img $attributes_html", $image_html );
933 + }
934 + }
935 +
936 + $content = str_replace( $find, $replace, $content );
937 + $this->enqueue_assets();
938 + return $content;
939 + }
940 +
941 + /**
942 + * Adds the data attributes themselves to img tags.
943 + *
944 + * @see add_data_img_tags_and_enqueue_assets()
945 + * @see https://developer.wordpress.org/reference/functions/wp_get_attachment_image/ Documentation about wp_get_attachment_image
946 + *
947 + * @param string[] $attr Array of attribute values for the image markup, keyed by attribute name.
948 + * @param null|WP_Post $attachment Image attachment post.
949 + *
950 + * @return string[] Modified image attributes.
951 + */
952 + public function add_data_to_images( $attr, $attachment = null ) {
953 + if (
954 + class_exists( 'Jetpack_AMP_Support' )
955 + && Jetpack_AMP_Support::is_amp_request()
956 + ) {
957 + return $attr;
958 + }
959 +
960 + if (
961 + ! $attachment instanceof WP_Post
962 + || ! isset( $attachment->ID )
963 + || ! wp_attachment_is_image( $attachment )
964 + ) {
965 + return $attr;
966 + }
967 +
968 + $attachment_id = (int) $attachment->ID;
969 + $orig_file = wp_get_attachment_image_src( $attachment_id, 'full' );
970 + $orig_file = $orig_file[0] ?? wp_get_attachment_url( $attachment_id );
971 + $meta = wp_get_attachment_metadata( $attachment_id );
972 + $size = isset( $meta['width'] ) ? (int) $meta['width'] . ',' . (int) $meta['height'] : '';
973 + $img_meta = ( ! empty( $meta['image_meta'] ) ) ? (array) $meta['image_meta'] : array();
974 + $comments_opened = (int) comments_open( $attachment_id );
975 + $display_exif = $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_exif', true ) );
976 +
977 + /**
978 + * Note: Cannot generate a filename from the width and height wp_get_attachment_image_src() returns because
979 + * it takes the $content_width global variable themes can set in consideration, therefore returning sizes
980 + * which when used to generate a filename will likely result in a 404 on the image.
981 + * $content_width has no filter we could temporarily de-register, run wp_get_attachment_image_src(), then
982 + * re-register. So using returned file URL instead, which we can define the sizes from through filename
983 + * parsing in the JS, as this is a failsafe file reference.
984 + *
985 + * EG with Twenty Eleven activated:
986 + * array(4) { [0]=> string(82) "http://vanillawpinstall.blah/wp-content/uploads/2012/06/IMG_3534-1024x764.jpg" [1]=> int(584) [2]=> int(435) [3]=> bool(true) }
987 + *
988 + * EG with Twenty Ten activated:
989 + * array(4) { [0]=> string(82) "http://vanillawpinstall.blah/wp-content/uploads/2012/06/IMG_3534-1024x764.jpg" [1]=> int(640) [2]=> int(477) [3]=> bool(true) }
990 + */
991 +
992 + $large_file_info = wp_get_attachment_image_src( $attachment_id, 'large' );
993 + $large_file = $large_file_info[0] ?? '';
994 +
995 + $attachment_title = wptexturize( $attachment->post_title );
996 + $attachment_desc = wpautop( wptexturize( $attachment->post_content ) );
997 + $attachment_caption = wpautop( wptexturize( $attachment->post_excerpt ) );
998 +
999 + $attr['data-attachment-id'] = $attachment_id;
1000 + $attr['data-permalink'] = esc_attr( get_permalink( $attachment_id ) );
1001 + $attr['data-orig-file'] = esc_attr( $orig_file );
1002 + $attr['data-orig-size'] = $size;
1003 + $attr['data-comments-opened'] = $comments_opened;
1004 +
1005 + /*
1006 + Lets the Carousel show its "has comments" badge without fetching the comments
1007 + themselves. Omitted when there are none, which is the common case, so galleries
1008 + without comments pay nothing for it.
1009 + */
1010 + $comments_count = (int) $attachment->comment_count;
1011 + if ( $comments_count > 0 ) {
1012 + $attr['data-comments-count'] = $comments_count;
1013 + }
1014 +
1015 + if ( $display_exif ) {
1016 + // See https://github.com/Automattic/jetpack/issues/2765.
1017 + if ( isset( $img_meta['keywords'] ) ) {
1018 + unset( $img_meta['keywords'] );
1019 + }
1020 +
1021 + /*
1022 + Filtering on `is_scalar` alone kept every "" and "0" in the metadata array, which
1023 + on a typical photo is most of it. The carousel skips those values when it renders
1024 + the EXIF panel anyway, so serialising them only inflates the page. Mirror that
1025 + check here: drop empties and numeric zeroes, but keep text that merely casts to
1026 + zero, such as a camera name.
1027 + */
1028 + $img_meta = array_filter(
1029 + array_map( 'strval', array_filter( $img_meta, 'is_scalar' ) ),
1030 + function ( $value ) {
1031 + return '' !== $value && ! ( is_numeric( $value ) && 0.0 === (float) $value );
1032 + }
1033 + );
1034 +
1035 + // With nothing left to show, the attribute itself is dead weight.
1036 + if ( ! empty( $img_meta ) ) {
1037 + $attr['data-image-meta'] = esc_attr( wp_json_encode( $img_meta, JSON_UNESCAPED_SLASHES | JSON_HEX_AMP ) );
1038 + }
1039 + }
1040 +
1041 + // The lines below use `esc_attr( htmlspecialchars( ) )` because esc_attr tries to be too smart and won't double-encode, and we need that here.
1042 + $attr['data-image-title'] = esc_attr( htmlspecialchars( $attachment_title, ENT_COMPAT ) );
1043 + $attr['data-image-description'] = esc_attr( htmlspecialchars( $attachment_desc, ENT_COMPAT ) );
1044 + $attr['data-image-caption'] = esc_attr( htmlspecialchars( $attachment_caption, ENT_COMPAT ) );
1045 + $attr['data-large-file'] = esc_attr( $large_file );
1046 + return $attr;
1047 + }
1048 +
1049 + /**
1050 + * Add additional attributes to the Gallery container HTML.
1051 + *
1052 + * @param string $html The HTML to which the additional attributes are added.
1053 + *
1054 + * @return string
1055 + */
1056 + public function add_data_to_container( $html ) {
1057 + global $post;
1058 + if (
1059 + class_exists( 'Jetpack_AMP_Support' )
1060 + && Jetpack_AMP_Support::is_amp_request()
1061 + ) {
1062 + return $html;
1063 + }
1064 +
1065 + if ( isset( $post ) ) {
1066 + $blog_id = (int) get_current_blog_id();
1067 +
1068 + $extra_data = array(
1069 + 'data-carousel-extra' => array(
1070 + 'blog_id' => $blog_id,
1071 + 'permalink' => get_permalink( $post->ID ),
1072 + ),
1073 + );
1074 +
1075 + /**
1076 + * Filter the data added to the Gallery container.
1077 + *
1078 + * @module carousel
1079 + *
1080 + * @since 1.6.0
1081 + *
1082 + * @param array $extra_data Array of data about the site and the post.
1083 + */
1084 + $extra_data = apply_filters( 'jp_carousel_add_data_to_container', $extra_data );
1085 + foreach ( (array) $extra_data as $data_key => $data_values ) {
1086 + $html = str_replace( '<div ', '<div ' . esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ) . "' ", $html );
1087 + $html = str_replace( '<ul class="wp-block-gallery', '<ul ' . esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ) . "' class=\"wp-block-gallery", $html );
1088 + $html = str_replace( '<ul class="blocks-gallery-grid', '<ul ' . esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ) . "' class=\"blocks-gallery-grid", $html );
1089 + $html = preg_replace( '/\<figure([^>]*)class="(wp-block-gallery[^"]*?has-nested-images.*?)"/', '<figure ' . esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ) . "' $1 class=\"$2\"", $html );
1090 + }
1091 + }
1092 +
1093 + return $html;
1094 + }
1095 +
1096 + /**
1097 + * Conditionally adds amp-lightbox to galleries and images.
1098 + *
1099 + * This applies to gallery blocks and shortcodes,
1100 + * in addition to images that are wrapped in a link to the page.
1101 + * Images wrapped in a link to the media file shouldn't get an amp-lightbox.
1102 + *
1103 + * @param string $content The content to possibly add amp-lightbox to.
1104 + * @return string The content, with amp-lightbox possibly added.
1105 + */
1106 + public function maybe_add_amp_lightbox( $content ) {
1107 + $content = preg_replace(
1108 + array(
1109 + '#(<figure)[^>]*(?=class=(["\']?)[^>]*wp-block-gallery[^>]*\2)#is', // Gallery block.
1110 + '#(\[gallery)(?=\s+)#', // Gallery shortcode.
1111 + ),
1112 + array(
1113 + '\1 data-amp-lightbox="true" ', // https://github.com/ampproject/amp-wp/blob/1094ea03bd5dc92889405a47a8c41de1a88908de/includes/sanitizers/class-amp-gallery-block-sanitizer.php#L84.
1114 + '\1 amp-lightbox="true"', // https://github.com/ampproject/amp-wp/blob/1094ea03bd5dc92889405a47a8c41de1a88908de/includes/embeds/class-amp-gallery-embed.php#L64.
1115 + ),
1116 + $content
1117 + );
1118 +
1119 + return preg_replace_callback(
1120 + '#(<a[^>]* href=(["\']?)(\S+)\2>)\s*(<img[^>]*)(class=(["\']?)[^>]*wp-image-[0-9]+[^>]*\6.*>)\s*</a>#is',
1121 + static function ( $matches ) {
1122 + if ( ! preg_match( '#\.\w+$#', $matches[3] ) ) {
1123 + // The a[href] doesn't end in a file extension like .jpeg, so this is not a link to the media file, and should get a lightbox.
1124 + return $matches[4] . ' data-amp-lightbox="true" lightbox="true" ' . $matches[5]; // https://github.com/ampproject/amp-wp/blob/1094ea03bd5dc92889405a47a8c41de1a88908de/includes/sanitizers/class-amp-img-sanitizer.php#L419.
1125 + }
1126 +
1127 + return $matches[0];
1128 + },
1129 + $content
1130 + );
1131 + }
1132 +
1133 + /**
1134 + * Retrieves comment information
1135 + *
1136 + * @return never
1137 + */
1138 + public function get_attachment_comments() {
1139 + if ( ! headers_sent() ) {
1140 + header( 'Content-type: text/javascript' );
1141 + }
1142 +
1143 + /**
1144 + * Allows for the checking of privileges of the blog user before comments
1145 + * are packaged as JSON and sent back from the get_attachment_comments
1146 + * AJAX endpoint
1147 + *
1148 + * @module carousel
1149 + *
1150 + * @since 1.6.0
1151 + */
1152 + do_action( 'jp_carousel_check_blog_user_privileges' );
1153 +
1154 + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- we do not need to verify the nonce for this public request for publicly accessible data (as checked below).
1155 + $attachment_id = ( isset( $_REQUEST['id'] ) ) ? (int) $_REQUEST['id'] : 0;
1156 + $offset = ( isset( $_REQUEST['offset'] ) ) ? (int) $_REQUEST['offset'] : 0;
1157 + // phpcs:enable
1158 +
1159 + if ( ! $attachment_id ) {
1160 + wp_send_json_error(
1161 + __( 'Missing attachment ID.', 'jetpack' ),
1162 + 403,
1163 + JSON_UNESCAPED_SLASHES
1164 + );
1165 + }
1166 +
1167 + $attachment_post = get_post( $attachment_id );
1168 + // If we have no info about that attachment, bail.
1169 + if ( ! ( $attachment_post instanceof WP_Post ) ) {
1170 + wp_send_json_error(
1171 + __( 'Missing attachment info.', 'jetpack' ),
1172 + 403,
1173 + JSON_UNESCAPED_SLASHES
1174 + );
1175 + }
1176 +
1177 + // This AJAX call should only be used to fetch comments of attachments.
1178 + if ( 'attachment' !== $attachment_post->post_type ) {
1179 + wp_send_json_error(
1180 + __( 'You aren’t authorized to do that.', 'jetpack' ),
1181 + 403,
1182 + JSON_UNESCAPED_SLASHES
1183 + );
1184 + }
1185 +
1186 + $parent_post = get_post_parent( $attachment_id );
1187 +
1188 + /*
1189 + * If we have no info about that parent post, no extra checks.
1190 + * The attachment doesn't have a parent post, so is public.
1191 + * If we have a parent post, let's ensure the user has access to it.
1192 + */
1193 + if ( $parent_post instanceof WP_Post ) {
1194 + /*
1195 + * Fetch info about user making the request.
1196 + * If we have no info, bail.
1197 + * Even logged out users should get a WP_User user with id 0.
1198 + */
1199 + $current_user = wp_get_current_user();
1200 + if ( ! ( $current_user instanceof WP_User ) ) {
1201 + wp_send_json_error(
1202 + __( 'Missing user info.', 'jetpack' ),
1203 + 403,
1204 + JSON_UNESCAPED_SLASHES
1205 + );
1206 + }
1207 +
1208 + /*
1209 + * If a post is private / draft
1210 + * and the current user doesn't have access to it,
1211 + * bail.
1212 + */
1213 + if (
1214 + 'publish' !== $parent_post->post_status
1215 + && ! current_user_can( 'read_post', $parent_post->ID )
1216 + ) {
1217 + wp_send_json_error(
1218 + __( 'You aren’t authorized to do that.', 'jetpack' ),
1219 + 403,
1220 + JSON_UNESCAPED_SLASHES
1221 + );
1222 + }
1223 + }
1224 +
1225 + if ( $offset < 1 ) {
1226 + $offset = 0;
1227 + }
1228 +
1229 + $comments = get_comments(
1230 + array(
1231 + 'status' => 'approve',
1232 + 'order' => ( 'asc' === get_option( 'comment_order' ) ) ? 'ASC' : 'DESC',
1233 + 'number' => 10,
1234 + 'offset' => $offset,
1235 + 'post_id' => $attachment_id,
1236 + )
1237 + );
1238 +
1239 + $out = array();
1240 +
1241 + // Can't just send the results, they contain the commenter's email address.
1242 + foreach ( $comments as $comment ) {
1243 + $avatar = get_avatar( $comment->comment_author_email, 64 );
1244 + if ( ! $avatar ) {
1245 + $avatar = '';
1246 + }
1247 + $out[] = array(
1248 + 'id' => $comment->comment_ID,
1249 + 'parent_id' => $comment->comment_parent,
1250 + 'author_markup' => get_comment_author_link( $comment->comment_ID ),
1251 + 'gravatar_markup' => $avatar,
1252 + 'date_gmt' => $comment->comment_date_gmt,
1253 + 'content' => wpautop( $comment->comment_content ),
1254 + );
1255 + }
1256 +
1257 + wp_send_json( $out, null, JSON_UNESCAPED_SLASHES );
1258 + }
1259 +
1260 + /**
1261 + * Adds a new comment to the database
1262 + *
1263 + * @return never
1264 + */
1265 + public function post_attachment_comment() {
1266 + if ( ! headers_sent() ) {
1267 + header( 'Content-type: text/javascript' );
1268 + }
1269 +
1270 + if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'carousel_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unslash or sanitize nonces either
1271 + die( wp_json_encode( array( 'error' => __( 'Nonce verification failed.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1272 + }
1273 +
1274 + $_blog_id = isset( $_POST['blog_id'] ) ? (int) $_POST['blog_id'] : 0;
1275 + $_post_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
1276 + $comment = isset( $_POST['comment'] ) ? filter_var( wp_unslash( $_POST['comment'] ) ) : null;
1277 +
1278 + if ( empty( $_blog_id ) ) {
1279 + die( wp_json_encode( array( 'error' => __( 'Missing target blog ID.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1280 + }
1281 +
1282 + if ( empty( $_post_id ) ) {
1283 + die( wp_json_encode( array( 'error' => __( 'Missing target post ID.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1284 + }
1285 +
1286 + if ( empty( $comment ) ) {
1287 + die( wp_json_encode( array( 'error' => __( 'No comment text was submitted.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1288 + }
1289 +
1290 + // Used in context like NewDash.
1291 + $switched = false;
1292 + if ( is_multisite() && get_current_blog_id() !== $_blog_id ) {
1293 + switch_to_blog( $_blog_id );
1294 + $switched = true;
1295 + }
1296 +
1297 + /** This action is documented in modules/carousel/jetpack-carousel.php */
1298 + do_action( 'jp_carousel_check_blog_user_privileges' );
1299 +
1300 + if ( ! comments_open( $_post_id ) ) {
1301 + if ( $switched ) {
1302 + restore_current_blog();
1303 + }
1304 + die( wp_json_encode( array( 'error' => __( 'Comments on this post are closed.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1305 + }
1306 +
1307 + if ( is_user_logged_in() ) {
1308 + $user = wp_get_current_user();
1309 + $user_id = $user->ID;
1310 + $display_name = $user->display_name;
1311 + $email = $user->user_email;
1312 + $url = $user->user_url;
1313 +
1314 + if ( empty( $user_id ) ) {
1315 + if ( $switched ) {
1316 + restore_current_blog();
1317 + }
1318 + die( wp_json_encode( array( 'error' => __( 'Sorry, but we could not authenticate your request.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1319 + }
1320 + } else {
1321 + $user_id = 0;
1322 + $display_name = isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : null;
1323 + $email = null;
1324 + if ( isset( $_POST['email'] ) && is_string( $_POST['email'] ) ) {
1325 + $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Checked or sanitized below.
1326 + }
1327 + $url = isset( $_POST['url'] ) && is_string( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : null;
1328 +
1329 + if ( get_option( 'require_name_email' ) ) {
1330 + if ( empty( $display_name ) ) {
1331 + if ( $switched ) {
1332 + restore_current_blog();
1333 + }
1334 + die( wp_json_encode( array( 'error' => __( 'Please provide your name.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1335 + }
1336 +
1337 + if ( empty( $email ) ) {
1338 + if ( $switched ) {
1339 + restore_current_blog();
1340 + }
1341 + die( wp_json_encode( array( 'error' => __( 'Please provide an email address.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1342 + }
1343 +
1344 + if ( ! is_email( $email ) ) {
1345 + if ( $switched ) {
1346 + restore_current_blog();
1347 + }
1348 + die( wp_json_encode( array( 'error' => __( 'Please provide a valid email address.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1349 + }
1350 + } else {
1351 + $email = $email !== null ? sanitize_email( $email ) : null;
1352 + }
1353 + }
1354 +
1355 + $comment_data = array(
1356 + 'comment_content' => $comment,
1357 + 'comment_post_ID' => $_post_id,
1358 + 'comment_author' => $display_name,
1359 + 'comment_author_email' => $email,
1360 + 'comment_author_url' => $url,
1361 + 'comment_approved' => 0,
1362 + 'comment_type' => 'comment',
1363 + );
1364 +
1365 + if ( ! empty( $user_id ) ) {
1366 + $comment_data['user_id'] = $user_id;
1367 + }
1368 +
1369 + // Note: wp_new_comment() sanitizes and validates the values (too).
1370 + $comment_id = wp_new_comment( $comment_data );
1371 +
1372 + /**
1373 + * Fires before adding a new comment to the database via the get_attachment_comments ajax endpoint.
1374 + *
1375 + * @module carousel
1376 + *
1377 + * @since 1.6.0
1378 + */
1379 + do_action( 'jp_carousel_post_attachment_comment' );
1380 + $comment_status = wp_get_comment_status( $comment_id );
1381 +
1382 + if ( $switched ) {
1383 + restore_current_blog();
1384 + }
1385 +
1386 + die(
1387 + wp_json_encode(
1388 + array(
1389 + 'comment_id' => $comment_id,
1390 + 'comment_status' => $comment_status,
1391 + ),
1392 + JSON_UNESCAPED_SLASHES
1393 + )
1394 + );
1395 + }
1396 +
1397 + /**
1398 + * Register Carousel settings
1399 + */
1400 + public function register_settings() {
1401 + add_settings_section( 'carousel_section', __( 'Image Gallery Carousel', 'jetpack' ), array( $this, 'carousel_section_callback' ), 'media' );
1402 +
1403 + if ( ! $this->in_jetpack ) {
1404 + add_settings_field( 'carousel_enable_it', __( 'Enable carousel', 'jetpack' ), array( $this, 'carousel_enable_it_callback' ), 'media', 'carousel_section' );
1405 + register_setting( 'media', 'carousel_enable_it', array( $this, 'carousel_enable_it_sanitize' ) );
1406 + }
1407 +
1408 + add_settings_field( 'carousel_background_color', __( 'Background color', 'jetpack' ), array( $this, 'carousel_background_color_callback' ), 'media', 'carousel_section' );
1409 + register_setting( 'media', 'carousel_background_color', array( $this, 'carousel_background_color_sanitize' ) );
1410 +
1411 + add_settings_field( 'carousel_display_exif', __( 'Metadata', 'jetpack' ), array( $this, 'carousel_display_exif_callback' ), 'media', 'carousel_section' );
1412 + register_setting( 'media', 'carousel_display_exif', array( $this, 'carousel_display_exif_sanitize' ) );
1413 +
1414 + add_settings_field( 'carousel_display_comments', __( 'Comments', 'jetpack' ), array( $this, 'carousel_display_comments_callback' ), 'media', 'carousel_section' );
1415 + register_setting( 'media', 'carousel_display_comments', array( $this, 'carousel_display_comments_sanitize' ) );
1416 + }
1417 +
1418 + /**
1419 + * Fulfill the settings section callback requirement by returning nothing.
1420 + */
1421 + public function carousel_section_callback() {
1422 + }
1423 +
1424 + /**
1425 + * Tests if a value is set
1426 + *
1427 + * @param mixed $value The value passed into this function with which to test.
1428 + * @param bool $default_to_1 Default is true.
1429 + *
1430 + * @return bool
1431 + */
1432 + public function test_1or0_option( $value, $default_to_1 = true ) {
1433 + if ( $default_to_1 ) {
1434 + // Boolean false (===) of $value means it has not yet been set, in which case we do want to default to 1.
1435 + if ( false === $value ) {
1436 + $value = 1;
1437 + }
1438 + }
1439 + return ( 1 == $value ) ? 1 : 0; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1440 + }
1441 +
1442 + /**
1443 + * Ensures the value returned is in the correct format.
1444 + *
1445 + * @see test_1or0_option()
1446 + * @param mixed $value The value returned from the test_1or0_option function.
1447 + *
1448 + * @return int
1449 + */
1450 + public function sanitize_1or0_option( $value ) {
1451 + return ( 1 == $value ) ? 1 : 0; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1452 + }
1453 +
1454 + /**
1455 + * Outputs a settings checkbox.
1456 + *
1457 + * @param string $name - For name attribute.
1458 + * @param string $label_text - For label attribute.
1459 + * @param string $extra_text - Additional checkbox description text. Defaults to empty.
1460 + * @param bool $default_to_checked - If the checkbox is checked. Default is true.
1461 + */
1462 + public function settings_checkbox( $name, $label_text, $extra_text = '', $default_to_checked = true ) {
1463 + if ( empty( $name ) ) {
1464 + return;
1465 + }
1466 + $option = $this->test_1or0_option( get_option( $name ), $default_to_checked );
1467 + echo '<fieldset>';
1468 + echo '<input type="checkbox" name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '" value="1" ';
1469 + checked( '1', $option );
1470 + echo '/> <label for="' . esc_attr( $name ) . '">' . wp_kses_post( $label_text ) . '</label>';
1471 + if ( ! empty( $extra_text ) ) {
1472 + echo '<p class="description">' . wp_kses_post( $extra_text ) . '</p>';
1473 + }
1474 + echo '</fieldset>';
1475 + }
1476 +
1477 + /**
1478 + * Output a selection list options
1479 + *
1480 + * @param string $name - For name attribute.
1481 + * @param string $values - For the different option values.
1482 + * @param string $extra_text - Additional option section description text. Defaults to empty.
1483 + */
1484 + public function settings_select( $name, $values, $extra_text = '' ) {
1485 + if ( empty( $name ) || ! is_array( $values ) || empty( $values ) ) {
1486 + return;
1487 + }
1488 + $option = get_option( $name );
1489 + echo '<fieldset>';
1490 + echo '<select name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '">';
1491 + foreach ( $values as $key => $value ) {
1492 + echo '<option value="' . esc_attr( $key ) . '" ';
1493 + selected( $key, $option );
1494 + echo '>' . esc_html( $value ) . '</option>';
1495 + }
1496 + echo '</select>';
1497 + if ( ! empty( $extra_text ) ) {
1498 + echo '<p class="description">' . wp_kses_post( $extra_text ) . '</p>';
1499 + }
1500 + echo '</fieldset>';
1501 + }
1502 +
1503 + /**
1504 + * Callback for checkbox and label of field that allows to toggle exif display.
1505 + */
1506 + public function carousel_display_exif_callback() {
1507 + $this->settings_checkbox( 'carousel_display_exif', __( 'Show photo metadata (<a href="https://en.wikipedia.org/wiki/Exchangeable_image_file_format" rel="noopener noreferrer" target="_blank">Exif</a>) in carousel, when available.', 'jetpack' ) );
1508 + }
1509 +
1510 + /**
1511 + * Callback for checkbox and label of field that allows to toggle comments.
1512 + */
1513 + public function carousel_display_comments_callback() {
1514 + $this->settings_checkbox( 'carousel_display_comments', esc_html__( 'Show comments area in carousel', 'jetpack' ) );
1515 + }
1516 +
1517 + /**
1518 + * Sanitize input for the `carousel_display_exif` setting.
1519 + *
1520 + * @param mixed $value User input setting value.
1521 + *
1522 + * @return int Sanitized value, only 1 or 0.
1523 + */
1524 + public function carousel_display_exif_sanitize( $value ) {
1525 + return $this->sanitize_1or0_option( $value );
1526 + }
1527 +
1528 + /**
1529 + * Return sanitized option for value that controls whether comments will be hidden or not.
1530 + *
1531 + * @param mixed $value Value to sanitize.
1532 + *
1533 + * @return int Sanitized value, only 1 or 0.
1534 + */
1535 + public function carousel_display_comments_sanitize( $value ) {
1536 + return $this->sanitize_1or0_option( $value );
1537 + }
1538 +
1539 + /**
1540 + * Callback for the Carousel background color.
1541 + */
1542 + public function carousel_background_color_callback() {
1543 + $this->settings_select(
1544 + 'carousel_background_color',
1545 + array(
1546 + 'black' => __( 'Black', 'jetpack' ),
1547 + 'white' => __( 'White', 'jetpack' ),
1548 + )
1549 + );
1550 + }
1551 +
1552 + /**
1553 + * Sanitizing the Carousel backgound color selection.
1554 + *
1555 + * @param string $value The color string to sanitize.
1556 + *
1557 + * @return string Sanitized value, 'white' or 'black'.
1558 + */
1559 + public function carousel_background_color_sanitize( $value ) {
1560 + return ( 'white' === $value ) ? 'white' : 'black';
1561 + }
1562 +
1563 + /**
1564 + * Callback to display text for the carousel_enable_it settings field.
1565 + */
1566 + public function carousel_enable_it_callback() {
1567 + $this->settings_checkbox( 'carousel_enable_it', __( 'Display images in full-size carousel slideshow.', 'jetpack' ) );
1568 + }
1569 +
1570 + /**
1571 + * Sanitize input for the `carousel_enable_it` setting.
1572 + *
1573 + * @param mixed $value User input.
1574 + *
1575 + * @return int Sanitized value, only 1 or 0.
1576 + */
1577 + public function carousel_enable_it_sanitize( $value ) {
1578 + return $this->sanitize_1or0_option( $value );
1579 + }
1580 +}
1581 +
1582 +new Jetpack_Carousel();