card metadata. */ public static function get_keyword_index() { $cached_index = get_transient( self::CACHE_KEY ); if ( false !== $cached_index && is_array( $cached_index ) ) { return $cached_index; } $index = array(); $boxes = get_posts( array( 'post_type' => 'affistyle_box', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( empty( $boxes ) || ! is_array( $boxes ) ) { set_transient( self::CACHE_KEY, $index, 24 * HOUR_IN_SECONDS ); return $index; } foreach ( $boxes as $box_id ) { $box_id = absint( $box_id ); $keywords = get_post_meta( $box_id, '_affistyle_keywords', true ); if ( empty( $keywords ) ) { continue; } // Support both array and comma-separated string formats. if ( is_string( $keywords ) ) { $keywords = explode( ',', $keywords ); } if ( ! is_array( $keywords ) ) { continue; } $title = get_post_meta( $box_id, '_affistyle_title_1', true ); $price = get_post_meta( $box_id, '_affistyle_price_1', true ); $rating = get_post_meta( $box_id, '_affistyle_rating_score', true ); $image = get_post_meta( $box_id, '_affistyle_image_1', true ); $button_text = get_post_meta( $box_id, '_affistyle_button_text', true ); if ( empty( $title ) ) { $title = get_the_title( $box_id ); } if ( empty( $image ) ) { $image = get_post_meta( $box_id, '_affistyle_image_url', true ); } if ( empty( $button_text ) ) { $button_text = __( 'Check Deal', 'affistyle' ); } $cloaked_url = class_exists( 'AffiStyle_Cloaking' ) ? AffiStyle_Cloaking::get_cloaked_url( $box_id, 1 ) : get_permalink( $box_id ); // Append tracking source to the destination. $cloaked_url = add_query_arg( array( 'affistyle_source' => 'keyword_auto_link', 'aff_src' => 'keyword_auto_link', ), $cloaked_url ); $card_data = array( 'box_id' => $box_id, 'title' => $title, 'price' => $price, 'rating' => $rating, 'image' => $image, 'button_text' => $button_text, 'cloaked_url' => $cloaked_url, ); foreach ( $keywords as $raw_kw ) { $clean_kw = trim( (string) $raw_kw ); if ( '' === $clean_kw ) { continue; } $normalized_kw = function_exists( 'mb_strtolower' ) ? mb_strtolower( $clean_kw, 'UTF-8' ) : strtolower( $clean_kw ); // Avoid overwriting a more specific keyword if already set. if ( ! isset( $index[ $normalized_kw ] ) ) { $index[ $normalized_kw ] = $card_data; } } } set_transient( self::CACHE_KEY, $index, 24 * HOUR_IN_SECONDS ); return $index; } /** * Main the_content filter callback. Parses and injects auto-links safely. * * @param string $content Post content HTML. * @return string Filtered post content. */ public function filter_content( $content ) { // Guard clause 1: Verify string input. if ( empty( $content ) || ! is_string( $content ) ) { return $content; } // Guard clause 2: Skip in admin, feeds, XML-RPC, and REST API requests. if ( is_admin() || is_feed() || wp_is_json_request() ) { return $content; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return $content; } // Guard clause 3: Allow external filters to bypass auto-linking (e.g. Pro post opt-out). $can_process = apply_filters( 'affistyle_autolink_can_process', true, $content, get_the_ID() ); if ( true !== $can_process ) { return $content; } // Retrieve active keyword dictionary. $keyword_index = static::get_keyword_index(); if ( empty( $keyword_index ) || ! is_array( $keyword_index ) ) { return $content; } // Sort keywords by string length in descending order to avoid prefix collision. uksort( $keyword_index, array( $this, 'sort_by_length_desc' ) ); // Link limits. $max_total_links = apply_filters( 'affistyle_autolink_max_links_per_post', 2, get_the_ID() ); $max_per_keyword = apply_filters( 'affistyle_autolink_max_per_keyword', 1, get_the_ID() ); $total_links_done = 0; $keyword_counts = array(); // Compile the master alternation regex pattern. $escaped_keywords = array(); foreach ( array_keys( $keyword_index ) as $kw ) { $escaped_keywords[] = preg_quote( $kw, '/' ); } $master_pattern = '/(?]+>)/u', $content, -1, PREG_SPLIT_DELIM_CAPTURE ); if ( empty( $tokens ) || ! is_array( $tokens ) ) { return $content; } $output_buffer = ''; $tag_stack = array(); // List of HTML tags inside which text must never be linked. $protected_tags = array( 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'script', 'style', 'code', 'pre', 'kbd', 'button', 'input', 'select', 'textarea', 'svg', 'canvas', ); foreach ( $tokens as $token_idx => $token ) { // Fast exit: If maximum links reached for this post, append the remaining tokens and stop. if ( $total_links_done >= $max_total_links ) { $output_buffer .= implode( '', array_slice( $tokens, $token_idx ) ); break; } // Check if token is an HTML tag. if ( isset( $token[0] ) && '<' === $token[0] && '>' === substr( $token, -1 ) ) { // 1. Closing tag: if ( preg_match( '#^#i', $token, $close_match ) ) { $closing_tag = strtolower( $close_match[1] ); // Pop from tag stack. $pos = array_search( $closing_tag, $tag_stack, true ); if ( false !== $pos ) { array_splice( $tag_stack, $pos, 1 ); } } elseif ( ! preg_match( '#/>$#', $token ) ) { // 2. Opening tag (excluding self-closing tags like ,
,
). if ( preg_match( '#^<([a-z0-9_-]+)(\s[^>]*)?>#i', $token, $open_match ) ) { $opening_tag = strtolower( $open_match[1] ); $tag_attrs = isset( $open_match[2] ) ? $open_match[2] : ''; // Void tags in HTML5 that never have a closing tag. $void_tags = array( 'img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'param', 'source', 'track', 'wbr' ); if ( ! in_array( $opening_tag, $void_tags, true ) ) { // Check if tag is explicitly protected or has no-autolink classes. $is_protected = in_array( $opening_tag, $protected_tags, true ); if ( ! $is_protected && ! empty( $tag_attrs ) ) { if ( preg_match( '/class=["\'][^"\']*(affistyle-card|affistyle-box-container|affistyle-no-autolink)[^"\']*["\']/i', $tag_attrs ) ) { $is_protected = true; } if ( false !== stripos( $tag_attrs, 'data-no-autolink' ) ) { $is_protected = true; } } if ( $is_protected ) { $tag_stack[] = $opening_tag; } } } } $output_buffer .= $token; continue; } // If inside any protected tag, pass text token untouched. if ( ! empty( $tag_stack ) ) { $output_buffer .= $token; continue; } // Skip token if it does not contain any of the target keywords. if ( ! preg_match( $master_pattern, $token ) ) { $output_buffer .= $token; continue; } // Process pure text token using safe preg_replace_callback. $processed_text = preg_replace_callback( $master_pattern, function ( $matches ) use ( &$total_links_done, &$keyword_counts, $max_total_links, $max_per_keyword, $keyword_index ) { $match_text = $matches[0]; if ( $total_links_done >= $max_total_links ) { return $match_text; } $normalized_match = function_exists( 'mb_strtolower' ) ? mb_strtolower( $match_text, 'UTF-8' ) : strtolower( $match_text ); if ( ! isset( $keyword_index[ $normalized_match ] ) ) { return $match_text; } $current_kw_count = isset( $keyword_counts[ $normalized_match ] ) ? $keyword_counts[ $normalized_match ] : 0; if ( $current_kw_count >= $max_per_keyword ) { return $match_text; } $item_data = $keyword_index[ $normalized_match ]; $box_id = absint( $item_data['box_id'] ); $cloaked_url = esc_url( $item_data['cloaked_url'] ); $rel_attr = class_exists( 'AffiStyle_Compliance' ) ? AffiStyle_Compliance::get_rel_attributes( $box_id ) : 'nofollow sponsored noopener'; $target_attr = class_exists( 'AffiStyle_Compliance' ) ? AffiStyle_Compliance::get_link_target( $box_id ) : '_blank'; $link_classes = apply_filters( 'affistyle_autolink_link_classes', array( 'affistyle-autolink' ), $box_id, $item_data ); if ( is_array( $link_classes ) ) { $link_classes = implode( ' ', array_map( 'sanitize_html_class', $link_classes ) ); } else { $link_classes = sanitize_text_field( (string) $link_classes ); } $extra_attrs = apply_filters( 'affistyle_autolink_anchor_attributes', '', $box_id, $normalized_match, $item_data ); // Increment counters. $total_links_done++; $keyword_counts[ $normalized_match ] = $current_kw_count + 1; // Construct the HTML anchor. return sprintf( '%7$s', $cloaked_url, esc_attr( $link_classes ), esc_attr( $target_attr ), esc_attr( $rel_attr ), $box_id, ! empty( $extra_attrs ) ? ' ' . trim( $extra_attrs ) : '', esc_html( $match_text ) ); }, $token ); $output_buffer .= $processed_text; } // If auto-links were injected, ensure frontend styling is enqueued. if ( $total_links_done > 0 && function_exists( 'wp_enqueue_style' ) && defined( 'AFFISTYLE_URL' ) && defined( 'AFFISTYLE_VERSION' ) ) { if ( ! wp_style_is( 'affistyle-frontend-css', 'enqueued' ) && ! wp_style_is( 'affistyle-block-frontend-css', 'enqueued' ) ) { wp_enqueue_style( 'affistyle-frontend-css', AFFISTYLE_URL . 'assets/css/affistyle-frontend.css', array(), AFFISTYLE_VERSION ); } } return $output_buffer; } /** * Helper comparison callback to sort keywords by length descending. * * @param string $a First keyword. * @param string $b Second keyword. * @return int Comparison result. */ private function sort_by_length_desc( $a, $b ) { $len_a = function_exists( 'mb_strlen' ) ? mb_strlen( $a, 'UTF-8' ) : strlen( $a ); $len_b = function_exists( 'mb_strlen' ) ? mb_strlen( $b, 'UTF-8' ) : strlen( $b ); if ( $len_a === $len_b ) { return 0; } return ( $len_a > $len_b ) ? -1 : 1; } }