array( 'available_stock', 'purchase_limit', 'archived' ), 'variant' => array( 'available_stock' ), ); } /** * Get product data safe to serialize into public page HTML. * * @param \SureCart\Models\Product|array|object|null $product The product model or its array/object form. * * @return array|mixed The stripped product array. Non-object/array input passes through untouched. */ public static function product( $product ) { return ( new self() )->stripPrivateProductFields( self::toDataArray( $product ) ); } /** * Get price data safe to serialize into public page HTML. * * Also strips an expanded product on the price. * * @param \SureCart\Models\Price|array|object|null $price The price model or its array/object form. * * @return array|mixed The stripped price array. Non-object/array input passes through untouched. */ public static function price( $price ) { return ( new self() )->stripPrivatePriceFields( self::toDataArray( $price ) ); } /** * Get variant data safe to serialize into public page HTML. * * @param \SureCart\Models\Variant|array|object|null $variant The variant model or its array/object form. * * @return array|mixed The stripped variant array. Non-object/array input passes through untouched. */ public static function variant( $variant ) { return ( new self() )->stripPrivateVariantFields( self::toDataArray( $variant ) ); } /** * Get variant option data safe to serialize into public page HTML. * * @param \SureCart\Models\VariantOption|array|object|null $option The variant option model or its array/object form. * * @return array|mixed The stripped variant option array. Non-object/array input passes through untouched. */ public static function variantOption( $option ) { return ( new self() )->stripPrivateVariantOptionFields( self::toDataArray( $option ) ); } /** * Strip every catalog object expanded anywhere in a guest-facing response. * * The checkout and line item routes accept anonymous callers by design * (guest checkout), and the platform applies `price.product` / `variant.product` * to every price or variant in the response — line items, swaps, bundle * components, bumps, upsells, the upsell funnel. Matching on `object` instead * of known paths means a new expandable relation can't reopen the leak. * * @param array|mixed $data Response data. Anything else passes through untouched. * * @return array|mixed */ public static function response( $data ) { return ( new self() )->stripCatalogTree( $data ); } /** * Depth-first strip of catalog objects, keyed by their `object` type. * * @param array|mixed $data Response data. * * @return array|mixed */ private function stripCatalogTree( $data ) { // accessor values can still be raw objects after Model::toArray; keep their JSON shape. if ( $data instanceof \stdClass ) { return (object) $this->stripCatalogTree( (array) $data ); } if ( ! is_array( $data ) ) { return $data; } foreach ( $data as $key => $value ) { $data[ $key ] = $this->stripCatalogTree( $value ); } // keyed by each model's $object_name — a new catalog model with private fields must be added here. $strippers = array( 'product' => 'stripPrivateProductFields', 'price' => 'stripPrivatePriceFields', 'variant' => 'stripPrivateVariantFields', 'variant_option' => 'stripPrivateVariantOptionFields', 'bundle_item' => 'stripPrivateBundleItemFields', 'product_collection' => 'stripPrivateCollectionFields', 'review' => 'stripPrivateReviewFields', ); $type = is_string( $data['object'] ?? null ) ? $data['object'] : ''; return isset( $strippers[ $type ] ) ? $this->{$strippers[ $type ]}( $data ) : $data; } /** * Normalize a model/object to the exact array shape wp_json_encode outputs, * so stripping sees what would have been serialized. * * @param mixed $data The data to normalize. * * @return array|mixed */ private static function toDataArray( $data ) { if ( null === $data || is_scalar( $data ) ) { return $data; } return json_decode( wp_json_encode( $data ), true ); } }