File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Integration.tar
IntegrationInitializer.php 0000644 00000002305 15154233267 0011754 0 ustar 00 <?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.php 0000644 00000001052 15154233267 0011367 0 ustar 00 <?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.php 0000644 00000031745 15154233267 0007466 0 ustar 00 <?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.php 0000644 00000024123 15154233267 0007216 0 ustar 00 <?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.php 0000644 00000004740 15154233267 0010643 0 ustar 00 <?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.php 0000644 00000007010 15154233267 0011330 0 ustar 00 <?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.php 0000644 00000014637 15154233267 0012375 0 ustar 00 <?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.php 0000644 00000014255 15154233267 0011202 0 ustar 00 <?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' ) ] );
}
}