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