__( 'FAQs', 'accordion-faq-plugin' ), 'priority' => 50, 'callback' => array( $this, 'render_tab' ), ); return $tabs; } /** * Render the product FAQ tab, honoring the per-product selection. * * @return void */ public function render_tab() { global $product; $product_id = 0; if ( is_object( $product ) && method_exists( $product, 'get_id' ) ) { $product_id = $product->get_id(); } elseif ( function_exists( 'get_the_ID' ) ) { $product_id = (int) get_the_ID(); } $args = array( 'search' => true ); $ids = $product_id ? (array) get_post_meta( $product_id, self::META_IDS, true ) : array(); $ids = array_values( array_filter( array_map( 'absint', $ids ) ) ); $cat = $product_id ? (string) get_post_meta( $product_id, self::META_CAT, true ) : ''; if ( ! empty( $ids ) ) { $args['ids'] = $ids; } elseif ( '' !== $cat ) { $args['category'] = $cat; } else { /** * Filter the fallback FAQ category slug shown on product pages when a * product has no explicit selection. * * @param string $slug Category slug (empty = all FAQs). * @param int $product_id Product ID. */ $args['category'] = apply_filters( 'accordion_faq_woo_category', '', $product_id ); } $faq_html = accordion_faq()->renderer()->render( $args ); if ( '' === $faq_html ) { return; } echo '

' . esc_html__( 'Frequently Asked Questions', 'accordion-faq-plugin' ) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $faq_html is assembled from per-value escaped template partials and the_content-filtered post markup; re-escaping would corrupt valid HTML (embeds, etc.). echo $faq_html; } /** * Register the per-product FAQ metabox. * * @return void */ public function add_metabox() { if ( ! class_exists( 'WooCommerce' ) ) { return; } add_meta_box( 'accordion-faq-product', __( 'Product FAQs', 'accordion-faq-plugin' ), array( $this, 'render_metabox' ), 'product', 'side', 'default' ); } /** * Render the metabox UI. * * @param \WP_Post $post Product post. * @return void */ public function render_metabox( $post ) { wp_nonce_field( self::NONCE, self::NONCE ); $selected_ids = array_map( 'absint', (array) get_post_meta( $post->ID, self::META_IDS, true ) ); $selected_cat = (string) get_post_meta( $post->ID, self::META_CAT, true ); $terms = get_terms( array( 'taxonomy' => Taxonomy::TAXONOMY, 'hide_empty' => false ) ); $faqs = get_posts( array( 'post_type' => PostType::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 200, 'orderby' => 'title', 'order' => 'ASC', ) ); echo '

' . esc_html__( 'Choose FAQs to show on this product. Leave empty to fall back to a category or the global default.', 'accordion-faq-plugin' ) . '

'; echo '


'; echo '

'; echo '


'; echo '' . esc_html__( 'Overrides the category. Ctrl/Cmd-click to select several.', 'accordion-faq-plugin' ) . '
'; echo '

'; } /** * Save the per-product FAQ selection. * * @param int $post_id Product ID. * @param \WP_Post $post Product. * @return void */ public function save( $post_id, $post ) { if ( ! isset( $_POST[ self::NONCE ] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE ] ) ), self::NONCE ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } $ids = isset( $_POST['afaq_product_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['afaq_product_ids'] ) ) : array(); $ids = array_values( array_filter( $ids ) ); if ( empty( $ids ) ) { delete_post_meta( $post_id, self::META_IDS ); } else { update_post_meta( $post_id, self::META_IDS, $ids ); } $cat = isset( $_POST['afaq_product_cat'] ) ? sanitize_title( wp_unslash( $_POST['afaq_product_cat'] ) ) : ''; if ( '' === $cat ) { delete_post_meta( $post_id, self::META_CAT ); } else { update_post_meta( $post_id, self::META_CAT, $cat ); } } }