PluginProbe
bBlocks – Essential Gutenberg Blocks & Patterns Collection / 2.1.7
bBlocks – Essential Gutenberg Blocks & Patterns Collection v2.1.7
2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.43 2.0.42 2.0.41 2.0.40 2.0.39 2.0.38 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.5.1 1.5.2 1.5.3 1.5.4 All 107 releases
b-blocks / includes / blocks / post-masonry-grid / PostMasonryGrid.php

PostMasonryGrid.php in bBlocks – Essential Gutenberg Blocks & Patterns Collection 2.1.7, at includes/blocks/post-masonry-grid/PostMasonryGrid.php

572 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post Masonry Grid — shared server logic.
4 *
5 * Provides attribute sanitization, query building, card markup rendering, and a
6 * hardened `admin-ajax.php` endpoint (`bb_pmg_load_more`) used by the frontend
7 * `view.js` to append additional cards on Load More. The same card renderer is
8 * used by `render.php` (initial server render) and by the AJAX handler so markup
9 * is byte-identical and escaping lives in exactly one place.
10 *
11 * Security model for `bb_pmg_load_more`:
12 * - Nonce verified on every request via check_ajax_referer() (the same
13 * `wp_ajax` action localized to the frontend, so logged-out visitors get a
14 * valid nonce too).
15 * - All inputs sanitized: post_type (sanitize_key + post_type_exists),
16 * taxonomy/term IDs (sanitize_key / absint), paged (absint), orderby/order
17 * (strict allowlists).
18 * - All output escaped (esc_html / esc_url / esc_attr / wp_kses_post).
19 * - Responses use wp_send_json_success / wp_send_json_error; wp_reset_postdata().
20 *
21 * @package bBlocks
22 */
23
24 namespace BBlocks\Inc\Blocks;
25
26 if ( ! defined( 'ABSPATH' ) ) {
27 exit;
28 }
29
30 class PostMasonryGrid {
31
32 /**
33 * Allowed orderby values.
34 *
35 * @var string[]
36 */
37 const ORDERBY = [ 'date', 'title', 'modified', 'rand', 'comment_count' ];
38
39 /**
40 * Allowed order values (uppercased).
41 *
42 * @var string[]
43 */
44 const ORDER = [ 'ASC', 'DESC' ];
45
46 /**
47 * Allowed card styles.
48 *
49 * @var string[]
50 */
51 const CARD_STYLES = [ 'image-top', 'image-overlay', 'text-only' ];
52
53 /**
54 * Allowed image ratios.
55 *
56 * @var string[]
57 */
58 const RATIOS = [ 'auto', '1-1', '4-3', '16-9', '3-2' ];
59
60 /**
61 * Allowed image sizes.
62 *
63 * @var string[]
64 */
65 const IMAGE_SIZES = [ 'thumbnail', 'medium', 'large', 'full' ];
66
67 /**
68 * Allowed card shadows.
69 *
70 * @var string[]
71 */
72 const SHADOWS = [ 'none', 'small', 'medium', 'large' ];
73
74 /**
75 * Allowed title font weights.
76 *
77 * @var string[]
78 */
79 const FONT_WEIGHTS = [ '400', '500', '600', '700' ];
80
81 /**
82 * Allowed alignments.
83 *
84 * @var string[]
85 */
86 const ALIGNMENTS = [ 'left', 'center', 'right' ];
87
88 /**
89 * Hook the AJAX endpoint (public + logged-in).
90 */
91 public function __construct() {
92 add_action( 'wp_ajax_bb_pmg_load_more', [ $this, 'ajaxLoadMore' ] );
93 add_action( 'wp_ajax_nopriv_bb_pmg_load_more', [ $this, 'ajaxLoadMore' ] );
94 }
95
96 /* ----------------------------------------------------------------------
97 * Sanitizers
98 * ------------------------------------------------------------------- */
99
100 /**
101 * Sanitize a CSS color value (hex, rgb/hsl, var(), or a CSS keyword).
102 *
103 * @param mixed $color Raw color.
104 * @param string $fallback Fallback when invalid.
105 * @return string
106 */
107 public static function sanitizeColor( $color, $fallback = '' ) {
108 $color = trim( (string) $color );
109 if ( '' === $color ) {
110 return $fallback;
111 }
112 if ( preg_match( '/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color ) ) {
113 return $color;
114 }
115 if ( preg_match( '/^(rgb|rgba|hsl|hsla)\s*\([0-9\s,%.\/]+\)$/i', $color ) ) {
116 return $color;
117 }
118 if ( preg_match( '/^var\(\s*--[a-zA-Z0-9\-_]+\s*(,\s*[a-zA-Z0-9 #%.,\-_\/]+)?\s*\)$/', $color ) ) {
119 return $color;
120 }
121 if ( preg_match( '/^[a-zA-Z]{1,30}$/', $color ) ) {
122 return $color;
123 }
124 return $fallback;
125 }
126
127 /**
128 * Clamp a value to an integer range.
129 *
130 * @param mixed $value Raw value.
131 * @param int $min Minimum.
132 * @param int $max Maximum.
133 * @param int $fallback Fallback when non-numeric.
134 * @return int
135 */
136 public static function clampInt( $value, $min, $max, $fallback ) {
137 if ( ! is_numeric( $value ) ) {
138 return (int) $fallback;
139 }
140 $value = (int) $value;
141 if ( $value < $min ) {
142 return (int) $min;
143 }
144 if ( $value > $max ) {
145 return (int) $max;
146 }
147 return $value;
148 }
149
150 /**
151 * Pick a value from an allowlist.
152 *
153 * @param mixed $value Raw value.
154 * @param string[] $allowed Allowed values.
155 * @param string $fallback Fallback.
156 * @return string
157 */
158 public static function pickFrom( $value, array $allowed, $fallback ) {
159 $value = is_string( $value ) ? trim( $value ) : '';
160 return in_array( $value, $allowed, true ) ? $value : $fallback;
161 }
162
163 /**
164 * Sanitize a comma-separated list of positive integers to an array of ints.
165 *
166 * @param mixed $value Raw comma-separated string.
167 * @return int[]
168 */
169 public static function parseIdList( $value ) {
170 if ( is_array( $value ) ) {
171 $parts = $value;
172 } else {
173 $parts = explode( ',', (string) $value );
174 }
175 $ids = array_map( 'absint', $parts );
176 $ids = array_filter( $ids );
177 return array_values( array_unique( $ids ) );
178 }
179
180 /**
181 * Map an aspect-ratio token to a CSS aspect-ratio value.
182 *
183 * @param string $ratio Ratio token.
184 * @return string
185 */
186 public static function ratioToCss( $ratio ) {
187 $map = [
188 'auto' => 'auto',
189 '1-1' => '1 / 1',
190 '4-3' => '4 / 3',
191 '16-9' => '16 / 9',
192 '3-2' => '3 / 2',
193 ];
194 return $map[ $ratio ] ?? '16 / 9';
195 }
196
197 /**
198 * Normalize and sanitize the full attribute set into a safe, typed array.
199 *
200 * @param array $attributes Raw block attributes.
201 * @return array
202 */
203 public static function resolveAttributes( array $attributes ) {
204 $postTypeRaw = isset( $attributes['postType'] ) ? sanitize_key( (string) $attributes['postType'] ) : 'post';
205 $postType = post_type_exists( $postTypeRaw ) ? $postTypeRaw : 'post';
206
207 $filterLabel = isset( $attributes['filterLabel'] ) ? wp_strip_all_tags( (string) $attributes['filterLabel'] ) : '';
208 $filterLabel = '' !== trim( $filterLabel ) ? $filterLabel : __( 'All', 'b-blocks' );
209
210 $loadMoreLabel = isset( $attributes['loadMoreLabel'] ) ? wp_strip_all_tags( (string) $attributes['loadMoreLabel'] ) : '';
211 $loadMoreLabel = '' !== trim( $loadMoreLabel ) ? $loadMoreLabel : __( 'Load More', 'b-blocks' );
212
213 $readMoreLabel = isset( $attributes['readMoreLabel'] ) ? wp_strip_all_tags( (string) $attributes['readMoreLabel'] ) : '';
214 $readMoreLabel = '' !== trim( $readMoreLabel ) ? $readMoreLabel : __( 'Read More', 'b-blocks' );
215
216 $noPostsText = isset( $attributes['noPostsText'] ) ? wp_strip_all_tags( (string) $attributes['noPostsText'] ) : '';
217 $noPostsText = '' !== trim( $noPostsText ) ? $noPostsText : __( 'No posts found.', 'b-blocks' );
218
219 return [
220 'postType' => $postType,
221 'postsPerPage' => self::clampInt( $attributes['postsPerPage'] ?? 9, 1, 48, 9 ),
222 'orderBy' => self::pickFrom( $attributes['orderBy'] ?? 'date', self::ORDERBY, 'date' ),
223 'order' => self::pickFrom( strtoupper( (string) ( $attributes['order'] ?? 'DESC' ) ), self::ORDER, 'DESC' ),
224 'categoryIds' => self::parseIdList( $attributes['categoryIds'] ?? [] ),
225 'tagIds' => self::parseIdList( $attributes['tagIds'] ?? [] ),
226 'excludeIds' => self::parseIdList( $attributes['excludeIds'] ?? '' ),
227
228 'filterEnabled' => ! isset( $attributes['filterEnabled'] ) || (bool) $attributes['filterEnabled'],
229 'filterLabel' => $filterLabel,
230
231 'loadMoreEnabled' => ! isset( $attributes['loadMoreEnabled'] ) || (bool) $attributes['loadMoreEnabled'],
232 'loadMoreLabel' => $loadMoreLabel,
233 'loadMoreStep' => self::clampInt( $attributes['loadMoreStep'] ?? 6, 1, 24, 6 ),
234
235 'cardStyle' => self::pickFrom( $attributes['cardStyle'] ?? 'image-top', self::CARD_STYLES, 'image-top' ),
236 'imageRatio' => self::pickFrom( $attributes['imageRatio'] ?? '16-9', self::RATIOS, '16-9' ),
237 'imageSize' => self::pickFrom( $attributes['imageSize'] ?? 'large', self::IMAGE_SIZES, 'large' ),
238
239 'showExcerpt' => ! isset( $attributes['showExcerpt'] ) || (bool) $attributes['showExcerpt'],
240 'excerptLength' => self::clampInt( $attributes['excerptLength'] ?? 20, 5, 60, 20 ),
241 'showDate' => ! isset( $attributes['showDate'] ) || (bool) $attributes['showDate'],
242 'showAuthor' => ! empty( $attributes['showAuthor'] ),
243 'showCategory' => ! isset( $attributes['showCategory'] ) || (bool) $attributes['showCategory'],
244 'showReadMore' => ! isset( $attributes['showReadMore'] ) || (bool) $attributes['showReadMore'],
245 'readMoreLabel' => $readMoreLabel,
246
247 'colsDesktop' => self::clampInt( $attributes['colsDesktop'] ?? 3, 1, 5, 3 ),
248 'colsTablet' => self::clampInt( $attributes['colsTablet'] ?? 2, 1, 4, 2 ),
249 'colsMobile' => self::clampInt( $attributes['colsMobile'] ?? 1, 1, 2, 1 ),
250 'gap' => self::clampInt( $attributes['gap'] ?? 24, 0, 80, 24 ),
251 'maxWidth' => self::clampInt( $attributes['maxWidth'] ?? 0, 0, 1600, 0 ),
252 'align' => self::pickFrom( $attributes['align'] ?? 'center', self::ALIGNMENTS, 'center' ),
253
254 'cardBg' => self::sanitizeColor( $attributes['cardBg'] ?? '', '#ffffff' ),
255 'cardRadius' => self::clampInt( $attributes['cardRadius'] ?? 8, 0, 32, 8 ),
256 'cardShadow' => self::pickFrom( $attributes['cardShadow'] ?? 'small', self::SHADOWS, 'small' ),
257 'accentColor' => self::sanitizeColor( $attributes['accentColor'] ?? '', '#2563eb' ),
258 'titleColor' => self::sanitizeColor( $attributes['titleColor'] ?? '', 'inherit' ),
259 'metaColor' => self::sanitizeColor( $attributes['metaColor'] ?? '', 'inherit' ),
260 'excerptColor' => self::sanitizeColor( $attributes['excerptColor'] ?? '', 'inherit' ),
261 'titleFontSize' => self::clampInt( $attributes['titleFontSize'] ?? 18, 12, 40, 18 ),
262 'titleFontWeight' => self::pickFrom( (string) ( $attributes['titleFontWeight'] ?? '600' ), self::FONT_WEIGHTS, '600' ),
263 'metaFontSize' => self::clampInt( $attributes['metaFontSize'] ?? 13, 10, 20, 13 ),
264 'excerptFontSize' => self::clampInt( $attributes['excerptFontSize'] ?? 14, 10, 22, 14 ),
265 'overlayColor' => self::sanitizeColor( $attributes['overlayColor'] ?? '', 'rgba(0,0,0,0.45)' ),
266
267 'filterBarAlignment' => self::pickFrom( $attributes['filterBarAlignment'] ?? 'left', self::ALIGNMENTS, 'left' ),
268 'filterBarGap' => self::clampInt( $attributes['filterBarGap'] ?? 8, 0, 32, 8 ),
269 'filterActiveBg' => self::sanitizeColor( $attributes['filterActiveBg'] ?? '', '' ),
270 'filterActiveText' => self::sanitizeColor( $attributes['filterActiveText'] ?? '', '#ffffff' ),
271 'filterInactiveBg' => self::sanitizeColor( $attributes['filterInactiveBg'] ?? '', '#f3f4f6' ),
272 'filterInactiveText' => self::sanitizeColor( $attributes['filterInactiveText'] ?? '', '#374151' ),
273 'filterPillRadius' => self::clampInt( $attributes['filterPillRadius'] ?? 9999, 0, 9999, 9999 ),
274
275 'loadMoreBg' => self::sanitizeColor( $attributes['loadMoreBg'] ?? '', '' ),
276 'loadMoreText' => self::sanitizeColor( $attributes['loadMoreText'] ?? '', '#ffffff' ),
277 'loadMoreRadius' => self::clampInt( $attributes['loadMoreRadius'] ?? 6, 0, 50, 6 ),
278
279 'noPostsText' => $noPostsText,
280 ];
281 }
282
283 /* ----------------------------------------------------------------------
284 * Query
285 * ------------------------------------------------------------------- */
286
287 /**
288 * Build sanitized WP_Query args.
289 *
290 * @param array $a Resolved attributes.
291 * @param int $paged Page number (1-based).
292 * @return array
293 */
294 public static function buildQueryArgs( array $a, $paged ) {
295 $paged = max( 1, absint( $paged ) );
296
297 $args = [
298 'post_type' => $a['postType'],
299 'posts_per_page' => $a['postsPerPage'],
300 'paged' => $paged,
301 'post_status' => 'publish',
302 'orderby' => $a['orderBy'],
303 'order' => $a['order'],
304 'has_password' => false,
305 'ignore_sticky_posts' => true,
306 ];
307
308 if ( ! empty( $a['excludeIds'] ) ) {
309 $args['post__not_in'] = $a['excludeIds'];
310 }
311
312 $taxQuery = [];
313 if ( ! empty( $a['categoryIds'] ) && taxonomy_exists( 'category' ) ) {
314 $taxQuery[] = [
315 'taxonomy' => 'category',
316 'field' => 'term_id',
317 'terms' => $a['categoryIds'],
318 ];
319 }
320 if ( ! empty( $a['tagIds'] ) && taxonomy_exists( 'post_tag' ) ) {
321 $taxQuery[] = [
322 'taxonomy' => 'post_tag',
323 'field' => 'term_id',
324 'terms' => $a['tagIds'],
325 ];
326 }
327 if ( ! empty( $taxQuery ) ) {
328 if ( count( $taxQuery ) > 1 ) {
329 $taxQuery['relation'] = 'AND';
330 }
331 $args['tax_query'] = $taxQuery; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
332 }
333
334 return $args;
335 }
336
337 /* ----------------------------------------------------------------------
338 * Card rendering
339 * ------------------------------------------------------------------- */
340
341 /**
342 * Render the cards for a query as an escaped HTML fragment.
343 *
344 * Each card is an `<li>` so the container can be a `<ul role="list">`.
345 *
346 * @param \WP_Query $query The query.
347 * @param array $a Resolved attributes.
348 * @return string Escaped HTML.
349 */
350 public static function renderCards( $query, array $a ) {
351 $isOverlay = ( 'image-overlay' === $a['cardStyle'] );
352 $showImage = ( 'text-only' !== $a['cardStyle'] );
353
354 ob_start();
355
356 while ( $query->have_posts() ) :
357 $query->the_post();
358 $postId = get_the_ID();
359 $permalink = get_permalink( $postId );
360 $titleText = get_the_title( $postId );
361 $titleId = 'bb-pmg-title-' . $postId . '-' . wp_rand( 1000, 9999 );
362
363 // Category slugs for the client-side filter data attribute.
364 $catSlugs = [];
365 $postCats = get_the_terms( $postId, 'category' );
366 if ( ! empty( $postCats ) && ! is_wp_error( $postCats ) ) {
367 foreach ( $postCats as $catTerm ) {
368 $catSlugs[] = $catTerm->slug;
369 }
370 }
371
372 // Featured image.
373 $imageUrl = '';
374 $imageAlt = '';
375 if ( $showImage && has_post_thumbnail( $postId ) ) {
376 $thumbUrl = get_the_post_thumbnail_url( $postId, $a['imageSize'] );
377 if ( $thumbUrl ) {
378 $imageUrl = $thumbUrl;
379 $thumbId = get_post_thumbnail_id( $postId );
380 if ( $thumbId ) {
381 $metaAlt = get_post_meta( $thumbId, '_wp_attachment_image_alt', true );
382 if ( is_string( $metaAlt ) ) {
383 $imageAlt = trim( wp_strip_all_tags( $metaAlt ) );
384 }
385 }
386 if ( '' === $imageAlt ) {
387 $imageAlt = $titleText;
388 }
389 }
390 }
391
392 // Primary category badge.
393 $termName = '';
394 $termLink = '';
395 if ( $a['showCategory'] && ! empty( $postCats ) && ! is_wp_error( $postCats ) ) {
396 $firstTerm = $postCats[0];
397 $termName = $firstTerm->name;
398 $link = get_term_link( $firstTerm );
399 if ( ! is_wp_error( $link ) ) {
400 $termLink = $link;
401 }
402 }
403
404 // Excerpt.
405 $excerptText = '';
406 if ( $a['showExcerpt'] ) {
407 $excerptText = wp_trim_words( get_the_excerpt( $postId ), $a['excerptLength'], '&hellip;' );
408 }
409
410 $cardClasses = 'bb-pmg-card bb-pmg-card--' . $a['cardStyle'];
411 ?>
412 <li
413 class='<?php echo esc_attr( $cardClasses ); ?>'
414 data-bb-pmg-cats='<?php echo esc_attr( wp_json_encode( $catSlugs ) ); ?>'
415 >
416 <article class='bb-pmg-article' aria-labelledby='<?php echo esc_attr( $titleId ); ?>'>
417 <?php if ( $isOverlay ) : ?>
418 <a class='bb-pmg-overlay-link' href='<?php echo esc_url( $permalink ); ?>'>
419 <?php if ( '' !== $imageUrl ) : ?>
420 <img class='bb-pmg-image' src='<?php echo esc_url( $imageUrl ); ?>' alt='<?php echo esc_attr( $imageAlt ); ?>' loading='lazy' decoding='async' />
421 <?php endif; ?>
422 <span class='bb-pmg-overlay-scrim' aria-hidden='true'></span>
423 <span class='bb-pmg-card-body'>
424 <?php if ( '' !== $termName ) : ?>
425 <span class='bb-pmg-badge'><?php echo esc_html( $termName ); ?></span>
426 <?php endif; ?>
427 <span class='bb-pmg-title' id='<?php echo esc_attr( $titleId ); ?>'><?php echo esc_html( $titleText ); ?></span>
428 <?php if ( $a['showDate'] || $a['showAuthor'] ) : ?>
429 <span class='bb-pmg-meta'>
430 <?php echo wp_kses_post( self::metaHtml( $postId, $a ) ); ?>
431 </span>
432 <?php endif; ?>
433 </span>
434 </a>
435 <?php else : ?>
436 <?php if ( '' !== $imageUrl ) : ?>
437 <a class='bb-pmg-image-link' href='<?php echo esc_url( $permalink ); ?>' tabindex='-1' aria-hidden='true'>
438 <img class='bb-pmg-image' src='<?php echo esc_url( $imageUrl ); ?>' alt='<?php echo esc_attr( $imageAlt ); ?>' loading='lazy' decoding='async' />
439 </a>
440 <?php endif; ?>
441
442 <div class='bb-pmg-card-body'>
443 <?php if ( '' !== $termName ) : ?>
444 <?php if ( '' !== $termLink ) : ?>
445 <a class='bb-pmg-badge' href='<?php echo esc_url( $termLink ); ?>'><?php echo esc_html( $termName ); ?></a>
446 <?php else : ?>
447 <span class='bb-pmg-badge'><?php echo esc_html( $termName ); ?></span>
448 <?php endif; ?>
449 <?php endif; ?>
450
451 <h3 class='bb-pmg-title' id='<?php echo esc_attr( $titleId ); ?>'>
452 <a class='bb-pmg-title-link' href='<?php echo esc_url( $permalink ); ?>'>
453 <?php echo esc_html( $titleText ); ?>
454 </a>
455 </h3>
456
457 <?php if ( $a['showDate'] || $a['showAuthor'] ) : ?>
458 <div class='bb-pmg-meta'>
459 <?php echo wp_kses_post( self::metaHtml( $postId, $a ) ); ?>
460 </div>
461 <?php endif; ?>
462
463 <?php if ( $a['showExcerpt'] && '' !== $excerptText ) : ?>
464 <p class='bb-pmg-excerpt'><?php echo wp_kses_post( $excerptText ); ?></p>
465 <?php endif; ?>
466
467 <?php if ( $a['showReadMore'] ) : ?>
468 <a class='bb-pmg-readmore' href='<?php echo esc_url( $permalink ); ?>'>
469 <?php echo esc_html( $a['readMoreLabel'] ); ?>
470 <span class='screen-reader-text'> &mdash; <?php echo esc_html( $titleText ); ?></span>
471 </a>
472 <?php endif; ?>
473 </div>
474 <?php endif; ?>
475 </article>
476 </li>
477 <?php
478 endwhile;
479
480 return ob_get_clean();
481 }
482
483 /**
484 * Build the escaped meta row HTML (author + date).
485 *
486 * @param int $postId Post ID.
487 * @param array $a Resolved attributes.
488 * @return string Escaped HTML.
489 */
490 protected static function metaHtml( $postId, array $a ) {
491 ob_start();
492 if ( $a['showAuthor'] ) :
493 ?>
494 <span class='bb-pmg-meta-author'>
495 <span class='screen-reader-text'><?php echo esc_html__( 'Author:', 'b-blocks' ); ?> </span>
496 <?php echo esc_html( get_the_author_meta( 'display_name', (int) get_post_field( 'post_author', $postId ) ) ); ?>
497 </span>
498 <?php
499 endif;
500 if ( $a['showAuthor'] && $a['showDate'] ) :
501 ?>
502 <span class='bb-pmg-meta-sep' aria-hidden='true'>&middot;</span>
503 <?php
504 endif;
505 if ( $a['showDate'] ) :
506 ?>
507 <time class='bb-pmg-meta-date' datetime='<?php echo esc_attr( get_the_date( 'c', $postId ) ); ?>'>
508 <?php echo esc_html( get_the_date( '', $postId ) ); ?>
509 </time>
510 <?php
511 endif;
512 return ob_get_clean();
513 }
514
515 /* ----------------------------------------------------------------------
516 * AJAX endpoint
517 * ------------------------------------------------------------------- */
518
519 /**
520 * Handle the `bb_pmg_load_more` AJAX request.
521 *
522 * Returns JSON: { html, page, hasMore, count }.
523 */
524 public function ajaxLoadMore() {
525 // Nonce — shared plugin action handle. Dies with 403 on failure.
526 check_ajax_referer( 'wp_ajax', 'nonce' );
527
528 $raw = isset( $_POST['attributes'] ) ? wp_unslash( $_POST['attributes'] ) : ''; // phpcs:ignore WordPress.Security.ValidationSanitization.MissingUnslash, WordPress.Security.ValidationSanitization.InputNotSanitized -- JSON decoded then each field individually sanitized in resolveAttributes().
529 $attributes = is_string( $raw ) ? json_decode( $raw, true ) : [];
530 if ( ! is_array( $attributes ) ) {
531 $attributes = [];
532 }
533
534 $a = self::resolveAttributes( $attributes );
535
536 $paged = isset( $_POST['paged'] ) ? max( 1, absint( wp_unslash( $_POST['paged'] ) ) ) : 1;
537
538 $args = self::buildQueryArgs( $a, $paged );
539 $query = new \WP_Query( $args );
540
541 $html = '';
542 $count = 0;
543 if ( $query->have_posts() ) {
544 $html = self::renderCards( $query, $a );
545 $count = (int) $query->post_count;
546 }
547 wp_reset_postdata();
548
549 $maxPages = (int) $query->max_num_pages;
550 $hasMore = $paged < $maxPages;
551
552 if ( '' === $html ) {
553 wp_send_json_error(
554 [
555 'message' => esc_html( $a['noPostsText'] ),
556 ]
557 );
558 }
559
560 wp_send_json_success(
561 [
562 'html' => $html,
563 'page' => $paged,
564 'hasMore' => $hasMore,
565 'count' => $count,
566 ]
567 );
568 }
569 }
570
571 new PostMasonryGrid();
572