Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
27.26% covered (danger)
27.26%
163 / 598
12.12% covered (danger)
12.12%
4 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Carousel
27.39% covered (danger)
27.39%
163 / 595
12.12% covered (danger)
12.12%
4 / 33
11627.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 check_amp_support
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_disable_jp_carousel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_disable_jp_carousel_single_images
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_enable_jp_carousel_single_images_media_file
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 asset_version
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 display_bail_message
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 check_if_shortcode_processed_and_enqueue_assets
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 check_content_for_blocks
n/a
0 / 0
n/a
0 / 0
5
 remove_core_lightbox_in_gallery
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 filter_gallery_block_render
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 enqueue_assets
92.21% covered (success)
92.21%
71 / 77
0.00% covered (danger)
0.00%
0 / 1
8.03
 add_carousel_skeleton
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 1
132
 set_in_gallery
n/a
0 / 0
n/a
0 / 0
3
 add_data_img_tags_and_enqueue_assets
68.00% covered (warning)
68.00%
34 / 50
0.00% covered (danger)
0.00%
0 / 1
30.83
 add_data_to_images
91.43% covered (success)
91.43%
32 / 35
0.00% covered (danger)
0.00%
0 / 1
10.06
 add_data_to_container
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 maybe_add_amp_lightbox
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 get_attachment_comments
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
240
 post_attachment_comment
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
1056
 register_settings
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 carousel_section_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 test_1or0_option
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 sanitize_1or0_option
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 settings_checkbox
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 settings_select
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 carousel_display_exif_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_display_comments_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_display_exif_sanitize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_display_comments_sanitize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_background_color_callback
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_background_color_sanitize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 carousel_enable_it_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 carousel_enable_it_sanitize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Module: Jetpack Carousel
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Assets;
9use Automattic\Jetpack\Stats\Options as Stats_Options;
10use Automattic\Jetpack\Status;
11use Automattic\Jetpack\Status\Host;
12
13if ( ! defined( 'ABSPATH' ) ) {
14    exit( 0 );
15}
16
17/**
18 * Jetpack_Carousel class.
19 *
20 * @phan-constructor-used-for-side-effects
21 */
22class 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
459            // 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).
460            // Also: not hardcoding path since there is no guarantee site is running on site root in self-hosted context.
461            $is_logged_in         = is_user_logged_in();
462            $comment_registration = (int) get_option( 'comment_registration' );
463            $require_name_email   = (int) get_option( 'require_name_email' );
464            $localize_strings     = array(
465                'widths'                          => $this->prebuilt_widths,
466                'is_logged_in'                    => $is_logged_in,
467                'lang'                            => strtolower( substr( get_locale(), 0, 2 ) ),
468                'ajaxurl'                         => set_url_scheme( admin_url( 'admin-ajax.php' ) ),
469                'nonce'                           => wp_create_nonce( 'carousel_nonce' ),
470                'display_exif'                    => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_exif', true ) ),
471                'display_comments'                => $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_comments', true ) ),
472                'single_image_gallery'            => $this->single_image_gallery_enabled,
473                'single_image_gallery_media_file' => $this->single_image_gallery_enabled_media_file,
474                'background_color'                => $this->carousel_background_color_sanitize( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_background_color', '' ) ),
475                'comment'                         => __( 'Comment', 'jetpack' ),
476                'post_comment'                    => __( 'Post Comment', 'jetpack' ),
477                'write_comment'                   => __( 'Write a Comment...', 'jetpack' ),
478                'loading_comments'                => __( 'Loading Comments...', 'jetpack' ),
479                'image_label'                     => __( 'Open image in full-screen.', 'jetpack' ),
480                'download_original'               => sprintf(
481                    /* translators: %1s is the full-size image width, and %2s is the height. */
482                    __( 'View full size <span class="photo-size">%1$s<span class="photo-size-times">&times;</span>%2$s</span>', 'jetpack' ),
483                    '{0}',
484                    '{1}'
485                ),
486                'no_comment_text'                 => __( 'Please be sure to submit some text with your comment.', 'jetpack' ),
487                'no_comment_email'                => __( 'Please provide an email address to comment.', 'jetpack' ),
488                'no_comment_author'               => __( 'Please provide your name to comment.', 'jetpack' ),
489                'comment_post_error'              => __( 'Sorry, but there was an error posting your comment. Please try again later.', 'jetpack' ),
490                'comment_approved'                => __( 'Your comment was approved.', 'jetpack' ),
491                'comment_unapproved'              => __( 'Your comment is in moderation.', 'jetpack' ),
492                'camera'                          => __( 'Camera', 'jetpack' ),
493                'aperture'                        => __( 'Aperture', 'jetpack' ),
494                'shutter_speed'                   => __( 'Shutter Speed', 'jetpack' ),
495                'focal_length'                    => __( 'Focal Length', 'jetpack' ),
496                'copyright'                       => __( 'Copyright', 'jetpack' ),
497                'comment_registration'            => $comment_registration,
498                'require_name_email'              => $require_name_email,
499                /** This action is documented in core/src/wp-includes/link-template.php */
500                'login_url'                       => wp_login_url( apply_filters( 'the_permalink', get_permalink() ) ),
501                'blog_id'                         => (int) get_current_blog_id(),
502                'meta_data'                       => array( 'camera', 'aperture', 'shutter_speed', 'focal_length', 'copyright' ),
503            );
504
505            /**
506             * Handle WP stats for images in full-screen.
507             * Build string with tracking info.
508             */
509
510            /**
511             * Filter if Jetpack should enable stats collection on carousel views
512             *
513             * @module carousel
514             *
515             * @since 4.3.2
516             *
517             * @param bool Enable Jetpack Carousel stat collection. Default false.
518             */
519            if ( apply_filters( 'jetpack_enable_carousel_stats', false ) && in_array( 'stats', Jetpack::get_active_modules(), true ) && ! ( new Status() )->is_offline_mode() ) {
520                $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;
521
522                // Set the stats as empty if user is logged in but logged-in users shouldn't be tracked.
523                if ( is_user_logged_in() ) {
524                    $stats_options        = Stats_Options::get_options();
525                    $track_loggedin_users = isset( $stats_options['count_roles'] ) ? (bool) $stats_options['count_roles'] : false;
526
527                    if ( ! $track_loggedin_users ) {
528                        $localize_strings['stats'] = '';
529                    }
530                }
531            }
532
533            /**
534             * Filter the strings passed to the Carousel's js file.
535             *
536             * @module carousel
537             *
538             * @since 1.6.0
539             *
540             * @param array $localize_strings Array of strings passed to the Jetpack js file.
541             */
542            $localize_strings = apply_filters( 'jp_carousel_localize_strings', $localize_strings );
543            wp_localize_script( 'jetpack-carousel', 'jetpackCarouselStrings', $localize_strings );
544            wp_enqueue_style(
545                'jetpack-swiper-library',
546                plugins_url( '_inc/blocks/swiper.css', JETPACK__PLUGIN_FILE ),
547                array(),
548                JETPACK__VERSION
549            );
550            wp_enqueue_style( 'jetpack-carousel', plugins_url( 'jetpack-carousel.css', __FILE__ ), array(), $this->asset_version( JETPACK__VERSION ) );
551            wp_style_add_data( 'jetpack-carousel', 'rtl', 'replace' );
552
553            /**
554             * Fires after carousel assets are enqueued for the first time.
555             * Allows for adding additional assets to the carousel page.
556             *
557             * @module carousel
558             *
559             * @since 1.6.0
560             *
561             * @param bool $first_run First load if Carousel on the page.
562             * @param array $localized_strings Array of strings passed to the Jetpack js file.
563             */
564            do_action( 'jp_carousel_enqueue_assets', $this->first_run, $localize_strings );
565
566            // Add the carousel skeleton to the page.
567            $this->localize_strings = $localize_strings;
568            add_action( 'wp_footer', array( $this, 'add_carousel_skeleton' ) );
569
570            $this->first_run = false;
571        }
572    }
573
574    /**
575     * Generate the HTML skeleton that will be picked up by the Carousel JS and used for showing the carousel.
576     */
577    public function add_carousel_skeleton() {
578        $localize_strings = $this->localize_strings;
579        $is_light         = ( 'white' === $localize_strings['background_color'] );
580        // Determine whether to fall back to standard local comments.
581        $use_local_comments = ! isset( $localize_strings['jetpack_comments_iframe_src'] ) || empty( $localize_strings['jetpack_comments_iframe_src'] );
582        $current_user       = wp_get_current_user();
583        $require_name_email = (int) get_option( 'require_name_email' );
584        /* translators: %s is replaced with a field name in the form, e.g. "Email" */
585        $required = ( $require_name_email ) ? __( '%s (Required)', 'jetpack' ) : '%s';
586        require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-spinner.php';
587        ?>
588        <div id="jp-carousel-loading-overlay">
589            <div id="jp-carousel-loading-wrapper">
590                <span id="jp-carousel-library-loading"><?php echo Jetpack_Spinner::render( 40 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?></span>
591            </div>
592        </div>
593        <div class="jp-carousel-overlay<?php echo( $is_light ? ' jp-carousel-light' : '' ); ?>" style="display: none;">
594
595        <div class="jp-carousel-container<?php echo( $is_light ? ' jp-carousel-light' : '' ); ?>">
596            <!-- The Carousel Swiper -->
597            <div
598                class="jp-carousel-wrap swiper jp-carousel-swiper-container jp-carousel-transitions"
599                itemscope
600                itemtype="https://schema.org/ImageGallery">
601                <div class="jp-carousel swiper-wrapper"></div>
602                <div class="jp-swiper-button-prev swiper-button-prev">
603                    <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
604                        <mask id="maskPrev" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="6" width="9" height="12">
605                            <path d="M16.2072 16.59L11.6496 12L16.2072 7.41L14.8041 6L8.8335 12L14.8041 18L16.2072 16.59Z" fill="white"/>
606                        </mask>
607                        <g mask="url(#maskPrev)">
608                            <rect x="0.579102" width="23.8823" height="24" fill="#FFFFFF"/>
609                        </g>
610                    </svg>
611                </div>
612                <div class="jp-swiper-button-next swiper-button-next">
613                    <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
614                        <mask id="maskNext" mask-type="alpha" maskUnits="userSpaceOnUse" x="8" y="6" width="8" height="12">
615                            <path d="M8.59814 16.59L13.1557 12L8.59814 7.41L10.0012 6L15.9718 12L10.0012 18L8.59814 16.59Z" fill="white"/>
616                        </mask>
617                        <g mask="url(#maskNext)">
618                            <rect x="0.34375" width="23.8822" height="24" fill="#FFFFFF"/>
619                        </g>
620                    </svg>
621                </div>
622            </div>
623            <!-- The main close buton -->
624            <div class="jp-carousel-close-hint">
625                <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
626                    <mask id="maskClose" mask-type="alpha" maskUnits="userSpaceOnUse" x="5" y="5" width="15" height="14">
627                        <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"/>
628                    </mask>
629                    <g mask="url(#maskClose)">
630                        <rect x="0.409668" width="23.8823" height="24" fill="#FFFFFF"/>
631                    </g>
632                </svg>
633            </div>
634            <!-- Image info, comments and meta -->
635            <div class="jp-carousel-info">
636                <div class="jp-carousel-info-footer">
637                    <div class="jp-carousel-pagination-container">
638                        <div class="jp-swiper-pagination swiper-pagination"></div>
639                        <div class="jp-carousel-pagination"></div>
640                    </div>
641                    <div class="jp-carousel-photo-title-container">
642                        <h2 class="jp-carousel-photo-caption"></h2>
643                    </div>
644                    <div class="jp-carousel-photo-icons-container">
645                        <a href="#" class="jp-carousel-icon-btn jp-carousel-icon-info" aria-label="<?php esc_attr_e( 'Toggle photo metadata visibility', 'jetpack' ); ?>">
646                            <span class="jp-carousel-icon">
647                                <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
648                                    <mask id="maskInfo" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="21" height="20">
649                                        <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"/>
650                                    </mask>
651                                    <g mask="url(#maskInfo)">
652                                        <rect x="0.8125" width="23.8823" height="24" fill="#FFFFFF"/>
653                                    </g>
654                                </svg>
655                            </span>
656                        </a>
657                        <?php if ( $localize_strings['display_comments'] ) : ?>
658                        <a href="#" class="jp-carousel-icon-btn jp-carousel-icon-comments" aria-label="<?php esc_attr_e( 'Toggle photo comments visibility', 'jetpack' ); ?>">
659                            <span class="jp-carousel-icon">
660                                <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
661                                    <mask id="maskComments" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="21" height="20">
662                                        <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"/>
663                                    </mask>
664                                    <g mask="url(#maskComments)">
665                                        <rect x="0.34668" width="23.8823" height="24" fill="#FFFFFF"/>
666                                    </g>
667                                </svg>
668
669                                <span class="jp-carousel-has-comments-indicator" aria-label="<?php esc_attr_e( 'This image has comments.', 'jetpack' ); ?>"></span>
670                            </span>
671                        </a>
672                        <?php endif; ?>
673                    </div>
674                </div>
675                <div class="jp-carousel-info-extra">
676                    <div class="jp-carousel-info-content-wrapper">
677                        <div class="jp-carousel-photo-title-container">
678                            <h2 class="jp-carousel-photo-title"></h2>
679                        </div>
680                        <div class="jp-carousel-comments-wrapper">
681                            <?php if ( $localize_strings['display_comments'] ) : ?>
682                                <div id="jp-carousel-comments-loading">
683                                    <span><?php echo esc_html( $localize_strings['loading_comments'] ); ?></span>
684                                </div>
685                                <div class="jp-carousel-comments"></div>
686                                <div id="jp-carousel-comment-form-container">
687                                    <span id="jp-carousel-comment-form-spinner"><?php echo Jetpack_Spinner::render( 20 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?></span>
688                                    <div id="jp-carousel-comment-post-results"></div>
689                                    <?php if ( $use_local_comments ) : ?>
690                                        <?php if ( ! $localize_strings['is_logged_in'] && $localize_strings['comment_registration'] ) : ?>
691                                            <div id="jp-carousel-comment-form-commenting-as">
692                                                <p id="jp-carousel-commenting-as">
693                                                    <?php
694                                                        echo wp_kses(
695                                                            __( 'You must be <a href="#" class="jp-carousel-comment-login">logged in</a> to post a comment.', 'jetpack' ),
696                                                            array(
697                                                                'a' => array(
698                                                                    'href'  => array(),
699                                                                    'class' => array(),
700                                                                ),
701                                                            )
702                                                        );
703                                                    ?>
704                                                </p>
705                                            </div>
706                                        <?php else : ?>
707                                            <form id="jp-carousel-comment-form">
708                                                <label for="jp-carousel-comment-form-comment-field" class="screen-reader-text"><?php echo esc_attr( $localize_strings['write_comment'] ); ?></label>
709                                                <textarea
710                                                    name="comment"
711                                                    class="jp-carousel-comment-form-field jp-carousel-comment-form-textarea"
712                                                    id="jp-carousel-comment-form-comment-field"
713                                                    placeholder="<?php echo esc_attr( $localize_strings['write_comment'] ); ?>"
714                                                ></textarea>
715                                                <div id="jp-carousel-comment-form-submit-and-info-wrapper">
716                                                    <div id="jp-carousel-comment-form-commenting-as">
717                                                        <?php if ( $localize_strings['is_logged_in'] ) : ?>
718                                                            <p id="jp-carousel-commenting-as">
719                                                                <?php
720                                                                    printf(
721                                                                        /* translators: %s is replaced with the user's display name */
722                                                                        esc_html__( 'Commenting as %s', 'jetpack' ),
723                                                                        esc_html( $current_user->data->display_name )
724                                                                    );
725                                                                ?>
726                                                            </p>
727                                                        <?php else : ?>
728                                                            <fieldset>
729                                                                <label for="jp-carousel-comment-form-email-field"><?php echo esc_html( sprintf( $required, __( 'Email', 'jetpack' ) ) ); ?></label>
730                                                                <input type="text" name="email" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-email-field" />
731                                                            </fieldset>
732                                                            <fieldset>
733                                                                <label for="jp-carousel-comment-form-author-field"><?php echo esc_html( sprintf( $required, __( 'Name', 'jetpack' ) ) ); ?></label>
734                                                                <input type="text" name="author" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-author-field" />
735                                                            </fieldset>
736                                                            <fieldset>
737                                                                <label for="jp-carousel-comment-form-url-field"><?php esc_html_e( 'Website', 'jetpack' ); ?></label>
738                                                                <input type="text" name="url" class="jp-carousel-comment-form-field jp-carousel-comment-form-text-field" id="jp-carousel-comment-form-url-field" />
739                                                            </fieldset>
740                                                        <?php endif ?>
741                                                    </div>
742                                                    <input
743                                                        type="submit"
744                                                        name="submit"
745                                                        class="jp-carousel-comment-form-button"
746                                                        id="jp-carousel-comment-form-button-submit"
747                                                        value="<?php echo esc_attr( $localize_strings['post_comment'] ); ?>" />
748                                                </div>
749                                            </form>
750                                        <?php endif ?>
751                                    <?php endif ?>
752                                </div>
753                            <?php endif ?>
754                        </div>
755                        <div class="jp-carousel-image-meta">
756                            <div class="jp-carousel-title-and-caption">
757                                <div class="jp-carousel-photo-info">
758                                    <h3 class="jp-carousel-caption" itemprop="caption description"></h3>
759                                </div>
760
761                                <div class="jp-carousel-photo-description"></div>
762                            </div>
763                            <ul class="jp-carousel-image-exif" style="display: none;"></ul>
764                            <a class="jp-carousel-image-download" href="#" target="_blank" style="display: none;">
765                                <svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
766                                    <mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="3" width="19" height="18">
767                                        <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"/>
768                                    </mask>
769                                    <g mask="url(#mask0)">
770                                        <rect x="0.870605" width="23.8823" height="24" fill="#FFFFFF"/>
771                                    </g>
772                                </svg>
773                                <span class="jp-carousel-download-text"></span>
774                            </a>
775                            <div class="jp-carousel-image-map" style="display: none;"></div>
776                        </div>
777                    </div>
778                </div>
779            </div>
780        </div>
781
782        </div>
783        <?php
784    }
785
786    /**
787     * Sets the "in_gallery" flag when the first gallery is encountered (unless in AMP mode).
788     *
789     * @deprecated since 10.8
790     *
791     * @param string $output Gallery shortcode output. Passed through unchanged.
792     *
793     * @return string
794     */
795    public function set_in_gallery( $output ) {
796        if (
797            class_exists( 'Jetpack_AMP_Support' )
798            && Jetpack_AMP_Support::is_amp_request()
799        ) {
800            return $output;
801        }
802        $this->in_gallery = true;
803        return $output;
804    }
805
806    /**
807     * Adds data-* attributes required by carousel to img tags in post HTML
808     * content. To be used by 'the_content' filter.
809     *
810     * @see add_data_to_images()
811     * @see wp_make_content_images_responsive() in wp-includes/media.php
812     *
813     * @param string $content HTML content of the post.
814     * @return string
815     */
816    public function add_data_img_tags_and_enqueue_assets( $content ) {
817        if ( ! is_string( $content ) || $content === '' ) {
818            return '';
819        }
820        if (
821            class_exists( 'Jetpack_AMP_Support' )
822            && Jetpack_AMP_Support::is_amp_request()
823        ) {
824            return $this->maybe_add_amp_lightbox( $content );
825        }
826
827        if ( ! preg_match_all( '/<img [^>]+>/', $content, $matches ) ) {
828            return $content;
829        }
830        $selected_images = array();
831        foreach ( $matches[0] as $image_html ) {
832            if (
833                preg_match( '/(wp-image-|data-id=)\"?([0-9]+)\"?/i', $image_html, $class_id )
834                && ! str_contains( $image_html, 'wp-block-jetpack-slideshow_image' )
835            ) {
836                /**
837                 * Allow filtering the attachment ID used to fetch and populate metadata about an image in a gallery.
838                 *
839                 * @module carousel
840                 *
841                 * @since 12.6
842                 *
843                 * @param int    $attachment_id Attachment ID pulled from image HTML.
844                 * @param string $image_html    Full HTML image tag.
845                 */
846                $attachment_id = absint(
847                    apply_filters(
848                        'jetpack_carousel_image_attachment_id',
849                        $class_id[2],
850                        $image_html
851                    )
852                );
853
854                /**
855                 * The same image tag may be used more than once but with different attribs,
856                 * so save each of them against the attachment id.
857                 */
858                if ( ! isset( $selected_images[ $attachment_id ] ) || ! in_array( $image_html, $selected_images[ $attachment_id ], true ) ) {
859                    $selected_images[ $attachment_id ][] = $image_html;
860                }
861            }
862        }
863
864        $find    = array();
865        $replace = array();
866        if ( empty( $selected_images ) ) {
867            return $content;
868        }
869
870        $attachments = get_posts(
871            array(
872                'include'          => array_keys( $selected_images ),
873                'post_type'        => 'any',
874                'post_status'      => 'any',
875                'suppress_filters' => false,
876            )
877        );
878
879        foreach ( $attachments as $attachment ) {
880            /*
881             * If the item from get_posts isn't an attachment, skip. This can occur when copy-pasta from another WP site.
882             * 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" />"
883             * then, we're going to look up post 7 below, which making sure it is an attachment.
884             *
885             * This is meant as a relatively quick fix, as a better fix is likely to update the get_posts call above to only
886             * include attachments.
887             */
888            if (
889                ! isset( $attachment->ID )
890                || ! wp_attachment_is_image( $attachment->ID )
891                || ! isset( $selected_images[ $attachment->ID ] )
892            ) {
893                continue;
894            }
895            $image_elements = $selected_images[ $attachment->ID ];
896
897            if ( ! is_array( $image_elements ) ) {
898                continue;
899            }
900
901            $attributes      = $this->add_data_to_images( array(), $attachment );
902            $attributes_html = '';
903            foreach ( $attributes as $k => $v ) {
904                $attributes_html .= esc_attr( $k ) . '="' . esc_attr( $v ) . '" ';
905            }
906            foreach ( $image_elements as $image_html ) {
907                $find[]    = $image_html;
908                $replace[] = str_replace( '<img ', "<img $attributes_html", $image_html );
909            }
910        }
911
912        $content = str_replace( $find, $replace, $content );
913        $this->enqueue_assets();
914        return $content;
915    }
916
917    /**
918     * Adds the data attributes themselves to img tags.
919     *
920     * @see add_data_img_tags_and_enqueue_assets()
921     * @see https://developer.wordpress.org/reference/functions/wp_get_attachment_image/ Documentation about wp_get_attachment_image
922     *
923     * @param string[]     $attr       Array of attribute values for the image markup, keyed by attribute name.
924     * @param null|WP_Post $attachment Image attachment post.
925     *
926     * @return string[] Modified image attributes.
927     */
928    public function add_data_to_images( $attr, $attachment = null ) {
929        if (
930            class_exists( 'Jetpack_AMP_Support' )
931            && Jetpack_AMP_Support::is_amp_request()
932        ) {
933            return $attr;
934        }
935
936        if (
937            ! $attachment instanceof WP_Post
938            || ! isset( $attachment->ID )
939            || ! wp_attachment_is_image( $attachment )
940        ) {
941            return $attr;
942        }
943
944        $attachment_id   = (int) $attachment->ID;
945        $orig_file       = wp_get_attachment_image_src( $attachment_id, 'full' );
946        $orig_file       = $orig_file[0] ?? wp_get_attachment_url( $attachment_id );
947        $meta            = wp_get_attachment_metadata( $attachment_id );
948        $size            = isset( $meta['width'] ) ? (int) $meta['width'] . ',' . (int) $meta['height'] : '';
949        $img_meta        = ( ! empty( $meta['image_meta'] ) ) ? (array) $meta['image_meta'] : array();
950        $comments_opened = (int) comments_open( $attachment_id );
951        $display_exif    = $this->test_1or0_option( Jetpack_Options::get_option_and_ensure_autoload( 'carousel_display_exif', true ) );
952
953        /**
954         * Note: Cannot generate a filename from the width and height wp_get_attachment_image_src() returns because
955         * it takes the $content_width global variable themes can set in consideration, therefore returning sizes
956         * which when used to generate a filename will likely result in a 404 on the image.
957         * $content_width has no filter we could temporarily de-register, run wp_get_attachment_image_src(), then
958         * re-register. So using returned file URL instead, which we can define the sizes from through filename
959         * parsing in the JS, as this is a failsafe file reference.
960         *
961         * EG with Twenty Eleven activated:
962         * 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) }
963         *
964         * EG with Twenty Ten activated:
965         * 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) }
966         */
967
968        $large_file_info = wp_get_attachment_image_src( $attachment_id, 'large' );
969        $large_file      = $large_file_info[0] ?? '';
970
971        $attachment_title   = wptexturize( $attachment->post_title );
972        $attachment_desc    = wpautop( wptexturize( $attachment->post_content ) );
973        $attachment_caption = wpautop( wptexturize( $attachment->post_excerpt ) );
974
975        $attr['data-attachment-id']   = $attachment_id;
976        $attr['data-permalink']       = esc_attr( get_permalink( $attachment_id ) );
977        $attr['data-orig-file']       = esc_attr( $orig_file );
978        $attr['data-orig-size']       = $size;
979        $attr['data-comments-opened'] = $comments_opened;
980
981        if ( $display_exif ) {
982            // See https://github.com/Automattic/jetpack/issues/2765.
983            if ( isset( $img_meta['keywords'] ) ) {
984                unset( $img_meta['keywords'] );
985            }
986
987            $img_meta                = wp_json_encode( array_map( 'strval', array_filter( $img_meta, 'is_scalar' ) ), JSON_UNESCAPED_SLASHES | JSON_HEX_AMP );
988            $attr['data-image-meta'] = esc_attr( $img_meta );
989        }
990
991        // 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.
992        $attr['data-image-title']       = esc_attr( htmlspecialchars( $attachment_title, ENT_COMPAT ) );
993        $attr['data-image-description'] = esc_attr( htmlspecialchars( $attachment_desc, ENT_COMPAT ) );
994        $attr['data-image-caption']     = esc_attr( htmlspecialchars( $attachment_caption, ENT_COMPAT ) );
995        $attr['data-large-file']        = esc_attr( $large_file );
996        return $attr;
997    }
998
999    /**
1000     * Add additional attributes to the Gallery container HTML.
1001     *
1002     * @param string $html The HTML to which the additional attributes are added.
1003     *
1004     * @return string
1005     */
1006    public function add_data_to_container( $html ) {
1007        global $post;
1008        if (
1009            class_exists( 'Jetpack_AMP_Support' )
1010            && Jetpack_AMP_Support::is_amp_request()
1011        ) {
1012            return $html;
1013        }
1014
1015        if ( isset( $post ) ) {
1016            $blog_id = (int) get_current_blog_id();
1017
1018            $extra_data = array(
1019                'data-carousel-extra' => array(
1020                    'blog_id'   => $blog_id,
1021                    'permalink' => get_permalink( $post->ID ),
1022                ),
1023            );
1024
1025            /**
1026             * Filter the data added to the Gallery container.
1027             *
1028             * @module carousel
1029             *
1030             * @since 1.6.0
1031             *
1032             * @param array $extra_data Array of data about the site and the post.
1033             */
1034            $extra_data = apply_filters( 'jp_carousel_add_data_to_container', $extra_data );
1035            foreach ( (array) $extra_data as $data_key => $data_values ) {
1036                $html = str_replace( '<div ', '<div ' . esc_attr( $data_key ) . "='" . esc_attr( wp_json_encode( $data_values, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ) . "' ", $html );
1037                $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 );
1038                $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 );
1039                $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 );
1040            }
1041        }
1042
1043        return $html;
1044    }
1045
1046    /**
1047     * Conditionally adds amp-lightbox to galleries and images.
1048     *
1049     * This applies to gallery blocks and shortcodes,
1050     * in addition to images that are wrapped in a link to the page.
1051     * Images wrapped in a link to the media file shouldn't get an amp-lightbox.
1052     *
1053     * @param string $content The content to possibly add amp-lightbox to.
1054     * @return string The content, with amp-lightbox possibly added.
1055     */
1056    public function maybe_add_amp_lightbox( $content ) {
1057        $content = preg_replace(
1058            array(
1059                '#(<figure)[^>]*(?=class=(["\']?)[^>]*wp-block-gallery[^>]*\2)#is', // Gallery block.
1060                '#(\[gallery)(?=\s+)#', // Gallery shortcode.
1061            ),
1062            array(
1063                '\1 data-amp-lightbox="true" ', // https://github.com/ampproject/amp-wp/blob/1094ea03bd5dc92889405a47a8c41de1a88908de/includes/sanitizers/class-amp-gallery-block-sanitizer.php#L84.
1064                '\1 amp-lightbox="true"', // https://github.com/ampproject/amp-wp/blob/1094ea03bd5dc92889405a47a8c41de1a88908de/includes/embeds/class-amp-gallery-embed.php#L64.
1065            ),
1066            $content
1067        );
1068
1069        return preg_replace_callback(
1070            '#(<a[^>]* href=(["\']?)(\S+)\2>)\s*(<img[^>]*)(class=(["\']?)[^>]*wp-image-[0-9]+[^>]*\6.*>)\s*</a>#is',
1071            static function ( $matches ) {
1072                if ( ! preg_match( '#\.\w+$#', $matches[3] ) ) {
1073                    // 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.
1074                    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.
1075                }
1076
1077                return $matches[0];
1078            },
1079            $content
1080        );
1081    }
1082
1083    /**
1084     * Retrieves comment information
1085     *
1086     * @return string
1087     */
1088    public function get_attachment_comments() {
1089        if ( ! headers_sent() ) {
1090            header( 'Content-type: text/javascript' );
1091        }
1092
1093        /**
1094         * Allows for the checking of privileges of the blog user before comments
1095         * are packaged as JSON and sent back from the get_attachment_comments
1096         * AJAX endpoint
1097         *
1098         * @module carousel
1099         *
1100         * @since 1.6.0
1101         */
1102        do_action( 'jp_carousel_check_blog_user_privileges' );
1103
1104        // 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).
1105        $attachment_id = ( isset( $_REQUEST['id'] ) ) ? (int) $_REQUEST['id'] : 0;
1106        $offset        = ( isset( $_REQUEST['offset'] ) ) ? (int) $_REQUEST['offset'] : 0;
1107        // phpcs:enable
1108
1109        if ( ! $attachment_id ) {
1110            wp_send_json_error(
1111                __( 'Missing attachment ID.', 'jetpack' ),
1112                403,
1113                JSON_UNESCAPED_SLASHES
1114            );
1115            return;
1116        }
1117
1118        $attachment_post = get_post( $attachment_id );
1119        // If we have no info about that attachment, bail.
1120        if ( ! ( $attachment_post instanceof WP_Post ) ) {
1121            wp_send_json_error(
1122                __( 'Missing attachment info.', 'jetpack' ),
1123                403,
1124                JSON_UNESCAPED_SLASHES
1125            );
1126            return;
1127        }
1128
1129        // This AJAX call should only be used to fetch comments of attachments.
1130        if ( 'attachment' !== $attachment_post->post_type ) {
1131            wp_send_json_error(
1132                __( 'You aren’t authorized to do that.', 'jetpack' ),
1133                403,
1134                JSON_UNESCAPED_SLASHES
1135            );
1136            return;
1137        }
1138
1139        $parent_post = get_post_parent( $attachment_id );
1140
1141        /*
1142         * If we have no info about that parent post, no extra checks.
1143         * The attachment doesn't have a parent post, so is public.
1144         * If we have a parent post, let's ensure the user has access to it.
1145         */
1146        if ( $parent_post instanceof WP_Post ) {
1147            /*
1148             * Fetch info about user making the request.
1149             * If we have no info, bail.
1150             * Even logged out users should get a WP_User user with id 0.
1151             */
1152            $current_user = wp_get_current_user();
1153            if ( ! ( $current_user instanceof WP_User ) ) {
1154                wp_send_json_error(
1155                    __( 'Missing user info.', 'jetpack' ),
1156                    403,
1157                    JSON_UNESCAPED_SLASHES
1158                );
1159                return;
1160            }
1161
1162            /*
1163             * If a post is private / draft
1164             * and the current user doesn't have access to it,
1165             * bail.
1166             */
1167            if (
1168                'publish' !== $parent_post->post_status
1169                && ! current_user_can( 'read_post', $parent_post->ID )
1170            ) {
1171                wp_send_json_error(
1172                    __( 'You aren’t authorized to do that.', 'jetpack' ),
1173                    403,
1174                    JSON_UNESCAPED_SLASHES
1175                );
1176                return;
1177            }
1178        }
1179
1180        if ( $offset < 1 ) {
1181            $offset = 0;
1182        }
1183
1184        $comments = get_comments(
1185            array(
1186                'status'  => 'approve',
1187                'order'   => ( 'asc' === get_option( 'comment_order' ) ) ? 'ASC' : 'DESC',
1188                'number'  => 10,
1189                'offset'  => $offset,
1190                'post_id' => $attachment_id,
1191            )
1192        );
1193
1194        $out = array();
1195
1196        // Can't just send the results, they contain the commenter's email address.
1197        foreach ( $comments as $comment ) {
1198            $avatar = get_avatar( $comment->comment_author_email, 64 );
1199            if ( ! $avatar ) {
1200                $avatar = '';
1201            }
1202            $out[] = array(
1203                'id'              => $comment->comment_ID,
1204                'parent_id'       => $comment->comment_parent,
1205                'author_markup'   => get_comment_author_link( $comment->comment_ID ),
1206                'gravatar_markup' => $avatar,
1207                'date_gmt'        => $comment->comment_date_gmt,
1208                'content'         => wpautop( $comment->comment_content ),
1209            );
1210        }
1211
1212        wp_send_json( $out, null, JSON_UNESCAPED_SLASHES );
1213    }
1214
1215    /**
1216     * Adds a new comment to the database
1217     *
1218     * @return never
1219     */
1220    public function post_attachment_comment() {
1221        if ( ! headers_sent() ) {
1222            header( 'Content-type: text/javascript' );
1223        }
1224
1225        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
1226            die( wp_json_encode( array( 'error' => __( 'Nonce verification failed.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1227        }
1228
1229        $_blog_id = isset( $_POST['blog_id'] ) ? (int) $_POST['blog_id'] : 0;
1230        $_post_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
1231        $comment  = isset( $_POST['comment'] ) ? filter_var( wp_unslash( $_POST['comment'] ) ) : null;
1232
1233        if ( empty( $_blog_id ) ) {
1234            die( wp_json_encode( array( 'error' => __( 'Missing target blog ID.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1235        }
1236
1237        if ( empty( $_post_id ) ) {
1238            die( wp_json_encode( array( 'error' => __( 'Missing target post ID.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1239        }
1240
1241        if ( empty( $comment ) ) {
1242            die( wp_json_encode( array( 'error' => __( 'No comment text was submitted.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1243        }
1244
1245        // Used in context like NewDash.
1246        $switched = false;
1247        if ( is_multisite() && get_current_blog_id() !== $_blog_id ) {
1248            switch_to_blog( $_blog_id );
1249            $switched = true;
1250        }
1251
1252        /** This action is documented in modules/carousel/jetpack-carousel.php */
1253        do_action( 'jp_carousel_check_blog_user_privileges' );
1254
1255        if ( ! comments_open( $_post_id ) ) {
1256            if ( $switched ) {
1257                restore_current_blog();
1258            }
1259            die( wp_json_encode( array( 'error' => __( 'Comments on this post are closed.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1260        }
1261
1262        if ( is_user_logged_in() ) {
1263            $user         = wp_get_current_user();
1264            $user_id      = $user->ID;
1265            $display_name = $user->display_name;
1266            $email        = $user->user_email;
1267            $url          = $user->user_url;
1268
1269            if ( empty( $user_id ) ) {
1270                if ( $switched ) {
1271                    restore_current_blog();
1272                }
1273                die( wp_json_encode( array( 'error' => __( 'Sorry, but we could not authenticate your request.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1274            }
1275        } else {
1276            $user_id      = 0;
1277            $display_name = isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : null;
1278            $email        = null;
1279            if ( isset( $_POST['email'] ) && is_string( $_POST['email'] ) ) {
1280                $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Checked or sanitized below.
1281            }
1282            $url = isset( $_POST['url'] ) && is_string( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : null;
1283
1284            if ( get_option( 'require_name_email' ) ) {
1285                if ( empty( $display_name ) ) {
1286                    if ( $switched ) {
1287                        restore_current_blog();
1288                    }
1289                    die( wp_json_encode( array( 'error' => __( 'Please provide your name.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1290                }
1291
1292                if ( empty( $email ) ) {
1293                    if ( $switched ) {
1294                        restore_current_blog();
1295                    }
1296                    die( wp_json_encode( array( 'error' => __( 'Please provide an email address.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1297                }
1298
1299                if ( ! is_email( $email ) ) {
1300                    if ( $switched ) {
1301                        restore_current_blog();
1302                    }
1303                    die( wp_json_encode( array( 'error' => __( 'Please provide a valid email address.', 'jetpack' ) ), JSON_UNESCAPED_SLASHES ) );
1304                }
1305            } else {
1306                $email = $email !== null ? sanitize_email( $email ) : null;
1307            }
1308        }
1309
1310        $comment_data = array(
1311            'comment_content'      => $comment,
1312            'comment_post_ID'      => $_post_id,
1313            'comment_author'       => $display_name,
1314            'comment_author_email' => $email,
1315            'comment_author_url'   => $url,
1316            'comment_approved'     => 0,
1317            'comment_type'         => 'comment',
1318        );
1319
1320        if ( ! empty( $user_id ) ) {
1321            $comment_data['user_id'] = $user_id;
1322        }
1323
1324        // Note: wp_new_comment() sanitizes and validates the values (too).
1325        $comment_id = wp_new_comment( $comment_data );
1326
1327        /**
1328         * Fires before adding a new comment to the database via the get_attachment_comments ajax endpoint.
1329         *
1330         * @module carousel
1331         *
1332         * @since 1.6.0
1333         */
1334        do_action( 'jp_carousel_post_attachment_comment' );
1335        $comment_status = wp_get_comment_status( $comment_id );
1336
1337        if ( $switched ) {
1338            restore_current_blog();
1339        }
1340
1341        die(
1342            wp_json_encode(
1343                array(
1344                    'comment_id'     => $comment_id,
1345                    'comment_status' => $comment_status,
1346                ),
1347                JSON_UNESCAPED_SLASHES
1348            )
1349        );
1350    }
1351
1352    /**
1353     * Register Carousel settings
1354     */
1355    public function register_settings() {
1356        add_settings_section( 'carousel_section', __( 'Image Gallery Carousel', 'jetpack' ), array( $this, 'carousel_section_callback' ), 'media' );
1357
1358        if ( ! $this->in_jetpack ) {
1359            add_settings_field( 'carousel_enable_it', __( 'Enable carousel', 'jetpack' ), array( $this, 'carousel_enable_it_callback' ), 'media', 'carousel_section' );
1360            register_setting( 'media', 'carousel_enable_it', array( $this, 'carousel_enable_it_sanitize' ) );
1361        }
1362
1363        add_settings_field( 'carousel_background_color', __( 'Background color', 'jetpack' ), array( $this, 'carousel_background_color_callback' ), 'media', 'carousel_section' );
1364        register_setting( 'media', 'carousel_background_color', array( $this, 'carousel_background_color_sanitize' ) );
1365
1366        add_settings_field( 'carousel_display_exif', __( 'Metadata', 'jetpack' ), array( $this, 'carousel_display_exif_callback' ), 'media', 'carousel_section' );
1367        register_setting( 'media', 'carousel_display_exif', array( $this, 'carousel_display_exif_sanitize' ) );
1368
1369        add_settings_field( 'carousel_display_comments', __( 'Comments', 'jetpack' ), array( $this, 'carousel_display_comments_callback' ), 'media', 'carousel_section' );
1370        register_setting( 'media', 'carousel_display_comments', array( $this, 'carousel_display_comments_sanitize' ) );
1371    }
1372
1373    /**
1374     * Fulfill the settings section callback requirement by returning nothing.
1375     */
1376    public function carousel_section_callback() {
1377    }
1378
1379    /**
1380     * Tests if a value is set
1381     *
1382     * @param mixed $value The value passed into this function with which to test.
1383     * @param bool  $default_to_1 Default is true.
1384     *
1385     * @return bool
1386     */
1387    public function test_1or0_option( $value, $default_to_1 = true ) {
1388        if ( $default_to_1 ) {
1389            // Boolean false (===) of $value means it has not yet been set, in which case we do want to default to 1.
1390            if ( false === $value ) {
1391                $value = 1;
1392            }
1393        }
1394        return ( 1 == $value ) ? 1 : 0; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1395    }
1396
1397    /**
1398     * Ensures the value returned is in the correct format.
1399     *
1400     * @see test_1or0_option()
1401     * @param mixed $value The value returned from the test_1or0_option function.
1402     *
1403     * @return int
1404     */
1405    public function sanitize_1or0_option( $value ) {
1406        return ( 1 == $value ) ? 1 : 0; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1407    }
1408
1409    /**
1410     * Outputs a settings checkbox.
1411     *
1412     * @param string $name - For name attribute.
1413     * @param string $label_text - For label attribute.
1414     * @param string $extra_text - Additional checkbox description text. Defaults to empty.
1415     * @param bool   $default_to_checked - If the checkbox is checked. Default is true.
1416     */
1417    public function settings_checkbox( $name, $label_text, $extra_text = '', $default_to_checked = true ) {
1418        if ( empty( $name ) ) {
1419            return;
1420        }
1421        $option = $this->test_1or0_option( get_option( $name ), $default_to_checked );
1422        echo '<fieldset>';
1423        echo '<input type="checkbox" name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '" value="1" ';
1424        checked( '1', $option );
1425        echo '/> <label for="' . esc_attr( $name ) . '">' . wp_kses_post( $label_text ) . '</label>';
1426        if ( ! empty( $extra_text ) ) {
1427            echo '<p class="description">' . wp_kses_post( $extra_text ) . '</p>';
1428        }
1429        echo '</fieldset>';
1430    }
1431
1432    /**
1433     * Output a selection list options
1434     *
1435     * @param string $name - For name attribute.
1436     * @param string $values - For the different option values.
1437     * @param string $extra_text - Additional option section description text. Defaults to empty.
1438     */
1439    public function settings_select( $name, $values, $extra_text = '' ) {
1440        if ( empty( $name ) || ! is_array( $values ) || empty( $values ) ) {
1441            return;
1442        }
1443        $option = get_option( $name );
1444        echo '<fieldset>';
1445        echo '<select name="' . esc_attr( $name ) . '" id="' . esc_attr( $name ) . '">';
1446        foreach ( $values as $key => $value ) {
1447            echo '<option value="' . esc_attr( $key ) . '" ';
1448            selected( $key, $option );
1449            echo '>' . esc_html( $value ) . '</option>';
1450        }
1451        echo '</select>';
1452        if ( ! empty( $extra_text ) ) {
1453            echo '<p class="description">' . wp_kses_post( $extra_text ) . '</p>';
1454        }
1455        echo '</fieldset>';
1456    }
1457
1458    /**
1459     * Callback for checkbox and label of field that allows to toggle exif display.
1460     */
1461    public function carousel_display_exif_callback() {
1462        $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' ) );
1463    }
1464
1465    /**
1466     * Callback for checkbox and label of field that allows to toggle comments.
1467     */
1468    public function carousel_display_comments_callback() {
1469        $this->settings_checkbox( 'carousel_display_comments', esc_html__( 'Show comments area in carousel', 'jetpack' ) );
1470    }
1471
1472    /**
1473     * Sanitize input for the `carousel_display_exif` setting.
1474     *
1475     * @param mixed $value User input setting value.
1476     *
1477     * @return int Sanitized value, only 1 or 0.
1478     */
1479    public function carousel_display_exif_sanitize( $value ) {
1480        return $this->sanitize_1or0_option( $value );
1481    }
1482
1483    /**
1484     * Return sanitized option for value that controls whether comments will be hidden or not.
1485     *
1486     * @param mixed $value Value to sanitize.
1487     *
1488     * @return int Sanitized value, only 1 or 0.
1489     */
1490    public function carousel_display_comments_sanitize( $value ) {
1491        return $this->sanitize_1or0_option( $value );
1492    }
1493
1494    /**
1495     * Callback for the Carousel background color.
1496     */
1497    public function carousel_background_color_callback() {
1498        $this->settings_select(
1499            'carousel_background_color',
1500            array(
1501                'black' => __( 'Black', 'jetpack' ),
1502                'white' => __( 'White', 'jetpack' ),
1503            )
1504        );
1505    }
1506
1507    /**
1508     * Sanitizing the Carousel backgound color selection.
1509     *
1510     * @param string $value The color string to sanitize.
1511     *
1512     * @return string Sanitized value, 'white' or 'black'.
1513     */
1514    public function carousel_background_color_sanitize( $value ) {
1515        return ( 'white' === $value ) ? 'white' : 'black';
1516    }
1517
1518    /**
1519     * Callback to display text for the carousel_enable_it settings field.
1520     */
1521    public function carousel_enable_it_callback() {
1522        $this->settings_checkbox( 'carousel_enable_it', __( 'Display images in full-size carousel slideshow.', 'jetpack' ) );
1523    }
1524
1525    /**
1526     * Sanitize input for the `carousel_enable_it` setting.
1527     *
1528     * @param mixed $value User input.
1529     *
1530     * @return int Sanitized value, only 1 or 0.
1531     */
1532    public function carousel_enable_it_sanitize( $value ) {
1533        return $this->sanitize_1or0_option( $value );
1534    }
1535}
1536
1537new Jetpack_Carousel();