'error' ) ); } $option_name = isset( $_POST['option_name'] ) ? sanitize_text_field( $_POST['option_name'] ) : ''; if ( $option_name ) { update_option( $option_name, $page_id ); } wp_send_json( array( 'status' => 'success', 'page_id' => $page_id ) ); } /** * Build the post array for page creation from POST data. * * @since 2.2.5 * * @return array Post array suitable for wp_insert_post. */ private static function build_page_post_array(): array { // Nonce verified in create_page() before calling this method. // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_title = isset( $_POST['page_title'] ) ? sanitize_text_field( $_POST['page_title'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_meta_key = isset( $_POST['page_meta_key'] ) ? sanitize_text_field( $_POST['page_meta_key'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $page_meta_value = isset( $_POST['page_meta_value'] ) ? sanitize_text_field( $_POST['page_meta_value'] ) : ''; $meta_input = array(); if ( $page_meta_key && $page_meta_value ) { $meta_input = array( $page_meta_key => $page_meta_value ); } return array( 'post_type' => 'page', 'post_status' => 'publish', 'post_title' => $page_title, 'post_content' => '', 'meta_input' => $meta_input, ); } // ─────────────────────────────────────────────────────────────── // Select2 content search // ─────────────────────────────────────────────────────────────── /** * AJAX handler for Select2 content search. * * @since 2.2.5 * * @return void Sends JSON response and exits. */ public function select_content_ajax() { $term = isset( $_GET['term'] ) ? sanitize_text_field( wp_unslash( $_GET['term'] ) ) : ''; $nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : ''; $source = isset( $_GET['source'] ) ? sanitize_text_field( wp_unslash( $_GET['source'] ) ) : ''; if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'You are not allowed to do this.' ); } if ( empty( $term ) || empty( $source ) || empty( $nonce ) || ! wp_verify_nonce( $nonce, 'merchant_admin_options' ) ) { wp_send_json_error(); } $options = ( 'user' === $source ) ? self::search_users( $term ) : self::search_posts_or_products( $term, $source ); wp_send_json_success( $options ); } /** * Search posts or products for Select2. * * @since 2.2.5 * * @param string $term Search term. * @param string $source Post type ('post' or 'product'). * * @return array Select2-compatible results. */ private static function search_posts_or_products( string $term, string $source ): array { $query = static::make_wp_query( array( 's' => $term, 'post_type' => $source, 'post_status' => 'publish', 'posts_per_page' => 25, 'order' => 'DESC', ) ); $options = array(); foreach ( $query->posts as $post ) { $options[] = array( 'id' => $post->ID, 'text' => $post->post_title ); } return $options; } /** * Search users for Select2. * * @since 2.2.5 * * @param string $term Search term. * * @return array Select2-compatible results. */ private static function search_users( string $term ): array { $query = static::make_wp_user_query( array( 'search' => '*' . $term . '*', 'search_columns' => array( 'user_login', 'user_nicename', 'user_email', 'user_url' ), 'number' => 25, ) ); $options = array(); foreach ( $query->results as $user ) { $options[] = array( 'id' => $user->ID, 'text' => $user->display_name ); } return $options; } // ─────────────────────────────────────────────────────────────── // Reviews // ─────────────────────────────────────────────────────────────── /** * AJAX handler for searching product reviews. * * @since 1.10.4 * * @return void Sends JSON response and exits. */ public function search_reviews() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'merchant_admin_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Nonce verification failed', 'merchant' ) ) ); } $term = ''; if ( isset( $_POST['search'] ) && ! empty( $_POST['search'] ) ) { $term = trim( sanitize_text_field( $_POST['search'] ) ); } wp_send_json_success( self::products_selector_search_results( $term ) ); } /** * AJAX handler for loading additional product reviews. * * @since 1.10.4 * * @return void Sends JSON response and exits. */ public function load_more_reviews() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'merchant_admin_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Nonce verification failed', 'merchant' ) ) ); } $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; $offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0; if( 0 === $product_id ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid product ID', 'merchant' ) ) ); } wp_send_json_success( self::get_rendered_product_reviews( wc_get_product( $product_id ), $offset ) ); } // ─────────────────────────────────────────────────────────────── // Module settings backup / restore // ─────────────────────────────────────────────────────────────── /** * AJAX handler for exporting module settings. * * @since 2.2.5 * * @return void Sends JSON response and exits. */ public function get_module_settings() { if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_GET['nonce'] ), 'merchant_admin_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Nonce verification failed', 'merchant' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to access this page', 'merchant' ) ) ); } if ( ! isset( $_GET['module_id'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Module ID is required', 'merchant' ) ) ); } $module_id = sanitize_text_field( $_GET['module_id'] ); $modules = merchant_get_modules_data(); if ( ! isset( $modules[ $module_id ] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Module not found', 'merchant' ) ) ); } $module_object = Merchant_Modules::get_module( $module_id ); if ( ! $module_object ) { wp_send_json_error( array( 'message' => esc_html__( 'Module not available', 'merchant' ) ) ); } wp_send_json_success( array( 'timestamp' => time(), 'module_id' => $module_id, 'settings' => $module_object->get_module_settings(), ) ); } /** * AJAX handler for restoring module settings from a backup. * * @since 2.2.5 * * @return void Sends JSON response and exits. */ public function restore_module_settings() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'merchant_admin_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Nonce verification failed', 'merchant' ) ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to access this page', 'merchant' ) ) ); } $module_id = self::get_validated_module_id( $_POST, 'module_id' ); $sanitized_settings = self::parse_and_validate_settings( $module_id ); $module_object = Merchant_Modules::get_module( $module_id ); if ( ! $module_object ) { wp_send_json_error( array( 'message' => esc_html__( 'Module not available', 'merchant' ) ) ); } $module_object->update_module_settings( $sanitized_settings['settings'] ); $admin_url = add_query_arg( array( 'page' => 'merchant', 'module' => $module_id, ), esc_url( admin_url( 'admin.php' ) ) ); wp_send_json_success( array( 'message' => esc_html__( 'Settings restored successfully', 'merchant' ), 'redirect_url' => $admin_url, ) ); } /** * Validate and return a module ID from request data. * * @since 2.2.5 * * @param array $data Request data ($_GET or $_POST). * @param string $key Key to look up. * * @return string Sanitized module ID. */ private static function get_validated_module_id( array $data, string $key ): string { if ( ! isset( $data[ $key ] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Module ID is required', 'merchant' ) ) ); } $module_id = sanitize_text_field( $data[ $key ] ); $modules = merchant_get_modules_data(); if ( ! isset( $modules[ $module_id ] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Module not found', 'merchant' ) ) ); } return $module_id; } /** * Parse, validate, and sanitize module settings from POST JSON. * * @since 2.2.5 * * @param string $module_id Expected module ID for cross-check. * * @return array Sanitized settings array. */ private static function parse_and_validate_settings( string $module_id ): array { // Nonce verified in restore_module_settings() before calling this method. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! isset( $_POST['module_settings'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Default settings not found', 'merchant' ) ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing $module_settings = json_decode( wp_unslash( $_POST['module_settings'] ), true ); if ( json_last_error() !== JSON_ERROR_NONE ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing wp_send_json_error( array( 'message' => esc_html__( 'Invalid settings data', 'merchant' ), $_POST['module_settings'] ) ); } $sanitized = map_deep( $module_settings, 'sanitize_text_field' ); if ( ! isset( $sanitized['module_id'] ) || $sanitized['module_id'] !== $module_id ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid module ID', 'merchant' ) ) ); } if ( ! isset( $sanitized['settings'] ) || ! is_array( $sanitized['settings'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid or missing settings data', 'merchant' ) ) ); } return $sanitized; } // ─────────────────────────────────────────────────────────────── // Review images // ─────────────────────────────────────────────────────────────── /** * AJAX handler for retrieving review images. * * @since 1.10.4 * * @return void Sends JSON response and exits. */ public function get_review_images() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'merchant_admin_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Nonce verification failed', 'merchant' ) ) ); } $review_id = 0; if ( ! isset( $_POST['review_id'] ) || ! is_numeric( $_POST['review_id'] ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid review ID', 'merchant' ) ) ); } else { $review_id = absint( $_POST['review_id'] ); } $photos_ids = get_comment_meta( $review_id, 'review_images', true ); if ( empty( $photos_ids ) ) { wp_send_json_error( array( 'message' => esc_html__( 'No images found', 'merchant' ) ) ); } $images = array_map( static function ( $image_id ) { return wp_get_attachment_image_url( $image_id, 'full' ); }, $photos_ids ); $response = '
'; $response .= '
'; foreach ( $images as $image ) { $response .= '
'; } $response .= '
'; wp_send_json_success( $response ); } // ─────────────────────────────────────────────────────────────── // Product search // ─────────────────────────────────────────────────────────────── /** * AJAX handler for WooCommerce product search. * * Orchestrates: validate → parse types → build query → render results. * * @since 2.2.5 * * @return void Outputs HTML and exits. */ public function products_search() { check_ajax_referer( 'merchant_admin_options', 'nonce' ); if ( ! isset( $_POST['keyword'] ) || empty( $_POST['keyword'] ) ) { wp_die(); return; // @codeCoverageIgnore } $keyword = sanitize_text_field( $_POST['keyword'] ); $types = self::get_product_search_types(); $hierarchy = self::is_hierarchy_search( $types ); $added_ids = isset( $_POST['ids'] ) ? explode( ',', sanitize_text_field( $_POST['ids'] ) ) : array(); $query_args = self::build_product_search_query_args( $keyword, $types, $added_ids ); $query = static::make_wp_query( $query_args ); if ( $query->have_posts() ) { echo self::render_product_search_results( $query, $types, $hierarchy ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_reset_postdata(); } else { // translators: %s is the search keyword echo wp_kses( '', array( 'ul' => array(), 'span' => array(), ) ); } wp_die(); } /** * Parse product types from POST data. * * @since 2.2.5 * * @return string[] Product type slugs. */ private static function get_product_search_types(): array { // Nonce verified in products_search() before calling this method. // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( isset( $_POST['product_types'] ) && ! empty( $_POST['product_types'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return explode( ',', sanitize_text_field( $_POST['product_types'] ) ); } return array( 'simple', 'variable' ); } /** * Determine whether the search should show parent/child hierarchy. * * @since 2.2.5 * * @param string[] $types Product types. * * @return bool */ private static function is_hierarchy_search( array $types ): bool { return in_array( 'all', $types, true ) || ( in_array( 'variation', $types, true ) && in_array( 'variable', $types, true ) ); } /** * Build WP_Query arguments for product search. * * @since 2.2.5 * * @param string $keyword Search keyword. * @param string[] $types Product type slugs. * @param string[] $added_ids Already-added product IDs to exclude. * * @return array WP_Query arguments. */ private static function build_product_search_query_args( string $keyword, array $types, array $added_ids ): array { if ( is_numeric( $keyword ) ) { $args = array( 'p' => absint( $keyword ), 'post_type' => 'product', ); } else { $args = array( 'post_type' => 'product', 'post_status' => array( 'publish', 'private' ), 's' => $keyword, 'posts_per_page' => 10, 'post__not_in' => array_map( 'absint', $added_ids ), ); if ( ! empty( $types ) && ! in_array( 'all', $types, true ) ) { $product_types = $types; if ( in_array( 'variation', $types, true ) ) { $product_types[] = 'variable'; } // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query $args['tax_query'] = array( array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $product_types, ), ); } } return self::append_category_filter( $args ); } /** * Append a product_cat tax_query clause if categories are present in POST. * * @since 2.2.5 * * @param array $args Existing WP_Query arguments. * * @return array Modified arguments. */ private static function append_category_filter( array $args ): array { // Nonce verified in products_search() before calling this method. // phpcs:ignore WordPress.Security.NonceVerification.Missing $categories = array_map( 'sanitize_text_field', $_POST['categories'] ?? array() ); if ( is_array( $categories ) && ! empty( $categories ) ) { $args['tax_query'][] = array( 'taxonomy' => 'product_cat', 'field' => 'slug', 'terms' => $categories, 'operator' => 'IN', ); } return $args; } /** * Render product search results as HTML list items. * * @since 2.2.5 * * @param WP_Query $query The executed query. * @param string[] $types Product type slugs. * @param bool $hierarchy Whether to show hierarchy. * * @return string HTML output. */ private static function render_product_search_results( WP_Query $query, array $types, bool $hierarchy ): string { $html = ''; return $html; } /** * Render child variations for a variable product. * * @since 2.2.5 * * @param WC_Product $product Parent product. * @param string[] $types Product type slugs. * @param bool $hierarchy Whether to show hierarchy styling. * * @return string HTML list items for variations. */ private static function render_product_variations( $product, array $types, bool $hierarchy ): string { if ( ! $product->is_type( 'variable' ) ) { return ''; } if ( ! empty( $types ) && ! in_array( 'all', $types, true ) && ! in_array( 'variation', $types, true ) ) { return ''; } $children = $product->get_children(); if ( ! is_array( $children ) || empty( $children ) ) { return ''; } $html = ''; foreach ( $children as $child_id ) { $child_product = wc_get_product( $child_id ); /** @var WC_Product_Variation $child_product */ if ( ! static::are_variation_attributes_set( $child_product ) ) { continue; } $html .= static::product_data_li( $child_product, true, $hierarchy ); } return $html; } // ─────────────────────────────────────────────────────────────── // Products selector (reviews) // ─────────────────────────────────────────────────────────────── /** * Search products with reviews and render results. * * @since 1.10.4 * * @param string $term Optional search term. * @param int[] $exclude Optional product IDs to exclude. * * @return string Rendered HTML. */ public static function products_selector_search_results( $term = '', $exclude = array() ) { $args = array( 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => 20, 'post__not_in' => $exclude, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_key' => '_wc_review_count', 'orderby' => 'meta_value_num', 'order' => 'DESC', ); if ( $term ) { $args['s'] = $term; } $query = static::make_wc_product_query( $args ); $products = $query->get_products(); $response = ''; foreach ( $products as $product ) { $response .= self::render_product_review_item( $product ); } return $response; } /** * Render a single product item in the reviews selector. * * @since 2.2.5 * * @param WC_Product $product The product. * * @return string Rendered HTML. */ protected static function render_product_review_item( $product ): string { $reviews_count = $product->get_review_count(); $reviews_text = _n( 'Review', 'Reviews', $reviews_count, 'merchant' ); $has_reviews_cl = $reviews_count > 0 ? ' product-item-has-reviews' : ''; $product_id = $product->get_id(); $html = '
'; $html .= self::render_product_review_header( $product, $reviews_count, $reviews_text ); if ( $reviews_count > 0 ) { $reviews = self::get_rendered_product_reviews( $product ); $rendered_reviews = $reviews['reviews'] . $reviews['load_more']; $html .= '
' . $rendered_reviews . '
'; } $html .= '
'; return $html; } /** * Render the header section of a product review item. * * @since 2.2.5 * * @param WC_Product $product The product. * @param int $reviews_count Number of reviews. * @param string $reviews_text Localized review/reviews label. * * @return string Rendered HTML. */ protected static function render_product_review_header( $product, int $reviews_count, string $reviews_text ): string { $product_id = $product->get_id(); $edit_link = get_edit_post_link( $product_id ); $html = '
'; $html .= '
' . get_the_post_thumbnail( $product_id, 'thumbnail' ) . '
'; $html .= '
' . $product->get_name() . '
'; $html .= '
'; $html .= '
' . wc_get_rating_html( $product->get_average_rating(), $reviews_count ) . '
'; if ( $reviews_count > 0 ) { $html .= '
0
' . esc_attr__( 'Number of selected reviews', 'merchant' ) . '
'; } $html .= '
' . esc_html( $product->get_status() ) . '
'; $html .= '
' . $reviews_count . ' ' . $reviews_text . '
'; $html .= '
'; $html .= '
'; // .header return $html; } /** * Get rendered HTML for a product's reviews with pagination. * * @since 1.10.4 * * @param WC_Product $product The WooCommerce product object. * @param int $offset Number of reviews to skip. * * @return array{reviews: string, load_more: string} */ public static function get_rendered_product_reviews( $product, $offset = 0 ) { $reviews_count = $product->get_review_count(); if ( $reviews_count === 0 ) { return array( 'reviews' => '', 'load_more' => '' ); } /** @since 1.10.4 */ $reviews_needed = apply_filters( 'merchant_reviews_per_page', 10 ); $reviews = get_comments( array( 'post_id' => $product->get_id(), 'status' => array( 'approve', 'spam', 'hold' ), 'type' => 'review', 'number' => $reviews_needed, 'offset' => $offset, 'fields' => 'ids', ) ); $response = ''; if ( ! empty( $reviews ) ) { $response .= '
'; foreach ( $reviews as $review ) { $response .= self::get_review( $review ); } $response .= '
'; } $load_more = self::render_load_more_button( $reviews_count, $reviews_needed, $offset ); return array( 'reviews' => $response, 'load_more' => $load_more ); } /** * Render a "load more" button if additional reviews exist. * * @since 2.2.5 * * @param int $total Total review count. * @param int $per_page Reviews per page. * @param int $offset Current offset. * * @return string HTML or empty string. */ private static function render_load_more_button( int $total, int $per_page, int $offset ): string { if ( $total <= $per_page || $offset + $per_page >= $total ) { return ''; } return '
' . '' . '
'; } // ─────────────────────────────────────────────────────────────── // Single review rendering // ─────────────────────────────────────────────────────────────── /** * Render a single review comment as HTML. * * @since 1.10.4 * * @param int $review_id The WooCommerce review (comment) ID. * * @return string Rendered review HTML. */ public static function get_review( $review_id ) { $review = get_comment( $review_id ); if ( ! $review ) { /** @since 1.10.4 */ return apply_filters( 'merchant_single_review_item_rendered', '', null ); } $product = wc_get_product( $review->comment_post_ID ); if ( ! $product ) { /** @since 1.10.4 */ return apply_filters( 'merchant_single_review_item_rendered', '', $review ); } $response = self::render_review_header( $review, $product ); $response .= self::render_review_body( $review, $product ); $response .= self::render_review_actions( $review ); $response .= ''; /** @since 1.10.4 */ return apply_filters( 'merchant_single_review_item_rendered', $response, $review ); } /** * Render review header: checkbox + author + product image/name + rating. * * @since 2.2.5 * * @param object $review Comment object. * @param WC_Product $product Product object. * * @return string HTML. */ private static function render_review_header( $review, $product ): string { $edit_link = get_edit_post_link( $product->get_id() ); $rating = get_comment_meta( $review->comment_ID, 'rating', true ); $status = wp_get_comment_status( $review->comment_ID ); $html = '
'; $html .= '
'; $html .= '
' . esc_html( $review->comment_author ) . '
'; $html .= '
' . $product->get_image( 'thumbnail' ) . '
'; $html .= '
' . esc_html( $product->get_name() ) . '
'; $html .= '
' . wc_get_rating_html( $rating ) . '
'; $html .= '
' . esc_html( $status ) . '
'; return $html; } /** * Render review body: content + photos + date + edit link. * * @since 2.2.5 * * @param object $review Comment object. * @param WC_Product $product Product object. * * @return string HTML. */ private static function render_review_body( $review, $product ): string { $html = '
' . esc_html( wp_trim_words( $review->comment_content, 8, '...' ) ) . '
'; $html .= '
'; $html .= '
' . '
' . esc_attr__( 'View customer\'s uploaded review photos.', 'merchant' ) . '
' . self::get_review_main_photo( $review->comment_ID ) . '
'; $html .= '
' . esc_html( get_comment_date( 'j/n/Y', $review->comment_ID ) ) . '
'; $html .= '
' . esc_html__( 'Edit', 'merchant' ) . '
'; return $html; } /** * Render review action buttons: delete + move. * * @since 2.2.5 * * @param object $review Comment object. * * @return string HTML. */ private static function render_review_actions( $review ): string { $html = ''; $html .= ''; return $html; } /** * Get the primary photo thumbnail for a review. * * @since 1.10.4 * * @param int $comment_id The review (comment) ID. * * @return string Rendered HTML or empty string. */ public static function get_review_main_photo( $comment_id ) { $photos_ids = get_comment_meta( $comment_id, 'review_images', true ); if ( empty( $photos_ids ) ) { return ''; } $response = ''; $first_image = wp_get_attachment_image_url( $photos_ids[0], array( 60, 60 ) ); $counter = ''; if ( count( $photos_ids ) > 1 ) { $counter = '' . count( $photos_ids ) . ''; } $response .= '
' . $counter . '
'; return $response; } // ─────────────────────────────────────────────────────────────── // Product data list item // ─────────────────────────────────────────────────────────────── /** * Render a product as an `
  • ` element for the product selector. * * @since 2.2.5 * * @param WC_Product $product The WooCommerce product object. * @param bool|array $search Whether this is a search result (true = "Add" button). * @param bool $hierarchy Whether to apply hierarchy indentation. * * @return string Rendered HTML. */ public static function product_data_li( $product, $search = false, $hierarchy = false ): string { $attrs = self::build_product_li_attributes( $product, $hierarchy ); $html = '
  • '; $html .= self::render_product_image_html( $product ); $html .= self::render_product_data_html( $product ); $html .= self::render_product_type_and_button( $product, $search ); $html .= '
  • '; return $html; } /** * Build HTML attributes for a product list item. * * @since 2.2.5 * * @param WC_Product $product Product object. * @param bool $hierarchy Whether to show hierarchy. * * @return string Attribute string. */ private static function build_product_li_attributes( $product, bool $hierarchy ): string { $product_id = $product->get_id(); $product_sku = $product->get_sku(); $product_name = $product->get_name(); $price = $product->get_price(); $key = $product_id . '_' . $product_sku; $item_class = 'product-item'; if ( $hierarchy && $product->is_type( 'variation' ) ) { $item_class .= ' hierarchy-style'; } return 'class="' . esc_attr( $item_class ) . '"' . ' data-key="' . esc_attr( $key ) . '"' . ' data-name="' . esc_attr( $product_name ) . '"' . ' data-sku="' . esc_attr( $product_sku ) . '"' . ' data-id="' . esc_attr( $product_id ) . '"' . ' data-price="' . esc_attr( $price ) . '"'; } /** * Render the product image with wp_kses. * * @since 2.2.5 * * @param WC_Product $product Product object. * * @return string Sanitized image HTML. */ private static function render_product_image_html( $product ): string { /** @since 1.9.1 */ $product_image = apply_filters( 'merchant_product_item_product_image', '' . $product->get_image( array( 30, 30 ) ) . '', $product ); return wp_kses( $product_image, array( 'span' => array( 'class' => true ), 'img' => array( 'src' => true, 'alt' => true, 'decoding' => true, 'srcset' => true, 'loading' => true, 'sizes' => true, 'class' => true, 'width' => true, 'height' => true, ), ) ); } /** * Render product name, price, and sold-individually notice. * * @since 2.2.5 * * @param WC_Product $product Product object. * * @return string HTML. */ private static function render_product_data_html( $product ): string { $price_html = wp_kses( $product->get_price_html(), array( 'span' => array( 'class' => true ), 'del' => array( 'aria-hidden' => true ), 'ins' => array(), 'bdi' => array(), ) ); $sold_individually = $product->is_sold_individually() ? '' . esc_html__( 'sold individually', 'merchant' ) . ' ' : ''; return '' . '' . esc_html( $product->get_name() ) . '' . '' . $price_html . ' ' . $sold_individually . ''; } /** * Render product type badge and add/remove button. * * @since 2.2.5 * * @param WC_Product $product Product object. * @param bool $search True for "Add" button, false for "Remove". * * @return string HTML. */ private static function render_product_type_and_button( $product, bool $search ): string { $product_id = $product->get_id(); $edit_link = get_edit_post_link( $product_id ); if ( $product->is_type( 'variation' ) ) { $edit_link = get_edit_post_link( $product->get_parent_id() ); } /** @since 1.9.0 */ $product_info = apply_filters( 'merchant_pro_product_bundle_item_product_info', $product->get_type() . '
    #' . $product_id, $product ); $btn_label = $search ? esc_html__( 'Add', 'merchant' ) : esc_html__( 'Remove', 'merchant' ); $btn_char = $search ? '+' : '×'; $remove_btn = '' . $btn_char . ''; return '' . wp_kses_post( $product_info ) . ' ' . wp_kses( $remove_btn, array( 'span' => array( 'class' => true, 'aria-label' => true ) ) ); } // ─────────────────────────────────────────────────────────────── // Variation helpers // ─────────────────────────────────────────────────────────────── /** * Check if all variation attributes are set for a product variation. * * @since 2.2.5 * * @param WC_Product_Variation $variation The variation product object. * * @return bool True if all attributes are set, false otherwise. */ public static function are_variation_attributes_set( $variation ) { if ( $variation && $variation->is_type( 'variation' ) ) { $variation_attributes = $variation->get_variation_attributes(); foreach ( $variation_attributes as $attribute => $value ) { if ( empty( $value ) ) { return false; } } return true; } return false; } } // Initialize. new Merchant_Admin_Ajax();