File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/AccountService.php.tar
uyarreklam.com.tr/httpdocs/wp-content/plugins/google-listings-and-ads/src/Ads/AccountService.php 0000644 00000026533 15154451736 0031776 0 ustar 00 var/www/vhosts <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsConversionAction;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\BillingSetupStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Connection;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountService
*
* Container used to access:
* - Ads
* - AdsConversionAction
* - Connection
* - Merchant
* - MerchantAccountState
* - Middleware
* - TransientsInterface
*
* @since 1.11.0
* @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
*/
class AccountService implements ContainerAwareInterface, OptionsAwareInterface, Service {
use ContainerAwareTrait;
use OptionsAwareTrait;
/**
* @var AdsAccountState
*/
protected $state;
/**
* AccountService constructor.
*
* @param AdsAccountState $state
*/
public function __construct( AdsAccountState $state ) {
$this->state = $state;
}
/**
* Get Ads accounts associated with the connected Google account.
*
* @return array
* @throws Exception When an API error occurs.
*/
public function get_accounts(): array {
return $this->container->get( Ads::class )->get_ads_accounts();
}
/**
* Get the connected ads account.
*
* @return array
*/
public function get_connected_account(): array {
$id = $this->options->get_ads_id();
$status = [
'id' => $id,
'currency' => $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ),
'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ) ), ENT_QUOTES ),
'status' => $id ? 'connected' : 'disconnected',
];
$incomplete = $this->state->last_incomplete_step();
if ( ! empty( $incomplete ) ) {
$status['status'] = 'incomplete';
$status['step'] = $incomplete;
}
$status += $this->state->get_step_data( 'set_id' );
return $status;
}
/**
* Use an existing Ads account. Mark the 'set_id' step as done and sets the Ads ID.
*
* @param int $account_id The Ads account ID to use.
*
* @throws Exception If there is already an Ads account ID.
*/
public function use_existing_account( int $account_id ) {
$ads_id = $this->options->get_ads_id();
if ( $ads_id && $ads_id !== $account_id ) {
throw new Exception(
/* translators: 1: is a numeric account ID */
sprintf( __( 'Ads account %1$d already connected.', 'google-listings-and-ads' ), $ads_id )
);
}
$state = $this->state->get();
// Don't do anything if this step was already finished.
if ( AdsAccountState::STEP_DONE === $state['set_id']['status'] ) {
return;
}
$this->container->get( Middleware::class )->link_ads_account( $account_id );
// Skip billing setup flow when using an existing account.
$state['set_id']['status'] = AdsAccountState::STEP_DONE;
$state['billing']['status'] = AdsAccountState::STEP_DONE;
$this->state->update( $state );
}
/**
* Performs the steps necessary to setup an ads account.
* Should always resume up at the last pending or unfinished step.
* If the Ads account has already been created, the ID is simply returned.
*
* @return array The newly created (or pre-existing) Ads ID.
* @throws Exception If an error occurs during any step.
*/
public function setup_account(): array {
$state = $this->state->get();
$ads_id = $this->options->get_ads_id();
$account = [ 'id' => $ads_id ];
foreach ( $state as $name => &$step ) {
if ( AdsAccountState::STEP_DONE === $step['status'] ) {
continue;
}
try {
switch ( $name ) {
case 'set_id':
// Just in case, don't create another Ads ID.
if ( ! empty( $ads_id ) ) {
break;
}
$account = $this->container->get( Middleware::class )->create_ads_account();
$step['data']['sub_account'] = true;
$step['data']['created_timestamp'] = time();
break;
case 'billing':
$this->check_billing_status( $account );
break;
case 'conversion_action':
$this->create_conversion_action();
break;
case 'link_merchant':
// Continue to next step if the MC account is not connected yet.
if ( ! $this->options->get_merchant_id() ) {
// Save step as pending and continue the foreach loop with `continue 2`.
$state[ $name ]['status'] = AdsAccountState::STEP_PENDING;
$this->state->update( $state );
continue 2;
}
$this->link_merchant_account();
break;
case 'account_access':
$this->check_ads_account_has_access();
break;
default:
throw new Exception(
/* translators: 1: is a string representing an unknown step name */
sprintf( __( 'Unknown ads account creation step %1$s', 'google-listings-and-ads' ), $name )
);
}
$step['status'] = AdsAccountState::STEP_DONE;
$step['message'] = '';
$this->state->update( $state );
} catch ( Exception $e ) {
$step['status'] = AdsAccountState::STEP_ERROR;
$step['message'] = $e->getMessage();
$this->state->update( $state );
throw $e;
}
}
return $account;
}
/**
* Gets the billing setup status and returns a setup URL if available.
*
* @return array
*/
public function get_billing_status(): array {
$status = $this->container->get( Ads::class )->get_billing_status();
if ( BillingSetupStatus::APPROVED === $status ) {
$this->state->complete_step( 'billing' );
return [ 'status' => $status ];
}
$billing_url = $this->options->get( OptionsInterface::ADS_BILLING_URL );
// Check if user has provided the access and ocid is present.
$connection_status = $this->container->get( Connection::class )->get_status();
$email = $connection_status['email'] ?? '';
$has_access = $this->container->get( Ads::class )->has_access( $email );
$ocid = $this->options->get( OptionsInterface::ADS_ACCOUNT_OCID, null );
// Link directly to the payment page if the customer already has access.
if ( $has_access ) {
$billing_url = add_query_arg(
[
'ocid' => $ocid ?: 0,
],
'https://ads.google.com/aw/signup/payment'
);
}
return [
'status' => $status,
'billing_url' => $billing_url,
];
}
/**
* Check if the Ads account has access.
*
* @throws ExceptionWithResponseData If the account doesn't have access.
*/
private function check_ads_account_has_access() {
$access_status = $this->get_ads_account_has_access();
if ( ! $access_status['has_access'] ) {
throw new ExceptionWithResponseData(
__( 'Account must be accepted before completing setup.', 'google-listings-and-ads' ),
428,
null,
$access_status
);
}
}
/**
* Gets the Ads account access status.
*
* @return array {
* Returns the access status, last completed account setup step,
* and invite link if available.
*
* @type bool $has_access Whether the customer has access to the account.
* @type string $step The last completed setup step for the Ads account.
* @type string $invite_link The URL to the invite link.
* }
*/
public function get_ads_account_has_access() {
$has_access = false;
// Check if an Ads ID is present.
if ( $this->options->get_ads_id() ) {
$connection_status = $this->container->get( Connection::class )->get_status();
$email = $connection_status['email'] ?? '';
}
// If no email, means google account is not connected.
if ( ! empty( $email ) ) {
$has_access = $this->container->get( Ads::class )->has_access( $email );
}
// If we have access, complete the step so that it won't be called next time.
if ( $has_access ) {
$this->state->complete_step( 'account_access' );
}
return [
'has_access' => $has_access,
'step' => $this->state->last_incomplete_step(),
'invite_link' => $this->options->get( OptionsInterface::ADS_BILLING_URL, '' ),
];
}
/**
* Disconnect Ads account
*/
public function disconnect() {
$this->options->delete( OptionsInterface::ADS_ACCOUNT_CURRENCY );
$this->options->delete( OptionsInterface::ADS_ACCOUNT_OCID );
$this->options->delete( OptionsInterface::ADS_ACCOUNT_STATE );
$this->options->delete( OptionsInterface::ADS_BILLING_URL );
$this->options->delete( OptionsInterface::ADS_CONVERSION_ACTION );
$this->options->delete( OptionsInterface::ADS_ID );
$this->options->delete( OptionsInterface::ADS_SETUP_COMPLETED_AT );
$this->options->delete( OptionsInterface::CAMPAIGN_CONVERT_STATUS );
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::ADS_CAMPAIGN_COUNT );
}
/**
* Confirm the billing flow has been completed.
*
* @param array $account Account details.
*
* @throws ExceptionWithResponseData If this step hasn't been completed yet.
*/
private function check_billing_status( array $account ) {
$status = BillingSetupStatus::UNKNOWN;
// Only check billing status if we haven't just created the account.
if ( empty( $account['billing_url'] ) ) {
$status = $this->container->get( Ads::class )->get_billing_status();
}
if ( BillingSetupStatus::APPROVED !== $status ) {
throw new ExceptionWithResponseData(
__( 'Billing setup must be completed.', 'google-listings-and-ads' ),
428,
null,
[
'billing_url' => $this->options->get( OptionsInterface::ADS_BILLING_URL ),
'billing_status' => $status,
]
);
}
}
/**
* Get the callback function for linking a merchant account.
*
* @throws Exception When the ads account hasn't been set yet.
*/
private function link_merchant_account() {
if ( ! $this->options->get_ads_id() ) {
throw new Exception( 'An Ads account must be connected' );
}
$mc_state = $this->container->get( MerchantAccountState::class );
// Create link for Merchant and accept it in Ads.
$waiting_acceptance = $this->container->get( Merchant::class )->link_ads_id( $this->options->get_ads_id() );
if ( $waiting_acceptance ) {
$this->container->get( Ads::class )->accept_merchant_link( $this->options->get_merchant_id() );
}
$mc_state->complete_step( 'link_ads' );
}
/**
* Create the generic GLA conversion action and store the details as an option.
*
* @throws Exception If the conversion action can't be created.
*/
private function create_conversion_action(): void {
$action = $this->container->get( AdsConversionAction::class )->create_conversion_action();
$this->options->update( OptionsInterface::ADS_CONVERSION_ACTION, $action );
}
}
httpdocs/wp-content/plugins/google-listings-and-ads/src/MerchantCenter/AccountService.php 0000644 00000055711 15154734000 0034155 0 ustar 00 var/www/vhosts/uyarreklam.com.tr <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter;
use Automattic\Jetpack\Connection\Client;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Merchant;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\SiteVerification;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\OAuthService;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\MerchantIssueTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingRateTable;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Table\ShippingTimeTable;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ApiNotReady;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\CleanupSyncedProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerInterface;
use Exception;
use Jetpack_Options;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountService
*
* Container used to access:
* - Ads
* - AdsAccountState
* - JobRepository
* - Merchant
* - MerchantCenterService
* - MerchantIssueTable
* - MerchantStatuses
* - Middleware
* - SiteVerification
* - ShippingRateTable
* - ShippingTimeTable
*
* @since 1.12.0
* @package Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter
*/
class AccountService implements ContainerAwareInterface, OptionsAwareInterface, Service {
use ContainerAwareTrait;
use OptionsAwareTrait;
use PluginHelper;
/**
* @var MerchantAccountState
*/
protected $state;
/**
* Perform a website claim with overwrite.
*
* @var bool
*/
protected $overwrite_claim = false;
/**
* Allow switching the existing website URL.
*
* @var bool
*/
protected $allow_switch_url = false;
/**
* AccountService constructor.
*
* @param MerchantAccountState $state
*/
public function __construct( MerchantAccountState $state ) {
$this->state = $state;
}
/**
* Get all Merchant Accounts associated with the connected account.
*
* @return array
* @throws Exception When an API error occurs.
*/
public function get_accounts(): array {
return $this->container->get( Middleware::class )->get_merchant_accounts();
}
/**
* Use an existing MC account. Mark the 'set_id' step as done, update the MC account's website URL,
* and sets the Merchant ID.
*
* @param int $account_id The merchant ID to use.
*
* @throws ExceptionWithResponseData If there's a website URL conflict, or account data can't be retrieved.
*/
public function use_existing_account_id( int $account_id ): void {
// Reset the process if the provided ID isn't the same as the one stored in options.
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && $merchant_id !== $account_id ) {
$this->reset_account_setup();
}
$state = $this->state->get();
// Don't do anything if this step was already finished.
if ( MerchantAccountState::STEP_DONE === $state['set_id']['status'] ) {
return;
}
try {
// Make sure the existing account has the correct website URL (or fail).
$this->maybe_add_merchant_center_url( $account_id );
// Re-fetch state as it might have changed.
$state = $this->state->get();
$middleware = $this->container->get( Middleware::class );
// Maybe the existing account is a sub-account!
$state['set_id']['data']['from_mca'] = false;
foreach ( $middleware->get_merchant_accounts() as $existing_account ) {
if ( $existing_account['id'] === $account_id ) {
$state['set_id']['data']['from_mca'] = $existing_account['subaccount'];
break;
}
}
$middleware->link_merchant_account( $account_id );
$state['set_id']['status'] = MerchantAccountState::STEP_DONE;
$this->state->update( $state );
} catch ( ExceptionWithResponseData $e ) {
throw $e;
} catch ( Exception $e ) {
throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() );
}
}
/**
* Run the process for setting up a Merchant Center account (sub-account or standalone).
*
* @param int $account_id
*
* @return array The account ID if setup has completed.
* @throws ExceptionWithResponseData When the account is already connected or a setup error occurs.
*/
public function setup_account( int $account_id ) {
// Reset the process if the provided ID isn't the same as the one stored in options.
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && $merchant_id !== $account_id ) {
$this->reset_account_setup();
}
try {
return $this->setup_account_steps();
} catch ( ExceptionWithResponseData | ApiNotReady $e ) {
throw $e;
} catch ( Exception $e ) {
throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() );
}
}
/**
* Create or link an account, switching the URL during the set_id step.
*
* @param int $account_id
*
* @return array
* @throws ExceptionWithResponseData When a setup error occurs.
*/
public function switch_url( int $account_id ): array {
$state = $this->state->get();
$switch_necessary = ! empty( $state['set_id']['data']['old_url'] );
$set_id_status = $state['set_id']['status'] ?? MerchantAccountState::STEP_PENDING;
if ( ! $account_id || MerchantAccountState::STEP_DONE === $set_id_status || ! $switch_necessary ) {
throw $this->prepare_exception(
__( 'Attempting invalid URL switch.', 'google-listings-and-ads' )
);
}
$this->allow_switch_url = true;
$this->use_existing_account_id( $account_id );
return $this->setup_account( $account_id );
}
/**
* Create or link an account, overwriting the website claim during the claim step.
*
* @param int $account_id
*
* @return array
* @throws ExceptionWithResponseData When a setup error occurs.
*/
public function overwrite_claim( int $account_id ): array {
$state = $this->state->get( false );
$overwrite_necessary = ! empty( $state['claim']['data']['overwrite_required'] );
$claim_status = $state['claim']['status'] ?? MerchantAccountState::STEP_PENDING;
if ( MerchantAccountState::STEP_DONE === $claim_status || ! $overwrite_necessary ) {
throw $this->prepare_exception(
__( 'Attempting invalid claim overwrite.', 'google-listings-and-ads' )
);
}
$this->overwrite_claim = true;
return $this->setup_account( $account_id );
}
/**
* Get the connected merchant account.
*
* @return array
*/
public function get_connected_status(): array {
/** @var NotificationsService $notifications_service */
$notifications_service = $this->container->get( NotificationsService::class );
$id = $this->options->get_merchant_id();
$wpcom_rest_api_status = $this->options->get( OptionsInterface::WPCOM_REST_API_STATUS );
// If token is revoked outside the extension. Set the status as error to force the merchant to grant access again.
if ( $wpcom_rest_api_status === 'approved' && ! $this->is_wpcom_api_status_healthy() ) {
$wpcom_rest_api_status = OAuthService::STATUS_ERROR;
$this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $wpcom_rest_api_status );
}
$status = [
'id' => $id,
'status' => $id ? 'connected' : 'disconnected',
'notification_service_enabled' => $notifications_service->is_enabled(),
'wpcom_rest_api_status' => $wpcom_rest_api_status,
];
$incomplete = $this->state->last_incomplete_step();
if ( ! empty( $incomplete ) ) {
$status['status'] = 'incomplete';
$status['step'] = $incomplete;
}
return $status;
}
/**
* Return the setup status to determine what step to continue at.
*
* @return array
*/
public function get_setup_status(): array {
return $this->container->get( MerchantCenterService::class )->get_setup_status();
}
/**
* Disconnect Merchant Center account
*/
public function disconnect() {
$this->options->delete( OptionsInterface::CONTACT_INFO_SETUP );
$this->options->delete( OptionsInterface::MC_SETUP_COMPLETED_AT );
$this->options->delete( OptionsInterface::MERCHANT_ACCOUNT_STATE );
$this->options->delete( OptionsInterface::MERCHANT_CENTER );
$this->options->delete( OptionsInterface::SITE_VERIFICATION );
$this->options->delete( OptionsInterface::TARGET_AUDIENCE );
$this->options->delete( OptionsInterface::MERCHANT_ID );
$this->options->delete( OptionsInterface::CLAIMED_URL_HASH );
$this->container->get( MerchantStatuses::class )->delete();
$this->container->get( MerchantIssueTable::class )->truncate();
$this->container->get( ShippingRateTable::class )->truncate();
$this->container->get( ShippingTimeTable::class )->truncate();
$this->container->get( JobRepository::class )->get( CleanupSyncedProducts::class )->schedule();
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_ACCOUNT_REVIEW );
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::URL_MATCHES );
$this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_IS_SUBACCOUNT );
}
/**
* Performs the steps necessary to initialize a Merchant Center account.
* Should always resume up at the last pending or unfinished step. If the Merchant Center account
* has already been created, the ID is simply returned.
*
* @return array The newly created (or pre-existing) Merchant account data.
* @throws ExceptionWithResponseData If an error occurs during any step.
* @throws Exception If the step is unknown.
* @throws ApiNotReady If we should wait to complete the next step.
*/
private function setup_account_steps() {
$state = $this->state->get();
$merchant_id = $this->options->get_merchant_id();
$merchant = $this->container->get( Merchant::class );
$middleware = $this->container->get( Middleware::class );
foreach ( $state as $name => &$step ) {
if ( MerchantAccountState::STEP_DONE === $step['status'] ) {
continue;
}
if ( 'link' === $name ) {
$time_to_wait = $this->state->get_seconds_to_wait_after_created();
if ( $time_to_wait ) {
sleep( $time_to_wait );
}
}
try {
switch ( $name ) {
case 'set_id':
// Just in case, don't create another merchant ID.
if ( ! empty( $merchant_id ) ) {
break;
}
$merchant_id = $middleware->create_merchant_account();
$step['data']['from_mca'] = true;
$step['data']['created_timestamp'] = time();
break;
case 'verify':
// Skip if previously verified.
if ( $this->state->is_site_verified() ) {
break;
}
$site_url = esc_url_raw( $this->get_site_url() );
$this->container->get( SiteVerification::class )->verify_site( $site_url );
break;
case 'link':
$middleware->link_merchant_to_mca();
break;
case 'claim':
// At this step, the website URL is assumed to be correct.
// If the URL is already claimed, no claim should be attempted.
if ( $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed() ) {
break;
}
if ( $this->overwrite_claim ) {
$middleware->claim_merchant_website( true );
} else {
$merchant->claimwebsite();
}
break;
case 'link_ads':
// Continue to next step if Ads account is not connected yet.
if ( ! $this->options->get_ads_id() ) {
// Save step as pending and continue the foreach loop with `continue 2`.
$state[ $name ]['status'] = MerchantAccountState::STEP_PENDING;
$this->state->update( $state );
continue 2;
}
$this->link_ads_account();
break;
default:
throw new Exception(
sprintf(
/* translators: 1: is a string representing an unknown step name */
__( 'Unknown merchant account creation step %1$s', 'google-listings-and-ads' ),
$name
)
);
}
$step['status'] = MerchantAccountState::STEP_DONE;
$step['message'] = '';
$this->state->update( $state );
} catch ( Exception $e ) {
$step['status'] = MerchantAccountState::STEP_ERROR;
$step['message'] = $e->getMessage();
// URL already claimed.
if ( 'claim' === $name && 403 === $e->getCode() ) {
$data = [
'id' => $merchant_id,
'website_url' => $this->strip_url_protocol(
esc_url_raw( $this->get_site_url() )
),
];
// Sub-account: request overwrite confirmation.
if ( $state['set_id']['data']['from_mca'] ?? true ) {
do_action( 'woocommerce_gla_site_claim_overwrite_required', [] );
$step['data']['overwrite_required'] = true;
$e = $this->prepare_exception( $e->getMessage(), $data, $e->getCode() );
} else {
do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'independent_account' ] );
// Independent account: overwrite not possible.
$e = $this->prepare_exception(
__( 'Unable to claim website URL with this Merchant Center Account.', 'google-listings-and-ads' ),
$data,
406
);
}
} elseif ( 'link' === $name && 401 === $e->getCode() ) {
// New sub-account not yet manipulable.
$state['set_id']['data']['created_timestamp'] = time();
$e = ApiNotReady::retry_after( MerchantAccountState::MC_DELAY_AFTER_CREATE );
}
$this->state->update( $state );
throw $e;
}
}
return [ 'id' => $merchant_id ];
}
/**
* Restart the account setup when we are connecting with a different account ID.
* Do not allow reset when the full setup process has completed.
*
* @throws ExceptionWithResponseData When the full setup process is completed.
*/
private function reset_account_setup() {
// Can't reset if the MC connection process has been completed previously.
if ( $this->container->get( MerchantCenterService::class )->is_setup_complete() ) {
throw $this->prepare_exception(
sprintf(
/* translators: 1: is a numeric account ID */
__( 'Merchant Center account already connected: %d', 'google-listings-and-ads' ),
$this->options->get_merchant_id()
)
);
}
$this->disconnect();
}
/**
* Ensure the Merchant Center account's Website URL matches the site URL. Update an empty value or
* a different, unclaimed URL value. Throw a 409 exception if a different, claimed URL is found.
*
* @param int $merchant_id The Merchant Center account to update.
*
* @throws ExceptionWithResponseData If the account URL doesn't match the site URL or the URL is invalid.
*/
private function maybe_add_merchant_center_url( int $merchant_id ) {
$site_url = esc_url_raw( $this->get_site_url() );
if ( ! wc_is_valid_url( $site_url ) ) {
throw $this->prepare_exception( __( 'Invalid site URL.', 'google-listings-and-ads' ) );
}
/** @var Merchant $merchant */
$merchant = $this->container->get( Merchant::class );
/** @var Account $account */
$account = $merchant->get_account( $merchant_id );
$account_url = $account->getWebsiteUrl() ?: '';
if ( untrailingslashit( $site_url ) !== untrailingslashit( $account_url ) ) {
$is_website_claimed = $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed();
if ( ! empty( $account_url ) && $is_website_claimed && ! $this->allow_switch_url ) {
$state = $this->state->get();
$state['set_id']['data']['old_url'] = $account_url;
$state['set_id']['status'] = MerchantAccountState::STEP_ERROR;
$this->state->update( $state );
$clean_account_url = $this->strip_url_protocol( $account_url );
$clean_site_url = $this->strip_url_protocol( $site_url );
do_action( 'woocommerce_gla_url_switch_required', [] );
throw $this->prepare_exception(
sprintf(
/* translators: 1: is a website URL (without the protocol) */
__( 'This Merchant Center account already has a verified and claimed URL, %1$s', 'google-listings-and-ads' ),
$clean_account_url
),
[
'id' => $merchant_id,
'claimed_url' => $clean_account_url,
'new_url' => $clean_site_url,
],
409
);
}
$account->setWebsiteUrl( $site_url );
$merchant->update_account( $account );
// Clear previous hashed URL.
$this->options->delete( OptionsInterface::CLAIMED_URL_HASH );
do_action( 'woocommerce_gla_url_switch_success', [] );
}
}
/**
* Get the callback function for linking an Ads account.
*
* @throws Exception When the merchant account hasn't been set yet.
*/
private function link_ads_account() {
if ( ! $this->options->get_merchant_id() ) {
throw new Exception( 'A Merchant Center account must be connected' );
}
$ads_state = $this->container->get( AdsAccountState::class );
// Create link for Merchant and accept it in Ads.
$waiting_acceptance = $this->container->get( Merchant::class )->link_ads_id( $this->options->get_ads_id() );
if ( $waiting_acceptance ) {
$this->container->get( Ads::class )->accept_merchant_link( $this->options->get_merchant_id() );
}
$ads_state->complete_step( 'link_merchant' );
}
/**
* Prepares an Exception to be thrown with Merchant data:
* - Ensure it has the merchant_id value
* - Default to a 400 error code
*
* @param string $message
* @param array $data
* @param int|null $code
*
* @return ExceptionWithResponseData
*/
private function prepare_exception( string $message, array $data = [], ?int $code = null ): ExceptionWithResponseData {
$merchant_id = $this->options->get_merchant_id();
if ( $merchant_id && ! isset( $data['id'] ) ) {
$data['id'] = $merchant_id;
}
return new ExceptionWithResponseData( $message, $code ?: 400, null, $data );
}
/**
* Delete the option regarding WPCOM authorization
*
* @return bool
*/
public function reset_wpcom_api_authorization_data(): bool {
$this->delete_wpcom_api_auth_nonce();
$this->delete_wpcom_api_status_transient();
return $this->options->delete( OptionsInterface::WPCOM_REST_API_STATUS );
}
/**
* Update the status of the merchant granting access to Google's WPCOM app in the database.
* Before updating the status in the DB it will compare the nonce stored in the DB with the nonce passed to the API.
*
* @param string $status The status of the merchant granting access to Google's WPCOM app.
* @param string $nonce The nonce provided by Google in the URL query parameter when Google redirects back to merchant's site.
*
* @return bool
* @throws ExceptionWithResponseData If the stored nonce / nonce from query param is not provided, or the nonces mismatch.
*/
public function update_wpcom_api_authorization( string $status, string $nonce ): bool {
try {
$stored_nonce = $this->options->get( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE );
if ( empty( $stored_nonce ) ) {
throw $this->prepare_exception(
__( 'No stored nonce found in the database, skip updating auth status.', 'google-listings-and-ads' )
);
}
if ( empty( $nonce ) ) {
throw $this->prepare_exception(
__( 'Nonce is not provided, skip updating auth status.', 'google-listings-and-ads' )
);
}
if ( $stored_nonce !== $nonce ) {
$this->delete_wpcom_api_auth_nonce();
throw $this->prepare_exception(
__( 'Nonces mismatch, skip updating auth status.', 'google-listings-and-ads' )
);
}
$this->delete_wpcom_api_auth_nonce();
/**
* When the WPCOM Authorization status has been updated.
*
* @event update_wpcom_api_authorization
* @property string status The status of the request.
* @property int|null blog_id The blog ID.
*/
do_action(
'woocommerce_gla_track_event',
'update_wpcom_api_authorization',
[
'status' => $status,
'blog_id' => Jetpack_Options::get_option( 'id' ),
]
);
$this->delete_wpcom_api_status_transient();
return $this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $status );
} catch ( ExceptionWithResponseData $e ) {
/**
* When the WPCOM Authorization status has been updated with errors.
*
* @event update_wpcom_api_authorization
* @property string status The status of the request.
* @property int|null blog_id The blog ID.
*/
do_action(
'woocommerce_gla_track_event',
'update_wpcom_api_authorization',
[
'status' => $e->getMessage(),
'blog_id' => Jetpack_Options::get_option( 'id' ),
]
);
throw $e;
}
}
/**
* Delete the nonce of "verifying Google is the one redirect back to merchant site and set the auth status" in the database.
*
* @return bool
*/
public function delete_wpcom_api_auth_nonce(): bool {
return $this->options->delete( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE );
}
/**
* Deletes the transient storing the WPCOM Status data.
*/
public function delete_wpcom_api_status_transient(): void {
$transients = $this->container->get( TransientsInterface::class );
$transients->delete( TransientsInterface::WPCOM_API_STATUS );
}
/**
* Check if the WPCOM API Status is healthy by doing a request to /wc/partners/google/remote-site-status endpoint in WPCOM.
*
* @return bool True when the status is healthy, false otherwise.
*/
public function is_wpcom_api_status_healthy() {
/** @var TransientsInterface $transients */
$transients = $this->container->get( TransientsInterface::class );
$status = $transients->get( TransientsInterface::WPCOM_API_STATUS );
if ( ! $status ) {
$integration_status_args = [
'method' => 'GET',
'timeout' => 30,
'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/remote-site-status',
'user_id' => get_current_user_id(),
];
$integration_remote_request_response = Client::remote_request( $integration_status_args, null );
if ( is_wp_error( $integration_remote_request_response ) ) {
$status = [ 'is_healthy' => false ];
} else {
$status = json_decode( wp_remote_retrieve_body( $integration_remote_request_response ), true ) ?? [ 'is_healthy' => false ];
}
$transients->set( TransientsInterface::WPCOM_API_STATUS, $status, MINUTE_IN_SECONDS * 30 );
}
return isset( $status['is_healthy'] ) && $status['is_healthy'] && $status['is_wc_rest_api_healthy'] && $status['is_partner_token_healthy'];
}
}