register_scopes_hook();
$this->register_screen_hook();
add_filter(
'option_' . self::OPTION,
function( $option ) {
$option = (array) $option;
/**
* Filters the AdSense account ID to use.
*
* @since 1.0.0
*
* @param string $account_id Empty by default, will fall back to the option value if not set.
*/
$account_id = apply_filters( 'googlesitekit_adsense_account_id', '' );
if ( ! empty( $account_id ) ) {
$option['accountID'] = $account_id;
}
/**
* Migrate 'adsenseTagEnabled' to 'useSnippet'.
*/
if ( ! isset( $option['useSnippet'] ) && isset( $option['adsenseTagEnabled'] ) ) {
$option['useSnippet'] = (bool) $option['adsenseTagEnabled'];
}
// Ensure the old key is removed regardless. No-op if not set.
unset( $option['adsenseTagEnabled'] );
/**
* Enable the snippet by default.
*/
if ( ! isset( $option['useSnippet'] ) ) {
$option['useSnippet'] = true;
}
return $option;
}
);
add_action( // For non-AMP, plus AMP Native and Transitional.
'wp_head',
function() {
$this->output_adsense_script();
}
);
add_filter( // For AMP Reader, and AMP Native and Transitional (as fallback).
'the_content',
function( $content ) {
return $this->amp_content_add_auto_ads( $content );
}
);
add_filter( // Load amp-auto-ads component for AMP Reader.
'amp_post_template_data',
function( $data ) {
return $this->amp_data_load_auto_ads_component( $data );
}
);
if ( $this->is_connected() ) {
remove_filter( 'option_googlesitekit_analytics_adsense_linked', '__return_false' );
}
}
/**
* Gets required Google OAuth scopes for the module.
*
* @since 1.0.0
*
* @return array List of Google OAuth scopes.
*/
public function get_scopes() {
return array(
'https://www.googleapis.com/auth/adsense',
);
}
/**
* Returns all module information data for passing it to JavaScript.
*
* @since 1.0.0
*
* @return array Module information data.
*/
public function prepare_info_for_js() {
$info = parent::prepare_info_for_js();
$info['provides'] = array(
__( 'Monetize your website', 'google-site-kit' ),
__( 'Intelligent, automatic ad placement', 'google-site-kit' ),
);
$info['settings'] = $this->options->get( self::OPTION );
// Clear datapoints that don't need to be localized.
$idenfifier_args = array(
'source' => 'site-kit',
'url' => rawurlencode( $this->context->get_reference_site_url() ),
);
$signup_args = array(
'utm_source' => 'site-kit',
'utm_medium' => 'wordpress_signup',
);
$info['accountURL'] = add_query_arg( $idenfifier_args, $this->get_data( 'account-url' ) );
$info['signupURL'] = add_query_arg( $signup_args, $info['accountURL'] );
$info['rootURL'] = add_query_arg( $idenfifier_args, 'https://www.google.com/adsense/' );
return $info;
}
/**
* Checks whether the module is connected.
*
* A module being connected means that all steps required as part of its activation are completed.
*
* @since 1.0.0
*
* @return bool True if module is connected, false otherwise.
*/
public function is_connected() {
$settings = (array) $this->options->get( self::OPTION );
// TODO: Remove the latter at some point as it's here for back-compat.
if ( empty( $settings['setupComplete'] ) && empty( $settings['setup_complete'] ) ) {
return false;
}
return parent::is_connected();
}
/**
* Cleans up when the module is deactivated.
*
* @since 1.0.0
*/
public function on_deactivation() {
$this->options->delete( self::OPTION );
}
/**
* Adds the AdSense script tag as soon as the client id is available.
*
* Used for account verification and ad display.
*
* @since 1.0.0
*/
protected function output_adsense_script() {
// Bail early if we are checking for the tag presence from the back end.
$tag_verify = ! empty( $_GET['tagverify'] ) ? true : false; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification
if ( $tag_verify ) {
return;
}
// Bail if we don't have a client ID.
$client_id = $this->get_data( 'client-id' );
if ( is_wp_error( $client_id ) || ! $client_id ) {
return;
}
$tag_enabled = $this->get_data( 'use-snippet' );
// If we have client id default behaviour should be placing the tag unless the user has opted out.
if ( false === $tag_enabled ) {
return;
}
// On AMP, preferably use the new 'wp_body_open' hook, falling back to 'the_content' below.
if ( $this->context->is_amp() ) {
add_action(
'wp_body_open',
function() use ( $client_id ) {
if ( $this->adsense_tag_printed ) {
return;
}
?>
adsense_tag_printed = true;
},
-9999
);
return;
}
if ( $this->adsense_tag_printed ) {
return;
}
// If we haven't completed the account connection yet, we still insert the AdSense tag
// because it is required for account verification.
?>
adsense_tag_printed = true;
}
/**
* Adds AMP auto ads script if opted in.
*
* This only affects AMP Reader mode, the others are automatically covered.
*
* @since 1.0.0
*
* @param array $data AMP template data.
* @return array Filtered $data.
*/
protected function amp_data_load_auto_ads_component( $data ) {
if ( ! $this->is_connected() ) {
return $data;
}
$tag_enabled = $this->get_data( 'use-snippet' );
if ( is_wp_error( $tag_enabled ) || ! $tag_enabled ) {
return $data;
}
$client_id = $this->get_data( 'client-id' );
if ( is_wp_error( $client_id ) || ! $client_id ) {
return $data;
}
$data['amp_component_scripts']['amp-auto-ads'] = 'https://cdn.ampproject.org/v0/amp-auto-ads-0.1.js';
return $data;
}
/**
* Adds the AMP auto ads tag if opted in.
*
* @since 1.0.0
*
* @param string $content The page content.
* @return string Filtered $content.
*/
protected function amp_content_add_auto_ads( $content ) {
if ( ! $this->context->is_amp() ) {
return $content;
}
if ( ! $this->is_connected() ) {
return $content;
}
$tag_enabled = $this->get_data( 'use-snippet' );
if ( is_wp_error( $tag_enabled ) || ! $tag_enabled ) {
return $content;
}
$client_id = $this->get_data( 'client-id' );
if ( is_wp_error( $client_id ) || ! $client_id ) {
return $content;
}
if ( $this->adsense_tag_printed ) {
return $content;
}
$this->adsense_tag_printed = true;
return ' ' . $content;
}
/**
* Returns the mapping between available datapoints and their services.
*
* @since 1.0.0
*
* @return array Associative array of $datapoint => $service_identifier pairs.
*/
protected function get_datapoint_services() {
return array(
// GET / POST.
'connection' => '',
'account-id' => '',
'client-id' => '',
'use-snippet' => '',
'account-status' => '',
// GET.
'account-url' => '',
'reports-url' => '',
'notifications' => '',
'accounts' => 'adsense',
'alerts' => 'adsense',
'clients' => 'adsense',
'urlchannels' => 'adsense',
'earnings' => 'adsense',
// POST.
'setup-complete' => '',
);
}
/**
* Creates a request object for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
*
* @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure.
*/
protected function create_data_request( Data_Request $data ) {
$method = $data->method;
$datapoint = $data->datapoint;
if ( 'GET' === $method ) {
switch ( $datapoint ) {
case 'connection':
return function() {
$option = (array) $this->options->get( self::OPTION );
// TODO: Remove this at some point (migration of old options).
if ( isset( $option['account_id'] ) || isset( $option['client_id'] ) || isset( $option['account_status'] ) ) {
if ( isset( $option['account_id'] ) ) {
if ( ! isset( $option['accountID'] ) ) {
$option['accountID'] = $option['account_id'];
}
unset( $option['account_id'] );
}
if ( isset( $option['client_id'] ) ) {
if ( ! isset( $option['clientID'] ) ) {
$option['clientID'] = $option['client_id'];
}
unset( $option['client_id'] );
}
if ( isset( $option['account_status'] ) ) {
if ( ! isset( $option['accountStatus'] ) ) {
$option['accountStatus'] = $option['account_status'];
}
unset( $option['account_status'] );
}
$this->options->set( self::OPTION, $option );
}
// TODO: Remove this at some point (migration of old 'accountId' option).
if ( isset( $option['accountId'] ) ) {
if ( ! isset( $option['accountID'] ) ) {
$option['accountID'] = $option['accountId'];
}
unset( $option['accountId'] );
}
// TODO: Remove this at some point (migration of old 'clientId' option).
if ( isset( $option['clientId'] ) ) {
if ( ! isset( $option['clientID'] ) ) {
$option['clientID'] = $option['clientId'];
}
unset( $option['clientId'] );
}
$defaults = array(
'accountID' => '',
'clientID' => '',
'accountStatus' => '',
);
return array_intersect_key( array_merge( $defaults, $option ), $defaults );
};
case 'account-id':
return function() {
$option = (array) $this->options->get( self::OPTION );
// TODO: Remove this at some point (migration of old option).
if ( isset( $option['account_id'] ) ) {
if ( ! isset( $option['accountID'] ) ) {
$option['accountID'] = $option['account_id'];
}
unset( $option['account_id'] );
$this->options->set( self::OPTION, $option );
}
if ( empty( $option['accountID'] ) ) {
return new WP_Error( 'account_id_not_set', __( 'AdSense account ID not set.', 'google-site-kit' ), array( 'status' => 404 ) );
}
return $option['accountID'];
};
case 'client-id':
return function() {
$option = (array) $this->options->get( self::OPTION );
// TODO: Remove this at some point (migration of old option).
if ( isset( $option['client_id'] ) ) {
if ( ! isset( $option['clientID'] ) ) {
$option['clientID'] = $option['client_id'];
}
unset( $option['client_id'] );
$this->options->set( self::OPTION, $option );
}
if ( empty( $option['clientID'] ) ) {
return new WP_Error( 'client_id_not_set', __( 'AdSense client ID not set.', 'google-site-kit' ), array( 'status' => 404 ) );
}
return $option['clientID'];
};
case 'use-snippet':
return function() {
$option = (array) $this->options->get( self::OPTION );
return ! empty( $option['useSnippet'] );
};
case 'account-status':
return function() {
$option = (array) $this->options->get( self::OPTION );
// TODO: Remove this at some point (migration of old option).
if ( isset( $option['account_status'] ) ) {
if ( ! isset( $option['accountStatus'] ) ) {
$option['accountStatus'] = $option['account_status'];
}
unset( $option['account_status'] );
$this->options->set( self::OPTION, $option );
}
if ( empty( $option['accountStatus'] ) ) {
return new WP_Error( 'account_status_not_set', __( 'AdSense account status not set.', 'google-site-kit' ), array( 'status' => 404 ) );
}
return $option['accountStatus'];
};
case 'account-url':
return function() {
$account_id = $this->get_data( 'account-id' );
if ( ! is_wp_error( $account_id ) && $account_id ) {
return sprintf( 'https://www.google.com/adsense/new/%s/home', $account_id );
}
return 'https://www.google.com/adsense/signup/new';
};
case 'reports-url':
return function() {
$account_id = $this->get_data( 'account-id' );
if ( ! is_wp_error( $account_id ) && $account_id ) {
return sprintf( 'https://www.google.com/adsense/new/u/0/%s/main/viewreports', $account_id );
}
return 'https://www.google.com/adsense/start';
};
case 'notifications':
return function() {
$alerts = $this->get_data( 'alerts' );
if ( is_wp_error( $alerts ) || empty( $alerts ) ) {
return array();
}
$alerts = array_filter(
$alerts,
function( Google_Service_AdSense_Alert $alert ) {
return 'SEVERE' === $alert->getSeverity();
}
);
// There is no SEVERE alert, return empty.
if ( empty( $alerts ) ) {
return array();
}
/**
* First Alert
*
* @var Google_Service_AdSense_Alert $alert
*/
$alert = array_shift( $alerts );
return array(
array(
'id' => 'adsense-notification',
'title' => __( 'Alert found!', 'google-site-kit' ),
'description' => $alert->getMessage(),
'isDismissible' => true,
'winImage' => 'sun-small.png',
'format' => 'large',
'severity' => 'win-info',
'ctaURL' => $this->get_data( 'account-url' ),
'ctaLabel' => __( 'Go to AdSense', 'google-site-kit' ),
'ctaTarget' => '_blank',
),
);
};
case 'accounts':
$service = $this->get_service( 'adsense' );
return $service->accounts->listAccounts();
case 'alerts':
if ( ! isset( $data['accountID'] ) ) {
$data['accountID'] = $this->get_data( 'account-id' );
if ( is_wp_error( $data['accountID'] ) || ! $data['accountID'] ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) );
}
}
$service = $this->get_service( 'adsense' );
return $service->accounts_alerts->listAccountsAlerts( $data['accountID'] );
case 'clients':
$service = $this->get_service( 'adsense' );
return $service->adclients->listAdclients();
case 'urlchannels':
if ( ! isset( $data['clientID'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) );
}
$service = $this->get_service( 'adsense' );
return $service->urlchannels->listUrlchannels( $data['clientID'] );
case 'earnings':
$dates = $this->date_range_to_dates( $data['dateRange'] ?: 'last-28-days' );
if ( is_wp_error( $dates ) ) {
return $dates;
}
list ( $start_date, $end_date ) = $dates;
$dimensions = (array) $data['dimensions'];
$args = compact( 'start_date', 'end_date', 'dimensions' );
if ( isset( $data['limit'] ) ) {
$args['row_limit'] = $data['limit'];
}
return $this->create_adsense_earning_data_request( $args );
}
} elseif ( 'POST' === $method ) {
switch ( $datapoint ) {
case 'connection':
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$keys = array( 'accountID', 'clientID', 'accountStatus' );
foreach ( $keys as $key ) {
if ( isset( $data[ $key ] ) ) {
$option[ $key ] = $data[ $key ];
}
}
$this->options->set( self::OPTION, $option );
return true;
};
case 'account-id':
if ( ! isset( $data['accountID'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ), array( 'status' => 400 ) );
}
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$option['accountID'] = $data['accountID'];
$this->options->set( self::OPTION, $option );
return true;
};
case 'client-id':
if ( ! isset( $data['clientID'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) );
}
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$option['clientID'] = $data['clientID'];
$this->options->set( self::OPTION, $option );
return true;
};
case 'use-snippet':
if ( ! isset( $data['useSnippet'] ) ) {
return new WP_Error(
'missing_required_param',
sprintf(
/* translators: %s: Missing parameter name */
__( 'Request parameter is empty: %s.', 'google-site-kit' ),
'useSnippet'
),
array( 'status' => 400 )
);
}
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$option['useSnippet'] = (bool) $data['useSnippet'];
$this->options->set( self::OPTION, $option );
return true;
};
case 'account-status':
if ( ! isset( $data['accountStatus'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountStatus' ), array( 'status' => 400 ) );
}
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$option['accountStatus'] = $data['accountStatus'];
$this->options->set( self::OPTION, $option );
return true;
};
case 'setup-complete':
if ( ! isset( $data['clientID'] ) ) {
/* translators: %s: Missing parameter name */
return new WP_Error( 'missing_required_param', sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'clientID' ), array( 'status' => 400 ) );
}
return function() use ( $data ) {
$option = (array) $this->options->get( self::OPTION );
$option['setupComplete'] = true;
$option['clientID'] = $data['clientID'];
$option['useSnippet'] = ! empty( $data['useSnippet'] );
$this->options->set( self::OPTION, $option );
return true;
};
}
}
return new WP_Error( 'invalid_datapoint', __( 'Invalid datapoint.', 'google-site-kit' ) );
}
/**
* Parses a response for the given datapoint.
*
* @since 1.0.0
*
* @param Data_Request $data Data request object.
* @param mixed $response Request response.
*
* @return mixed Parsed response data on success, or WP_Error on failure.
*/
protected function parse_data_response( Data_Request $data, $response ) {
$method = $data->method;
$datapoint = $data->datapoint;
if ( 'GET' === $method ) {
switch ( $datapoint ) {
case 'accounts':
// Store the matched account as soon as we have it.
$accounts = $response->getItems();
if ( ! empty( $accounts ) ) {
$account_id = $this->get_data( 'account-id' );
if ( is_wp_error( $account_id ) || ! $account_id ) {
$this->set_data( 'account-id', array( 'accountID' => $accounts[0]->id ) );
}
}
// TODO: Parse this response to a regular array.
return $accounts;
case 'alerts':
// TODO: Parse this response to a regular array.
return $response->getItems();
case 'clients':
// TODO: Parse this response to a regular array.
return $response->getItems();
case 'urlchannels':
// TODO: Parse this response to a regular array.
return $response->getItems();
case 'earnings':
return $response;
}
}
return $response;
}
/**
* Gets an array of dates for the given named date range.
*
* @param string $date_range Named date range.
* E.g. 'last-28-days'.
*
* @return array|WP_Error Array of [startDate, endDate] or WP_Error if invalid named range.
*/
private function date_range_to_dates( $date_range ) {
switch ( $date_range ) {
case 'today':
return array(
date( 'Y-m-d', strtotime( 'today' ) ),
date( 'Y-m-d', strtotime( 'today' ) ),
);
case 'yesterday':
return array(
date( 'Y-m-d', strtotime( 'yesterday' ) ),
date( 'Y-m-d', strtotime( 'yesterday' ) ),
);
case 'same-day-last-week':
return array(
date( 'Y-m-d', strtotime( '7 days ago' ) ),
date( 'Y-m-d', strtotime( '7 days ago' ) ),
);
case 'this-month':
return array(
date( 'Y-m-01' ),
date( 'Y-m-d', strtotime( 'today' ) ),
);
case 'this-month-last-year':
$last_year = intval( date( 'Y' ) ) - 1;
$last_date_of_month = date( 't', strtotime( $last_year . '-' . date( 'm' ) . '-01' ) );
return array(
date( $last_year . '-m-01' ),
date( $last_year . '-m-' . $last_date_of_month ),
);
case 'prev-7-days':
return array(
date( 'Y-m-d', strtotime( '14 days ago' ) ),
date( 'Y-m-d', strtotime( '8 days ago' ) ),
);
case 'prev-28-days':
return array(
date( 'Y-m-d', strtotime( '56 days ago' ) ),
date( 'Y-m-d', strtotime( '29 days ago' ) ),
);
// Intentional fallthrough.
case 'last-7-days':
case 'last-14-days':
case 'last-28-days':
case 'last-90-days':
return $this->parse_date_range( $date_range );
}
return new WP_Error( 'invalid_date_range', __( 'Invalid date range.', 'google-site-kit' ) );
}
/**
* Creates a new AdSense earning request for the current account, site and given arguments.
*
* @since 1.0.0
*
* @param array $args {
* Optional. Additional arguments.
*
* @type array $dimensions List of request dimensions. Default empty array.
* @type string $start_date Start date in 'Y-m-d' format. Default empty string.
* @type string $end_date End date in 'Y-m-d' format. Default empty string.
* @type int $row_limit Limit of rows to return. Default none (will be skipped).
* }
* @return RequestInterface|WP_Error AdSense earning request instance.
*/
protected function create_adsense_earning_data_request( array $args = array() ) {
$args = wp_parse_args(
$args,
array(
'dimensions' => array(),
'start_date' => '',
'end_date' => '',
'row_limit' => '',
)
);
$account_id = $this->get_data( 'account-id' );
if ( is_wp_error( $account_id ) ) {
return $account_id;
}
$opt_params = array(
'locale' => get_locale(),
'metric' => array( 'EARNINGS', 'PAGE_VIEWS_RPM', 'IMPRESSIONS' ),
);
if ( ! empty( $args['dimensions'] ) ) {
$opt_params['dimension'] = (array) $args['dimensions'];
}
if ( ! empty( $args['row_limit'] ) ) {
$opt_params['maxResults'] = (int) $args['row_limit'];
}
$host = wp_parse_url( $this->context->get_reference_site_url(), PHP_URL_HOST );
if ( ! empty( $host ) ) {
$opt_params['filter'] = 'DOMAIN_NAME==' . $host;
}
$service = $this->get_service( 'adsense' );
return $service->accounts_reports->generate( $account_id, $args['start_date'], $args['end_date'], $opt_params );
}
/**
* Sets up information about the module.
*
* @since 1.0.0
*
* @return array Associative array of module info.
*/
protected function setup_info() {
$idenfifier_args = array(
'source' => 'site-kit',
'url' => $this->context->get_reference_site_url(),
);
return array(
'slug' => 'adsense',
'name' => _x( 'AdSense', 'Service name', 'google-site-kit' ),
'description' => __( 'Earn money by placing ads on your website. It’s free and easy.', 'google-site-kit' ),
'cta' => __( 'Monetize Your Site.', 'google-site-kit' ),
'order' => 2,
'homepage' => add_query_arg( $idenfifier_args, $this->get_data( 'reports-url' ) ),
'learn_more' => __( 'https://www.google.com/intl/en_us/adsense/start/', 'google-site-kit' ),
'group' => __( 'Additional Google Services', 'google-site-kit' ),
'tags' => array( 'monetize' ),
);
}
/**
* Sets up the Google services the module should use.
*
* This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested
* for the first time.
*
* @since 1.0.0
*
* @param Google_Client $client Google client instance.
* @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an
* instance of Google_Service.
*/
protected function setup_services( Google_Client $client ) {
return array(
'adsense' => new Google_Service_AdSense( $client ),
);
}
}