
<center><h2><strong>Ubuntu</strong></h2>
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
<!DOCTYPE html>
<html>
<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingRateQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingTimeQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductRepository;
use WC_Product;
use WP_REST_Response;
use WP_REST_Request;

defined( 'ABSPATH' ) || exit;

/**
 * Class WPCOMProxy
 *
 * Prepares and filters data pulled by WPCOM proxy based on the `gla_syncable` query parameter.
 *
 * The `gla_syncable` parameter indicates that the request originates from the WPCOM proxy.
 * It's not intended to provide security, as it does not expose any data
 * that should be hidden from REST API users.
 *
 * Its primary purpose is to prevent global endpoints from being cluttered with additional data
 * and to conceal undocumented implementation details of the integration between the G4W plugin and WPCOM proxy.
 *
 * ContainerAware used to access:
 * - AttributeManager
 * - MerchantCenterService
 * - ProductRepository
 * - ShippingRateQuery
 * - ShippingTimeQuery
 *
 * @since 2.8.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class WPCOMProxy implements Service, Registerable, ContainerAwareInterface, OptionsAwareInterface {

	use ContainerAwareTrait;
	use OptionsAwareTrait;
	use PluginHelper;

	/**
	 * The protected resources. Only items with visibility set to sync-and-show will be returned.
	 */
	protected const PROTECTED_RESOURCES = [
		'products',
		'coupons',
	];

	/**
	 * The settings group for the REST API.
	 *
	 * @var string
	 */
	protected const SETTINGS_GROUP = 'google-for-woocommerce';

	/**
	 * The meta key used to filter the items.
	 *
	 * @var string
	 */
	public const KEY_VISIBILITY = '_wc_gla_visibility';

	/**
	 * Get the post types to be filtered.
	 *
	 * @return array
	 */
	private function get_post_types_to_filter() {
		/** @var ProductRepository $product_repository */
		$product_repository = $this->container->get( ProductRepository::class );

		return [
			'product'           => [
				'meta_query' => $product_repository->get_sync_ready_products_meta_query( true ),
			],
			'shop_coupon'       => [
				'meta_query' => [
					[
						'key'     => self::KEY_VISIBILITY,
						'value'   => ChannelVisibility::SYNC_AND_SHOW,
						'compare' => '=',
					],
					[
						'key'     => 'customer_email',
						'compare' => 'NOT EXISTS',
					],
				],
			],
			'product_variation' => [
				'meta_query' => null,
			],
		];
	}

	/**
	 * Register all filters.
	 */
	public function register(): void {
		// Allow to filter by gla_syncable.
		add_filter(
			'woocommerce_rest_query_vars',
			function ( $valid_vars ) {
				$valid_vars[] = 'gla_syncable';
				return $valid_vars;
			}
		);

		$this->register_callbacks();
		$this->add_g4w_settings();

		foreach ( array_keys( $this->get_post_types_to_filter() ) as $object_type ) {
			$this->register_object_types_filter( $object_type );
		}
	}

	/**
	 * Register the filters for a specific object type.
	 *
	 * @param string $object_type The object type.
	 */
	protected function register_object_types_filter( string $object_type ): void {
		add_filter(
			'woocommerce_rest_prepare_' . $object_type . '_object',
			[ $this, 'filter_response_by_syncable_item' ],
			PHP_INT_MAX, // Run this filter last to override any other response.
			3
		);

		add_filter(
			'woocommerce_rest_prepare_' . $object_type . '_object',
			[ $this, 'prepare_response' ],
			PHP_INT_MAX - 1,
			3
		);

		add_filter(
			'woocommerce_rest_' . $object_type . '_object_query',
			[ $this, 'filter_by_metaquery' ],
			10,
			2
		);
	}

	/**
	 * Register the `rest_request_after_callbacks`, to prepare shipping methods.
	 */
	protected function register_callbacks() {
		add_filter(
			'rest_request_after_callbacks',
			/**
			 * Set data for settings & prepare data for shipping methods.
			 *
			 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response The response object.
			 * @param mixed                                             $handler  The handler.
			 * @param WP_REST_Request                                   $request  The request object.
			 */
			function ( $response, $handler, $request ) {
				if ( ! $this->is_gla_request( $request ) || ! $response instanceof WP_REST_Response ) {
					return $response;
				}

				if ( $request->get_route() === '/wc/v3/settings/' . self::SETTINGS_GROUP ) {

					/** @var MerchantCenterService $merchant_center */
					$merchant_center_service = $this->container->get( MerchantCenterService::class );
					/** @var ShippingRateQuery $shipping_rate_query */
					$shipping_rate_query = $this->container->get( ShippingRateQuery::class );
					/** @var ShippingTimeQuery $shipping_time_query */
					$shipping_time_query = $this->container->get( ShippingTimeQuery::class );

					$merchant_center = $merchant_center_service->is_connected()
						? $this->options->get( OptionsInterface::MERCHANT_CENTER, null )
						: null;

					$data = $response->get_data();

					$data[] = [
						'id'    => 'gla_plugin_version',
						'label' => 'Google for WooCommerce: Current plugin version',
						'value' => $this->get_version(),
					];
					$data[] = [
						'id'    => 'gla_google_connected',
						'label' => 'Google for WooCommerce: Is Google account connected?',
						'value' => $merchant_center_service->is_google_connected(),
					];
					$data[] = [
						'id'    => 'gla_language',
						'label' => 'Google for WooCommerce: Store language',
						'value' => get_locale(),
					];
					$data[] = [
						'id'    => 'gla_merchant_center',
						'label' => 'Google for WooCommerce: Merchant Center settings',
						'value' => $merchant_center,
					];
					$data[] = [
						'id'    => 'gla_shipping_rates',
						'label' => 'Google for WooCommerce: Shipping Rates',
						'value' => (object) $shipping_rate_query->get_all_shipping_rates(),
					];
					$data[] = [
						'id'    => 'gla_shipping_times',
						'label' => 'Google for WooCommerce: Shipping Times',
						'value' => (object) $shipping_time_query->get_all_shipping_times(),
					];
					$data[] = [
						'id'    => 'gla_target_audience',
						'label' => 'Google for WooCommerce: Target Audience',
						'value' => $this->options->get( OptionsInterface::TARGET_AUDIENCE, null ),
					];

					$response->set_data( array_values( $data ) );
				}

				$response->set_data( $this->prepare_data( $response->get_data(), $request ) );
				return $response;
			},
			10,
			3
		);
	}

	/**
	 * Add the Google for WooCommerce settings to the WooCommerce REST API.
	 *
	 * @return void
	 */
	protected function add_g4w_settings() {
		add_filter(
			'woocommerce_settings_groups',
			function ( $locations ) {
				$locations[] = [
					'id'          => self::SETTINGS_GROUP,
					'label'       => 'Google for WooCommerce',
					'description' => 'Settings of the Google for WooCommerce plugin.',
				];
				return $locations;
			}
		);

		// Hack to make the settings group show up in the response.
		add_filter(
			'woocommerce_settings-' . self::SETTINGS_GROUP,
			function ( $data ) {
				/*
				 * We need to add non-empty return value in the filter, to be able to pass the valid group check.
				 * We provide invalid 'type', so the entry will be ignored
				 * in the response by `WC_REST_Setting_Options_V2_Controller::get_items`
				 */
				$data[] = [
					'id'         => 'gla_settings_placeholder',
					'option_key' => 'gla_settings_placeholder',
					'type'       => '_invalid_type_',
				];

				/*
				 * This way `rest_request_after_callbacks` will get an empty data set
				 * and could provide complete option- and non-option related settings.
				 */
				return $data;
			}
		);
	}

	/**
	 * Prepares the data converting the empty arrays in objects for consistency.
	 *
	 * @param array           $data The response data to parse
	 * @param WP_REST_Request $request The request object.
	 * @return mixed
	 */
	public function prepare_data( $data, $request ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		if ( preg_match( '/^\/wc\/v3\/shipping\/zones\/\d+\/methods/', $request->get_route() ) ) {
			foreach ( $data as $key => $value ) {
				if ( isset( $value['settings'] ) && empty( $value['settings'] ) ) {
					$data[ $key ]['settings'] = (object) $value['settings'];
				}
			}
		}

		return $data;
	}

	/**
	 * Whether the request is coming from the WPCOM proxy.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return bool
	 */
	protected function is_gla_request( WP_REST_Request $request ): bool {
		// WPCOM proxy will set the gla_syncable to 1 if the request is coming from the proxy and it is the Google App.
		return $request->get_param( 'gla_syncable' ) === '1';
	}

	/**
	 * Get route pieces: resource and id, if present.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return array The route pieces.
	 */
	protected function get_route_pieces( WP_REST_Request $request ): array {
		$route   = $request->get_route();
		$pattern = '/(?P<resource>[\w]+)(?:\/(?P<id>[\d]+))?$/';
		preg_match( $pattern, $route, $matches );

		return $matches;
	}

	/**
	 * Filter response by syncable item.
	 *
	 * @param WP_REST_Response $response The response object.
	 * @param mixed            $item     The item.
	 * @param WP_REST_Request  $request  The request object.
	 *
	 * @return WP_REST_Response The response object updated.
	 */
	public function filter_response_by_syncable_item( $response, $item, WP_REST_Request $request ): WP_REST_Response {
		if ( ! $this->is_gla_request( $request ) ) {
			return $response;
		}

		$pieces = $this->get_route_pieces( $request );

		if ( ! isset( $pieces['id'] ) || ! isset( $pieces['resource'] ) || ! in_array( $pieces['resource'], self::PROTECTED_RESOURCES, true ) ) {
			return $response;
		}

		// Product is opt-out but coupon is opt-in
		$is_syncable = $pieces['resource'] === 'products';
		$meta_data   = $response->get_data()['meta_data'] ?? [];

		foreach ( $meta_data as $meta ) {
			if ( $meta->key === self::KEY_VISIBILITY ) {
				$is_syncable = $meta->value === ChannelVisibility::SYNC_AND_SHOW;
				break;
			}
		}

		if ( $is_syncable ) {
			return $response;
		}

		return new WP_REST_Response(
			[
				'code'    => 'gla_rest_item_no_syncable',
				'message' => 'Item not syncable',
				'data'    => [
					'status' => '403',
				],
			],
			403
		);
	}

	/**
	 * Query items with specific args for example where _wc_gla_visibility is set to sync-and-show.
	 *
	 * @param array           $args    The query args.
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return array The query args updated.
	 * */
	public function filter_by_metaquery( array $args, WP_REST_Request $request ): array {
		if ( ! $this->is_gla_request( $request ) ) {
			return $args;
		}

		$post_type         = $args['post_type'];
		$post_type_filters = $this->get_post_types_to_filter()[ $post_type ];

		if ( ! isset( $post_type_filters['meta_query'] ) || ! is_array( $post_type_filters['meta_query'] ) ) {
			return $args;
		}

		$meta_query = $post_type_filters['meta_query'];

		if ( empty( $args['meta_query'] ) ) {
			$args['meta_query'] = $meta_query;
		} else {
			array_push( $args['meta_query'], $meta_query );
		}

		return $args;
	}

	/**
	 * Prepares the response when the request is coming from the WPCOM proxy:
	 *
	 * Filter all the private metadata and returns only the public metadata and those prefixed with _wc_gla
	 * For WooCommerce products, it will add the attribute mapping values.
	 *
	 * @param WP_REST_Response $response The response object.
	 * @param mixed            $item     The item.
	 * @param WP_REST_Request  $request  The request object.
	 *
	 * @return WP_REST_Response The response object updated.
	 */
	public function prepare_response( WP_REST_Response $response, $item, WP_REST_Request $request ): WP_REST_Response {
		if ( ! $this->is_gla_request( $request ) ) {
			return $response;
		}

		$data     = $response->get_data();
		$resource = $this->get_route_pieces( $request )['resource'] ?? null;

		if ( $item instanceof WC_Product && ( $resource === 'products' || $resource === 'variations' ) ) {
			/** @var AttributeManager $attribute_manager */
			$attribute_manager = $this->container->get( AttributeManager::class );

			$attr = $attribute_manager->get_all_aggregated_values( $item );
			// In case of empty array, convert to object to keep the response consistent.
			$data['gla_attributes'] = (object) $attr;

			// Force types and prevent user type change for fields as Google has strict type requirements.
			$data['price']         = strval( $data['price'] ?? null );
			$data['regular_price'] = strval( $data['regular_price'] ?? null );
			$data['sale_price']    = strval( $data['sale_price'] ?? null );
		}

		foreach ( $data['meta_data'] ?? [] as $key => $meta ) {
			if ( str_starts_with( $meta->key, '_' ) && ! str_starts_with( $meta->key, '_wc_gla' ) ) {
				unset( $data['meta_data'][ $key ] );
			}
		}

		$data['meta_data'] = array_values( $data['meta_data'] );

		$response->set_data( $data );

		return $response;
	}
}
