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