File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Ads.tar
AccountService.php 0000644 00000026533 15154256563 0010217 0 ustar 00 <?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 );
}
}
AdsAwareInterface.php 0000644 00000000563 15154256563 0010605 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;
defined( 'ABSPATH' ) || exit;
/**
* Interface AdsAwareInterface
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
*/
interface AdsAwareInterface {
/**
* @param AdsService $ads_service
*/
public function set_ads_object( AdsService $ads_service ): void;
}
AdsAwareTrait.php 0000644 00000000743 15154256563 0007770 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;
defined( 'ABSPATH' ) || exit;
/**
* Trait AdsAwareTrait
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
*/
trait AdsAwareTrait {
/**
* The AdsService object.
*
* @var AdsService
*/
protected $ads_service;
/**
* @param AdsService $ads_service
*/
public function set_ads_object( AdsService $ads_service ): void {
$this->ads_service = $ads_service;
}
}
AdsService.php 0000644 00000003740 15154256563 0007325 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
defined( 'ABSPATH' ) || exit;
/**
* Class AdsService
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
*/
class AdsService implements OptionsAwareInterface, Service {
use OptionsAwareTrait;
/** @var AdsAccountState */
protected $account_state;
/**
* AdsService constructor.
*
* @since 1.11.0
*
* @param AdsAccountState $account_state
*/
public function __construct( AdsAccountState $account_state ) {
$this->account_state = $account_state;
}
/**
* Determine whether Ads setup has been started.
*
* @since 1.11.0
* @return bool
*/
public function is_setup_started(): bool {
return $this->account_state->last_incomplete_step() !== '' && ! $this->is_setup_complete();
}
/**
* Determine whether Ads setup has completed.
*
* @return bool
*/
public function is_setup_complete(): bool {
return boolval( $this->options->get( OptionsInterface::ADS_SETUP_COMPLETED_AT, false ) );
}
/**
* Determine whether Ads has connected.
*
* @return bool
*/
public function is_connected(): bool {
$google_connected = boolval( $this->options->get( OptionsInterface::GOOGLE_CONNECTED, false ) );
return $google_connected && $this->is_setup_complete();
}
/**
* Determine whether the Ads account is connected, even when pending billing.
*
* @return bool
*/
public function connected_account(): bool {
$id = $this->options->get_ads_id();
$last_step = $this->account_state->last_incomplete_step();
return $id && ( $last_step === '' || $last_step === 'billing' );
}
}
AssetSuggestionsService.php 0000644 00000057466 15154256563 0012146 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\ArrayUtil;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\ImageUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\DimensionUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsAssetGroupAsset;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType;
use Exception;
use WP_Query;
use wpdb;
use DOMDocument;
/**
* Class AssetSuggestionsService
*
* Suggest assets and possible final URLs.
*
* @since 2.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Ads
*/
class AssetSuggestionsService implements Service {
/**
* WP Proxy
*
* @var WP
*/
protected WP $wp;
/**
* WC Proxy
*
* @var WC
*/
protected WC $wc;
/**
* Image utilities.
*
* @var ImageUtility
*/
protected ImageUtility $image_utility;
/**
* The AdsAssetGroupAsset class.
*
* @var AdsAssetGroupAsset
*/
protected $asset_group_asset;
/**
* WordPress database access abstraction class.
*
* @var wpdb
*/
protected $wpdb;
/**
* Image requirements.
*/
protected const IMAGE_REQUIREMENTS = [
self::MARKETING_IMAGE_KEY => [
'minimum' => [ 600, 314 ],
'recommended' => [ 1200, 628 ],
'max_qty' => 8,
],
self::SQUARE_MARKETING_IMAGE_KEY => [
'minimum' => [ 300, 300 ],
'recommended' => [ 1200, 1200 ],
'max_qty' => 8,
],
self::PORTRAIT_MARKETING_IMAGE_KEY => [
'minimum' => [ 480, 600 ],
'recommended' => [ 960, 1200 ],
'max_qty' => 4,
],
self::LOGO_IMAGE_KEY => [
'minimum' => [ 128, 128 ],
'recommended' => [ 1200, 1200 ],
'max_qty' => 20,
],
];
/**
* Default maximum marketing images.
*/
protected const DEFAULT_MAXIMUM_MARKETING_IMAGES = 20;
/**
* The subsize key for the square marketing image.
*/
protected const SQUARE_MARKETING_IMAGE_KEY = 'gla_square_marketing_asset';
/**
* The subsize key for the marketing image.
*/
protected const MARKETING_IMAGE_KEY = 'gla_marketing_asset';
/**
* The subsize key for the portrait marketing image.
*/
protected const PORTRAIT_MARKETING_IMAGE_KEY = 'gla_portrait_marketing_asset';
/**
* The subsize key for the logo image.
*/
protected const LOGO_IMAGE_KEY = 'gla_logo_asset';
/**
* The homepage key ID.
*/
protected const HOMEPAGE_KEY_ID = 0;
/**
* AssetSuggestionsService constructor.
*
* @param WP $wp WP Proxy.
* @param WC $wc WC Proxy.
* @param ImageUtility $image_utility Image utility.
* @param wpdb $wpdb WordPress database access abstraction class.
* @param AdsAssetGroupAsset $asset_group_asset The AdsAssetGroupAsset class.
*/
public function __construct( WP $wp, WC $wc, ImageUtility $image_utility, wpdb $wpdb, AdsAssetGroupAsset $asset_group_asset ) {
$this->wp = $wp;
$this->wc = $wc;
$this->wpdb = $wpdb;
$this->image_utility = $image_utility;
$this->asset_group_asset = $asset_group_asset;
}
/**
* Get WP and other campaigns' assets from the specific post or term.
*
* @param int $id Post ID, Term ID or self::HOMEPAGE_KEY_ID if it's the homepage.
* @param string $type Only possible values are post, term and homepage.
*/
public function get_assets_suggestions( int $id, string $type ): array {
$asset_group_assets = $this->get_asset_group_asset_suggestions( $id, $type );
if ( ! empty( $asset_group_assets ) ) {
return $asset_group_assets;
}
return $this->get_wp_assets( $id, $type );
}
/**
* Get URL for a specific post or term.
*
* @param int $id Post ID, Term ID or self::HOMEPAGE_KEY_ID
* @param string $type Only possible values are post, term and homepage.
*
* @return string The URL.
* @throws Exception If the ID is invalid.
*/
protected function get_url( int $id, string $type ): string {
if ( $type === 'post' ) {
$url = get_permalink( $id );
} elseif ( $type === 'term' ) {
$url = get_term_link( $id );
} else {
$url = get_bloginfo( 'url' );
}
if ( is_wp_error( $url ) || empty( $url ) ) {
throw new Exception(
/* translators: 1: is an integer representing an unknown Term ID */
sprintf( __( 'Invalid Term ID or Post ID or site url %1$d', 'google-listings-and-ads' ), $id )
);
}
return $url;
}
/**
* Get other campaigns' assets from the specific url.
*
* @param int $id Post or Term ID.
* @param string $type Only possible values are post or term.
*/
protected function get_asset_group_asset_suggestions( int $id, string $type ): array {
$final_url = $this->get_url( $id, $type );
// Suggest the assets from the first asset group if exists.
$asset_group_assets = $this->asset_group_asset->get_assets_by_final_url( $final_url, true );
if ( empty( $asset_group_assets ) ) {
return [];
}
return array_merge( $this->get_suggestions_common_fields( [] ), [ 'final_url' => $final_url ], $asset_group_assets );
}
/**
* Get assets from specific post or term.
*
* @param int $id Post or Term ID, or self::HOMEPAGE_KEY_ID.
* @param string $type Only possible values are post or term.
*
* @return array All assets available for specific term, post or homepage.
* @throws Exception If the ID is invalid.
*/
protected function get_wp_assets( int $id, string $type ): array {
if ( $type === 'post' ) {
return $this->get_post_assets( $id );
} elseif ( $type === 'term' ) {
return $this->get_term_assets( $id );
} else {
return $this->get_homepage_assets();
}
}
/**
* Get assets from the homepage.
*
* @return array Assets available for the homepage.
* @throws Exception If the homepage id is invalid.
*/
protected function get_homepage_assets(): array {
$home_page = $this->wp->get_static_homepage();
// Static homepage.
if ( $home_page ) {
return $this->get_post_assets( $home_page->ID );
}
// Get images from the latest posts.
$posts = $this->wp->get_posts( [] );
$inserted_images_ids = array_map( [ $this, 'get_html_inserted_images' ], array_column( $posts, 'post_content' ) );
$ids = array_merge( $this->get_post_image_attachments( [ 'post_parent__in' => array_column( $posts, 'ID' ) ] ), ...$inserted_images_ids );
$marketing_images = $this->get_url_attachments_by_ids( $ids );
// Non static homepage.
return array_merge(
[
AssetFieldType::HEADLINE => [ __( 'Homepage', 'google-listings-and-ads' ) ],
AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . __( 'Homepage', 'google-listings-and-ads' ) ],
AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'description' ) ] ),
'display_url_path' => [],
'final_url' => get_bloginfo( 'url' ),
],
$this->get_suggestions_common_fields( $marketing_images )
);
}
/**
* Get assets from specific post.
*
* @param int $id Post ID.
*
* @return array All assets for specific post.
* @throws Exception If the Post ID is invalid.
*/
protected function get_post_assets( int $id ): array {
$post = get_post( $id );
if ( ! $post || $post->post_status === 'trash' ) {
throw new Exception(
/* translators: 1: is an integer representing an unknown Post ID */
sprintf( __( 'Invalid Post ID %1$d', 'google-listings-and-ads' ), $id )
);
}
$attachments_ids = $this->get_post_image_attachments(
[
'post_parent' => $id,
]
);
if ( $id === wc_get_page_id( 'shop' ) ) {
$attachments_ids = [ ...$attachments_ids, ...$this->get_shop_attachments() ];
}
if ( $post->post_type === 'product' || $post->post_type === 'product_variation' ) {
$product = $this->wc->maybe_get_product( $id );
$attachments_ids = [ ...$attachments_ids, ...$product->get_gallery_image_ids() ];
}
$attachments_ids = [ ...$attachments_ids, ...$this->get_gallery_images_ids( $id ), ...$this->get_html_inserted_images( $post->post_content ), get_post_thumbnail_id( $id ) ];
$marketing_images = $this->get_url_attachments_by_ids( $attachments_ids );
$long_headline = get_bloginfo( 'name' ) . ': ' . $post->post_title;
return array_merge(
[
AssetFieldType::HEADLINE => [ $post->post_title ],
AssetFieldType::LONG_HEADLINE => [ $long_headline ],
AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ $post->post_excerpt, get_bloginfo( 'description' ) ] ),
'display_url_path' => [ $post->post_name ],
'final_url' => get_permalink( $id ),
],
$this->get_suggestions_common_fields( $marketing_images )
);
}
/**
* Get assets from specific term.
*
* @param int $id Term ID.
*
* @return array All assets for specific term.
* @throws Exception If the Term ID is invalid.
*/
protected function get_term_assets( int $id ): array {
$term = get_term( $id );
if ( ! $term ) {
throw new Exception(
/* translators: 1: is an integer representing an unknown Term ID */
sprintf( __( 'Invalid Term ID %1$d', 'google-listings-and-ads' ), $id )
);
}
$posts_assigned_to_term = $this->get_posts_assigned_to_a_term( $term->term_id, $term->taxonomy );
$posts_ids_assigned_to_term = [];
$attachments_ids = [];
foreach ( $posts_assigned_to_term as $post ) {
$attachments_ids[] = get_post_thumbnail_id( $post->ID );
$posts_ids_assigned_to_term[] = $post->ID;
}
if ( count( $posts_assigned_to_term ) ) {
$attachments_ids = [ ...$this->get_post_image_attachments( [ 'post_parent__in' => $posts_ids_assigned_to_term ] ), ...$attachments_ids ];
}
$marketing_images = $this->get_url_attachments_by_ids( $attachments_ids );
return array_merge(
[
AssetFieldType::HEADLINE => [ $term->name ],
AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . $term->name ],
AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ wp_strip_all_tags( $term->description ), get_bloginfo( 'description' ) ] ),
'display_url_path' => [ $term->slug ],
'final_url' => get_term_link( $term->term_id ),
],
$this->get_suggestions_common_fields( $marketing_images )
);
}
/**
* Get inserted images from HTML.
*
* @param string $html HTML string.
*
* @return array Array of image IDs.
*/
protected function get_html_inserted_images( string $html ): array {
if ( empty( $html ) ) {
return [];
}
// Malformed HTML can cause DOMDocument to throw warnings. With the below line, we can suppress them and work only with the HTML that has been parsed.
libxml_use_internal_errors( true );
$dom = new DOMDocument();
if ( $dom->loadHTML( $html ) ) {
$images = $dom->getElementsByTagName( 'img' );
$images_ids = [];
$pattern = '/-\d+x\d+\.(jpg|jpeg|png)$/i';
foreach ( $images as $image ) {
$url_unscaled = preg_replace(
$pattern,
'.${1}',
$image->getAttribute( 'src' ),
);
$image_id = attachment_url_to_postid( $url_unscaled );
// Look for scaled image if the original image is not found.
if ( $image_id === 0 ) {
$url_scaled = preg_replace(
$pattern,
'-scaled.${1}',
$image->getAttribute( 'src' ),
);
$image_id = attachment_url_to_postid( $url_scaled );
}
if ( $image_id > 0 ) {
$images_ids[] = $image_id;
}
}
}
return $images_ids;
}
/**
* Get logo images urls.
*
* @return array Logo images urls.
*/
protected function get_logo_images(): array {
$logo_images = $this->get_url_attachments_by_ids( [ get_theme_mod( 'custom_logo' ) ], [ self::LOGO_IMAGE_KEY ] );
return $logo_images[ self::LOGO_IMAGE_KEY ] ?? [];
}
/**
* Get posts linked to a specific term.
*
* @param int $term_id Term ID.
* @param string $taxonomy_name Taxonomy name.
*
* @return array List of posts assigned to the term.
*/
protected function get_posts_assigned_to_a_term( int $term_id, string $taxonomy_name ): array {
$args = [
'post_type' => 'any',
'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
'tax_query' => [
[
'taxonomy' => $taxonomy_name,
'terms' => $term_id,
'field' => 'term_id',
'include_children' => false,
],
],
];
return $this->wp->get_posts( $args );
}
/**
* Get attachments related to the shop page.
*
* @return array Shop attachments.
*/
protected function get_shop_attachments(): array {
return $this->get_post_image_attachments(
[
'post_parent__in' => $this->get_shop_products(),
]
);
}
/**
*
* Get products that will be use to offer image assets.
*
* @param array $args See WP_Query::parse_query() for all available arguments.
* @return array Shop products.
*/
protected function get_shop_products( array $args = [] ): array {
$defaults = [
'post_type' => 'product',
'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
'fields' => 'ids',
];
$args = wp_parse_args( $args, $defaults );
return $this->wp->get_posts( $args );
}
/**
* Get gallery images ids.
*
* @param int $post_id Post ID that contains the gallery.
*
* @return array List of gallery images ids.
*/
protected function get_gallery_images_ids( int $post_id ): array {
$gallery = get_post_gallery( $post_id, false );
if ( ! $gallery || ! isset( $gallery['ids'] ) ) {
return [];
}
return explode( ',', $gallery['ids'] );
}
/**
* Get unique attachments ids converted to int values.
*
* @param array $ids Attachments ids.
* @param int $maximum_images Maximum number of images to return.
*
* @return array List of unique attachments ids converted to int values.
*/
protected function prepare_image_ids( array $ids, int $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array {
$ids = array_unique( ArrayUtil::remove_empty_values( $ids ) );
$ids = array_map( 'intval', $ids );
return array_slice( $ids, 0, $maximum_images );
}
/**
* Get URL for each attachment using an array of attachment ids and a list of subsizes.
*
* @param array $ids Attachments ids.
* @param array $size_keys Image subsize keys.
* @param int $maximum_images Maximum number of images to return.
*
* @return array A list of attachments urls.
*/
protected function get_url_attachments_by_ids( array $ids, array $size_keys = [ self::SQUARE_MARKETING_IMAGE_KEY, self::MARKETING_IMAGE_KEY, self::PORTRAIT_MARKETING_IMAGE_KEY ], $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array {
$ids = $this->prepare_image_ids( $ids, $maximum_images );
$marketing_images = [];
foreach ( $ids as $id ) {
$metadata = wp_get_attachment_metadata( $id );
if ( ! $metadata ) {
continue;
}
foreach ( $size_keys as $size_key ) {
if ( count( $marketing_images[ $size_key ] ?? [] ) >= self::IMAGE_REQUIREMENTS[ $size_key ]['max_qty'] ) {
continue;
}
$minimum_size = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['minimum'] );
$recommended_size = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['recommended'] );
$image_size = new DimensionUtility( $metadata['width'], $metadata['height'] );
$suggested_size = $this->image_utility->recommend_size( $image_size, $recommended_size, $minimum_size );
// If the original size matches the suggested size with a precision of +-1px.
if ( $suggested_size && $suggested_size->equals( $image_size ) ) {
$marketing_images[ $size_key ][] = wp_get_attachment_url( $id );
} elseif ( isset( $metadata['sizes'][ $size_key ] ) ) {
// use the sub size.
$marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key );
} elseif ( $suggested_size && $this->image_utility->maybe_add_subsize_image( $id, $size_key, $suggested_size ) ) {
// use the resized image.
$marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key );
}
}
}
return $marketing_images;
}
/**
* Get Attachmets for specific posts.
*
* @param array $args See WP_Query::parse_query() for all available arguments.
*
* @return array List of attachments
*/
protected function get_post_image_attachments( array $args = [] ): array {
$defaults = [
'post_type' => 'attachment',
'post_mime_type' => [ 'image/jpeg', 'image/png', 'image/jpg' ],
'fields' => 'ids',
'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES,
];
$args = wp_parse_args( $args, $defaults );
return $this->wp->get_posts( $args );
}
/**
* Get posts that can be used to suggest assets
*
* @param string $search The search query.
* @param int $per_page Number of items per page.
* @param int $offset Used in the get_posts query.
*
* @return array formatted post suggestions
*/
protected function get_post_suggestions( string $search, int $per_page, int $offset = 0 ): array {
if ( $per_page <= 0 ) {
return [];
}
$post_suggestions = [];
$excluded_post_types = [ 'attachment' ];
$post_types = $this->wp->get_post_types(
[
'exclude_from_search' => false,
'public' => true,
]
);
// Exclude attachment post_type
$filtered_post_types = array_diff( $post_types, $excluded_post_types );
$args = [
'post_type' => $filtered_post_types,
'posts_per_page' => $per_page,
'post_status' => 'publish',
'search_title' => $search,
'offset' => $offset,
'suppress_filters' => false,
];
add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 );
$posts = $this->wp->get_posts( $args );
remove_filter( 'posts_where', [ $this, 'title_filter' ] );
foreach ( $posts as $post ) {
$post_suggestions[] = $this->format_final_url_response( $post->ID, 'post', $post->post_title, get_permalink( $post->ID ) );
}
return $post_suggestions;
}
/**
* Filter for the posts_where hook, adds WHERE clause to search
* for the 'search_title' parameter in the post titles (when present).
*
* @param string $where The WHERE clause of the query.
* @param WP_Query $wp_query The WP_Query instance (passed by reference).
*
* @return string The updated WHERE clause.
*/
public function title_filter( string $where, WP_Query $wp_query ): string {
$search_title = $wp_query->get( 'search_title' );
if ( $search_title ) {
$title_search = '%' . $this->wpdb->esc_like( $search_title ) . '%';
$where .= $this->wpdb->prepare( " AND `{$this->wpdb->posts}`.`post_title` LIKE %s", $title_search ); // phpcs:ignore WordPress.DB.PreparedSQL
}
return $where;
}
/**
* Get terms that can be used to suggest assets
*
* @param string $search The search query
* @param int $per_page Number of items per page
*
* @return array formatted terms suggestions
*/
protected function get_terms_suggestions( string $search, int $per_page ): array {
$terms_suggestions = [];
// get_terms evaluates $per_page_terms = 0 as a falsy, therefore it will not add the LIMIT clausure returning all the results.
// See: https://github.com/WordPress/WordPress/blob/abe134c2090e84080adc46187884201a4badd649/wp-includes/class-wp-term-query.php#L868
if ( $per_page <= 0 ) {
return [];
}
// Get all taxonomies that are public, show_in_menu = true helps to exclude taxonomies such as "product_shipping_class".
$taxonomies = $this->wp->get_taxonomies(
[
'public' => true,
'show_in_menu' => true,
],
);
$terms = $this->wp->get_terms(
[
'taxonomy' => $taxonomies,
'hide_empty' => false,
'number' => $per_page,
'name__like' => $search,
]
);
foreach ( $terms as $term ) {
$terms_suggestions[] = $this->format_final_url_response( $term->term_id, 'term', $term->name, get_term_link( $term->term_id, $term->taxonomy ) );
}
return $terms_suggestions;
}
/**
* Return a list of final urls that can be used to suggest assets.
*
* @param string $search The search query
* @param int $per_page Number of items per page
* @param string $order_by Order by: type, title, url
*
* @return array final urls with their title, id & type.
*/
public function get_final_url_suggestions( string $search = '', int $per_page = 30, string $order_by = 'title' ): array {
if ( empty( $search ) ) {
return $this->get_defaults_final_url_suggestions();
}
$homepage = [];
// If the search query contains the word "homepage" add the homepage to the results.
if ( strpos( 'homepage', strtolower( $search ) ) !== false ) {
$homepage[] = $this->get_homepage_final_url();
--$per_page;
}
// Split possible results between posts and terms.
$per_page_posts = (int) ceil( $per_page / 2 );
$posts = $this->get_post_suggestions( $search, $per_page_posts );
// Try to get more results using the terms
$per_page_terms = $per_page - count( $posts );
$terms = $this->get_terms_suggestions( $search, $per_page_terms );
$pending_results = $per_page - count( $posts ) - count( $terms );
$more_results = [];
// Try to get more results using posts
if ( $pending_results > 0 && count( $posts ) === $per_page_posts ) {
$more_results = $this->get_post_suggestions( $search, $pending_results, $per_page_posts );
}
$result = array_merge( $homepage, $posts, $terms, $more_results );
return $this->sort_results( $result, $order_by );
}
/**
* Get the final url for the homepage.
*
* @return array final url for the homepage.
*/
protected function get_homepage_final_url(): array {
return $this->format_final_url_response( self::HOMEPAGE_KEY_ID, 'homepage', __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'url' ) );
}
/**
* Get defaults final urls suggestions.
*
* @return array default final urls.
*/
protected function get_defaults_final_url_suggestions(): array {
$defaults = [ $this->get_homepage_final_url() ];
$shop_page = $this->wp->get_shop_page();
if ( $shop_page ) {
$defaults[] = $this->format_final_url_response( $shop_page->ID, 'post', $shop_page->post_title, get_permalink( $shop_page->ID ) );
}
return $defaults;
}
/**
* Order suggestions alphabetically
*
* @param array $results Results as an associative array
* @param string $field Sort by a specific field
*
* @return array response sorted alphabetically
*/
protected function sort_results( array $results, string $field ): array {
usort(
$results,
function ( $a, $b ) use ( $field ) {
return strcmp( strtolower( (string) $a[ $field ] ), strtolower( (string) $b[ $field ] ) );
}
);
return $results;
}
/**
* Return an assotiave array with the page suggestion response format.
*
* @param int $id post id, term id or self::HOMEPAGE_KEY_ID.
* @param string $type post|term
* @param string $title page|term title
* @param string $url page|term url
*
* @return array response formated.
*/
protected function format_final_url_response( int $id, string $type, string $title, string $url ): array {
return [
'id' => $id,
'type' => $type,
'title' => $title,
'url' => $url,
];
}
/**
* Get the suggested common fieds.
*
* @param array $marketing_images Marketing images.
*
* @return array Suggested common fields.
*/
protected function get_suggestions_common_fields( array $marketing_images ): array {
return [
AssetFieldType::LOGO => $this->get_logo_images(),
AssetFieldType::BUSINESS_NAME => get_bloginfo( 'name' ),
AssetFieldType::SQUARE_MARKETING_IMAGE => $marketing_images[ self::SQUARE_MARKETING_IMAGE_KEY ] ?? [],
AssetFieldType::MARKETING_IMAGE => $marketing_images [ self::MARKETING_IMAGE_KEY ] ?? [],
AssetFieldType::PORTRAIT_MARKETING_IMAGE => $marketing_images [ self::PORTRAIT_MARKETING_IMAGE_KEY ] ?? [],
AssetFieldType::CALL_TO_ACTION_SELECTION => null,
];
}
}
GoogleAdsClient.php 0000644 00000002660 15154457752 0010303 0 ustar 00 <?php
declare( strict_types=1 );
/**
* Overrides vendor/googleads/google-ads-php/src/Google/Ads/GoogleAds/Lib/V18/GoogleAdsClient.php
*
* phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
* phpcs:disable WordPress.NamingConventions.ValidVariableName
*/
namespace Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Auth\Credentials\InsecureCredentials;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\GuzzleHttp\Client;
/**
* A Google Ads API client for handling common configuration and OAuth2 settings.
*/
class GoogleAdsClient {
use ServiceClientFactoryTrait;
/** @var Client $httpClient */
private $httpClient = null;
/**
* GoogleAdsClient constructor
*
* @param string $endpoint Endpoint URL to send requests to.
*/
public function __construct( string $endpoint ) {
$this->oAuth2Credential = new InsecureCredentials();
$this->endpoint = $endpoint;
}
/**
* Set a guzzle client to use for requests.
*
* @param Client $client Guzzle client.
*/
public function setHttpClient( Client $client ) {
$this->httpClient = $client;
}
/**
* Build a HTTP Handler to handle the requests.
*/
protected function buildHttpHandler() {
return [ HttpHandlerFactory::build( $this->httpClient ), 'async' ];
}
}
ServiceClientFactoryTrait.php 0000644 00000015464 15154457752 0012401 0 ustar 00 <?php
declare( strict_types=1 );
/**
* Overrides vendor/googleads/google-ads-php/src/Google/Ads/GoogleAds/Lib/V18/ServiceClientFactoryTrait.php
*
* phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
* phpcs:disable WordPress.NamingConventions.ValidVariableName
* phpcs:disable Squiz.Commenting.VariableComment
*/
namespace Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads;
use Google\Ads\GoogleAds\Constants;
use Google\Ads\GoogleAds\Lib\ConfigurationTrait;
use Google\Ads\GoogleAds\V18\Services\Client\AccountLinkServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupAdLabelServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupAdServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupCriterionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdGroupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AdServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AssetGroupListingGroupFilterServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\AssetGroupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\BillingSetupServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignBudgetServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignCriterionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CampaignServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\ConversionActionServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CustomerServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\CustomerUserAccessServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\GeoTargetConstantServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\GoogleAdsServiceClient;
use Google\Ads\GoogleAds\V18\Services\Client\ProductLinkInvitationServiceClient;
/**
* Contains service client factory methods.
*/
trait ServiceClientFactoryTrait {
use ConfigurationTrait;
private static $CREDENTIALS_LOADER_KEY = 'credentials';
private static $DEVELOPER_TOKEN_KEY = 'developer-token';
private static $LOGIN_CUSTOMER_ID_KEY = 'login-customer-id';
private static $LINKED_CUSTOMER_ID_KEY = 'linked-customer-id';
private static $SERVICE_ADDRESS_KEY = 'serviceAddress';
private static $DEFAULT_SERVICE_ADDRESS = 'googleads.googleapis.com';
private static $TRANSPORT_KEY = 'transport';
/**
* Gets the Google Ads client options for making API calls.
*
* @return array the client options
*/
public function getGoogleAdsClientOptions(): array {
$clientOptions = [
self::$CREDENTIALS_LOADER_KEY => $this->getOAuth2Credential(),
self::$DEVELOPER_TOKEN_KEY => '',
self::$TRANSPORT_KEY => 'rest',
'libName' => Constants::LIBRARY_NAME,
'libVersion' => Constants::LIBRARY_VERSION,
];
if ( ! empty( $this->getEndpoint() ) ) {
$clientOptions += [ self::$SERVICE_ADDRESS_KEY => $this->getEndpoint() ];
}
if ( isset( $this->httpClient ) ) {
$clientOptions['transportConfig'] = [
'rest' => [
'httpHandler' => $this->buildHttpHandler(),
],
];
}
return $clientOptions;
}
/**
* @return AccountLinkServiceClient
*/
public function getAccountLinkServiceClient(): AccountLinkServiceClient {
return new AccountLinkServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AdGroupAdLabelServiceClient
*/
public function getAdGroupAdLabelServiceClient(): AdGroupAdLabelServiceClient {
return new AdGroupAdLabelServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AdGroupAdServiceClient
*/
public function getAdGroupAdServiceClient(): AdGroupAdServiceClient {
return new AdGroupAdServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AdGroupCriterionServiceClient
*/
public function getAdGroupCriterionServiceClient(): AdGroupCriterionServiceClient {
return new AdGroupCriterionServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AdGroupServiceClient
*/
public function getAdGroupServiceClient(): AdGroupServiceClient {
return new AdGroupServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AdServiceClient
*/
public function getAdServiceClient(): AdServiceClient {
return new AdServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AssetGroupListingGroupFilterServiceClient
*/
public function getAssetGroupListingGroupFilterServiceClient(): AssetGroupListingGroupFilterServiceClient {
return new AssetGroupListingGroupFilterServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return AssetGroupServiceClient
*/
public function getAssetGroupServiceClient(): AssetGroupServiceClient {
return new AssetGroupServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return BillingSetupServiceClient
*/
public function getBillingSetupServiceClient(): BillingSetupServiceClient {
return new BillingSetupServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return CampaignBudgetServiceClient
*/
public function getCampaignBudgetServiceClient(): CampaignBudgetServiceClient {
return new CampaignBudgetServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return CampaignCriterionServiceClient
*/
public function getCampaignCriterionServiceClient(): CampaignCriterionServiceClient {
return new CampaignCriterionServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return CampaignServiceClient
*/
public function getCampaignServiceClient(): CampaignServiceClient {
return new CampaignServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return ConversionActionServiceClient
*/
public function getConversionActionServiceClient(): ConversionActionServiceClient {
return new ConversionActionServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return CustomerServiceClient
*/
public function getCustomerServiceClient(): CustomerServiceClient {
return new CustomerServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return CustomerUserAccessServiceClient
*/
public function getCustomerUserAccessServiceClient(): CustomerUserAccessServiceClient {
return new CustomerUserAccessServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return GeoTargetConstantServiceClient
*/
public function getGeoTargetConstantServiceClient(): GeoTargetConstantServiceClient {
return new GeoTargetConstantServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return GoogleAdsServiceClient
*/
public function getGoogleAdsServiceClient(): GoogleAdsServiceClient {
return new GoogleAdsServiceClient( $this->getGoogleAdsClientOptions() );
}
/**
* @return ProductLinkInvitationServiceClient
*/
public function getProductLinkInvitationServiceClient(): ProductLinkInvitationServiceClient {
return new ProductLinkInvitationServiceClient( $this->getGoogleAdsClientOptions() );
}
}
AccountController.php 0000644 00000013122 15155676447 0010740 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AccountService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class AccountController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class AccountController extends BaseController {
/**
* Service used to access / update Ads account data.
*
* @var AccountService
*/
protected $account;
/**
* AccountController constructor.
*
* @param RESTServer $server
* @param AccountService $account
*/
public function __construct( RESTServer $server, AccountService $account ) {
parent::__construct( $server );
$this->account = $account;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'ads/accounts',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_accounts_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->create_or_link_account_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'ads/connection',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_connected_ads_account_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->disconnect_ads_account_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
$this->register_route(
'ads/billing-status',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_billing_status_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
$this->register_route(
'ads/account-status',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_ads_account_has_access(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the callback function for the list accounts request.
*
* @return callable
*/
protected function get_accounts_callback(): callable {
return function () {
try {
return new Response( $this->account->get_accounts() );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for creating or linking an account.
*
* @return callable
*/
protected function create_or_link_account_callback(): callable {
return function ( Request $request ) {
try {
$link_id = absint( $request['id'] );
if ( $link_id ) {
$this->account->use_existing_account( $link_id );
}
$account_data = $this->account->setup_account();
return $this->prepare_item_for_response( $account_data, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for the connected ads account.
*
* @return callable
*/
protected function get_connected_ads_account_callback(): callable {
return function () {
return $this->account->get_connected_account();
};
}
/**
* Get the callback function for disconnecting a merchant.
*
* @return callable
*/
protected function disconnect_ads_account_callback(): callable {
return function () {
$this->account->disconnect();
return [
'status' => 'success',
'message' => __( 'Successfully disconnected.', 'google-listings-and-ads' ),
];
};
}
/**
* Get the callback function for retrieving the billing setup status.
*
* @return callable
*/
protected function get_billing_status_callback(): callable {
return function () {
return $this->account->get_billing_status();
};
}
/**
* Get the callback function for retrieving the account access status for ads.
*
* @return callable
*/
protected function get_ads_account_has_access(): callable {
return function () {
try {
return $this->account->get_ads_account_has_access();
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'number',
'description' => __( 'Google Ads Account ID.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => false,
],
'billing_url' => [
'type' => 'string',
'description' => __( 'Billing Flow URL.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'account';
}
}
AssetGroupController.php 0000644 00000022143 15155676450 0011435 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsAssetGroup;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType;
use WP_REST_Request as Request;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class for handling API requests related to the asset groups.
* See https://developers.google.com/google-ads/api/reference/rpc/v18/AssetGroup
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class AssetGroupController extends BaseController {
/**
* The AdsAssetGroup class.
*
* @var AdsAssetGroup $ads_asset_group
*/
protected $ads_asset_group;
/**
* AssetGroupController constructor.
*
* @param RESTServer $rest_server
* @param AdsAssetGroup $ads_asset_group
*/
public function __construct( RESTServer $rest_server, AdsAssetGroup $ads_asset_group ) {
parent::__construct( $rest_server );
$this->ads_asset_group = $ads_asset_group;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'ads/campaigns/asset-groups/(?P<id>[\d]+)',
[
[
'methods' => TransportMethods::EDITABLE,
'callback' => $this->edit_asset_group_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->edit_asset_group_params(),
],
]
);
$this->register_route(
'ads/campaigns/asset-groups',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_asset_groups_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_asset_group_params(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->create_asset_group_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_asset_group_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the schema for the asset group.
*
* @return array The asset group schema.
*/
public function get_asset_group_fields(): array {
return [
'final_url' => [
'type' => 'string',
'description' => __( 'Final URL.', 'google-listings-and-ads' ),
],
'path1' => [
'type' => 'string',
'description' => __( 'Asset Group path 1.', 'google-listings-and-ads' ),
],
'path2' => [
'type' => 'string',
'description' => __( 'Asset Group path 2.', 'google-listings-and-ads' ),
],
];
}
/**
* Get the edit asset group params params to update an asset group.
*
* @return array The edit asset group params.
*/
public function edit_asset_group_params(): array {
return array_merge(
[
'id' => [
'description' => __( 'Asset Group ID.', 'google-listings-and-ads' ),
'type' => 'integer',
'required' => true,
],
'assets' => [
'type' => 'array',
'description' => __( 'List of asset to be edited.', 'google-listings-and-ads' ),
'items' => $this->get_schema_asset(),
'default' => [],
],
],
$this->get_asset_group_fields()
);
}
/**
* Get the assets groups params.
*
* @return array
*/
public function get_asset_group_params(): array {
return [
'campaign_id' => [
'description' => __( 'Campaign ID.', 'google-listings-and-ads' ),
'type' => 'integer',
'validate_callback' => 'rest_validate_request_arg',
'required' => true,
],
];
}
/**
* Get Asset Groups by Campaign ID.
*
* @return callable
*/
protected function get_asset_groups_callback(): callable {
return function ( Request $request ) {
try {
$campaign_id = $request->get_param( 'campaign_id' );
return array_map(
function ( $item ) use ( $request ) {
$data = $this->prepare_item_for_response( $item, $request );
return $this->prepare_response_for_collection( $data );
},
$this->ads_asset_group->get_asset_groups_by_campaign_id( $campaign_id )
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Create asset group.
*
* @return callable
*/
public function create_asset_group_callback(): callable {
return function ( Request $request ) {
try {
$asset_group_id = $this->ads_asset_group->create_asset_group( $request->get_param( 'campaign_id' ) );
return [
'status' => 'success',
'message' => __( 'Successfully created asset group.', 'google-listings-and-ads' ),
'id' => $asset_group_id,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Edit asset group.
*
* @return callable
*/
public function edit_asset_group_callback(): callable {
return function ( Request $request ) {
try {
$asset_group_fields = array_intersect_key(
$request->get_params(),
$this->get_asset_group_fields()
);
if ( empty( $asset_group_fields ) && empty( $request->get_param( 'assets' ) ) ) {
throw new Exception( __( 'No asset group fields to update.', 'google-listings-and-ads' ) );
}
$asset_group_id = $this->ads_asset_group->edit_asset_group( $request->get_param( 'id' ), $asset_group_fields, $request->get_param( 'assets' ) );
return [
'status' => 'success',
'message' => __( 'Successfully edited asset group.', 'google-listings-and-ads' ),
'id' => $asset_group_id,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'number',
'description' => __( 'Asset Group ID', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'final_url' => [
'type' => 'string',
'description' => __( 'Final URL', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'display_url_path' => [
'type' => 'array',
'description' => __( 'Text that may appear appended to the url displayed in the ad.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'assets' => [
'type' => 'array',
'description' => __( 'Asset is a part of an ad which can be shared across multiple ads. It can be an image, headlines, descriptions, etc.', 'google-listings-and-ads' ),
'items' => [
'type' => 'object',
'properties' => [
AssetFieldType::SQUARE_MARKETING_IMAGE => $this->get_schema_field_type_asset(),
AssetFieldType::MARKETING_IMAGE => $this->get_schema_field_type_asset(),
AssetFieldType::PORTRAIT_MARKETING_IMAGE => $this->get_schema_field_type_asset(),
AssetFieldType::LOGO => $this->get_schema_field_type_asset(),
AssetFieldType::BUSINESS_NAME => $this->get_schema_field_type_asset(),
AssetFieldType::HEADLINE => $this->get_schema_field_type_asset(),
AssetFieldType::DESCRIPTION => $this->get_schema_field_type_asset(),
AssetFieldType::LONG_HEADLINE => $this->get_schema_field_type_asset(),
AssetFieldType::CALL_TO_ACTION_SELECTION => $this->get_schema_field_type_asset(),
],
],
],
];
}
/**
* Get the item schema for the field type asset.
*
* @return array the field type asset schema.
*/
protected function get_schema_field_type_asset(): array {
return [
'type' => 'array',
'items' => $this->get_schema_asset(),
'required' => false,
];
}
/**
* Get the item schema for the asset.
*
* @return array
*/
protected function get_schema_asset() {
return [
'type' => 'object',
'properties' => [
'id' => [
'type' => [ 'integer', 'null' ],
'description' => __( 'Asset ID', 'google-listings-and-ads' ),
],
'content' => [
'type' => [ 'string', 'null' ],
'description' => __( 'Asset content', 'google-listings-and-ads' ),
],
'field_type' => [
'type' => 'string',
'description' => __( 'Asset field type', 'google-listings-and-ads' ),
'required' => true,
'context' => [ 'edit' ],
'enum' => [
AssetFieldType::HEADLINE,
AssetFieldType::LONG_HEADLINE,
AssetFieldType::DESCRIPTION,
AssetFieldType::BUSINESS_NAME,
AssetFieldType::MARKETING_IMAGE,
AssetFieldType::SQUARE_MARKETING_IMAGE,
AssetFieldType::LOGO,
AssetFieldType::CALL_TO_ACTION_SELECTION,
AssetFieldType::PORTRAIT_MARKETING_IMAGE,
],
],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'asset-group';
}
}
AssetSuggestionsController.php 0000644 00000014135 15155676450 0012655 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AssetSuggestionsService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class AssetSuggestionsController
*
* @since 2.4.0
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class AssetSuggestionsController extends BaseController {
/**
* Service used to populate ads suggestions data.
*
* @var AssetSuggestionsService
*/
protected $asset_suggestions_service;
/**
* AssetSuggestionsController constructor.
*
* @param RESTServer $server
* @param AssetSuggestionsService $asset_suggestions
*/
public function __construct( RESTServer $server, AssetSuggestionsService $asset_suggestions ) {
parent::__construct( $server );
$this->asset_suggestions_service = $asset_suggestions;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'assets/suggestions',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_assets_suggestions_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_assets_suggestions_params(),
],
]
);
$this->register_route(
'assets/final-url/suggestions',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_final_url_suggestions_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
return [
'search' => [
'description' => __( 'Search for post title or term name', 'google-listings-and-ads' ),
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
],
'per_page' => [
'description' => __( 'The number of items to be return', 'google-listings-and-ads' ),
'type' => 'number',
'default' => 30,
'sanitize_callback' => 'absint',
'minimum' => 1,
'validate_callback' => 'rest_validate_request_arg',
],
'order_by' => [
'description' => __( 'Sort retrieved items by parameter', 'google-listings-and-ads' ),
'type' => 'string',
'default' => 'title',
'sanitize_callback' => 'sanitize_text_field',
'enum' => [ 'type', 'title', 'url' ],
'validate_callback' => 'rest_validate_request_arg',
],
];
}
/**
* Get the assets suggestions params.
*
* @return array
*/
public function get_assets_suggestions_params(): array {
return [
'id' => [
'description' => __( 'Post ID or Term ID.', 'google-listings-and-ads' ),
'type' => 'number',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'required' => true,
],
'type' => [
'description' => __( 'Type linked to the id.', 'google-listings-and-ads' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'enum' => [ 'post', 'term', 'homepage' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => true,
],
];
}
/**
* Get the callback function for the assets suggestions request.
*
* @return callable
*/
protected function get_assets_suggestions_callback(): callable {
return function ( Request $request ) {
try {
$id = $request->get_param( 'id' );
$type = $request->get_param( 'type' );
return $this->asset_suggestions_service->get_assets_suggestions( $id, $type );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for the list of final-url suggestions request.
*
* @return callable
*/
protected function get_final_url_suggestions_callback(): callable {
return function ( Request $request ) {
$search = $request->get_param( 'search' );
$per_page = $request->get_param( 'per_page' );
$order_by = $request->get_param( 'order_by' );
return array_map(
function ( $item ) use ( $request ) {
$data = $this->prepare_item_for_response( $item, $request );
return $this->prepare_response_for_collection( $data );
},
$this->asset_suggestions_service->get_final_url_suggestions( $search, $per_page, $order_by )
);
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'number',
'description' => __( 'Post ID or Term ID', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'type' => [
'type' => 'string',
'description' => __( 'Post, term or homepage', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'enum' => [ 'post', 'term', 'homepage' ],
'readonly' => true,
],
'title' => [
'type' => 'string',
'description' => __( 'The post or term title', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'url' => [
'type' => 'string',
'description' => __( 'The URL linked to the post/term', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'asset_final_url_suggestions';
}
}
BudgetRecommendationController.php 0000644 00000012206 15155676450 0013437 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\DB\Query\BudgetRecommendationQuery;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class BudgetRecommendationController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class BudgetRecommendationController extends BaseController implements ISO3166AwareInterface {
use CountryCodeTrait;
/**
* @var BudgetRecommendationQuery
*/
protected $budget_recommendation_query;
/**
* @var Ads
*/
protected $ads;
/**
* BudgetRecommendationController constructor.
*
* @param RESTServer $rest_server
* @param BudgetRecommendationQuery $budget_recommendation_query
* @param Ads $ads
*/
public function __construct( RESTServer $rest_server, BudgetRecommendationQuery $budget_recommendation_query, Ads $ads ) {
parent::__construct( $rest_server );
$this->budget_recommendation_query = $budget_recommendation_query;
$this->ads = $ads;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'ads/campaigns/budget-recommendation',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_budget_recommendation_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
return [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
'country_codes' => [
'type' => 'array',
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_country_code_validate_callback(),
'items' => [
'type' => 'string',
],
'required' => true,
'minItems' => 1,
],
];
}
/**
* @return callable
*/
protected function get_budget_recommendation_callback(): callable {
return function ( Request $request ) {
$country_codes = $request->get_param( 'country_codes' );
$currency = $this->ads->get_ads_currency();
if ( ! $currency ) {
return new Response(
[
'message' => __( 'No currency available for the Ads account.', 'google-listings-and-ads' ),
'currency' => $currency,
'country_codes' => $country_codes,
],
400
);
}
$recommendations = $this
->budget_recommendation_query
->where( 'country', $country_codes, 'IN' )
->where( 'currency', $currency )
->get_results();
if ( ! $recommendations ) {
return new Response(
[
'message' => __( 'Cannot find any budget recommendations.', 'google-listings-and-ads' ),
'currency' => $currency,
'country_codes' => $country_codes,
],
404
);
}
$returned_recommendations = array_map(
function ( $recommendation ) {
return [
'country' => $recommendation['country'],
'daily_budget' => (int) $recommendation['daily_budget'],
];
},
$recommendations
);
return $this->prepare_item_for_response(
[
'currency' => $currency,
'recommendations' => $returned_recommendations,
],
$request
);
};
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'currency' => [
'type' => 'string',
'description' => __( 'The currency to use for the shipping rate.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'validate_callback' => 'rest_validate_request_arg',
],
'recommendations' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'country' => [
'type' => 'string',
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'daily_budget' => [
'type' => 'number',
'description' => __( 'The recommended daily budget for a country.', 'google-listings-and-ads' ),
],
],
],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'budget-recommendation';
}
}
CampaignController.php 0000644 00000030775 15155676450 0011072 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaign;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignType;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\CountryCodeTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GoogleHelperAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ISO3166AwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use DateTime;
use Exception;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class CampaignController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class CampaignController extends BaseController implements GoogleHelperAwareInterface, ISO3166AwareInterface {
use CountryCodeTrait;
/**
* @var AdsCampaign
*/
protected $ads_campaign;
/**
* CampaignController constructor.
*
* @param RESTServer $server
* @param AdsCampaign $ads_campaign
*/
public function __construct( RESTServer $server, AdsCampaign $ads_campaign ) {
parent::__construct( $server );
$this->ads_campaign = $ads_campaign;
}
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'ads/campaigns',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_campaigns_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->create_campaign_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_schema_properties(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'ads/campaigns/(?P<id>[\d]+)',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_campaign_callback(),
'permission_callback' => $this->get_permission_callback(),
],
[
'methods' => TransportMethods::EDITABLE,
'callback' => $this->edit_campaign_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_edit_schema(),
],
[
'methods' => TransportMethods::DELETABLE,
'callback' => $this->delete_campaign_callback(),
'permission_callback' => $this->get_permission_callback(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for listing campaigns.
*
* @return callable
*/
protected function get_campaigns_callback(): callable {
return function ( Request $request ) {
try {
$exclude_removed = $request->get_param( 'exclude_removed' );
return array_map(
function ( $campaign ) use ( $request ) {
$data = $this->prepare_item_for_response( $campaign, $request );
return $this->prepare_response_for_collection( $data );
},
$this->ads_campaign->get_campaigns( $exclude_removed, true, $request->get_params() )
);
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for creating a campaign.
*
* @return callable
*/
protected function create_campaign_callback(): callable {
return function ( Request $request ) {
try {
$fields = array_intersect_key( $request->get_json_params(), $this->get_schema_properties() );
// Set the default value of campaign name.
if ( empty( $fields['name'] ) ) {
$current_date_time = ( new DateTime( 'now', wp_timezone() ) )->format( 'Y-m-d H:i:s' );
$fields['name'] = sprintf(
/* translators: %s: current date time. */
__( 'Campaign %s', 'google-listings-and-ads' ),
$current_date_time
);
}
$campaign = $this->ads_campaign->create_campaign( $fields );
/**
* When a campaign has been successfully created.
*
* @event gla_created_campaign
* @property int id Campaign ID.
* @property string status Campaign status, `enabled` or `paused`.
* @property string name Campaign name, generated based on date.
* @property float amount Campaign budget.
* @property string country Base target country code.
* @property string targeted_locations Additional target country codes.
* @property string source The source of the campaign creation.
*/
do_action(
'woocommerce_gla_track_event',
'created_campaign',
[
'id' => $campaign['id'],
'status' => $campaign['status'],
'name' => $campaign['name'],
'amount' => $campaign['amount'],
'country' => $campaign['country'],
'targeted_locations' => join( ',', $campaign['targeted_locations'] ),
'source' => $fields['label'] ?? '',
]
);
return $this->prepare_item_for_response( $campaign, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for listing a single campaign.
*
* @return callable
*/
protected function get_campaign_callback(): callable {
return function ( Request $request ) {
try {
$id = absint( $request['id'] );
$campaign = $this->ads_campaign->get_campaign( $id );
if ( empty( $campaign ) ) {
return new Response(
[
'message' => __( 'Campaign is not available.', 'google-listings-and-ads' ),
'id' => $id,
],
404
);
}
return $this->prepare_item_for_response( $campaign, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for editing a campaign.
*
* @return callable
*/
protected function edit_campaign_callback(): callable {
return function ( Request $request ) {
try {
$fields = array_intersect_key( $request->get_json_params(), $this->get_edit_schema() );
if ( empty( $fields ) ) {
return new Response(
[
'status' => 'invalid_data',
'message' => __( 'Invalid edit data.', 'google-listings-and-ads' ),
],
400
);
}
$campaign_id = $this->ads_campaign->edit_campaign( absint( $request['id'] ), $fields );
/**
* When a campaign has been successfully edited.
*
* @event gla_edited_campaign
* @property int id Campaign ID.
* @property string status Campaign status, `enabled` or `paused`.
* @property string name Campaign name, generated based on date.
* @property float amount Campaign budget.
*/
do_action(
'woocommerce_gla_track_event',
'edited_campaign',
array_merge(
[
'id' => $campaign_id,
],
$fields,
)
);
return [
'status' => 'success',
'message' => __( 'Successfully edited campaign.', 'google-listings-and-ads' ),
'id' => $campaign_id,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for deleting a campaign.
*
* @return callable
*/
protected function delete_campaign_callback(): callable {
return function ( Request $request ) {
try {
$deleted_id = $this->ads_campaign->delete_campaign( absint( $request['id'] ) );
/**
* When a campaign has been successfully deleted.
*
* @event gla_deleted_campaign
* @property int id Campaign ID.
*/
do_action(
'woocommerce_gla_track_event',
'deleted_campaign',
[
'id' => $deleted_id,
]
);
return [
'status' => 'success',
'message' => __( 'Successfully deleted campaign.', 'google-listings-and-ads' ),
'id' => $deleted_id,
];
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the schema for fields we are allowed to edit.
*
* @return array
*/
protected function get_edit_schema(): array {
$allowed = [
'name',
'status',
'amount',
];
$fields = array_intersect_key( $this->get_schema_properties(), array_flip( $allowed ) );
// Unset required to allow editing individual fields.
array_walk(
$fields,
function ( &$value ) {
unset( $value['required'] );
}
);
return $fields;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
return [
'exclude_removed' => [
'description' => __( 'Exclude removed campaigns.', 'google-listings-and-ads' ),
'type' => 'boolean',
'default' => true,
'validate_callback' => 'rest_validate_request_arg',
],
'per_page' => [
'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ),
'type' => 'integer',
'minimum' => 1,
'maximum' => 10000,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
];
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'id' => [
'type' => 'integer',
'description' => __( 'ID number.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
'readonly' => true,
],
'name' => [
'type' => 'string',
'description' => __( 'Descriptive campaign name.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => false,
],
'status' => [
'type' => 'string',
'enum' => CampaignStatus::labels(),
'description' => __( 'Campaign status.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
],
'type' => [
'type' => 'string',
'enum' => CampaignType::labels(),
'description' => __( 'Campaign type.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
],
'amount' => [
'type' => 'number',
'description' => __( 'Daily budget amount in the local currency.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => true,
],
'country' => [
'type' => 'string',
'description' => __( 'Country code of sale country in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_supported_country_code_validate_callback(),
'readonly' => true,
],
'targeted_locations' => [
'type' => 'array',
'description' => __( 'The locations that an Ads campaign is targeting in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
'sanitize_callback' => $this->get_country_code_sanitize_callback(),
'validate_callback' => $this->get_supported_country_code_validate_callback(),
'required' => true,
'minItems' => 1,
'items' => [
'type' => 'string',
],
],
'label' => [
'type' => 'string',
'description' => __( 'The name of the label to assign to the campaign.', 'google-listings-and-ads' ),
'context' => [ 'edit' ],
'validate_callback' => 'rest_validate_request_arg',
'required' => false,
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'campaign';
}
}
ReportsController.php 0000644 00000015173 15155676450 0011004 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsReport;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CampaignStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseReportsController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Exception;
use WP_REST_Request as Request;
defined( 'ABSPATH' ) || exit;
/**
* Class ReportsController
*
* ContainerAware used for:
* - AdsReport
* - WP (in parent class)
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class ReportsController extends BaseReportsController {
/**
* Register rest routes with WordPress.
*/
public function register_routes(): void {
$this->register_route(
'ads/reports/programs',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_programs_report_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
$this->register_route(
'ads/reports/products',
[
[
'methods' => TransportMethods::READABLE,
'callback' => $this->get_products_report_callback(),
'permission_callback' => $this->get_permission_callback(),
'args' => $this->get_collection_params(),
],
'schema' => $this->get_api_response_schema_callback(),
]
);
}
/**
* Get the callback function for the programs report request.
*
* @return callable
*/
protected function get_programs_report_callback(): callable {
return function ( Request $request ) {
try {
/** @var AdsReport $ads */
$ads = $this->container->get( AdsReport::class );
$data = $ads->get_report_data( 'campaigns', $this->prepare_query_arguments( $request ) );
return $this->prepare_item_for_response( $data, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the callback function for the products report request.
*
* @return callable
*/
protected function get_products_report_callback(): callable {
return function ( Request $request ) {
try {
/** @var AdsReport $ads */
$ads = $this->container->get( AdsReport::class );
$data = $ads->get_report_data( 'products', $this->prepare_query_arguments( $request ) );
return $this->prepare_item_for_response( $data, $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
}
};
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params(): array {
$params = parent::get_collection_params();
$params['interval'] = [
'description' => __( 'Time interval to use for segments in the returned data.', 'google-listings-and-ads' ),
'type' => 'string',
'enum' => [
'day',
'week',
'month',
'quarter',
'year',
],
'validate_callback' => 'rest_validate_request_arg',
];
return $params;
}
/**
* Get the item schema for the controller.
*
* @return array
*/
protected function get_schema_properties(): array {
return [
'products' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => __( 'Product ID.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'name' => [
'type' => 'string',
'description' => __( 'Product name.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
],
'subtotals' => $this->get_totals_schema(),
],
],
],
'campaigns' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'integer',
'description' => __( 'ID number.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'name' => [
'type' => 'string',
'description' => __( 'Campaign name.', 'google-listings-and-ads' ),
'context' => [ 'view', 'edit' ],
],
'status' => [
'type' => 'string',
'enum' => CampaignStatus::labels(),
'description' => __( 'Campaign status.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'isConverted' => [
'type' => 'boolean',
'description' => __( 'Whether the campaign has been converted', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'subtotals' => $this->get_totals_schema(),
],
],
],
'intervals' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'interval' => [
'type' => 'string',
'description' => __( 'ID of this report segment.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'subtotals' => $this->get_totals_schema(),
],
],
],
'totals' => $this->get_totals_schema(),
'next_page' => [
'type' => 'string',
'description' => __( 'Token to retrieve the next page of results.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
];
}
/**
* Return schema for total fields.
*
* @return array
*/
protected function get_totals_schema(): array {
return [
'type' => 'object',
'properties' => [
'clicks' => [
'type' => 'integer',
'description' => __( 'Clicks.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'impressions' => [
'type' => 'integer',
'description' => __( 'Impressions.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'sales' => [
'type' => 'number',
'description' => __( 'Sales amount.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'spend' => [
'type' => 'number',
'description' => __( 'Spend amount.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
'conversions' => [
'type' => 'number',
'description' => __( 'Conversions.', 'google-listings-and-ads' ),
'context' => [ 'view' ],
],
],
];
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'reports';
}
}
SetupCompleteController.php 0000644 00000005045 15155676450 0012134 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\MerchantMetrics;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\EmptySchemaPropertiesTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\RESTServer;
use WP_REST_Request as Request;
use WP_REST_Response as Response;
defined( 'ABSPATH' ) || exit;
/**
* Class SetupCompleteController
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads
*/
class SetupCompleteController extends BaseController {
use EmptySchemaPropertiesTrait;
/**
* Service used to access metrics from the Ads Account.
*
* @var MerchantMetrics
*/
protected $metrics;
/**
* SetupCompleteController constructor.
*
* @param RESTServer $server
* @param MerchantMetrics $metrics
*/
public function __construct( RESTServer $server, MerchantMetrics $metrics ) {
parent::__construct( $server );
$this->metrics = $metrics;
}
/**
* Registers the routes for the objects of the controller.
*/
public function register_routes() {
$this->register_route(
'ads/setup/complete',
[
[
'methods' => TransportMethods::CREATABLE,
'callback' => $this->get_setup_complete_callback(),
'permission_callback' => $this->get_permission_callback(),
],
]
);
}
/**
* Get the callback function for marking setup complete.
*
* @return callable
*/
protected function get_setup_complete_callback(): callable {
return function ( Request $request ) {
do_action( 'woocommerce_gla_ads_setup_completed' );
/**
* Ads onboarding has been successfully completed.
*
* @event gla_ads_setup_completed
* @property int campaign_count Number of campaigns for the connected Ads account.
*/
do_action(
'woocommerce_gla_track_event',
'ads_setup_completed',
[
'campaign_count' => $this->metrics->get_campaign_count(),
]
);
return new Response(
[
'status' => 'success',
'message' => __( 'Successfully marked Ads setup as completed.', 'google-listings-and-ads' ),
]
);
};
}
/**
* Get the item schema name for the controller.
*
* Used for building the API response schema.
*
* @return string
*/
protected function get_schema_title(): string {
return 'ads_setup_complete';
}
}