PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / admin / pages / license / src / Services / ApiService.php

ApiService.php in wpForo Forum 3.1.6, at admin/pages/license/src/Services/ApiService.php

447 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace gVectors\License\Services;
4
5 // Exit if accessed directly
6 use gVectors\License\Config;
7 use gVectors\License\LicenseModule;
8
9 if( ! defined( 'ABSPATH' ) ) exit;
10
11 /**
12 * Handles all communication with the gVectors proxy server.
13 * NEVER communicates directly with Paddle API - all requests go through the proxy.
14 */
15 class ApiService {
16 private $config;
17 private $proxy_url;
18 private $timeout = 30;
19 private $products_transient_name;
20 private $all_addons_transient_name;
21 private $prices_transient_name;
22
23 public function __construct( Config $config ) {
24 $this->config = $config;
25 $this->proxy_url = trailingslashit( $this->config->get_proxy_server_url() );
26 $this->products_transient_name = $this->config->get_core_plugin_slug() . '_gvectors_products';
27 $this->all_addons_transient_name = $this->config->get_core_plugin_slug() . '_gvectors_all_addons';
28 $this->prices_transient_name = $this->config->get_core_plugin_slug() . '_gvectors_prices_';
29 }
30
31 /**
32 * Get single product details
33 */
34 public function get_product( string $product_id ): array {
35 $data = $this->get_products();
36 $products = $data['products'] ?? $data;
37 foreach( $products as $product ) {
38 if( $product['id'] === $product_id ) return $product;
39 }
40
41 return [];
42 }
43
44 // ==========================================
45 // Products
46 // ==========================================
47
48 /**
49 * Get all available gVectors products/addons from the License Server
50 */
51 public function get_products(): array {
52 $cached = get_transient( $this->products_transient_name );
53 if( $cached !== false ) return $cached;
54
55 $response = $this->request( 'products', [], 'GET' );
56 if( ! empty( $response['success'] ) && ! empty( $response['data'] ) ) {
57 $data = $response['data'];
58 // Handle wrapped response with checkout_mode
59 if( isset( $data['products'] ) && is_array( $data['products'] ) ) {
60 $result = [
61 'products' => $data['products'],
62 'checkout_mode' => ! empty( $data['checkout_mode'] ) ? $data['checkout_mode'] : 'both',
63 ];
64 } else {
65 // Backward compatibility: plain array of products
66 $result = [
67 'products' => $data,
68 'checkout_mode' => 'both',
69 ];
70 }
71 set_transient( $this->products_transient_name, $result, 6 * HOUR_IN_SECONDS );
72
73 return $result;
74 }
75
76 return [ 'products' => [], 'checkout_mode' => 'both' ];
77 }
78
79 /**
80 * Make an authenticated request to the proxy server
81 */
82 private function request( string $endpoint, array $body = [], string $method = 'POST' ): array {
83 $url = $this->proxy_url . ltrim( $endpoint, '/' );
84
85 $body['site_domain'] = LicenseModule::get_site_domain();
86 $body['site_token'] = LicenseModule::get_site_token();
87 $body['wp_version'] = get_bloginfo( 'version' );
88 $body['core_slug'] = $this->config->get_core_plugin_slug();
89 $body['core_version'] = $this->config->get_core_plugin_slug() . '-' . $this->config->get_core_plugin_version();
90 $body['php_version'] = phpversion();
91
92 $args = [
93 'timeout' => $this->timeout,
94 'sslverify' => true,
95 'headers' => [
96 'Content-Type' => 'application/json',
97 'Accept' => 'application/json',
98 'X-gVectors-Site' => LicenseModule::get_site_domain(),
99 'X-gVectors-Token' => LicenseModule::get_site_token(),
100 ],
101 ];
102
103 if( $method === 'GET' ) {
104 $url = add_query_arg( $body, $url );
105 $args['method'] = 'GET';
106 $response = wp_remote_get( $url, $args );
107 } else {
108 $args['body'] = wp_json_encode( $body );
109 $args['method'] = $method;
110 $response = wp_remote_post( $url, $args );
111 }
112
113 if( is_wp_error( $response ) ) {
114 return [
115 'success' => false,
116 'error' => $response->get_error_message(),
117 'code' => 'wp_error',
118 ];
119 }
120
121 $code = wp_remote_retrieve_response_code( $response );
122 $body = wp_remote_retrieve_body( $response );
123 $data = json_decode( $body, true );
124
125 if( $code < 200 || $code >= 300 ) {
126 return [
127 'success' => false,
128 'error' => $data['error'] ?? 'HTTP Error ' . $code,
129 'code' => $code,
130 'data' => $data,
131 ];
132 }
133
134 if( ! is_array( $data ) ) {
135 return [
136 'success' => false,
137 'error' => 'Invalid response from server',
138 'code' => 'invalid_response',
139 ];
140 }
141
142 return $data;
143 }
144
145 /**
146 * Get product prices
147 */
148 public function get_prices( string $product_id ): array {
149 $cached = get_transient( $this->prices_transient_name . $product_id );
150 if( $cached !== false ) return $cached;
151
152 $response = $this->request( 'products/' . $product_id . '/prices', [], 'GET' );
153 if( ! empty( $response['success'] ) && ! empty( $response['data'] ) ) {
154 set_transient( $this->prices_transient_name . $product_id, $response['data'], 6 * HOUR_IN_SECONDS );
155
156 return $response['data'];
157 }
158
159 return [];
160 }
161
162 // ==========================================
163 // Checkout
164 // ==========================================
165
166 /**
167 * Create a checkout transaction server-side via the proxy.
168 * Returns transaction_id for Paddle.js to open the checkout overlay.
169 */
170 public function check_overlap( string $price_id, string $product_id ): array {
171 return $this->request( 'checkout/check-overlap', [
172 'price_id' => $price_id,
173 'product_id' => $product_id,
174 ] );
175 }
176
177 public function create_checkout( string $price_id, string $product_id = '', array $custom_data = [], ?array $checkout_options = null ): array {
178 $body = [
179 'price_id' => $price_id,
180 'product_id' => $product_id,
181 'custom_data' => $custom_data,
182 ];
183 if( $checkout_options ) {
184 $body['checkout_options'] = $checkout_options;
185 }
186
187 return $this->request( 'checkout', $body );
188 }
189
190 /**
191 * Poll/verify a transaction status after checkout
192 */
193 public function verify_transaction( string $transaction_id ): array {
194 return $this->request( 'verify-transaction', [
195 'transaction_id' => $transaction_id,
196 ], 'GET' );
197 }
198
199 /**
200 * Abandoned-checkout phase check: is the transaction still unpaid?
201 * The proxy attaches the phase discount and returns the recovery email.
202 */
203 public function check_abandoned( string $transaction_id, int $phase_minutes ): array {
204 return $this->request( 'abandoned-checkout', [
205 'transaction_id' => $transaction_id,
206 'phase' => $phase_minutes,
207 ], 'GET' );
208 }
209
210 // ==========================================
211 // License
212 // ==========================================
213
214 /**
215 * Activate a license key for this site
216 */
217 public function activate_license( string $license_key, string $product_id = '' ): array {
218 return $this->request( 'license/activate', [
219 'license_key' => $license_key,
220 'product_id' => $product_id,
221 ] );
222 }
223
224 /**
225 * Unified activation — send any key type to license/activate; proxy detects the type.
226 * Pass an empty string for $key to trigger activate-by-domain.
227 */
228 public function activate_unified( string $key, string $product_id = '' ): array {
229 return $this->request( 'license/activate', [
230 'key' => $key,
231 'product_id' => $product_id,
232 ] );
233 }
234
235 /**
236 * Activate licenses by transaction ID (optionally for a specific product)
237 */
238 public function activate_license_by_transaction( string $transaction_id, string $product_id = '' ): array {
239 return $this->request( 'license/activate', [
240 'key' => $transaction_id,
241 'product_id' => $product_id,
242 ] );
243 }
244
245 /**
246 * Activate all licenses registered for this site's domain
247 */
248 public function activate_licenses_by_domain(): array {
249 return $this->request( 'license/activate', [ 'key' => '' ] );
250 }
251
252 /**
253 * Deactivate a license key for this site
254 */
255 public function deactivate_license( string $license_key ): array {
256 return $this->request( 'license/deactivate', [
257 'license_key' => $license_key,
258 ] );
259 }
260
261 /**
262 * Validate/check a license key status
263 */
264 public function validate_license( string $license_key ): array {
265 return $this->request( 'license/validate', [
266 'license_key' => $license_key,
267 ] );
268 }
269
270 /**
271 * Validate multiple license keys in a single request.
272 * Returns: { success: true, data: { licenses: { 'gvl_xxx': { status, valid, ... }, ... } } }
273 */
274 public function validate_licenses_batch( array $license_keys ): array {
275 return $this->request( 'license/validate-batch', [
276 'license_keys' => $license_keys,
277 ] );
278 }
279
280 // ==========================================
281 // Subscription
282 // ==========================================
283
284 /**
285 * Get subscription details
286 */
287 public function get_subscription( string $subscription_id ): array {
288 return $this->request( 'subscription/' . $subscription_id, [], 'GET' );
289 }
290
291 public function get_license_timeline( string $license_key ): array {
292 return $this->request( 'license/timeline', [ 'license_key' => $license_key ] );
293 }
294
295 /**
296 * Cancel a subscription
297 */
298 public function cancel_subscription( string $subscription_id ): array {
299 return $this->request( 'subscription/' . $subscription_id . '/cancel' );
300 }
301
302 /**
303 * Pause a subscription
304 */
305 public function pause_subscription( string $subscription_id ): array {
306 return $this->request( 'subscription/' . $subscription_id . '/pause' );
307 }
308
309 /**
310 * Resume a paused subscription
311 */
312 public function resume_subscription( string $subscription_id ): array {
313 return $this->request( 'subscription/' . $subscription_id . '/resume' );
314 }
315
316 /**
317 * Update subscription payment method
318 */
319 public function update_subscription_payment( string $subscription_id ): array {
320 return $this->request( 'subscription/' . $subscription_id . '/update-payment' );
321 }
322
323 /**
324 * Get Paddle customer portal URL for a subscription
325 */
326 public function get_subscription_portal_url( string $subscription_id ): array {
327 return $this->request( 'subscription/' . $subscription_id . '/portal-url' );
328 }
329
330 // ==========================================
331 // Legacy License (old gVectors license system)
332 // ==========================================
333
334 /**
335 * Check if an addon has a valid legacy license (from the old wp_gvt_activation_keys system)
336 * for this site domain. The proxy server checks the old license tables.
337 *
338 * Returns: { success: true, data: { has_legacy_license: bool, status: 'active'|'expired', expired: bool, ... } }
339 */
340 public function check_legacy_license( string $plugin_slug ): array {
341 return $this->request( 'addon/check-legacy-license', [
342 'plugin_slug' => $plugin_slug,
343 ] );
344 }
345
346 /**
347 * Batch-check legacy licenses for multiple addon slugs in a single request.
348 * More efficient than individual calls during cron verification.
349 *
350 * Returns: { success: true, data: { addons: { 'slug' => { has_legacy_license: bool, status: ..., ... }, ... } } }
351 */
352 public function check_legacy_licenses_batch( array $plugin_slugs ): array {
353 return $this->request( 'addon/check-legacy-licenses', [
354 'plugin_slugs' => $plugin_slugs,
355 ] );
356 }
357
358 /**
359 * Migrate an active legacy gVectors license to the new Paddle-based license system.
360 * The proxy server creates a new row in the licenses table using the original activation
361 * key. Returns full license data that the client stores in gvectors_licenses so that
362 * all future validation and update flows run through the new system.
363 */
364 public function activate_legacy_license( string $plugin_slug ): array {
365 return $this->request( 'addon/activate-legacy-license', [
366 'plugin_slug' => $plugin_slug,
367 ] );
368 }
369
370 // ==========================================
371 // Addon Updates
372 // ==========================================
373
374 /**
375 * Get the full list of all available addons from the proxy server (installed or not).
376 * Returns an array of addons with slug, name, version, description.
377 */
378 public function get_all_addons(): array {
379 $cached = get_transient( $this->all_addons_transient_name );
380 if( $cached !== false ) return [ 'success' => true, 'data' => $cached ];
381
382 $response = $this->request( 'addon/list', [], 'GET' );
383 if( ! empty( $response['success'] ) && ! empty( $response['data']['addons'] ) ) {
384 set_transient( $this->all_addons_transient_name, $response['data'], 12 * HOUR_IN_SECONDS );
385
386 return $response;
387 }
388
389 return $response;
390 }
391
392 // ==========================================
393 // Addon Downloads
394 // ==========================================
395
396 /**
397 * Get a signed download URL for an addon
398 */
399 public function get_addon_download_url( string $product_id, string $license_key ): array {
400 return $this->request( 'addon/download-url', [
401 'product_id' => $product_id,
402 'license_key' => $license_key,
403 ] );
404 }
405
406 // ==========================================
407 // Trial
408 // ==========================================
409
410 /**
411 * Start a free trial for a product
412 */
413 public function start_trial( string $product_id ): array {
414 return $this->request( 'trial/start', [
415 'product_id' => $product_id,
416 ] );
417 }
418
419 // ==========================================
420 // Account / Customer
421 // ==========================================
422
423 /**
424 * Get customer account info (purchases, subscriptions, licenses)
425 */
426 public function get_account(): array {
427 return $this->request( 'account', [], 'GET' );
428 }
429
430 /**
431 * Clear all cached data
432 */
433 public function clear_cache(): void {
434 delete_transient( $this->products_transient_name );
435 delete_transient( $this->all_addons_transient_name );
436
437 global $wpdb;
438 $wpdb->query(
439 $wpdb->prepare(
440 "DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
441 '_transient_' . $this->prices_transient_name . '%',
442 '_transient_timeout_' . $this->prices_transient_name . '%'
443 )
444 );
445 }
446 }
447