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 / includes / license / class-license-shop-client.php
advanced-ads / includes / license Last commit date
class-license-exchange.php 2 days ago class-license-package-installer.php 2 days ago class-license-product-map.php 2 days ago class-license-shop-client.php 2 days ago class-license-site-activation.php 2 days ago class-license.php 2 days ago utils.php 2 days ago
class-license-shop-client.php
489 lines
1 <?php
2 /**
3 * HTTP client for license shop REST (activate, deactivate, validate).
4 *
5 * @package AdvancedAds
6 * @since 2.0.9
7 */
8
9 namespace AdvancedAds\License;
10
11 use AdvancedAds\Constants;
12 use WP_Error;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Shop REST calls and local/development HTTP bypass for `.test` hosts.
18 */
19 final class License_Shop_Client {
20
21 /**
22 * Shop REST activate endpoint URL.
23 *
24 * @return string
25 */
26 public static function get_activate_endpoint(): string {
27 $base = defined( 'AA_SHOP_URL' ) ? AA_SHOP_URL : 'https://wpadvancedads.com';
28
29 return untrailingslashit( $base ) . '/wp-json/advanced-ads/v2/license/activate';
30 }
31
32 /**
33 * Shop REST deactivate endpoint URL.
34 *
35 * @return string
36 */
37 public static function get_deactivate_endpoint(): string {
38 $base = defined( 'AA_SHOP_URL' ) ? AA_SHOP_URL : 'https://wpadvancedads.com';
39
40 return untrailingslashit( $base ) . '/wp-json/advanced-ads/v2/license/deactivate';
41 }
42
43 /**
44 * Shop REST validate endpoint URL (fresh package download URLs for this site).
45 *
46 * @return string
47 */
48 public static function get_validate_endpoint(): string {
49 $base = defined( 'AA_SHOP_URL' ) ? AA_SHOP_URL : 'https://wpadvancedads.com';
50
51 return untrailingslashit( $base ) . '/wp-json/advanced-ads/v2/license/validate';
52 }
53
54 /**
55 * Whether the configured shop/API host is a local or development environment.
56 *
57 * Laragon, Valet, and similar setups use `.test` / `.local` hosts that resolve to
58 * 127.0.0.1. WordPress then rejects outbound requests via {@see wp_http_validate_url()}
59 * unless the host is treated as external.
60 *
61 * @return bool
62 */
63 public static function uses_local_development_host(): bool {
64 if ( defined( 'AA_SHOP_URL' ) && ! License_Utils::should_verify_ssl_for_url( AA_SHOP_URL ) ) {
65 return true;
66 }
67
68 if ( ! License_Utils::should_verify_ssl_for_url( site_url() ) ) {
69 return true;
70 }
71
72 return ! License_Utils::should_verify_ssl_for_url( Constants::API_ENDPOINT );
73 }
74
75 /**
76 * Mark the configured shop host as external for local/development HTTP requests.
77 *
78 * @param bool $is_external Whether WordPress considers the host external.
79 * @param string $host Request host.
80 * @param string $url Request URL.
81 * @return bool
82 */
83 public static function allow_local_development_http_request( $is_external, string $host, string $url ) {
84 foreach ( self::get_configured_api_hosts() as $shop_host ) {
85 if ( $host === $shop_host ) {
86 return true;
87 }
88 }
89
90 if ( self::uses_local_development_host() && ! License_Utils::should_verify_ssl_for_url( 'https://' . $host ) ) {
91 return true;
92 }
93
94 return $is_external;
95 }
96
97 /**
98 * Register the local/development shop HTTP bypass for admin add-on updates.
99 *
100 * @return void
101 */
102 public static function register_local_development_http_filters(): void {
103 self::add_local_development_http_filter();
104 }
105
106 /**
107 * Shop hosts from {@see AA_SHOP_URL} and {@see Constants::API_ENDPOINT}.
108 *
109 * @return string[]
110 */
111 public static function get_configured_api_hosts(): array {
112 $hosts = [];
113
114 if ( defined( 'AA_SHOP_URL' ) ) {
115 $shop_host = wp_parse_url( AA_SHOP_URL, PHP_URL_HOST );
116 if ( is_string( $shop_host ) && '' !== $shop_host ) {
117 $hosts[] = $shop_host;
118 }
119 }
120
121 $api_host = wp_parse_url( Constants::API_ENDPOINT, PHP_URL_HOST );
122 if ( is_string( $api_host ) && '' !== $api_host ) {
123 $hosts[] = $api_host;
124 }
125
126 return array_values( array_unique( $hosts ) );
127 }
128
129 /**
130 * HTTP timeout (seconds) for shop REST calls.
131 *
132 * @return int
133 */
134 public static function http_timeout(): int {
135 return 1200;
136 }
137
138 /**
139 * HTTP timeout (seconds) for signed package download URLs.
140 *
141 * @param string $download_url Package URL.
142 * @return int
143 */
144 public static function package_download_timeout( string $download_url = '' ): int {
145 unset( $download_url );
146
147 return 1200;
148 }
149
150 /**
151 * Whether outbound HTTPS to the shop should verify certificates.
152 *
153 * @return bool
154 */
155 public static function should_verify_ssl(): bool {
156 if ( ! defined( 'AA_SHOP_URL' ) ) {
157 return true;
158 }
159
160 return License_Utils::should_verify_ssl_for_url( AA_SHOP_URL );
161 }
162
163 /**
164 * Whether a package download URL targets the configured shop host.
165 *
166 * @param string $download_url Package download URL from the shop.
167 * @return bool
168 */
169 public static function is_shop_download_url( string $download_url ): bool {
170 $host = wp_parse_url( $download_url, PHP_URL_HOST );
171 if ( ! is_string( $host ) || '' === $host ) {
172 return false;
173 }
174
175 foreach ( self::get_configured_api_hosts() as $shop_host ) {
176 if ( $host === $shop_host ) {
177 return true;
178 }
179 }
180
181 return false;
182 }
183
184 /**
185 * Temporarily allow shop HTTP during a single license API or download request.
186 *
187 * @return void
188 */
189 public static function add_local_development_http_filter(): void {
190 if ( ! self::uses_local_development_host() ) {
191 return;
192 }
193
194 add_filter( 'http_request_host_is_external', [ self::class, 'allow_local_development_http_request' ], 10, 3 );
195 }
196
197 /**
198 * Remove the temporary local/development shop HTTP bypass.
199 *
200 * @return void
201 */
202 public static function remove_local_development_http_filter(): void {
203 remove_filter( 'http_request_host_is_external', [ self::class, 'allow_local_development_http_request' ], 10 );
204 }
205
206 /**
207 * HTTP args for server-side shop REST calls.
208 *
209 * @param array<string, mixed> $args Request args to merge.
210 * @return array<string, mixed>
211 */
212 public static function http_request_args( array $args = [] ): array {
213 $merged = array_merge(
214 [
215 'timeout' => self::http_timeout(),
216 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8' ],
217 ],
218 $args
219 );
220
221 if ( ! self::should_verify_ssl() ) {
222 $merged['sslverify'] = false;
223 }
224
225 return $merged;
226 }
227
228 /**
229 * Deactivate a site on the shop REST (/license/deactivate).
230 *
231 * @param string $license_key License key string.
232 * @param string $site Site hostname.
233 * @return array<int, array<string, mixed>>|WP_Error Rich list when shop returns one, else [].
234 */
235 public static function request_deactivate( string $license_key, string $site ) {
236 $license_key = trim( $license_key );
237 $site = trim( $site );
238
239 if ( '' === $license_key || '' === $site ) {
240 return new WP_Error(
241 'advanced_ads_license_deactivate_invalid',
242 __( 'Provide license and site.', 'advanced-ads' )
243 );
244 }
245
246 self::add_local_development_http_filter();
247
248 $response = wp_remote_post(
249 self::get_deactivate_endpoint(),
250 self::http_request_args(
251 [
252 'body' => wp_json_encode(
253 [
254 'license' => $license_key,
255 'site' => $site,
256 ]
257 ),
258 ]
259 )
260 );
261
262 self::remove_local_development_http_filter();
263
264 if ( is_wp_error( $response ) ) {
265 return $response;
266 }
267
268 $code = wp_remote_retrieve_response_code( $response );
269 if ( $code < 200 || $code >= 300 ) {
270 $body = json_decode( wp_remote_retrieve_body( $response ), true );
271 $message = is_array( $body ) ? (string) ( $body['message'] ?? $body['data']['message'] ?? '' ) : '';
272
273 return new WP_Error(
274 'advanced_ads_license_deactivate_http',
275 '' !== $message ? $message : __( 'Failed to deactivate license.', 'advanced-ads' ),
276 [ 'status' => $code ]
277 );
278 }
279
280 return self::parse_rich_response( wp_remote_retrieve_body( $response ) );
281 }
282
283 /**
284 * Activate a site on the shop REST (/license/activate).
285 *
286 * @param string $license_key License key string.
287 * @param string $site Site hostname.
288 * @param int $license_id Optional EDD SL license post ID.
289 * @return array<int, array<string, mixed>>|WP_Error Rich list when shop returns one, else [].
290 */
291 public static function request_activate( string $license_key, string $site, int $license_id = 0 ) {
292 $license_key = trim( $license_key );
293 $site = trim( $site );
294
295 if ( '' === $license_key || '' === $site ) {
296 return new WP_Error(
297 'advanced_ads_license_activate_invalid',
298 __( 'Provide license and site.', 'advanced-ads' )
299 );
300 }
301
302 $payload = [
303 'license' => $license_key,
304 'site' => $site,
305 ];
306 if ( $license_id > 0 ) {
307 $payload['license_id'] = $license_id;
308 }
309
310 self::add_local_development_http_filter();
311
312 $response = wp_remote_post(
313 self::get_activate_endpoint(),
314 self::http_request_args(
315 [
316 'body' => wp_json_encode( $payload ),
317 ]
318 )
319 );
320
321 self::remove_local_development_http_filter();
322
323 if ( is_wp_error( $response ) ) {
324 return $response;
325 }
326
327 $code = wp_remote_retrieve_response_code( $response );
328 if ( $code < 200 || $code >= 300 ) {
329 $body = json_decode( wp_remote_retrieve_body( $response ), true );
330 $api_code = is_array( $body ) ? (string) ( $body['code'] ?? '' ) : '';
331 if ( $license_id > 0 && 403 === (int) $code && 'identity_mismatch' === $api_code ) {
332 return self::request_activate( $license_key, $site, 0 );
333 }
334
335 $message = is_array( $body ) ? (string) ( $body['message'] ?? $body['data']['message'] ?? '' ) : '';
336
337 return new WP_Error(
338 'advanced_ads_license_activate_http',
339 '' !== $message ? $message : __( 'Failed to activate license.', 'advanced-ads' ),
340 [ 'status' => $code ]
341 );
342 }
343
344 return self::parse_rich_response( wp_remote_retrieve_body( $response ) );
345 }
346
347 /**
348 * Fetch one license row from the shop validate endpoint.
349 *
350 * @param array<string, mixed> $row License row containing licenseKey.
351 * @return array<string, mixed>|WP_Error Updated row or error.
352 */
353 public static function fetch_license_row( array $row ) {
354 $license_key = trim( (string) ( $row['licenseKey'] ?? '' ) );
355 if ( '' === $license_key ) {
356 return $row;
357 }
358
359 self::add_local_development_http_filter();
360
361 $response = wp_remote_post(
362 self::get_validate_endpoint(),
363 self::http_request_args(
364 [
365 'body' => wp_json_encode(
366 [
367 'license' => $license_key,
368 'site' => site_url(),
369 ]
370 ),
371 ]
372 )
373 );
374
375 self::remove_local_development_http_filter();
376
377 if ( is_wp_error( $response ) ) {
378 return $response;
379 }
380
381 $code = (int) wp_remote_retrieve_response_code( $response );
382 if ( 200 !== $code ) {
383 return self::validate_http_error( $response, $code );
384 }
385
386 $data = json_decode( wp_remote_retrieve_body( $response ), true );
387 if ( ! is_array( $data ) ) {
388 return new WP_Error(
389 'advanced_ads_license_validate_parse',
390 __( 'Invalid license validate response from the shop.', 'advanced-ads' )
391 );
392 }
393
394 $rows = isset( $data[0] ) ? $data : ( isset( $data['licenses'] ) && is_array( $data['licenses'] ) ? $data['licenses'] : $data );
395 if ( ! is_array( $rows ) ) {
396 return new WP_Error(
397 'advanced_ads_license_validate_parse',
398 __( 'Invalid license validate response from the shop.', 'advanced-ads' )
399 );
400 }
401
402 foreach ( $rows as $fresh ) {
403 if ( ! is_array( $fresh ) ) {
404 continue;
405 }
406 if ( (string) ( $fresh['licenseKey'] ?? '' ) === $license_key ) {
407 return $fresh;
408 }
409 }
410
411 if ( isset( $rows['licenseKey'] ) ) {
412 return $rows;
413 }
414
415 return $row;
416 }
417
418 /**
419 * Whether a shop validate error means the license key no longer exists.
420 *
421 * @param WP_Error $error Error from {@see self::fetch_license_row()}.
422 * @return bool
423 */
424 public static function is_validate_not_found_error( WP_Error $error ): bool {
425 if ( 'not_found' === $error->get_error_code() ) {
426 return true;
427 }
428
429 $data = $error->get_error_data();
430 if ( is_array( $data ) && 404 === (int) ( $data['status'] ?? 0 ) ) {
431 return true;
432 }
433
434 return false;
435 }
436
437 /**
438 * Parse shop JSON body into a normalized rich license list.
439 *
440 * @param string $body Response body.
441 * @return array<int, array<string, mixed>>
442 */
443 private static function parse_rich_response( string $body ): array {
444 $data = json_decode( $body, true );
445 if ( ! is_array( $data ) ) {
446 return [];
447 }
448
449 if ( isset( $data[0] ) && is_array( $data[0] ) ) {
450 return License::normalize_list( $data );
451 }
452
453 if ( isset( $data['licenseKey'] ) ) {
454 return License::normalize_list( [ $data ] );
455 }
456
457 return [];
458 }
459
460 /**
461 * Build WP_Error from a non-200 shop validate HTTP response.
462 *
463 * @param array<string, mixed>|\WP_HTTP_Requests_Response $response HTTP response.
464 * @param int $code HTTP status code.
465 * @return WP_Error
466 */
467 private static function validate_http_error( $response, int $code ): WP_Error {
468 $body = json_decode( wp_remote_retrieve_body( $response ), true );
469 $message = is_array( $body ) ? (string) ( $body['message'] ?? '' ) : '';
470 $error_code = is_array( $body ) && ! empty( $body['code'] )
471 ? sanitize_key( (string) $body['code'] )
472 : 'advanced_ads_license_validate_http';
473
474 if ( '' === $message ) {
475 $message = sprintf(
476 /* translators: %d: HTTP status code */
477 __( 'Could not refresh license download URLs from the shop (HTTP %d).', 'advanced-ads' ),
478 $code
479 );
480 }
481
482 return new WP_Error(
483 $error_code,
484 $message,
485 [ 'status' => $code ]
486 );
487 }
488 }
489