$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 .= '
';
}
if ($half_stars) {
$html .= '
';
}
for ($i = 0; $i < $empty_stars; $i++) {
$html .= '
';
}
$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) . '...';
}
}
}