Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 322
0.00% covered (danger)
0.00%
0 / 11
CRAP
n/a
0 / 0
newspack_blocks_hpb_maximum_image_width
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
newspack_blocks_filter_hpb_sizes
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
newspack_blocks_retrieve_homepage_articles_blocks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
newspack_blocks_collect_all_attribute_values
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
newspack_blocks_get_homepage_articles_css_string
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
110
newspack_blocks_enqueue_block_homepage_articles_styles
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
newspack_blocks_render_block_homepage_articles
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 1
1122
newspack_blocks_register_homepage_articles
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
newspack_blocks_format_avatars
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
newspack_blocks_format_byline
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
newspack_blocks_format_categories
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Server-side rendering of the `newspack-blocks/homepage-posts` block.
4 *
5 * @package WordPress
6 */
7
8/**
9 * Calculate the maximum width of an image in the Homepage Posts block.
10 */
11function newspack_blocks_hpb_maximum_image_width() {
12    $max_width = 0;
13
14    global $newspack_blocks_hpb_rendering_context;
15    if ( isset( $newspack_blocks_hpb_rendering_context['attrs'] ) ) {
16        $attributes = $newspack_blocks_hpb_rendering_context['attrs'];
17        if ( empty( $attributes ) ) {
18            return $max_width;
19        }
20        if ( isset( $attributes['align'] ) && in_array( $attributes['align'], [ 'full', 'wide' ], true ) ) {
21            // For full and wide alignments, the image width is more than 100% of the content width
22            // and depends on site width. Can't make assumptions about the site width.
23            return $max_width;
24        }
25        $site_content_width  = 1200;
26        $is_image_half_width = in_array( $attributes['mediaPosition'], [ 'left', 'right' ], true );
27        if ( 'grid' === $attributes['postLayout'] ) {
28            $columns = absint( $attributes['columns'] );
29            if ( $is_image_half_width ) {
30                // If the media position is on left or right, the image is 50% of the column width.
31                $columns = $columns * 2;
32            }
33            return $site_content_width / $columns;
34        } elseif ( 'list' === $attributes['postLayout'] && $is_image_half_width ) {
35            return $site_content_width / 2;
36        }
37    }
38    return $max_width;
39}
40
41/**
42 * Set image `sizes` attribute based on the maximum image width.
43 *
44 * @param array $sizes Sizes for the sizes attribute.
45 */
46function newspack_blocks_filter_hpb_sizes( $sizes ) {
47    /**
48     * Disables automatic image sizes optimization for Homepage Posts Block.
49     * By default, Newspack optimizes the 'sizes' attribute based on the
50     * computed maximum image width. Set to true to use default WordPress behavior.
51     *
52     * @constant NEWSPACK_DISABLE_HPB_IMAGE_OPTIMISATION
53     * @type     bool
54     * @default  Image optimization enabled
55     * @status   draft
56     *
57     * @example define( 'NEWSPACK_DISABLE_HPB_IMAGE_OPTIMISATION', true );
58     */
59    if ( defined( 'NEWSPACK_DISABLE_HPB_IMAGE_OPTIMISATION' ) && NEWSPACK_DISABLE_HPB_IMAGE_OPTIMISATION ) {
60        return $sizes;
61    }
62    global $newspack_blocks_hpb_current_theme;
63    if ( ! $newspack_blocks_hpb_current_theme ) {
64        $newspack_blocks_hpb_current_theme = wp_get_theme()->template;
65    }
66    if ( stripos( $newspack_blocks_hpb_current_theme, 'newspack' ) === false ) {
67        // Bail if not using a Newspack theme – assumptions about the site content width can't be made then.
68        return $sizes;
69    }
70    $max_width = newspack_blocks_hpb_maximum_image_width();
71    if ( 0 !== $max_width ) {
72        // >=782px is the desktop size – set width as computed.
73        $sizes = '(min-width: 782px) ' . $max_width . 'px';
74        // Between 600-782px is the tablet size – all columns will collapse to two-column layout
75        // (assuming 5% padding on each side and between columns).
76        $sizes .= ', (min-width: 600px) 42.5vw';
77        // <=600px is the mobile size – columns will stack to full width
78        // (assumming 5% side padding on each side).
79        $sizes .= ', 90vw';
80    }
81    return $sizes;
82}
83
84/**
85 * Retrieve Homepage Articles blocks from blocks, recursively.
86 *
87 * @param array  $blocks The blocks to search.
88 * @param string $block_name The block name to search for.
89 */
90function newspack_blocks_retrieve_homepage_articles_blocks( $blocks, $block_name ) {
91    $ha_blocks = [];
92    foreach ( $blocks as $block ) {
93        if ( $block_name === $block['blockName'] ) {
94            $ha_blocks = array_merge( $ha_blocks, [ $block ] );
95        }
96        if ( is_array( $block['innerBlocks'] ) ) {
97            $ha_blocks = array_merge( $ha_blocks, newspack_blocks_retrieve_homepage_articles_blocks( $block['innerBlocks'], $block_name ) );
98        }
99    }
100    return $ha_blocks;
101}
102
103/**
104 * Collect all attributes' values used in a set of blocks.
105 *
106 * @param array $blocks The blocks to search.
107 */
108function newspack_blocks_collect_all_attribute_values( $blocks ) {
109    $result = [];
110
111    foreach ( $blocks as $block ) {
112        foreach ( $block as $key => $value ) {
113            if ( ! isset( $result[ $key ] ) ) {
114                $result[ $key ] = [];
115            }
116            if ( ! in_array( $value, $result[ $key ], true ) ) {
117                $result[ $key ][] = $value;
118            }
119        }
120    }
121
122    return $result;
123}
124
125/**
126 * Output a CSS string based on attributes used in a set of blocks.
127 * This is to mitigate CLS. Any CSS that might cause CLS should be output here,
128 * inline and before the blocks are printed.
129 *
130 * @param array $attrs The attributes used in the blocks.
131 */
132function newspack_blocks_get_homepage_articles_css_string( $attrs ) {
133    $entry_title_type_scale = [
134        '0.7em',
135        '0.9em',
136        '1em',
137        '1.2em',
138        '1.4em',
139        '1.7em',
140        '2em',
141        '2.2em',
142        '2.4em',
143        '2.6em',
144    ];
145
146    ob_start();
147    ?>
148        .wp-block-newspack-blocks-homepage-articles article .entry-title {
149            font-size: 1.2em;
150        }
151        .wp-block-newspack-blocks-homepage-articles .entry-meta {
152            display: flex;
153            flex-wrap: wrap;
154            align-items: center;
155            margin-top: 0.5em;
156        }
157        .wp-block-newspack-blocks-homepage-articles article .entry-meta {
158            font-size: 0.8em;
159        }
160        .wp-block-newspack-blocks-homepage-articles article .avatar {
161            height: 25px;
162            width: 25px;
163        }
164        .wp-block-newspack-blocks-homepage-articles .post-thumbnail{
165            margin: 0;
166            margin-bottom: 0.25em;
167        }
168        .wp-block-newspack-blocks-homepage-articles .post-thumbnail img {
169            height: auto;
170            width: 100%;
171        }
172        .wp-block-newspack-blocks-homepage-articles .post-thumbnail figcaption {
173            margin-bottom: 0.5em;
174        }
175        .wp-block-newspack-blocks-homepage-articles p {
176            margin: 0.5em 0;
177        }
178
179        <?php
180        if ( isset( $attrs['typeScale'] ) ) {
181            foreach ( $attrs['typeScale'] as $scale ) {
182                echo esc_html(
183                    ".wpnbha.ts-$scale .entry-title{font-size: {$entry_title_type_scale[$scale - 1]}}"
184                );
185                if ( in_array( $scale, [ 8, 9, 10 ], true ) ) {
186                    echo esc_html(
187                        ".wpnbha.ts-$scale .entry-title {line-height: 1.1;}"
188                    );
189                }
190                if ( in_array( $scale, [ 7, 8, 9, 10 ], true ) ) {
191                    echo esc_html(
192                        ".wpnbha.ts-$scale .newspack-post-subtitle {font-size: 1.4em;}"
193                    );
194                }
195                if ( in_array( $scale, [ 6 ], true ) ) {
196                    echo esc_html(
197                        ".wpnbha.ts-$scale article .newspack-post-subtitle {font-size: 1.4em;}"
198                    );
199                }
200                if ( in_array( $scale, [ 5 ], true ) ) {
201                    echo esc_html(
202                        ".wpnbha.ts-$scale article .newspack-post-subtitle {font-size: 1.2em;}"
203                    );
204                }
205                if ( in_array( $scale, [ 1, 2, 3 ], true ) ) {
206                    echo esc_html(
207                        ".wpnbha.ts-$scale article .newspack-post-subtitle, .wpnbha.ts-$scale article .entry-wrapper p, .wpnbha.ts-$scale article .entry-wrapper .more-link, .wpnbha.ts-$scale article .entry-meta {font-size: 0.8em;}"
208                    );
209                }
210            }
211        }
212        if ( isset( $attrs['showSubtitle'] ) && in_array( 1, $attrs['showSubtitle'], false ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.FoundNonStrictFalse
213            echo esc_html(
214                '.newspack-post-subtitle--in-homepage-block {
215                    margin-top: 0.3em;
216                    margin-bottom: 0;
217                    line-height: 1.4;
218                    font-style: italic;
219                }'
220            );
221        }
222        ?>
223    <?php
224    return ob_get_clean();
225}
226
227/**
228 * Gather all Homepage Articles blocks on the page and output only the needed CSS.
229 */
230function newspack_blocks_enqueue_block_homepage_articles_styles() {
231    global $newspack_blocks_hpb_all_blocks;
232    if ( ! is_array( $newspack_blocks_hpb_all_blocks ) ) {
233        $block_name = apply_filters( 'newspack_blocks_block_name', 'newspack-blocks/homepage-articles' );
234        $newspack_blocks_hpb_all_blocks = newspack_blocks_retrieve_homepage_articles_blocks(
235            parse_blocks( get_the_content() ),
236            $block_name
237        );
238        $all_used_attrs = newspack_blocks_collect_all_attribute_values( array_column( $newspack_blocks_hpb_all_blocks, 'attrs' ) );
239        $css_string = newspack_blocks_get_homepage_articles_css_string( $all_used_attrs );
240        wp_register_style( 'newspack-blocks-homepage-articles-inline', false, [], \NEWSPACK_BLOCKS__VERSION );
241        wp_enqueue_style( 'newspack-blocks-homepage-articles-inline' );
242        wp_add_inline_style( 'newspack-blocks-homepage-articles-inline', $css_string );
243    }
244}
245
246/**
247 * Renders the `newspack-blocks/homepage-posts` block on server.
248 *
249 * @param array $attributes The block attributes.
250 *
251 * @return string Returns the post content with latest posts added.
252 */
253function newspack_blocks_render_block_homepage_articles( $attributes ) {
254    // Don't output the block inside RSS feeds.
255    if ( is_feed() ) {
256        return;
257    }
258
259    $block_name = apply_filters( 'newspack_blocks_block_name', 'newspack-blocks/homepage-articles' );
260    $article_query = new WP_Query( Newspack_Blocks::build_articles_query( $attributes, $block_name ) );
261    if ( ! $article_query->have_posts() ) {
262        return;
263    }
264
265    // If the block is rendered in wp_kses_post() call, ensure the `time` tag is allowed.
266    \add_filter(
267        'wp_kses_allowed_html',
268        function( $tags ) {
269            $tags['time'] = [
270                'class'    => true,
271                'datetime' => true,
272            ];
273            return $tags;
274        }
275    );
276
277    newspack_blocks_enqueue_block_homepage_articles_styles();
278
279    // This will let the FSE plugin know we need CSS/JS now.
280    do_action( 'newspack_blocks_render_homepage_articles' );
281
282    $classes = Newspack_Blocks::block_classes( 'homepage-articles', $attributes, [ 'wpnbha' ] );
283
284    if ( isset( $attributes['postLayout'] ) && 'grid' === $attributes['postLayout'] ) {
285        $classes .= ' is-grid';
286    }
287    if ( isset( $attributes['columns'] ) && 'grid' === $attributes['postLayout'] ) {
288        $classes .= ' columns-' . $attributes['columns'] . ' colgap-' . $attributes['colGap'];
289    }
290    if ( $attributes['showImage'] ) {
291        $classes .= ' show-image';
292    }
293    if ( $attributes['showImage'] && isset( $attributes['mediaPosition'] ) ) {
294        $classes .= ' image-align' . $attributes['mediaPosition'];
295    }
296    if ( isset( $attributes['typeScale'] ) ) {
297        $classes .= ' ts-' . $attributes['typeScale'];
298    }
299    if ( $attributes['showImage'] && isset( $attributes['imageScale'] ) ) {
300        $classes .= ' is-' . $attributes['imageScale'];
301    }
302    if ( $attributes['showImage'] ) {
303        $classes .= ' is-' . $attributes['imageShape'];
304    }
305    if ( $attributes['showImage'] && $attributes['mobileStack'] ) {
306        $classes .= ' mobile-stack';
307    }
308    if ( $attributes['showCaption'] ) {
309        $classes .= ' show-caption';
310    }
311    if ( $attributes['showCredit'] ) {
312        $classes .= ' show-credit';
313    }
314    if ( $attributes['showCategory'] ) {
315        $classes .= ' show-category';
316    }
317    if ( isset( $attributes['className'] ) ) {
318        $classes .= ' ' . $attributes['className'];
319    }
320    if ( $attributes['textAlign'] ) {
321        $classes .= ' has-text-align-' . $attributes['textAlign'];
322    }
323
324    if ( '' !== $attributes['textColor'] || '' !== $attributes['customTextColor'] ) {
325        $classes .= ' has-text-color';
326    }
327    if ( '' !== $attributes['textColor'] ) {
328        $classes .= ' has-' . $attributes['textColor'] . '-color';
329    }
330
331    $styles = '';
332
333    if ( '' !== $attributes['customTextColor'] ) {
334        $styles = 'color: ' . $attributes['customTextColor'] . ';';
335    }
336
337    $articles_rest_url = add_query_arg(
338        array_merge(
339            map_deep(
340                $attributes,
341                function( $attribute ) {
342                    return false === $attribute ? '0' : rawurlencode( $attribute );
343                }
344            ),
345            [
346                'page' => 2,
347            ]
348        ),
349        rest_url( '/newspack-blocks/v1/articles' )
350    );
351
352    $page = $article_query->paged ?? 1;
353
354    $has_more_pages = ( ++$page ) <= $article_query->max_num_pages;
355
356    /**
357     * Hide the "More" button on private sites.
358     *
359     * Client-side fetching from a private WP.com blog requires authentication,
360     * which is not provided in the current implementation.
361     * See https://github.com/Automattic/newspack-blocks/issues/306.
362     */
363    $is_blog_private = (int) get_option( 'blog_public' ) === -1;
364
365    $has_more_button = ! $is_blog_private && $has_more_pages && (bool) $attributes['moreButton'];
366
367    if ( $has_more_button ) {
368        $classes .= ' has-more-button';
369    }
370
371    ob_start();
372
373    ?>
374    <div
375        class="<?php echo esc_attr( $classes ); ?>"
376        style="<?php echo esc_attr( $styles ); ?>"
377        >
378        <div data-posts data-current-post-id="<?php the_ID(); ?>">
379            <?php if ( '' !== $attributes['sectionHeader'] ) : ?>
380                <h2 class="article-section-title">
381                    <span><?php echo wp_kses_post( $attributes['sectionHeader'] ); ?></span>
382                </h2>
383            <?php endif; ?>
384            <?php
385            echo Newspack_Blocks::template_inc( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
386                __DIR__ . '/templates/articles-list.php',
387                [
388                    'articles_rest_url' => $articles_rest_url, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
389                    'article_query'     => $article_query, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
390                    'attributes'        => $attributes, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
391                ]
392            );
393            ?>
394        </div>
395        <?php
396
397        if ( $has_more_button ) :
398            $load_more = '';
399            if ( (bool) $attributes['infiniteScroll'] ) {
400                $load_more = 'data-infinite-scroll="true"';
401            }
402            ?>
403            <button type="button" class="wp-block-button__link" <?php echo esc_attr( $load_more ); ?> data-next="<?php echo esc_url( $articles_rest_url ); ?>">
404                <span class="label">
405                    <?php
406                    if ( ! empty( $attributes['moreButtonText'] ) ) {
407                        echo esc_html( $attributes['moreButtonText'] );
408                    } else {
409                        esc_html_e( 'Load more posts', 'jetpack-mu-wpcom' );
410                    }
411                    ?>
412                </span>
413                <span class="loading"></span>
414            </button>
415            <p class="error">
416                <?php esc_html_e( 'Something went wrong. Please refresh the page and/or try again.', 'jetpack-mu-wpcom' ); ?>
417            </p>
418
419        <?php endif; ?>
420
421    </div>
422    <?php
423
424    $content = ob_get_clean();
425    Newspack_Blocks::enqueue_view_assets( 'homepage-articles', 'defer' );
426
427    return $content;
428}
429
430/**
431 * Registers the `newspack-blocks/homepage-articles` block on server.
432 */
433function newspack_blocks_register_homepage_articles() {
434    $block = json_decode(
435        file_get_contents( __DIR__ . '/block.json' ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
436        true
437    );
438    register_block_type(
439        apply_filters( 'newspack_blocks_block_name', 'newspack-blocks/' . $block['name'] ),
440        apply_filters(
441            'newspack_blocks_block_args',
442            array(
443                'api_version'     => $block['apiVersion'],
444                'attributes'      => $block['attributes'],
445                'render_callback' => 'newspack_blocks_render_block_homepage_articles',
446                'supports'        => [],
447            ),
448            $block['name']
449        )
450    );
451}
452add_action( 'init', 'newspack_blocks_register_homepage_articles' );
453
454
455/**
456 * Renders author avatar markup.
457 *
458 * @param array $author_info Author info array.
459 *
460 * @return string Returns formatted Avatar markup
461 */
462function newspack_blocks_format_avatars( $author_info ) {
463    $elements = array_map(
464        function ( $author ) {
465            return sprintf(
466                '<a href="%s">%s</a>',
467                esc_url( $author->url ),
468                wp_kses(
469                    $author->avatar,
470                    Newspack_Blocks::get_sanitized_image_attributes()
471                )
472            );
473        },
474        $author_info
475    );
476
477    return implode( '', $elements );
478}
479
480/**
481 * Renders byline markup.
482 *
483 * @param object[] $author_info Author info array.
484 *
485 * @return string Returns byline markup.
486 */
487function newspack_blocks_format_byline( $author_info ) {
488    $index    = -1;
489    $elements = array_merge(
490        [
491            '<span class="author-prefix">' . esc_html_x( 'by', 'post author', 'jetpack-mu-wpcom' ) . '</span> ',
492        ],
493        array_reduce(
494            $author_info,
495            function ( $accumulator, $author ) use ( $author_info, &$index ) {
496                $index++;
497                $penultimate = count( $author_info ) - 2;
498                return array_merge(
499                    $accumulator,
500                    [
501                        sprintf(
502                            /* translators: 1: author link. 2: author name. 3. variable seperator (comma, 'and', or empty) */
503                            '<span class="author vcard"><a class="url fn n" href="%1$s">%2$s</a></span>',
504                            esc_url( $author->url ),
505                            esc_html( $author->display_name )
506                        ),
507                        ( $index < $penultimate ) ? ', ' : '',
508                        ( count( $author_info ) > 1 && $penultimate === $index ) ? esc_html_x( ' and ', 'post author', 'jetpack-mu-wpcom' ) : '',
509                    ]
510                );
511            },
512            []
513        )
514    );
515
516    $byline = implode( '', $elements );
517
518    /**
519     * Filters the byline markup.
520     *
521     * @param string   $byline      Byline markup.
522     * @param objcet[] $author_info Author info array.
523     */
524    return apply_filters( 'newspack_blocks_post_byline', $byline, $author_info );
525}
526
527/**
528 * Renders category markup plus filter.
529 *
530 * @param string $post_id Post ID.
531 */
532function newspack_blocks_format_categories( $post_id ) {
533    $category = false;
534    // Use Yoast primary category if set.
535    if ( class_exists( 'WPSEO_Primary_Term' ) ) {
536        $primary_term = new WPSEO_Primary_Term( 'category', $post_id );
537        $category_id  = $primary_term->get_primary_term();
538        if ( $category_id ) {
539            $category = get_term( $category_id );
540        }
541    }
542    if ( ! $category ) {
543        $categories_list = get_the_category();
544        if ( ! empty( $categories_list ) ) {
545            $category = $categories_list[0];
546        }
547    }
548
549    if ( ! is_a( $category, 'WP_Term' ) ) {
550        return '';
551    }
552
553    $category_link      = get_category_link( $category->term_id );
554    $category_formatted = esc_html( $category->name );
555
556    if ( ! empty( $category_link ) ) {
557        $category_formatted = '<a href="' . esc_url( $category_link ) . '">' . $category_formatted . '</a>';
558    }
559
560    return apply_filters( 'newspack_blocks_categories', $category_formatted );
561}