$value ) { $css = ''; foreach ( $value as $j => $val ) { if ( 'font-family' === $j && 'Default' === $val ) { continue; } if ( ! empty( $val ) ) { $sanitized_val = self::sanitize_css_value( $val ); if ( 'font-family' === $j ) { $css .= $j . ': "' . $sanitized_val . '";'; } else { $css .= $j . ': ' . $sanitized_val . ';'; } } } if ( ! empty( $css ) ) { $styling_css .= $id; $styling_css .= $key . '{'; $styling_css .= $css . '}'; } } return $styling_css; } /** * Sanitize a CSS value to prevent XSS. * * @param string $value The CSS value to sanitize. * @return string Sanitized CSS value. * @since 1.3.9.4 */ public static function sanitize_css_value( $value ) { if ( ! is_string( $value ) ) { return $value; } $value = wp_strip_all_tags( $value ); $value = preg_replace( '/[<>"\']/', '', $value ); $value = preg_replace( '/[\x00-\x1F\x7F]/', '', $value ); return $value; } /** * Get CSS value * * Syntax: * * get_css_value( VALUE, UNIT ); * * E.g. * * get_css_value( VALUE, 'em' ); * * @param string $value CSS value. * @param string $unit CSS unit. * @since x.x.x */ public static function get_css_value( $value = '', $unit = '' ) { if ( '' === $value ) { return $value; } $css_val = ''; if ( ! empty( $value ) ) { $css_val = esc_attr( $value ) . $unit; } return $css_val; } public static function get_css_boxshadow( $v ) { if ( isset( $v['enable'] ) && $v['enable'] === true ) { $h_offset = isset( $v['h_offset'] ) ? $v['h_offset'] : 0; $v_offset = isset( $v['v_offset'] ) ? $v['v_offset'] : 5; $blur = isset( $v['blur'] ) ? $v['blur'] : 20; $spread = isset( $v['spread'] ) ? $v['spread'] : 0; $color = isset( $v['color']['color'] ) ? $v['color']['color'] : 'rgba(210,213,218,0.2)'; $inset = isset( $v['inset'] ) && $v['inset'] === true ? 'inset' : ''; return $h_offset . 'px ' . $v_offset . 'px ' . $blur . 'px ' . $spread . 'px ' . $color . $inset; } else { return 'none'; } } public static function get_fontweight_variation( $variation ) { $fontType = $variation[1]; $fontWeight = (int) $fontType * 100; return $fontWeight; } public static function get_font_style( $variation ) { $variationType = $variation[0]; $font = $variationType === 'n' ? 'normal' : ( $variationType === 'i' ? 'italic' : 'Default' ); return $font; } public static function validate_tag( $tag, $default = 'h2' ) { $allowed_tags = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p' ); return in_array( $tag, $allowed_tags, true ) ? $tag : $default; } const STAR_PATH = 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z'; /** * Render a 5 star rating, supporting fractional values such as 4.3. * * @param float|string $rating Rating value between 0 and 5. * @param string $fill_color Color of the filled portion. * @param string $empty_color Color of the empty portion. * @param int|string $size Star size in pixels. * @param array $args { * @type bool $inline_size Whether to output inline width/height. Default true. * @type string $wrapper_class Optional wrapper span class. * } * @return string */ public static function render_stars( $rating, $fill_color, $empty_color, $size = 25, $args = array() ) { $rating = max( 0, min( 5, (float) $rating ) ); $size = is_numeric( $size ) ? (int) $size : 25; $inline_size = ! isset( $args['inline_size'] ) || $args['inline_size']; $wrapper_class = $args['wrapper_class'] ?? ''; $dimensions = $inline_size ? sprintf( 'width:%1$dpx;height:%1$dpx;', $size ) : ''; $icon = sprintf( '', esc_attr( $size ), esc_attr( self::STAR_PATH ) ); $stars = ''; for ( $i = 1; $i <= 5; $i++ ) { $fill_percent = min( 1, max( 0, $rating - $i + 1 ) ) * 100; $is_partial = $fill_percent > 0 && $fill_percent < 100; $is_inactive = $fill_percent < 100; $classes = 'affx-star'; $classes .= $is_inactive ? ' affx-star-inactive' : ''; $classes .= $is_partial ? ' affx-star-partial' : ''; $partial = $is_partial ? sprintf( '%s', esc_attr( $fill_color ), esc_attr( round( $fill_percent, 2 ) ), $icon ) : ''; $stars .= sprintf( '%s%s', esc_attr( $classes ), esc_attr( $is_inactive ? $empty_color : $fill_color ), esc_attr( $dimensions ), $icon, $partial ); } if ( $wrapper_class ) { return sprintf( '%s', esc_attr( $wrapper_class ), $stars ); } return $stars; } /** * Render a list * * @param array $args { * @type string $listType 'unordered' or 'ordered' * @type string $unorderedType 'icon' or 'bullet' * @type array $listItems Array of list items * @type string $iconName Optional icon class name * } * @return string */ public static function render_list( $args ) { $defaults = array( 'listType' => 'unordered', 'unorderedType' => 'icon', 'listItems' => array(), 'iconName' => '', 'wrapperClass' => '', ); $args = wp_parse_args( $args, $defaults ); extract( $args ); $listTag = $listType === 'unordered' ? 'ul' : 'ol'; $wrapperClasses = array( 'affiliatex-list' ); $wrapperClasses[] .= 'affiliatex-list-type-' . $listType; $wrapperClasses[] .= $unorderedType === 'icon' ? 'afx-icon-list' : 'bullet'; if ( is_string( $wrapperClass ) && '' !== $wrapperClass ) { $wrapperClasses[] = $wrapperClass; } // Parse Amazon shortcode if listItems is a string. if ( is_string( $listItems ) ) { $listItems = affx_maybe_parse_amazon_shortcode( $listItems ); } ob_start(); include AFFILIATEX_PLUGIN_DIR . '/templates/components/list.php'; return ob_get_clean(); } /** * Convert an associative array of html element attributes into string format. * * @param array $args An associative array of attribute keys and values. * @return string */ public static function array_to_attributes( array $args = array() ): string { $string = ''; if ( is_array( $args ) && ! empty( $args ) ) { $string = implode( ' ', array_map( function ( $key, $value ) { if ( is_array( $value ) ) { $value = implode( ' ', $value ); } return esc_attr( $key ) . '="' . esc_attr( $value ) . '"'; }, array_keys( $args ), $args ) ); } return $string; } /** * This function handles both media library images and images with url * * @param int $image_id The WordPress media library image ID * @param string $image_url The image URL * @param string $image_alt The image alt text * @param bool $is_sitestripe Whether to show sitestripe * @param string $sitestripe The sitestripe HTML content * @param string $size The image size. * @return string The image HTML or empty string if no image available */ public static function affiliatex_get_media_image_html( $image_id = 0, $image_url = '', $image_alt = '', $is_sitestripe = false, $sitestripe = '', $size = 'full' ) { if ( $is_sitestripe && ! empty( $sitestripe ) ) { return $sitestripe; } if ( ! empty( $image_id ) && wp_attachment_is_image( $image_id ) ) { // Setting height to auto to prevent image distortion $image = wp_get_attachment_image( $image_id, $size, false, array( 'style' => 'height: auto;' ) ); return $image; } if ( ! empty( $image_url ) ) { $fallback_url_bc = str_replace( 'app/src/images/fallback.jpg', 'src/images/fallback.jpg', $image_url ); $processed_url = do_shortcode( $fallback_url_bc ); $escaped_url = esc_url( $processed_url ); return sprintf( '%s', $escaped_url, esc_attr( $image_alt ) ); } return ''; } /** * Check if the current page has AffiliateX Elementor widgets. * * @param int|null $post_id Optional. Post ID to check. Defaults to current post. * @return bool */ public static function has_elementor_widgets( $post_id = null ) { if ( ! class_exists( '\Elementor\Plugin' ) ) { return false; } if ( ! $post_id ) { $post_id = get_the_ID(); } if ( ! $post_id || ! \Elementor\Plugin::$instance->db->is_built_with_elementor( $post_id ) ) { return false; } $elementor_data = get_post_meta( $post_id, '_elementor_data', true ); return $elementor_data && ( strpos( $elementor_data, 'affiliatex-' ) !== false || strpos( $elementor_data, '"widgetType":"affx-' ) !== false ); } /** * Is_affiliatex_block - Returns true when viewing a page with AffiliateX blocks. * * Checks for both Gutenberg blocks. * * @return bool */ public static function is_affiliatex_block() { $affx_block = has_block( 'affiliatex/buttons' ) || has_block( 'affiliatex/pros-and-cons' ) || has_block( 'affiliatex/cta' ) || has_block( 'affiliatex/notice' ) || has_block( 'affiliatex/verdict' ) || has_block( 'affiliatex/single-product' ) || has_block( 'affiliatex/specifications' ) || has_block( 'affiliatex/versus-line' ) || has_block( 'affiliatex/single-product-pros-and-cons' ) || has_block( 'affiliatex/product-image-button' ) || has_block( 'affiliatex/single-coupon' ) || has_block( 'affiliatex/coupon-grid' ) || has_block( 'affiliatex/product-tabs' ) || has_block( 'affiliatex/coupon-listing' ) || has_block( 'affiliatex/top-products' ) || has_block( 'affiliatex/versus' ) || has_block( 'affiliatex/product-table' ) || has_block( 'affiliatex/product-comparison' ) || has_block( 'affiliatex/rating-box' ) || has_block( 'affiliatex/dynamic-listing' ); return apply_filters( 'is_affiliatex_block', $affx_block ); } /** * Returns true when viewing a page with AffiliateX blocks or widgets. * * Checks for both Gutenberg blocks and Elementor widgets. * * @return bool */ public static function post_has_affiliatex_items() { return apply_filters( 'post_has_affiliatex_items', self::is_affiliatex_block() || self::has_elementor_widgets() ); } /** * Generate wrapper tag and attributes for clickable blocks. * * @param array $attributes Array of necessary attributes: edFullBlockLink, blockUrl, blockRelNoFollow, blockRelSponsored, blockDownload, blockOpenInNewTab * * @since 1.3.9 * @return array Array with 'tag' and 'attributes' keys */ public static function get_clickable_wrapper_config( array $attributes ) { extract( $attributes ); if ( ! $edFullBlockLink || empty( $blockUrl ) ) { return array( 'tag' => 'div', 'attributes' => '', ); } $rel_attributes = array( 'noopener' ); if ( $blockRelNoFollow ) { $rel_attributes[] = 'nofollow'; } if ( $blockRelSponsored ) { $rel_attributes[] = 'sponsored'; } $attributes = sprintf( 'href="%s"', esc_url( do_shortcode( $blockUrl ) ) ); if ( ! empty( $rel_attributes ) ) { $attributes .= sprintf( ' rel="%s"', esc_attr( implode( ' ', $rel_attributes ) ) ); } if ( $blockOpenInNewTab ) { $attributes .= ' target="_blank"'; } if ( $blockDownload ) { $attributes .= ' download'; } return array( 'tag' => 'a', 'attributes' => $attributes, ); } /** * Remove inline tags while keeping their inner content. Used for * RichText fields rendered inside a wrapper anchor, where nested anchors * are invalid HTML5 and the browser parser silently breaks the layout. * * @param string $content * @return string */ public static function strip_anchor_tags( $content ) { if ( ! is_string( $content ) || $content === '' ) { return $content; } return preg_replace( '#]*>#i', '', $content ); } /** * Escape HTML content while allowing SVG elements. * * @param string $content The content to escape. * @return string Escaped content with SVG allowed. */ public static function kses( $content ) { $allowed_html = array_merge( wp_kses_allowed_html( 'post' ), array( 'svg' => array( 'class' => true, 'aria-hidden' => true, 'aria-labelledby' => true, 'role' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), 'g' => array( 'fill' => true, 'stroke' => true, 'stroke-width' => true, ), ) ); return wp_kses( $content, $allowed_html ); } /** * Generate data attributes for Read More wrapper element. * * @param array $config { * Configuration array for Read More attributes. * * @type bool $edReadMore Whether Read More is enabled. * @type string $productContentType Content type: 'paragraph', 'list', or 'amazon'. * @type int $descriptionLength Character limit for truncation. Default 150. * @type int $listItemCount Item limit for list truncation. Default 3. * } * @return string HTML data attributes string or empty string if Read More is disabled. */ public static function get_readmore_attrs( $config ) { extract( $config ); $data_attrs = ''; if ( ! empty( $edReadMore ) ) { $content_type = ( $productContentType === 'list' || $productContentType === 'amazon' ) ? 'list' : 'paragraph'; $char_limit = $descriptionLength ?? 150; $item_limit = $listItemCount ?? 3; $data_attrs = sprintf( ' data-content-type="%s" data-char-limit="%d" data-item-limit="%d"', esc_attr( $content_type ), absint( $char_limit ), absint( $item_limit ) ); } return $data_attrs; } /** * Get Read More button HTML. * * @param bool $enabled Whether read more is enabled. * @param string $read_more_text Read more button text. * @param string $read_less_text Read less button text. * @return string Button HTML or empty string. */ public static function get_readmore_btn( $enabled, $read_more_text = 'Read more', $read_less_text = 'Read less' ) { if ( empty( $enabled ) ) { return ''; } return sprintf( '', esc_attr( $read_more_text ), esc_attr( $read_less_text ), esc_html( $read_more_text ) ); } /** * Resolve a responsive attribute value for a device, legacy scalars pass through. * * Cascade: empty tablet inherits desktop, empty mobile inherits tablet. * * @param mixed $value Scalar or {desktop,tablet,mobile} object. * @param string $device One of 'desktop', 'tablet', 'mobile'. * @return mixed */ public static function get_responsive_value( $value, string $device = 'desktop' ) { if ( ! is_array( $value ) || ! array_key_exists( 'desktop', $value ) ) { return $value; } $desktop = $value['desktop']; $tablet = self::is_empty_device_value( $value['tablet'] ?? null ) ? $desktop : $value['tablet']; $mobile = self::is_empty_device_value( $value['mobile'] ?? null ) ? $tablet : $value['mobile']; if ( 'tablet' === $device ) { return $tablet; } if ( 'mobile' === $device ) { return $mobile; } return $desktop; } /** * Whether a per-device value should inherit from the larger device. * * @param mixed $value Device value. * @return bool */ private static function is_empty_device_value( $value ): bool { return null === $value || '' === $value; } /** * Build affx-hide-* classes from a {desktop,tablet,mobile} visibility attribute. * * @param mixed $hide_on Visibility attribute value, may be missing on old content. * @return string Space separated classes, empty string when nothing is hidden. */ public static function get_responsive_hide_classes( $hide_on ): string { if ( ! is_array( $hide_on ) ) { return ''; } $classes = array(); foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { if ( ! empty( $hide_on[ $device ] ) && 'false' !== $hide_on[ $device ] ) { $classes[] = 'affx-hide-' . $device; } } return implode( ' ', $classes ); } }