prefix + a hardcoded literal string. * They are never constructed from user input. The per-line phpcs suppressions * below are intentional and reviewed — they silence sniffs that cannot * statically determine a variable is safe, even when it provably is. * * @package TrustBio */ if ( ! defined( 'ABSPATH' ) ) { exit; } // ============================================================ // DB helpers // ============================================================ /** * Get all author categories. * * @param bool $trustbio_enabled_only Return only enabled categories. * @return array */ function trustbio_get_categories( bool $trustbio_enabled_only = false ): array { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_categories'; if ( $trustbio_enabled_only ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return (array) $wpdb->get_results( "SELECT * FROM `{$trustbio_t}` WHERE is_enabled = 1 ORDER BY sort_order ASC, id ASC" ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return (array) $wpdb->get_results( "SELECT * FROM `{$trustbio_t}` ORDER BY sort_order ASC, id ASC" ); } /** * Get a single author category by ID. * * @param int $trustbio_id Category ID. * @return object|null */ function trustbio_get_category( int $trustbio_id ): ?object { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_categories'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$trustbio_t}` WHERE id = %d", $trustbio_id ) ); } /** * Get active author fields. * * @param bool $trustbio_social_only Return only social fields. * @return array */ function trustbio_get_fields( bool $trustbio_social_only = false ): array { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_fields'; if ( $trustbio_social_only ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_rows = (array) $wpdb->get_results( "SELECT * FROM `{$trustbio_t}` WHERE is_active = 1 AND is_social = 1 ORDER BY sort_order ASC" ); /** * Filters the active social profile fields. * * @since 1.0.0 * @param array $fields Field rows, in sort order. */ return (array) apply_filters( 'trustbio_social_fields', $trustbio_rows ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_rows = (array) $wpdb->get_results( "SELECT * FROM `{$trustbio_t}` WHERE is_active = 1 ORDER BY sort_order ASC" ); /** * Filters the active author fields. * * @since 1.0.0 * @param array $fields Field rows, in sort order. */ return (array) apply_filters( 'trustbio_author_fields', $trustbio_rows ); } /** * Get all fields including disabled ones — for admin screens. * * @return array */ function trustbio_get_all_fields(): array { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_fields'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return (array) $wpdb->get_results( "SELECT * FROM `{$trustbio_t}` ORDER BY sort_order ASC" ); } /** * Get an author record by ID. * * @param int $trustbio_id Author ID. * @return object|null */ function trustbio_get_author( int $trustbio_id ): ?object { $trustbio_cache_key = 'trustbio_author_' . $trustbio_id; $trustbio_cached = wp_cache_get( $trustbio_cache_key, 'trustbio' ); if ( false !== $trustbio_cached ) { return $trustbio_cached ?: null; } global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_authors'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$trustbio_t}` WHERE id = %d", $trustbio_id ) ); wp_cache_set( $trustbio_cache_key, $trustbio_row, 'trustbio', HOUR_IN_SECONDS ); return $trustbio_row ?: null; } /** * Forget a cached author row. * * trustbio_get_author() caches for an hour. Without this, a site running a * persistent object cache — Redis or Memcached, which most managed hosts * enable — kept serving the old row after an edit: a corrected name, bio or * job title would not appear on the front end for up to an hour, and a deleted * author carried on being rendered. Where there is no persistent cache the * entry only lives for the current request, which is why this went unnoticed. * * Call it after every write to the authors table. * * @param int $trustbio_id Author ID. * @return void */ function trustbio_clear_author_cache( int $trustbio_id ): void { wp_cache_delete( 'trustbio_author_' . $trustbio_id, 'trustbio' ); // The Settings screen's counts include the author total. // trustbio_get_author_by_slug() reads the table directly and caches // nothing, so there is no second entry to clear. wp_cache_delete( 'trustbio_sp_stats', 'trustbio' ); } /** * Get an author record by slug. * * @param string $trustbio_slug Author slug. * @return object|null */ function trustbio_get_author_by_slug( string $trustbio_slug ): ?object { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_authors'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$trustbio_t}` WHERE slug = %s", $trustbio_slug ) ); } /** * Get social/field values for an author, keyed by field slug. * * @param int $trustbio_author_id Author ID. * @return array */ function trustbio_get_author_field_values( int $trustbio_author_id ): array { global $wpdb; $trustbio_vt = $wpdb->prefix . 'trustbio_author_field_values'; $trustbio_ft = $wpdb->prefix . 'trustbio_author_fields'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter "SELECT f.slug, v.value FROM `{$trustbio_vt}` v INNER JOIN `{$trustbio_ft}` f ON f.id = v.field_id WHERE v.author_id = %d", $trustbio_author_id ) ); $trustbio_result = array(); foreach ( (array) $trustbio_rows as $trustbio_row ) { $trustbio_result[ $trustbio_row->slug ] = $trustbio_row->value; } return $trustbio_result; } /** * Get authors assigned to a post. * * @param int $trustbio_post_id Post ID. * @return array */ function trustbio_get_post_authors( int $trustbio_post_id ): array { $trustbio_raw = get_post_meta( $trustbio_post_id, '_trustbio_authors', true ); if ( empty( $trustbio_raw ) || ! is_array( $trustbio_raw ) ) { return trustbio_get_fallback_author( $trustbio_post_id ); } $trustbio_result = array(); foreach ( $trustbio_raw as $trustbio_entry ) { if ( ! is_array( $trustbio_entry ) || ! isset( $trustbio_entry['author_id'] ) ) { continue; } $trustbio_author = trustbio_get_author( (int) $trustbio_entry['author_id'] ); if ( ! $trustbio_author ) { continue; } $trustbio_category = isset( $trustbio_entry['category_id'] ) ? trustbio_get_category( (int) $trustbio_entry['category_id'] ) : null; $trustbio_result[] = array( 'author' => $trustbio_author, 'category' => $trustbio_category, 'sort_order' => (int) ( $trustbio_entry['sort_order'] ?? 0 ), ); } usort( $trustbio_result, static function ( $trustbio_a, $trustbio_b ) { return $trustbio_a['sort_order'] <=> $trustbio_b['sort_order']; } ); return $trustbio_result; } /** * Build a fallback author entry from the native WordPress post_author field. * * @param int $trustbio_post_id Post ID. * @return array */ function trustbio_get_fallback_author( int $trustbio_post_id ): array { global $wpdb; $trustbio_wp_user_id = (int) get_post_field( 'post_author', $trustbio_post_id ); if ( ! $trustbio_wp_user_id ) { return array(); } $trustbio_t = $wpdb->prefix . 'trustbio_authors'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_author = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$trustbio_t}` WHERE wp_user_id = %d AND is_active = 1 LIMIT 1", $trustbio_wp_user_id ) ); if ( ! $trustbio_author ) { return array(); } $trustbio_category = $trustbio_author->category_id ? trustbio_get_category( (int) $trustbio_author->category_id ) : null; return array( array( 'author' => $trustbio_author, 'category' => $trustbio_category, 'sort_order' => 0, ), ); } /** * Get an author box by ID with settings decoded. * * @param int $trustbio_box_id Author Box ID. * @return object|null */ function trustbio_get_author_box( int $trustbio_box_id ): ?object { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_boxes'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_box = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$trustbio_t}` WHERE id = %d", $trustbio_box_id ) ); if ( $trustbio_box && ! empty( $trustbio_box->settings ) ) { $trustbio_box->settings = json_decode( $trustbio_box->settings, true ); } return $trustbio_box; } /** * Get the global default author box. * * @return object|null */ function trustbio_get_default_author_box(): ?object { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_author_boxes'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_box = $wpdb->get_row( "SELECT * FROM `{$trustbio_t}` WHERE is_default = 1 LIMIT 1" ); if ( ! $trustbio_box ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_box = $wpdb->get_row( "SELECT * FROM `{$trustbio_t}` ORDER BY id ASC LIMIT 1" ); } if ( $trustbio_box && ! empty( $trustbio_box->settings ) ) { $trustbio_box->settings = json_decode( $trustbio_box->settings, true ); } return $trustbio_box; } // ============================================================ // Avatar helpers // ============================================================ /** * Get the avatar URL for an author with fallback chain. * * @param object $trustbio_author Author object. * @param int $trustbio_size Avatar size in pixels. * @return string Escaped URL. */ function trustbio_get_avatar_url( object $trustbio_author, int $trustbio_size = 80 ): string { if ( ! empty( $trustbio_author->avatar_attach_id ) ) { $trustbio_img = wp_get_attachment_image_src( (int) $trustbio_author->avatar_attach_id, array( $trustbio_size, $trustbio_size ) ); if ( $trustbio_img ) { return esc_url( $trustbio_img[0] ); } } if ( ! empty( $trustbio_author->avatar_url ) ) { return esc_url( $trustbio_author->avatar_url ); } if ( ! empty( $trustbio_author->email ) ) { return esc_url( get_avatar_url( $trustbio_author->email, array( 'size' => $trustbio_size ) ) ); } if ( ! empty( $trustbio_author->wp_user_id ) ) { $trustbio_wp_user = get_userdata( (int) $trustbio_author->wp_user_id ); if ( $trustbio_wp_user ) { return esc_url( get_avatar_url( $trustbio_wp_user->user_email, array( 'size' => $trustbio_size ) ) ); } } return esc_url( get_avatar_url( 0, array( 'size' => $trustbio_size ) ) ); } // ============================================================ // URL / slug helpers // ============================================================ /** * Get the public profile URL for an author. * * @param object $trustbio_author Author object. * @return string */ function trustbio_get_author_url( object $trustbio_author ): string { $trustbio_base = sanitize_title( get_option( 'trustbio_author_url_base', 'authors' ) ); return trailingslashit( home_url( $trustbio_base . '/' . $trustbio_author->slug ) ); } /** * Generate a unique author slug. * * @param string $trustbio_name Display name to slugify. * @param int $trustbio_exclude_id Author ID to skip on edits. * @return string */ function trustbio_generate_author_slug( string $trustbio_name, int $trustbio_exclude_id = 0 ): string { global $wpdb; $trustbio_t = $wpdb->prefix . 'trustbio_authors'; $trustbio_base = sanitize_title( $trustbio_name ); $trustbio_slug = $trustbio_base; $trustbio_counter = 1; while ( true ) { if ( $trustbio_exclude_id > 0 ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$trustbio_t}` WHERE slug = %s AND id != %d", $trustbio_slug, $trustbio_exclude_id ) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $trustbio_existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM `{$trustbio_t}` WHERE slug = %s", $trustbio_slug ) ); } if ( ! $trustbio_existing ) { break; } $trustbio_slug = $trustbio_base . '-' . $trustbio_counter; ++$trustbio_counter; } return $trustbio_slug; } // ============================================================ // Schema helpers // ============================================================ /** * Field slugs that map to schema.org sameAs (authority identity signals). * * @return array */ function trustbio_get_same_as_field_slugs(): array { return array( 'twitter', 'linkedin', 'facebook', 'instagram', 'youtube', 'github', 'medium', 'substack', 'wikipedia', 'google_scholar', 'researchgate', 'orcid', 'amazon_author', 'mastodon', 'threads', 'bluesky', ); } // ============================================================ // Security helpers // ============================================================ /** * The HTML a TrustBio front-end block is allowed to contain. * * WordPress's post allowlist, plus the SVG elements and attributes the * credential badges and social icons are drawn with. A plain * wp_kses_post() would strip every icon in the plugin, because SVG is not * in the post allowlist at all. * * @return array> */ function trustbio_allowed_block_html(): array { $trustbio_svg_attr = array( 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'fill-rule' => true, 'clip-rule' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'd' => true, 'cx' => true, 'cy' => true, 'r' => true, 'x' => true, 'y' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'rx' => true, 'ry' => true, 'points' => true, 'transform' => true, 'class' => true, 'style' => true, 'role' => true, 'focusable' => true, 'aria-hidden' => true, 'aria-label' => true, 'aria-labelledby' => true, ); $trustbio_allowed = wp_kses_allowed_html( 'post' ); foreach ( array( 'svg', 'path', 'g', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'defs', 'use', 'title' ) as $trustbio_el ) { $trustbio_allowed[ $trustbio_el ] = $trustbio_svg_attr; } /* * The post allowlist does not carry aria-* or role on ordinary elements, * and the boxes use both: the badge icons are aria-hidden and the social * links are labelled. Dropping them would leave the markup valid but * less accessible than it was. */ foreach ( array( 'div', 'span', 'a', 'p', 'img', 'time', 'strong', 'em', 'small', 'ul', 'ol', 'li' ) as $trustbio_el ) { if ( ! isset( $trustbio_allowed[ $trustbio_el ] ) || ! is_array( $trustbio_allowed[ $trustbio_el ] ) ) { $trustbio_allowed[ $trustbio_el ] = array(); } $trustbio_allowed[ $trustbio_el ] += array( 'class' => true, 'style' => true, 'id' => true, 'title' => true, 'role' => true, 'aria-hidden' => true, 'aria-label' => true, 'aria-labelledby' => true, 'datetime' => true, ); } /** * Filter the HTML a TrustBio front-end block may contain. * * @since 1.0.0 * @param array> $trustbio_allowed wp_kses allowlist. */ return (array) apply_filters( 'trustbio_allowed_block_html', $trustbio_allowed ); } /** * Last-pass escaping for a finished front-end block. * * Every value in these blocks is already escaped where it is interpolated, * field by field, as the markup is built. This is the belt to that pair of * braces, and it sits where it does for a reason: the author box and the * review bar are returned from the_content, the_title and two shortcodes, * so whatever they return is printed by WordPress without further * treatment. Constraining the finished string means a future edit to any * one of the builders cannot quietly put unescaped data on a page. * * It is wp_kses() with an explicit allowlist, not a pass-through: markup * outside the allowlist really is removed. * * @param string $trustbio_html A block of TrustBio-generated markup. * @return string */ function trustbio_kses_block( string $trustbio_html ): string { if ( '' === $trustbio_html ) { return ''; } return wp_kses( $trustbio_html, trustbio_allowed_block_html() ); } /** * Verify an AJAX request: nonce + capability. * Calls check_ajax_referer() directly so PHPCS can see the verification. * * @param string $trustbio_nonce_action Nonce action. * @param string $trustbio_nonce_key $_POST key holding the nonce. * @param string $trustbio_capability Required capability. */ function trustbio_verify_ajax_request( string $trustbio_nonce_action, string $trustbio_nonce_key = 'nonce', string $trustbio_capability = 'manage_options' ): void { check_ajax_referer( $trustbio_nonce_action, $trustbio_nonce_key ); if ( ! current_user_can( $trustbio_capability ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'trustbio' ) ), 403 ); } } // ============================================================ // Misc helpers // ============================================================ /** * Check if TrustBio is enabled for a given post type. * * @param string $trustbio_post_type Post type slug. * @return bool */ /** * Get all enabled post types as an array of slugs. * * @return array */ function trustbio_get_enabled_post_types(): array { $trustbio_enabled = get_option( 'trustbio_enabled_post_types', array( 'post', 'page' ) ); if ( ! is_array( $trustbio_enabled ) ) { $trustbio_enabled = array( 'post', 'page' ); } return array_map( 'sanitize_key', $trustbio_enabled ); } function trustbio_is_enabled_for_post_type( string $trustbio_post_type ): bool { $trustbio_enabled = get_option( 'trustbio_enabled_post_types', array( 'post', 'page' ) ); return in_array( $trustbio_post_type, (array) $trustbio_enabled, true ); } /** * Retrieve a TrustBio setting by short key. * * @param string $trustbio_key Short key without the 'trustbio_' prefix. * @param mixed $trustbio_default Default value. * @return mixed */ function trustbio_get_setting( string $trustbio_key, $trustbio_default = null ) { return get_option( 'trustbio_' . $trustbio_key, $trustbio_default ); } /** * Render social icon links for an author — standalone helper for use in templates. * Duo templates call this directly for each author they render. * * @param object $trustbio_author Author object. * @param array $trustbio_s Box settings array. * @param string $trustbio_layout Layout slug (used to adjust icon size for small layouts). * @return string HTML — empty string when no social fields are set. */ function trustbio_render_social_icons_html( object $trustbio_author, array $trustbio_s, string $trustbio_layout = '' ): string { $trustbio_field_values = trustbio_get_author_field_values( $trustbio_author->id ); $trustbio_social_fields = trustbio_get_fields( true ); if ( empty( $trustbio_social_fields ) || empty( $trustbio_field_values ) ) { return ''; } // Duo layouts use smaller icons. $trustbio_is_duo = in_array( $trustbio_layout, array( 'duo-light', 'duo-dark' ), true ); $trustbio_icon_size = $trustbio_is_duo ? 11 : (int) ( $trustbio_s['icon_size'] ?? 20 ); $trustbio_icon_fill = $trustbio_is_duo ? ( 'duo-dark' === $trustbio_layout ? '#d9c08d' : '#374151' ) : esc_attr( trustbio_social_icon_fill( $trustbio_s ) ); $trustbio_html = ''; foreach ( $trustbio_social_fields as $trustbio_field ) { $trustbio_url = $trustbio_field_values[ $trustbio_field->slug ] ?? ''; if ( empty( $trustbio_url ) ) { continue; } $trustbio_svg = trustbio_get_social_svg( $trustbio_field->slug, $trustbio_icon_size, $trustbio_icon_fill ); $trustbio_html .= sprintf( '%s', esc_url( $trustbio_url ), esc_attr( $trustbio_field->slug ), esc_attr( $trustbio_field->label ), $trustbio_svg // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } return $trustbio_html; } /** * Return an inline SVG for a given social field slug. * * @param string $trustbio_slug Field slug. * @param int $trustbio_size Icon size in px. * @param string $trustbio_color Fill colour. * @return string SVG HTML. */ function trustbio_get_social_svg( string $trustbio_slug, int $trustbio_size, string $trustbio_color ): string { $trustbio_s = (string) $trustbio_size; $trustbio_c = esc_attr( $trustbio_color ); $trustbio_paths = array( 'twitter' => '', 'twitter_x' => '', 'linkedin' => '', 'facebook' => '', 'instagram' => '', 'youtube' => '', 'github' => '', ); $trustbio_path = $trustbio_paths[ $trustbio_slug ] ?? ''; return sprintf( '%s', $trustbio_s, $trustbio_s, $trustbio_c, $trustbio_path ); } /** * Return the inline review-date HTML for a specific author on a post. * * Checks if this author is the designated reviewer for the post, then * delegates to TrustBio_Review_Date::render_inline_review(). Returns '' * when the author is not the reviewer or no review date is set. * * @param int $trustbio_post_id Post ID. * @param int $trustbio_author_id TrustBio author ID (not WP user ID). * @param string $trustbio_accent_col Accent colour for the icon (hex). * @return string HTML or empty string. */ /** * Build an inline style string for the author box wrapper from saved settings. * * Only emits styles the user has explicitly configured so CSS-class defaults * are not overridden unless the admin actually changed them. * * @param array $settings Box settings array. * @return string Inline CSS or empty string. */ function trustbio_build_wrapper_style( array $settings ): string { $trustbio_parts = []; if ( ! empty( $settings['bg_color'] ) ) { $trustbio_parts[] = 'background:' . $settings['bg_color']; } $trustbio_bs = $settings['border_style'] ?? 'none'; if ( 'none' !== $trustbio_bs && '' !== $trustbio_bs ) { $trustbio_bw = (int) ( $settings['border_width'] ?? 0 ); $trustbio_bc = $settings['border_color'] ?? '#e5e7eb'; $trustbio_bt = isset( $settings['border_top'] ) ? (int) $settings['border_top'] : $trustbio_bw; $trustbio_br = isset( $settings['border_right'] ) ? (int) $settings['border_right'] : $trustbio_bw; $trustbio_bb = isset( $settings['border_bottom'] ) ? (int) $settings['border_bottom'] : $trustbio_bw; $trustbio_bl = isset( $settings['border_left'] ) ? (int) $settings['border_left'] : $trustbio_bw; if ( $trustbio_bt || $trustbio_br || $trustbio_bb || $trustbio_bl ) { $trustbio_parts[] = "border-top:{$trustbio_bt}px {$trustbio_bs} {$trustbio_bc}"; $trustbio_parts[] = "border-right:{$trustbio_br}px {$trustbio_bs} {$trustbio_bc}"; $trustbio_parts[] = "border-bottom:{$trustbio_bb}px {$trustbio_bs} {$trustbio_bc}"; $trustbio_parts[] = "border-left:{$trustbio_bl}px {$trustbio_bs} {$trustbio_bc}"; } } $trustbio_brad = (int) ( $settings['border_radius'] ?? 0 ); if ( $trustbio_brad ) { $trustbio_parts[] = "border-radius:{$trustbio_brad}px"; } $trustbio_pad = (int) ( $settings['padding'] ?? 0 ); if ( $trustbio_pad ) { $trustbio_parts[] = "padding:{$trustbio_pad}px"; } // CSS custom properties — allow per-box font and button overrides. $trustbio_font_h = $settings['font_heading'] ?? ''; if ( $trustbio_font_h ) { $trustbio_parts[] = '--tb-font-heading:' . $trustbio_font_h; } $trustbio_font_l = $settings['font_label'] ?? ''; if ( $trustbio_font_l ) { $trustbio_parts[] = '--tb-font-label:' . $trustbio_font_l; } $trustbio_btn_bg = $settings['btn_color'] ?? ''; if ( $trustbio_btn_bg ) { $trustbio_parts[] = '--tb-btn-bg:' . $trustbio_btn_bg; } $trustbio_btn_color = $settings['btn_text_color'] ?? ''; if ( $trustbio_btn_color ) { $trustbio_parts[] = '--tb-btn-color:' . $trustbio_btn_color; } /* * Builder controls that have no markup of their own. * * Avatar shape, box shadow, the social-icon treatment and the label/job * title type scale were all sanitised and saved but never reached the * page -- the layout stylesheet hard-coded each one. They are emitted as * custom properties here instead, because every caller that renders a box * already calls this function: the eight front-end templates and the * builder's own live preview. One source, so the preview cannot drift * from what the visitor sees. * * Each property is consumed in trustbio-public.css as * var( --tb-x, ), so a box that never * set the key keeps exactly the appearance it has today. */ // Avatar shape. Layout fallbacks differ (simple uses 14px), so only // override when the box actually carries a choice. $trustbio_shape_radius = array( 'circle' => '50%', 'rounded' => '14px', 'square' => '0', ); $trustbio_shape = $settings['avatar_shape'] ?? ''; if ( isset( $trustbio_shape_radius[ $trustbio_shape ] ) ) { $trustbio_parts[] = '--tb-avatar-radius:' . $trustbio_shape_radius[ $trustbio_shape ]; } // Avatar size. The slider ran from 40 to 160 and moved nothing but the // resolution of the Gravatar fetch; the rendered size was hard-coded per // layout. $trustbio_avatar_size = (int) ( $settings['avatar_size'] ?? 0 ); if ( $trustbio_avatar_size ) { $trustbio_parts[] = '--tb-avatar-size:' . $trustbio_avatar_size . 'px'; } // Box shadow. Absent means a box saved before the control worked, which // should keep its shadow -- so only an explicit false turns it off. if ( array_key_exists( 'box_shadow', $settings ) ) { $trustbio_parts[] = empty( $settings['box_shadow'] ) ? '--tb-shadow:none' : '--tb-shadow:0 30px 60px -30px rgba(20,20,20,.18), 0 8px 20px -10px rgba(20,20,20,.08)'; } // Social icon chrome. The fill itself is an SVG attribute, set by // trustbio_social_icon_fill() so the two stay in step. $trustbio_icon_style = $settings['icon_style'] ?? ''; $trustbio_icon_col = $settings['icon_color'] ?? '#374151'; if ( 'monochrome' === $trustbio_icon_style ) { $trustbio_parts[] = '--tb-icon-bg:' . $trustbio_icon_col; $trustbio_parts[] = '--tb-icon-border:0 solid transparent'; } elseif ( 'outline' === $trustbio_icon_style ) { $trustbio_parts[] = '--tb-icon-bg:transparent'; $trustbio_parts[] = '--tb-icon-border:1.5px solid #e5e7eb'; } elseif ( 'color' === $trustbio_icon_style ) { $trustbio_parts[] = '--tb-icon-bg:#ece8e1'; $trustbio_parts[] = '--tb-icon-border:0 solid transparent'; } $trustbio_icon_size = (int) ( $settings['icon_size'] ?? 0 ); if ( $trustbio_icon_size ) { $trustbio_parts[] = '--tb-icon-size:' . $trustbio_icon_size . 'px'; // The chip grows with the glyph, as the builder preview always drew it. $trustbio_parts[] = '--tb-icon-chip:' . ( $trustbio_icon_size + 17 ) . 'px'; } // Category label (the kicker / role line) and job title. $trustbio_label_color = $settings['label_color'] ?? ''; if ( $trustbio_label_color ) { $trustbio_parts[] = '--tb-label-color:' . $trustbio_label_color; } $trustbio_label_size = (int) ( $settings['label_size'] ?? 0 ); if ( $trustbio_label_size ) { $trustbio_parts[] = '--tb-label-size:' . $trustbio_label_size . 'px'; } $trustbio_label_weight = $settings['label_weight'] ?? ''; if ( $trustbio_label_weight ) { $trustbio_parts[] = '--tb-label-weight:' . $trustbio_label_weight; } $trustbio_jobtitle_size = (int) ( $settings['jobtitle_size'] ?? 0 ); if ( $trustbio_jobtitle_size ) { $trustbio_parts[] = '--tb-jobtitle-size:' . $trustbio_jobtitle_size . 'px'; } return implode( ';', $trustbio_parts ); } /** * Fill colour for a social glyph, given the box's icon settings. * * Kept apart from the CSS custom properties because the fill is an attribute * on the SVG itself. Monochrome inverts: the chip takes the chosen colour and * the glyph goes white, which is the only style where icon_color is not the * glyph colour. * * @param array $settings Box settings. * @return string Hex colour. */ function trustbio_social_icon_fill( array $settings ): string { $trustbio_color = ( $settings['icon_color'] ?? '' ) ?: '#374151'; return 'monochrome' === ( $settings['icon_style'] ?? '' ) ? '#ffffff' : $trustbio_color; } /** * Apply bio character limit to a bio string. * * Strips HTML tags, then truncates to $limit characters if $limit > 0. * Returns the original bio unchanged when $limit is 0 (no limit). * * @param string $bio Raw bio text (may contain HTML). * @param int $limit Maximum character count. 0 = no limit. * @return string Truncated plain-text bio, or original bio when limit is 0. */ function trustbio_apply_bio_limit( string $bio, int $limit ): string { if ( $limit <= 0 || '' === $bio ) { return $bio; } $plain = wp_strip_all_tags( $bio ); if ( mb_strlen( $plain ) > $limit ) { return mb_substr( $plain, 0, $limit ) . '…'; } return $plain; } /** * Check if a TrustBio feature module is enabled. * * Returns true when the option has never been set (backward-compatible default = all on), * or when the feature slug appears in the stored enabled-features array. * * @param string $trustbio_feature Feature slug. * @return bool */ function trustbio_feature_enabled( string $trustbio_feature ): bool { $trustbio_stored = get_option( 'trustbio_enabled_features', null ); if ( null === $trustbio_stored ) { return true; // Option not set yet → all features on for existing installs. } return in_array( $trustbio_feature, (array) $trustbio_stored, true ); } function trustbio_get_inline_review_html( int $trustbio_post_id, int $trustbio_author_id, string $trustbio_accent_col = '#6b7280' ): string { if ( ! $trustbio_post_id || ! $trustbio_author_id ) { return ''; } $trustbio_reviewer_id = (int) get_post_meta( $trustbio_post_id, TrustBio_Review_Date::META_REVIEWER, true ); if ( $trustbio_reviewer_id !== $trustbio_author_id ) { return ''; } $trustbio_review = new TrustBio_Review_Date(); return $trustbio_review->render_inline_review( $trustbio_post_id, $trustbio_accent_col ); }