PluginProbe ʕ •ᴥ•ʔ
GenerateBlocks / 2.4.0
GenerateBlocks v2.4.0
2.4.0 trunk 1.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.7.0 1.7.1 1.7.2 1.7.3 1.8.0 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.1.2 2.2.0 2.2.1 2.3.0
generateblocks / includes / class-dynamic-content.php
generateblocks / includes Last commit date
blocks 1 month ago dynamic-tags 1 week ago pattern-library 1 month ago utils 2 years ago class-do-css.php 3 years ago class-dynamic-content.php 1 week ago class-dynamic-tag-security.php 1 week ago class-enqueue-css.php 1 week ago class-legacy-attributes.php 4 years ago class-map-deprecated-attributes.php 3 years ago class-meta-handler.php 1 week ago class-plugin-update.php 1 year ago class-query-loop.php 2 years ago class-query-utils.php 1 week ago class-render-blocks.php 1 week ago class-rest.php 1 year ago class-save-gate.php 1 week ago class-settings.php 1 year ago dashboard.php 1 week ago defaults.php 1 year ago deprecated.php 1 year ago functions.php 1 week ago general.php 1 week ago
class-dynamic-content.php
1509 lines
1 <?php
2 /**
3 * Handles option changes on plugin updates.
4 *
5 * @package GenerateBlocks
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit; // Exit if accessed directly.
10 }
11
12 /**
13 * Process option updates if necessary.
14 */
15 class GenerateBlocks_Dynamic_Content {
16 /**
17 * Class instance.
18 *
19 * @access private
20 * @var $instance Class instance.
21 */
22 private static $instance;
23
24 /**
25 * For the excerpt we need to keep track of ids to prevent infinite loops.
26 *
27 * @var array $source_ids The current post id.
28 */
29 private static $source_ids = [];
30
31 /**
32 * Initiator
33 */
34 public static function get_instance() {
35 if ( ! isset( self::$instance ) ) {
36 self::$instance = new self();
37 }
38 return self::$instance;
39 }
40
41 /**
42 * Constructor.
43 */
44 public function __construct() {
45 add_filter( 'generateblocks_defaults', array( $this, 'add_block_defaults' ) );
46 add_filter( 'generateblocks_background_image_url', array( $this, 'set_dynamic_background_image' ), 10, 2 );
47 add_filter( 'generateblocks_button_count', array( $this, 'update_button_count' ), 10, 3 );
48 }
49
50 /**
51 * Get the requested dynamic content.
52 *
53 * @param array $attributes The block attributes.
54 * @param WP_Block $block Block instance.
55 */
56 public static function get_content( $attributes, $block ) {
57 $content = '';
58
59 if ( self::should_block_block_renderer_attribute_resolution( $attributes ) ) {
60 return '';
61 }
62
63 switch ( $attributes['dynamicContentType'] ) {
64 case 'post-title':
65 $content = self::get_post_title( $attributes );
66 break;
67
68 case 'post-excerpt':
69 $content = self::get_post_excerpt( $attributes );
70 // Once we have the excerpt content we are safe to clear the source ids.
71 // By doing so we avoid empty content for subsequent calls.
72 self::$source_ids = [];
73 break;
74
75 case 'post-date':
76 $content = self::get_post_date( $attributes );
77 break;
78
79 case 'post-meta':
80 $content = self::get_post_meta( $attributes );
81 break;
82
83 case 'comments-number':
84 $content = self::get_comments_number( $attributes );
85 break;
86
87 case 'terms':
88 $content = self::get_terms( $attributes );
89 break;
90
91 case 'author-meta':
92 $content = self::get_author_meta( $attributes );
93 break;
94
95 case 'author-email':
96 $content = self::get_user_data( self::get_source_author_id( $attributes ), 'user_email' );
97 break;
98
99 case 'author-name':
100 $content = self::get_user_data( self::get_source_author_id( $attributes ), 'display_name' );
101 break;
102
103 case 'author-nickname':
104 $content = self::get_user_data( self::get_source_author_id( $attributes ), 'nickname' );
105 break;
106
107 case 'author-first-name':
108 $content = self::get_user_data( self::get_source_author_id( $attributes ), 'first_name' );
109 break;
110
111 case 'author-last-name':
112 $content = self::get_user_data( self::get_source_author_id( $attributes ), 'last_name' );
113 break;
114
115 case 'pagination-numbers':
116 $content = self::get_paginate_links( $attributes, $block );
117 break;
118
119 case 'featured-image':
120 $content = self::get_dynamic_image( $attributes, $block );
121 break;
122
123 case 'caption':
124 $content = self::get_image_caption( $attributes, $block );
125 break;
126
127 case 'alt-text':
128 $content = self::get_image_alt_text( $attributes, $block );
129 break;
130
131 case 'image-description':
132 $content = self::get_image_description( $attributes, $block );
133 break;
134 }
135
136 return apply_filters(
137 'generateblocks_dynamic_content_output',
138 $content,
139 $attributes,
140 $block
141 );
142 }
143
144 /**
145 * Get the requested post title.
146 *
147 * @param array $attributes The block attributes.
148 */
149 public static function get_post_title( $attributes ) {
150 return get_the_title( self::get_source_id( $attributes ) );
151 }
152
153 /**
154 * Get the post excerpt
155 *
156 * @param array $attributes The block attributes.
157 *
158 * @return string
159 */
160 public static function get_post_excerpt( $attributes ) {
161 $source_id = self::get_source_id( $attributes );
162 $unique_id = $attributes['uniqueId'];
163
164 // This prevents endless loops by not rendering excerpts within themselves.
165 if ( ! $source_id || ( isset( self::$source_ids[ $unique_id ] ) && $source_id === self::$source_ids[ $unique_id ] ) ) {
166 return '';
167 }
168
169 self::$source_ids[ $unique_id ] = $source_id;
170
171 $filter_excerpt_length = function( $length ) use ( $attributes ) {
172 return isset( $attributes['excerptLength'] ) ? $attributes['excerptLength'] : $length;
173 };
174
175 add_filter(
176 'excerpt_length',
177 $filter_excerpt_length,
178 100
179 );
180
181 if ( isset( $attributes['useDefaultMoreLink'] ) && ! $attributes['useDefaultMoreLink'] ) {
182 $filter_more_text = function() use ( $attributes ) {
183 if ( empty( $attributes['customMoreLinkText'] ) ) {
184 return ' ...';
185 }
186
187 return apply_filters(
188 'generateblocks_dynamic_excerpt_more_link',
189 sprintf(
190 ' ... <a class="gb-dynamic-read-more" href="%1$s" aria-label="%3$s">%2$s</a>',
191 esc_url( get_permalink( get_the_ID() ) ),
192 wp_kses_post( $attributes['customMoreLinkText'] ),
193 sprintf(
194 /* translators: Aria-label describing the read more button */
195 _x( 'More on %s', 'more on post title', 'generateblocks' ),
196 the_title_attribute( 'echo=0' )
197 )
198 )
199 );
200 };
201
202 add_filter(
203 'excerpt_more',
204 $filter_more_text,
205 100
206 );
207 }
208
209 $excerpt = get_the_excerpt( $source_id );
210
211 if ( isset( $filter_excerpt_length ) ) {
212 remove_filter(
213 'excerpt_length',
214 $filter_excerpt_length,
215 100
216 );
217 }
218
219 if ( isset( $filter_more_text ) ) {
220 remove_filter(
221 'excerpt_more',
222 $filter_more_text,
223 100
224 );
225 }
226
227 return $excerpt;
228 }
229
230 /**
231 * Get the requested post date.
232 *
233 * @param array $attributes The block attributes.
234 */
235 public static function get_post_date( $attributes ) {
236 $id = self::get_source_id( $attributes );
237
238 if ( ! $id ) {
239 return;
240 }
241
242 $updated_time = get_the_modified_time( 'U', $id );
243 $published_time = get_the_time( 'U', $id ) + 1800;
244
245 $post_date = sprintf(
246 '<time class="entry-date published" datetime="%1$s">%2$s</time>',
247 esc_attr( get_the_date( 'c', $id ) ),
248 esc_html( get_the_date( '', $id ) )
249 );
250
251 $is_updated_date = isset( $attributes['dateType'] ) && 'updated' === $attributes['dateType'];
252
253 if ( ! empty( $attributes['dateReplacePublished'] ) || $is_updated_date ) {
254 if ( $updated_time > $published_time ) {
255 $post_date = sprintf(
256 '<time class="entry-date updated-date" datetime="%1$s">%2$s</time>',
257 esc_attr( get_the_modified_date( 'c', $id ) ),
258 esc_html( get_the_modified_date( '', $id ) )
259 );
260 } elseif ( $is_updated_date ) {
261 // If we're showing the updated date but no updated date exists, don't display anything.
262 return '';
263 }
264 }
265
266 return $post_date;
267 }
268
269 /**
270 * Get the requested post meta.
271 *
272 * @param array $attributes The block attributes.
273 */
274 public static function get_post_meta( $attributes ) {
275 if ( isset( $attributes['metaFieldName'] ) ) {
276 $meta_value = get_post_meta( self::get_source_id( $attributes ), $attributes['metaFieldName'], true );
277 $value = (
278 is_string( $meta_value ) ||
279 is_integer( $meta_value ) ||
280 is_float( $meta_value )
281 ) ? $meta_value : '';
282
283 add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
284 $value = wp_kses_post( $value );
285 remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
286
287 $filtered = apply_filters(
288 'generateblocks_dynamic_content_post_meta',
289 $value,
290 self::get_source_id( $attributes ),
291 $attributes
292 );
293
294 // A filter (e.g. the ACF integration in Pro) can replace the sanitized value with
295 // raw field output, so sanitize again when the value changed. Non-string results
296 // (ACF image IDs consumed by get_dynamic_image_id()) pass through untouched, and
297 // an unchanged value skips the second pass entirely.
298 if ( is_string( $filtered ) && $filtered !== $value ) {
299 add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
300 $filtered = wp_kses_post( $filtered );
301 remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
302 }
303
304 return $filtered;
305 }
306 }
307
308 /**
309 * Get the requested author meta.
310 *
311 * @param array $attributes The block attributes.
312 */
313 public static function get_author_meta( $attributes ) {
314 if ( isset( $attributes['metaFieldName'] ) ) {
315 $id = self::get_source_id( $attributes );
316
317 if ( ! $id ) {
318 return;
319 }
320
321 $author_id = get_post_field( 'post_author', $id );
322
323 if ( ! $author_id ) {
324 return;
325 }
326
327 $value = self::get_user_data( $author_id, $attributes['metaFieldName'] );
328
329 // Author meta is rendered into the page body unescaped by the consumer, so sanitize
330 // it here. Mirror get_post_meta(): allow the same iframe embeds, strip scripts/handlers.
331 if ( is_string( $value ) ) {
332 add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
333 $value = wp_kses_post( $value );
334 remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
335 }
336
337 $filtered = apply_filters(
338 'generateblocks_dynamic_content_author_meta',
339 $value,
340 $author_id,
341 $attributes
342 );
343
344 // Same re-sanitize as get_post_meta(): a filter (e.g. the ACF integration in Pro)
345 // can replace the sanitized value with raw field output. Unchanged or non-string
346 // values pass through untouched.
347 if ( is_string( $filtered ) && $filtered !== $value ) {
348 add_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
349 $filtered = wp_kses_post( $filtered );
350 remove_filter( 'wp_kses_allowed_html', [ 'GenerateBlocks_Dynamic_Content', 'expand_allowed_html' ], 10, 2 );
351 }
352
353 return $filtered;
354 }
355 }
356
357 /**
358 * Get the number of comments.
359 *
360 * @param array $attributes The block attributes.
361 */
362 public static function get_comments_number( $attributes ) {
363 $id = self::get_source_id( $attributes );
364
365 if ( ! $id ) {
366 return;
367 }
368
369 if ( ! isset( $attributes['noCommentsText'] ) ) {
370 $attributes['noCommentsText'] = __( 'No comments', 'generateblocks' );
371 }
372
373 if ( ! post_password_required( $id ) && ( comments_open( $id ) || get_comments_number( $id ) ) ) {
374 if ( '' === $attributes['noCommentsText'] && get_comments_number( $id ) < 1 ) {
375 return $attributes['noCommentsText'];
376 }
377
378 $comments_text = get_comments_number_text(
379 $attributes['noCommentsText'],
380 ! empty( $attributes['singleCommentText'] ) ? $attributes['singleCommentText'] : __( '1 comment', 'generateblocks' ),
381 ! empty( $attributes['multipleCommentsText'] ) ? $attributes['multipleCommentsText'] : __( '% comments', 'generateblocks' )
382 );
383
384 return $comments_text;
385 } else {
386 return $attributes['noCommentsText'];
387 }
388 }
389
390 /**
391 * Get a list of terms.
392 *
393 * @param array $attributes The block attributes.
394 */
395 public static function get_terms( $attributes ) {
396 $id = self::get_source_id( $attributes );
397
398 if ( ! $id ) {
399 return;
400 }
401
402 $is_button = isset( $attributes['isButton'] );
403 $taxonomy = isset( $attributes['termTaxonomy'] ) ? $attributes['termTaxonomy'] : 'category';
404 $terms = get_the_terms( $id, $taxonomy );
405 $link_type = isset( $attributes['dynamicLinkType'] ) ? $attributes['dynamicLinkType'] : '';
406
407 if ( is_wp_error( $terms ) ) {
408 return;
409 }
410
411 $term_items = array();
412
413 foreach ( (array) $terms as $index => $term ) {
414 if ( ! isset( $term->name ) ) {
415 continue;
416 }
417
418 if ( $is_button ) {
419 $term_items[ $index ] = array(
420 'content' => wp_kses_post( $term->name ),
421 'attributes' => array(
422 'class' => 'post-term-item post-term-' . $term->slug,
423 ),
424 );
425 } else {
426 $term_items[ $index ] = sprintf(
427 '<span class="post-term-item term-%2$s">%1$s</span>',
428 wp_kses_post( $term->name ),
429 esc_attr( $term->slug )
430 );
431 }
432
433 if ( 'term-archives' === $link_type ) {
434 $term_link = get_term_link( $term, $taxonomy );
435
436 if ( ! is_wp_error( $term_link ) ) {
437 if ( $is_button ) {
438 $term_items[ $index ]['attributes']['href'] = esc_url( get_term_link( $term, $taxonomy ) );
439 } else {
440 $term_items[ $index ] = sprintf(
441 '<span class="post-term-item term-%3$s"><a href="%1$s">%2$s</a></span>',
442 esc_url( get_term_link( $term, $taxonomy ) ),
443 wp_kses_post( $term->name ),
444 esc_attr( $term->slug )
445 );
446 }
447 }
448 }
449 }
450
451 if ( empty( $term_items ) ) {
452 return '';
453 }
454
455 $sep = isset( $attributes['termSeparator'] ) ? $attributes['termSeparator'] : ', ';
456 $term_output = $is_button ? $term_items : implode( $sep, $term_items );
457
458 return $term_output;
459 }
460
461 /**
462 * Get the pagination numbers.
463 *
464 * @param array $attributes The block attributes.
465 * @param WP_Block $block Block instance.
466 */
467 public static function get_paginate_links( $attributes, $block ) {
468 $page_key = isset( $block->context['generateblocks/queryId'] ) ? 'query-' . $block->context['generateblocks/queryId'] . '-page' : 'query-page';
469 $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // phpcs:ignore -- No data processing happening.
470 $max_page = isset( $block->context['generateblocks/query']['pages'] ) ? (int) $block->context['generateblocks/query']['pages'] : 0;
471
472 global $wp_query;
473
474 if ( isset( $block->context['generateblocks/inheritQuery'] ) && $block->context['generateblocks/inheritQuery'] ) {
475 // Take into account if we have set a bigger `max page`
476 // than what the query has.
477 $total = ! $max_page || $max_page > $wp_query->max_num_pages ? $wp_query->max_num_pages : $max_page;
478 $paginate_args = array(
479 'prev_next' => false,
480 'total' => $total,
481 );
482 $links = paginate_links( $paginate_args );
483 } else {
484 $query_args = apply_filters(
485 'generateblocks_query_loop_args',
486 GenerateBlocks_Query_Loop::get_query_args( $block, $page ),
487 $attributes,
488 $block
489 );
490
491 $block_query = new WP_Query( $query_args );
492
493 // `paginate_links` works with the global $wp_query, so we have to
494 // temporarily switch it with our custom query.
495 $prev_wp_query = $wp_query;
496 $wp_query = $block_query; // phpcs:ignore -- No way around overwriting core global.
497 $total = ! $max_page || $max_page > $wp_query->max_num_pages ? $wp_query->max_num_pages : $max_page;
498
499 $paginate_args = array(
500 'base' => '%_%',
501 'format' => "?$page_key=%#%",
502 'current' => max( 1, $page ),
503 'total' => $total,
504 'prev_next' => false,
505 );
506
507 if ( 1 !== $page ) {
508 /**
509 * `paginate_links` doesn't use the provided `format` when the page is `1`.
510 * This is great for the main query as it removes the extra query params
511 * making the URL shorter, but in the case of multiple custom queries is
512 * problematic. It results in returning an empty link which ends up with
513 * a link to the current page.
514 *
515 * A way to address this is to add a `fake` query arg with no value that
516 * is the same for all custom queries. This way the link is not empty and
517 * preserves all the other existent query args.
518 *
519 * @see https://developer.wordpress.org/reference/functions/paginate_links/
520 *
521 * The proper fix of this should be in core. Track Ticket:
522 * @see https://core.trac.wordpress.org/ticket/53868
523 *
524 * TODO: After two WP versions (starting from the WP version the core patch landed),
525 * we should remove this and call `paginate_links` with the proper new arg.
526 */
527 $paginate_args['add_args'] = array( 'cst' => '' );
528 }
529
530 // We still need to preserve `paged` query param if exists, as is used
531 // for Queries that inherit from global context.
532 $paged = empty( $_GET['paged'] ) ? null : (int) $_GET['paged']; // phpcs:ignore -- No data processing happening.
533
534 if ( $paged ) {
535 $paginate_args['add_args'] = array( 'paged' => $paged );
536 }
537
538 $links = paginate_links( $paginate_args );
539 $wp_query = $prev_wp_query; // phpcs:ignore -- Restoring core global.
540 wp_reset_postdata(); // Restore original Post Data.
541 }
542
543 $doc = self::load_html( $links );
544
545 if ( ! $doc ) {
546 return;
547 }
548
549 $data = array();
550 $html_nodes = $doc->getElementsByTagName( '*' );
551
552 foreach ( $html_nodes as $index => $node ) {
553 $classes = $node->getAttribute( 'class' ) ? $node->getAttribute( 'class' ) : '';
554
555 if ( $node->getAttribute( 'aria-current' ) ) {
556 $classes = str_replace( 'current', 'gb-block-is-current', $classes );
557 }
558
559 // phpcs:ignore -- DOMDocument doesn't use snake-case.
560 if ( 'span' === $node->tagName || 'a' === $node->tagName ) {
561 $data[ $index ]['href'] = $node->getAttribute( 'href' ) ? $node->getAttribute( 'href' ) : '';
562 $data[ $index ]['aria-current'] = $node->getAttribute( 'aria-current' ) ? $node->getAttribute( 'aria-current' ) : '';
563 $data[ $index ]['class'] = $classes;
564
565 // phpcs:ignore -- DOMDocument doesn't use snake-case.
566 foreach ( $node->childNodes as $childNode ) {
567 $data[ $index ]['content'] = $doc->saveHTML( $childNode );
568 }
569 }
570 }
571
572 $paginate_links = array_values( $data );
573 $link_items = array();
574
575 foreach ( (array) $paginate_links as $index => $link ) {
576 $link_items[ $index ] = array(
577 'content' => $link['content'],
578 'attributes' => array(
579 'href' => $link['href'],
580 'aria-current' => $link['aria-current'],
581 'class' => $link['class'],
582 ),
583 );
584 }
585
586 if ( empty( $link_items ) ) {
587 return '';
588 }
589
590 return $link_items;
591 }
592
593 /**
594 * Get the dynamic image.
595 *
596 * @param array $attributes The block attributes.
597 * @param WP_Block $block Block instance.
598 */
599 public static function get_dynamic_image( $attributes, $block ) {
600 $id = self::get_dynamic_image_id( $attributes );
601
602 if ( ! $id ) {
603 $id = apply_filters( 'generateblocks_dynamic_image_fallback', '', $attributes, $block );
604
605 // If still empty return.
606 if ( ! $id ) {
607 return;
608 }
609 }
610
611 $classes = array(
612 'gb-image-' . $attributes['uniqueId'],
613 isset( $attributes['className'] ) ? $attributes['className'] : '',
614 );
615
616 if ( ! empty( $attributes['align'] ) ) {
617 $classes[] = 'align' . $attributes['align'];
618 }
619
620 $html_attributes = array(
621 'id' => isset( $attributes['anchor'] ) ? $attributes['anchor'] : '',
622 'class' => implode( ' ', $classes ),
623 );
624
625 $parsed_html_attributes = generateblocks_parse_attr( 'image', $html_attributes, $attributes, $block );
626 $parsed_html_attributes = array_map( 'trim', $parsed_html_attributes );
627 $parsed_html_attributes = array_filter( $parsed_html_attributes );
628
629 if ( ! empty( $attributes['dynamicContentType'] ) ) {
630 if ( 'author-avatar' === $attributes['dynamicContentType'] ) {
631 $author_id = self::get_source_author_id( $attributes );
632 return get_avatar(
633 $author_id,
634 $attributes['width'],
635 '',
636 '',
637 $parsed_html_attributes
638 );
639 }
640 }
641
642 if ( $id && ! is_numeric( $id ) ) {
643 // Our image ID isn't a number - must be a static URL.
644 $html_attributes['src'] = $id;
645
646 return sprintf(
647 '<img %s />',
648 generateblocks_attr( 'image', $html_attributes, $attributes, $block )
649 );
650 }
651
652 $dynamic_image = wp_get_attachment_image(
653 $id,
654 isset( $attributes['sizeSlug'] ) ? $attributes['sizeSlug'] : 'full',
655 false,
656 $parsed_html_attributes
657 );
658
659 if ( ! $dynamic_image ) {
660 return '';
661 }
662
663 return $dynamic_image;
664 }
665
666 /**
667 * Get our source ID.
668 *
669 * @param array $attributes The block attributes.
670 */
671 public static function get_source_id( $attributes ) {
672 $id = get_the_ID();
673
674 if (
675 isset( $attributes['dynamicSource'] ) &&
676 'current-post' !== $attributes['dynamicSource'] &&
677 isset( $attributes['postId'] )
678 ) {
679 $id = absint( $attributes['postId'] );
680 }
681
682 $image_content_types = array( 'caption', 'post-title', 'alt-text', 'image-description' );
683
684 if ( isset( $attributes['dynamicContentType'] ) ) {
685 if ( in_array( $attributes['dynamicContentType'], $image_content_types ) ) {
686 if ( isset( $attributes['dynamicImage'] ) ) {
687 $id = $attributes['dynamicImage'];
688 } elseif (
689 isset( $attributes['postId'] ) &&
690 isset( $attributes['postType'] ) &&
691 'attachment' === $attributes['postType']
692 ) {
693 // Use the saved post ID if we're working with a static image.
694 $id = absint( $attributes['postId'] );
695 }
696 }
697 }
698
699 return apply_filters(
700 'generateblocks_dynamic_source_id',
701 $id,
702 $attributes
703 );
704 }
705
706 /**
707 * Get the source post author id.
708 *
709 * @param array $attributes The block attributes.
710 *
711 * @return int|boolean
712 */
713 public static function get_source_author_id( $attributes ) {
714 $id = self::get_source_id( $attributes );
715
716 if ( ! $id ) {
717 return false;
718 }
719
720 $author_id = get_post_field( 'post_author', $id );
721
722 if ( ! $author_id ) {
723 return false;
724 }
725
726 return $author_id;
727 }
728
729 /**
730 * Get the dynamic image ID.
731 *
732 * @param array $attributes The block attributes.
733 */
734 public static function get_dynamic_image_id( $attributes ) {
735 if ( self::should_block_block_renderer_attribute_resolution( $attributes ) ) {
736 return '';
737 }
738
739 $id = self::get_source_id( $attributes );
740
741 if ( ! $id ) {
742 return;
743 }
744
745 if ( ! empty( $attributes['dynamicContentType'] ) ) {
746 if ( 'post-meta' === $attributes['dynamicContentType'] ) {
747 $id = self::get_post_meta( $attributes );
748 }
749
750 if ( 'featured-image' === $attributes['dynamicContentType'] ) {
751 $id = get_post_thumbnail_id( $id );
752 }
753 }
754
755 return $id;
756 }
757
758 /**
759 * Get the dynamic background image url.
760 *
761 * @param array $attributes The block attributes.
762 *
763 * @return int|boolean
764 */
765 public static function get_dynamic_background_image_url( $attributes ) {
766 $id = self::get_dynamic_image_id( $attributes );
767
768 if ( ! $id ) {
769 return false;
770 }
771
772 if ( empty( $attributes['dynamicContentType'] ) ) {
773 return;
774 }
775
776 if ( $id && ! is_numeric( $id ) ) {
777 // Our image ID isn't a number - must be a static URL.
778 return $id;
779 }
780
781 $url = wp_get_attachment_image_url(
782 $id,
783 isset( $attributes['bgImageSize'] ) ? $attributes['bgImageSize'] : 'full'
784 );
785
786 return apply_filters(
787 'generateblocks_dynamic_background_image_url',
788 $url,
789 $attributes
790 );
791 }
792
793 /**
794 * Get our dynamic URL.
795 *
796 * @param array $attributes The block attributes.
797 * @param object $block The block object.
798 */
799 public static function get_dynamic_url( $attributes, $block ) {
800 if ( self::should_block_block_renderer_attribute_resolution( $attributes ) ) {
801 return '';
802 }
803
804 $id = self::get_source_id( $attributes );
805 $author_id = get_post_field( 'post_author', $id );
806 $link_type = isset( $attributes['dynamicLinkType'] ) ? $attributes['dynamicLinkType'] : '';
807 $url = '';
808
809 if ( 'single-post' === $link_type ) {
810 $url = get_permalink( $id );
811 }
812
813 if ( 'single-image' === $link_type ) {
814 if ( ! empty( $attributes['dynamicContentType'] ) ) {
815 $image_id = self::get_dynamic_image_id( $attributes );
816
817 if ( $image_id && ! is_numeric( $image_id ) ) {
818 // Our image ID isn't a number - must be a static URL.
819 return is_string( $image_id ) ? esc_url_raw( $image_id ) : '';
820 }
821 } else {
822 $image_id = ! empty( $attributes['mediaId'] ) ? $attributes['mediaId'] : false;
823 }
824
825 if ( $image_id ) {
826 $url = wp_get_attachment_url( $image_id );
827 } elseif ( ! empty( $attributes['mediaUrl'] ) ) {
828 $url = $attributes['mediaUrl'];
829 }
830 }
831
832 if ( isset( $attributes['linkMetaFieldName'] ) ) {
833 if ( 'post-meta' === $link_type ) {
834 $url = get_post_meta( $id, $attributes['linkMetaFieldName'], true );
835
836 $url = apply_filters(
837 'generateblocks_dynamic_url_post_meta',
838 $url,
839 self::get_source_id( $attributes ),
840 $attributes
841 );
842
843 if ( isset( $attributes['linkMetaFieldType'] ) ) {
844 $url = $attributes['linkMetaFieldType'] . $url;
845 }
846 }
847
848 if ( 'author-meta' === $link_type ) {
849 $url = self::get_user_data( $author_id, $attributes['linkMetaFieldName'] );
850
851 $url = apply_filters(
852 'generateblocks_dynamic_url_author_meta',
853 $url,
854 $author_id,
855 $attributes
856 );
857
858 if ( isset( $attributes['linkMetaFieldType'] ) ) {
859 $url = $attributes['linkMetaFieldType'] . $url;
860 }
861 }
862 }
863
864 if ( 'author-email' === $link_type ) {
865 $url = self::get_user_data( $author_id, 'user_email' );
866
867 if ( isset( $attributes['linkMetaFieldType'] ) ) {
868 $url = $attributes['linkMetaFieldType'] . $url;
869 }
870 }
871
872 if ( 'author-archives' === $link_type ) {
873 $url = get_author_posts_url( $author_id );
874 }
875
876 if ( 'comments-area' === $link_type ) {
877 $url = get_comments_link( $id );
878 }
879
880 if ( 'pagination-next' === $link_type ) {
881 $page_key = isset( $block->context['generateblocks/queryId'] ) ? 'query-' . $block->context['generateblocks/queryId'] . '-page' : 'query-page';
882 $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // phpcs:ignore -- No data processing happening.
883 $max_page = isset( $block->context['generateblocks/query']['pages'] ) ? (int) $block->context['generateblocks/query']['pages'] : 0;
884
885 if ( isset( $block->context['generateblocks/inheritQuery'] ) && $block->context['generateblocks/inheritQuery'] ) {
886 global $wp_query, $paged;
887
888 if ( ! $max_page || $max_page > $wp_query->max_num_pages ) {
889 $max_page = $wp_query->max_num_pages;
890 }
891
892 if ( ! $paged ) {
893 $paged = 1; // phpcs:ignore -- Need to overrite global here.
894 }
895
896 $nextpage = (int) $paged + 1;
897
898 if ( $nextpage <= $max_page ) {
899 $url = next_posts( $max_page, false );
900 }
901 } elseif ( ! $max_page || $max_page > $page ) {
902 $query_args = apply_filters(
903 'generateblocks_query_loop_args',
904 GenerateBlocks_Query_Loop::get_query_args( $block, $page ),
905 $attributes,
906 $block
907 );
908
909 $custom_query = new WP_Query( $query_args );
910 $custom_query_max_pages = (int) $custom_query->max_num_pages;
911
912 if ( $custom_query_max_pages && $custom_query_max_pages !== $page ) {
913 $url = esc_url( add_query_arg( $page_key, $page + 1 ) );
914 }
915
916 wp_reset_postdata(); // Restore original Post Data.
917 }
918 }
919
920 if ( 'pagination-prev' === $link_type ) {
921 $page_key = isset( $block->context['generateblocks/queryId'] ) ? 'query-' . $block->context['generateblocks/queryId'] . '-page' : 'query-page';
922 $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // phpcs:ignore -- No data processing happening.
923
924 if ( isset( $block->context['generateblocks/inheritQuery'] ) && $block->context['generateblocks/inheritQuery'] ) {
925 global $paged;
926
927 if ( $paged > 1 ) {
928 $url = previous_posts( false );
929 }
930 } elseif ( 1 !== $page ) {
931 $url = esc_url( add_query_arg( $page_key, $page - 1 ) );
932 }
933 }
934
935 $url = apply_filters(
936 'generateblocks_dynamic_url_output',
937 $url,
938 $attributes,
939 $block
940 );
941
942 return is_string( $url ) ? esc_url_raw( $url ) : '';
943 }
944
945 /**
946 * Get user data.
947 *
948 * @param int $author_id The ID of the user.
949 * @param string|void $field The field to look up.
950 */
951 public static function get_user_data( $author_id, $field ) {
952 if ( ! $author_id ) {
953 return;
954 }
955
956 $data = get_user_meta( $author_id, $field, true );
957
958 if ( ! $data ) {
959 $user_data_names = array(
960 'user_nicename',
961 'user_email',
962 'display_name',
963 );
964
965 if ( in_array( $field, $user_data_names ) ) {
966 $user_data = get_userdata( $author_id );
967
968 if ( $user_data ) {
969 switch ( $field ) {
970 case 'user_nicename':
971 $data = $user_data->user_nicename;
972 break;
973
974 case 'user_email':
975 $data = $user_data->user_email;
976 break;
977
978 case 'display_name':
979 $data = $user_data->display_name;
980 break;
981 }
982 }
983 }
984 }
985
986 return $data;
987 }
988
989 /**
990 * Run HTML through DOMDocument so we can use parts of it
991 * when needed.
992 *
993 * @param string $content The content to run through DOMDocument.
994 */
995 public static function load_html( $content ) {
996 if ( ! class_exists( 'DOMDocument' ) ) {
997 return;
998 }
999
1000 $doc = new DOMDocument();
1001
1002 // Enable user error handling for the HTML parsing. HTML5 elements aren't
1003 // supported (as of PHP 7.4) and There's no way to guarantee that the markup
1004 // is valid anyway, so we're just going to ignore all errors in parsing.
1005 // Nested heading elements will still be parsed.
1006 // The lack of HTML5 support is a libxml2 issue:
1007 // https://bugzilla.gnome.org/show_bug.cgi?id=761534.
1008 libxml_use_internal_errors( true );
1009
1010 // Parse the post content into an HTML document.
1011 // Ensure UTF-8 encoding.
1012 // https://stackoverflow.com/a/37834812.
1013 $doc->loadHTML(
1014 sprintf(
1015 '<html><head><meta http-equiv="Content-Type" content="text/html; charset=%s"></head><body>%s</body></html>',
1016 esc_attr( get_bloginfo( 'charset' ) ),
1017 $content
1018 )
1019 );
1020
1021 // We're done parsing, so we can disable user error handling. This also
1022 // clears any existing errors, which helps avoid a memory leak.
1023 libxml_use_internal_errors( false );
1024
1025 return $doc;
1026 }
1027
1028 /**
1029 * Extracts the icon element from our content.
1030 * This is useful when using icons in dynamic blocks.
1031 *
1032 * @param string $content The content to search through.
1033 */
1034 public static function get_icon_html( $content ) {
1035 $doc = self::load_html( $content );
1036
1037 if ( ! $doc ) {
1038 return;
1039 }
1040
1041 $icon_html = '';
1042 $html_nodes = $doc->getElementsByTagName( 'span' );
1043
1044 foreach ( $html_nodes as $node ) {
1045 if ( 'gb-icon' === $node->getAttribute( 'class' ) ) {
1046 $icon_html = $doc->saveHTML( $node );
1047 }
1048 }
1049
1050 return $icon_html;
1051 }
1052
1053 /**
1054 * Get the dynamic image caption.
1055 *
1056 * @param array $attributes The block attributes.
1057 * @param object $block The block object.
1058 */
1059 public static function get_image_caption( $attributes, $block ) {
1060 $id = self::get_source_id( $attributes );
1061
1062 if ( ! $id ) {
1063 return;
1064 }
1065
1066 return wp_get_attachment_caption( $id );
1067 }
1068
1069 /**
1070 * Get the image alt text.
1071 *
1072 * @param array $attributes The block attributes.
1073 * @param object $block The block object.
1074 */
1075 public static function get_image_alt_text( $attributes, $block ) {
1076 $id = self::get_source_id( $attributes );
1077
1078 if ( ! $id ) {
1079 return '';
1080 }
1081
1082 return get_post_meta( $id, '_wp_attachment_image_alt', true );
1083 }
1084
1085 /**
1086 * Get the image description.
1087 *
1088 * @param array $attributes The block attributes.
1089 * @param object $block The block object.
1090 */
1091 public static function get_image_description( $attributes, $block ) {
1092 $id = self::get_source_id( $attributes );
1093
1094 if ( ! $id ) {
1095 return '';
1096 }
1097
1098 $media = get_post( $id );
1099 $status = $media->post_status ?? '';
1100
1101 if ( 'publish' !== $status && ! current_user_can( 'read_private_posts' ) ) {
1102 return '';
1103 }
1104
1105 return isset( $media ) ? $media->post_content : '';
1106 }
1107
1108 /**
1109 * Extracts the static content the user has entered.
1110 * This is useful when using dynamic links with static content.
1111 *
1112 * @param string $content The content to search through.
1113 */
1114 public static function get_static_content( $content ) {
1115 $doc = self::load_html( $content );
1116
1117 if ( ! $doc ) {
1118 return;
1119 }
1120
1121 $static_content = '';
1122 $html_nodes = $doc->getElementsByTagName( '*' );
1123
1124 foreach ( $html_nodes as $node ) {
1125 $classes = explode( ' ', $node->getAttribute( 'class' ) );
1126
1127 if (
1128 in_array( 'gb-button-text', $classes ) ||
1129 in_array( 'gb-headline-text', $classes ) ||
1130 in_array( 'gb-block-image', $classes )
1131 ) {
1132 // Captions are added dynamically in class-image.php, so we can remove
1133 // the static one here if it exists.
1134 if ( in_array( 'gb-block-image', $classes ) ) {
1135 $figcaptions = $node->getElementsByTagName( 'figcaption' );
1136
1137 if ( ! empty( $figcaptions ) ) {
1138 foreach ( $figcaptions as $figcaption ) {
1139 // phpcs:ignore -- DOMDocument doesn't use snake-case.
1140 $figcaption->parentNode->removeChild( $figcaption );
1141 }
1142 }
1143 }
1144
1145 // phpcs:ignore -- DOMDocument doesn't use snake-case.
1146 foreach ( $node->childNodes as $childNode ) {
1147 $static_content .= $doc->saveHTML( $childNode );
1148 }
1149 }
1150 }
1151
1152 return $static_content;
1153 }
1154
1155 /**
1156 * Set our dynamic background image.
1157 *
1158 * @param string $url Existing background image URL.
1159 * @param array $settings Block settings.
1160 */
1161 public function set_dynamic_background_image( $url, $settings ) {
1162 if ( $settings['useDynamicData'] && '' !== $settings['dynamicContentType'] ) {
1163 $dynamic_image_url = self::get_dynamic_background_image_url( $settings );
1164
1165 if ( $dynamic_image_url ) {
1166 $url = $dynamic_image_url;
1167 }
1168 }
1169
1170 return $url;
1171 }
1172
1173 /**
1174 * Add defaults for our Query settings.
1175 *
1176 * @param array $defaults Block defaults.
1177 */
1178 public function add_block_defaults( $defaults ) {
1179 $defaults['container']['useDynamicData'] = false;
1180 $defaults['container']['dynamicContentType'] = '';
1181 $defaults['container']['dynamicLinkType'] = '';
1182
1183 return $defaults;
1184 }
1185
1186 /**
1187 * Update button count depending on dynamic content.
1188 *
1189 * @param int $button_count How many buttons the block container has.
1190 * @param array $attributes The block attributes.
1191 * @param object $block The block data.
1192 */
1193 public function update_button_count( $button_count, $attributes, $block ) {
1194 $inner_blocks = $block->parsed_block['innerBlocks'];
1195
1196 foreach ( (array) $inner_blocks as $inner_block ) {
1197 $block_attributes = $inner_block['attrs'];
1198
1199 // Remove button from count if it has no dynamic content.
1200 if ( ! empty( $block_attributes['dynamicContentType'] ) && ! self::get_content( $block_attributes, $block ) ) {
1201 $button_count--;
1202 }
1203 }
1204
1205 return $button_count;
1206 }
1207
1208 /**
1209 * Expand the wp_kses_post sanitization function to allow iframe HTML tags
1210 *
1211 * @param array $tags The allowed tags, attributes, and/or attribute values.
1212 * @param string $context Context to judge allowed tags by. Allowed values are 'post'.
1213 * @return array
1214 */
1215 public static function expand_allowed_html( $tags, $context ) {
1216 if ( ! isset( $tags['iframe'] ) ) {
1217 $tags['iframe'] = [
1218 'src' => true,
1219 'height' => true,
1220 'width' => true,
1221 'frameborder' => true,
1222 'allowfullscreen' => true,
1223 'title' => true,
1224 ];
1225 }
1226
1227 $tags = apply_filters( 'generateblocks_dynamic_content_allowed_html', $tags, $context );
1228
1229 return $tags;
1230 }
1231
1232 /**
1233 * Detect whether legacy v1 dynamic content attributes exist in serialized content.
1234 *
1235 * @since 2.2.0
1236 *
1237 * @param string $content Serialized post content.
1238 * @return bool
1239 */
1240 public static function content_has_dynamic_attribute_markers( $content ) {
1241 $content = GenerateBlocks_Dynamic_Tag_Security::normalize_serialized_content( $content );
1242
1243 if ( '' === $content ) {
1244 return false;
1245 }
1246
1247 $markers = [
1248 '"useDynamicData":true',
1249 '"dynamicLinkType":"post-meta"',
1250 '"dynamicLinkType":"author-meta"',
1251 '"dynamicContentType":"author-email"',
1252 '"dynamicLinkType":"author-email"',
1253 ];
1254
1255 foreach ( $markers as $marker ) {
1256 if ( false !== strpos( $content, $marker ) ) {
1257 return true;
1258 }
1259 }
1260
1261 /**
1262 * The literal markers above are formatting-sensitive: block-comment JSON submitted via
1263 * the REST API (e.g. `"useDynamicData" : true`) is not re-serialized until after the
1264 * rest_pre_insert validation runs, so a reformatted attribute can slip past the substring
1265 * scan. Fall back to inspecting the parsed block attributes so JSON whitespace or key
1266 * order cannot evade detection.
1267 */
1268 if (
1269 function_exists( 'has_blocks' ) && has_blocks( $content ) &&
1270 function_exists( 'parse_blocks' )
1271 ) {
1272 return self::blocks_have_dynamic_attribute_markers( parse_blocks( $content ) );
1273 }
1274
1275 return false;
1276 }
1277
1278 /**
1279 * Recursively determine whether any parsed block carries legacy dynamic content attributes.
1280 *
1281 * Mirrors the markers in content_has_dynamic_attribute_markers() but works on decoded
1282 * attributes, so it is immune to the JSON formatting of the serialized block comment.
1283 *
1284 * @since 2.4.0
1285 *
1286 * @param array $blocks Parsed block list.
1287 * @return bool
1288 */
1289 protected static function blocks_have_dynamic_attribute_markers( $blocks ) {
1290 foreach ( (array) $blocks as $block ) {
1291 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
1292
1293 if ( ! empty( $attrs['useDynamicData'] ) ) {
1294 return true;
1295 }
1296
1297 if (
1298 ! empty( $attrs['dynamicLinkType'] ) &&
1299 in_array( $attrs['dynamicLinkType'], [ 'post-meta', 'author-meta', 'author-email' ], true )
1300 ) {
1301 return true;
1302 }
1303
1304 if ( isset( $attrs['dynamicContentType'] ) && 'author-email' === $attrs['dynamicContentType'] ) {
1305 return true;
1306 }
1307
1308 // A block that points dynamic data or a dynamic link at an explicitly chosen post or
1309 // attachment is a dynamic-data marker too, even when no other marker is present
1310 // (e.g. a caption sourced from an attachment postId, a dynamicImage, a single-image
1311 // mediaId, or dynamicLinkType:single-post). Uses the same resolver the renderer
1312 // mirrors so marker detection and rendering never drift apart.
1313 if ( ! empty( self::get_attribute_referenced_post_ids( $attrs ) ) ) {
1314 return true;
1315 }
1316
1317 if (
1318 ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) &&
1319 self::blocks_have_dynamic_attribute_markers( $block['innerBlocks'] )
1320 ) {
1321 return true;
1322 }
1323 }
1324
1325 return false;
1326 }
1327
1328 /**
1329 * Resolve the explicit post/attachment IDs a block's dynamic data and links would read.
1330 *
1331 * Mirrors get_source_id() and get_dynamic_url() source selection so marker detection
1332 * sees the SAME referenced object the renderer does. The current-post fallback (get_the_ID)
1333 * is intentionally not collected — the visitor is already authorized to see the post being
1334 * rendered; only an explicitly chosen post/attachment can cross to data the author can't read.
1335 *
1336 * @since 2.4.0
1337 *
1338 * @param array $attrs Block attributes.
1339 * @return array<int, int> Unique, non-zero referenced IDs.
1340 */
1341 protected static function get_attribute_referenced_post_ids( $attrs ) {
1342 if ( ! is_array( $attrs ) ) {
1343 return [];
1344 }
1345
1346 $content_type = isset( $attrs['dynamicContentType'] ) ? (string) $attrs['dynamicContentType'] : '';
1347 $link_type = isset( $attrs['dynamicLinkType'] ) ? (string) $attrs['dynamicLinkType'] : '';
1348 $has_dynamic_data = ! empty( $attrs['useDynamicData'] );
1349
1350 // Legacy dynamic attributes are mostly inert without useDynamicData: the block render
1351 // callbacks return static content before calling get_content()/get_dynamic_url(). The
1352 // one exception is the remove-if-empty guard in
1353 // GenerateBlocks_Render_Block::filter_rendered_blocks(), which calls get_dynamic_url()
1354 // whenever dynamicLinkType + dynamicLinkRemoveIfEmpty are set — even with dynamic data
1355 // off — because existing sites rely on that hiding behavior. That read path must be
1356 // validated too, or a block saved with useDynamicData off could probe an unreadable
1357 // post via the rendered/hidden result. Keep this condition in sync with
1358 // filter_rendered_blocks().
1359 $reads_link_without_dynamic_data = '' !== $link_type && ! empty( $attrs['dynamicLinkRemoveIfEmpty'] );
1360
1361 if ( ! $has_dynamic_data && ! $reads_link_without_dynamic_data ) {
1362 return [];
1363 }
1364
1365 // Nothing dynamic renders (and no source id is read) unless a content or link type is set.
1366 if ( '' === $content_type && '' === $link_type ) {
1367 return [];
1368 }
1369
1370 $source = isset( $attrs['dynamicSource'] ) ? (string) $attrs['dynamicSource'] : '';
1371 $explicit_source = '' !== $source && 'current-post' !== $source;
1372 $post_type = isset( $attrs['postType'] ) ? (string) $attrs['postType'] : '';
1373 $image_types = [ 'caption', 'post-title', 'alt-text', 'image-description' ];
1374 $ids = [];
1375
1376 // Pagination content/link types build their URL from query/page state and ignore the
1377 // source post, so a stale postId on them is not a reference. Every other source-aware
1378 // content/link type resolves through get_source_id(); keep this list in sync with the
1379 // pagination branches of get_content()/get_dynamic_url().
1380 $query_derived_types = [ 'pagination-numbers', 'pagination-next', 'pagination-prev' ];
1381
1382 // Mirror get_source_id(): the explicit object the content path reads. For image content
1383 // types a dynamicImage (or a saved attachment postId) overrides the source REGARDLESS of
1384 // dynamicSource, which is why a current-post source still reaches another object's data.
1385 // Content paths are only reachable with useDynamicData on — every block render callback
1386 // returns static content before get_content() otherwise.
1387 if ( $has_dynamic_data && '' !== $content_type ) {
1388 if ( in_array( $content_type, $image_types, true ) ) {
1389 if ( isset( $attrs['dynamicImage'] ) ) {
1390 $ids[] = $attrs['dynamicImage'];
1391 } elseif ( isset( $attrs['postId'] ) && 'attachment' === $post_type ) {
1392 $ids[] = $attrs['postId'];
1393 } elseif ( $explicit_source && isset( $attrs['postId'] ) ) {
1394 $ids[] = $attrs['postId'];
1395 }
1396 } elseif (
1397 $explicit_source &&
1398 isset( $attrs['postId'] ) &&
1399 ! in_array( $content_type, $query_derived_types, true )
1400 ) {
1401 $ids[] = $attrs['postId'];
1402 }
1403 }
1404
1405 // Mirror get_dynamic_url(): the explicit object a dynamic link reads. Reached either
1406 // with useDynamicData on (block render callbacks) or via the remove-if-empty guard in
1407 // filter_rendered_blocks(), which reads the link with dynamic data off.
1408 if ( '' !== $link_type ) {
1409 if ( 'single-image' === $link_type ) {
1410 if ( '' !== $content_type && isset( $attrs['dynamicImage'] ) ) {
1411 $ids[] = $attrs['dynamicImage'];
1412 } elseif ( '' === $content_type && isset( $attrs['mediaId'] ) ) {
1413 $ids[] = $attrs['mediaId'];
1414 }
1415 } elseif (
1416 $explicit_source &&
1417 isset( $attrs['postId'] ) &&
1418 ! in_array( $link_type, $query_derived_types, true )
1419 ) {
1420 // single-post / post-meta / author-* / comments-area resolve via get_source_id();
1421 // pagination links are excluded above because they ignore postId.
1422 $ids[] = $attrs['postId'];
1423 }
1424 }
1425
1426 $normalized = [];
1427
1428 foreach ( $ids as $id ) {
1429 $id = absint( $id );
1430
1431 if ( $id ) {
1432 $normalized[ $id ] = $id;
1433 }
1434 }
1435
1436 return array_values( $normalized );
1437 }
1438
1439 /**
1440 * Whether legacy attributes should be blanked for the current render source.
1441 *
1442 * @since 2.4.0
1443 *
1444 * @param array $attributes Block attributes.
1445 * @return bool
1446 */
1447 protected static function should_block_block_renderer_attribute_resolution( $attributes ) {
1448 unset( $attributes );
1449
1450 if (
1451 class_exists( 'GenerateBlocks_Dynamic_Tag_Security' ) &&
1452 method_exists( 'GenerateBlocks_Dynamic_Tag_Security', 'should_suppress_dynamic_data' ) &&
1453 GenerateBlocks_Dynamic_Tag_Security::should_suppress_dynamic_data()
1454 ) {
1455 return true;
1456 }
1457
1458 if (
1459 class_exists( 'GenerateBlocks_Dynamic_Tag_Security' ) &&
1460 method_exists( 'GenerateBlocks_Dynamic_Tag_Security', 'is_non_trusted_block_renderer_rest_request' ) &&
1461 GenerateBlocks_Dynamic_Tag_Security::is_non_trusted_block_renderer_rest_request()
1462 ) {
1463 return true;
1464 }
1465
1466 return false;
1467 }
1468
1469 /*
1470 * -------------------------------------------------------------------------
1471 * Removed 2.2.x validation API — fatal-prevention stubs.
1472 *
1473 * These public methods shipped in stable releases; no-op stubs remain so an
1474 * external caller can never fatal on update. See the matching section in
1475 * GenerateBlocks_Dynamic_Tag_Security.
1476 * -------------------------------------------------------------------------
1477 */
1478
1479 /**
1480 * Stub for the removed 2.2.x legacy-attribute validator.
1481 *
1482 * @deprecated 2.4.0 No-op stub; see the save gate (save_is_restricted()).
1483 *
1484 * @param string $content Serialized post content.
1485 * @return true
1486 */
1487 public static function validate_dynamic_content_attributes( $content ) {
1488 unset( $content );
1489
1490 return true;
1491 }
1492
1493 /**
1494 * Stub for the removed 2.2.x violation collector.
1495 *
1496 * @deprecated 2.4.0 No-op stub; the violation model no longer exists.
1497 *
1498 * @param string $content Serialized post content.
1499 * @return array Always empty.
1500 */
1501 public static function get_dynamic_attribute_violation_items( $content ) {
1502 unset( $content );
1503
1504 return [];
1505 }
1506 }
1507
1508 GenerateBlocks_Dynamic_Content::get_instance();
1509