Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 746
0.00% covered (danger)
0.00%
0 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Newspack_Blocks
0.00% covered (danger)
0.00%
0 / 745
0.00% covered (danger)
0.00%
0 / 45
64262
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 hide_post_content_when_iframe_block_is_fullscreen
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 add_body_classes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 script_enqueue_helper
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_placeholder_blocks_assets
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 get_custom_taxonomies
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
6
 can_use_name_your_price
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_block_editor_assets
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
42
 manage_view_scripts
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 enqueue_block_styles_assets
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_view_assets
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 block_classes
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 block_styles
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 image_size_for_orientation
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 1
42
 add_image_sizes
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 should_deduplicate_block
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_specific_posts_from_blocks
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 build_articles_query
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 1
3540
 template_inc
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 prepare_authors
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 get_term_classes
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
182
 get_patterns_for_post_type
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 get_all_sponsors
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 get_sponsor_label
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 get_sponsor_byline
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 get_sponsor_logos
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 newspack_display_sponsors_and_authors
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 newspack_display_sponsors_and_categories
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 remove_wc_memberships_excerpt_limit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 filter_excerpt
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 remove_excerpt_filter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 filter_excerpt_length
n/a
0 / 0
n/a
0 / 0
4
 remove_excerpt_length_filter
n/a
0 / 0
n/a
0 / 0
2
 more_excerpt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filter_excerpt_more
n/a
0 / 0
n/a
0 / 0
2
 remove_excerpt_more_filter
n/a
0 / 0
n/a
0 / 0
1
 get_post_link
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 sanitize_svg
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 disable_jetpack_donate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 template_include
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 get_post_status_label
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 get_color_for_contrast
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 get_sanitized_image_attributes
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 get_displayed_post_date
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_datetime_post_date
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_formatted_displayed_post_date
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_article_meta_footer
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_formatted_amount
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
272
 get_image_caption
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * Newspack blocks functionality
4 *
5 * @package Newspack_Blocks
6 */
7
8/**
9 * Newspack blocks functionality
10 */
11class Newspack_Blocks {
12
13    /**
14     * Script handles.
15     */
16    const SCRIPT_HANDLES = [
17        'modal-checkout'       => 'newspack-blocks-donate-modal-checkout',
18        'modal-checkout-block' => 'newspack-blocks-donate-modal-checkout-block',
19        'frequency-based'      => 'newspack-blocks-donate-frequency-based',
20        'tiers-based'          => 'newspack-blocks-donate-tiers-based',
21    ];
22
23    /**
24     * Add hooks and filters.
25     */
26    public static function init() {
27        add_action( 'after_setup_theme', [ __CLASS__, 'add_image_sizes' ] );
28        add_post_type_support( 'post', 'newspack_blocks' );
29        add_post_type_support( 'page', 'newspack_blocks' );
30        add_action( 'jetpack_register_gutenberg_extensions', [ __CLASS__, 'disable_jetpack_donate' ], 99 );
31        add_filter( 'the_content', [ __CLASS__, 'hide_post_content_when_iframe_block_is_fullscreen' ] );
32        add_filter( 'body_class', [ __CLASS__, 'add_body_classes' ] );
33        add_filter( 'admin_body_class', [ __CLASS__, 'add_body_classes' ] );
34
35        /**
36         * Disable NextGEN's `C_NextGen_Shortcode_Manager`.
37         *
38         * The way it currently parses `the_content` conflicts with the REST API
39         * request to save a post containing a Homepage Posts block. This is due to
40         * how it uses output buffering through `ob_start()` on REST requests.
41         *
42         * @link https://plugins.trac.wordpress.org/browser/nextgen-gallery/tags/3.23/non_pope/class.nextgen_shortcode_manager.php#L193.
43         */
44        if ( ! defined( 'NGG_DISABLE_SHORTCODE_MANAGER' ) ) {
45            define( 'NGG_DISABLE_SHORTCODE_MANAGER', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
46        }
47    }
48
49    /**
50     * Hide the post content when it contains an iframe block that is set to fullscreen mode.
51     *
52     * @param string $content post content from the_content hook.
53     * @return string the post content.
54     */
55    public static function hide_post_content_when_iframe_block_is_fullscreen( $content ) {
56        if ( has_block( 'newspack-blocks/iframe' ) ) {
57            $blocks = parse_blocks( get_post()->post_content );
58
59            foreach ( $blocks as $block ) {
60                if ( 'newspack-blocks/iframe' === $block['blockName']
61                    && is_array( $block['attrs'] )
62                    && array_key_exists( 'isFullScreen', $block['attrs'] )
63                    && $block['attrs']['isFullScreen']
64                    ) {
65                    // we don't need the post content since the iframe will be fullscreen.
66                    $content = render_block( $block );
67
68                    add_filter(
69                        'body_class',
70                        function( $classes ) {
71                            $classes[] = 'newspack-post-with-fullscreen-iframe';
72                            return $classes;
73                        }
74                    );
75
76                    // we don't need to show Newspack popups since the iframe will take over them.
77                    add_filter( 'newspack_popups_assess_has_disabled_popups', '__return_true' );
78                }
79            }
80        }
81
82        return $content;
83    }
84
85    /**
86     * Body class.
87     *
88     * @param string|array $classes Array or string of body class names.
89     * @return string|array Modified array or string of body class names.
90     */
91    public static function add_body_classes( $classes ) {
92        if ( wp_is_block_theme() ) {
93            // Handle string (admin) vs array (frontend) cases.
94            if ( is_string( $classes ) ) {
95                $classes .= ' is-block-theme ';
96            } else {
97                $classes[] = 'is-block-theme';
98            }
99        }
100
101        return $classes;
102    }
103
104    /**
105     * Gather dependencies and paths needed for script enqueuing.
106     *
107     * @param string $script_path Path to the script relative to plugin root.
108     *
109     * @return array Associative array including dependency array, version, and web path to the script. Returns false if script doesn't exist.
110     */
111    public static function script_enqueue_helper( $script_path ) {
112        $local_path = NEWSPACK_BLOCKS__PLUGIN_DIR . $script_path;
113        if ( ! file_exists( $local_path ) ) {
114            return false;
115        }
116
117        $path_info   = pathinfo( $local_path );
118        $asset_path  = $path_info['dirname'] . '/' . $path_info['filename'] . '.asset.php';
119        $script_data = file_exists( $asset_path )
120            ? require $asset_path
121            : array(
122                'dependencies' => [ 'wp-a11y', 'wp-escape-html', 'wp-i18n', 'wp-polyfill' ],
123                'version'      => filemtime( $local_path ),
124            );
125
126        $script_data['script_path'] = plugins_url( $script_path, NEWSPACK_BLOCKS__PLUGIN_FILE );
127        return $script_data;
128    }
129
130    /**
131     * Enqueue placeholder blocks assets.
132     */
133    public static function enqueue_placeholder_blocks_assets() {
134        $script_data = self::script_enqueue_helper( NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . 'placeholder_blocks.js' );
135        if ( $script_data ) {
136            wp_enqueue_script(
137                'newspack-blocks-placeholder-blocks',
138                $script_data['script_path'],
139                $script_data['dependencies'],
140                $script_data['version'],
141                true
142            );
143            wp_set_script_translations(
144                'newspack-blocks-placeholder-blocks',
145                'jetpack-mu-wpcom',
146                plugin_dir_path( NEWSPACK_BLOCKS__PLUGIN_FILE ) . 'languages'
147            );
148        }
149    }
150
151    /**
152     * Gets the list of custom taxonomies that will be available for filtering in the blocks
153     *
154     * @return array Array of custom taxonomies where each taxonomy is an array with slug and label keys.
155     */
156    public static function get_custom_taxonomies() {
157        $custom_taxonomies = array_map(
158            function( $tax ) {
159                if ( ! empty( array_intersect( [ 'post', 'page' ], $tax->object_type ) ) ) {
160                    return [
161                        'slug'  => $tax->name,
162                        'label' => $tax->label,
163                    ];
164                }
165            },
166            get_taxonomies(
167                [
168                    'public'       => true,
169                    '_builtin'     => false,
170                    'show_in_rest' => true,
171                ],
172                'objects'
173            )
174        );
175        $custom_taxonomies = array_values(
176            array_filter(
177                $custom_taxonomies,
178                function( $tax ) {
179                    return ! empty( $tax );
180                }
181            )
182        );
183
184        /**
185         * Filters the custom taxonomies that will be available in the Home Page block.
186         *
187         * By default, on the top of category and tags, will display any public taxonomy applied to post or pages
188         *
189         * @param array $custom_taxonomies Array of custom taxonomies where each taxonomy is an array with slug and label keys.
190         */
191        return apply_filters( 'newspack_blocks_home_page_block_custom_taxonomies', $custom_taxonomies );
192    }
193
194    /**
195     * Check if the Name Your Price extension is available.
196     *
197     * @return bool True if available, false if not.
198     */
199    public static function can_use_name_your_price() {
200        // If the donation platform is NRH, the Donate block should behave as if Name Your Price is available.
201        if ( method_exists( 'Newspack\Donations', 'is_platform_nrh' ) && \Newspack\Donations::is_platform_nrh() ) {
202            return true;
203        }
204        return class_exists( 'WC_Name_Your_Price_Helpers' );
205    }
206
207    /**
208     * Enqueue block scripts and styles for editor.
209     */
210    public static function enqueue_block_editor_assets() {
211        $script_data = static::script_enqueue_helper( NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . 'editor.js' );
212
213        if ( $script_data ) {
214            wp_enqueue_script(
215                'newspack-blocks-editor',
216                $script_data['script_path'],
217                $script_data['dependencies'],
218                $script_data['version'],
219                true
220            );
221
222            $localized_data = [
223                'patterns'                   => self::get_patterns_for_post_type( get_post_type() ),
224                'posts_rest_url'             => rest_url( 'newspack-blocks/v1/newspack-blocks-posts' ),
225                'specific_posts_rest_url'    => rest_url( 'newspack-blocks/v1/newspack-blocks-specific-posts' ),
226                'authors_rest_url'           => rest_url( 'newspack-blocks/v1/authors' ),
227                'assets_path'                => plugins_url( '/src/assets', NEWSPACK_BLOCKS__PLUGIN_FILE ),
228                'post_subtitle'              => get_theme_support( 'post-subtitle' ),
229                'iframe_accepted_file_mimes' => WP_REST_Newspack_Iframe_Controller::iframe_accepted_file_mimes(),
230                'iframe_can_upload_archives' => WP_REST_Newspack_Iframe_Controller::can_upload_archives(),
231                'supports_recaptcha'         => class_exists( 'Newspack\Recaptcha' ),
232                'has_recaptcha'              => class_exists( 'Newspack\Recaptcha' ) && \Newspack\Recaptcha::can_use_captcha(),
233                'recaptcha_url'              => admin_url( 'admin.php?page=newspack-settings' ),
234                'custom_taxonomies'          => self::get_custom_taxonomies(),
235                'can_use_name_your_price'    => self::can_use_name_your_price(),
236                'tier_amounts_template'      => self::get_formatted_amount(),
237                'currency'                   => function_exists( 'get_woocommerce_currency' ) ? \get_woocommerce_currency() : 'USD',
238            ];
239
240            if ( class_exists( 'WP_REST_Newspack_Author_List_Controller' ) ) {
241                $localized_data['can_use_cap']    = class_exists( 'CoAuthors_Guest_Authors' );
242                $author_list_controller           = new WP_REST_Newspack_Author_List_Controller();
243                $localized_data['editable_roles'] = $author_list_controller->get_editable_roles();
244            }
245
246            if ( class_exists( '\Newspack\Authors_Custom_Fields' ) ) {
247                $localized_data['author_custom_fields'] = \Newspack\Authors_Custom_Fields::get_custom_fields();
248            }
249
250            wp_localize_script(
251                'newspack-blocks-editor',
252                'newspack_blocks_data',
253                $localized_data
254            );
255
256            wp_set_script_translations(
257                'newspack-blocks-editor',
258                'jetpack-mu-wpcom',
259                plugin_dir_path( NEWSPACK_BLOCKS__PLUGIN_FILE ) . 'languages'
260            );
261        }
262
263        $editor_style = plugins_url( NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . 'editor.css', NEWSPACK_BLOCKS__PLUGIN_FILE );
264
265        wp_enqueue_style(
266            'newspack-blocks-editor',
267            $editor_style,
268            array(),
269            NEWSPACK_BLOCKS__VERSION
270        );
271    }
272
273    /**
274     * Enqueue block scripts and styles for view.
275     */
276    public static function manage_view_scripts() {
277        if ( is_admin() ) {
278            // In editor environment, do nothing.
279            return;
280        }
281        $src_directory  = NEWSPACK_BLOCKS__PLUGIN_DIR . 'src/blocks/';
282        $dist_directory = NEWSPACK_BLOCKS__PLUGIN_DIR . 'dist/';
283        $iterator       = new DirectoryIterator( $src_directory );
284        foreach ( $iterator as $block_directory ) {
285            if ( ! $block_directory->isDir() || $block_directory->isDot() ) {
286                continue;
287            }
288            $type = $block_directory->getFilename();
289
290            /* If view.php is found, include it and use for block rendering. */
291            $view_php_path = $src_directory . $type . '/view.php';
292
293            if ( file_exists( $view_php_path ) ) {
294                include_once $view_php_path;
295                continue;
296            }
297
298            /* If view.php is missing but view Javascript file is found, do generic view asset loading. */
299            $view_js_path = $dist_directory . $type . '/view.js';
300            if ( file_exists( $view_js_path ) ) {
301                register_block_type(
302                    "newspack-blocks/{$type}",
303                    array(
304                        'render_callback' => function( $attributes, $content ) use ( $type ) {
305                            self::enqueue_view_assets( $type );
306                            return $content;
307                        },
308                    )
309                );
310            }
311        }
312    }
313
314    /**
315     * Enqueue block styles stylesheet.
316     */
317    public static function enqueue_block_styles_assets() {
318        $style_path = NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . 'block_styles' . ( is_rtl() ? '.rtl' : '' ) . '.css';
319        if ( file_exists( NEWSPACK_BLOCKS__PLUGIN_DIR . $style_path ) ) {
320            wp_enqueue_style(
321                'newspack-blocks-block-styles-stylesheet',
322                plugins_url( $style_path, NEWSPACK_BLOCKS__PLUGIN_FILE ),
323                array(),
324                NEWSPACK_BLOCKS__VERSION
325            );
326        }
327    }
328
329    /**
330     * Enqueue view scripts and styles for a single block.
331     *
332     * @param string $type The block's type.
333     */
334    public static function enqueue_view_assets( $type ) {
335        $style_path = apply_filters(
336            'newspack_blocks_enqueue_view_assets',
337            NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . $type . '/view' . ( is_rtl() ? '.rtl' : '' ) . '.css',
338            $type,
339            is_rtl()
340        );
341
342        if ( file_exists( NEWSPACK_BLOCKS__PLUGIN_DIR . $style_path ) ) {
343            wp_enqueue_style(
344                "newspack-blocks-{$type}",
345                plugins_url( $style_path, NEWSPACK_BLOCKS__PLUGIN_FILE ),
346                array(),
347                NEWSPACK_BLOCKS__VERSION
348            );
349        }
350        $script_data = static::script_enqueue_helper( NEWSPACK_BLOCKS__BLOCKS_DIRECTORY . $type . '/view.js' );
351        if ( $script_data ) {
352            wp_enqueue_script(
353                "newspack-blocks-{$type}",
354                $script_data['script_path'],
355                $script_data['dependencies'],
356                $script_data['version'],
357                true
358            );
359        }
360    }
361
362    /**
363     * Utility to assemble the class for a server-side rendered block.
364     *
365     * @param string $type The block type.
366     * @param array  $attributes Block attributes.
367     * @param array  $extra Additional classes to be added to the class list.
368     *
369     * @return string Class list separated by spaces.
370     */
371    public static function block_classes( $type, $attributes = array(), $extra = array() ) {
372        $classes = [ "wp-block-newspack-blocks-{$type}" ];
373
374        if ( ! empty( $attributes['align'] ) ) {
375            $classes[] = 'align' . $attributes['align'];
376        }
377        if ( ! empty( $attributes['hideControls'] ) ) {
378            $classes[] = 'hide-controls';
379        }
380        if ( isset( $attributes['className'] ) ) {
381            array_push( $classes, $attributes['className'] );
382        }
383        if ( is_array( $extra ) && ! empty( $extra ) ) {
384            $classes = array_merge( $classes, $extra );
385        }
386
387        return implode( ' ', $classes );
388    }
389
390    /**
391     * Utility to assemble the styles for a server-side rendered block.
392     *
393     * @param array $attributes Block attributes.
394     * @param array $extra      Additional styles to be added to the style list.
395     *
396     * @return string style list.
397     */
398    public static function block_styles( $attributes = [], $extra = [] ) {
399        $styles = [];
400        if ( isset( $attributes['style'] ) && is_array( $attributes['style'] ) ) {
401            $engine_styles = wp_style_engine_get_styles( $attributes['style'], [ 'context' => 'block-supports' ] );
402            if ( isset( $engine_styles['css'] ) ) {
403                $styles[] = $engine_styles['css'];
404            }
405        }
406
407        if ( is_array( $extra ) && ! empty( $extra ) ) {
408            $styles = array_merge( $styles, $extra );
409        }
410
411        return implode( '', $styles );
412    }
413
414    /**
415     * Return the most appropriate thumbnail size to display.
416     *
417     * @param string $orientation The block's orientation settings: landscape|portrait|square.
418     *
419     * @return string Returns the thumbnail key to use.
420     */
421    public static function image_size_for_orientation( $orientation = 'landscape' ) {
422        $sizes = array(
423            'landscape' => array(
424                'large'        => array(
425                    1200,
426                    900,
427                ),
428                'medium'       => array(
429                    800,
430                    600,
431                ),
432                'intermediate' => array(
433                    600,
434                    450,
435                ),
436                'small'        => array(
437                    400,
438                    300,
439                ),
440                'tiny'         => array(
441                    200,
442                    150,
443                ),
444            ),
445            'portrait'  => array(
446                'large'        => array(
447                    900,
448                    1200,
449                ),
450                'medium'       => array(
451                    600,
452                    800,
453                ),
454                'intermediate' => array(
455                    450,
456                    600,
457                ),
458                'small'        => array(
459                    300,
460                    400,
461                ),
462                'tiny'         => array(
463                    150,
464                    200,
465                ),
466            ),
467            'square'    => array(
468                'large'        => array(
469                    1200,
470                    1200,
471                ),
472                'medium'       => array(
473                    800,
474                    800,
475                ),
476                'intermediate' => array(
477                    600,
478                    600,
479                ),
480                'small'        => array(
481                    400,
482                    400,
483                ),
484                'tiny'         => array(
485                    200,
486                    200,
487                ),
488            ),
489        );
490
491        if ( isset( $sizes[ $orientation ] ) ) {
492            foreach ( $sizes[ $orientation ] as $key => $dimensions ) {
493                $attachment = wp_get_attachment_image_src(
494                    get_post_thumbnail_id( get_the_ID() ),
495                    'newspack-article-block-' . $orientation . '-' . $key
496                );
497                if ( ! empty( $attachment ) && $dimensions[0] === $attachment[1] && $dimensions[1] === $attachment[2] ) {
498                    return 'newspack-article-block-' . $orientation . '-' . $key;
499                }
500            }
501        }
502
503        return 'large';
504    }
505
506    /**
507     * Registers image sizes required for Newspack Blocks.
508     */
509    public static function add_image_sizes() {
510        add_image_size( 'newspack-article-block-landscape-large', 1200, 900, true );
511        add_image_size( 'newspack-article-block-portrait-large', 900, 1200, true );
512        add_image_size( 'newspack-article-block-square-large', 1200, 1200, true );
513
514        add_image_size( 'newspack-article-block-landscape-medium', 800, 600, true );
515        add_image_size( 'newspack-article-block-portrait-medium', 600, 800, true );
516        add_image_size( 'newspack-article-block-square-medium', 800, 800, true );
517
518        add_image_size( 'newspack-article-block-landscape-intermediate', 600, 450, true );
519        add_image_size( 'newspack-article-block-portrait-intermediate', 450, 600, true );
520        add_image_size( 'newspack-article-block-square-intermediate', 600, 600, true );
521
522        add_image_size( 'newspack-article-block-landscape-small', 400, 300, true );
523        add_image_size( 'newspack-article-block-portrait-small', 300, 400, true );
524        add_image_size( 'newspack-article-block-square-small', 400, 400, true );
525
526        add_image_size( 'newspack-article-block-landscape-tiny', 200, 150, true );
527        add_image_size( 'newspack-article-block-portrait-tiny', 150, 200, true );
528        add_image_size( 'newspack-article-block-square-tiny', 200, 200, true );
529
530        add_image_size( 'newspack-article-block-uncropped', 1200, 9999, false );
531    }
532
533    /**
534     * Whether the block should be included in the deduplication logic.
535     *
536     * @param array $attributes Block attributes.
537     *
538     * @return bool
539     */
540    public static function should_deduplicate_block( $attributes ) {
541        /**
542         * Filters whether to use deduplication while rendering the given block.
543         *
544         * @param bool   $deduplicate Whether to deduplicate.
545         * @param array  $attributes  The block attributes.
546         */
547        return apply_filters( 'newspack_blocks_should_deduplicate', $attributes['deduplicate'] ?? true, $attributes );
548    }
549
550    /**
551     * Get all "specificPosts" ids from given blocks.
552     *
553     * @param array  $blocks     An array of blocks.
554     * @param string $block_name Name of the block requesting the query.
555     *
556     * @return array All "specificPosts" ids from all eligible blocks.
557     */
558    private static function get_specific_posts_from_blocks( $blocks, $block_name ) {
559        $specific_posts = [];
560        foreach ( $blocks as $block ) {
561            if ( ! empty( $block['innerBlocks'] ) ) {
562                $specific_posts = array_merge(
563                    $specific_posts,
564                    self::get_specific_posts_from_blocks( $block['innerBlocks'], $block_name )
565                );
566                continue;
567            }
568            if (
569                $block_name === $block['blockName'] &&
570                self::should_deduplicate_block( $block['attrs'] ) &&
571                ! empty( $block['attrs']['specificMode'] ) &&
572                ! empty( $block['attrs']['specificPosts'] )
573            ) {
574                $specific_posts = array_merge(
575                    $specific_posts,
576                    $block['attrs']['specificPosts']
577                );
578            }
579        }
580        return $specific_posts;
581    }
582
583    /**
584     * Builds and returns query args based on block attributes.
585     *
586     * @param array $attributes An array of block attributes.
587     * @param array $block_name Name of the block requesting the query.
588     *
589     * @return array
590     */
591    public static function build_articles_query( $attributes, $block_name ) {
592        global $newspack_blocks_post_id;
593        if ( ! $newspack_blocks_post_id ) {
594            $newspack_blocks_post_id = array();
595        }
596
597        // Get all blocks and gather specificPosts ids of all eligible blocks.
598        global $newspack_blocks_all_specific_posts_ids;
599        if ( ! is_array( $newspack_blocks_all_specific_posts_ids ) ) {
600            $blocks                                 = parse_blocks( get_the_content() );
601            $newspack_blocks_all_specific_posts_ids = self::get_specific_posts_from_blocks( $blocks, $block_name );
602        }
603
604        $post_type              = isset( $attributes['postType'] ) ? $attributes['postType'] : [ 'post' ];
605        $included_post_statuses = [ 'publish' ];
606        if ( current_user_can( 'edit_others_posts' ) && isset( $attributes['includedPostStatuses'] ) ) {
607            $included_post_statuses = $attributes['includedPostStatuses'];
608        }
609        $authors                    = isset( $attributes['authors'] ) ? $attributes['authors'] : array();
610        $categories                 = isset( $attributes['categories'] ) ? $attributes['categories'] : array();
611        $include_subcategories      = isset( $attributes['includeSubcategories'] ) ? intval( $attributes['includeSubcategories'] ) : false;
612        $category_join              = isset( $attributes['categoryJoinType'] ) ? $attributes['categoryJoinType'] : 'or';
613        $tags                       = isset( $attributes['tags'] ) ? $attributes['tags'] : array();
614        $custom_taxonomies          = isset( $attributes['customTaxonomies'] ) ? $attributes['customTaxonomies'] : array();
615        $tag_exclusions             = isset( $attributes['tagExclusions'] ) ? $attributes['tagExclusions'] : array();
616        $category_exclusions        = isset( $attributes['categoryExclusions'] ) ? $attributes['categoryExclusions'] : array();
617        $custom_taxonomy_exclusions = isset( $attributes['customTaxonomyExclusions'] ) ? $attributes['customTaxonomyExclusions'] : array();
618        $specific_posts             = isset( $attributes['specificPosts'] ) ? $attributes['specificPosts'] : array();
619        $posts_to_show              = intval( $attributes['postsToShow'] );
620        $specific_mode              = isset( $attributes['specificMode'] ) ? intval( $attributes['specificMode'] ) : false;
621        $args                       = array(
622            'post_type'           => $post_type,
623            'post_status'         => $included_post_statuses,
624            'suppress_filters'    => false,
625            'ignore_sticky_posts' => true,
626            'has_password'        => false,
627            'is_newspack_query'   => true,
628            'tax_query'           => [], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
629        );
630        if ( $specific_mode && $specific_posts ) {
631            $args['posts_per_page'] = count( $specific_posts );
632            $args['post__in']       = $specific_posts;
633            $args['orderby']        = 'post__in';
634        } else {
635            $args['posts_per_page'] = $posts_to_show;
636            if ( ! self::should_deduplicate_block( $attributes ) ) {
637                $args['post__not_in'] = [ get_the_ID() ]; // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
638            } else {
639                if ( count( $newspack_blocks_all_specific_posts_ids ) ) {
640                    $args['post__not_in'] = $newspack_blocks_all_specific_posts_ids; // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
641                }
642                $current_post_id = get_the_ID();
643                $args['post__not_in'] = array_merge( // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
644                    $args['post__not_in'] ?? [],
645                    array_keys( $newspack_blocks_post_id ),
646                    is_singular() && $current_post_id ? [ $current_post_id ] : []
647                );
648            }
649            if ( $categories && count( $categories ) ) {
650                if ( 'or' === $category_join && 1 === $include_subcategories ) {
651                    $children = [];
652                    foreach ( $categories as $parent ) {
653                        $children = array_merge( $children, get_categories( [ 'child_of' => $parent ] ) );
654                        foreach ( $children as $child ) {
655                            $categories[] = $child->term_id;
656                        }
657                    }
658                }
659                if ( 'or' === $category_join ) {
660                    $args['category__in'] = $categories;
661                } else {
662                    $args['category__and'] = $categories;
663                }
664            }
665            if ( $tags && count( $tags ) ) {
666                $args['tag__in'] = $tags;
667            }
668            if ( $tag_exclusions && count( $tag_exclusions ) ) {
669                $args['tag__not_in'] = $tag_exclusions;
670            }
671            if ( $category_exclusions && count( $category_exclusions ) ) {
672                $args['category__not_in'] = $category_exclusions;
673            }
674            if ( ! empty( $custom_taxonomies ) ) {
675                foreach ( $custom_taxonomies as $taxonomy ) {
676                    if ( ! empty( $taxonomy['slug'] ) && ! empty( $taxonomy['terms'] ) ) {
677                        $args['tax_query'][] = [
678                            'taxonomy'         => $taxonomy['slug'],
679                            'field'            => 'term_id',
680                            'terms'            => $taxonomy['terms'],
681                            'include_children' => false,
682                        ];
683                    }
684                }
685            }
686            if ( $custom_taxonomy_exclusions && count( $custom_taxonomy_exclusions ) ) {
687                foreach ( $custom_taxonomy_exclusions as $exclusion ) {
688                    $args['tax_query'][] = [
689                        'field'            => 'term_id',
690                        'include_children' => false,
691                        'operator'         => 'NOT IN',
692                        'taxonomy'         => $exclusion['slug'],
693                        'terms'            => $exclusion['terms'],
694                    ];
695                }
696            }
697
698            if ( $authors && count( $authors ) ) {
699                global $coauthors_plus;
700                $is_co_authors_plus_active = is_object( $coauthors_plus ) && method_exists( $coauthors_plus, 'get_coauthor_by' );
701
702                if ( ! $is_co_authors_plus_active ) {
703                    $args['author__in'] = $authors;
704                } else {
705                    /**
706                     * When CoAuthors Plus is active, we ignore the 'author__in' parameter and search only by the author taxonomy.
707                     *
708                     * If CAP has been activated recently, the author taxonomy may not have been populated yet. You'll need to run
709                     * wp co-authors-plus create-author-terms-for-posts to make sure all posts have the author terms in place.
710                     */
711                    $authors_term_ids = [];
712                    foreach ( $authors as $author_id ) {
713                        $co_author = $coauthors_plus->get_coauthor_by( 'id', $author_id );
714                        if ( is_object( $co_author ) ) {
715                            $term = $coauthors_plus->get_author_term( $co_author );
716                            if ( $term ) {
717                                $authors_term_ids[] = $term->term_id;
718                            } else {
719                                // If the author term does not exist, force a non-match, otherwise all posts will be returned.
720                                // CAP's cli command to create author terms will only create terms for users that have authored posts.
721                                $authors_term_ids[] = -1;
722                            }
723
724                            // If it's a guest author, also check the linked author.
725                            if ( 'guest-author' === $co_author->type && ! empty( $co_author->wp_user ) && $co_author->wp_user instanceof \WP_User ) {
726                                $term = $coauthors_plus->get_author_term( $co_author->wp_user );
727                                if ( $term ) {
728                                    $authors_term_ids[] = $term->term_id;
729                                }
730                            }
731
732                            // If it's a regular wp user, check and include any linked guest authors.
733                            if ( 'wpuser' === $co_author->type ) {
734                                $authors_controller = new WP_REST_Newspack_Authors_Controller();
735                                $linked_guest_author_post = $authors_controller->get_linked_guest_author( $co_author->user_login );
736                                if ( $linked_guest_author_post ) {
737                                    $linked_guest_author_object = $coauthors_plus->get_coauthor_by( 'id', $author_id );
738                                    if ( is_object( $linked_guest_author_object ) ) {
739                                        $term = $coauthors_plus->get_author_term( $linked_guest_author_object );
740                                        if ( $term ) {
741                                            $authors_term_ids[] = $term->term_id;
742                                        }
743                                    }
744                                }
745                            }
746                        }
747                    }
748                    if ( count( $authors_term_ids ) ) {
749                        $args['tax_query'][] = [
750                            'taxonomy' => 'author',
751                            'field'    => 'term_id',
752                            'terms'    => $authors_term_ids,
753                        ];
754                    }
755                }
756            }
757        }
758
759        /**
760         * Customize the WP_Query arguments to fetch post articles before the actual query is executed.
761         *
762         * The filter is called after the build_articles_query() function is called by a Newspack block to
763         * build the WP_Query arguments based on the given attributes and block requesting the query.
764         *
765         * @param array     $args       WP_Query arguments as created by build_articles_query()
766         * @param array     $attributes The attributes initial passed to build_articles_query()
767         * @param string    $block_name The name of the requesting block to create the query args for
768         */
769        return apply_filters( 'newspack_blocks_build_articles_query', $args, $attributes, $block_name );
770    }
771
772    /**
773     * Loads a template with given data in scope.
774     *
775     * @param string $template full Path to the template to be included.
776     * @param array  $data          Data to be passed into the template to be included.
777     * @return string
778     */
779    public static function template_inc( $template, $data = array() ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
780        if ( ! strpos( $template, '.php' ) ) {
781            $template = $template . '.php';
782        }
783        if ( ! is_file( $template ) ) {
784            return '';
785        }
786        ob_start();
787        include $template;
788        $contents = ob_get_contents();
789        ob_end_clean();
790        return $contents;
791    }
792
793    /**
794     * Prepare an array of authors, taking presence of CoAuthors Plus into account.
795     *
796     * @return object[] Array of user objects.
797     */
798    public static function prepare_authors() {
799        $authors = [];
800
801        if ( function_exists( 'get_coauthors' ) ) {
802            $authors = get_coauthors();
803            foreach ( $authors as $author ) {
804                $author->avatar = coauthors_get_avatar( $author, 48 );
805                $author->url    = get_author_posts_url( $author->ID, $author->user_nicename );
806            }
807        }
808
809        if ( empty( $authors ) ) {
810            $id = get_the_author_meta( 'ID' );
811            $authors = array(
812                (object) array(
813                    'ID'            => $id,
814                    'avatar'        => get_avatar( $id, 48 ),
815                    'url'           => get_author_posts_url( $id ),
816                    'user_nicename' => get_the_author(),
817                    'display_name'  => get_the_author_meta( 'display_name' ),
818                ),
819            );
820        }
821
822        /**
823         * Filters the authors array.
824         *
825         * @param object[] $authors Array of user objects.
826         */
827        return apply_filters( 'newspack_blocks_post_authors', $authors );
828    }
829
830    /**
831     * Prepare a list of classes based on assigned tags, categories, post formats and types.
832     *
833     * @param string $post_id Post ID.
834     * @return string CSS classes.
835     */
836    public static function get_term_classes( $post_id ) {
837        $classes = [];
838
839        $tags = get_the_terms( $post_id, 'post_tag' );
840        if ( ! empty( $tags ) ) {
841            foreach ( $tags as $tag ) {
842                if ( ! empty( $tag->slug ) ) {
843                    $classes[] = 'tag-' . $tag->slug;
844                }
845            }
846        }
847
848        $categories = get_the_terms( $post_id, 'category' );
849        if ( ! empty( $categories ) ) {
850            foreach ( $categories as $cat ) {
851                if ( ! empty( $cat->slug ) ) {
852                    $classes[] = 'category-' . $cat->slug;
853                }
854            }
855        }
856
857        foreach ( self::get_custom_taxonomies() as $tax ) {
858            $terms = get_the_terms( $post_id, $tax['slug'] );
859            if ( ! empty( $terms ) ) {
860                foreach ( $terms as $term ) {
861                    if ( ! empty( $term->taxonomy ) && ! empty( $term->slug ) ) {
862                        $classes[] = $term->taxonomy . '-' . $term->slug;
863                    }
864                }
865            }
866        }
867
868        $post_type = get_post_type( $post_id );
869        if ( false !== $post_type ) {
870            $classes[] = 'type-' . $post_type;
871        }
872
873        /**
874         * Filter the array of class names before applying them to the HTML.
875         *
876         * @param array $classes Array of term class names.
877         *
878         * @return array Filtered array of term class names.
879         */
880        $classes = apply_filters( 'newspack_blocks_term_classes', $classes );
881
882        return implode( ' ', $classes );
883    }
884
885    /**
886     * Get patterns for post type.
887     *
888     * @param string $post_type Post type.
889     * @return array Array of patterns.
890     */
891    public static function get_patterns_for_post_type( $post_type = null ) {
892        $patterns    = apply_filters( 'newspack_blocks_patterns', [], $post_type );
893        $categorized = [];
894        $clean       = [];
895        foreach ( $patterns as $pattern ) {
896            if ( ! isset( $pattern['image'] ) || ! $pattern['image'] ) {
897                continue;
898            }
899            $category = isset( $pattern['category'] ) ? $pattern['category'] : __( 'Common', 'jetpack-mu-wpcom' );
900            if ( ! isset( $categorized[ $category ] ) ) {
901                $categorized[ $category ] = [];
902            }
903            $categorized[ $category ][] = $pattern;
904        }
905        $categories = array_keys( $categorized );
906        sort( $categories );
907        foreach ( $categories as $category ) {
908            $clean[] = [
909                'title' => $category,
910                'items' => $categorized[ $category ],
911            ];
912        }
913        return $clean;
914    }
915
916    /**
917     * Function to check if plugin is enabled, and if there are sponsors.
918     *
919     * @see https://github.com/Automattic/newspack-sponsors/blob/8ebf72ec4fe744bca405a1f6fe8cd5bce3a29e6a/includes/newspack-sponsors-theme-helpers.php#L35
920     *
921     * @param int|null    $id    ID of the post or archive term to get sponsors for.
922     *                           If not provided, we will try to guess based on context.
923     * @param string|null $scope Scope of the sponsors to get. Can be 'native' or
924     *                           'underwritten'. If provided, only sponsors with the
925     *                           matching scope will be returned. If not, all sponsors
926     *                           will be returned regardless of scope.
927     * @param string|null $type  Type of the $id given: 'post' or 'archive'. If not
928     *                           provided, we will try to guess based on context.
929     * @param array       $logo_options Optional array of logo options. Valid options:
930     *                                  maxwidth: max width of the logo image, in pixels.
931     *                                  maxheight: max height of the logo image, in pixels.
932     * @return array Array of sponsors.
933     */
934    public static function get_all_sponsors( $id = null, $scope = 'native', $type = 'post', $logo_options = array(
935        'maxwidth'  => 80,
936        'maxheight' => 40,
937    ) ) {
938        if ( function_exists( '\Newspack_Sponsors\get_sponsors_for_post' ) ) {
939            if ( is_singular() ) {
940                $scope_override = get_post_meta( $id, 'newspack_sponsor_sponsorship_scope', true );
941
942                // Scope override: if post is set to display as native-sponsored, return all sponsors.
943                if ( 'native' === $scope_override ) {
944                    $scope = null;
945                }
946
947                // Scope override: if post is set to display as underwritten, return nothing.
948                if ( 'underwritten' === $scope_override ) {
949                    return [];
950                }
951            }
952
953            return \Newspack_Sponsors\get_all_sponsors( $id, $scope, $type, $logo_options ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
954        }
955
956        return false;
957    }
958
959    /**
960     * Function to return sponsor 'flag' from first sponsor.
961     *
962     * @param array  $sponsors Array of sponsors.
963     * @param string $id Post ID.
964     * @return string|boolean Sponsor flag label, or false if none found.
965     */
966    public static function get_sponsor_label( $sponsors = null, $id = null ) {
967        if ( null === $sponsors && ! empty( $id ) ) {
968            $sponsors = self::get_all_sponsors( $id );
969        }
970
971        if ( ! empty( $sponsors ) ) {
972            $sponsor_flag = $sponsors[0]['sponsor_flag'];
973            return $sponsor_flag;
974        }
975
976        return false;
977    }
978
979    /**
980     * Outputs the sponsor byline markup for the theme.
981     *
982     * @param array  $sponsors Array of sponsors.
983     * @param string $id Post ID.
984     * @return array|boolean Array of Sponsor byline information, or false if none found.
985     */
986    public static function get_sponsor_byline( $sponsors = null, $id = null ) {
987        if ( null === $sponsors & ! empty( $id ) ) {
988            $sponsors = self::get_all_sponsors( $id );
989        }
990
991        if ( ! empty( $sponsors ) ) {
992            $sponsor_count = count( $sponsors );
993            $i             = 1;
994            $sponsor_list  = [];
995
996            foreach ( $sponsors as $sponsor ) {
997                $i++;
998                if ( $sponsor_count === $i ) :
999                    /* translators: separates last two sponsor names; needs a space on either side. */
1000                    $sep = esc_html__( ' and ', 'jetpack-mu-wpcom' );
1001                elseif ( $sponsor_count > $i ) :
1002                    /* translators: separates all but the last two sponsor names; needs a space at the end. */
1003                    $sep = esc_html__( ', ', 'jetpack-mu-wpcom' );
1004                else :
1005                    $sep = '';
1006                endif;
1007
1008                $sponsor_list[] = array(
1009                    'byline' => $sponsor['sponsor_byline'],
1010                    'url'    => $sponsor['sponsor_url'],
1011                    'name'   => $sponsor['sponsor_name'],
1012                    'sep'    => $sep,
1013                );
1014            }
1015            return $sponsor_list;
1016        }
1017
1018        return false;
1019    }
1020
1021    /**
1022     * Outputs set of sponsor logos with links.
1023     *
1024     * @param array  $sponsors Array of sponsors.
1025     * @param string $id Post ID.
1026     * @return array Array of sponsor logo images, or false if none found.
1027     */
1028    public static function get_sponsor_logos( $sponsors = null, $id = null ) {
1029        if ( null === $sponsors && ! empty( $id ) ) {
1030            $sponsors = self::get_all_sponsors(
1031                $id,
1032                'native',
1033                'post',
1034                array(
1035                    'maxwidth'  => 80,
1036                    'maxheight' => 40,
1037                )
1038            );
1039        }
1040
1041        if ( ! empty( $sponsors ) ) {
1042            $sponsor_logos = [];
1043            foreach ( $sponsors as $sponsor ) {
1044                if ( ! empty( $sponsor['sponsor_logo'] ) ) :
1045                    $sponsor_logos[] = array(
1046                        'url'    => $sponsor['sponsor_url'],
1047                        'src'    => esc_url( $sponsor['sponsor_logo']['src'] ),
1048                        'alt'    => esc_attr( $sponsor['sponsor_name'] ),
1049                        'width'  => esc_attr( $sponsor['sponsor_logo']['img_width'] ),
1050                        'height' => esc_attr( $sponsor['sponsor_logo']['img_height'] ),
1051                    );
1052                endif;
1053            }
1054
1055            return $sponsor_logos;
1056        }
1057
1058        return false;
1059    }
1060
1061    /**
1062     * If at least one native sponsor is set to display both sponsors and authors, show the authors.
1063     *
1064     * @param array $sponsors Array of sponsors.
1065     *
1066     * @return boolean True if we should display both sponsors and categories, false if we should display only sponsors.
1067     */
1068    public static function newspack_display_sponsors_and_authors( $sponsors ) {
1069        if ( function_exists( '\Newspack_Sponsors\newspack_display_sponsors_and_authors' ) ) {
1070            return \Newspack_Sponsors\newspack_display_sponsors_and_authors( $sponsors );
1071        }
1072        return false;
1073    }
1074
1075    /**
1076     * If at least one native sponsor is set to display both sponsors and categories, show the categories.
1077     *
1078     * @param array $sponsors Array of sponsors.
1079     *
1080     * @return boolean True if we should display both sponsors and categories, false if we should display only sponsors.
1081     */
1082    public static function newspack_display_sponsors_and_categories( $sponsors ) {
1083        if ( function_exists( '\Newspack_Sponsors\newspack_display_sponsors_and_categories' ) ) {
1084            return \Newspack_Sponsors\newspack_display_sponsors_and_categories( $sponsors );
1085        }
1086        return false;
1087    }
1088
1089    /**
1090     * Closure for excerpt filtering that can be added and removed.
1091     *
1092     * @var Closure
1093     */
1094    public static $newspack_blocks_excerpt_closure = null;
1095
1096    /**
1097     * Closure for excerpt length filtering that can be added and removed.
1098     *
1099     * @var Closure
1100     * @deprecated
1101     */
1102    public static $newspack_blocks_excerpt_length_closure = null;
1103
1104    /**
1105     * Function to override WooCommerce Membership's Excerpt Length filter.
1106     *
1107     * @return string Current post's original excerpt.
1108     */
1109    public static function remove_wc_memberships_excerpt_limit() {
1110        $excerpt = get_the_excerpt( get_the_id() );
1111        return $excerpt;
1112    }
1113
1114    /**
1115     * Filter for excerpt length.
1116     *
1117     * @param array $attributes The block's attributes.
1118     */
1119    public static function filter_excerpt( $attributes ) {
1120        if ( empty( $attributes['excerptLength'] ) || ! $attributes['showExcerpt'] ) {
1121            return;
1122        }
1123
1124        self::$newspack_blocks_excerpt_closure = function( $text = '', $post = null ) use ( $attributes ) {
1125            // If we have a manually entered excerpt, use that and allow some tags.
1126            if ( ! empty( $post->post_excerpt ) ) {
1127                $excerpt      = $post->post_excerpt;
1128                $allowed_tags = '<em>,<i>,<strong>,<b>,<u>,<ul>,<ol>,<li>,<h1>,<h2>,<h3>,<h4>,<h5>,<h6>,<img>,<a>,<p>';
1129            } else {
1130                // If we don't, built an excerpt but allow no tags.
1131                $excerpt      = $post->post_content;
1132                $allowed_tags = '';
1133            }
1134
1135            // Recreate logic from wp_trim_excerpt (https://developer.wordpress.org/reference/functions/wp_trim_excerpt/).
1136            $excerpt = strip_shortcodes( $excerpt );
1137            $excerpt = excerpt_remove_blocks( $excerpt );
1138            $excerpt = wpautop( $excerpt );
1139            $excerpt = str_replace( ']]>', ']]&gt;', $excerpt );
1140
1141            // Strip HTML tags except for the explicitly allowed tags.
1142            $excerpt = strip_tags( $excerpt, $allowed_tags ); // phpcs:ignore WordPressVIPMinimum.Functions.StripTags.StripTagsTwoParameters
1143
1144            // Get excerpt length. If not provided a valid length, use the default excerpt length.
1145            if ( empty( $attributes['excerptLength'] ) || ! is_numeric( $attributes['excerptLength'] ) ) {
1146                $excerpt_length = 55;
1147            } else {
1148                $excerpt_length = $attributes['excerptLength'];
1149            }
1150
1151            // Set excerpt length (https://core.trac.wordpress.org/ticket/29533#comment:3).
1152            $excerpt = force_balance_tags( html_entity_decode( wp_trim_words( htmlentities( $excerpt, ENT_COMPAT ), $excerpt_length, static::more_excerpt(), ENT_COMPAT ) ) );
1153
1154            return $excerpt;
1155        };
1156        add_filter( 'get_the_excerpt', self::$newspack_blocks_excerpt_closure, 11, 2 );
1157    }
1158
1159    /**
1160     * Remove excerpt filter after Homepage Posts block loop.
1161     */
1162    public static function remove_excerpt_filter() {
1163        if ( static::$newspack_blocks_excerpt_closure ) {
1164            remove_filter( 'get_the_excerpt', static::$newspack_blocks_excerpt_closure, 11 );
1165        }
1166    }
1167
1168    /**
1169     * Filter for excerpt length.
1170     *
1171     * @deprecated
1172     * @param array $attributes The block's attributes.
1173     */
1174    public static function filter_excerpt_length( $attributes ) {
1175        // If showing excerpt, filter the length using the block attribute.
1176        if ( isset( $attributes['excerptLength'] ) && $attributes['showExcerpt'] ) {
1177            self::$newspack_blocks_excerpt_length_closure = add_filter(
1178                'excerpt_length',
1179                function() use ( $attributes ) {
1180                    if ( $attributes['excerptLength'] ) {
1181                        return $attributes['excerptLength'];
1182                    }
1183                    return 55;
1184                },
1185                999
1186            );
1187            add_filter( 'wc_memberships_trimmed_restricted_excerpt', [ 'Newspack_Blocks', 'remove_wc_memberships_excerpt_limit' ], 999 );
1188        }
1189    }
1190
1191    /**
1192     * Remove excerpt length filter after Homepage Posts block loop.
1193     *
1194     * @deprecated
1195     */
1196    public static function remove_excerpt_length_filter() {
1197        if ( self::$newspack_blocks_excerpt_length_closure ) {
1198            remove_filter(
1199                'excerpt_length',
1200                self::$newspack_blocks_excerpt_length_closure,
1201                999
1202            );
1203            remove_filter( 'wc_memberships_trimmed_restricted_excerpt', [ 'Newspack_Blocks', 'remove_wc_memberships_excerpt_limit' ] );
1204        }
1205    }
1206
1207    /**
1208     * Return a excerpt more replacement when using the 'Read More' link.
1209     */
1210    public static function more_excerpt() {
1211        return '…';
1212    }
1213
1214    /**
1215     * Filter for excerpt ellipsis.
1216     *
1217     * @deprecated
1218     * @param array $attributes The block's attributes.
1219     */
1220    public static function filter_excerpt_more( $attributes ) {
1221        // If showing the 'Read More' link, modify the ellipsis.
1222        if ( $attributes['showReadMore'] ) {
1223            add_filter( 'excerpt_more', [ __CLASS__, 'more_excerpt' ], 999 );
1224        }
1225    }
1226
1227    /**
1228     * Remove excerpt ellipsis filter after Homepage Posts block loop.
1229     *
1230     * @deprecated
1231     */
1232    public static function remove_excerpt_more_filter() {
1233        remove_filter( 'excerpt_more', [ __CLASS__, 'more_excerpt' ], 999 );
1234    }
1235
1236    /**
1237     * Utility to get the link for the given post ID. If the post has an external URL meta value, use that.
1238     * Otherwise, use the permalink. But if the post type doesn't have a public singular view, don't link.
1239     *
1240     * @param int $post_id Post ID for which to get the link. Will default to current post if none given.
1241     * @return string|boolean The URL for the post, or false if it can't be linked to.
1242     */
1243    public static function get_post_link( $post_id = null ) {
1244        if ( null === $post_id ) {
1245            $post_id = get_the_ID();
1246        }
1247
1248        $post_type        = get_post_type( $post_id );
1249        $sponsor_url      = get_post_meta( $post_id, 'newspack_sponsor_url', true );
1250        $supporter_url    = get_post_meta( $post_id, 'newspack_supporter_url', true );
1251        $external_url     = ! empty( $sponsor_url ) ? $sponsor_url : $supporter_url;
1252        $post_type_info   = get_post_type_object( $post_type );
1253        $link             = ! empty( $external_url ) ? $external_url : get_permalink();
1254        $should_have_link = ! empty( $post_type_info->public ) || ! empty( $external_url ); // False if a sponsor or supporter without an external URL.
1255
1256        return $should_have_link ? $link : false;
1257    }
1258
1259    /**
1260     * Sanitize SVG markup for front-end display.
1261     *
1262     * @param string $svg SVG markup to sanitize.
1263     * @return string Sanitized markup.
1264     */
1265    public static function sanitize_svg( $svg = '' ) {
1266        $allowed_html = [
1267            'svg'  => [
1268                'xmlns'       => [],
1269                'fill'        => [],
1270                'viewbox'     => [],
1271                'role'        => [],
1272                'aria-hidden' => [],
1273                'focusable'   => [],
1274                'height'      => [],
1275                'width'       => [],
1276            ],
1277            'path' => [
1278                'd'    => [],
1279                'fill' => [],
1280            ],
1281        ];
1282
1283        return wp_kses( $svg, $allowed_html );
1284    }
1285
1286    /**
1287     * Disable Jetpack's donate block when using Newspack donations.
1288     */
1289    public static function disable_jetpack_donate() {
1290        // Do nothing if Jetpack's blocks or Newspack aren't being used.
1291        if ( ! class_exists( 'Jetpack_Gutenberg' ) || ! class_exists( 'Newspack' ) ) {
1292            return;
1293        }
1294
1295        // Allow Jetpack donations if Newspack donations isn't set up.
1296        $donate_settings = Newspack\Donations::get_donation_settings();
1297        if ( is_wp_error( $donate_settings ) ) {
1298            return;
1299        }
1300
1301        // Tell Jetpack to mark the donations feature as unavailable.
1302        Jetpack_Gutenberg::set_extension_unavailable(
1303            'jetpack/donations',
1304            esc_html__( 'Jetpack donations is disabled in favour of Newspack donations.', 'jetpack-mu-wpcom' )
1305        );
1306    }
1307
1308    /**
1309     * Loads a template with given data in scope.
1310     *
1311     * @param string $template Name of the template to be included.
1312     * @param array  $data     Data to be passed into the template to be included.
1313     * @param string $path     (Optional) Path to the folder containing the template.
1314     * @return string
1315     */
1316    public static function template_include( $template, $data = [], $path = NEWSPACK_BLOCKS__PLUGIN_DIR . 'src/templates/' ) {
1317        if ( ! strpos( $template, '.php' ) ) {
1318            $template = $template . '.php';
1319        }
1320        $path .= $template;
1321        if ( ! is_file( $path ) ) {
1322            return '';
1323        }
1324        ob_start();
1325        include $path;
1326        $contents = ob_get_contents();
1327        ob_end_clean();
1328        return $contents;
1329    }
1330
1331    /**
1332     * Get post status label.
1333     */
1334    public static function get_post_status_label() {
1335        $post_status          = get_post_status();
1336        $post_statuses_labels = [
1337            'draft'  => __( 'Draft', 'jetpack-mu-wpcom' ),
1338            'future' => __( 'Scheduled', 'jetpack-mu-wpcom' ),
1339        ];
1340        if ( 'publish' !== $post_status ) {
1341            ob_start();
1342            ?>
1343                <div class="newspack-preview-label"><?php echo esc_html( $post_statuses_labels[ $post_status ] ); ?></div>
1344            <?php
1345            return ob_get_clean();
1346        }
1347    }
1348
1349    /**
1350     * Pick either white or black, whatever has sufficient contrast with the color being passed to it.
1351     * From Newspack Theme functions.
1352     *
1353     * @param  string $hex Hexidecimal value of the color to adjust.
1354     * @return string Either black or white hexidecimal values.
1355     *
1356     * @ref https://stackoverflow.com/questions/1331591/given-a-background-color-black-or-white-text
1357     */
1358    public static function get_color_for_contrast( $hex ) {
1359        // Hex RGB.
1360        $r1 = hexdec( substr( $hex, 1, 2 ) );
1361        $g1 = hexdec( substr( $hex, 3, 2 ) );
1362        $b1 = hexdec( substr( $hex, 5, 2 ) );
1363        // Black RGB.
1364        $black_color    = '#000';
1365        $r2_black_color = hexdec( substr( $black_color, 1, 2 ) );
1366        $g2_black_color = hexdec( substr( $black_color, 3, 2 ) );
1367        $b2_black_color = hexdec( substr( $black_color, 5, 2 ) );
1368        // Calc contrast ratio.
1369        $l1             = 0.2126 * pow( $r1 / 255, 2.2 ) +
1370        0.7152 * pow( $g1 / 255, 2.2 ) +
1371        0.0722 * pow( $b1 / 255, 2.2 );
1372        $l2             = 0.2126 * pow( $r2_black_color / 255, 2.2 ) +
1373        0.7152 * pow( $g2_black_color / 255, 2.2 ) +
1374        0.0722 * pow( $b2_black_color / 255, 2.2 );
1375        $contrast_ratio = 0;
1376        if ( $l1 > $l2 ) {
1377            $contrast_ratio = (int) ( ( $l1 + 0.05 ) / ( $l2 + 0.05 ) );
1378        } else {
1379            $contrast_ratio = (int) ( ( $l2 + 0.05 ) / ( $l1 + 0.05 ) );
1380        }
1381        if ( $contrast_ratio > 5 ) {
1382            // If contrast is more than 5, return black color.
1383            return 'black';
1384        } else {
1385            // if not, return white color.
1386            return 'white';
1387        }
1388    }
1389
1390    /**
1391     * Get an array of allowed HTML attributes for sanitizing image markup.
1392     * For use with wp_kses: https://developer.wordpress.org/reference/functions/wp_kses/
1393     *
1394     * @return array
1395     */
1396    public static function get_sanitized_image_attributes() {
1397        return [
1398            'img'      => [
1399                'alt'      => true,
1400                'class'    => true,
1401                'data-*'   => true,
1402                'decoding' => true,
1403                'height'   => true,
1404                'loading'  => true,
1405                'sizes'    => true,
1406                'src'      => true,
1407                'srcset'   => true,
1408                'width'    => true,
1409            ],
1410            'noscript' => [],
1411            'a'        => [
1412                'href' => true,
1413            ],
1414        ];
1415    }
1416
1417    /**
1418     * Get post date to be displayed.
1419     *
1420     * @param WP_Post $post Post object.
1421     * @return string Date string.
1422     */
1423    public static function get_displayed_post_date( $post = null ) {
1424        if ( $post === null ) {
1425            $post = get_post();
1426        }
1427        return apply_filters( 'newspack_blocks_displayed_post_date', mysql_to_rfc3339( $post->post_date ), $post );
1428    }
1429
1430    /**
1431     * Get post date in ISO-8601 format to be used in the datetime attribute.
1432     *
1433     * @param WP_Post $post Post object.
1434     * @return string Date string in ISO-8601 format.
1435     */
1436    public static function get_datetime_post_date( $post = null ) {
1437        if ( $post === null ) {
1438            $post = get_post();
1439        }
1440        /**
1441         * Filters the post date used for the datetime attribute.
1442         *
1443         * @param string Date string in a format appropriate for datetime attributes.
1444         */
1445        return apply_filters( 'newspack_blocks_displayed_post_date', get_post_datetime( $post )->format( 'c' ), $post );
1446    }
1447
1448    /**
1449     * Get post date to be displayed, formatted.
1450     *
1451     * @param WP_Post $post Post object.
1452     * @return string Formatted date.
1453     */
1454    public static function get_formatted_displayed_post_date( $post = null ) {
1455        if ( $post === null ) {
1456            $post = get_post();
1457        }
1458        $date           = self::get_displayed_post_date( $post );
1459        $date           = new DateTime( $date );
1460        $date_format    = get_option( 'date_format' );
1461        $date_formatted = date_i18n( $date_format, $date->getTimestamp() );
1462        return apply_filters( 'newspack_blocks_formatted_displayed_post_date', $date_formatted, $post );
1463    }
1464
1465    /**
1466     * Get article meta footer.
1467     *
1468     * @param WP_Post $post Post object.
1469     */
1470    public static function get_article_meta_footer( $post = null ) {
1471        if ( $post === null ) {
1472            $post = get_post();
1473        }
1474        $meta_footer = apply_filters( 'newspack_blocks_article_meta_footer', '', $post );
1475        if ( strlen( $meta_footer ) > 0 ) {
1476            return '<span style="margin: 0 6px;" class="newspack_blocks__article-meta-footer__separator">|</span>' . $meta_footer;
1477        }
1478    }
1479
1480    /**
1481     * Get a formatted HTML string containing amount and frequency of a donation.
1482     *
1483     * @param float  $amount Amount.
1484     * @param string $frequency Frequency.
1485     * @param bool   $hide_once_label Whether to hide the "once" label.
1486     *
1487     * @return string
1488     */
1489    public static function get_formatted_amount( $amount = null, $frequency = null, $hide_once_label = false ) {
1490        if ( ! function_exists( 'wc_price' ) || ( method_exists( 'Newspack\Donations', 'is_platform_wc' ) && ! \Newspack\Donations::is_platform_wc() ) ) {
1491            if ( empty( $amount ) ) {
1492                return false;
1493            }
1494
1495            // Translators: %s is the %s is the frequency.
1496            $frequency_string = 'once' === $frequency ? $frequency : sprintf( __( 'per %s', 'jetpack-mu-wpcom' ), $frequency );
1497            $formatter        = new NumberFormatter( \get_locale(), NumberFormatter::CURRENCY );
1498            $formatted_price  = '<span class="price-amount">' . $formatter->formatCurrency( $amount, 'USD' ) . '</span> <span class="tier-frequency">' . $frequency_string . '</span>';
1499            return str_replace( '.00', '', $formatted_price );
1500        }
1501
1502        $wc_formatted_amount = '';
1503        if ( null === $amount && null === $frequency ) {
1504            $currency_symbol     = function_exists( 'get_woocommerce_currency_symbol' ) ? \get_woocommerce_currency_symbol() : '&#36;';
1505            $wc_formatted_amount = '<span class="woocommerce-Price-amount amount"><bdi><span class="woocommerce-Price-currencySymbol">' . $currency_symbol . '</span>AMOUNT_PLACEHOLDER</bdi></span> FREQUENCY_PLACEHOLDER';
1506        } else {
1507            // If it's a float but with no decimal value, treat it as an int.
1508            if ( is_float( $amount ) && floor( $amount ) == $amount ) {
1509                $amount = (int) $amount;
1510            }
1511            // Format the amount with currency symbol and separators.
1512            $amount_string = \wc_price(
1513                $amount,
1514                [ 'decimals' => is_int( $amount ) ? 0 : 2 ]
1515            );
1516
1517            if ( ! function_exists( 'wcs_price_string' ) ) {
1518                return $amount_string;
1519            }
1520            $price_args          = [
1521                'recurring_amount'    => $amount_string,
1522                'subscription_period' => 'once' === $frequency ? 'day' : $frequency,
1523            ];
1524            $wc_formatted_amount = \wcs_price_string( $price_args );
1525
1526            if ( 'once' === $frequency ) {
1527                $once_label          = $hide_once_label ? '' : __( ' once', 'jetpack-mu-wpcom' );
1528                $wc_formatted_amount = preg_replace( '/ \/ ?.*/', $once_label, $wc_formatted_amount );
1529            }
1530            $wc_formatted_amount = str_replace( ' / ', __( ' per ', 'jetpack-mu-wpcom' ), $wc_formatted_amount );
1531        }
1532
1533        return '<span class="wpbnbd__tiers__amount__value">' . $wc_formatted_amount . '</span>';
1534    }
1535
1536    /**
1537     * Get an image caption, optionally with credit appended.
1538     *
1539     * @param int  $attachment_id Attachment ID of the image.
1540     * @param bool $include_caption Whether to include the caption.
1541     * @param bool $include_credit Whether to include the credit.
1542     *
1543     * @return string
1544     */
1545    public static function get_image_caption( $attachment_id = null, $include_caption = true, $include_credit = false ) {
1546        if ( ! $attachment_id || ( ! $include_caption && ! $include_credit ) ) {
1547            return '';
1548        }
1549
1550        $caption = $include_caption ? wp_get_attachment_caption( $attachment_id ) : '';
1551        $credit  = '';
1552
1553        if ( $include_credit && method_exists( 'Newspack\Newspack_Image_Credits', 'get_media_credit_string' ) ) {
1554            $credit = \Newspack\Newspack_Image_Credits::get_media_credit_string( $attachment_id );
1555        }
1556
1557        $full_caption = trim( $caption . ' ' . $credit );
1558        if ( empty( $full_caption ) ) {
1559            return '';
1560        }
1561
1562        $combined_caption = sprintf(
1563            '<figcaption%1$s>%2$s</figcaption>',
1564            ! empty( $credit ) ? ' class="has-credit"' : '',
1565            $full_caption
1566        );
1567
1568        return $combined_caption;
1569    }
1570}
1571Newspack_Blocks::init();