*/ class Affreviews_Blocks { /** * The ID of this plugin. * * @since 1.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * The version of this plugin. * * @since 1.0.0 * @access private * @var string $version The current version of this plugin. */ private $version; /** * Initialize the class and set its properties. * * @since 1.0.0 * @param string $plugin_name The name of the plugin. * @param string $version The version of this plugin. */ public function __construct( $plugin_name, $version ) { $this->plugin_name = $plugin_name; $this->version = $version; } /** * Register our blocks. * * @since 1.0.0 */ public function register_blocks() { $table = register_block_type( AFFREVIEWS_DIR . 'blocks/build/table', array( 'render_callback' => array( $this, 'callback_block_table' ), ) ); $grid = register_block_type( AFFREVIEWS_DIR . 'blocks/build/grid', array( 'render_callback' => array( $this, 'callback_block_grid' ), ) ); $table_large = register_block_type( AFFREVIEWS_DIR . 'blocks/build/table-large', array( 'render_callback' => array( $this, 'callback_block_table_large' ), ) ); $this->add_loop_pattern_data( array( 'grid' => array( $grid, 'review-grid-s1' ), 'table' => array( $table, 'review-table-s1' ), 'table-large' => array( $table_large, 'review-table-large-s1' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/single', array( 'render_callback' => array( $this, 'callback_block_single' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/single-large', array( 'render_callback' => array( $this, 'callback_block_single_large' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/pros-cons', array( 'render_callback' => array( $this, 'callback_block_pros_cons' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/info-list', array( 'render_callback' => array( $this, 'callback_block_info_list' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/rating-stars', array( 'render_callback' => array( $this, 'callback_block_rating_stars' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/pros-cons-list', array( 'render_callback' => array( $this, 'callback_block_pros_cons_list' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/info-spec-list', array( 'render_callback' => array( $this, 'callback_block_info_spec_list' ), ) ); $affiliate_button = register_block_type( AFFREVIEWS_DIR . 'blocks/build/affiliate-button', array( 'render_callback' => array( $this, 'callback_block_affiliate_button' ), ) ); // Site link defaults, shown in the editor while the block doesn't override them. if ( $affiliate_button && $affiliate_button->editor_script_handles ) { $site_link = affreviews_get_aff_link_settings(); wp_add_inline_script( $affiliate_button->editor_script_handles[0], 'var affreviewsLinkDefaults = ' . wp_json_encode( array( 'rel' => $site_link['rel'], 'newTab' => $site_link['new_tab'], ) ) . ';', 'before' ); } register_block_type( AFFREVIEWS_DIR . 'blocks/build/review-link', array( 'render_callback' => array( $this, 'callback_block_review_link' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/review-description', array( 'render_callback' => array( $this, 'callback_block_review_description' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/review-tag', array( 'render_callback' => array( $this, 'callback_block_review_tag' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/bonus-badge', array( 'render_callback' => array( $this, 'callback_block_bonus_badge' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/logo-thumbnail', array( 'render_callback' => array( $this, 'callback_block_logo_thumbnail' ), ) ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/review-context' ); register_block_type( AFFREVIEWS_DIR . 'blocks/build/review-terms', array( 'render_callback' => array( $this, 'callback_block_review_terms' ), ) ); } /** * Callback for table block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_table( $args, $content ) { $query_args = $this->build_blocks_query_args( $args ); $block_query = new WP_Query( $query_args ); $output = ''; $params = array( 'block_query' => $block_query, 'args' => $args, ); if ( $block_query->have_posts() ) { ob_start(); affreviews_get_template( 'blocks/block-reviews-table-style.php', true, false, $params ); $output = ob_get_clean(); } wp_reset_postdata(); return $output; } /** * Callback for grid block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_grid( $args, $content ) { $query_args = $this->build_blocks_query_args( $args ); $block_query = new WP_Query( $query_args ); $output = ''; $params = array( 'block_query' => $block_query, 'args' => $args, ); if ( $block_query->have_posts() ) { ob_start(); affreviews_get_template( 'blocks/block-reviews-grid-style.php', true, false, $params ); $output = ob_get_clean(); } wp_reset_postdata(); return $output; } /** * Callback for table large block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_table_large( $args, $content ) { $query_args = $this->build_blocks_query_args( $args ); $block_query = new WP_Query( $query_args ); $output = ''; $params = array( 'block_query' => $block_query, 'args' => $args, ); if ( $block_query->have_posts() ) { ob_start(); affreviews_get_template( 'blocks/block-reviews-table-style-large.php', true, false, $params ); $output = ob_get_clean(); } wp_reset_postdata(); return $output; } /** * Create the query for tables and lists * * @param [type] $args * @return array */ public function build_blocks_query_args( $args ) { $num = $args['num'] ?? '6'; $selected_posts = $args['selectedPosts'] ?? ''; $categories = $args['categories'] ?? ''; $orderby = $args['orderby'] ?? ''; $order = $args['order'] ?? ''; $output = ''; $query_args = array( 'post_type' => AFFREVIEWS_CPT, 'posts_per_page' => $num, 'post_status' => 'publish', 'ignore_sticky_posts' => true, 'no_found_rows' => true, ); // Ordering and define spefici reviews if ( ! empty( $selected_posts ) && is_array( $selected_posts ) ) { $query_args['post__in'] = $selected_posts; $query_args['orderby'] = 'post__in'; $query_args['order'] = 'ASC'; } else { if ( 'rating' === $orderby ) { $query_args['meta_key'] = 'affreviews_rating'; $query_args['orderby'] = 'meta_value_num'; } else { $query_args['orderby'] = $orderby; } $query_args['order'] = $order; } // Specific terms if ( ! empty( $categories ) && is_array( $categories ) ) { $query_args['tax_query'] = array( array( 'taxonomy' => AFFREVIEWS_TAX, 'field' => 'term_id', 'terms' => $categories, 'operator' => 'AND', ), ); } return $query_args; } /** * Callback for Single block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_single( $args, $content ) { $output = ''; if ( ! $this->can_show_selected_review( $args ) ) { return $output; } ob_start(); affreviews_get_template( 'blocks/block-reviews-single.php', true, false, $args ); $output = ob_get_clean(); return $output; } /** * Callback for Single block large * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_single_large( $args, $content ) { $output = ''; if ( ! $this->can_show_selected_review( $args ) ) { return $output; } ob_start(); affreviews_get_template( 'blocks/block-reviews-single-large.php', true, false, $args ); $output = ob_get_clean(); return $output; } /** * Callback for ProsCons block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_pros_cons( $args, $content ) { $output = ''; if ( ! $this->can_show_selected_review( $args ) ) { return $output; } ob_start(); affreviews_get_template( 'blocks/block-reviews-pros-cons.php', true, false, $args ); $output = ob_get_clean(); return $output; } /** * Callback for Info List block * * @param [type] $args * @param [type] $content * @return void */ public function callback_block_info_list( $args, $content ) { $output = ''; if ( ! $this->can_show_selected_review( $args ) ) { return $output; } ob_start(); affreviews_get_template( 'blocks/block-reviews-info-list.php', true, false, $args ); $output = ob_get_clean(); return $output; } /** * Whether a legacy single-review block may show its selected review: a * published one, or one the current user can read, as for the v2 blocks. * * IDs of posts that no longer exist still render as before, so legacy * markup only changes where an unpublished review used to leak. * * @param array $args Block attributes. * @return bool */ private function can_show_selected_review( $args ) { $post_id = ! empty( $args['selectedPost'] ) ? (int) $args['selectedPost'] : 0; $post = $post_id ? get_post( $post_id ) : null; return ! $post || 'publish' === $post->post_status || current_user_can( 'read_post', $post_id ); } /** * Resolve which review an atomic block displays. * * Order: the block's own selectedPost, then the postId context (Query Loop * or Review Context wrapper), then the current post. In the editor preview * the block renderer endpoint sets the current post from its post_id arg. * * When none is found and the editor asked for sample data, the sample * review's ID is returned instead (see Affreviews_Sample_Review). * * @param array $attributes Block attributes. * @param WP_Block|null $block Block instance. * @return int Review ID, or 0 when there is no review the visitor may see. */ private function resolve_review_id( $attributes, $block ) { $post_id = ! empty( $attributes['selectedPost'] ) ? (int) $attributes['selectedPost'] : 0; if ( ! $post_id && $block instanceof WP_Block && ! empty( $block->context['postId'] ) ) { $post_id = (int) $block->context['postId']; } if ( ! $post_id ) { $post_id = (int) get_the_ID(); } if ( ! $post_id || AFFREVIEWS_CPT !== get_post_type( $post_id ) ) { return Affreviews_Sample_Review::is_requested() ? Affreviews_Sample_Review::id() : 0; } if ( 'publish' !== get_post_status( $post_id ) && ! current_user_can( 'read_post', $post_id ) ) { return 0; } return $post_id; } /** * Global posts replaced by Review Context wrappers, restored in order. * * @var array */ private $review_context_posts = array(); /** * Make the wrapper's review the current post while its inner blocks render. * * The wrapper passes its review as postId context, but some core blocks * read the global post instead (core/post-title calls get_the_title() * without an ID), so on the front end they showed the page being viewed. * This does what the Query Loop does for each item. Same visibility rules * as resolve_review_id(): published, or readable by the current user. * * @param string|null $pre_render Short-circuit content (passed through). * @param array $parsed_block Block being rendered. * @return string|null */ public function setup_review_context_post( $pre_render, $parsed_block ) { if ( 'affreviews/review-context' !== $parsed_block['blockName'] || null !== $pre_render ) { return $pre_render; } global $post; $this->review_context_posts[] = $post; $review_id = (int) ( $parsed_block['attrs']['selectedPost'] ?? 0 ); if ( $review_id && AFFREVIEWS_CPT === get_post_type( $review_id ) && ( 'publish' === get_post_status( $review_id ) || current_user_can( 'read_post', $review_id ) ) ) { $post = get_post( $review_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- restored in reset_review_context_post(). setup_postdata( $post ); } return $pre_render; } /** * Restore the global post after a Review Context wrapper has rendered. * * @param string $block_content Rendered wrapper. * @return string */ public function reset_review_context_post( $block_content ) { global $post; if ( $this->review_context_posts ) { $post = array_pop( $this->review_context_posts ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- restores the post replaced above. if ( $post instanceof WP_Post ) { setup_postdata( $post ); } } return $block_content; } /** * Wrap block output in a div carrying the block supports (colors, * typography, spacing), custom class name and anchor. * * @param string $html Inner HTML. * @param string $class Extra class names for the wrapper. * @return string */ private function block_wrapper( $html, $class = '' ) { $extra = $class ? array( 'class' => $class ) : array(); return '
' . $html . '
'; } /** * Callback for Rating Stars block */ public function callback_block_rating_stars( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); if ( ! $post_id ) { return ''; } $rating_num = affreviews_get_rating_num( $post_id ); $show_num = ! empty( $args['showNumber'] ); $score_html = ( $show_num && $rating_num ) ? '' . esc_html( number_format( $rating_num, 1 ) ) . '' : ''; $size_class = ( isset( $args['starSize'] ) && 'small' === $args['starSize'] ) ? 'is-star-size-small' : ''; return $this->block_wrapper( wp_kses( affreviews_get_rating( $post_id ), affreviews_kses_extended_ruleset() ) . $score_html, $size_class ); } /** * Callback for Pros & Cons List block */ public function callback_block_pros_cons_list( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); if ( ! $post_id ) { return ''; } $max_items = isset( $args['maxItems'] ) ? intval( $args['maxItems'] ) : 10; $show_type = isset( $args['showType'] ) ? $args['showType'] : 'all'; $show_title = ! isset( $args['showTitles'] ) || $args['showTitles']; $pros_title = ! empty( $args['positivesTitle'] ) ? $args['positivesTitle'] : __( 'Positives', 'affiliate-reviews' ); $cons_title = ! empty( $args['negativesTitle'] ) ? $args['negativesTitle'] : __( 'Negatives', 'affiliate-reviews' ); $pros = ( 'cons' !== $show_type ) ? affreviews_get_pros( $post_id, $max_items ) : ''; $cons = ( 'pros' !== $show_type ) ? affreviews_get_cons( $post_id, $max_items ) : ''; if ( ! $pros && ! $cons ) { return ''; } $output = '
'; if ( $pros ) { $output .= '
' . ( $show_title ? '' . esc_html( $pros_title ) . '' : '' ) . wp_kses( $pros, affreviews_kses_extended_ruleset() ) . '
'; } if ( $cons ) { $output .= '
' . ( $show_title ? '' . esc_html( $cons_title ) . '' : '' ) . wp_kses( $cons, affreviews_kses_extended_ruleset() ) . '
'; } $output .= '
'; return $this->block_wrapper( $output, empty( $args['stacked'] ) ? '' : 'is-stacked' ); } /** * Callback for Specifications List block */ public function callback_block_info_spec_list( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $info = $post_id ? affreviews_get_infolist( $post_id ) : ''; if ( ! $info ) { return ''; } $info_title = ! empty( $args['infoTitle'] ) ? $args['infoTitle'] : __( 'Information', 'affiliate-reviews' ); $title_html = ! isset( $args['showTitle'] ) || $args['showTitle'] ? '' . esc_html( $info_title ) . '' : ''; return $this->block_wrapper( $title_html . wp_kses_post( $info ) ); } /** * Callback for Affiliate Button block * * Links to the review's affiliate URL (affreviews_afflink), or its cloaked * link, like the legacy button. Renders nothing when the review has no * affiliate link. */ public function callback_block_affiliate_button( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $link = $post_id ? affreviews_get_aff_link_url( $post_id ) : ''; if ( empty( $link ) ) { return ''; } $button_text = ! empty( $args['buttonText'] ) ? $args['buttonText'] : __( 'Check Best Deals', 'affiliate-reviews' ); // rel and openInNewTab are unset unless the block overrides the site setting. $site_link = affreviews_get_aff_link_settings(); $rel = isset( $args['rel'] ) ? trim( $args['rel'] ) : $site_link['rel']; $link_atts = $rel ? 'rel="' . esc_attr( $rel ) . '"' : ''; if ( isset( $args['openInNewTab'] ) ? $args['openInNewTab'] : $site_link['new_tab'] ) { $link_atts .= ' target="_blank"'; } $link_atts = apply_filters( 'affreviews_aff_button_atts', trim( $link_atts ) ); $is_text_link = isset( $args['linkStyle'] ) && 'text-link' === $args['linkStyle']; $element = $this->get_link_element_styles( $args ); $wrapper_classes = array( $is_text_link ? 'is-text-link' : 'is-button' ); if ( ! empty( $args['justification'] ) && in_array( $args['justification'], array( 'left', 'center', 'right' ), true ) ) { $wrapper_classes[] = 'is-content-justification-' . $args['justification']; } if ( ! $is_text_link && ! empty( $args['width'] ) && in_array( $args['width'], array( 'auto', 'full' ), true ) ) { $wrapper_classes[] = 'is-width-' . $args['width']; } $class = trim( ( $is_text_link ? '' : 'affr-button-el ' ) . $element['classnames'] ); $a = '' . esc_html( $button_text ) . ''; return $this->block_wrapper( '', implode( ' ', $wrapper_classes ) ); } /** * Classes and inline CSS for the block settings the Affiliate Button applies * to its link instead of the wrapper (colors, typography, border, padding, * shadow; see __experimentalSkipSerialization in its block.json). * * @param array $args Block attributes. * @return array { @type string $classnames, @type string $css } */ private function get_link_element_styles( $args ) { $style = isset( $args['style'] ) && is_array( $args['style'] ) ? $args['style'] : array(); $input = array( 'color' => isset( $style['color'] ) ? $style['color'] : array(), 'typography' => isset( $style['typography'] ) ? $style['typography'] : array(), 'border' => isset( $style['border'] ) ? $style['border'] : array(), 'spacing' => isset( $style['spacing']['padding'] ) ? array( 'padding' => $style['spacing']['padding'] ) : array(), 'shadow' => isset( $style['shadow'] ) ? $style['shadow'] : null, ); // Presets picked in the editor are stored as separate attributes. $presets = array( 'textColor' => array( 'color', 'text', 'color' ), 'backgroundColor' => array( 'color', 'background', 'color' ), 'gradient' => array( 'color', 'gradient', 'gradient' ), 'fontSize' => array( 'typography', 'fontSize', 'font-size' ), 'fontFamily' => array( 'typography', 'fontFamily', 'font-family' ), 'borderColor' => array( 'border', 'color', 'color' ), ); foreach ( $presets as $attr => $target ) { if ( ! empty( $args[ $attr ] ) && is_string( $args[ $attr ] ) ) { $input[ $target[0] ][ $target[1] ] = 'var:preset|' . $target[2] . '|' . $args[ $attr ]; } } // Custom font sizes become fluid when the theme enables it, as in core. if ( ! empty( $input['typography']['fontSize'] ) && 0 !== strpos( $input['typography']['fontSize'], 'var:' ) && function_exists( 'wp_get_typography_font_size_value' ) ) { $input['typography']['fontSize'] = wp_get_typography_font_size_value( array( 'size' => $input['typography']['fontSize'] ) ); } $styles = function_exists( 'wp_style_engine_get_styles' ) ? wp_style_engine_get_styles( array_filter( $input ) ) : array(); return array( 'classnames' => isset( $styles['classnames'] ) ? $styles['classnames'] : '', 'css' => isset( $styles['css'] ) ? $styles['css'] : '', ); } /** * Callback for Review Link block * * The "Read Review" link: points to the review page, or to the external * review URL when reviews are not publicly visible. */ public function callback_block_review_link( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); if ( Affreviews_Sample_Review::is_sample( $post_id ) ) { $link = '#'; } else { $link = $post_id ? affreviews_get_review_link_url( $post_id ) : ''; } if ( empty( $link ) ) { return ''; } $link_text = ! empty( $args['linkText'] ) ? $args['linkText'] : __( 'Read Review', 'affiliate-reviews' ); return $this->block_wrapper( '' ); } /** * Callback for Review Description block */ public function callback_block_review_description( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $description = $post_id ? affreviews_get_description( $post_id ) : ''; if ( empty( $description ) ) { return ''; } return $this->block_wrapper( wp_kses_post( $description ) ); } /** * Callback for Review Tag block * * The tag colors come from the review; "overlay" pins it to the top-left * corner like the legacy loops. */ public function callback_block_review_tag( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $tag = $post_id ? affreviews_get_tag( $post_id ) : ''; if ( empty( $tag ) ) { return ''; } $placement = ( isset( $args['placement'] ) && 'overlay' === $args['placement'] ) ? 'overlay' : 'inline'; return $this->block_wrapper( wp_kses_post( $tag ), 'is-placement-' . $placement ); } /** * Callback for Bonus Badge block */ public function callback_block_bonus_badge( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $bonus = $post_id ? affreviews_get_bonus( $post_id ) : ''; if ( empty( $bonus ) ) { return ''; } return $this->block_wrapper( wp_kses_post( $bonus ) ); } /** * Callback for Logo / Thumbnail block */ public function callback_block_logo_thumbnail( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); if ( ! $post_id ) { return ''; } $frame_style = ! empty( $args['frameStyle'] ) ? $args['frameStyle'] : 'default'; if ( 'default' === $frame_style ) { // Same source as the legacy blocks: the global "Thumbnail style" setting. $settings = get_option( 'affreviews_visual_settings' ); $atts = ! empty( $settings['thumbnail_style'] ) ? array( $settings['thumbnail_style'] ) : array(); } else { $atts = ( 'none' !== $frame_style ) ? array( 'logo-' . $frame_style ) : array(); } $thumb = Affreviews_Sample_Review::is_sample( $post_id ) ? Affreviews_Sample_Review::thumb( $atts ) : affreviews_get_thumb( $post_id, $atts ); if ( empty( $thumb ) ) { return ''; } return $this->block_wrapper( wp_kses_post( $thumb ) ); } /** * Callback for Review Terms block */ public function callback_block_review_terms( $args, $content, $block = null ) { $post_id = $this->resolve_review_id( $args, $block ); $terms = $post_id ? affreviews_get_terms( $post_id ) : ''; if ( empty( $terms ) ) { return ''; } return $this->block_wrapper( wp_kses_post( $terms ) ); } /** * Load the block styles and the visual settings' CSS variables in the * editor. * * Hooked to enqueue_block_assets so they reach the iframed editor canvas * the supported way; the is_admin() check keeps it off the front end, * which loads them in Affreviews_Public. * * @return void */ public function register_editor_styles() { if ( ! is_admin() ) { return; } wp_enqueue_style( $this->plugin_name, AFFREVIEWS_URL . 'public/assets/build/public.css', array(), $this->version, 'all' ); wp_enqueue_style( $this->plugin_name . '-layouts', AFFREVIEWS_URL . 'public/assets/build/layouts.css', array( $this->plugin_name ), $this->version, 'all' ); affreviews_register_css_vars( $this->plugin_name ); } /** * Make links in the block previews inert in the editor, so clicking a * button or thumbnail selects the block instead of following the link. * * Hooked to enqueue_block_assets so it also reaches the iframed editor * canvas; the is_admin() check keeps it off the front end. Review Context * is skipped: it holds inner blocks whose links are edited in place. * * @return void */ public function register_editor_preview_styles() { if ( ! is_admin() ) { return; } $handle = $this->plugin_name . '-editor-preview'; wp_register_style( $handle, false, array(), $this->version ); wp_enqueue_style( $handle ); wp_add_inline_style( $handle, '.wp-block[data-type^="affreviews/"]:not([data-type="affreviews/review-context"]) a{pointer-events:none;cursor:default;}' ); } /** * Block category * * @return void */ public function register_block_categories() { $categories[] = array( 'slug' => 'affreviews', 'title' => 'Affiliate Reviews', ); $categories[] = array( 'slug' => 'affreviews-deprecated', 'title' => 'Affiliate Reviews – Deprecated', ); return $categories; } /** * Let Query Loop blocks query reviews when the post type is not public. * * Core only uses the block's postType when is_post_type_viewable() is * true, and falls back to "post" otherwise. Reviews are not publicly * queryable unless "reviews_visibility" is set to visible, so without * this every review loop would query posts instead. * * @param array $query Query vars built from the Query block. * @param WP_Block $block The Post Template block being rendered. * @return array */ public function filter_query_reviews_post_type( $query, $block ) { if ( isset( $block->context['query']['postType'] ) && AFFREVIEWS_CPT === $block->context['query']['postType'] ) { $query['post_type'] = AFFREVIEWS_CPT; } return $query; } /** * Apply the Query Loop taxonomy filter for review categories. * * Core skips taxQuery terms for taxonomies that are not publicly * viewable, and the review category taxonomy is never public. Without * this the editor preview (REST) filters by category but the front end * shows every review. Supports both the old taxQuery format * ({"affreviews_tax":[1]}) and the include/exclude format of WP 7.0+. * * @param array $query Query vars built from the Query block. * @param WP_Block $block The Post Template block being rendered. * @return array */ public function filter_query_review_categories( $query, $block ) { $tax_query = $block->context['query']['taxQuery'] ?? null; if ( empty( $tax_query ) || ! is_array( $tax_query ) || is_taxonomy_viewable( AFFREVIEWS_TAX ) ) { return $query; } $has_new_format = ! array_diff( array_keys( $tax_query ), array( 'include', 'exclude' ) ); $groups = $has_new_format ? array( 'IN' => $tax_query['include'][ AFFREVIEWS_TAX ] ?? array(), 'NOT IN' => $tax_query['exclude'][ AFFREVIEWS_TAX ] ?? array(), ) : array( 'IN' => $tax_query[ AFFREVIEWS_TAX ] ?? array() ); foreach ( $groups as $operator => $terms ) { $terms = array_filter( array_map( 'intval', (array) $terms ) ); if ( $terms ) { $query['tax_query'][] = array( 'taxonomy' => AFFREVIEWS_TAX, 'terms' => $terms, 'operator' => $operator, 'include_children' => false, ); } } return $query; } /** * Support rating-based ordering in Query Loop blocks. * * When a core/query block uses "affreviews_rating" as orderBy, map it to * a numeric meta_value_num query on the review rating meta key. Only the * query whose block context asks for it is changed. * * @param array $query Query vars built from the Query block. * @param WP_Block $block The Post Template block being rendered. * @return array */ public function filter_query_rating_orderby( $query, $block ) { if ( isset( $block->context['query']['orderBy'] ) && 'affreviews_rating' === $block->context['query']['orderBy'] ) { $query['meta_key'] = 'affreviews_rating'; $query['orderby'] = 'meta_value_num'; } return $query; } /** * Show only the reviews picked in the Reviews Query "Specify reviews" * setting, in the order they were picked. * * The IDs are stored in the query attribute as "affreviewsInclude". Like * the legacy blocks' "selectedPosts", picking reviews overrides the order. * Runs after filter_query_rating_orderby, so it also drops the rating * meta_key, which would otherwise hide picked reviews without a rating. * * @param array $query Query vars built from the Query block. * @param WP_Block $block The Post Template block being rendered. * @return array */ public function filter_query_selected_reviews( $query, $block ) { if ( ! empty( $block->context['query']['affreviewsInclude'] ) ) { $query = $this->apply_selected_reviews( $query, $block->context['query']['affreviewsInclude'] ); } return $query; } /** * Limit WP_Query args to the given review IDs, in that order. * * @param array $args WP_Query args. * @param array|string $ids Review IDs (array or comma-separated list). * @return array */ private function apply_selected_reviews( $args, $ids ) { $ids = wp_parse_id_list( $ids ); if ( $ids ) { $args['post__in'] = $ids; $args['orderby'] = 'post__in'; unset( $args['meta_key'] ); } return $args; } /** * Support the Reviews Query settings in the Query Loop editor preview. * * The Query Loop block's editor preview fetches posts via the REST API, * passing orderby and any extra query keys as request params. The rating * and random orders must be registered, or the REST API rejects the * request with a 400 (rest_invalid_param) and no posts render in the * editor. The rating order maps to a numeric meta query, and * "affreviewsInclude" to the picked reviews, as on the front end. * * @return void */ public function register_rest_query_args() { $post_type = AFFREVIEWS_CPT; add_filter( "rest_{$post_type}_collection_params", function ( $query_params ) { if ( isset( $query_params['orderby']['enum'] ) ) { $query_params['orderby']['enum'] = array_values( array_unique( array_merge( $query_params['orderby']['enum'], array( 'affreviews_rating', 'rand' ) ) ) ); } return $query_params; } ); add_filter( "rest_{$post_type}_query", function ( $args, $request ) { if ( 'affreviews_rating' === $request['orderby'] ) { $args['meta_key'] = 'affreviews_rating'; $args['orderby'] = 'meta_value_num'; } if ( ! empty( $request['affreviewsInclude'] ) ) { $args = $this->apply_selected_reviews( $args, $request['affreviewsInclude'] ); } return $args; }, 10, 2 ); } /** * Give the legacy loop blocks' editor scripts the markup of the pattern * each one transforms into, as `affreviewsLoopPatterns[ key ]`, so the * transform builds the same blocks as the pattern. * * @param array $blocks Key => array( WP_Block_Type|false, pattern file name ). * @return void */ private function add_loop_pattern_data( $blocks ) { foreach ( $blocks as $key => list( $block_type, $pattern ) ) { if ( ! $block_type || ! $block_type->editor_script_handles ) { continue; } $pattern_data = require AFFREVIEWS_DIR . 'blocks/patterns/' . $pattern . '.php'; wp_add_inline_script( $block_type->editor_script_handles[0], 'window.affreviewsLoopPatterns = window.affreviewsLoopPatterns || {};' . 'window.affreviewsLoopPatterns[' . wp_json_encode( $key ) . '] = ' . wp_json_encode( $pattern_data['content'] ) . ';', 'before' ); } } /** * Register Categories for Patterns * * @return void */ public function register_pattern_categories() { register_block_pattern_category( 'affreviews_loops', array( 'label' => __( 'Affiliate Reviews', 'affiliate-reviews' ) ) ); } /** * Register Patterns * * @return void */ public function register_patterns() { $block_patterns = array( 'review-grid-s1', 'review-loop-large-s1', 'review-loop-small-s1', 'review-table-large-s1', 'review-table-s1', 'single-review-large', 'single-review', 'review-summary-s1', 'review-loop-compact-s1', 'review-compare-s1', ); $block_patterns = apply_filters( 'affreviews_block_patterns', $block_patterns ); foreach ( $block_patterns as $block_pattern ) { $pattern_file = AFFREVIEWS_DIR . 'blocks/patterns/' . $block_pattern . '.php'; register_block_pattern( 'affreviews/' . $block_pattern, require $pattern_file ); } } }