HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Integration.tar
IntegrationInitializer.php000064400000002305151542332670011754 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ValidateInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;

defined( 'ABSPATH' ) || exit;

/**
 * Class IntegrationInitializer
 *
 * Initializes all active integrations.
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class IntegrationInitializer implements Service, Registerable {

	use ValidateInterface;

	/**
	 * @var IntegrationInterface[]
	 */
	protected $integrations = [];

	/**
	 * IntegrationInitializer constructor.
	 *
	 * @param IntegrationInterface[] $integrations
	 */
	public function __construct( array $integrations ) {
		foreach ( $integrations as $integration ) {
			$this->validate_instanceof( $integration, IntegrationInterface::class );
			$this->integrations[] = $integration;
		}
	}

	/**
	 * Initialize all active integrations.
	 */
	public function register(): void {
		foreach ( $this->integrations as $integration ) {
			if ( $integration->is_active() ) {
				$integration->init();
			}
		}
	}
}
IntegrationInterface.php000064400000001052151542332670011367 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

defined( 'ABSPATH' ) || exit;

/**
 * Interface IntegrationInterface
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
interface IntegrationInterface {
	/**
	 * Returns whether the integration is active or not.
	 *
	 * @return bool
	 */
	public function is_active(): bool;

	/**
	 * Initializes the integration (e.g. by registering the required hooks, filters, etc.).
	 *
	 * @return void
	 */
	public function init(): void;
}
JetpackWPCOM.php000064400000031745151542332670007466 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Conditional;
use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Nonce_Handler;
use Jetpack_Signature;
use Jetpack_Options;

defined( 'ABSPATH' ) || exit;

/**
 * Class JetpackWPCOM
 *
 * Initializes the Jetpack function required to connect the WPCOM App.
 * This class can be deleted when the jetpack-connection package includes these functions.
 *
 * The majority of these class methods have been copied from the Jetpack class.
 *
 * @see https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/class.jetpack.php
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class JetpackWPCOM implements Service, Registerable, Conditional {

	/**
	 * Verified data for JSON authorization request
	 *
	 * @var array
	 */
	public $json_api_authorization_request = [];

	/**
	 * Connection manager.
	 *
	 * @var Automattic\Jetpack\Connection\Manager
	 */
	protected $connection_manager;

	/**
	 * Initialize all active integrations.
	 */
	public function register(): void {
		add_action( 'login_form_jetpack_json_api_authorization', [ $this, 'login_form_json_api_authorization' ] );

		// This filter only simulates the Jetpack version for the test connection response, and it can be any value greater than 1.2.3.
		add_filter(
			'jetpack_xmlrpc_test_connection_response',
			function () {
				return '9.5';
			}
		);
	}

	/**
	 * Check if this class is required based on the presence of the Jetpack class.
	 *
	 * @return bool Whether the class is needed.
	 */
	public static function is_needed(): bool {
		return ! class_exists( 'Jetpack' );
	}

	/**
	 * Handles the login action for Authorizing the JSON API
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5301
	 */
	public function login_form_json_api_authorization() {
		$this->verify_json_api_authorization_request();
		add_action( 'wp_login', [ $this, 'store_json_api_authorization_token' ], 10, 2 );
		add_action( 'login_message', [ $this, 'login_message_json_api_authorization' ] );
		add_action( 'login_form', [ $this, 'preserve_action_in_login_form_for_json_api_authorization' ] );
		add_filter( 'site_url', [ $this, 'post_login_form_to_signed_url' ], 10, 3 );
	}

	/**
	 * If someone logs in to approve API access, store the Access Code in usermeta.
	 *
	 * @param string  $user_login Unused.
	 * @param WP_User $user User logged in.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5349
	 */
	public function store_json_api_authorization_token( $user_login, $user ) {
		add_filter( 'login_redirect', [ $this, 'add_token_to_login_redirect_json_api_authorization' ], 10, 3 );
		add_filter( 'allowed_redirect_hosts', [ $this, 'allow_wpcom_public_api_domain' ] );
		$token = wp_generate_password( 32, false );
		update_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], $token );
	}

	/**
	 * Make sure the POSTed request is handled by the same action.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5336
	 */
	public function preserve_action_in_login_form_for_json_api_authorization() {
		$http_host   = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- escaped with esc_url below.
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- escaped with esc_url below.
		echo "<input type='hidden' name='action' value='jetpack_json_api_authorization' />\n";
		echo "<input type='hidden' name='jetpack_json_api_original_query' value='" . esc_url( set_url_scheme( $http_host . $request_uri ) ) . "' />\n";
	}

	/**
	 * Make sure the login form is POSTed to the signed URL so we can reverify the request.
	 *
	 * @param string $url Redirect URL.
	 * @param string $path Path.
	 * @param string $scheme URL Scheme.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/class.jetpack.php#L5318
	 */
	public function post_login_form_to_signed_url( $url, $path, $scheme ) {
		if ( 'wp-login.php' !== $path || ( 'login_post' !== $scheme && 'login' !== $scheme ) ) {
			return $url;
		}
		$query_string = isset( $_SERVER['QUERY_STRING'] ) ? wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$parsed_url   = wp_parse_url( $url );
		$url          = strtok( $url, '?' );
		$url          = "$url?{$query_string}";
		if ( ! empty( $parsed_url['query'] ) ) {
			$url .= "&{$parsed_url['query']}";
		}

		return $url;
	}

	/**
	 * Add the Access Code details to the public-api.wordpress.com redirect.
	 *
	 * @param string  $redirect_to URL.
	 * @param string  $original_redirect_to URL.
	 * @param WP_User $user WP_User for the redirect.
	 *
	 * @return string
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5401
	 */
	public function add_token_to_login_redirect_json_api_authorization( $redirect_to, $original_redirect_to, $user ) {
		return add_query_arg(
			urlencode_deep(
				[
					'jetpack-code'    => get_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], true ),
					'jetpack-user-id' => (int) $user->ID,
					'jetpack-state'   => $this->json_api_authorization_request['state'],
				]
			),
			$redirect_to
		);
	}

	/**
	 * Add public-api.wordpress.com to the safe redirect allowed list - only added when someone allows API access.
	 * To be used with a filter of allowed domains for a redirect.
	 *
	 * @param array $domains Allowed WP.com Environments.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5363
	 */
	public function allow_wpcom_public_api_domain( $domains ) {
		$domains[] = 'public-api.wordpress.com';
		return $domains;
	}

	/**
	 * Check if the redirect is encoded.
	 *
	 * @param string $redirect_url Redirect URL.
	 *
	 * @return bool If redirect has been encoded.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5375
	 */
	public static function is_redirect_encoded( $redirect_url ) {
		return preg_match( '/https?%3A%2F%2F/i', $redirect_url ) > 0;
	}

	/**
	 * HTML for the JSON API authorization notice.
	 *
	 * @return string
	 *
	 * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5603
	 */
	public function login_message_json_api_authorization() {
		return '<p class="message">' . sprintf(
			/* translators: Name/image of the client requesting authorization */
			esc_html__( '%s wants to access your site’s data. Log in to authorize that access.', 'google-listings-and-ads' ),
			'<strong>' . esc_html( $this->json_api_authorization_request['client_title'] ) . '</strong>'
		) . '<img src="' . esc_url( $this->json_api_authorization_request['client_image'] ) . '" /></p>';
	}

	/**
	 * Verifies the request by checking the signature
	 *
	 * @param null|array $environment Value to override $_REQUEST.
	 *
	 * @see https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/class.jetpack.php#L5422
	 */
	public function verify_json_api_authorization_request( $environment = null ) {
		$environment = $environment === null
			? $_REQUEST // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verification handled later in function.
			: $environment;

		list( $env_token,, $env_user_id ) = explode( ':', $environment['token'] );
		$token                            = ( new Tokens() )->get_access_token( $env_user_id, $env_token );
		if ( ! $token || empty( $token->secret ) ) {
			wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'google-listings-and-ads' ) );
		}

		$die_error = __( 'Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :).  Either way, please close this window.', 'google-listings-and-ads' );

		// Host has encoded the request URL, probably as a result of a bad http => https redirect.
		if ( self::is_redirect_encoded( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no site changes, we're erroring out.
			/**
			 * Jetpack authorisation request Error.
			 */
			do_action( 'jetpack_verify_api_authorization_request_error_double_encode' );
			$die_error = sprintf(
				/* translators: %s is a URL */
				__( 'Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our <a href="%s">support page</a> for details about how to resolve this.', 'google-listings-and-ads' ),
				esc_url( 'https://jetpack.com/support/double-encoding/' )
			);
		}

		$jetpack_signature = new Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) );

		if ( isset( $environment['jetpack_json_api_original_query'] ) ) {
			$signature = $jetpack_signature->sign_request(
				$environment['token'],
				$environment['timestamp'],
				$environment['nonce'],
				'',
				'GET',
				$environment['jetpack_json_api_original_query'],
				null,
				true
			);
		} else {
			$signature = $jetpack_signature->sign_current_request(
				[
					'body'   => null,
					'method' => 'GET',
				]
			);
		}

		if ( ! $signature ) {
			wp_die(
				wp_kses(
					$die_error,
					[
						'a' => [
							'href' => [],
						],
					]
				)
			);
		} elseif ( is_wp_error( $signature ) ) {
			wp_die(
				wp_kses(
					$die_error,
					[
						'a' => [
							'href' => [],
						],
					]
				)
			);
		} elseif ( ! hash_equals( $signature, $environment['signature'] ) ) {
			if ( is_ssl() ) {
				// If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well.
				$signature = $jetpack_signature->sign_current_request(
					[
						'scheme' => 'http',
						'body'   => null,
						'method' => 'GET',
					]
				);
				if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) {
					wp_die(
						wp_kses(
							$die_error,
							[
								'a' => [
									'href' => [],
								],
							]
						)
					);
				}
			} else {
				wp_die(
					wp_kses(
						$die_error,
						[
							'a' => [
								'href' => [],
							],
						]
					)
				);
			}
		}

		$timestamp = (int) $environment['timestamp'];
		$nonce     = stripslashes( (string) $environment['nonce'] );

		if ( ! $this->connection_manager ) {
			$this->connection_manager = new Connection_Manager();
		}

		if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) {
			// De-nonce the nonce, at least for 5 minutes.
			// We have to reuse this nonce at least once (used the first time when the initial request is made, used a second time when the login form is POSTed).
			$old_nonce_time = get_option( "jetpack_nonce_{$timestamp}_{$nonce}" );
			if ( $old_nonce_time < time() - 300 ) {
				wp_die( esc_html__( 'The authorization process expired. Please go back and try again.', 'google-listings-and-ads' ) );
			}
		}

		$data         = json_decode( base64_decode( stripslashes( $environment['data'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$data_filters = [
			'state'        => 'opaque',
			'client_id'    => 'int',
			'client_title' => 'string',
			'client_image' => 'url',
		];

		foreach ( $data_filters as $key => $sanitation ) {
			if ( ! isset( $data->$key ) ) {
				wp_die(
					wp_kses(
						$die_error,
						[
							'a' => [
								'href' => [],
							],
						]
					)
				);
			}

			switch ( $sanitation ) {
				case 'int':
					$this->json_api_authorization_request[ $key ] = (int) $data->$key;
					break;
				case 'opaque':
					$this->json_api_authorization_request[ $key ] = (string) $data->$key;
					break;
				case 'string':
					$this->json_api_authorization_request[ $key ] = wp_kses( (string) $data->$key, [] );
					break;
				case 'url':
					$this->json_api_authorization_request[ $key ] = esc_url_raw( (string) $data->$key );
					break;
			}
		}

		if ( empty( $this->json_api_authorization_request['client_id'] ) ) {
			wp_die(
				wp_kses(
					$die_error,
					[
						'a' => [
							'href' => [],
						],
					]
				)
			);
		}
	}
}
WPCOMProxy.php000064400000024123151542332670007216 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\ShippingTimeQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use WC_Product;
use WP_REST_Response;
use WP_REST_Request;

defined( 'ABSPATH' ) || exit;

/**
 * Class WPCOMProxy
 *
 * Initializes the hooks to filter the data sent to the WPCOM proxy depending on the query parameter gla_syncable.
 *
 * @since 2.8.0
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class WPCOMProxy implements Service, Registerable, OptionsAwareInterface {

	use OptionsAwareTrait;

	/**
	 * The ShippingTimeQuery object.
	 *
	 * @var ShippingTimeQuery
	 */
	protected $shipping_time_query;

	/**
	 * The AttributeManager object.
	 *
	 * @var AttributeManager
	 */
	protected $attribute_manager;

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

	/**
	 * WPCOMProxy constructor.
	 *
	 * @param ShippingTimeQuery $shipping_time_query The ShippingTimeQuery object.
	 * @param AttributeManager  $attribute_manager   The AttributeManager object.
	 */
	public function __construct( ShippingTimeQuery $shipping_time_query, AttributeManager $attribute_manager ) {
		$this->shipping_time_query = $shipping_time_query;
		$this->attribute_manager   = $attribute_manager;
	}

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

	/**
	 * The Post types to be filtered.
	 *
	 * @var array
	 */
	public static $post_types_to_filter = [
		'product'           => [
			'meta_query' => [
				[
					'key'     => self::KEY_VISIBILITY,
					'value'   => ChannelVisibility::SYNC_AND_SHOW,
					'compare' => '=',
				],
			],
		],
		'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();

		foreach ( array_keys( self::$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 callbacks.
	 */
	protected function register_callbacks() {
		add_filter(
			'rest_request_after_callbacks',
			/**
			 * Add the Google for WooCommerce and Ads settings to the settings/general response.
			 *
			 * @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;
				}

				$data = $response->get_data();

				if ( $request->get_route() === '/wc/v3/settings/general' ) {
					$data[] = [
						'id'    => 'gla_target_audience',
						'label' => 'Google for WooCommerce: Target Audience',
						'value' => $this->options->get( OptionsInterface::TARGET_AUDIENCE, [] ),
					];

					$data[] = [
						'id'    => 'gla_shipping_times',
						'label' => 'Google for WooCommerce: Shipping Times',
						'value' => $this->shipping_time_query->get_all_shipping_times(),
					];

					$data[] = [
						'id'    => 'gla_language',
						'label' => 'Google for WooCommerce: Store language',
						'value' => get_locale(),
					];

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

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

	/**
	 * 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;
		}

		foreach ( $data as $key => $value ) {
			if ( preg_match( '/^\/wc\/v3\/shipping\/zones\/\d+\/methods/', $request->get_route() ) && 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;
		}

		$meta_data = $response->get_data()['meta_data'] ?? [];

		foreach ( $meta_data as $meta ) {
			if ( $meta->key === self::KEY_VISIBILITY && $meta->value === ChannelVisibility::SYNC_AND_SHOW ) {
				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 = self::$post_types_to_filter[ $post_type ];

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

		$args['meta_query'] = [ ...$args['meta_query'] ?? [], ...$post_type_filters['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' ) ) {
			$attr = $this->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;
	}
}
WooCommerceBrands.php000064400000004740151542332670010643 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use WC_Product;
use WC_Product_Variation;
use WP_Term;

defined( 'ABSPATH' ) || exit;

/**
 * Class WooCommerceBrands
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class WooCommerceBrands implements IntegrationInterface {

	protected const VALUE_KEY = 'woocommerce_brands';

	/**
	 * The WP proxy object.
	 *
	 * @var WP
	 */
	protected $wp;

	/**
	 * WooCommerceBrands constructor.
	 *
	 * @param WP $wp
	 */
	public function __construct( WP $wp ) {
		$this->wp = $wp;
	}

	/**
	 * Returns whether the integration is active or not.
	 *
	 * @return bool
	 */
	public function is_active(): bool {
		return defined( 'WC_BRANDS_VERSION' );
	}

	/**
	 * Initializes the integration (e.g. by registering the required hooks, filters, etc.).
	 *
	 * @return void
	 */
	public function init(): void {
		add_filter(
			'woocommerce_gla_product_attribute_value_options_brand',
			function ( array $value_options ) {
				return $this->add_value_option( $value_options );
			}
		);
		add_filter(
			'woocommerce_gla_product_attribute_value_brand',
			function ( $value, WC_Product $product ) {
				return $this->get_brand( $value, $product );
			},
			10,
			2
		);
	}

	/**
	 * @param array $value_options
	 *
	 * @return array
	 */
	protected function add_value_option( array $value_options ): array {
		$value_options[ self::VALUE_KEY ] = 'From WooCommerce Brands';

		return $value_options;
	}

	/**
	 * @param mixed      $value
	 * @param WC_Product $product
	 *
	 * @return mixed
	 */
	protected function get_brand( $value, WC_Product $product ) {
		if ( self::VALUE_KEY === $value ) {
			$product_id = $product instanceof WC_Product_Variation ? $product->get_parent_id() : $product->get_id();

			$terms = $this->wp->get_the_terms( $product_id, 'product_brand' );
			if ( is_array( $terms ) ) {
				return $this->get_brand_from_terms( $terms );
			}
		}

		return self::VALUE_KEY === $value ? null : $value;
	}

	/**
	 * Returns the brand from the given taxonomy terms.
	 *
	 * If multiple, it returns the first selected brand as primary brand
	 *
	 * @param WP_Term[] $terms
	 *
	 * @return string
	 */
	protected function get_brand_from_terms( array $terms ): string {
		$brands = [];
		foreach ( $terms as $term ) {
			$brands[] = $term->name;
			if ( empty( $term->parent ) ) {
				return $term->name;
			}
		}

		return $brands[0];
	}
}
WooCommercePreOrders.php000064400000007010151542332670011330 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\ProductHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\WCProductAdapter;
use DateTimeZone;
use Exception;
use WC_DateTime;
use WC_Pre_Orders_Product;
use WC_Product;

defined( 'ABSPATH' ) || exit;

/**
 * Class WooCommercePreOrders
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 *
 * @link https://woocommerce.com/products/woocommerce-pre-orders/
 *
 * @since 1.5.0
 */
class WooCommercePreOrders implements IntegrationInterface {

	/**
	 * @var ProductHelper
	 */
	protected $product_helper;

	/**
	 * WooCommercePreOrders constructor.
	 *
	 * @param ProductHelper $product_helper
	 */
	public function __construct( ProductHelper $product_helper ) {
		$this->product_helper = $product_helper;
	}

	/**
	 * Returns whether the integration is active or not.
	 *
	 * @return bool
	 */
	public function is_active(): bool {
		return defined( 'WC_PRE_ORDERS_VERSION' );
	}

	/**
	 * Initializes the integration (e.g. by registering the required hooks, filters, etc.).
	 *
	 * @return void
	 */
	public function init(): void {
		add_filter(
			'woocommerce_gla_product_attribute_values',
			function ( array $attributes, WC_Product $product ) {
				return $this->maybe_set_preorder_availability( $attributes, $product );
			},
			2,
			10
		);

		add_action(
			'wc_pre_orders_pre_orders_disabled_for_product',
			function ( $product_id ) {
				$this->trigger_sync( $product_id );
			},
		);
	}

	/**
	 * Sets the product's availability to "preorder" if it's in-stock and can be pre-ordered.
	 *
	 * @param array      $attributes
	 * @param WC_Product $product
	 *
	 * @return array
	 */
	protected function maybe_set_preorder_availability( array $attributes, WC_Product $product ): array {
		if ( $product->is_in_stock() && WC_Pre_Orders_Product::product_can_be_pre_ordered( $product ) ) {
			$attributes['availability'] = WCProductAdapter::AVAILABILITY_PREORDER;

			$availability_date = $this->get_availability_datetime( $product );
			if ( ! empty( $availability_date ) ) {
				$attributes['availabilityDate'] = (string) $availability_date;
			}
		}

		return $attributes;
	}

	/**
	 * @param WC_Product $product
	 *
	 * @return WC_DateTime|null
	 */
	protected function get_availability_datetime( WC_Product $product ): ?WC_DateTime {
		$product   = $this->product_helper->maybe_swap_for_parent( $product );
		$timestamp = $product->get_meta( '_wc_pre_orders_availability_datetime', true );

		if ( empty( $timestamp ) ) {
			return null;
		}

		try {
			return new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
		} catch ( Exception $e ) {
			do_action( 'woocommerce_gla_exception', $e, __METHOD__ );

			return null;
		}
	}

	/**
	 * Triggers an update job for the product to be synced with Merchant Center.
	 *
	 * This is required because WooCommerce Pre-orders updates the product's metadata via `update_post_meta`, which
	 * does not automatically trigger a sync.
	 *
	 * @hooked wc_pre_orders_pre_orders_disabled_for_product
	 *
	 * @param mixed $product_id
	 */
	protected function trigger_sync( $product_id ): void {
		try {
			$product = $this->product_helper->get_wc_product( (int) $product_id );
		} catch ( InvalidValue $e ) {
			do_action( 'woocommerce_gla_exception', $e, __METHOD__ );

			return;
		}

		// Manually trigger an update job by saving the product object.
		$product->save();
	}
}
WooCommerceProductBundles.php000064400000014637151542332670012375 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\AttributeManager;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\IsBundle;
use WC_Product;
use WC_Product_Bundle;

defined( 'ABSPATH' ) || exit;

/**
 * Class WooCommerceProductBundles
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class WooCommerceProductBundles implements IntegrationInterface {

	/**
	 * @var AttributeManager
	 */
	protected $attribute_manager;

	/**
	 * WooCommerceProductBundles constructor.
	 *
	 * @param AttributeManager $attribute_manager
	 */
	public function __construct( AttributeManager $attribute_manager ) {
		$this->attribute_manager = $attribute_manager;
	}

	/**
	 * Returns whether the integration is active or not.
	 *
	 * @return bool
	 */
	public function is_active(): bool {
		return class_exists( 'WC_Bundles' ) && is_callable( 'WC_Bundles::instance' );
	}

	/**
	 * Initializes the integration (e.g. by registering the required hooks, filters, etc.).
	 *
	 * @return void
	 */
	public function init(): void {
		$this->init_product_types();

		// update the isBundle attribute for bundle products
		add_action(
			'woocommerce_new_product',
			function ( int $product_id, WC_Product $product ) {
				$this->handle_update_product( $product );
			},
			10,
			2
		);
		add_action(
			'woocommerce_update_product',
			function ( int $product_id, WC_Product $product ) {
				$this->handle_update_product( $product );
			},
			10,
			2
		);

		// recalculate the product price for bundles
		add_filter(
			'woocommerce_gla_product_attribute_value_price',
			function ( float $price, WC_Product $product, bool $tax_excluded ) {
				return $this->calculate_price( $price, $product, $tax_excluded );
			},
			10,
			3
		);
		add_filter(
			'woocommerce_gla_product_attribute_value_sale_price',
			function ( float $sale_price, WC_Product $product, bool $tax_excluded ) {
				return $this->calculate_sale_price( $sale_price, $product, $tax_excluded );
			},
			10,
			3
		);

		// adapt the `is_virtual` property for bundle products
		add_filter(
			'woocommerce_gla_product_property_value_is_virtual',
			function ( bool $is_virtual, WC_Product $product ) {
				return $this->is_virtual_bundle( $is_virtual, $product );
			},
			10,
			2
		);

		// filter unsupported bundle products
		add_filter(
			'woocommerce_gla_get_sync_ready_products_pre_filter',
			function ( array $products ) {
				return $this->get_sync_ready_bundle_products( $products );
			}
		);
	}

	/**
	 * Adds the "bundle" product type to the list of applicable types
	 * for every attribute that can be applied to "simple" products.
	 *
	 * @return void
	 */
	protected function init_product_types(): void {
		// every attribute that applies to simple products also applies to bundle products.
		foreach ( AttributeManager::get_available_attribute_types() as $attribute_type ) {
			$attribute_id     = call_user_func( [ $attribute_type, 'get_id' ] );
			$applicable_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] );
			if ( ! in_array( 'simple', $applicable_types, true ) ) {
				continue;
			}

			add_filter(
				"woocommerce_gla_attribute_applicable_product_types_{$attribute_id}",
				function ( array $applicable_types ) {
					return $this->add_bundle_type( $applicable_types );
				}
			);
		}

		// hide the isBundle attribute on 'bundle' products (we set it automatically to true)
		add_filter(
			'woocommerce_gla_attribute_hidden_product_types_isBundle',
			function ( array $applicable_types ) {
				return $this->add_bundle_type( $applicable_types );
			}
		);

		// add the 'bundle' type to list of supported product types
		add_filter(
			'woocommerce_gla_supported_product_types',
			function ( array $product_types ) {
				return $this->add_bundle_type( $product_types );
			}
		);
	}

	/**
	 * @param array $types
	 *
	 * @return array
	 */
	private function add_bundle_type( array $types ): array {
		$types[] = 'bundle';

		return $types;
	}

	/**
	 * Set the isBundle product attribute to 'true' if product is a bundle.
	 *
	 * @param WC_Product $product
	 */
	private function handle_update_product( WC_Product $product ) {
		if ( $product->is_type( 'bundle' ) ) {
			$this->attribute_manager->update( $product, new IsBundle( true ) );
		}
	}

	/**
	 * @param float      $price        Calculated price of the product
	 * @param WC_Product $product      WooCommerce product
	 * @param bool       $tax_excluded Whether tax is excluded from product price
	 */
	private function calculate_price( float $price, WC_Product $product, bool $tax_excluded ): float {
		if ( ! $product instanceof WC_Product_Bundle ) {
			return $price;
		}

		return $tax_excluded ? $product->get_bundle_regular_price_excluding_tax() : $product->get_bundle_regular_price_including_tax();
	}

	/**
	 * @param float      $sale_price   Calculated sale price of the product
	 * @param WC_Product $product      WooCommerce product
	 * @param bool       $tax_excluded Whether tax is excluded from product price
	 */
	private function calculate_sale_price( float $sale_price, WC_Product $product, bool $tax_excluded ): float {
		if ( ! $product instanceof WC_Product_Bundle ) {
			return $sale_price;
		}

		$regular_price = $tax_excluded ? $product->get_bundle_regular_price_excluding_tax() : $product->get_bundle_regular_price_including_tax();
		$price         = $tax_excluded ? $product->get_bundle_price_excluding_tax() : $product->get_bundle_price_including_tax();

		// return current price as the sale price if it's lower than the regular price.
		if ( $price < $regular_price ) {
			return $price;
		}

		return $sale_price;
	}

	/**
	 * @param bool       $is_virtual Whether a product is virtual
	 * @param WC_Product $product    WooCommerce product
	 */
	private function is_virtual_bundle( bool $is_virtual, WC_Product $product ): bool {
		if ( $product instanceof WC_Product_Bundle && is_callable( [ $product, 'is_virtual_bundle' ] ) ) {
			return $product->is_virtual_bundle();
		}

		return $is_virtual;
	}

	/**
	 * Skip unsupported bundle products.
	 *
	 * @param WC_Product[] $products WooCommerce products
	 */
	private function get_sync_ready_bundle_products( array $products ): array {
		return array_filter(
			$products,
			function ( WC_Product $product ) {
				if ( $product instanceof WC_Product_Bundle && $product->requires_input() ) {
					return false;
				}

				return true;
			}
		);
	}
}
YoastWooCommerceSeo.php000064400000014255151542332670011202 0ustar00<?php
declare( strict_types=1 );

namespace Automattic\WooCommerce\GoogleListingsAndAds\Integration;

use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\GTIN;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\Attributes\MPN;
use WC_Product;
use WC_Product_Variation;

defined( 'ABSPATH' ) || exit;

/**
 * Class YoastWooCommerceSeo
 *
 * @package Automattic\WooCommerce\GoogleListingsAndAds\Integration
 */
class YoastWooCommerceSeo implements IntegrationInterface {

	protected const VALUE_KEY = 'yoast_seo';

	/**
	 * @var array Meta values stored by Yoast WooCommerce SEO plugin (per product).
	 */
	protected $yoast_global_identifiers = [];

	/**
	 * Returns whether the integration is active or not.
	 *
	 * @return bool
	 */
	public function is_active(): bool {
		return defined( 'WPSEO_WOO_VERSION' );
	}

	/**
	 * Initializes the integration (e.g. by registering the required hooks, filters, etc.).
	 *
	 * @return void
	 */
	public function init(): void {
		add_filter(
			'woocommerce_gla_product_attribute_value_options_mpn',
			function ( array $value_options ) {
				return $this->add_value_option( $value_options );
			}
		);
		add_filter(
			'woocommerce_gla_product_attribute_value_options_gtin',
			function ( array $value_options ) {
				return $this->add_value_option( $value_options );
			}
		);
		add_filter(
			'woocommerce_gla_product_attribute_value_mpn',
			function ( $value, WC_Product $product ) {
				return $this->get_mpn( $value, $product );
			},
			10,
			2
		);
		add_filter(
			'woocommerce_gla_product_attribute_value_gtin',
			function ( $value, WC_Product $product ) {
				return $this->get_gtin( $value, $product );
			},
			10,
			2
		);

		add_filter(
			'woocommerce_gla_attribute_mapping_sources',
			function ( $sources, $attribute_id ) {
				return $this->load_yoast_seo_attribute_mapping_sources( $sources, $attribute_id );
			},
			10,
			2
		);

		add_filter(
			'woocommerce_gla_gtin_migration_value',
			function ( $gtin, $product ) {
				if ( ! $gtin || self::VALUE_KEY === $gtin ) {
					return $this->get_gtin( self::VALUE_KEY, $product );
				}

				return $gtin;
			},
			10,
			2
		);
	}

	/**
	 * @param array $value_options
	 *
	 * @return array
	 */
	protected function add_value_option( array $value_options ): array {
		$value_options[ self::VALUE_KEY ] = 'From Yoast WooCommerce SEO';

		return $value_options;
	}

	/**
	 * @param mixed      $value
	 * @param WC_Product $product
	 *
	 * @return mixed
	 */
	protected function get_mpn( $value, WC_Product $product ) {
		if ( strpos( $value, self::VALUE_KEY ) === 0 ) {
			$value = $this->get_identifier_value( 'mpn', $product );
		}

		return ! empty( $value ) ? $value : null;
	}

	/**
	 * @param mixed      $value
	 * @param WC_Product $product
	 *
	 * @return mixed
	 */
	protected function get_gtin( $value, WC_Product $product ) {
		if ( strpos( $value, self::VALUE_KEY ) === 0 ) {
			$gtin_values = [
				$this->get_identifier_value( 'isbn', $product ),
				$this->get_identifier_value( 'gtin8', $product ),
				$this->get_identifier_value( 'gtin12', $product ),
				$this->get_identifier_value( 'gtin13', $product ),
				$this->get_identifier_value( 'gtin14', $product ),
			];
			$gtin_values = array_values( array_filter( $gtin_values ) );

			$value = $gtin_values[0] ?? null;
		}

		return $value;
	}

	/**
	 * Get the identifier value from cache or product meta.
	 *
	 * @param string     $key
	 * @param WC_Product $product
	 *
	 * @return mixed|null
	 */
	protected function get_identifier_value( string $key, WC_Product $product ) {
		$product_id = $product->get_id();

		if ( ! isset( $this->yoast_global_identifiers[ $product_id ] ) ) {
			$this->yoast_global_identifiers[ $product_id ] = $this->get_identifier_meta( $product );
		}

		return ! empty( $this->yoast_global_identifiers[ $product_id ][ $key ] ) ? $this->yoast_global_identifiers[ $product_id ][ $key ] : null;
	}

	/**
	 * Get identifier meta from product.
	 * For variations fallback to parent product if meta is empty.
	 *
	 * @since 2.3.1
	 *
	 * @param WC_Product $product
	 *
	 * @return mixed|null
	 */
	protected function get_identifier_meta( WC_Product $product ) {
		if ( ! $product ) {
			return null;
		}

		if ( $product instanceof WC_Product_Variation ) {
			$identifiers = $product->get_meta( 'wpseo_variation_global_identifiers_values', true );

			if ( ! is_array( $identifiers ) || empty( array_filter( $identifiers ) ) ) {
				$parent_product = wc_get_product( $product->get_parent_id() );
				$identifiers    = $this->get_identifier_meta( $parent_product );
			}

			return $identifiers;
		}

		return $product->get_meta( 'wpseo_global_identifier_values', true );
	}

	/**
	 *
	 * Merge the YOAST Fields with the Attribute Mapping available sources
	 *
	 * @param array  $sources The current sources
	 * @param string $attribute_id The Attribute ID
	 * @return array The merged sources
	 */
	protected function load_yoast_seo_attribute_mapping_sources( array $sources, string $attribute_id ): array {
		if ( $attribute_id === GTIN::get_id() ) {
			return array_merge( self::get_yoast_seo_attribute_mapping_gtin_sources(), $sources );
		}

		if ( $attribute_id === MPN::get_id() ) {
			return array_merge( self::get_yoast_seo_attribute_mapping_mpn_sources(), $sources );
		}

		return $sources;
	}

	/**
	 * Load the group disabled option for Attribute mapping YOAST SEO
	 *
	 * @return array The disabled group option
	 */
	protected function get_yoast_seo_attribute_mapping_group_source(): array {
		return [ 'disabled:' . self::VALUE_KEY => __( '- Yoast SEO -', 'google-listings-and-ads' ) ];
	}

	/**
	 * Load the GTIN Fields for Attribute mapping YOAST SEO
	 *
	 * @return array The GTIN sources
	 */
	protected function get_yoast_seo_attribute_mapping_gtin_sources(): array {
		return array_merge( self::get_yoast_seo_attribute_mapping_group_source(), [ self::VALUE_KEY . ':gtin' => __( 'GTIN Field', 'google-listings-and-ads' ) ] );
	}

	/**
	 * Load the MPN Fields for Attribute mapping YOAST SEO
	 *
	 * @return array The MPN sources
	 */
	protected function get_yoast_seo_attribute_mapping_mpn_sources(): array {
		return array_merge( self::get_yoast_seo_attribute_mapping_group_source(), [ self::VALUE_KEY . ':mpn' => __( 'MPN Field', 'google-listings-and-ads' ) ] );
	}
}