PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.7.2 13.8.3 All 506 releases
jetpack / modules / carousel / jetpack-carousel.php

jetpack-carousel.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at modules/carousel/jetpack-carousel.php

1,583 lines 58.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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();
1583