'GET', 'callback' => [__CLASS__, 'get_products'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Get single product register_rest_route('woonoow/v1', '/products/(?P\d+)', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_product'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Create product register_rest_route('woonoow/v1', '/products', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_product'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Update product register_rest_route('woonoow/v1', '/products/(?P\d+)', [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_product'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Delete product register_rest_route('woonoow/v1', '/products/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_product'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Get product categories register_rest_route('woonoow/v1', '/products/categories', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_categories'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Get product tags register_rest_route('woonoow/v1', '/products/tags', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_tags'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Get product attributes register_rest_route('woonoow/v1', '/products/attributes', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_attributes'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Create category register_rest_route('woonoow/v1', '/products/categories', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_category'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Update category register_rest_route('woonoow/v1', '/products/categories/(?P\d+)', [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_category'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Delete category register_rest_route('woonoow/v1', '/products/categories/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_category'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Create tag register_rest_route('woonoow/v1', '/products/tags', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_tag'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Update tag register_rest_route('woonoow/v1', '/products/tags/(?P\d+)', [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_tag'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Delete tag register_rest_route('woonoow/v1', '/products/tags/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_tag'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Create attribute register_rest_route('woonoow/v1', '/products/attributes', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_attribute'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Update attribute register_rest_route('woonoow/v1', '/products/attributes/(?P\d+)', [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_attribute'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); // Delete attribute register_rest_route('woonoow/v1', '/products/attributes/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_attribute'], 'permission_callback' => [Permissions::class, 'check_admin_permission'], ]); } /** * Get products list with filters */ public static function get_products(WP_REST_Request $request) { try { $page = max(1, (int) $request->get_param('page')); $per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20))); $search = $request->get_param('search'); $status = $request->get_param('status'); $category = $request->get_param('category'); $type = $request->get_param('type'); $stock_status = $request->get_param('stock_status'); $orderby = $request->get_param('orderby') ?: 'date'; $order = $request->get_param('order') ?: 'DESC'; $args = [ 'post_type' => 'product', 'posts_per_page' => $per_page, 'paged' => $page, 'orderby' => $orderby, 'order' => $order, ]; // Search if ($search) { $args['s'] = sanitize_text_field($search); } // Status filter if ($status) { $args['post_status'] = $status; } else { $args['post_status'] = ['publish', 'draft', 'pending', 'private']; } // Category filter if ($category) { $args['tax_query'] = [ [ 'taxonomy' => 'product_cat', 'field' => 'term_id', 'terms' => (int) $category, ], ]; } // Type filter if ($type) { $args['tax_query'] = $args['tax_query'] ?? []; $args['tax_query'][] = [ 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $type, ]; } // Stock status filter if ($stock_status) { $args['meta_query'] = [ [ 'key' => '_stock_status', 'value' => $stock_status, ], ]; } $query = new \WP_Query($args); $products = []; foreach ($query->posts as $post) { $product = wc_get_product($post->ID); if ($product) { $products[] = self::format_product_list_item($product); } } $response = new WP_REST_Response([ 'rows' => $products, 'total' => $query->found_posts, 'page' => $page, 'per_page' => $per_page, 'pages' => $query->max_num_pages, '_debug' => 'ProductsController-v2-' . time(), // Verify this code is running ], 200); // Prevent caching $response->header('Cache-Control', 'no-cache, no-store, must-revalidate'); $response->header('Pragma', 'no-cache'); $response->header('Expires', '0'); return $response; } catch (\Exception $e) { return new WP_Error('products_error', $e->getMessage(), ['status' => 500]); } } /** * Get single product */ public static function get_product(WP_REST_Request $request) { $id = (int) $request->get_param('id'); $product = wc_get_product($id); if (!$product) { return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]); } return new WP_REST_Response(self::format_product_full($product), 200); } /** * Create new product */ public static function create_product(WP_REST_Request $request) { try { $data = $request->get_json_params(); // Validate required fields if (empty($data['name'])) { return new WP_Error('missing_name', __('Product name is required', 'woonoow'), ['status' => 400]); } // Determine product type $type = $data['type'] ?? 'simple'; if ($type === 'variable') { $product = new WC_Product_Variable(); } else { $product = new WC_Product_Simple(); } // Set basic data - sanitize all inputs $product->set_name(self::sanitize_text($data['name'])); if (!empty($data['slug'])) { $product->set_slug(self::sanitize_slug($data['slug'])); } $product->set_status(sanitize_key($data['status'] ?? 'publish')); $product->set_description(self::sanitize_textarea($data['description'] ?? '')); $product->set_short_description(self::sanitize_textarea($data['short_description'] ?? '')); if (!empty($data['sku'])) { $product->set_sku(self::sanitize_text($data['sku'])); } if (!empty($data['regular_price'])) { $product->set_regular_price(self::sanitize_number($data['regular_price'])); } if (!empty($data['sale_price'])) { $product->set_sale_price(self::sanitize_number($data['sale_price'])); } $product->set_manage_stock($data['manage_stock'] ?? false); if (!empty($data['manage_stock'])) { $product->set_stock_quantity($data['stock_quantity'] ?? 0); } $product->set_stock_status($data['stock_status'] ?? 'instock'); // Optional fields - sanitize dimensions if (!empty($data['weight'])) { $product->set_weight(self::sanitize_number($data['weight'])); } if (!empty($data['length'])) { $product->set_length(self::sanitize_number($data['length'])); } if (!empty($data['width'])) { $product->set_width(self::sanitize_number($data['width'])); } if (!empty($data['height'])) { $product->set_height(self::sanitize_number($data['height'])); } // Virtual and downloadable if (isset($data['virtual'])) { $product->set_virtual((bool) $data['virtual']); } if (isset($data['downloadable'])) { $product->set_downloadable((bool) $data['downloadable']); } if (isset($data['featured'])) { $product->set_featured((bool) $data['featured']); } // Categories if (!empty($data['categories']) && is_array($data['categories'])) { $product->set_category_ids($data['categories']); } // Tags if (!empty($data['tags']) && is_array($data['tags'])) { $product->set_tag_ids($data['tags']); } // Images - support both image_id/gallery_image_ids and images array if (!empty($data['images']) && is_array($data['images'])) { // Convert URLs to attachment IDs $image_ids = []; foreach ($data['images'] as $image_url) { $attachment_id = attachment_url_to_postid($image_url); if ($attachment_id) { $image_ids[] = $attachment_id; } } if (!empty($image_ids)) { // First image is featured $product->set_image_id($image_ids[0]); // Rest are gallery if (count($image_ids) > 1) { $product->set_gallery_image_ids(array_slice($image_ids, 1)); } } } else { // Legacy support for direct IDs if (!empty($data['image_id'])) { $product->set_image_id($data['image_id']); } if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) { $product->set_gallery_image_ids($data['gallery_image_ids']); } } $product->save(); // Handle variations for variable products if ($type === 'variable' && !empty($data['attributes']) && is_array($data['attributes'])) { self::save_product_attributes($product, $data['attributes']); if (!empty($data['variations']) && is_array($data['variations'])) { self::save_product_variations($product, $data['variations']); } } return new WP_REST_Response(self::format_product_full($product), 201); } catch (Exception $e) { return new WP_Error('create_failed', $e->getMessage(), ['status' => 500]); } } /** * Update product */ public static function update_product(WP_REST_Request $request) { $id = (int) $request->get_param('id'); $data = $request->get_json_params(); $product = wc_get_product($id); if (!$product) { return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]); } // Update basic data - sanitize all inputs if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name'])); if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug'])); if (isset($data['status'])) $product->set_status(sanitize_key($data['status'])); if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description'])); if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description'])); if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku'])); if (isset($data['regular_price'])) $product->set_regular_price(self::sanitize_number($data['regular_price'])); if (isset($data['sale_price'])) $product->set_sale_price(self::sanitize_number($data['sale_price'])); if (isset($data['manage_stock'])) { $product->set_manage_stock($data['manage_stock']); if ($data['manage_stock']) { if (isset($data['stock_quantity'])) $product->set_stock_quantity($data['stock_quantity']); } } if (isset($data['stock_status'])) $product->set_stock_status(sanitize_key($data['stock_status'])); if (isset($data['weight'])) $product->set_weight(self::sanitize_number($data['weight'])); if (isset($data['length'])) $product->set_length(self::sanitize_number($data['length'])); if (isset($data['width'])) $product->set_width(self::sanitize_number($data['width'])); if (isset($data['height'])) $product->set_height(self::sanitize_number($data['height'])); // Virtual and downloadable if (isset($data['virtual'])) { $product->set_virtual((bool) $data['virtual']); } if (isset($data['downloadable'])) { $product->set_downloadable((bool) $data['downloadable']); } if (isset($data['featured'])) { $product->set_featured((bool) $data['featured']); } // Categories if (isset($data['categories'])) { $product->set_category_ids($data['categories']); } // Tags if (isset($data['tags'])) { $product->set_tag_ids($data['tags']); } // Images - support both image_id/gallery_image_ids and images array if (isset($data['images']) && is_array($data['images']) && !empty($data['images'])) { // Convert URLs to attachment IDs $image_ids = []; foreach ($data['images'] as $image_url) { $attachment_id = attachment_url_to_postid($image_url); if ($attachment_id) { $image_ids[] = $attachment_id; } } if (!empty($image_ids)) { // First image is featured $product->set_image_id($image_ids[0]); // Rest are gallery if (count($image_ids) > 1) { $product->set_gallery_image_ids(array_slice($image_ids, 1)); } else { $product->set_gallery_image_ids([]); } } } else { // Legacy support for direct IDs if (isset($data['image_id'])) { $product->set_image_id($data['image_id']); } if (isset($data['gallery_image_ids'])) { $product->set_gallery_image_ids($data['gallery_image_ids']); } } // Update custom meta fields (Level 1 compatibility) if (isset($data['meta']) && is_array($data['meta'])) { self::update_product_meta_data($product, $data['meta']); } $product->save(); // Allow plugins to perform additional updates (Level 1 compatibility) do_action('woonoow/product_updated', $product, $data, $request); // Handle variations for variable products if ($product->is_type('variable')) { if (isset($data['attributes'])) { self::save_product_attributes($product, $data['attributes']); } if (isset($data['variations'])) { self::save_product_variations($product, $data['variations']); } } return new WP_REST_Response(self::format_product_full($product), 200); } /** * Delete product */ public static function delete_product(WP_REST_Request $request) { $id = (int) $request->get_param('id'); $force = $request->get_param('force') === 'true'; $product = wc_get_product($id); if (!$product) { return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]); } $result = $product->delete($force); if (!$result) { return new WP_Error('delete_failed', __('Failed to delete product', 'woonoow'), ['status' => 500]); } return new WP_REST_Response(['success' => true, 'id' => $id], 200); } /** * Get product categories */ public static function get_categories(WP_REST_Request $request) { $terms = get_terms([ 'taxonomy' => 'product_cat', 'hide_empty' => false, ]); if (is_wp_error($terms)) { return new WP_REST_Response([], 200); // Return empty array on error } $categories = []; foreach ($terms as $term) { $categories[] = [ 'term_id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, 'parent' => $term->parent, 'count' => $term->count, ]; } return new WP_REST_Response($categories, 200); } /** * Get product tags */ public static function get_tags(WP_REST_Request $request) { $terms = get_terms([ 'taxonomy' => 'product_tag', 'hide_empty' => false, ]); if (is_wp_error($terms)) { return new WP_REST_Response([], 200); // Return empty array on error } $tags = []; foreach ($terms as $term) { $tags[] = [ 'term_id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, 'count' => $term->count, ]; } return new WP_REST_Response($tags, 200); } /** * Get product attributes */ public static function get_attributes(WP_REST_Request $request) { $attributes = wc_get_attribute_taxonomies(); $result = []; foreach ($attributes as $attribute) { $result[] = [ 'attribute_id' => $attribute->attribute_id, 'attribute_name' => $attribute->attribute_name, 'attribute_label' => $attribute->attribute_label, 'attribute_type' => $attribute->attribute_type, 'attribute_orderby' => $attribute->attribute_orderby, 'attribute_public' => $attribute->attribute_public, ]; } return new WP_REST_Response($result, 200); } /** * Format product for list view * Returns all essential product fields including type, status, prices, stock, etc. */ private static function format_product_list_item($product) { $image = wp_get_attachment_image_src($product->get_image_id(), 'thumbnail'); // Get price HTML - for variable products, show price range $price_html = $product->get_price_html(); if (empty($price_html) && $product->is_type('variable')) { $prices = $product->get_variation_prices(true); if (!empty($prices['price'])) { $min_price = min($prices['price']); $max_price = max($prices['price']); if ($min_price !== $max_price) { $price_html = wc_format_price_range($min_price, $max_price); } else { $price_html = wc_price($min_price); } } } return [ 'id' => $product->get_id(), 'name' => $product->get_name(), 'sku' => $product->get_sku(), 'type' => $product->get_type(), 'status' => $product->get_status(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price(), 'price_html' => $price_html, 'stock_status' => $product->get_stock_status(), 'stock_quantity' => $product->get_stock_quantity(), 'manage_stock' => $product->get_manage_stock(), 'image_url' => $image ? $image[0] : '', 'permalink' => get_permalink($product->get_id()), 'date_created' => $product->get_date_created() ? $product->get_date_created()->date('Y-m-d H:i:s') : '', 'date_modified' => $product->get_date_modified() ? $product->get_date_modified()->date('Y-m-d H:i:s') : '', ]; } /** * Format product with full details */ private static function format_product_full($product) { $data = self::format_product_list_item($product); // Add full details $data['description'] = $product->get_description(); $data['short_description'] = $product->get_short_description(); $data['slug'] = $product->get_slug(); $data['weight'] = $product->get_weight(); $data['length'] = $product->get_length(); $data['width'] = $product->get_width(); $data['height'] = $product->get_height(); $data['categories'] = $product->get_category_ids(); $data['tags'] = $product->get_tag_ids(); $data['image_id'] = $product->get_image_id(); $data['gallery_image_ids'] = $product->get_gallery_image_ids(); $data['virtual'] = $product->is_virtual(); $data['downloadable'] = $product->is_downloadable(); $data['featured'] = $product->is_featured(); // Images array (URLs) for frontend - featured + gallery $images = []; $featured_image_id = $product->get_image_id(); if ($featured_image_id) { $featured_url = wp_get_attachment_url($featured_image_id); if ($featured_url) { $images[] = $featured_url; } } foreach ($product->get_gallery_image_ids() as $image_id) { $url = wp_get_attachment_url($image_id); if ($url) { $images[] = $url; } } $data['images'] = $images; // Gallery images (detailed info) $gallery = []; foreach ($product->get_gallery_image_ids() as $image_id) { $image = wp_get_attachment_image_src($image_id, 'full'); if ($image) { $gallery[] = [ 'id' => $image_id, 'url' => $image[0], 'width' => $image[1], 'height' => $image[2], ]; } } $data['gallery'] = $gallery; // Variable product specifics if ($product->is_type('variable')) { $data['attributes'] = self::get_product_attributes($product); $data['variations'] = self::get_product_variations($product); } // Expose meta data (Level 1 compatibility) $data['meta'] = self::get_product_meta_data($product); // Allow plugins to modify response (Level 1 compatibility) $data = apply_filters('woonoow/product_api_data', $data, $product); return $data; } /** * Get product attributes */ private static function get_product_attributes($product) { $attributes = []; foreach ($product->get_attributes() as $attribute) { $attributes[] = [ 'id' => $attribute->get_id(), 'name' => $attribute->get_name(), 'options' => $attribute->get_options(), 'position' => $attribute->get_position(), 'visible' => $attribute->get_visible(), 'variation' => $attribute->get_variation(), ]; } return $attributes; } /** * Get product variations */ private static function get_product_variations($product) { $variations = []; foreach ($product->get_children() as $variation_id) { $variation = wc_get_product($variation_id); if ($variation) { $image = wp_get_attachment_image_src($variation->get_image_id(), 'thumbnail'); // Format attributes with human-readable names and values $formatted_attributes = []; // Get parent product attributes to know what to look for $parent_attributes = $product->get_attributes(); foreach ($parent_attributes as $parent_attr) { if (!$parent_attr->get_variation()) { continue; // Skip non-variation attributes } $attr_name = $parent_attr->get_name(); $clean_name = $attr_name; // Get the variation's value for this attribute if (strpos($attr_name, 'pa_') === 0) { // Global/taxonomy attribute $clean_name = wc_attribute_label($attr_name); $value = $variation->get_attribute($attr_name); // Convert slug to term name if (!empty($value)) { $term = get_term_by('slug', $value, $attr_name); $value = $term ? $term->name : $value; } } else { // Custom attribute - WooCommerce stores as 'attribute_' + exact attribute name $meta_key = 'attribute_' . $attr_name; $value = get_post_meta($variation_id, $meta_key, true); // Capitalize the attribute name for display $clean_name = ucfirst($attr_name); } $formatted_attributes[$clean_name] = $value; } $image_url = $image ? $image[0] : ''; if (!$image_url && $variation->get_image_id()) { $image_url = wp_get_attachment_url($variation->get_image_id()); } $variations[] = [ 'id' => $variation->get_id(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price(), 'stock_status' => $variation->get_stock_status(), 'stock_quantity' => $variation->get_stock_quantity(), 'manage_stock' => $variation->get_manage_stock(), 'attributes' => $formatted_attributes, 'image_id' => $variation->get_image_id(), 'image_url' => $image_url, 'image' => $image_url, // For form compatibility ]; } } return $variations; } /** * Save product attributes */ private static function save_product_attributes($product, $attributes_data) { $attributes = []; foreach ($attributes_data as $attr_data) { $attribute = new \WC_Product_Attribute(); $attribute->set_name($attr_data['name']); $attribute->set_options($attr_data['options']); $attribute->set_position($attr_data['position'] ?? 0); $attribute->set_visible($attr_data['visible'] ?? true); $attribute->set_variation($attr_data['variation'] ?? true); $attributes[] = $attribute; } $product->set_attributes($attributes); $product->save(); } /** * Save product variations */ private static function save_product_variations($product, $variations_data) { foreach ($variations_data as $var_data) { if (isset($var_data['id']) && $var_data['id']) { // Update existing variation $variation = wc_get_product($var_data['id']); } else { // Create new variation $variation = new WC_Product_Variation(); $variation->set_parent_id($product->get_id()); } if ($variation) { if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']); if (isset($var_data['regular_price'])) $variation->set_regular_price($var_data['regular_price']); if (isset($var_data['sale_price'])) $variation->set_sale_price($var_data['sale_price']); if (isset($var_data['stock_status'])) $variation->set_stock_status($var_data['stock_status']); if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']); if (isset($var_data['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']); if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']); // Handle image - support both image_id and image URL if (isset($var_data['image']) && !empty($var_data['image'])) { $image_id = attachment_url_to_postid($var_data['image']); if ($image_id) { $variation->set_image_id($image_id); } } elseif (isset($var_data['image_id'])) { $variation->set_image_id($var_data['image_id']); } $variation->save(); } } } /** * Get product meta data for API exposure (Level 1 compatibility) * Filters out internal meta unless explicitly allowed * * @param \WC_Product $product * @return array */ private static function get_product_meta_data($product) { $meta_data = []; foreach ($product->get_meta_data() as $meta) { $key = $meta->key; $value = $meta->value; // Skip internal WooCommerce meta (starts with _wc_) if (strpos($key, '_wc_') === 0) { continue; } // Skip WooNooW internal meta if (strpos($key, '_woonoow_') === 0) { continue; } // Public meta (no underscore) - always expose if (strpos($key, '_') !== 0) { $meta_data[$key] = $value; continue; } } return $meta_data; } /** * Update product meta data from API (Level 1 compatibility) * * @param \WC_Product $product * @param array $meta_updates */ private static function update_product_meta_data($product, $meta_updates) { // Get allowed updatable meta keys // Core has ZERO defaults - plugins register via filter $allowed = apply_filters('woonoow/product_updatable_meta', [], $product); foreach ($meta_updates as $key => $value) { // Skip internal WooCommerce meta if (strpos($key, '_wc_') === 0) { continue; } // Skip WooNooW internal meta if (strpos($key, '_woonoow_') === 0) { continue; } // Public meta (no underscore) - always allow if (strpos($key, '_') !== 0) { $product->update_meta_data($key, $value); continue; } // Private meta - check if allowed if (in_array($key, $allowed, true)) { $product->update_meta_data($key, $value); } } } }