*/
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( '' . $a . '
', 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
);
}
}
}