true, 'skip_graphql_type_registry' => true, ] ) ); } /** * Registers data-loaders to be used when resolving WooCommerce-related GraphQL types * * @param array $loaders Assigned loaders. * * @return array */ public static function graphql_data_loader_classes( $loaders ) { // WooCommerce customer loader. $loaders['wc_customer'] = WC_Customer_Loader::class; // WooCommerce CPT loader. $loaders['wc_post'] = WC_CPT_Loader::class; // WooCommerce DB loaders. $loaders['cart_item'] = WC_Cart_Item_Loader::class; $loaders['downloadable_item'] = WC_Downloadable_Item_Loader::class; $loaders['tax_class'] = WC_Tax_Class_Loader::class; $loaders['tax_rate'] = WC_Tax_Rate_Loader::class; $loaders['order_item'] = WC_Order_Item_Loader::class; $loaders['shipping_method'] = WC_Shipping_Method_Loader::class; $loaders['shipping_zone'] = WC_Shipping_Zone_Loader::class; return $loaders; } /** * Inject Union types that resolve to Product with Product types * * @param array $config WPUnion config. * @param \WPGraphQL\Type\WPUnionType $wp_union WPUnion object. * * @return array */ public static function inject_union_types( $config, $wp_union ) { $refresh_callback = false; if ( in_array( 'Product', $config['typeNames'], true ) ) { // Strip 'Product' from config and child product types. $config['typeNames'] = array_merge( array_filter( $config['typeNames'], static function ( $type ) { return 'Product' !== $type; } ), array_values( WooGraphQL::get_enabled_product_types() ), [ WooGraphQL::get_supported_product_type() ] ); $refresh_callback = true; } // Update 'types' callback. if ( $refresh_callback ) { $config['types'] = static function () use ( $config, $wp_union ) { $prepared_types = []; foreach ( $config['typeNames'] as $type_name ) { $prepared_types[] = $wp_union->type_registry->get_type( $type_name ); } return $prepared_types; }; } return $config; } /** * Inject Union type resolver that resolve to Product with Product types * * @param \WPGraphQL\Type\WPObjectType $type Type be resolve to. * @param mixed $value Object for which the type is being resolve config. * @param \WPGraphQL\Type\WPUnionType $wp_union WPUnion object. * * @return \WPGraphQL\Type\WPObjectType */ public static function inject_union_type_resolver( $type, $value, $wp_union ) { switch ( get_class( $value ) ) { case 'WPGraphQL\WooCommerce\Model\Product': case 'WPGraphQL\WooCommerce\Model\Coupon': case 'WPGraphQL\WooCommerce\Model\Order': $new_type = Factory::resolve_node_type( $type, $value ); if ( $new_type ) { $type = $wp_union->type_registry->get_type( $new_type ); } break; } return $type; } /** * Inject Union type resolver that resolve to Product with Product types * * @param \WPGraphQL\Type\WPObjectType|null $type Type be resolve to. * @param mixed $value Object for which the type is being resolve config. * * @throws \GraphQL\Error\UserError Invalid product type received. * * @return \WPGraphQL\Type\WPObjectType|null */ public static function inject_type_resolver( $type, $value ) { $type_registry = \WPGraphQL::get_type_registry(); switch ( $type ) { case 'Coupon': case 'Order': $new_type = Factory::resolve_node_type( $type, $value ); if ( $new_type ) { $type = $type_registry->get_type( $new_type ); } break; case 'ProductVariation': $type = self::resolve_product_variation_type( $value ); break; case 'Product': $type = self::resolve_product_type( $value ); }//end switch return $type; } /** * Resolves GraphQL type for provided product model. * * @param \WPGraphQL\WooCommerce\Model\Product|\WPGraphQL\WooCommerce\Model\Product_Variation $value Product model. * * @throws \GraphQL\Error\UserError Invalid product type requested. * * @return mixed */ public static function resolve_product_type( $value ) { $type_registry = \WPGraphQL::get_type_registry(); $possible_types = WooGraphQL::get_enabled_product_types(); if ( $value instanceof \WPGraphQL\Model\Post && ( 'product' === $value->post_type || 'product_variation' === $value->post_type ) ) { $product_model = \WPGraphQL::get_app_context() ->get_loader( 'wc_post' ) ->load( $value->ID ); } elseif ( $value instanceof \WPGraphQL\Model\Post && ( 'product' !== $value->post_type && 'product_variation' !== $value->post_type ) ) { throw new UserError( sprintf( /* translators: %s: Post type slug */ __( 'The "%s" post type is not a valid product type.', 'graphql-for-ecommerce' ), $value->post_type ) ); } else { $product_model = $value; } $product_type = $product_model->get_type(); if ( isset( $possible_types[ $product_type ] ) ) { return $type_registry->get_type( $possible_types[ $product_type ] ); } elseif ( $product_model instanceof \WPGraphQL\WooCommerce\Model\Product_Variation ) { return self::resolve_product_variation_type( $product_model ); } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) { $unsupported_type = WooGraphQL::get_supported_product_type(); return $type_registry->get_type( $unsupported_type ); } throw new UserError( sprintf( /* translators: %s: Product type */ __( 'The "%s" product type is not supported by the core GraphQL for eCommerce schema.', 'graphql-for-ecommerce' ), $product_model->type ) ); } /** * Resolves GraphQL type for provided product variation model. * * @param \WPGraphQL\WooCommerce\Model\Product_Variation $value Product model. * * @throws \GraphQL\Error\UserError Invalid product type requested. * * @return mixed */ public static function resolve_product_variation_type( $value ) { $type_registry = \WPGraphQL::get_type_registry(); $possible_types = WooGraphQL::get_enabled_product_variation_types(); // Normally $value is a Product_Variation model. In some setups the // variation node is loaded through the generic post loader (e.g. a cart // item's variation under Polylang, which doesn't manage the // product_variation post-type) and arrives as a base // \WPGraphQL\Model\Post with no get_type(). Fall back to resolving the // variation's product type from its ID. if ( is_callable( [ $value, 'get_type' ] ) ) { $product_type = $value->get_type(); } else { $variation_id = $value->databaseId ?? ( $value->ID ?? 0 ); $product = $variation_id ? wc_get_product( $variation_id ) : false; $product_type = $product ? $product->get_type() : null; } if ( $product_type && isset( $possible_types[ $product_type ] ) ) { return $type_registry->get_type( $possible_types[ $product_type ] ); } throw new UserError( sprintf( /* translators: %s: Product type */ __( 'The "%s" product variation type is not supported by the core GraphQL for eCommerce schema.', 'graphql-for-ecommerce' ), $product_type ?? '' ) ); } /** * Filter to make order notes visible in GraphQL queries for authorized users. * * @param bool $is_private Whether the data is private. * @param string $model_name The name of the model being checked. * @param mixed $data The data being checked. * * @return bool */ public static function make_order_notes_visible( $is_private, $model_name, $data ) { // Only apply to Comment models. if ( 'CommentObject' !== $model_name ) { return $is_private; } // Check if this is an order note. if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) { // Get the parent order. $order_id = absint( $data->comment_post_ID ); $order = wc_get_order( $order_id ); if ( ! $order ) { return true; // Keep it private if order not found. } // Allow shop managers and admins to see all order notes. if ( current_user_can( 'edit_shop_orders' ) ) { return false; // Not private. } // Allow customers to see customer notes on their own orders. $comment_id = absint( $data->comment_ID ); $is_customer_note = get_comment_meta( $comment_id, 'is_customer_note', true ); if ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) { if ( get_current_user_id() === $order->get_customer_id() ) { return false; // Not private. } } elseif ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) { /** @var \WC_Order|false $parent */ $parent = wc_get_order( $order->get_parent_id() ); if ( $parent && get_current_user_id() === $parent->get_customer_id() ) { return false; // Not private. } } // Otherwise keep it private. return true; } return $is_private; } /** * Filter to set order notes visibility to public for authorized users. * * @param string $visibility The visibility of the object. * @param string $model_name The name of the model being checked. * @param mixed $data The data being checked. * @param int|null $owner The owner of the object. * @param \WP_User $current_user The current user. * * @return string */ public static function set_order_notes_visibility( $visibility, $model_name, $data, $owner, $current_user ) { // Only apply to Comment models. if ( 'CommentObject' !== $model_name ) { return $visibility; } // Check if this is an order note and if user owns the order. if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) { $order = wc_get_order( $data->comment_post_ID ); // If user is the order owner, make it public. if ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) { return get_current_user_id() === $order->get_customer_id() ? 'public' : $visibility; } elseif ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) { /** @var \WC_Order|false $parent */ $parent = wc_get_order( $order->get_parent_id() ); return $parent && get_current_user_id() === $parent->get_customer_id() ? 'public' : $visibility; } } return $visibility; } }