PluginProbe ʕ •ᴥ•ʔ
GenerateBlocks / trunk
GenerateBlocks vtrunk
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 4 weeks ago dynamic-tags 4 weeks ago pattern-library 4 weeks ago utils 2 years ago class-do-css.php 3 years ago class-dynamic-content.php 4 weeks ago class-dynamic-tag-security.php 7 months ago class-enqueue-css.php 1 year ago class-legacy-attributes.php 4 years ago class-map-deprecated-attributes.php 3 years ago class-meta-handler.php 3 months ago class-plugin-update.php 1 year ago class-query-loop.php 2 years ago class-query-utils.php 4 weeks 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 4 weeks ago general.php 4 weeks ago
class-dynamic-content.php
1384 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 is_string( $image_id ) ? esc_url_raw( $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 $url = apply_filters(
893 'generateblocks_dynamic_url_output',
894 $url,
895 $attributes,
896 $block
897 );
898
899 return is_string( $url ) ? esc_url_raw( $url ) : '';
900 }
901
902 /**
903 * Get user data.
904 *
905 * @param int $author_id The ID of the user.
906 * @param string|void $field The field to look up.
907 */
908 public static function get_user_data( $author_id, $field ) {
909 if ( ! $author_id ) {
910 return;
911 }
912
913 $data = get_user_meta( $author_id, $field, true );
914
915 if ( ! $data ) {
916 $user_data_names = array(
917 'user_nicename',
918 'user_email',
919 'display_name',
920 );
921
922 if ( in_array( $field, $user_data_names ) ) {
923 $user_data = get_userdata( $author_id );
924
925 if ( $user_data ) {
926 switch ( $field ) {
927 case 'user_nicename':
928 $data = $user_data->user_nicename;
929 break;
930
931 case 'user_email':
932 $data = $user_data->user_email;
933 break;
934
935 case 'display_name':
936 $data = $user_data->display_name;
937 break;
938 }
939 }
940 }
941 }
942
943 return $data;
944 }
945
946 /**
947 * Run HTML through DOMDocument so we can use parts of it
948 * when needed.
949 *
950 * @param string $content The content to run through DOMDocument.
951 */
952 public static function load_html( $content ) {
953 if ( ! class_exists( 'DOMDocument' ) ) {
954 return;
955 }
956
957 $doc = new DOMDocument();
958
959 // Enable user error handling for the HTML parsing. HTML5 elements aren't
960 // supported (as of PHP 7.4) and There's no way to guarantee that the markup
961 // is valid anyway, so we're just going to ignore all errors in parsing.
962 // Nested heading elements will still be parsed.
963 // The lack of HTML5 support is a libxml2 issue:
964 // https://bugzilla.gnome.org/show_bug.cgi?id=761534.
965 libxml_use_internal_errors( true );
966
967 // Parse the post content into an HTML document.
968 // Ensure UTF-8 encoding.
969 // https://stackoverflow.com/a/37834812.
970 $doc->loadHTML(
971 sprintf(
972 '<html><head><meta http-equiv="Content-Type" content="text/html; charset=%s"></head><body>%s</body></html>',
973 esc_attr( get_bloginfo( 'charset' ) ),
974 $content
975 )
976 );
977
978 // We're done parsing, so we can disable user error handling. This also
979 // clears any existing errors, which helps avoid a memory leak.
980 libxml_use_internal_errors( false );
981
982 return $doc;
983 }
984
985 /**
986 * Extracts the icon element from our content.
987 * This is useful when using icons in dynamic blocks.
988 *
989 * @param string $content The content to search through.
990 */
991 public static function get_icon_html( $content ) {
992 $doc = self::load_html( $content );
993
994 if ( ! $doc ) {
995 return;
996 }
997
998 $icon_html = '';
999 $html_nodes = $doc->getElementsByTagName( 'span' );
1000
1001 foreach ( $html_nodes as $node ) {
1002 if ( 'gb-icon' === $node->getAttribute( 'class' ) ) {
1003 $icon_html = $doc->saveHTML( $node );
1004 }
1005 }
1006
1007 return $icon_html;
1008 }
1009
1010 /**
1011 * Get the dynamic image caption.
1012 *
1013 * @param array $attributes The block attributes.
1014 * @param object $block The block object.
1015 */
1016 public static function get_image_caption( $attributes, $block ) {
1017 $id = self::get_source_id( $attributes );
1018
1019 if ( ! $id ) {
1020 return;
1021 }
1022
1023 return wp_get_attachment_caption( $id );
1024 }
1025
1026 /**
1027 * Get the image alt text.
1028 *
1029 * @param array $attributes The block attributes.
1030 * @param object $block The block object.
1031 */
1032 public static function get_image_alt_text( $attributes, $block ) {
1033 $id = self::get_source_id( $attributes );
1034
1035 if ( ! $id ) {
1036 return '';
1037 }
1038
1039 return get_post_meta( $id, '_wp_attachment_image_alt', true );
1040 }
1041
1042 /**
1043 * Get the image description.
1044 *
1045 * @param array $attributes The block attributes.
1046 * @param object $block The block object.
1047 */
1048 public static function get_image_description( $attributes, $block ) {
1049 $id = self::get_source_id( $attributes );
1050
1051 if ( ! $id ) {
1052 return '';
1053 }
1054
1055 $media = get_post( $id );
1056 $status = $media->post_status ?? '';
1057
1058 if ( 'publish' !== $status && ! current_user_can( 'read_private_posts' ) ) {
1059 return '';
1060 }
1061
1062 return isset( $media ) ? $media->post_content : '';
1063 }
1064
1065 /**
1066 * Extracts the static content the user has entered.
1067 * This is useful when using dynamic links with static content.
1068 *
1069 * @param string $content The content to search through.
1070 */
1071 public static function get_static_content( $content ) {
1072 $doc = self::load_html( $content );
1073
1074 if ( ! $doc ) {
1075 return;
1076 }
1077
1078 $static_content = '';
1079 $html_nodes = $doc->getElementsByTagName( '*' );
1080
1081 foreach ( $html_nodes as $node ) {
1082 $classes = explode( ' ', $node->getAttribute( 'class' ) );
1083
1084 if (
1085 in_array( 'gb-button-text', $classes ) ||
1086 in_array( 'gb-headline-text', $classes ) ||
1087 in_array( 'gb-block-image', $classes )
1088 ) {
1089 // Captions are added dynamically in class-image.php, so we can remove
1090 // the static one here if it exists.
1091 if ( in_array( 'gb-block-image', $classes ) ) {
1092 $figcaptions = $node->getElementsByTagName( 'figcaption' );
1093
1094 if ( ! empty( $figcaptions ) ) {
1095 foreach ( $figcaptions as $figcaption ) {
1096 // phpcs:ignore -- DOMDocument doesn't use snake-case.
1097 $figcaption->parentNode->removeChild( $figcaption );
1098 }
1099 }
1100 }
1101
1102 // phpcs:ignore -- DOMDocument doesn't use snake-case.
1103 foreach ( $node->childNodes as $childNode ) {
1104 $static_content .= $doc->saveHTML( $childNode );
1105 }
1106 }
1107 }
1108
1109 return $static_content;
1110 }
1111
1112 /**
1113 * Set our dynamic background image.
1114 *
1115 * @param string $url Existing background image URL.
1116 * @param array $settings Block settings.
1117 */
1118 public function set_dynamic_background_image( $url, $settings ) {
1119 if ( $settings['useDynamicData'] && '' !== $settings['dynamicContentType'] ) {
1120 $dynamic_image_url = self::get_dynamic_background_image_url( $settings );
1121
1122 if ( $dynamic_image_url ) {
1123 $url = $dynamic_image_url;
1124 }
1125 }
1126
1127 return $url;
1128 }
1129
1130 /**
1131 * Add defaults for our Query settings.
1132 *
1133 * @param array $defaults Block defaults.
1134 */
1135 public function add_block_defaults( $defaults ) {
1136 $defaults['container']['useDynamicData'] = false;
1137 $defaults['container']['dynamicContentType'] = '';
1138 $defaults['container']['dynamicLinkType'] = '';
1139
1140 return $defaults;
1141 }
1142
1143 /**
1144 * Update button count depending on dynamic content.
1145 *
1146 * @param int $button_count How many buttons the block container has.
1147 * @param array $attributes The block attributes.
1148 * @param object $block The block data.
1149 */
1150 public function update_button_count( $button_count, $attributes, $block ) {
1151 $inner_blocks = $block->parsed_block['innerBlocks'];
1152
1153 foreach ( (array) $inner_blocks as $inner_block ) {
1154 $block_attributes = $inner_block['attrs'];
1155
1156 // Remove button from count if it has no dynamic content.
1157 if ( ! empty( $block_attributes['dynamicContentType'] ) && ! self::get_content( $block_attributes, $block ) ) {
1158 $button_count--;
1159 }
1160 }
1161
1162 return $button_count;
1163 }
1164
1165 /**
1166 * Expand the wp_kses_post sanitization function to allow iframe HTML tags
1167 *
1168 * @param array $tags The allowed tags, attributes, and/or attribute values.
1169 * @param string $context Context to judge allowed tags by. Allowed values are 'post'.
1170 * @return array
1171 */
1172 public static function expand_allowed_html( $tags, $context ) {
1173 if ( ! isset( $tags['iframe'] ) ) {
1174 $tags['iframe'] = [
1175 'src' => true,
1176 'height' => true,
1177 'width' => true,
1178 'frameborder' => true,
1179 'allowfullscreen' => true,
1180 'title' => true,
1181 ];
1182 }
1183
1184 $tags = apply_filters( 'generateblocks_dynamic_content_allowed_html', $tags, $context );
1185
1186 return $tags;
1187 }
1188
1189 /**
1190 * Detect whether legacy v1 dynamic content attributes exist in serialized content.
1191 *
1192 * @since 2.2.0
1193 *
1194 * @param string $content Serialized post content.
1195 * @return bool
1196 */
1197 public static function content_has_dynamic_attribute_markers( $content ) {
1198 $content = GenerateBlocks_Dynamic_Tag_Security::normalize_serialized_content( $content );
1199
1200 if ( '' === $content ) {
1201 return false;
1202 }
1203
1204 $markers = [
1205 '"useDynamicData":true',
1206 '"dynamicLinkType":"post-meta"',
1207 '"dynamicLinkType":"author-meta"',
1208 '"dynamicContentType":"author-email"',
1209 '"dynamicLinkType":"author-email"',
1210 ];
1211
1212 foreach ( $markers as $marker ) {
1213 if ( false !== strpos( $content, $marker ) ) {
1214 return true;
1215 }
1216 }
1217
1218 return false;
1219 }
1220
1221 /**
1222 * Validate dynamic content block attributes (metaFieldName/linkMetaFieldName) for legacy v1 blocks.
1223 *
1224 * @since 2.2.0
1225 *
1226 * @param string $content Serialized post content.
1227 * @return true|WP_Error
1228 */
1229 public static function validate_dynamic_content_attributes( $content ) {
1230 $violations = self::get_dynamic_attribute_violation_items( $content );
1231
1232 if ( empty( $violations ) ) {
1233 return true;
1234 }
1235
1236 $first = reset( $violations );
1237
1238 return $first['error'];
1239 }
1240
1241 /**
1242 * Retrieve dynamic attribute violation items.
1243 *
1244 * @since 2.2.0
1245 *
1246 * @param string $content Serialized post content.
1247 * @return array<int, array{type:string,field:string,error:WP_Error}>
1248 */
1249 public static function get_dynamic_attribute_violation_items( $content ) {
1250 $content = GenerateBlocks_Dynamic_Tag_Security::normalize_serialized_content( $content );
1251
1252 if ( ! self::content_has_dynamic_attribute_markers( $content ) ) {
1253 return [];
1254 }
1255
1256 if ( ! function_exists( 'has_blocks' ) || ! has_blocks( $content ) ) {
1257 return [];
1258 }
1259
1260 if ( ! function_exists( 'parse_blocks' ) ) {
1261 return [];
1262 }
1263
1264 $blocks = parse_blocks( $content );
1265
1266 if ( empty( $blocks ) || ! is_array( $blocks ) ) {
1267 return [];
1268 }
1269
1270 return self::collect_dynamic_attribute_block_violations( $blocks );
1271 }
1272
1273 /**
1274 * Recursively collect parsed block violations for unsafe meta usage.
1275 *
1276 * @since 2.2.0
1277 *
1278 * @param array $blocks Parsed block list.
1279 * @return array<int, array{type:string,field:string,error:WP_Error}>
1280 */
1281 protected static function collect_dynamic_attribute_block_violations( $blocks ) {
1282 $violations = [];
1283
1284 foreach ( $blocks as $block ) {
1285 $violations = array_merge(
1286 $violations,
1287 self::collect_single_block_dynamic_attribute_violations( $block )
1288 );
1289
1290 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
1291 $violations = array_merge(
1292 $violations,
1293 self::collect_dynamic_attribute_block_violations( $block['innerBlocks'] )
1294 );
1295 }
1296 }
1297
1298 return $violations;
1299 }
1300
1301 /**
1302 * Validate a single block's dynamic attributes.
1303 *
1304 * @since 2.2.0
1305 *
1306 * @param array $block Parsed block data.
1307 * @return array<int, array{type:string,field:string,error:WP_Error}>
1308 */
1309 protected static function collect_single_block_dynamic_attribute_violations( $block ) {
1310 $violations = [];
1311
1312 if ( empty( $block['attrs'] ) || ! is_array( $block['attrs'] ) ) {
1313 return $violations;
1314 }
1315
1316 $attrs = $block['attrs'];
1317 $should_validate_user_meta = GenerateBlocks_Dynamic_Tag_Security::should_validate_user_meta_fields();
1318
1319 if ( ! empty( $attrs['useDynamicData'] ) ) {
1320 $field_name = isset( $attrs['metaFieldName'] ) ? trim( (string) $attrs['metaFieldName'] ) : '';
1321 $type = isset( $attrs['dynamicContentType'] ) ? (string) $attrs['dynamicContentType'] : '';
1322
1323 if ( 'author-email' === $type ) {
1324 $field_name = 'user_email';
1325 }
1326
1327 if ( $field_name ) {
1328 if ( in_array( $type, [ 'author-meta', 'author-email' ], true ) ) {
1329 $result = $should_validate_user_meta ? GenerateBlocks_Dynamic_Tag_Security::validate_user_meta_field_name( $field_name ) : true;
1330 $type_key = 'user_meta';
1331 } elseif ( 'post-meta' === $type ) {
1332 $result = GenerateBlocks_Dynamic_Tag_Security::validate_post_meta_field_name( $field_name );
1333 $type_key = 'post_meta';
1334 } else {
1335 $result = true;
1336 $type_key = '';
1337 }
1338
1339 if ( isset( $result ) && is_wp_error( $result ) && $type_key ) {
1340 $violations[] = [
1341 'type' => $type_key,
1342 'field' => $field_name,
1343 'error' => $result,
1344 ];
1345 }
1346 }
1347 }
1348
1349 if ( isset( $attrs['linkMetaFieldName'] ) || ! empty( $attrs['dynamicLinkType'] ) ) {
1350 $link_field = isset( $attrs['linkMetaFieldName'] ) ? trim( (string) $attrs['linkMetaFieldName'] ) : '';
1351 $link_type = isset( $attrs['dynamicLinkType'] ) ? (string) $attrs['dynamicLinkType'] : '';
1352
1353 if ( 'author-email' === $link_type ) {
1354 $link_field = 'user_email';
1355 }
1356
1357 if ( $link_field ) {
1358 if ( in_array( $link_type, [ 'author-meta', 'author-email' ], true ) ) {
1359 $result = $should_validate_user_meta ? GenerateBlocks_Dynamic_Tag_Security::validate_user_meta_field_name( $link_field ) : true;
1360 $type_key = 'user_meta';
1361 } elseif ( 'post-meta' === $link_type ) {
1362 $result = GenerateBlocks_Dynamic_Tag_Security::validate_post_meta_field_name( $link_field );
1363 $type_key = 'post_meta';
1364 } else {
1365 $result = true;
1366 $type_key = '';
1367 }
1368
1369 if ( isset( $result ) && is_wp_error( $result ) && $type_key ) {
1370 $violations[] = [
1371 'type' => $type_key,
1372 'field' => $link_field,
1373 'error' => $result,
1374 ];
1375 }
1376 }
1377 }
1378
1379 return $violations;
1380 }
1381 }
1382
1383 GenerateBlocks_Dynamic_Content::get_instance();
1384