0 ? wc_get_product( $id ) : false; if ( ! $product || 'trash' === $product->get_status() ) { return array( 'id' => $id, 'status' => 'not_found', 'message' => 'This item no longer exists on the store.', ); } // Description changes report the texts too, so the app can show them and undo them. $with_text = isset( $changes['description'] ) || isset( $changes['short_description'] ); $before = self::row( $product, $with_text ); // Changed on the store since the app read it: hold it back instead of overwriting. The fingerprint catches // edits made within the same second as the app's read, which leave the modified date as it was. $date_changed = isset( $item['expected_modified'] ) && null !== $item['expected_modified'] && (int) $item['expected_modified'] !== (int) $before['date_modified']; $content_changed = ! empty( $item['expected_fingerprint'] ) && $item['expected_fingerprint'] !== $before['fingerprint']; if ( $date_changed || $content_changed ) { return array( 'id' => $id, 'status' => 'conflict', 'message' => 'This item was changed on the store after Storeloom loaded it.', 'before' => $before, ); } try { foreach ( $changes as $field => $value ) { self::set_field( $product, $field, $value ); } $product->save(); } catch ( Exception $e ) { return array( 'id' => $id, 'status' => 'error', 'message' => wp_strip_all_tags( $e->getMessage() ), 'before' => $before, ); } $saved = wc_get_product( $id ); return array( 'id' => $id, 'status' => 'ok', 'before' => $before, 'after' => self::row( $saved ? $saved : $product, $with_text ), ); } private static function row( WC_Product $product, $with_text ) { $row = Storeloom_Catalog::row( $product ); if ( $with_text ) { $texts = Storeloom_Catalog::texts( $product ); $row['description'] = $texts['description']; $row['short_description'] = $texts['short_description']; } return $row; } private static function set_field( WC_Product $product, $field, $value ) { $is_variation = $product->is_type( 'variation' ); switch ( $field ) { case 'name': if ( $is_variation ) { throw new Exception( 'Variation names come from their attributes and cannot be changed.' ); } $product->set_name( sanitize_text_field( $value ) ); break; case 'sku': $product->set_sku( wc_clean( $value ) ); // Throws WC_Data_Exception for invalid or duplicate SKUs. break; case 'status': $allowed = $is_variation ? array( 'publish', 'private' ) : array( 'publish', 'draft', 'pending', 'private' ); if ( ! in_array( $value, $allowed, true ) ) { throw new Exception( 'Unknown status.' ); } $product->set_status( $value ); break; case 'regular_price': $product->set_regular_price( wc_format_decimal( $value ) ); break; case 'sale_price': $product->set_sale_price( wc_format_decimal( $value ) ); break; case 'manage_stock': $product->set_manage_stock( 'yes' === $value || true === $value ); break; case 'stock_quantity': $product->set_stock_quantity( null === $value || '' === $value ? null : wc_stock_amount( $value ) ); break; case 'stock_status': $product->set_stock_status( wc_clean( $value ) ); break; case 'backorders': $product->set_backorders( wc_clean( $value ) ); break; case 'weight': $product->set_weight( wc_format_decimal( $value ) ); break; case 'length': $product->set_length( wc_format_decimal( $value ) ); break; case 'width': $product->set_width( wc_format_decimal( $value ) ); break; case 'height': $product->set_height( wc_format_decimal( $value ) ); break; case 'shipping_class_id': $product->set_shipping_class_id( absint( $value ) ); break; case 'tax_class': // WooCommerce quietly turns unknown classes into Standard; refuse them instead. $class = sanitize_title( (string) $value ); $valid = array_merge( array( '' ), WC_Tax::get_tax_class_slugs(), $is_variation ? array( 'parent' ) : array() ); if ( ! in_array( $class, $valid, true ) ) { throw new Exception( sprintf( 'The store has no tax class "%s".', esc_html( (string) $value ) ) ); } $product->set_tax_class( $class ); break; case 'catalog_visibility': if ( $is_variation ) { throw new Exception( 'Variations have no catalog visibility.' ); } $product->set_catalog_visibility( wc_clean( $value ) ); // Throws for unknown values. break; case 'featured': if ( $is_variation ) { throw new Exception( 'Variations cannot be featured.' ); } $product->set_featured( true === $value || 'yes' === $value || 1 === $value ); break; case 'description': case 'short_description': if ( $is_variation && 'short_description' === $field ) { throw new Exception( 'Variations have no short description.' ); } // Same filtering as post content: safe HTML only. $text = wp_kses_post( (string) $value ); if ( 'description' === $field ) { $product->set_description( $text ); } else { $product->set_short_description( $text ); } break; case 'sale_from_date': case 'sale_to_date': // Like WooCommerce's product screen: a sale starts at the beginning and ends at the end of the day, // in the store's timezone. if ( '' !== $value && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $value ) ) { throw new Exception( 'Sale dates must look like 2026-11-01.' ); } if ( 'sale_from_date' === $field ) { $product->set_date_on_sale_from( '' === $value ? '' : $value . ' 00:00:00' ); } else { $product->set_date_on_sale_to( '' === $value ? '' : $value . ' 23:59:59' ); } break; case 'category_ids': case 'tag_ids': if ( $is_variation ) { throw new Exception( 'Variations take their categories and tags from their product.' ); } $taxonomy = 'category_ids' === $field ? 'product_cat' : 'product_tag'; $ids = array_map( 'absint', is_array( $value ) ? $value : array() ); foreach ( $ids as $term_id ) { if ( ! term_exists( $term_id, $taxonomy ) ) { throw new Exception( sprintf( 'Term %d no longer exists on the store.', (int) $term_id ) ); } } if ( 'category_ids' === $field ) { $product->set_category_ids( $ids ); } else { $product->set_tag_ids( $ids ); } break; default: throw new Exception( sprintf( 'Unknown field: %s', esc_html( (string) $field ) ) ); } } }