PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 2.0.25
Advanced Ads – Ad Manager & AdSense v2.0.25
2.0.25 2.0.24 2.0.23 2.0.22 2.0.21 1.38.0 1.39.0 1.39.1 1.39.2 1.39.3 1.39.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.40.0 1.40.1 1.40.2 1.41.0 1.42.0 1.42.1 1.42.2 1.43.0 1.44.0 1.44.1 1.45.0 1.45.1 1.46.0 1.47.0 1.47.1 1.47.2 1.47.3 1.47.4 1.47.5 1.48.0 1.48.1 1.49.0 1.5.0 1.5.0.1 1.5.1 1.5.2 1.5.2.1 1.5.4 1.5.4.1 1.5.5 1.50.0 1.51.0 1.51.1 1.51.2 1.51.3 1.52.0 1.52.1 1.52.2 1.52.3 1.52.4 1.53.0 1.53.1 1.53.2 1.54.0 1.54.1 1.55.0 1.56.0 1.56.1 1.56.2 1.56.3 1.56.4 1.6 1.6.1 1.6.10 1.6.10.1 1.6.10.2 1.6.11 1.6.11.1 1.6.12 1.6.13 1.6.14 1.6.15 1.6.16 1.6.17 1.6.17.1 1.6.17.2 1.6.2 1.6.2.1 1.6.3 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.6.1 1.6.7 1.6.7.1 1.6.8 1.6.8.1 1.6.8.2 1.6.8.3 1.6.9 1.6.9.1 1.6.9.2 1.6.9.3 1.6.9.4 1.7 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.1.1 1.7.1.2 1.7.1.3 1.7.1.4 1.7.1.5 1.7.10 trunk 1.7.11 1.0.1 1.7.12 1.0.2 1.7.13 1.0.3 1.7.14 1.1.0 1.7.15 1.1.1 1.7.16 1.1.2 1.7.17 1.1.3 1.7.18 1.10 1.7.19 1.10.1 1.7.2 1.10.10 1.7.2.1 1.10.11 1.7.20 1.10.12 1.7.21 1.10.2 1.7.22 1.10.3 1.7.23 1.10.4 1.7.24 1.10.5 1.7.25 1.10.6 1.7.3 1.10.7 1.7.4 1.10.8 1.7.4.1 1.10.9 1.7.4.2 1.11 1.7.4.3 1.11.1 1.7.4.4 1.11.2 1.7.4.5 1.12 1.7.5 1.13 1.7.5.1 1.13.1 1.7.6 1.13.2 1.7.7 1.13.3 1.7.8 1.13.4 1.7.9 1.13.5 1.7.9.1 1.13.6 1.7.9.2 1.13.7 1.7.9.3 1.13.8 1.8 1.14 1.8.1 1.14.1 1.8.10 1.14.10 1.8.11 1.14.11 1.8.12 1.14.2 1.8.13 1.14.3 1.8.14 1.14.4 1.8.15 1.14.5 1.8.16 1.14.6 1.8.17 1.14.7 1.8.18 1.14.8 1.8.19 1.14.9 1.8.2 1.15 1.8.20 1.16 1.8.21 1.16.1 1.8.22 1.17 1.8.23 1.17.1 1.8.24 1.17.10 1.8.25 1.17.10-rc.1 1.8.26 1.17.11 1.8.27 1.17.12 1.8.28 1.17.12-rc.1 1.8.29 1.17.2 1.8.3 1.17.3 1.8.30 1.17.4 1.8.4 1.17.5 1.8.5 1.17.6 1.8.6 1.17.7 1.8.7 1.17.8 1.8.8 1.17.9 1.8.9 1.17.9-beta.1 1.9 1.18.0 2.0.0 1.19.0 2.0.1 1.19.1 2.0.10 1.2 2.0.11 1.2.1 2.0.12 1.2.2 2.0.13 1.2.3 2.0.14 1.2.4 2.0.15 1.2.5 2.0.16 1.2.6 2.0.17 1.2.7 2.0.18 1.20.0 2.0.19 1.20.0-rc.1 2.0.2 1.20.0-rc.2 2.0.20 1.20.1 2.0.3 1.20.2 2.0.4 1.20.3 2.0.5 1.21.0 2.0.6 1.21.1 2.0.7 1.22.0 2.0.8 1.22.1 2.0.9 1.22.2 1.23.0 1.23.1 1.23.2 1.24.0 1.24.1 1.24.2 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.29.0 1.29.1 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.30.0 1.30.1 1.30.2 1.30.2-rc.1 1.30.3 1.30.4 1.30.4-rc.1 1.30.5 1.31.0 1.31.1 1.32.0 1.32.0-rc.1 1.33.0 1.33.1 1.33.2 1.34.0 1.35.0 1.35.1 1.36.0 1.36.1 1.36.2 1.36.3 1.37.0 1.37.1 1.37.2
advanced-ads / modules / gadsense / includes / class-mapi.php
advanced-ads / modules / gadsense / includes Last commit date
types 1 year ago adsense-report-api.php 1 year ago class-ad-type-adsense.php 3 months ago class-adsense-report-data.php 1 year ago class-adsense-report.php 1 year ago class-gadsense-data.php 1 year ago class-mapi.php 1 year ago class-network-adsense.php 1 year ago
class-mapi.php
1763 lines
1 <?php // phpcs:ignoreFile
2
3 use AdvancedAds\Utilities\Conditional;
4 use AdvancedAds\Framework\Utilities\Params;
5
6 /**
7 * AdSense Management API class.
8 */
9 class Advanced_Ads_AdSense_MAPI {
10
11 /**
12 * The Adsense MAPI settings in the database.
13 *
14 * @var string
15 */
16 const OPTION_KEY = 'advanced-ads-adsense-mapi';
17
18 /**
19 * The URL for retrieving alerts from the Google AdSense API.
20 *
21 * @var string
22 */
23 const ALERTS_URL = 'https://adsense.googleapis.com/v2/accounts/PUBID/alerts';
24
25 /**
26 * Google Adsense Client ID.
27 *
28 * @var string
29 */
30 const CID = '400595147946-3ot506jh20qld7bqmg1l87ms4vn2uok5.apps.googleusercontent.com';
31
32 /**
33 * Google AdSense Client Secret.
34 *
35 * @var string
36 */
37 const CS = 'WKX8ghwUbxdrBcVmZ9WXOKph';
38
39 /**
40 * The redirect URI for the Google AdSense API authentication.
41 *
42 * @var string
43 */
44 const REDIRECT_URI = 'https://c.wpadvancedads.com/oauth.php';
45
46 /**
47 * The maximum number of API calls allowed per 24 hours.
48 *
49 * @var int
50 */
51 const CALL_PER_24H = 20;
52
53 /**
54 * Class Mapi
55 *
56 * @var Advanced_Ads_AdSense_MAPI
57 */
58 private static $instance = null;
59
60 /**
61 * Default options.
62 *
63 * @var array
64 */
65 private static $default_options = [];
66
67 /**
68 * Default account options.
69 *
70 * @var array
71 */
72 private static $empty_account_data = [
73 'default_app' => [
74 'access_token' => '',
75 'refresh_token' => '',
76 'expires' => 0,
77 'token_type' => '',
78 ],
79 'user_app' => [
80 'access_token' => '',
81 'refresh_token' => '',
82 'expires' => 0,
83 'token_type' => '',
84 ],
85 'ad_units' => [],
86 'details' => [],
87 'alerts' => [],
88 ];
89
90 /**
91 * Instance constructor
92 */
93 private function __construct() {
94
95 add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
96
97 add_action( 'wp_ajax_advads_gadsense_mapi_confirm_code', [ $this, 'ajax_confirm_code' ] );
98 add_action( 'wp_ajax_advads_gadsense_mapi_get_details', [ $this, 'ajax_get_account_details' ] );
99 add_action( 'wp_ajax_advads_gadsense_mapi_select_account', [ $this, 'ajax_account_selected' ] );
100 add_action( 'wp_ajax_advads_mapi_get_adCode', [ $this, 'ajax_get_ad_code' ] );
101 add_action( 'wp_ajax_advads-mapi-reconstructed-code', [ $this, 'ajax_save_reconstructed_code' ] );
102 add_action( 'wp_ajax_advads-mapi-save-manual-code', [ $this, 'ajax_save_manual_code' ] );
103 add_action( 'wp_ajax_advads-mapi-revoke-token', [ $this, 'ajax_revoke_tokken' ] );
104 add_action( 'wp_ajax_advads-mapi-get-alerts', [ $this, 'ajax_get_account_alerts' ] );
105 add_action( 'wp_ajax_advads-mapi-dismiss-alert', [ $this, 'ajax_dismiss_alert' ] );
106 add_action( 'wp_ajax_advads_adsense_report_refresh', [ 'Advanced_Ads_Overview_Widgets_Callbacks', 'ajax_gadsense_dashboard' ] );
107
108 add_action( 'admin_footer', [ $this, 'admin_footer' ] );
109
110 self::$default_options = [
111 'accounts' => [],
112 'ad_codes' => [],
113 'unsupported_units' => [],
114 'quota' => [
115 'count' => self::CALL_PER_24H,
116 'ts' => 0,
117 ],
118 'connect_error' => [],
119 ];
120
121 add_action( 'wp_loaded', [ $this, 'update_ad_health_notices' ] );
122 add_filter( 'advanced-ads-support-messages', [ 'Advanced_Ads_AdSense_MAPI', 'adsense_warnings_check' ] );
123 }
124
125 /**
126 * Update all MAPI related notices.
127 */
128 public function update_ad_health_notices() {
129 $mapi_options = self::get_option();
130
131 $connection_error_messages = self::get_connect_error_messages();
132
133 $health_class = Advanced_Ads_Ad_Health_Notices::get_instance();
134
135 // Last connection failed.
136 if ( isset( $mapi_options['connect_error'] ) && ! empty( $mapi_options['connect_error'] ) ) {
137
138 if ( isset( $connection_error_messages[ $mapi_options['connect_error']['reason'] ] ) ) {
139 $health_class->add( 'adsense_connect_' . $mapi_options['connect_error']['reason'] );
140 } else {
141 $health_class->add(
142 'adsense_connect_' . $mapi_options['connect_error']['reason'],
143 [
144 'text' => esc_html__( 'Last AdSense account connection attempt failed.', 'advanced-ads' ) . ' ' . $mapi_options['connect_error']['message'],
145 'type' => 'problem',
146 ]
147 );
148 }
149
150 foreach ( $health_class->notices as $key => $value ) {
151 // There was already a connection error but the user tried again and obtained another error.
152 if ( false !== stripos( $key, 'adsense_connect_' ) && 'adsense_connect_' . $mapi_options['connect_error']['reason'] !== $key ) {
153 $health_class->remove( $key );
154 }
155 }
156 } else {
157
158 // Once a connection has been established (or a the warning has been discarded on the AA settings page), remove connection related notices.
159 foreach ( $health_class->notices as $key => $value ) {
160 if ( false !== stripos( $key, 'adsense_connect_' ) ) {
161 $health_class->remove( $key );
162 }
163 }
164 }
165
166 $gadsense_data = Advanced_Ads_AdSense_Data::get_instance();
167 $adsense_id = $gadsense_data->get_adsense_id();
168 $alerts = self::get_stored_account_alerts( $adsense_id );
169
170 // AdSense account alerts (can not happens simultaneously with the connection error).
171 if ( is_array( $alerts ) && isset( $alerts['items'] ) && is_array( $alerts['items'] ) && $alerts['items'] ) {
172 $item_ids = [];
173 $alerts_advads_messages = Advanced_Ads_Adsense_MAPI::get_adsense_alert_messages();
174
175 foreach ( $alerts['items'] as $internal_id => $item ) {
176 $item_id = isset( $item['id'] ) ? $item['id'] : str_replace( '-', '_', strtoupper( $item['type'] ) );
177 $item_ids[] = $item_id;
178 if ( isset( $alerts_advads_messages[ $item_id ] ) ) {
179 $health_class->add( 'adsense_alert_' . $item_id );
180 } else {
181 $health_class->add(
182 'adsense_alert_' . $item_id,
183 [
184 'text' => $item['message'] . ' ' . self::get_adsense_error_link( $item_id ),
185 'type' => 'problem',
186 ]
187 );
188 }
189 }
190
191 // Remove notices that no more exist in the AdSense account (or have been dismissed).
192 $_remove_ids = [];
193 foreach ( $health_class->notices as $key => $value ) {
194 if ( false !== stripos( $key, 'adsense_alert_' ) ) {
195 $alert_id = substr( $key, strlen( 'adsense_alert_' ) );
196 if ( ! in_array( $alert_id, $item_ids, true ) ) {
197 $_remove_ids[] = $key;
198 }
199 }
200 }
201 foreach ( $_remove_ids as $id ) {
202 $health_class->remove( $id );
203 }
204 } else {
205 // No more alerts.
206 foreach ( $health_class->notices as $key => $value ) {
207 if ( false !== stripos( $key, 'adsense_alert_' ) ) {
208 $health_class->remove( $key );
209 }
210 }
211 }
212 }
213
214 /**
215 * Get available quota and eventual message about remaining call
216 */
217 public function get_quota() {
218 // Early bail!!
219 if ( self::use_user_app() ) {
220 return [ 'count' => PHP_INT_MAX ];
221 }
222
223 $now = time();
224 $options = $this->get_option();
225 if ( $now > $options['quota']['ts'] + ( 24 * 3600 ) ) {
226 return [
227 'count' => self::CALL_PER_24H,
228 ];
229 }
230
231 return [
232 'count' => $options['quota']['count'],
233 'msg' => $this->get_quota_msg(),
234 ];
235 }
236
237 /**
238 * Get the readable quota
239 */
240 public function get_quota_msg() {
241
242 $options = $this->get_option();
243 $now = time();
244 $secs = $options['quota']['ts'] + ( 24 * 3600 ) - $now;
245 $hours = floor( $secs / 3600 );
246 $mins = ceil( ( $secs - ( $hours * 3600 ) ) / 60 );
247
248 if ( 60 === $mins ) {
249 ++$hours;
250 $mins = 0;
251 }
252
253 if ( 0 === (int) $options['quota']['count'] ) {
254 $msg = sprintf(
255 'No API call left before %1$s %2$s.',
256 sprintf( '%s hours', $hours ),
257 sprintf( '%s minutes', $mins )
258 );
259
260 if ( 0 === $hours ) {
261 $msg = 'No API call left before.';
262 }
263
264 if ( 0 === $mins ) {
265 $msg = 'No API call left.';
266 }
267 } else {
268 $msg = sprintf(
269 '%1$s for the next %2$s %3$s',
270 sprintf( '%s API calls remaining', $options['quota']['count'] ),
271 sprintf( '%s hours', $hours ),
272 sprintf( '%s minutes', $mins )
273 );
274
275 if ( 0 === $hours ) {
276 $msg = sprintf( '%s API calls remaining.', $options['quota']['count'] );
277 }
278
279 if ( 0 === $mins ) {
280 $msg = sprintf(
281 '%1$s for the next %2$s',
282 sprintf( '%s API calls remaining', $options['quota']['count'] ),
283 sprintf( '%s hours', $hours )
284 );
285 }
286 }
287 return $msg;
288 }
289
290 /**
291 * Decrement quota by 1, and return message about remaining call
292 */
293 public function decrement_quota() {
294 $options = $this->get_option();
295 if ( 0 < $options['quota']['count'] ) {
296 --$options['quota']['count'];
297 $now = time();
298 if ( $now > $options['quota']['ts'] + ( 24 * 3600 ) ) {
299 $options['quota']['ts'] = $now;
300 }
301 update_option( self::OPTION_KEY, $options );
302 return $this->get_quota_msg();
303 }
304 }
305
306 /**
307 * Get ad code from Google fpr a given ad unit
308 *
309 * @param string $ad_unit the ad unit to get the ad code for.
310 *
311 * @return array response to send back to the browser.
312 */
313 public function get_ad_code( $ad_unit ) {
314 $options = self::get_option();
315 $gadsense_data = Advanced_Ads_AdSense_Data::get_instance();
316 $adsense_id = $gadsense_data->get_adsense_id();
317 $unit_id = explode( ':', $ad_unit )[1];
318 $url = 'https://adsense.googleapis.com/v2/accounts/' . $adsense_id . '/adclients/ca-' . $adsense_id . '/adunits/' . $unit_id . '/adcode';
319 $access_token = self::get_access_token( $adsense_id );
320
321 foreach ( Advanced_Ads_Network_Adsense::get_instance()->get_external_ad_units() as $unit ) {
322 if (
323 isset( $unit->raw )
324 && in_array( $unit->raw['contentAdsSettings']['type'], [ 'ARTICLE', 'FEED', 'MATCHED_CONTENT' ], true )
325 && $ad_unit === $unit->id
326 && ! array_key_exists( $ad_unit, $options['ad_codes'] )
327 ) {
328 $options['unsupported_units'][ $ad_unit ] = 1;
329 update_option( self::OPTION_KEY, $options );
330
331 return [
332 'status' => false,
333 'msg' => 'doesNotSupportAdUnitType',
334 ];
335 }
336 }
337
338 if ( ! isset( $access_token['msg'] ) ) {
339 $headers = [ 'Authorization' => 'Bearer ' . $access_token ];
340 $response = wp_remote_get( $url, [ 'headers' => $headers ] );
341 self::log( 'Get ad code for ad Unit [' . $ad_unit . ']' );
342
343 if ( is_wp_error( $response ) ) {
344 return [
345 'status' => false,
346 /* translators: %s: ad unit ID. */
347 'msg' => sprintf( esc_html__( 'Error while retrieving ad code for "%s".', 'advanced-ads' ), $ad_unit ),
348 'raw' => $response->get_error_message(),
349 ];
350 }
351 $ad_code = json_decode( $response['body'], true );
352 if ( null === $ad_code || ! isset( $ad_code['adCode'] ) ) {
353 if ( $ad_code['error'] &&
354 $ad_code['error']['errors'] &&
355 isset( $ad_code['error']['errors'][0]['reason'] ) &&
356 'doesNotSupportAdUnitType' === $ad_code['error']['errors'][0]['reason']
357 ) {
358 if ( array_key_exists( $ad_unit, $options['ad_codes'] ) && array_key_exists( $ad_unit, $options['unsupported_units'] ) ) {
359 unset( $options['unsupported_units'][ $ad_unit ] );
360 } else {
361 $options['unsupported_units'][ $ad_unit ] = 1;
362 }
363 update_option( self::OPTION_KEY, $options );
364
365 return [
366 'status' => false,
367 'msg' => 'doesNotSupportAdUnitType',
368 ];
369 }
370
371 return [
372 'status' => false,
373 /* translators: %s: ad unit ID. */
374 'msg' => sprintf( esc_html__( 'Invalid response while retrieving ad code for "%s".', 'advanced-ads' ), $ad_unit ),
375 'raw' => $response['body'],
376 ];
377 }
378 $options['ad_codes'][ $ad_unit ] = $ad_code['adCode'];
379 if ( isset( $options['unsupported_units'][ $ad_unit ] ) ) {
380 unset( $options['unsupported_units'][ $ad_unit ] );
381 }
382 update_option( self::OPTION_KEY, $options );
383
384 return $ad_code['adCode'];
385 }
386
387 // return the original error info.
388 return $access_token;
389 }
390
391 /**
392 * Convert ad unit data to v1.4 format to match the current UI and logics
393 *
394 * @param array $ad_unit ad unit in MAPI v2 format.
395 *
396 * @return array the ad unit in MAPI v1.4 format.
397 */
398 public static function convert_ad_unit_format( $ad_unit ) {
399 $chunks = explode( '/', $ad_unit['name'] );
400
401 return [
402 'name' => $ad_unit['displayName'],
403 'nameV2' => $ad_unit['name'],
404 'id' => $ad_unit['reportingDimensionId'],
405 'code' => $chunks[ count( $chunks ) - 1 ],
406 'status' => $ad_unit['state'],
407 'contentAdsSettings' => $ad_unit['contentAdsSettings'],
408 'reportingDimensionId' => $ad_unit['reportingDimensionId'],
409 ];
410 }
411
412 /**
413 * Get/Update ad unit list for a given client
414 *
415 * @param string $account publisher ID.
416 *
417 * @return array
418 */
419 public static function get_ad_units( $account ) {
420 $url = 'https://adsense.googleapis.com/v2/accounts/' . $account . '/adclients/ca-' . $account . '/adunits?pageSize=350';
421 $access_token = self::get_access_token( $account );
422
423 if ( isset( $access_token['msg'] ) ) {
424 // Return the token related message.
425 return $access_token;
426 }
427
428 $response = wp_remote_get( $url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token ] ] );
429 self::log( 'Get ad units list for ca-' . $account );
430
431 if ( is_wp_error( $response ) ) {
432 return [
433 'status' => false,
434 /* translators: %s is the publisher ID. */
435 'msg' => sprintf( esc_html__( 'Error while retrieving ad unit list for "%s".', 'advanced-ads' ), $account ),
436 'raw' => $response->get_error_message(),
437 ];
438 }
439
440 if ( trim( $response['body'] ) === '{}' ) {
441 // Empty account.
442 return [
443 'status' => false,
444 'msg' => sprintf(
445 /* translators: %1$s is the AdSense publisher ID; %2$s a starting a tag to the AdSense ad unit list and %3$s the closing link. */
446 esc_html__( 'The account "%1$s" does not seem to have any ad units. Please create some %2$shere%3$s.', 'advanced-ads' ),
447 $account,
448 '<a href="https://www.google.com/adsense/new/u/0/' . $account . '/myads/units" target="_blank">',
449 '</a>'
450 ),
451 'raw' => $response['body'],
452 ];
453 }
454
455 $response_body = json_decode( trim( $response['body'] ), true );
456
457 if ( null === $response_body || ! isset( $response_body['adUnits'] ) ) {
458 /* translators: %s is the publisher ID. */
459 $error_message = sprintf( esc_html__( 'Invalid response while retrieving ad unit list for "%s".', 'advanced-ads' ), $account );
460 // check the response for errors and display them for better problem-solving.
461 if ( ! empty( $response_body['error']['errors'] ) ) {
462 foreach ( $response_body['error']['errors'] as $err ) {
463 $hint = self::get_adsense_error_hint( $err['reason'] );
464 if ( $hint ) {
465 $error_message .= '<p class="description">' . $hint . '</p>';
466 }
467 $error_message .= sprintf(
468 '<p class="description">%1$s %2$s<br>%3$s %4$s</p>',
469 _x( 'Reason:', 'Reason of the API call error', 'advanced-ads' ),
470 $err['reason'],
471 _x( 'Message:', 'Error message from Google', 'advanced-ads' ),
472 $err['message']
473 );
474 }
475 }
476
477 return [
478 'status' => false,
479 'msg' => $error_message,
480 'raw' => trim( $response['body'] ),
481 ];
482 }
483
484 $options = self::get_option();
485
486 if ( ! isset( $response_body['nextPageToken'] ) ) {
487 // Results fit into a single page (of 350 items).
488 $new_ad_units = [];
489
490 foreach ( $response_body['adUnits'] as $item ) {
491 $item = self::convert_ad_unit_format( $item );
492 $new_ad_units[ $item['id'] ] = $item;
493 }
494
495 $options['accounts'][ $account ]['ad_units'] = $new_ad_units;
496 update_option( self::OPTION_KEY, $options );
497
498 return [ 'done' => true ];
499 }
500
501 // There are more than 350 items in the account.
502 $page_token = $response_body['nextPageToken'];
503 $new_ad_units = [];
504
505 foreach ( $response_body['adUnits'] as $item ) {
506 $item = self::convert_ad_unit_format( $item );
507 $new_ad_units[ $item['id'] ] = $item;
508 }
509
510 $page = 1;
511 // While there is a next page of results do . . .
512 while ( $page_token ) {
513 $access_token = self::get_access_token( $account );
514
515 if ( isset( $access_token['msg'] ) ) {
516 // return the original error info.
517 return $access_token;
518 }
519
520 $next_url = $url . '&pageToken=' . urlencode( $page_token );
521 $headers = [
522 'Authorization' => 'Bearer ' . $access_token,
523 ];
524 $response = wp_remote_get( $next_url, [ 'headers' => $headers ] );
525 self::log( 'Get ad unit list for ca-' . $account . ' page ' . $page );
526 ++$page;
527
528 if ( is_wp_error( $response ) ) {
529 return [
530 'status' => false,
531 /* translators: %s is the publisher ID. */
532 'msg' => sprintf( esc_html__( 'Error while retrieving ad unit list for "%s".', 'advanced-ads' ), $account ),
533 'raw' => $response->get_error_message(),
534 ];
535 }
536
537 $response_body = json_decode( $response['body'], true );
538 // Update page token if there are ad units left.
539 $page_token = isset( $response_body['nextPageToken'] ) ? $response_body['nextPageToken'] : false;
540 // Add items from this page into the final result.
541 foreach ( $response_body['adUnits'] as $item ) {
542 $item = self::convert_ad_unit_format( $item );
543 $new_ad_units[ $item['id'] ] = $item;
544 }
545 }
546
547 $options['accounts'][ $account ]['ad_units'] = $new_ad_units;
548 update_option( self::OPTION_KEY, $options );
549
550 return [ 'done' => true ];
551 }
552
553 /**
554 * Get the appropriate access token (default one or from user's Google app). Update it if needed.
555 *
556 * @param string $account publisher ID.
557 *
558 * @return array|string the access token on success, error info (as array) if an error occurred.
559 */
560 public static function get_access_token( $account ) {
561 $options = self::get_option();
562 if ( isset( $options['accounts'][ $account ] ) ) {
563 if ( self::use_user_app() ) {
564 if ( time() > $options['accounts'][ $account ]['user_app']['expires'] ) {
565 $new_tokens = self::renew_access_token( $account );
566 if ( $new_tokens['status'] ) {
567 return $new_tokens['access_token'];
568 } else {
569 return $new_tokens;
570 }
571 } else {
572 return $options['accounts'][ $account ]['user_app']['access_token'];
573 }
574 } else {
575 if ( time() > $options['accounts'][ $account ]['default_app']['expires'] ) {
576 $new_tokens = self::renew_access_token( $account );
577 if ( $new_tokens['status'] ) {
578 return $new_tokens['access_token'];
579 } else {
580 // return all error info [arr].
581 return $new_tokens;
582 }
583 } else {
584 return $options['accounts'][ $account ]['default_app']['access_token'];
585 }
586 }
587 } else {
588 // Account does not exist.
589 if ( ! empty( $options['accounts'] ) ) {
590 // There is another account connected.
591 return [
592 'status' => false,
593 'msg' => esc_html__( 'It seems that some changes have been made in the Advanced Ads settings. Please refresh this page.', 'advanced-ads' ),
594 'reload' => true,
595 ];
596 } else {
597 // No account at all.
598 return [
599 'status' => false,
600 /* translators: %s account id */
601 'msg' => wp_kses( sprintf( __( 'Advanced Ads does not have access to your account (<code>%s</code>) anymore.', 'advanced-ads' ), $account ), [ 'code' => true ] ),
602 'reload' => true,
603 ];
604 }
605 }
606 }
607
608 /**
609 * Renew the current access token.
610 *
611 * @param array $account AdSense account ID.
612 */
613 public static function renew_access_token( $account ) {
614 $cid = self::CID;
615 $cs = self::CS;
616 $options = self::get_option();
617 $access_token = $options['accounts'][ $account ]['default_app']['access_token'];
618 $refresh_token = $options['accounts'][ $account ]['default_app']['refresh_token'];
619
620 if ( self::use_user_app() ) {
621 $gadsense_data = Advanced_Ads_AdSense_Data::get_instance();
622 $_options = $gadsense_data->get_options();
623 $cid = ADVANCED_ADS_MAPI_CID;
624 $cs = ADVANCED_ADS_MAPI_CIS;
625 $access_token = $options['accounts'][ $account ]['user_app']['access_token'];
626 $refresh_token = $options['accounts'][ $account ]['user_app']['refresh_token'];
627 }
628
629 $url = 'https://www.googleapis.com/oauth2/v4/token';
630 $args = [
631 'body' => [
632 'refresh_token' => $refresh_token,
633 'client_id' => $cid,
634 'client_secret' => $cs,
635 'grant_type' => 'refresh_token',
636 ],
637 ];
638
639 $response = wp_remote_post( $url, $args );
640 self::log( 'Refresh access token' );
641
642 if ( is_wp_error( $response ) ) {
643 return [
644 'status' => false,
645 /* translators: %s account id */
646 'msg' => sprintf( esc_html__( 'error while renewing access token for "%s"', 'advanced-ads' ), $account ),
647 'raw' => $response->get_error_message(),
648 ];
649 } else {
650 $tokens = json_decode( $response['body'], true );
651 // checking for the $tokens is not enough. it can be empty.
652 // monitored this, when the access token is revoked from the outside
653 // this can happen, when the user connects from another domain.
654 if ( null !== $tokens && isset( $tokens['expires_in'] ) ) {
655 $expires = time() + absint( $tokens['expires_in'] );
656 if ( self::use_user_app() ) {
657 $options['accounts'][ $account ]['user_app']['access_token'] = $tokens['access_token'];
658 $options['accounts'][ $account ]['user_app']['expires'] = $expires;
659 } else {
660 $options['accounts'][ $account ]['default_app']['access_token'] = $tokens['access_token'];
661 $options['accounts'][ $account ]['default_app']['expires'] = $expires;
662 }
663 update_option( self::OPTION_KEY, $options );
664 return [
665 'status' => true,
666 'access_token' => $tokens['access_token'],
667 ];
668 } else {
669 return [
670 'status' => false,
671 'msg' => sprintf(
672 /* translators: %s AdSense account ID */
673 esc_html__( 'invalid response received while renewing access token for "%s"', 'advanced-ads' ),
674 $account
675 ) . ' ' . __( 'You could try to connect again under Advanced Ads > Settings > AdSense.', 'advanced-ads' ),
676 'raw' => $response['body'],
677 ];
678 }
679 }
680 }
681
682 /**
683 * Recoke a refresh token
684 */
685 public function ajax_revoke_tokken() {
686
687 $nonce = Params::post( 'nonce', '' );
688 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
689 die;
690 }
691 if ( false !== wp_verify_nonce( $nonce, 'advads-mapi' ) ) {
692 $adsense_id = stripslashes( Params::post( 'adsenseId', '' ) );
693 $options = self::get_option();
694
695 if ( self::use_user_app() ) {
696 $token = $options['accounts'][ $adsense_id ]['user_app']['refresh_token'];
697 } else {
698 $token = $options['accounts'][ $adsense_id ]['default_app']['refresh_token'];
699 }
700 $url = 'https://accounts.google.com/o/oauth2/revoke?token=' . $token;
701 $args = [
702 'timeout' => 5,
703 'header' => [ 'Content-type' => 'application/x-www-form-urlencoded' ],
704 ];
705
706 $response = wp_remote_post( $url, $args );
707
708 self::log( 'Revoke API access for ca-' . $adsense_id );
709
710 if ( is_wp_error( $response ) ) {
711 echo wp_json_encode( [ 'status' => false ] );
712 } else {
713 global $wpdb;
714 $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'advanced_ads_adsense_report_%';" );
715 delete_option( 'advanced-ads-adsense-dashboard-filter' );
716
717 header( 'Content-Type: application/json' );
718 unset( $options['accounts'][ $adsense_id ] );
719 update_option( self::OPTION_KEY, $options );
720 echo wp_json_encode( [ 'status' => true ] );
721 }
722 }
723 die;
724 }
725
726 /**
727 * When a user manually adds an ad code, save it
728 */
729 public function ajax_save_manual_code() {
730 if ( ! Conditional::user_can( 'advanced_ads_edit_ads' ) ) {
731 die();
732 }
733
734 if ( ! wp_verify_nonce( Params::request( 'nonce', '' ), 'advads-mapi' ) ) {
735 die();
736 }
737
738 $publisher_id = sanitize_text_field( wp_unslash( isset( $_POST['parsed_code']['pubId'] ) ? $_POST['parsed_code']['pubId'] : '' ) );
739 if ( ! $this->check_valid_publisher( $publisher_id ) ) {
740 wp_send_json_error(
741 [
742 'message' => __( 'This ad code is from a different AdSense Account', 'advanced-ads' ),
743 ],
744 400
745 );
746 }
747
748 if ( empty( $_POST['parsed_code']['slotId'] ) || empty( $_POST['raw_code'] ) ) {
749 die();
750 }
751
752 static $options;
753 if ( is_null( $options ) ) {
754 $options = self::get_option();
755 }
756
757 $slot_id = 'ca-' . $publisher_id . ':' . sanitize_text_field( wp_unslash( $_POST['parsed_code']['slotId'] ) );
758
759 // phpcs:disable WordPress.Security
760 $options['ad_codes'][ $slot_id ] = urldecode( Params::post( 'raw_code', '' ) );
761 // phpcs:enable
762
763 if ( array_key_exists( $slot_id, $options['unsupported_units'] ) ) {
764 unset( $options['unsupported_units'][ $slot_id ] );
765 }
766
767 wp_send_json_success( [ 'updated' => update_option( self::OPTION_KEY, $options ) ] );
768 }
769
770 /**
771 * Check if the provided AdSense Publisher ID matches the saved ID
772 *
773 * @param string $pub AdSense Publisher ID.
774 *
775 * @return bool
776 */
777 protected function check_valid_publisher( $pub ) {
778 return Advanced_Ads_AdSense_Data::get_instance()->get_adsense_id() === $pub;
779 }
780
781 /**
782 * Save ad code reconstructed from ad parameters
783 */
784 public function ajax_save_reconstructed_code() {
785 $nonce = Params::post( 'nonce', '' );
786 if ( ! Conditional::user_can( 'advanced_ads_edit_ads' ) ) {
787 die;
788 }
789
790 if ( false !== wp_verify_nonce( $nonce, 'advads-mapi' ) ) {
791 $code = stripslashes( Params::post( 'code', '' ) );
792 $slot = stripslashes( Params::post( 'slot', '' ) );
793 $gadsense_data = Advanced_Ads_AdSense_Data::get_instance();
794 $adsense_id = $gadsense_data->get_adsense_id();
795 $options = self::get_option();
796 $options['ad_codes'][ 'ca-' . $adsense_id . ':' . $slot ] = $code;
797 update_option( self::OPTION_KEY, $options );
798 header( 'Content-Type: application/json' );
799 echo wp_json_encode( [ 'status' => true ] );
800 }
801 die;
802 }
803
804 /**
805 * Get ad code for a given unit
806 */
807 public function ajax_get_ad_code() {
808 if ( ! Conditional::user_can( 'advanced_ads_edit_ads' ) ) {
809 wp_send_json_error( 'Unauthorized', 401 );
810 }
811
812 $post_data = wp_unslash( $_POST );
813 $nonce = isset( $post_data['nonce'] ) ? $post_data['nonce'] : '';
814 if ( false !== wp_verify_nonce( $nonce, 'advads-mapi' ) ) {
815 $unit = stripslashes( $post_data['unit'] );
816
817 if ( ! self::use_user_app() ) {
818 $quota = $this->get_quota();
819
820 // No more quota left.
821 if ( $quota['count'] < 1 ) {
822 header( 'Content-Type: application/json' );
823 $quota_msg = $this->get_quota_msg();
824 echo wp_json_encode(
825 [
826 'quota' => 0,
827 'quotaMsg' => $quota_msg,
828 ]
829 );
830 die;
831 }
832 }
833
834 $code = $this->get_ad_code( $unit );
835
836 /**
837 * Ad code is returned as string. Otherwise it's an error
838 */
839 if ( is_string( $code ) ) {
840 $ad_units = array_filter(
841 Advanced_Ads_Network_Adsense::get_instance()->get_external_ad_units(),
842 function ( Advanced_Ads_Ad_Network_Ad_Unit $ad_unit ) use ( $unit ) {
843 return $ad_unit->id === $unit;
844 }
845 );
846 $ad_unit = reset( $ad_units );
847 $response = [
848 'code' => $code,
849 'type' => self::format_ad_data( $ad_unit, 'type' ),
850 ];
851
852 /**
853 * Add quota info for default API creds
854 */
855 if ( ! self::use_user_app() ) {
856 $quota = $this->get_quota();
857 $quota_msg = $this->get_quota_msg();
858 $response['quota'] = $quota['count'];
859 $response['quotaMsg'] = $quota_msg;
860 }
861
862 header( 'Content-Type: application/json' );
863 echo wp_json_encode( $response );
864
865 } else {
866
867 // return info about the error.
868 header( 'Content-Type: application/json' );
869 echo wp_json_encode( $code );
870 }
871 }
872 die;
873 }
874
875 /**
876 * Dismiss an account alert
877 */
878 public function ajax_dismiss_alert() {
879 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
880 die;
881 }
882 $nonce = Params::post( 'nonce', '' );
883 if ( false !== wp_verify_nonce( $nonce, 'advads-mapi' ) ) {
884 $account = stripslashes( Params::post( 'account', '' ) );
885 $id = stripslashes( Params::post( 'id', '' ) );
886 $options = self::get_option();
887
888 $items = [];
889
890 // the account exists.
891 if ( isset( $options['accounts'][ $account ] ) ) {
892 // the alert exists.
893 if ( isset( $options['accounts'][ $account ]['alerts']['items'][ $id ] ) ) {
894 unset( $options['accounts'][ $account ]['alerts']['items'][ $id ] );
895
896 update_option( self::OPTION_KEY, $options );
897 $items = $options['accounts'][ $account ]['alerts']['items'];
898 }
899 }
900 wp_send_json(
901 [
902 'status' => true,
903 'alerts' => $items,
904 'length' => count( $items ),
905 ]
906 );
907 }
908 die;
909 }
910
911 /**
912 * Get / Update the list of alerts on an AdSense account.
913 */
914 public function ajax_get_account_alerts() {
915 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
916 die;
917 }
918
919 $nonce = Params::post( 'nonce', '' );
920 if ( false !== wp_verify_nonce( $nonce, 'mapi-alerts' ) ) {
921 $account = stripslashes( Params::post( 'account', '' ) );
922 $options = self::get_option();
923
924 // the account exists.
925 if ( isset( $options['accounts'][ $account ] ) && self::has_token( $account ) ) {
926 $access_token = self::get_access_token( $account );
927 $url = str_replace( 'PUBID', $account, self::ALERTS_URL );
928
929 if ( isset( $_POST['inlineAttempt'] ) ) {
930 if ( ! is_array( $options['accounts'][ $account ]['alerts'] ) ) {
931 $options['accounts'][ $account ]['alerts'] = [];
932 }
933 $options['accounts'][ $account ]['alerts']['inlineAttempt'] = time();
934 update_option( self::OPTION_KEY, $options );
935 }
936
937 // the token is valid.
938 if ( ! isset( $access_token['msg'] ) ) {
939 $headers = [
940 'Authorization' => 'Bearer ' . $access_token,
941 ];
942 $response = wp_remote_get( $url, [ 'headers' => $headers ] );
943
944 $this->log( 'Get AdSense alerts for ' . $account );
945
946 // the HTTP response is not an error.
947 if ( ! is_wp_error( $response ) ) {
948 $alerts = json_decode( $response['body'], true );
949
950 // the response body is valid.
951 if ( is_array( $alerts ) && isset( $alerts['alerts'] ) ) {
952 $items = [];
953 foreach ( $alerts['alerts'] as $item ) {
954 // Do not store alerts of type "INFO".
955 if ( strcasecmp( $item['severity'], 'INFO' ) !== 0 ) {
956 $items[ wp_generate_password( 6, false ) ] = $item;
957 }
958 }
959
960 // filter alerts that are not relevant to the user.
961 $items = self::filter_account_alerts( $items );
962
963 $alerts_array = [
964 'items' => $items,
965 'lastCheck' => time(),
966 ];
967
968 $options['accounts'][ $account ]['alerts'] = $alerts_array;
969 update_option( self::OPTION_KEY, $options );
970 $results = [
971 'status' => true,
972 'alerts' => $items,
973 'length' => count( $items ),
974 ];
975 header( 'Content-Type:application/json' );
976 echo wp_json_encode( $results );
977 } else {
978 $results = [
979 'status' => false,
980 'msg' => esc_html__( 'Invalid response body while retrieving account alerts', 'advanced-ads' ),
981 ];
982 header( 'Content-Type:application/json' );
983 echo wp_json_encode( $results );
984 }
985 } else {
986 $results = [
987 'status' => false,
988 'msg' => esc_html__( 'error while retrieving account alerts', 'advanced-ads' ),
989 'raw' => $response->get_error_message(),
990 ];
991 header( 'Content-Type:application/json' );
992 echo wp_json_encode( $results );
993 }
994 } else {
995 // return the original error info.
996 return $access_token;
997 }
998 } else {
999 header( 'Content-Type:application/json' );
1000 echo wp_json_encode( [ 'status' => false ] );
1001 }
1002 }
1003 die;
1004 }
1005
1006 /**
1007 * Get / Update the ad unit list for a given ad client. The corresponding <select /> input used in the ad selector is passed as a fied of an array
1008 */
1009 public function ajax_get_adUnits() {
1010 if ( ! Conditional::user_can( 'advanced_ads_edit_ads' ) ) {
1011 die;
1012 }
1013 $post_vars = wp_unslash( $_POST );
1014 $nonce = isset( $post_vars['nonce'] ) ? wp_strip_all_tags( $post_vars['nonce'] ) : '';
1015
1016 if ( wp_verify_nonce( $nonce, 'advads-mapi' ) && isset( $post_vars['account'] ) ) {
1017 $account = wp_strip_all_tags( stripslashes( $post_vars['account'] ) );
1018 $units = self::get_ad_units( $account );
1019
1020 if ( isset( $units['done'] ) && true === $units['done'] ) {
1021 ob_start();
1022 Advanced_Ads_AdSense_Admin::get_mapi_ad_selector();
1023 $ad_selector = ob_get_clean();
1024
1025 $response = [
1026 'status' => true,
1027 'html' => $ad_selector,
1028 ];
1029
1030 /**
1031 * Add quota info for default API creds
1032 */
1033 if ( ! self::use_user_app() ) {
1034 $quota = $this->get_quota();
1035 $quota_msg = $this->get_quota_msg();
1036 $response['quota'] = $quota['count'];
1037 $response['quotaMsg'] = $quota_msg;
1038 }
1039 } else {
1040 // Return the error info array.
1041 $response = $units;
1042 }
1043 header( 'Content-Type: application/json' );
1044 echo wp_json_encode( $response );
1045 }
1046 die;
1047 }
1048
1049 /**
1050 * Save account and token after account selection MCN.
1051 */
1052 public function ajax_account_selected() {
1053 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
1054 die;
1055 }
1056 $post_data = wp_unslash( $_POST );
1057 $nonce = $post_data['nonce'] ?? '';
1058
1059 if ( ! wp_verify_nonce( $nonce, 'advads-mapi' ) && ! check_ajax_referer( 'advanced_ads_wizard', 'nonce' ) ) {
1060 wp_send_json_error( 'Not Authorized', 401 );
1061 }
1062
1063 $token_data = wp_unslash( $post_data['token_data'] );
1064 $account = wp_unslash( $post_data['account'] );
1065
1066 if ( $token_data && $account ) {
1067 self::save_token_from_data( $token_data, $account );
1068 wp_send_json_success(
1069 [
1070 'status' => true,
1071 'account' => $account,
1072 ],
1073 200
1074 );
1075 }
1076 $error = 'Token data missing';
1077 if ( $token_data ) {
1078 $error = 'No account provided';
1079 }
1080
1081 wp_send_json_success(
1082 [
1083 'status' => false,
1084 'error_msg' => $error,
1085 ],
1086 200
1087 );
1088 }
1089
1090 /**
1091 * Get AdSense account details from a new access token.
1092 */
1093 public function ajax_get_account_details() {
1094 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
1095 wp_send_json_error( 'Unauthorized', 401 );
1096 }
1097
1098 $post_data = wp_unslash( $_POST );
1099 $nonce = $post_data['nonce'] ?? '';
1100
1101 if ( ! wp_verify_nonce( $nonce, 'advads-mapi' ) && ! check_ajax_referer( 'advanced_ads_wizard', 'nonce' ) ) {
1102 wp_send_json_error( 'Not Authorized', 401 );
1103 }
1104
1105 $url = 'https://adsense.googleapis.com/v2/accounts';
1106 $list_child_url = 'https://adsense.googleapis.com/v2/accounts/%pubid%:listChildAccounts';
1107 $token_data = wp_unslash( $post_data['token_data'] );
1108
1109 if ( ! is_array( $token_data ) ) {
1110 wp_send_json(
1111 [
1112 'status' => false,
1113 'error_msg' => esc_html__( 'No token provided. Token data needed to get account details.', 'advanced-ads' ),
1114 ]
1115 );
1116 }
1117
1118 $headers = [ 'Authorization' => 'Bearer ' . $token_data['access_token'] ];
1119 $response = wp_remote_get( $url, [ 'headers' => $headers ] );
1120
1121 self::log( 'Get account details from new access token' );
1122
1123 if ( is_wp_error( $response ) ) {
1124 wp_send_json(
1125 [
1126 'status' => false,
1127 'error_msg' => $response->get_error_message(),
1128 ]
1129 );
1130 }
1131 if ( trim( $response['body'] ) === '{}' ) {
1132 // No AdSense account.
1133 $options = self::get_option();
1134 $options['connect_error'] = [
1135 'message' => esc_html__( 'No AdSense account data found.', 'advanced-ads' ),
1136 'reason' => 'noAdsenseData',
1137 ];
1138 update_option( self::OPTION_KEY, $options );
1139 wp_send_json_error( [ 'error' => esc_html__( 'No AdSense account data found.', 'advanced-ads' ) ], 404 );
1140 }
1141
1142 $accounts = json_decode( trim( $response['body'] ), true );
1143
1144 if ( isset( $accounts['accounts'] ) ) {
1145 $pub_id = explode( '/', $accounts['accounts'][0]['name'] )[1];
1146 $child_accounts = wp_remote_get( str_replace( '%pubid%', $pub_id, $list_child_url ), [ 'headers' => $headers ] );
1147
1148 if ( is_wp_error( $child_accounts ) ) {
1149 wp_send_json_error( $child_accounts, 500 );
1150 }
1151 $accounts_list = json_decode( trim( $child_accounts['body'] ), true );
1152 if ( trim( $child_accounts['body'] ) === '{}' ) {
1153 // Standard AdSense account.
1154 self::save_token_from_data( $token_data, $accounts['accounts'][0] );
1155 wp_send_json_success(
1156 [
1157 'reload' => true,
1158 'account' => self::get_account_details( $accounts['accounts'][0] ),
1159 ]
1160 );
1161 }
1162
1163 if ( null !== $accounts_list ) {
1164 // Network account.
1165 $details = [];
1166 $html = '';
1167 $details[ $pub_id ] = [
1168 'id' => $pub_id,
1169 'name' => $accounts['accounts'][0]['displayName'],
1170 ];
1171 $html .= sprintf( '<option value="%1$s">%2$s [%3$s]</option>', esc_attr( $pub_id ), esc_html( $accounts['accounts'][0]['displayName'] ), esc_html( $pub_id ) );
1172
1173 foreach ( $accounts_list['accounts'] as $item ) {
1174 $account_id = explode( '/', $item['name'] )[1];
1175 $details[ $account_id ] = [
1176 'id' => $account_id,
1177 'name' => $item['displayName'],
1178 ];
1179 $html .= sprintf( '<option value="%1$s">%2$s [%3$s]</option>', esc_attr( $account_id ), esc_html( $item['displayName'] ), esc_html( $account_id ) );
1180 }
1181 wp_send_json_success(
1182 [
1183 'details' => $details,
1184 'html' => $html,
1185 'token_data' => $token_data,
1186 ]
1187 );
1188 }
1189 wp_send_json_error( [ 'message' => 'unexpected response - get child accounts' ], 400 );
1190 }
1191 }
1192
1193 /**
1194 * Submit Google API confirmation code. Save the token and update ad client list.
1195 */
1196 public function ajax_confirm_code() {
1197 if ( ! Conditional::user_can( 'advanced_ads_manage_options' ) ) {
1198 die;
1199 }
1200
1201 $nonce = wp_unslash( Params::post( 'nonce' ) ) ?? '';
1202
1203 if ( ! wp_verify_nonce( $nonce, 'advads-mapi' ) && ! check_ajax_referer( 'advanced_ads_wizard', 'nonce' ) ) {
1204 wp_send_json_error( 'Not Authorized', 401 );
1205 }
1206
1207 $code = urldecode( wp_unslash( Params::post( 'code' ) ) );
1208 $cid = self::CID;
1209 $cs = self::CS;
1210
1211 $use_user_app = self::use_user_app();
1212
1213 if ( $use_user_app ) {
1214 $cid = ADVANCED_ADS_MAPI_CID;
1215 $cs = ADVANCED_ADS_MAPI_CIS;
1216 }
1217
1218 $code_url = 'https://www.googleapis.com/oauth2/v4/token';
1219 $redirect_uri = self::REDIRECT_URI;
1220 $grant_type = 'authorization_code';
1221
1222 $args = [
1223 'timeout' => 10,
1224 'body' => [
1225 'code' => $code,
1226 'client_id' => $cid,
1227 'client_secret' => $cs,
1228 'redirect_uri' => $redirect_uri,
1229 'grant_type' => $grant_type,
1230 ],
1231 ];
1232
1233 $response = wp_remote_post( $code_url, $args );
1234
1235 self::log( 'Confirm authorization code' );
1236
1237 if ( is_wp_error( $response ) ) {
1238 wp_send_json_error(
1239 [
1240 'status' => false,
1241 'msg' => 'error while submitting code',
1242 'raw' => $response->get_error_message(),
1243 ],
1244 $response->get_error_code()
1245 );
1246 }
1247
1248 $token = json_decode( $response['body'], true );
1249
1250 if ( null !== $token && isset( $token['refresh_token'] ) ) {
1251 $expires = time() + absint( $token['expires_in'] );
1252 $token['expires'] = $expires;
1253 wp_send_json_success(
1254 [
1255 'status' => true,
1256 'token_data' => $token,
1257 ],
1258 200
1259 );
1260 }
1261
1262 wp_send_json_error(
1263 [
1264 'status' => false,
1265 'response_body' => $response['body'],
1266 ],
1267 400
1268 );
1269 }
1270
1271 /**
1272 * Enqueue admin scripts
1273 *
1274 * @param string $hook current page hook.
1275 */
1276 public function admin_scripts( $hook ) {
1277 if ( 'advanced-ads_page_advanced-ads-settings' === $hook ) {
1278 wp_enqueue_script( 'gasense/mapi/settings', GADSENSE_BASE_URL . 'admin/assets/js/mapi-settings.js', [ 'jquery', 'wp-util' ], ADVADS_VERSION );
1279 }
1280 }
1281
1282 /**
1283 * Print alert data in admin footer
1284 */
1285 public function admin_footer() {
1286 $data = Advanced_Ads_AdSense_Data::get_instance();
1287 $adsense_id = $data->get_adsense_id();
1288 $has_token = self::has_token( $adsense_id );
1289 $alerts = self::get_stored_account_alerts( $adsense_id );
1290 $refresh_alerts = false;
1291
1292 // default value, never checked for alerts.
1293 if ( [] === $alerts && $has_token ) {
1294 $refresh_alerts = true;
1295 }
1296 if ( $has_token && is_array( $alerts ) ) {
1297 // Check weekly for alerts.
1298 if ( isset( $alerts['lastCheck'] ) && time() > absint( $alerts['lastCheck'] ) + DAY_IN_SECONDS * 7 ) {
1299 $refresh_alerts = true;
1300 }
1301 // Only try to get the alerts in the background once a day.
1302 if ( isset( $alerts['inlineAttempt'] ) && time() < $alerts['inlineAttempt'] + DAY_IN_SECONDS ) {
1303 $refresh_alerts = false;
1304 }
1305 }
1306 if ( $refresh_alerts ) {
1307 $nonce = wp_create_nonce( 'mapi-alerts' );
1308 ?>
1309 <input type="hidden" id="advads-mapi-refresh-alerts" />
1310 <script type="text/javascript">
1311 ;(function($) {
1312
1313 $( '#mapi-alerts-overlay' ).css( 'display', 'block' );
1314
1315 var pubId = $( '#adsense-id' ).val();
1316 $.ajax({
1317 url: ajaxurl,
1318 type: 'post',
1319 data: {
1320 action: 'advads-mapi-get-alerts',
1321 account: '<?php echo esc_html( $adsense_id ); ?>',
1322 nonce: '<?php echo esc_html( $nonce ); ?>',
1323 inlineAttempt: 1,
1324 },
1325 success:function(response, status, XHR) {
1326 if ( 'undefined' != typeof response.alerts ) {
1327 $( '#advads-mapi-refresh-alerts' ).trigger( 'advadsMapiRefreshAlerts', [response] );
1328 }
1329 $( '#mapi-alerts-overlay' ).css( 'display', 'none' );
1330 },
1331 error:function(request, status, error) {
1332 $( '#mapi-alerts-overlay' ).css( 'display', 'none' );
1333 },
1334 });
1335
1336 })(window.jQuery);
1337 </script>
1338 <?php
1339 }
1340 }
1341
1342 /**
1343 * Logs a task to a file if ADVANCED_ADS_LOG_ADSENSE_API constant is defined and true.
1344 *
1345 * @param string $task The task to be logged. Default is 'No task provided'.
1346 *
1347 * @return void
1348 */
1349 public static function log( $task = 'No task provided' ): void {
1350 if ( ! defined( 'ADVANCED_ADS_LOG_ADSENSE_API' ) || ! ADVANCED_ADS_LOG_ADSENSE_API ) {
1351 return;
1352 }
1353
1354 $message = date_i18n( '[Y-m-d H:i:s]' ) . ' ' . $task . "\n";
1355 error_log( $message, 3, WP_CONTENT_DIR . '/advanced-ads-google-api-requests.log' );
1356 }
1357
1358 /**
1359 * Sort ad units list alphabetically
1360 *
1361 * @param array $adunits The array of ad units to be sorted.
1362 *
1363 * @return array The sorted array of ad units.
1364 */
1365 public static function get_sorted_adunits( $adunits ): array {
1366 $units_sorted_by_name = [];
1367 $units_by_id = [];
1368 foreach ( $adunits as $unit ) {
1369 $units_sorted_by_name[ $unit['name'] ] = $unit['id'];
1370 $units_by_id[ $unit['id'] ] = $unit;
1371 }
1372 ksort( $units_sorted_by_name );
1373 $units_sorted_by_name = array_flip( $units_sorted_by_name );
1374
1375 $results = [];
1376 foreach ( $units_sorted_by_name as $id => $name ) {
1377 $results[ $name ] = $units_by_id[ $id ];
1378 }
1379
1380 return $results;
1381 }
1382
1383 /**
1384 * Format ad type and size strings from Google for display
1385 *
1386 * @param Advanced_Ads_Ad_Network_Ad_Unit $ad_unit the ad unit for which to format the details.
1387 * @param string $format takes either type or size.
1388 *
1389 * @return string
1390 */
1391 public static function format_ad_data( Advanced_Ads_Ad_Network_Ad_Unit $ad_unit, $format = 'type' ) {
1392 if ( 'type' === $format ) {
1393 $str = $ad_unit->display_type;
1394 $options = self::get_option();
1395 if ( array_key_exists( $ad_unit->id, $options['ad_codes'] ) ) {
1396 preg_match_all( '/data-ad-format="(?<format>.+?)"|data-ad-layout="(?<layout>.+?)"/', $options['ad_codes'][ $ad_unit->id ], $matches );
1397 $format = array_filter( $matches['format'] );
1398 $layout = array_filter( $matches['layout'] );
1399 $format = reset( $format );
1400 $layout = reset( $layout );
1401 if ( empty( $format ) ) {
1402 $format = '';
1403 }
1404 if ( empty( $layout ) ) {
1405 $layout = '';
1406 }
1407
1408 if ( 'autorelaxed' === $format ) {
1409 $str = _x( 'Multiplex', 'AdSense ad type', 'advanced-ads' );
1410 } elseif ( 'fluid' === $format ) {
1411 if ( 'in-article' === $layout ) {
1412 $str = _x( 'In-article', 'AdSense ad type', 'advanced-ads' );
1413 } else {
1414 $str = _x( 'In-feed', 'AdSense ad type', 'advanced-ads' );
1415 }
1416 }
1417 }
1418
1419 switch ( $str ) {
1420 case 'DISPLAY':
1421 $str = _x( 'Display', 'AdSense ad type', 'advanced-ads' );
1422 break;
1423 case 'LINK':
1424 $str = _x( 'Link', 'AdSense ad type', 'advanced-ads' );
1425 break;
1426 case 'MATCHED_CONTENT':
1427 $str = _x( 'Multiplex', 'AdSense ad type', 'advanced-ads' );
1428 break;
1429 case 'ARTICLE':
1430 $str = _x( 'In-article', 'AdSense ad type', 'advanced-ads' );
1431 break;
1432 case 'FEED':
1433 $str = _x( 'In-feed', 'AdSense ad type', 'advanced-ads' );
1434 break;
1435 case 'TYPE_UNSPECIFIED':
1436 default:
1437 }
1438 } elseif ( 'size' === $format ) {
1439 // size.
1440 $str = '1x3' === $ad_unit->display_size
1441 ? 'Responsive' : $ad_unit->display_size;
1442
1443 if ( strpos( $str, 'SIZE_' ) === 0 ) {
1444 $str = str_replace( '_', 'x', substr( $str, 5 ) );
1445 }
1446 $str = ucfirst( strtolower( $str ) );
1447 } else {
1448 $str = '';
1449 }
1450
1451 return $str;
1452 }
1453
1454 /**
1455 * Check if the credential are the default ones or from user's app
1456 */
1457 public static function use_user_app() {
1458 if (
1459 ( defined( 'ADVANCED_ADS_MAPI_CID' ) && '' !== ADVANCED_ADS_MAPI_CID ) &&
1460 ( defined( 'ADVANCED_ADS_MAPI_CIS' ) && '' !== ADVANCED_ADS_MAPI_CIS )
1461 ) {
1462 return true;
1463 }
1464
1465 return false;
1466 }
1467
1468 /**
1469 * Checks if a Google AdSense token exists for the specified AdSense ID.
1470 *
1471 * @param string $adsense_id The AdSense ID to check for.
1472 *
1473 * @return bool Returns true if a token exists, false otherwise.
1474 */
1475 public static function has_token( $adsense_id = '' ): bool {
1476 if ( empty( $adsense_id ) ) {
1477 return false;
1478 }
1479
1480 $options = self::get_option();
1481 $look_for = self::use_user_app() ? 'user_app' : 'default_app';
1482
1483 if (
1484 isset( $options['accounts'][ $adsense_id ] ) &&
1485 ! empty( $options['accounts'][ $adsense_id ][ $look_for ]['refresh_token'] )
1486 ) {
1487 return true;
1488 }
1489
1490 return false;
1491 }
1492
1493 /**
1494 * Save token obtained from confirmation code
1495 *
1496 * @param string $token access token data.
1497 * @param array $details selected account details.
1498 */
1499 public static function save_token_from_data( $token, $details ) {
1500 $options = self::get_option();
1501 $adsense_id = isset( $details['id'] ) ? $details['id'] : explode( '/', $details['name'] )[1];
1502
1503 if ( ! isset( $options['accounts'][ $adsense_id ] ) ) {
1504 $options['accounts'][ $adsense_id ] = self::$empty_account_data;
1505 }
1506 if ( self::use_user_app() ) {
1507 $options['accounts'][ $adsense_id ]['user_app'] = [
1508 'access_token' => $token['access_token'],
1509 'refresh_token' => $token['refresh_token'],
1510 'expires' => $token['expires'],
1511 'token_type' => $token['token_type'],
1512 ];
1513 } else {
1514 $options['accounts'][ $adsense_id ]['default_app'] = [
1515 'access_token' => $token['access_token'],
1516 'refresh_token' => $token['refresh_token'],
1517 'expires' => $token['expires'],
1518 'token_type' => $token['token_type'],
1519 ];
1520 }
1521 $options['accounts'][ $adsense_id ]['details'] = [
1522 'id' => $adsense_id,
1523 'name' => isset( $details['displayName'] ) ? $details['displayName'] : $details['name'],
1524 ];
1525 $options['connect_error'] = [];
1526 update_option( self::OPTION_KEY, $options );
1527
1528 $gadsense_data = Advanced_Ads_AdSense_Data::get_instance();
1529 $gadsense_options = $gadsense_data->get_options();
1530 $gadsense_options['adsense-id'] = $adsense_id;
1531 update_option( GADSENSE_OPT_NAME, $gadsense_options );
1532 }
1533
1534 /**
1535 * Get a list of stored alerts for a given AdSense account.
1536 *
1537 * @param string $pub_id the publisher account.
1538 * @return array $alerts
1539 */
1540 public static function get_stored_account_alerts( $pub_id = '' ) {
1541 if ( empty( $pub_id ) ) {
1542 return false;
1543 }
1544
1545 $options = self::get_option();
1546
1547 if ( isset( $options['accounts'][ $pub_id ] ) ) {
1548 $account = [];
1549 if ( isset( $options['accounts'][ $pub_id ]['alerts'] ) && is_array( $options['accounts'][ $pub_id ]['alerts'] ) ) {
1550 $alerts = $options['accounts'][ $pub_id ]['alerts'];
1551 $account = self::filter_stored_account_alerts( $alerts );
1552 }
1553
1554 return $account;
1555 }
1556
1557 return false;
1558 }
1559
1560 /**
1561 * We filter out specific alerts from the AdSense account when they are
1562 * - duplicates
1563 * - irrelevant when placing ads in the frontend
1564 *
1565 * @param array $alert_items alerts.
1566 * @param null|array $disabled_alerts additional disabled alerts.
1567 * @return array filtered alert items.
1568 */
1569 public static function filter_account_alerts( array $alert_items, $disabled_alerts = null ) {
1570 if ( empty( $alert_items ) || ! is_array( $alert_items ) ) {
1571 return $alert_items;
1572 }
1573
1574 // the message IDs we don’t even import from AdSense.
1575 $disabled_adsense_alerts = [
1576 'SELLERS_JSON_CONSENT', // AdSense message: We encourage you to publish your seller information in the Google sellers.json file. Visit the account settings page to review your current visibility status.
1577 'REPORTING_HORIZON_LEGACY_DATA_NOTICE', // AdSense message: Data older than three years is no longer available in Reporting. This data can be downloaded for a limited time.
1578 ];
1579
1580 // additional messages to disable. Useful if the function is used in different situations.
1581 if ( ! empty( $disabled_alerts ) && is_array( $disabled_alerts ) ) {
1582 $disabled_adsense_alerts = array_merge( $disabled_adsense_alerts, $disabled_alerts );
1583 }
1584
1585 // remove alerts based on specific IDs.
1586 foreach ( $alert_items as $internal_id => $item ) {
1587 if (
1588 ( isset( $item['id'] ) && in_array( $item['id'], $disabled_adsense_alerts, true ) )
1589 || ( isset( $item['type'] ) && in_array( str_replace( '-', '_', strtoupper( $item['type'] ) ), $disabled_adsense_alerts, true ) )
1590 ) {
1591 unset( $alert_items[ $internal_id ] );
1592 }
1593 }
1594
1595 return $alert_items;
1596 }
1597
1598 /**
1599 * Remove alerts dynamically when showing them.
1600 * only applies to stored alerts and not when they are loaded directly from AdSense
1601 *
1602 * @param array $alerts Alert options.
1603 * @return array $alerts Modified alert options.
1604 */
1605 public static function filter_stored_account_alerts( array $alerts ) {
1606 if ( empty( $alerts['items'] ) || ! is_array( $alerts['items'] ) ) {
1607 return $alerts;
1608 }
1609
1610 $disabled_alerts = [];
1611
1612 /**
1613 * Asside from the basic filter, we also filter out some messages only from showing up while we still import them
1614 * This allows us to show them only under related conditions
1615 */
1616 // Remove `ads.txt` related alerts if the file is displayed to visitors.
1617 if ( Advanced_Ads_Ads_Txt_Admin::is_displayed() ) {
1618 $disabled_alerts = [ 'ALERT_TYPE_ADS_TXT_UNAUTHORIZED', 'ADS_TXT_MISSING', 'ADS_TXT_ISSUES' ];
1619 }
1620
1621 $alerts['items'] = self::filter_account_alerts( $alerts['items'], $disabled_alerts );
1622
1623 return $alerts;
1624 }
1625
1626 /**
1627 * Checks if there is any AdSense warning for the currently connected AdSense account.
1628 *
1629 * @param array $messages The array of messages.
1630 *
1631 * @return array The modified array.
1632 */
1633 public static function adsense_warnings_check( $messages ) {
1634 $data = Advanced_Ads_AdSense_Data::get_instance();
1635 $adsense_id = $data->get_adsense_id();
1636 $alerts = self::get_stored_account_alerts( $adsense_id );
1637
1638 if ( ! is_array( $messages ) ) {
1639 $messages = [];
1640 }
1641
1642 if ( ! empty( $alerts ) && ! empty( $alerts['items'] ) ) {
1643 $messages[] = sprintf(
1644 wp_kses(
1645 /* translators: %s admin setting page link */
1646 __( 'There are one or more warnings about the currently linked AdSense account. You can view them <a href="%s">here</a>', 'advanced-ads' ),
1647 [ 'a' => [ 'href' => true ] ]
1648 ),
1649 esc_url( admin_url( 'admin.php?page=advanced-ads-settings#top#adsense' ) )
1650 );
1651 }
1652 return $messages;
1653 }
1654
1655 /**
1656 * Get the class's option
1657 */
1658 public static function get_option() {
1659 $options = get_option( self::OPTION_KEY, [] );
1660 if ( ! is_array( $options ) ) {
1661 $options = [];
1662 }
1663 return $options + self::$default_options;
1664 }
1665
1666 /**
1667 * Get the URL to the AdSense error page
1668 *
1669 * @param string $code Add the error code to the URL.
1670 *
1671 * @return string The entire text with the url.
1672 */
1673 public static function get_adsense_error_link( $code = '' ) {
1674 if ( ! empty( $code ) ) {
1675 $code = '-' . $code;
1676 }
1677
1678 return sprintf(
1679 /* translators: %1$s is an anchor (link) opening tag, %2$s is the closing tag. */
1680 esc_attr__( 'Learn more about AdSense account issues %1$shere%2$s.', 'advanced-ads' ),
1681 '<a href="https://wpadvancedads.com/adsense-errors/?utm_source=advanced-ads&utm_medium=link&utm_campaign=adsense-error' . $code . '" target="_blank">',
1682 '</a>'
1683 );
1684 }
1685
1686 /**
1687 * Get custom account connection error message list.
1688 */
1689 public static function get_connect_error_messages() {
1690 $messages = [];
1691 $health_class = Advanced_Ads_Ad_Health_Notices::get_instance();
1692 foreach ( $health_class->default_notices as $key => $value ) {
1693 if ( 0 === strpos( $key, 'adsense_connect_' ) ) {
1694 $messages[ substr( $key, strlen( 'adsense_connect_' ) ) ] = $value['text'];
1695 }
1696 }
1697 return $messages;
1698 }
1699
1700 /**
1701 * Get custom messages for AdSense alerts.
1702 */
1703 public static function get_adsense_alert_messages() {
1704 $messages = [];
1705 $health_class = Advanced_Ads_Ad_Health_Notices::get_instance();
1706
1707 foreach ( $health_class->default_notices as $key => $value ) {
1708 if ( 0 === strpos( $key, 'adsense_alert_' ) ) {
1709 $messages[ substr( $key, strlen( 'adsense_alert_' ) ) ] = $value['text'];
1710 }
1711 }
1712 return $messages;
1713 }
1714
1715 /**
1716 * Get class instance
1717 *
1718 * @return Advanced_Ads_AdSense_MAPI
1719 */
1720 public static function get_instance() {
1721 if ( null === self::$instance ) {
1722 self::$instance = new self();
1723 }
1724
1725 return self::$instance;
1726 }
1727
1728 /**
1729 * Extract basic account info from the data sent by Google
1730 *
1731 * @param array $account account data from Google.
1732 *
1733 * @return array
1734 */
1735 private static function get_account_details( $account ) {
1736 return [
1737 'id' => str_replace( 'accounts/', '', $account['name'] ),
1738 'name' => $account['displayName'],
1739 ];
1740 }
1741
1742 /**
1743 * Get a hint for an error object that was received from AdSense
1744 *
1745 * @param string $reason The reason from the response's error.
1746 *
1747 * @return string|bool if there is a hint for this reason, a string containing the hint will be returned.
1748 */
1749 final public static function get_adsense_error_hint( $reason ) {
1750
1751 if ( 'authError' === $reason ) {
1752 return sprintf(
1753 /* translators: 1: A link to the settings page 2: The name of an ad network */
1754 __( 'Please try to <a href="%1$s" target="_blank">reconnect to your %2$s account</a>.', 'advanced-ads' ),
1755 admin_url( 'admin.php?page=advanced-ads-settings#top#adsense' ),
1756 'AdSense'
1757 );
1758 }
1759
1760 return false;
1761 }
1762 }
1763