method. Anything not listed (Pro / unknown) resolves to the fallback. */
const RESOLVERS = array(
'post_title' => 'tag_post_title',
'post_excerpt' => 'tag_post_excerpt',
'post_taxonomy' => 'tag_post_taxonomy',
'post_date' => 'tag_post_date',
'post_time' => 'tag_post_time',
'author_name' => 'tag_author_name',
'author_info_n_meta' => 'tag_author_info_n_meta',
'featured_image_data' => 'tag_featured_image_data',
);
/** authorMetaType => get_the_author_meta() field. */
const AUTHOR_FIELDS = array(
'description' => 'description',
'display_name' => 'display_name',
'first_name' => 'first_name',
'last_name' => 'last_name',
'nickname' => 'nickname',
'email' => 'user_email',
'url' => 'user_url',
);
const DEFAULT_TERM_SEPARATOR = ', ';
const DEFAULT_EXCERPT_LENGTH = 55; // WordPress default `excerpt_length`.
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
/**
* @param array $config `dynamicContent` block attribute.
* @param WP_Post|null $post Post to read from (block context or current post).
* @param array $ctx Optional: 'links' => false to forbid in the output
* (e.g. the block itself is already wrapped in a link).
* @return string Safe HTML; '' when there is nothing to show.
*/
public static function resolve( array $config, $post, array $ctx = array() ) {
$type = isset( $config['type'] ) && is_string( $config['type'] ) ? $config['type'] : '';
$html = '';
if ( $post instanceof WP_Post && isset( self::RESOLVERS[ $type ] ) && self::can_read( $post ) ) {
$method = self::RESOLVERS[ $type ];
$html = (string) self::$method( $config, $post, $ctx );
}
return self::format( $html, $config );
}
/**
* Get the day archive URL for a post's published or modified date.
*
* @param WP_Post $post Post whose date should be linked.
* @param bool $modified Whether to use the modified date.
* @return string Day archive URL, or an empty string when the date is unavailable.
*/
public static function date_archive_link( WP_Post $post, $modified = false ) {
$date = get_post_datetime( $post, $modified ? 'modified' : 'date' );
if ( ! $date ) {
return '';
}
return get_day_link(
(int) $date->format( 'Y' ),
(int) $date->format( 'm' ),
(int) $date->format( 'd' )
);
}
/* ------------------------------------------------------------------ */
/* Formatting (shared by every tag) */
/* ------------------------------------------------------------------ */
/** Empty value -> fallback. Real value -> before + value + after. */
private static function format( $html, array $config ) {
if ( '' === trim( wp_strip_all_tags( $html ) ) ) {
return esc_html( self::to_string( isset( $config['fallback'] ) ? $config['fallback'] : '' ) );
}
return esc_html( self::to_string( isset( $config['before'] ) ? $config['before'] : '' ) )
. $html
. esc_html( self::to_string( isset( $config['after'] ) ? $config['after'] : '' ) );
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
/** Published, or the current user may read it (previews, private posts). */
private static function can_read( WP_Post $post ) {
return 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID );
}
/** Any value -> string. Flat arrays are joined; objects / booleans / null become ''. */
private static function to_string( $value ) {
if ( is_array( $value ) ) {
return implode(
', ',
array_filter(
array_map(
function ( $item ) {
return self::to_string( $item );
},
$value
)
)
);
}
return is_scalar( $value ) && ! is_bool( $value ) ? (string) $value : '';
}
/** HTML / markup -> plain text (tags and comments removed, entities decoded). */
private static function plain( $html ) {
$text = html_entity_decode( wp_strip_all_tags( self::to_string( $html ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
$collapsed = preg_replace( '/\s+/u', ' ', $text );
return trim( null === $collapsed ? $text : $collapsed );
}
private static function links_allowed( array $config, array $ctx, $flag ) {
return ! empty( $config[ $flag ] ) && ( ! isset( $ctx['links'] ) || $ctx['links'] );
}
/** Only meta registered with show_in_rest (same set the editor preview can see). */
private static function author_meta( $author_id, $key ) {
if ( '' === $key || is_protected_meta( $key, 'user' ) ) {
return '';
}
$registered = get_registered_meta_keys( 'user' );
$allowed = ! empty( $registered[ $key ]['show_in_rest'] );
/**
* Filters whether a user meta key may be output by the Dynamic Text block.
*
* @param bool $allowed True when registered with show_in_rest.
* @param string $key Meta key.
* @param int $author_id Author user ID.
*/
if ( ! apply_filters( 'flbl_dynamic_text_author_meta_allowed', $allowed, $key, $author_id ) ) {
return '';
}
return get_user_meta( $author_id, $key, true );
}
/* ------------------------------------------------------------------ */
/* Tag resolvers: ( $config, $post, $ctx ) => safe HTML */
/* ------------------------------------------------------------------ */
private static function tag_post_title( array $config, WP_Post $post, array $ctx ) {
$as_links = self::links_allowed( $config, $ctx, 'titleLink' );
if ( $as_links ) {
$url = get_permalink( $post );
if ( ! is_wp_error( $url ) ) {
return '' . esc_html( self::plain( get_the_title( $post ) ) ) . '';
}
}
return esc_html( self::plain( get_the_title( $post ) ) );
}
private static function tag_post_excerpt( array $config, WP_Post $post, array $ctx ) {
// Never leak the excerpt / content of a password-protected post.
if ( post_password_required( $post ) ) {
return '';
}
// 'description' always uses the content; 'excerpt' prefers the stored
// excerpt and falls back to content, like WordPress' auto-excerpt.
$from_content = isset( $config['excerptType'] ) && 'description' === $config['excerptType'];
$stored = $from_content ? '' : self::plain( $post->post_excerpt );
$text = '' !== $stored ? $stored : self::plain( strip_shortcodes( $post->post_content ) );
if ( '' === $text ) {
return '';
}
// -1 = show all. > 0 = that many words. Empty / 0 = default behaviour:
// stored excerpts untouched, generated ones trimmed to the theme length.
$length = isset( $config['excerptLength'] ) ? (int) $config['excerptLength'] : 0;
if ( -1 === $length ) {
$limit = null;
} elseif ( $length > 0 ) {
$limit = $length;
} else {
$limit = '' !== $stored ? null : (int) apply_filters(
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter; preserve theme compatibility.
'excerpt_length',
self::DEFAULT_EXCERPT_LENGTH
);
}
$html = esc_html( $text );
if ( null === $limit ) {
return $html;
}
// `useCutomReadMore` keeps the spelling used by DynamicTextControls.
$more = isset( $config['useThemeReadMore'] ) && true === $config['useThemeReadMore']
? wp_kses_post(
apply_filters(
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter; preserve theme compatibility.
'excerpt_more',
' […]'
)
)
: esc_html( self::to_string( isset( $config['useCutomReadMore'] ) ? $config['useCutomReadMore'] : '' ) );
// Escaping first is safe: it adds no whitespace, so word counts are unchanged.
// Like wp_trim_words(), $more is appended only when the text was cut.
return wp_trim_words( $html, max( 1, $limit ), $more );
}
private static function tag_post_taxonomy( array $config, WP_Post $post, array $ctx ) {
// `taxnonomyType` keeps the spelling used by DynamicTextControls.
$is_tags = isset( $config['taxnonomyType'] ) && 'tags' === $config['taxnonomyType'];
$terms = get_the_terms( $post, $is_tags ? 'post_tag' : 'category' );
$as_links = self::links_allowed( $config, $ctx, 'termLink' );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return '';
}
$items = array();
foreach ( $terms as $term ) {
$name = esc_html( self::plain( $term->name ) );
if ( '' === $name ) {
continue;
}
if ( $as_links ) {
$url = get_term_link( $term );
if ( ! is_wp_error( $url ) ) {
$name = '' . $name . '';
}
}
$items[] = $name;
}
$separator = self::to_string( isset( $config['termSeparator'] ) ? $config['termSeparator'] : '' );
return implode( esc_html( '' === $separator ? self::DEFAULT_TERM_SEPARATOR : $separator ), $items );
}
private static function tag_post_date( array $config, WP_Post $post, array $ctx ) {
// Undefined -> 'F j, Y' (what the control shows); cleared ('') -> site format.
$format = trim( self::to_string( isset( $config['dateFormat'] ) ? $config['dateFormat'] : 'F j, Y' ) );
$modified = isset( $config['dateType'] ) && 'modified' === $config['dateType'];
$date = $modified ? get_the_modified_date( $format, $post ) : get_the_date( $format, $post );
if ( ! $date ) {
return '';
}
$html = esc_html( $date );
if ( self::links_allowed( $config, $ctx, 'dateLink' ) ) {
$url = self::date_archive_link( $post, $modified );
if ( '' !== $url ) {
$html = '' . $html . '';
}
}
return $html;
}
private static function tag_post_time( array $config, WP_Post $post, array $ctx ) {
$format = trim( self::to_string( isset( $config['timeFormat'] ) ? $config['timeFormat'] : 'g:i a' ) );
$modified = isset( $config['timeType'] ) && 'modified' === $config['timeType'];
$time = $modified ? get_the_modified_time( $format, $post ) : get_the_time( $format, $post );
return $time ? esc_html( $time ) : '';
}
private static function tag_author_name( array $config, WP_Post $post, array $ctx ) {
return esc_html( self::plain( get_the_author_meta( 'display_name', (int) $post->post_author ) ) );
}
private static function tag_author_info_n_meta( array $config, WP_Post $post, array $ctx ) {
$type = isset( $config['authorMetaType'] ) ? $config['authorMetaType'] : 'description';
$author_id = (int) $post->post_author;
if ( 'meta_key' === $type ) {
$key = trim( self::to_string( isset( $config['authorMetaKey'] ) ? $config['authorMetaKey'] : '' ) );
$value = self::author_meta( $author_id, $key );
} elseif ( is_string( $type ) && isset( self::AUTHOR_FIELDS[ $type ] ) ) {
$value = get_the_author_meta( self::AUTHOR_FIELDS[ $type ], $author_id );
} else {
return '';
}
return esc_html( self::plain( $value ) );
}
private static function tag_featured_image_data( array $config, WP_Post $post, array $ctx ) {
$media_id = (int) get_post_thumbnail_id( $post );
if ( ! $media_id ) {
return '';
}
$type = isset( $config['mediaType'] ) ? $config['mediaType'] : 'title';
// URLs are shown as text exactly like the editor preview; skip plain()
// so entity decoding can't mangle query strings.
if ( 'file_url' === $type ) {
return esc_html( self::to_string( wp_get_attachment_url( $media_id ) ) );
}
if ( 'attachment_url' === $type ) {
return esc_html( self::to_string( get_attachment_link( $media_id ) ) );
}
switch ( $type ) {
case 'title':
$value = get_the_title( $media_id );
break;
case 'alt':
$value = get_post_meta( $media_id, '_wp_attachment_image_alt', true );
break;
case 'caption':
$value = wp_get_attachment_caption( $media_id );
break;
case 'description':
$value = get_post_field( 'post_content', $media_id );
break;
default:
return '';
}
return esc_html( self::plain( $value ) );
}
}
endif;