$sanitized_name, 'post_excerpt' => $sanitized_excerpt, 'post_content' => $sanitized_description, 'post_status' => 'publish', 'post_type' => AFFILILABS_CPT_PRODUCT, 'tax_input' => array( AFFILILABS_TAXONOMY_PRODUCT_CATEGORY => is_array($category_id) ? array($category_id['term_id']) : array() ), 'post_author' => isset($product_data['post_author']) ? intval($product_data['post_author']) : get_current_user_id(), 'meta_input' => array( AFFILILABS_META_KEY_PRICE => $sanitized_price, AFFILILABS_META_KEY_PRICE_CURRENCY => $sanitized_price_currency, AFFILILABS_META_KEY_MARKETPLACE => $marketplace_id, // Already intval'd AFFILILABS_META_KEY_PROS => $sanitized_pros, AFFILILABS_META_KEY_CONS => $sanitized_cons, AFFILILABS_META_KEY_SHORT_FEATURES => $sanitized_short_features, AFFILILABS_META_KEY_AFFILIATE_LINK => $sanitized_affiliate_link, AFFILILABS_META_KEY_RATING => $sanitized_rating, AFFILILABS_META_KEY_RATING_RATIONALE => $sanitized_rating_rationale, AFFILILABS_META_KEY_RATING_SOURCES => $sanitized_rating_sources, AFFILILABS_META_KEY_CRITERIA_SOURCES => $sanitized_criteria_sources, AFFILILABS_META_KEY_RATING_QUALITY => $sanitized_rating_quality, AFFILILABS_META_KEY_RATING_USABILITY => $sanitized_rating_usability, AFFILILABS_META_KEY_RATING_EFFECTIVENESS => $sanitized_rating_effectiveness, AFFILILABS_META_KEY_RATING_VALUE => $sanitized_rating_value, AFFILILABS_META_KEY_RATING_SUPPORT => $sanitized_rating_support, AFFILILABS_META_KEY_PRODUCT_STATUS_MODE => $product_status_mode, AFFILILABS_META_KEY_APPLIED_PRODUCT_TEMPLATE_ID => $product_data['applied_template_id'] ?? null, ) ); if (isset($product_data['custom_fields']) && is_array($product_data['custom_fields'])) { foreach ($product_data['custom_fields'] as $key => $value) { $product['meta_input']["_affl_product_custom_" . sanitize_key($key)] = sanitize_text_field($value); } } if ($product_id) { $product['ID'] = $product_id; $res = wp_update_post($product, true); if (is_wp_error($res)) { AFFL_LoggerService::log(esc_html($res->get_error_message())); throw new Exception(esc_html($res->get_error_message())); } } else { $product_id = wp_insert_post($product, true); if (is_wp_error($product_id)) { AFFL_LoggerService::log(esc_html($product_id->get_error_message())); throw new Exception(esc_html($product_id->get_error_message())); } } $thumbnail_id = $product_data['image_id'] ?? null; $image_url_to_import = $product_data['image_url'] ?? null; // URL passed from import modal if ($thumbnail_id) { set_post_thumbnail($product_id, $thumbnail_id); } elseif ($image_url_to_import && filter_var($image_url_to_import, FILTER_VALIDATE_URL)) { AFFL_LoggerService::log("Attempting to download image for product ID " . esc_html($product_id) . " from URL: " . esc_url($image_url_to_import), 'INFO'); $image_id = media_sideload_image($image_url_to_import, $product_id, sanitize_text_field($product_data['name']), 'id'); if (!is_wp_error($image_id)) { set_post_thumbnail($product_id, $image_id); AFFL_LoggerService::log("Successfully downloaded and set image ID " . esc_html($image_id) . " for product ID " . esc_html($product_id), 'INFO'); } else { AFFL_LoggerService::log("Failed to download image for product ID " . esc_html($product_id) . " from URL: " . esc_url($image_url_to_import) . ". Error: " . esc_html($image_id->get_error_message()), 'ERROR'); } } else { delete_post_thumbnail($product_id); } if (isset($product_data['criterion_values']) && is_array($product_data['criterion_values'])) { AFFL_LoggerService::debug('Product Save - Criterion values: ' . esc_html(print_r($product_data['criterion_values'], true)), 'DEBUG'); $all_criterion_values = get_post_meta($product_id, AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUES, true); AFFL_LoggerService::debug('Product Save - Existing criterion values: ' . esc_html(print_r($all_criterion_values, true)), 'DEBUG'); if (!is_array($all_criterion_values)) { $all_criterion_values = array(); } foreach ($product_data['criterion_values'] as $criterion_id => $fields) { $criterion_id = intval($criterion_id); AFFL_LoggerService::debug('Product Save - Processing criterion ID: ' . esc_html($criterion_id) . ' with fields: ' . esc_html(print_r($fields, true)), 'DEBUG'); $criterion = get_post($criterion_id); if ($criterion && $criterion->post_type === AFFILILABS_CPT_CRITERION) { $criterion_fields = get_post_meta($criterion_id, AFFILILABS_META_KEY_CRITERION, true); if (!isset($all_criterion_values[$criterion_id])) { $all_criterion_values[$criterion_id] = array( 'id' => $criterion_id, 'name' => $criterion->post_title, 'fields' => array( 'counter' => 0, 'items' => array() ) ); } foreach ($fields as $field_data) { if (!isset($field_data['name']) || empty($field_data['name'])) { continue; } $field_name = trim($field_data['name']); $field_value = isset($field_data['value']) ? trim($field_data['value']) : ''; $field_type = isset($field_data['type']) ? trim($field_data['type']) : 'text'; $field_unit = isset($field_data['unit']) ? trim($field_data['unit']) : ''; $definition_id = $field_data['definition_id'] ?? null; if ($field_type === 'number' && !empty($field_value)) { $field_value = affl_sanitize_float_input($field_value); } if (empty($definition_id)) { AFFL_LoggerService::debug('Product Save - Skipping field due to missing definition ID: ' . esc_html(print_r($field_data, true)), 'DEBUG'); continue; } $definition_id = intval($definition_id); $existing_field_index = null; if (isset($all_criterion_values[$criterion_id]['fields']['items']) && is_array($all_criterion_values[$criterion_id]['fields']['items'])) { foreach ($all_criterion_values[$criterion_id]['fields']['items'] as $idx => $item) { if (isset($item[AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUE_DEFINITION_ID]) && $item[AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUE_DEFINITION_ID] === $definition_id) { $existing_field_index = $idx; break; } } } if ($existing_field_index !== null) { $all_criterion_values[$criterion_id]['fields']['items'][$existing_field_index]['value'] = $field_value; $all_criterion_values[$criterion_id]['fields']['items'][$existing_field_index]['name'] = $field_name; $all_criterion_values[$criterion_id]['fields']['items'][$existing_field_index]['type'] = $field_type; $all_criterion_values[$criterion_id]['fields']['items'][$existing_field_index]['unit'] = $field_unit; } else { $all_criterion_values[$criterion_id]['fields']['items'][] = array( AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUE_DEFINITION_ID => $definition_id, 'name' => $field_name, 'value' => $field_value, 'type' => $field_type, 'unit' => $field_unit ); } } } } update_post_meta($product_id, AFFILILABS_META_KEY_PRODUCT_CRITERION_VALUES, $all_criterion_values); AFFL_LoggerService::debug('Product Save - Final criterion values saved: ' . esc_html(print_r($all_criterion_values, true)), 'DEBUG'); } if ($marketplace_id) { AFFLMarketplaceService::update_product_count($marketplace_id); } return $product_id; } public static function delete(int $product_id): bool { // Need marketplace id to update total product count $marketplace_id = get_post_meta($product_id, AFFILILABS_META_KEY_MARKETPLACE, true); try { $res = wp_delete_post($product_id, true); if (!$res) { throw new Exception(__('There was an error deleting the product. Please try again.', 'affililabs')); } if ($marketplace_id) { AFFLMarketplaceService::update_product_count($marketplace_id); } return true; } catch (Exception $e) { AFFL_LoggerService::log(esc_html($e->getMessage())); return false; } } public static function fill_data(array $product_data) { self::truncate_description_if_needed($product_data); return AFFL_AIService::request('ai/fill_product_data', array( 'type' => 'fill_product_data', 'data' => $product_data, )); } public static function fill_criteria(array $product_data) { self::truncate_description_if_needed($product_data); return AFFL_AIService::request('ai/fill_criteria', array( 'type' => 'fill_criteria', 'data' => $product_data, )); } public static function web_rate_product(array $product_data) { self::truncate_description_if_needed($product_data); return AFFL_AIService::request('ai/web_rate_product', array( 'type' => 'rate_product', 'data' => $product_data, )); } public static function deep_review_product(array $product_data) { self::truncate_description_if_needed($product_data); return AFFL_AIService::request('ai/deep_review_product', array( 'type' => 'deep_review_product', 'data' => $product_data, 'affiliate_link' => $product_data['affiliate_link'] ?? '', )); } /** * Get all products with basic information * * @param string $post_status Filter products by status ('publish', 'draft', 'any', etc.) * @return array Array of products with basic info */ public static function get_all(string $post_status = 'any') { $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => -1, 'post_status' => $post_status, )); return array_map(function($product) { return array( 'id' => $product->ID, 'name' => $product->post_title, ); }, $products); } /** * Get all products with complete information including meta fields * * @param string $post_status Filter products by status ('publish', 'draft', 'any', etc.) * @return array Array of products with all meta data */ public static function get_all_with_meta(string $post_status = 'any') { $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => -1, 'post_status' => $post_status, )); return array_map(function($product) { return self::format_product_data($product); }, $products); } /** * Format product data with complete meta information * * @param WP_Post $product WordPress post object * @return array Formatted product data with meta fields */ private static function format_product_data($product) { // Get all meta fields // @TODO: Sanitize $price = get_post_meta($product->ID, AFFILILABS_META_KEY_PRICE, true); $price_currency = get_post_meta($product->ID, AFFILILABS_META_KEY_PRICE_CURRENCY, true); $rating = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING, true); $affiliate_link = get_post_meta($product->ID, AFFILILABS_META_KEY_AFFILIATE_LINK, true); $short_features = get_post_meta($product->ID, AFFILILABS_META_KEY_SHORT_FEATURES, true); $pros = get_post_meta($product->ID, AFFILILABS_META_KEY_PROS, true); $cons = get_post_meta($product->ID, AFFILILABS_META_KEY_CONS, true); $rating_sources = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_SOURCES, true); $criteria_sources = get_post_meta($product->ID, AFFILILABS_META_KEY_CRITERIA_SOURCES, true); $rating_quality = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_QUALITY, true); $rating_usability = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_USABILITY, true); $rating_effectiveness = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_EFFECTIVENESS, true); $rating_value = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_VALUE, true); $rating_support = get_post_meta($product->ID, AFFILILABS_META_KEY_RATING_SUPPORT, true); // Get product categories $categories = wp_get_post_terms($product->ID, AFFILILABS_TAXONOMY_PRODUCT_CATEGORY); $category_names = array(); $category_ids = array(); if (!is_wp_error($categories) && !empty($categories)) { foreach ($categories as $category) { $category_names[] = $category->name; $category_ids[] = $category->term_id; } } if (empty($price_currency)) { $price_currency = '$'; } $marketplace_id = get_post_meta($product->ID, AFFILILABS_META_KEY_MARKETPLACE, true); $marketplace_name = ''; if (!empty($marketplace_id)) { $marketplace_post = get_post($marketplace_id); if ($marketplace_post && $marketplace_post->post_type === AFFILILABS_CPT_MARKETPLACE) { $marketplace_name = $marketplace_post->post_title; } else { AFFL_LoggerService::log("Marketplace post not found or invalid for ID: " . esc_html($marketplace_id) . " on product ID: " . esc_html($product->ID), 'WARNING'); } } return array( 'id' => $product->ID, 'name' => $product->post_title, 'price' => $price, 'price_currency' => $price_currency, 'rating' => $rating, 'affiliate_link' => $affiliate_link, 'excerpt' => $product->post_excerpt, 'description' => $product->post_content, 'short_features' => $short_features, 'pros' => $pros, 'cons' => $cons, 'rating_sources' => $rating_sources, 'criteria_sources' => $criteria_sources, 'rating_quality' => $rating_quality, 'rating_usability' => $rating_usability, 'rating_effectiveness' => $rating_effectiveness, 'rating_value' => $rating_value, 'rating_support' => $rating_support, 'category_names' => $category_names, 'category_ids' => $category_ids, 'marketplace_id' => $marketplace_id, 'marketplace_name' => $marketplace_name, 'image_url' => get_the_post_thumbnail_url($product->ID, 'medium'), 'status_mode' => get_post_meta($product->ID, AFFILILABS_META_KEY_PRODUCT_STATUS_MODE, true) ?? 'manual', ); } /** * Get products by IDs with complete information * * @param array $ids Array of product IDs * @return array Array of products with all meta data */ public static function get_products_by_ids(array $ids) { if (empty($ids)) { return array(); } $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => -1, 'post__in' => $ids, 'post_status' => 'any', )); return array_map(function($product) { return self::format_product_data($product); }, $products); } /** * Get a single product by ID with complete information * * @param int $id Product ID * @return array|null Product data with all meta fields or null if not found */ public static function get_product_by_id(int $id) { $product = get_post($id); if (!$product || $product->post_type !== AFFILILABS_CPT_PRODUCT) { return null; } return self::format_product_data($product); } /** * Generate HTML for rating stars based on a rating value. * * @param float $rating The rating value (0-5). * @param string $alt_text Alt text for the star images. * @param string $img_style Inline style for the star images. * @param bool $show_score Whether to show the numerical score in parentheses. * @return string HTML markup for the rating stars. */ public static function get_rating_stars_html(float $rating, string $alt_text = 'star', string $img_style = 'width: 18px; height: auto;', bool $show_score = true): string { $html = ''; if ($rating > 0) { $r = $rating; if ($r > 5) $r = 5; if ($r < 0) $r = 0; $full_stars = floor($r); $half_stars = ($r - $full_stars) >= 0.1 ? 1 : 0; $empty_stars = 5 - $full_stars - $half_stars; if ($empty_stars < 0) $empty_stars = 0; $html .= ''; for ($i = 0; $i < $full_stars; $i++) { $html .= '' . esc_attr($alt_text) . ''; } if ($half_stars) { $html .= '' . esc_attr($alt_text) . ''; } for ($i = 0; $i < $empty_stars; $i++) { $html .= '' . esc_attr($alt_text) . ''; } $html .= ''; if ($show_score) { $html .= '(' . number_format($rating, 1) . ')'; } } return $html; } public static function normalize_list_data($data) { if (empty($data)) { return []; } affl_import(AFFILILABS_SERVICE_SETTINGS_FILE); $separator_setting = AFFLSettingsService::get_list_separator(); if (is_array($data)) { return array_values(array_filter(array_map('trim', $data))); } $normalized_data = $data; if ($separator_setting === 'comma') { $normalized_data = str_replace(',', "\n", $normalized_data); } $normalized_data = preg_replace('/\s*\n\s*/', "\n", $normalized_data); $items = array_map('trim', explode("\n", $normalized_data)); return array_values(array_filter($items, function($item) { return !empty($item); })); } public static function format_list_for_display($data) { if (empty($data) || !is_array($data)) { return ''; } affl_import(AFFILILABS_SERVICE_SETTINGS_FILE); $separator = AFFLSettingsService::get_list_separator(); if ($separator === 'comma') { return implode(', ', $data); } return implode("\n", $data); } public static function get_list_items_as_array($data) { if (is_array($data)) { return $data; } if (is_string($data) && !empty($data)) { return array_map('trim', explode("\n", $data)); } return []; } /** * Get products by category ID with complete information * * @param int $category_id Category ID * @return array Array of products with all meta data */ public static function get_products_by_category(int $category_id) { if (empty($category_id)) { return array(); } $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => -1, 'tax_query' => array( array( 'taxonomy' => AFFILILABS_TAXONOMY_PRODUCT_CATEGORY, 'field' => 'term_id', 'terms' => $category_id, ), ), 'post_status' => 'any', )); return array_map(function($product) { return self::format_product_data($product); }, $products); } /** * Get a random product with complete information * * @param string $post_status Filter products by status ('publish', 'draft', 'any', etc.) * @return array|null Random product data with all meta fields or null if no products found */ public static function get_random_product(string $post_status = 'publish') { $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => 1, 'post_status' => $post_status, 'orderby' => 'rand', )); if (empty($products)) { return null; } return self::format_product_data($products[0]); } /** * Get a random product from a specific category * * @param int $category_id Category ID * @param string $post_status Filter products by status ('publish', 'draft', 'any', etc.) * @return array|null Random product data with all meta fields or null if no products found */ public static function get_random_product_by_category(int $category_id, string $post_status = 'publish') { if (empty($category_id)) { return null; } $products = get_posts(array( 'post_type' => AFFILILABS_CPT_PRODUCT, 'posts_per_page' => 1, 'tax_query' => array( array( 'taxonomy' => AFFILILABS_TAXONOMY_PRODUCT_CATEGORY, 'field' => 'term_id', 'terms' => $category_id, ), ), 'post_status' => $post_status, 'orderby' => 'rand', )); if (empty($products)) { return null; } return self::format_product_data($products[0]); } /** * Truncate product description if it exceeds 1000 characters. * * @param array $product_data Reference to the product data array. */ private static function truncate_description_if_needed(array &$product_data) { if (isset($product_data['description']) && is_string($product_data['description']) && mb_strlen($product_data['description']) > 1000) { $product_data['description'] = mb_substr($product_data['description'], 0, 1000) . '...'; } } }