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