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 ), ); } }