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 ''; } } 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 = ''; } } $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;