403Webshell
Server IP : 104.171.130.249  /  Your IP : 216.73.216.146
Web Server : nginx/1.25.5
System : Linux 0dac65491b7e 6.8.0-134-generic #134-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 26 18:43:11 UTC 2026 x86_64
User : root ( 0)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : OFF  |  Perl : ON  |  Python : OFF  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /var/www/html/wp-content/themes/matreshka/inc/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/wp-content/themes/matreshka/inc/api-product-management.php
<?php
/**
 * Product Management API Endpoints
 *
 * Internal endpoints for supermarket programmers to manage products
 * via REST API. Requires WooCommerce + ACF.
 *
 * @package Matreshka
 */

defined( 'ABSPATH' ) || exit;

add_action( 'rest_api_init', 'matreshka_register_product_management_endpoints' );

function matreshka_register_product_management_endpoints() {
	$write_auth = [
		'permission_callback' => function () {
			return current_user_can( 'edit_products' );
		},
	];

	// Create single product
	register_rest_route( 'wc/v3', '/products/create', [
		'methods'  => 'POST',
		'callback' => 'matreshka_api_create_product',
		...$write_auth,
	] );

	// Batch create products
	register_rest_route( 'wc/v3', '/products/batch-create', [
		'methods'  => 'POST',
		'callback' => 'matreshka_api_batch_create_products',
		...$write_auth,
	] );

	// Update single product (by ID or SKU)
	register_rest_route( 'wc/v3', '/products/update/(?P<id>[\w.-]+)', [
		'methods'  => 'PUT',
		'callback' => 'matreshka_api_update_product',
		...$write_auth,
	] );

	// Batch update products
	register_rest_route( 'wc/v3', '/products/batch-update', [
		'methods'  => 'PUT',
		'callback' => 'matreshka_api_batch_update_products',
		...$write_auth,
	] );

	// Quick update price/stock (single, by ID or SKU)
	register_rest_route( 'wc/v3', '/products/quick-update/(?P<id>[\w.-]+)', [
		'methods'  => 'PATCH',
		'callback' => 'matreshka_api_quick_update_product',
		...$write_auth,
	] );

	// Quick update price/stock (batch)
	register_rest_route( 'wc/v3', '/products/batch-quick-update', [
		'methods'  => 'PATCH',
		'callback' => 'matreshka_api_batch_quick_update_products',
		...$write_auth,
	] );

	// Delete single product (by ID or SKU)
	register_rest_route( 'wc/v3', '/products/delete/(?P<id>[\w.-]+)', [
		'methods'  => 'DELETE',
		'callback' => 'matreshka_api_delete_product',
		'permission_callback' => function () {
			return current_user_can( 'delete_products' );
		},
	] );

	// Batch delete products
	register_rest_route( 'wc/v3', '/products/batch-delete', [
		'methods'  => 'DELETE',
		'callback' => 'matreshka_api_batch_delete_products',
		'permission_callback' => function () {
			return current_user_can( 'delete_products' );
		},
	] );

	$read_auth = [
		'permission_callback' => function () {
			return current_user_can( 'edit_products' );
		},
	];

	// Get single product (by ID or SKU)
	register_rest_route( 'wc/v3', '/products/get/(?P<id>[\w.-]+)', [
		'methods'  => 'GET',
		'callback' => 'matreshka_api_manage_get_product',
		...$read_auth,
	] );

	// Get list of products
	register_rest_route( 'wc/v3', '/products/list', [
		'methods'  => 'GET',
		'callback' => 'matreshka_api_manage_list_products',
		...$read_auth,
	] );
}

/* ──────────────────────────────────────────────────────────────
   HELPERS
   ────────────────────────────────────────────────────────────── */

/**
 * Resolve a product identifier (numeric ID or SKU string) to a product ID.
 *
 * @param string|int $identifier Product ID or SKU.
 * @return int Product ID, or 0 if not found.
 */
function matreshka_resolve_product_id( $identifier ) {
	if ( is_numeric( $identifier ) ) {
		return absint( $identifier );
	}

	$identifier = sanitize_text_field( $identifier );
	$product_id = wc_get_product_id_by_sku( $identifier );

	return $product_id ? absint( $product_id ) : 0;
}

/**
 * Resolve product ID from a batch item that may contain 'id' or 'sku'.
 *
 * @param array $item Batch item with 'id' and/or 'sku'.
 * @return int Product ID, or 0 if not found.
 */
function matreshka_resolve_batch_product_id( $item ) {
	if ( ! empty( $item['id'] ) && is_numeric( $item['id'] ) ) {
		return absint( $item['id'] );
	}
	if ( ! empty( $item['sku'] ) ) {
		return matreshka_resolve_product_id( $item['sku'] );
	}
	return 0;
}

/**
 * Sideload an image from a URL into the WP Media Library.
 *
 * @param string $url        Remote image URL.
 * @param int    $post_id    Post to attach image to (0 = unattached).
 * @param string $description Alt text / description.
 * @return int|WP_Error      Attachment ID on success.
 */
function matreshka_sideload_image( $url, $post_id = 0, $description = '' ) {
	if ( ! function_exists( 'media_sideload_image' ) ) {
		require_once ABSPATH . 'wp-admin/includes/media.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';
	}

	// Validate URL scheme
	$parsed = wp_parse_url( $url );
	if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], [ 'http', 'https' ], true ) ) {
		return new WP_Error( 'invalid_url', 'Image URL must use http or https scheme.' );
	}

	// Download to temp file
	$tmp = download_url( $url, 30 );
	if ( is_wp_error( $tmp ) ) {
		return $tmp;
	}

	// Validate MIME type
	$file_info = wp_check_filetype( basename( wp_parse_url( $url, PHP_URL_PATH ) ) );
	$allowed   = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ];
	$real_mime  = mime_content_type( $tmp );
	if ( ! in_array( $real_mime, $allowed, true ) ) {
		@unlink( $tmp );
		return new WP_Error( 'invalid_mime', 'File is not an allowed image type. Allowed: jpg, png, gif, webp.' );
	}

	$file_array = [
		'name'     => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
		'tmp_name' => $tmp,
	];

	$attachment_id = media_handle_sideload( $file_array, $post_id, $description );

	if ( is_wp_error( $attachment_id ) ) {
		@unlink( $tmp );
	}

	return $attachment_id;
}

/**
 * Validate required fields in product data.
 *
 * @param array $data Product data array.
 * @return true|WP_Error
 */
function matreshka_validate_product_data( $data ) {
	$errors = [];

	if ( empty( $data['name'] ) ) {
		$errors[] = 'name is required';
	}
	if ( ! isset( $data['regular_price'] ) || $data['regular_price'] === '' ) {
		$errors[] = 'regular_price is required';
	}
	if ( empty( $data['categories'] ) || ! is_array( $data['categories'] ) ) {
		$errors[] = 'categories is required (array of category IDs)';
	}
	if ( empty( $data['image_url'] ) ) {
		$errors[] = 'image_url is required (main product image)';
	}

	if ( ! empty( $errors ) ) {
		return new WP_Error(
			'validation_error',
			'Validation failed: ' . implode( '; ', $errors ),
			[ 'status' => 400, 'errors' => $errors ]
		);
	}

	return true;
}

/**
 * Create a single WooCommerce product from data array.
 *
 * @param array $data Product data.
 * @return array|WP_Error Product result or error.
 */
function matreshka_create_product_from_data( $data ) {
	// Validate required fields
	$valid = matreshka_validate_product_data( $data );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}

	$product = new WC_Product_Simple();

	// ─── Core fields ───
	$product->set_name( sanitize_text_field( $data['name'] ) );
	$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
	$product->set_status( sanitize_text_field( $data['status'] ?? 'publish' ) );
	$product->set_catalog_visibility( 'visible' );

	if ( ! empty( $data['sku'] ) ) {
		$product->set_sku( sanitize_text_field( $data['sku'] ) );
	}
	if ( isset( $data['sale_price'] ) && $data['sale_price'] !== '' ) {
		$product->set_sale_price( sanitize_text_field( (string) $data['sale_price'] ) );
	}
	if ( ! empty( $data['description'] ) ) {
		$product->set_description( wp_kses_post( $data['description'] ) );
	}
	if ( ! empty( $data['short_description'] ) ) {
		$product->set_short_description( wp_kses_post( $data['short_description'] ) );
	}

	// Stock
	$stock_status = sanitize_text_field( $data['stock_status'] ?? 'instock' );
	if ( in_array( $stock_status, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
		$product->set_stock_status( $stock_status );
	}
	if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
		$product->set_manage_stock( true );
		$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
	}

	// Weight & dimensions (WooCommerce native)
	if ( ! empty( $data['wc_weight'] ) ) {
		$product->set_weight( sanitize_text_field( $data['wc_weight'] ) );
	}

	// ─── Categories ───
	$category_ids = [];
	foreach ( (array) $data['categories'] as $cat ) {
		$cat_id = is_numeric( $cat ) ? absint( $cat ) : 0;
		if ( $cat_id && term_exists( $cat_id, 'product_cat' ) ) {
			$category_ids[] = $cat_id;
		}
	}
	if ( ! empty( $category_ids ) ) {
		$product->set_category_ids( $category_ids );
	}

	// Save product first to get an ID for image attachment
	$product_id = $product->save();
	if ( ! $product_id ) {
		return new WP_Error( 'create_failed', 'Failed to create product.', [ 'status' => 500 ] );
	}

	// ─── Main image ───
	$image_errors = [];
	if ( ! empty( $data['image_url'] ) ) {
		$img_id = matreshka_sideload_image(
			esc_url_raw( $data['image_url'] ),
			$product_id,
			sanitize_text_field( $data['name'] )
		);
		if ( is_wp_error( $img_id ) ) {
			$image_errors[] = 'main image: ' . $img_id->get_error_message();
		} else {
			$product->set_image_id( $img_id );
		}
	}

	// ─── Gallery images ───
	$gallery_ids = [];
	if ( ! empty( $data['gallery_image_urls'] ) && is_array( $data['gallery_image_urls'] ) ) {
		foreach ( $data['gallery_image_urls'] as $idx => $gallery_url ) {
			$gal_id = matreshka_sideload_image(
				esc_url_raw( $gallery_url ),
				$product_id,
				sanitize_text_field( $data['name'] ) . ' — gallery ' . ( $idx + 1 )
			);
			if ( is_wp_error( $gal_id ) ) {
				$image_errors[] = 'gallery image #' . ( $idx + 1 ) . ': ' . $gal_id->get_error_message();
			} else {
				$gallery_ids[] = $gal_id;
			}
		}
	}
	if ( ! empty( $gallery_ids ) ) {
		$product->set_gallery_image_ids( $gallery_ids );
	}

	// Save again with images
	$product->save();

	// ─── ACF custom fields ───
	$acf_fields = [
		'proteins', 'fats', 'carbs', 'calories',
		'weight', 'storage_life', 'storage', 'country',
		'proteins_label', 'fats_label', 'carbs_label', 'calories_label',
	];
	foreach ( $acf_fields as $field ) {
		if ( isset( $data[ $field ] ) && $data[ $field ] !== '' ) {
			update_field( $field, sanitize_text_field( $data[ $field ] ), $product_id );
		}
	}

	// ─── Build response ───
	$response = [
		'id'            => $product_id,
		'name'          => $product->get_name(),
		'slug'          => $product->get_slug(),
		'sku'           => $product->get_sku(),
		'status'        => $product->get_status(),
		'permalink'     => get_permalink( $product_id ),
		'regular_price' => $product->get_regular_price(),
		'sale_price'    => $product->get_sale_price(),
		'stock_status'  => $product->get_stock_status(),
		'image'         => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ) ?: '',
		'gallery'       => array_map( function ( $img_id ) {
			return [
				'id'        => $img_id,
				'thumbnail' => wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '',
				'full'      => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
			];
		}, $gallery_ids ),
		'categories'    => array_map( function ( $cat_id ) {
			$term = get_term( $cat_id, 'product_cat' );
			return $term && ! is_wp_error( $term )
				? [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug ]
				: null;
		}, $category_ids ),
		'custom'        => matreshka_get_product_custom_fields( $product_id, true ),
	];

	if ( ! empty( $image_errors ) ) {
		$response['image_warnings'] = $image_errors;
	}

	return $response;
}

/* ──────────────────────────────────────────────────────────────
   ENDPOINTS
   ────────────────────────────────────────────────────────────── */

/**
 * POST /wc/v3/products/create
 *
 * Create a single product.
 */
function matreshka_api_create_product( WP_REST_Request $request ) {
	$data   = $request->get_json_params();
	$result = matreshka_create_product_from_data( $data );

	if ( is_wp_error( $result ) ) {
		return new WP_REST_Response( [
			'success' => false,
			'code'    => $result->get_error_code(),
			'message' => $result->get_error_message(),
			'errors'  => $result->get_error_data()['errors'] ?? [],
		], $result->get_error_data()['status'] ?? 400 );
	}

	return new WP_REST_Response( [
		'success' => true,
		'data'    => $result,
	], 201 );
}

/**
 * POST /wc/v3/products/batch-create
 *
 * Create multiple products in a single request.
 * Body: { "products": [ {...}, {...}, ... ] }
 * Max 50 products per batch.
 */
function matreshka_api_batch_create_products( WP_REST_Request $request ) {
	$body     = $request->get_json_params();
	$products = $body['products'] ?? [];

	if ( empty( $products ) || ! is_array( $products ) ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'products array is required.',
		], 400 );
	}

	$max_batch = 50;
	if ( count( $products ) > $max_batch ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => "Maximum {$max_batch} products per batch request.",
		], 400 );
	}

	$results = [
		'created' => [],
		'errors'  => [],
	];

	foreach ( $products as $index => $product_data ) {
		$result = matreshka_create_product_from_data( $product_data );

		if ( is_wp_error( $result ) ) {
			$results['errors'][] = [
				'index'   => $index,
				'name'    => $product_data['name'] ?? '(unnamed)',
				'code'    => $result->get_error_code(),
				'message' => $result->get_error_message(),
			];
		} else {
			$results['created'][] = $result;
		}
	}

	$status = empty( $results['errors'] ) ? 201 : 207;

	return new WP_REST_Response( [
		'success'       => empty( $results['errors'] ),
		'total'         => count( $products ),
		'created_count' => count( $results['created'] ),
		'error_count'   => count( $results['errors'] ),
		'created'       => $results['created'],
		'errors'        => $results['errors'],
	], $status );
}

/* ──────────────────────────────────────────────────────────────
   UPDATE HELPERS
   ────────────────────────────────────────────────────────────── */

/**
 * Build a standardized product response array.
 */
function matreshka_build_product_response( $product ) {
	$id          = $product->get_id();
	$gallery_ids = $product->get_gallery_image_ids();

	return [
		'id'            => $id,
		'name'          => $product->get_name(),
		'slug'          => $product->get_slug(),
		'sku'           => $product->get_sku(),
		'status'        => $product->get_status(),
		'permalink'     => get_permalink( $id ),
		'regular_price' => $product->get_regular_price(),
		'sale_price'    => $product->get_sale_price(),
		'date_on_sale_to' => $product->get_date_on_sale_to() ? $product->get_date_on_sale_to()->date( 'Y-m-d H:i:s' ) : '',
		'stock_status'  => $product->get_stock_status(),
		'stock_quantity' => $product->get_stock_quantity(),
		'image'         => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ) ?: '',
		'gallery'       => array_map( function ( $img_id ) {
			return [
				'id'        => $img_id,
				'thumbnail' => wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '',
				'full'      => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
			];
		}, $gallery_ids ),
		'categories'    => array_map( function ( $cat_id ) {
			$term = get_term( $cat_id, 'product_cat' );
			return $term && ! is_wp_error( $term )
				? [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug ]
				: null;
		}, $product->get_category_ids() ),
		'custom'        => matreshka_get_product_custom_fields( $id, true ),
	];
}

/**
 * Update an existing WooCommerce product from data array.
 * Only provided fields are updated (partial update / PATCH semantics).
 *
 * @param int   $product_id Existing product ID.
 * @param array $data       Fields to update.
 * @return array|WP_Error   Product response or error.
 */
function matreshka_update_product_from_data( $product_id, $data ) {
	$product = wc_get_product( $product_id );
	if ( ! $product || ! $product instanceof WC_Product ) {
		return new WP_Error( 'not_found', 'Product not found.', [ 'status' => 404 ] );
	}

	// ─── Core fields (only if provided) ───
	if ( isset( $data['name'] ) && $data['name'] !== '' ) {
		$product->set_name( sanitize_text_field( $data['name'] ) );
	}
	if ( isset( $data['regular_price'] ) && $data['regular_price'] !== '' ) {
		$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
	}
	if ( array_key_exists( 'sale_price', $data ) ) {
		if ( $data['sale_price'] === '' || $data['sale_price'] === null ) {
			$product->set_sale_price( '' );
		} else {
			$product->set_sale_price( sanitize_text_field( (string) $data['sale_price'] ) );
		}
	}
	if ( array_key_exists( 'date_on_sale_to', $data ) ) {
		if ( empty( $data['date_on_sale_to'] ) ) {
			$product->set_date_on_sale_to( '' );
		} else {
			$product->set_date_on_sale_to( sanitize_text_field( $data['date_on_sale_to'] ) );
		}
	}
	if ( array_key_exists( 'date_on_sale_from', $data ) ) {
		if ( empty( $data['date_on_sale_from'] ) ) {
			$product->set_date_on_sale_from( '' );
		} else {
			$product->set_date_on_sale_from( sanitize_text_field( $data['date_on_sale_from'] ) );
		}
	}
	if ( isset( $data['sku'] ) ) {
		$product->set_sku( sanitize_text_field( $data['sku'] ) );
	}
	if ( isset( $data['status'] ) ) {
		$product->set_status( sanitize_text_field( $data['status'] ) );
	}
	if ( isset( $data['description'] ) ) {
		$product->set_description( wp_kses_post( $data['description'] ) );
	}
	if ( isset( $data['short_description'] ) ) {
		$product->set_short_description( wp_kses_post( $data['short_description'] ) );
	}

	// Stock
	if ( isset( $data['stock_status'] ) ) {
		$stock_status = sanitize_text_field( $data['stock_status'] );
		if ( in_array( $stock_status, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
			$product->set_stock_status( $stock_status );
		}
	}
	if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
		$product->set_manage_stock( true );
		$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
	}

	// WC weight
	if ( isset( $data['wc_weight'] ) ) {
		$product->set_weight( sanitize_text_field( $data['wc_weight'] ) );
	}

	// ─── Categories ───
	if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
		$category_ids = [];
		foreach ( $data['categories'] as $cat ) {
			$cat_id = is_numeric( $cat ) ? absint( $cat ) : 0;
			if ( $cat_id && term_exists( $cat_id, 'product_cat' ) ) {
				$category_ids[] = $cat_id;
			}
		}
		if ( ! empty( $category_ids ) ) {
			$product->set_category_ids( $category_ids );
		}
	}

	// ─── Main image ───
	$image_errors = [];
	if ( ! empty( $data['image_url'] ) ) {
		$img_id = matreshka_sideload_image(
			esc_url_raw( $data['image_url'] ),
			$product_id,
			$product->get_name()
		);
		if ( is_wp_error( $img_id ) ) {
			$image_errors[] = 'main image: ' . $img_id->get_error_message();
		} else {
			$product->set_image_id( $img_id );
		}
	}

	// ─── Gallery images (replace all) ───
	if ( isset( $data['gallery_image_urls'] ) && is_array( $data['gallery_image_urls'] ) ) {
		$gallery_ids = [];
		foreach ( $data['gallery_image_urls'] as $idx => $gallery_url ) {
			$gal_id = matreshka_sideload_image(
				esc_url_raw( $gallery_url ),
				$product_id,
				$product->get_name() . ' — gallery ' . ( $idx + 1 )
			);
			if ( is_wp_error( $gal_id ) ) {
				$image_errors[] = 'gallery image #' . ( $idx + 1 ) . ': ' . $gal_id->get_error_message();
			} else {
				$gallery_ids[] = $gal_id;
			}
		}
		$product->set_gallery_image_ids( $gallery_ids );
	}

	$product->save();

	// ─── ACF custom fields ───
	$acf_fields = [
		'proteins', 'fats', 'carbs', 'calories',
		'weight', 'storage_life', 'storage', 'country',
		'proteins_label', 'fats_label', 'carbs_label', 'calories_label',
	];
	foreach ( $acf_fields as $field ) {
		if ( isset( $data[ $field ] ) ) {
			update_field( $field, sanitize_text_field( $data[ $field ] ), $product_id );
		}
	}

	$response = matreshka_build_product_response( $product );

	if ( ! empty( $image_errors ) ) {
		$response['image_warnings'] = $image_errors;
	}

	return $response;
}

/* ──────────────────────────────────────────────────────────────
   UPDATE ENDPOINTS
   ────────────────────────────────────────────────────────────── */

/**
 * PUT /wc/v3/products/update/{id}
 *
 * Full update of a single product. Only provided fields are changed.
 */
function matreshka_api_update_product( WP_REST_Request $request ) {
	$data       = $request->get_json_params();
	$product_id = matreshka_resolve_product_id( $request['id'] );
	if ( ! $product_id ) {
		return new WP_REST_Response( [
			'success' => false,
			'code'    => 'not_found',
			'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
		], 404 );
	}
	$result = matreshka_update_product_from_data( $product_id, $data );

	if ( is_wp_error( $result ) ) {
		return new WP_REST_Response( [
			'success' => false,
			'code'    => $result->get_error_code(),
			'message' => $result->get_error_message(),
		], $result->get_error_data()['status'] ?? 400 );
	}

	return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
}

/**
 * PUT /wc/v3/products/batch-update
 *
 * Batch update products.
 * Body: { "products": [ { "id": 123, ...fields }, ... ] }
 * Max 100 per batch.
 */
function matreshka_api_batch_update_products( WP_REST_Request $request ) {
	$body     = $request->get_json_params();
	$products = $body['products'] ?? [];

	if ( empty( $products ) || ! is_array( $products ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'products array is required.' ], 400 );
	}

	$max_batch = 100;
	if ( count( $products ) > $max_batch ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
	}

	$results = [ 'updated' => [], 'errors' => [] ];

	foreach ( $products as $index => $item ) {
		$pid = matreshka_resolve_batch_product_id( $item );
		if ( ! $pid ) {
			$results['errors'][] = [ 'index' => $index, 'code' => 'missing_id', 'message' => 'id or sku is required.' ];
			continue;
		}
		$result = matreshka_update_product_from_data( $pid, $item );
		if ( is_wp_error( $result ) ) {
			$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'code' => $result->get_error_code(), 'message' => $result->get_error_message() ];
		} else {
			$results['updated'][] = $result;
		}
	}

	return new WP_REST_Response( [
		'success'       => empty( $results['errors'] ),
		'total'         => count( $products ),
		'updated_count' => count( $results['updated'] ),
		'error_count'   => count( $results['errors'] ),
		'updated'       => $results['updated'],
		'errors'        => $results['errors'],
	], empty( $results['errors'] ) ? 200 : 207 );
}

/* ──────────────────────────────────────────────────────────────
   QUICK UPDATE (price / sale / stock only)
   ────────────────────────────────────────────────────────────── */

/**
 * Apply quick-update fields to a product.
 * Accepts only: regular_price, sale_price, date_on_sale_to, date_on_sale_from, stock_status, stock_quantity.
 *
 * @return array|WP_Error Compact response.
 */
function matreshka_quick_update_product( $product_id, $data ) {
	$product = wc_get_product( $product_id );
	if ( ! $product || ! $product instanceof WC_Product ) {
		return new WP_Error( 'not_found', 'Product not found.', [ 'status' => 404 ] );
	}

	$allowed = [ 'regular_price', 'sale_price', 'date_on_sale_to', 'date_on_sale_from', 'stock_status', 'stock_quantity' ];
	$has_field = false;
	foreach ( $allowed as $key ) {
		if ( array_key_exists( $key, $data ) ) { $has_field = true; break; }
	}
	if ( ! $has_field ) {
		return new WP_Error( 'no_fields', 'At least one field required: ' . implode( ', ', $allowed ) . '.', [ 'status' => 400 ] );
	}

	if ( isset( $data['regular_price'] ) && $data['regular_price'] !== '' ) {
		$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
	}
	if ( array_key_exists( 'sale_price', $data ) ) {
		$product->set_sale_price( ( $data['sale_price'] === '' || $data['sale_price'] === null ) ? '' : sanitize_text_field( (string) $data['sale_price'] ) );
	}
	if ( array_key_exists( 'date_on_sale_to', $data ) ) {
		$product->set_date_on_sale_to( empty( $data['date_on_sale_to'] ) ? '' : sanitize_text_field( $data['date_on_sale_to'] ) );
	}
	if ( array_key_exists( 'date_on_sale_from', $data ) ) {
		$product->set_date_on_sale_from( empty( $data['date_on_sale_from'] ) ? '' : sanitize_text_field( $data['date_on_sale_from'] ) );
	}
	if ( isset( $data['stock_status'] ) ) {
		$ss = sanitize_text_field( $data['stock_status'] );
		if ( in_array( $ss, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
			$product->set_stock_status( $ss );
		}
	}
	if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
		$product->set_manage_stock( true );
		$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
	}

	$product->save();

	return [
		'id'              => $product->get_id(),
		'name'            => $product->get_name(),
		'regular_price'   => $product->get_regular_price(),
		'sale_price'      => $product->get_sale_price(),
		'date_on_sale_to' => $product->get_date_on_sale_to() ? $product->get_date_on_sale_to()->date( 'Y-m-d H:i:s' ) : '',
		'stock_status'    => $product->get_stock_status(),
		'stock_quantity'  => $product->get_stock_quantity(),
	];
}

/**
 * PATCH /wc/v3/products/quick-update/{id}
 */
function matreshka_api_quick_update_product( WP_REST_Request $request ) {
	$product_id = matreshka_resolve_product_id( $request['id'] );
	if ( ! $product_id ) {
		return new WP_REST_Response( [
			'success' => false,
			'code'    => 'not_found',
			'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
		], 404 );
	}
	$result = matreshka_quick_update_product( $product_id, $request->get_json_params() );

	if ( is_wp_error( $result ) ) {
		return new WP_REST_Response( [
			'success' => false, 'code' => $result->get_error_code(), 'message' => $result->get_error_message(),
		], $result->get_error_data()['status'] ?? 400 );
	}

	return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
}

/**
 * PATCH /wc/v3/products/batch-quick-update
 *
 * Body: { "products": [ { "id": 123, "regular_price": "99.90", ... }, ... ] }
 * Max 200 per batch.
 */
function matreshka_api_batch_quick_update_products( WP_REST_Request $request ) {
	$body     = $request->get_json_params();
	$products = $body['products'] ?? [];

	if ( empty( $products ) || ! is_array( $products ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'products array is required.' ], 400 );
	}

	$max_batch = 200;
	if ( count( $products ) > $max_batch ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
	}

	$results = [ 'updated' => [], 'errors' => [] ];

	foreach ( $products as $index => $item ) {
		$pid = matreshka_resolve_batch_product_id( $item );
		if ( ! $pid ) {
			$results['errors'][] = [ 'index' => $index, 'code' => 'missing_id', 'message' => 'id or sku is required.' ];
			continue;
		}
		$result = matreshka_quick_update_product( $pid, $item );
		if ( is_wp_error( $result ) ) {
			$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'code' => $result->get_error_code(), 'message' => $result->get_error_message() ];
		} else {
			$results['updated'][] = $result;
		}
	}

	return new WP_REST_Response( [
		'success'       => empty( $results['errors'] ),
		'total'         => count( $products ),
		'updated_count' => count( $results['updated'] ),
		'error_count'   => count( $results['errors'] ),
		'updated'       => $results['updated'],
		'errors'        => $results['errors'],
	], empty( $results['errors'] ) ? 200 : 207 );
}

/* ──────────────────────────────────────────────────────────────
   DELETE ENDPOINTS
   ────────────────────────────────────────────────────────────── */

/**
 * DELETE /wc/v3/products/delete/{id}
 *
 * Query param ?force=true to permanently delete (bypass trash).
 */
function matreshka_api_delete_product( WP_REST_Request $request ) {
	$product_id = matreshka_resolve_product_id( $request['id'] );
	if ( ! $product_id ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
		], 404 );
	}
	$force = filter_var( $request->get_param( 'force' ), FILTER_VALIDATE_BOOLEAN );

	$product = wc_get_product( $product_id );
	if ( ! $product || ! $product instanceof WC_Product ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Product not found.' ], 404 );
	}

	$name = $product->get_name();

	if ( $force ) {
		$product->delete( true );
		$action = 'permanently_deleted';
	} else {
		$product->delete( false );
		$action = 'trashed';
	}

	return new WP_REST_Response( [
		'success' => true,
		'data'    => [ 'id' => $product_id, 'name' => $name, 'action' => $action ],
	], 200 );
}

/**
 * DELETE /wc/v3/products/batch-delete
 *
 * Body: { "ids": [123, 456, ...], "force": false }
 * Max 100 per batch.
 */
function matreshka_api_batch_delete_products( WP_REST_Request $request ) {
	$body  = $request->get_json_params();
	$ids   = $body['ids'] ?? [];
	$force = ! empty( $body['force'] );

	if ( empty( $ids ) || ! is_array( $ids ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'ids array is required.' ], 400 );
	}

	$max_batch = 100;
	if ( count( $ids ) > $max_batch ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
	}

	$results = [ 'deleted' => [], 'errors' => [] ];

	foreach ( $ids as $index => $raw_id ) {
		$pid = matreshka_resolve_product_id( $raw_id );
		$product = $pid ? wc_get_product( $pid ) : null;

		if ( ! $product || ! $product instanceof WC_Product ) {
			$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'message' => 'Product not found.' ];
			continue;
		}

		$name   = $product->get_name();
		$action = $force ? 'permanently_deleted' : 'trashed';
		$product->delete( $force );

		$results['deleted'][] = [ 'id' => $pid, 'name' => $name, 'action' => $action ];
	}

	return new WP_REST_Response( [
		'success'       => empty( $results['errors'] ),
		'total'         => count( $ids ),
		'deleted_count' => count( $results['deleted'] ),
		'error_count'   => count( $results['errors'] ),
		'deleted'       => $results['deleted'],
		'errors'        => $results['errors'],
	], empty( $results['errors'] ) ? 200 : 207 );
}

/* ──────────────────────────────────────────────────────────────
   GET ENDPOINTS
   ────────────────────────────────────────────────────────────── */

/**
 * GET /wc/v3/products/get/{id}
 *
 * Return a single product by numeric ID or SKU.
 */
function matreshka_api_manage_get_product( WP_REST_Request $request ) {
	$product_id = matreshka_resolve_product_id( $request['id'] );
	if ( ! $product_id ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
		], 404 );
	}

	$product = wc_get_product( $product_id );
	if ( ! $product || ! $product instanceof WC_Product ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Product not found.' ], 404 );
	}

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_build_product_response( $product ),
	], 200 );
}

/**
 * GET /wc/v3/products/list
 *
 * Return a paginated list of products.
 *
 * Query params:
 *   per_page  — items per page (1–100, default 20)
 *   page      — page number (default 1)
 *   category  — filter by category ID
 *   status    — filter by status (publish, draft, pending, trash)
 *   search    — search by product name
 *   orderby   — date, title, price, id (default date)
 *   order     — asc, desc (default desc)
 */
function matreshka_api_manage_list_products( WP_REST_Request $request ) {
	$per_page = absint( $request->get_param( 'per_page' ) ?: 20 );
	$per_page = max( 1, min( 100, $per_page ) );
	$page     = max( 1, absint( $request->get_param( 'page' ) ?: 1 ) );

	$args = [
		'status'   => 'publish',
		'limit'    => $per_page,
		'page'     => $page,
		'paginate' => true,
	];

	// Category filter
	$category = $request->get_param( 'category' );
	if ( $category ) {
		if ( is_numeric( $category ) ) {
			$args['tax_query'] = [ [
				'taxonomy' => 'product_cat',
				'field'    => 'term_id',
				'terms'    => absint( $category ),
			] ];
		} else {
			$args['category'] = [ sanitize_text_field( $category ) ];
		}
	}

	// Status filter
	$status = $request->get_param( 'status' );
	if ( $status && in_array( $status, [ 'publish', 'draft', 'pending', 'trash' ], true ) ) {
		$args['status'] = $status;
	}

	// Search
	$search = $request->get_param( 'search' );
	if ( $search ) {
		$args['s'] = sanitize_text_field( $search );
	}

	// Ordering
	$orderby = $request->get_param( 'orderby' );
	if ( $orderby && in_array( $orderby, [ 'date', 'title', 'price', 'id' ], true ) ) {
		$args['orderby'] = $orderby;
	}
	$order = $request->get_param( 'order' );
	if ( $order && in_array( strtolower( $order ), [ 'asc', 'desc' ], true ) ) {
		$args['order'] = strtoupper( $order );
	}

	$query   = new WC_Product_Query( $args );
	$results = $query->get_products();

	$products = [];
	foreach ( $results->products as $product ) {
		$products[] = matreshka_build_product_response( $product );
	}

	$total       = (int) $results->total;
	$total_pages = (int) $results->max_num_pages;

	return new WP_REST_Response( [
		'success'     => true,
		'data'        => $products,
		'total'       => $total,
		'total_pages' => $total_pages,
		'page'        => $page,
		'per_page'    => $per_page,
	], 200 );
}

Youez - 2016 - github.com/yon3zu
LinuXploit