1<?php
2/**
3 * Server-side rendering of the `core/latest-posts` block.
4 *
5 * @package WordPress
6 */
7
8/**
9 * The excerpt length set by the Latest Posts core block
10 * set at render time and used by the block itself.
11 *
12 * @var int
13 */
14global $block_core_latest_posts_excerpt_length;
15$block_core_latest_posts_excerpt_length = 0;
16
17/**
18 * Callback for the excerpt_length filter used by
19 * the Latest Posts block at render time.
20 *
21 * @since 5.4.0
22 *
23 * @return int Returns the global $block_core_latest_posts_excerpt_length variable
24 * to allow the excerpt_length filter respect the Latest Block setting.
25 */
26function block_core_latest_posts_get_excerpt_length() {
27 global $block_core_latest_posts_excerpt_length;
28 return $block_core_latest_posts_excerpt_length;
29}
30
31/**
32 * Renders the `core/latest-posts` block on server.
33 *
34 * @since 5.0.0
35 *
36 * @global WP_Post $post Global post object.
37 * @global int $block_core_latest_posts_excerpt_length Excerpt length set by the Latest Posts core block.
38 *
39 * @param array $attributes The block attributes.
40 *
41 * @return string Returns the post content with latest posts added.
42 */
43function render_block_core_latest_posts( $attributes ) {
44 global $post, $block_core_latest_posts_excerpt_length;
45
46 $args = array(
47 'posts_per_page' => $attributes['postsToShow'],
48 'post_status' => 'publish',
49 'order' => $attributes['order'],
50 'orderby' => $attributes['orderBy'],
51 'ignore_sticky_posts' => true,
52 'no_found_rows' => true,
53 );
54
55 $block_core_latest_posts_excerpt_length = $attributes['excerptLength'];
56 add_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
57
58 if ( ! empty( $attributes['categories'] ) ) {
59 $args['category__in'] = array_column( $attributes['categories'], 'id' );
60 }
61 if ( isset( $attributes['selectedAuthor'] ) ) {
62 $args['author'] = $attributes['selectedAuthor'];
63 }
64
65 $query = new WP_Query();
66 $recent_posts = $query->query( $args );
67
68 if ( isset( $attributes['displayFeaturedImage'] ) && $attributes['displayFeaturedImage'] ) {
69 update_post_thumbnail_cache( $query );
70 }
71
72 $list_items_markup = '';
73
74 foreach ( $recent_posts as $post ) {
75 $post_link = esc_url( get_permalink( $post ) );
76 $title = get_the_title( $post );
77
78 if ( ! $title ) {
79 $title = __( '(no title)' );
80 }
81
82 $list_items_markup .= '<li>';
83
84 if ( $attributes['displayFeaturedImage'] && has_post_thumbnail( $post ) ) {
85 $image_style = '';
86 if ( isset( $attributes['featuredImageSizeWidth'] ) ) {
87 $image_style .= sprintf( 'max-width:%spx;', $attributes['featuredImageSizeWidth'] );
88 }
89 if ( isset( $attributes['featuredImageSizeHeight'] ) ) {
90 $image_style .= sprintf( 'max-height:%spx;', $attributes['featuredImageSizeHeight'] );
91 }
92
93 $image_classes = 'wp-block-latest-posts__featured-image';
94 if ( isset( $attributes['featuredImageAlign'] ) ) {
95 $image_classes .= ' align' . $attributes['featuredImageAlign'];
96 }
97
98 $featured_image = get_the_post_thumbnail(
99 $post,
100 $attributes['featuredImageSizeSlug'],
101 array(
102 'style' => esc_attr( $image_style ),
103 )
104 );
105 if ( $attributes['addLinkToFeaturedImage'] ) {
106 $featured_image = sprintf(
107 '<a href="%1$s" aria-label="%2$s">%3$s</a>',
108 esc_url( $post_link ),
109 esc_attr( $title ),
110 $featured_image
111 );
112 }
113 $list_items_markup .= sprintf(
114 '<div class="%1$s">%2$s</div>',
115 esc_attr( $image_classes ),
116 $featured_image
117 );
118 }
119
120 $list_items_markup .= sprintf(
121 '<a class="wp-block-latest-posts__post-title" href="%1$s">%2$s</a>',
122 esc_url( $post_link ),
123 $title
124 );
125
126 if ( isset( $attributes['displayAuthor'] ) && $attributes['displayAuthor'] ) {
127 $author_display_name = get_the_author_meta( 'display_name', $post->post_author );
128
129 /* translators: byline. %s: author. */
130 $byline = sprintf( __( 'by %s' ), $author_display_name );
131
132 if ( ! empty( $author_display_name ) ) {
133 $list_items_markup .= sprintf(
134 '<div class="wp-block-latest-posts__post-author">%1$s</div>',
135 $byline
136 );
137 }
138 }
139
140 if ( isset( $attributes['displayPostDate'] ) && $attributes['displayPostDate'] ) {
141 $list_items_markup .= sprintf(
142 '<time datetime="%1$s" class="wp-block-latest-posts__post-date">%2$s</time>',
143 esc_attr( get_the_date( 'c', $post ) ),
144 get_the_date( '', $post )
145 );
146 }
147
148 if ( isset( $attributes['displayPostContent'] ) && $attributes['displayPostContent']
149 && isset( $attributes['displayPostContentRadio'] ) && 'excerpt' === $attributes['displayPostContentRadio'] ) {
150
151 $trimmed_excerpt = get_the_excerpt( $post );
152
153 /*
154 * Adds a "Read more" link with screen reader text.
155 * […] is the default excerpt ending from wp_trim_excerpt() in Core.
156 */
157 if ( str_ends_with( $trimmed_excerpt, ' […]' ) ) {
158 /** This filter is documented in wp-includes/formatting.php */
159 $excerpt_length = (int) apply_filters( 'excerpt_length', $block_core_latest_posts_excerpt_length );
160 if ( $excerpt_length <= $block_core_latest_posts_excerpt_length ) {
161 $trimmed_excerpt = substr( $trimmed_excerpt, 0, -11 );
162 $trimmed_excerpt .= sprintf(
163 /* translators: 1: A URL to a post, 2: Hidden accessibility text: Post title */
164 __( '… <a class="wp-block-latest-posts__read-more" href="%1$s" rel="noopener noreferrer">Read more<span class="screen-reader-text">: %2$s</span></a>' ),
165 esc_url( $post_link ),
166 esc_html( $title )
167 );
168 }
169 }
170
171 if ( post_password_required( $post ) ) {
172 $trimmed_excerpt = __( 'This content is password protected.' );
173 }
174
175 $list_items_markup .= sprintf(
176 '<div class="wp-block-latest-posts__post-excerpt">%1$s</div>',
177 $trimmed_excerpt
178 );
179 }
180
181 if ( isset( $attributes['displayPostContent'] ) && $attributes['displayPostContent']
182 && isset( $attributes['displayPostContentRadio'] ) && 'full_post' === $attributes['displayPostContentRadio'] ) {
183
184 $post_content = html_entity_decode( $post->post_content, ENT_QUOTES, get_option( 'blog_charset' ) );
185
186 if ( post_password_required( $post ) ) {
187 $post_content = __( 'This content is password protected.' );
188 }
189
190 $list_items_markup .= sprintf(
191 '<div class="wp-block-latest-posts__post-full-content">%1$s</div>',
192 wp_kses_post( $post_content )
193 );
194 }
195
196 $list_items_markup .= "</li>\n";
197 }
198
199 remove_filter( 'excerpt_length', 'block_core_latest_posts_get_excerpt_length', 20 );
200
201 $classes = array( 'wp-block-latest-posts__list' );
202 if ( isset( $attributes['postLayout'] ) && 'grid' === $attributes['postLayout'] ) {
203 $classes[] = 'is-grid';
204 }
205 if ( isset( $attributes['columns'] ) && 'grid' === $attributes['postLayout'] ) {
206 $classes[] = 'columns-' . $attributes['columns'];
207 }
208 if ( isset( $attributes['displayPostDate'] ) && $attributes['displayPostDate'] ) {
209 $classes[] = 'has-dates';
210 }
211 if ( isset( $attributes['displayAuthor'] ) && $attributes['displayAuthor'] ) {
212 $classes[] = 'has-author';
213 }
214 if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
215 $classes[] = 'has-link-color';
216 }
217
218 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );
219
220 return sprintf(
221 '<ul %1$s>%2$s</ul>',
222 $wrapper_attributes,
223 $list_items_markup
224 );
225}
226
227/**
228 * Registers the `core/latest-posts` block on server.
229 *
230 * @since 5.0.0
231 */
232function register_block_core_latest_posts() {
233 register_block_type_from_metadata(
234 __DIR__ . '/latest-posts',
235 array(
236 'render_callback' => 'render_block_core_latest_posts',
237 )
238 );
239}
240add_action( 'init', 'register_block_core_latest_posts' );
241
242/**
243 * Handles outdated versions of the `core/latest-posts` block by converting
244 * attribute `categories` from a numeric string to an array with key `id`.
245 *
246 * This is done to accommodate the changes introduced in #20781 that sought to
247 * add support for multiple categories to the block. However, given that this
248 * block is dynamic, the usual provisions for block migration are insufficient,
249 * as they only act when a block is loaded in the editor.
250 *
251 * TODO: Remove when and if the bottom client-side deprecation for this block
252 * is removed.
253 *
254 * @since 5.5.0
255 *
256 * @param array $block A single parsed block object.
257 *
258 * @return array The migrated block object.
259 */
260function block_core_latest_posts_migrate_categories( $block ) {
261 if (
262 'core/latest-posts' === $block['blockName'] &&
263 ! empty( $block['attrs']['categories'] ) &&
264 is_string( $block['attrs']['categories'] )
265 ) {
266 $block['attrs']['categories'] = array(
267 array( 'id' => absint( $block['attrs']['categories'] ) ),
268 );
269 }
270
271 return $block;
272}
273add_filter( 'render_block_data', 'block_core_latest_posts_migrate_categories' );
274