Skip to content

Commit f0502fb

Browse files
authored
Merge pull request #16 from gravitywpcom/hub
Hub
2 parents edc0427 + 0cbc615 commit f0502fb

17 files changed

Lines changed: 7599 additions & 140 deletions

src/LicenseHandler.php

Lines changed: 402 additions & 90 deletions
Large diffs are not rendered by default.

src/changelog.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
= 2.1.0 =
2+
- NEW: Centralized Hub system — single API request returns license info + ALL plugins data.
3+
- NEW: Support for both Global License Key (All Access, List Add-ons, Agency) and Individual Plugin Keys (Single Add-ons).
4+
- NEW: Auto-detection of plan type from the server (All Access, List Add-ons, Single Add-on, Agency).
5+
- NEW: GravityWP Settings page with tabbed interface (Global / Individual Keys / Overview).
6+
- NEW: GravityWP Hub catalog page with modern card-based layout.
7+
- NEW: Hub_Manager with 12-hour cache reduces API requests from 20+/day to 2/day per customer.
8+
- NEW: Stale cache fallback when API is unreachable (Cloudflare blocks, network errors, etc.).
9+
- NEW: Concurrent request lock to prevent duplicate API calls from multiple plugins.
10+
- NEW: Auto-migration — legacy per-plugin keys silently migrate to Global or Individual storage.
11+
- NEW: Access source badges on plugin cards (Global vs Individual).
12+
- CHANGED: Plugin settings pages no longer show a license input — all keys managed centrally.
13+
- CHANGED: LicenseHandler uses Hub_Manager for validation when available.
14+
- CHANGED: Update checks and download links come from the hub cache (zero extra API calls).
15+
16+
= 2.0.6 =
17+
- Implementing the global licensed key.
18+
19+
= 2.0.5 =
20+
- Fix non unique #id in plugin setting.
21+
122
= 2.0.4 =
223
- Optimizing the validation function caches the result.
324

src/pluginUpdater.php

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ class Plugin_Updater {
3131
*
3232
* @var $result_count
3333
*/
34-
private $api_url_update = 'https://my.gravitywp.com/wp-json/paddlepress-api/v1/update';
34+
private $api_url_update = 'https://stg-mygravitywpcom-mygwpstage.kinsta.cloud/wp-json/paddlepress-api/v1/update';
3535

3636
/**
3737
* GravityWP license url endpoint of the API
3838
*
3939
* @var $result_count
4040
*/
41-
private $api_url_license = 'https://my.gravitywp.com/wp-json/paddlepress-api/v1/license';
41+
private $api_url_license = 'https://stg-mygravitywpcom-mygwpstage.kinsta.cloud/wp-json/paddlepress-api/v1/license';
4242

4343
/**
4444
* HTTP parameters on API requests
@@ -224,7 +224,36 @@ public function request_is_activate( $field_setting ) {
224224
$this->error_messages = nl2br( $this->generateErrorMessage( $json_data['message'] ) );
225225
return false;
226226
} else {
227-
$this->error_messages = nl2br( $this->generateErrorMessage( 'Please try again later. If the issue persists, please contact support.' ) );
227+
// Retrieve HTTP status code and headers for additional context.
228+
$http_code = wp_remote_retrieve_response_code( $response );
229+
$headers = wp_remote_retrieve_headers( $response );
230+
231+
// Check for block by Cloudflare.
232+
if ( $http_code === 403 && strpos( strtolower( $body ), 'cloudflare' ) !== false ) {
233+
$this->error_messages = nl2br(
234+
$this->generateErrorMessage(
235+
'Access to the license server was denied by Cloudflare. This happens when malicious activity was detected from your website\'s outgoing IP address. This often happens on shared hosting where other users use the same IP address for malicious activity. Contact your hosting provider to resolve this issue. For more information, visit <a href="https://gravitywp.com/doc/license-activation-issues/">this page</a>.'
236+
)
237+
);
238+
return false;
239+
}
240+
241+
// Format additional information into a string for logging or support use.
242+
$extra_info = sprintf(
243+
"HTTP Status Code: %d\nResponse Headers: %s",
244+
$http_code,
245+
json_encode( $headers )
246+
);
247+
248+
// Provide a detailed error message including the response code, extra info, and a reference URL.
249+
$this->error_messages = nl2br(
250+
$this->generateErrorMessage(
251+
sprintf(
252+
'An unexpected error occurred. Please try again later. You can also refer to <a href="https://gravitywp.com/doc/license-activation-issues/">this page</a> for more details. If the issue persists, provide the following information to support: %s.',
253+
esc_html( $extra_info ) // Ensure special characters in the info are safely included in an HTML context.
254+
)
255+
)
256+
);
228257
return false;
229258
}
230259
}
@@ -260,7 +289,7 @@ public function generateErrorMessage( $error ) {
260289
}
261290
} else {
262291
$sanitized_message = sanitize_text_field( $messages );
263-
$error_message .= "- $sanitized_message\n";
292+
$error_message .= "- $sanitized_message\n";
264293
}
265294
}
266295
} else {
@@ -323,65 +352,60 @@ public function gwp_is_valid( $cached, $key = null ) {
323352
}
324353

325354
/**
326-
* Checks the plugin's license status and updates the transient data accordingly.
327-
*
328-
* This method verifies the validity of the plugin's license key and updates the cached
329-
* license status. It also manages admin notices based on the validation result. If the
330-
* license key is valid, it removes any existing admin notices. Otherwise, it adds an
331-
* admin notice to inform the user about the invalid license status.
332-
*
333-
* Key Details:
334-
* - Verifies if the current context is not the plugins page in a multisite network,
335-
* and returns early if true.
336-
* - Checks existing transient data for license status response, and skips further checks
337-
* if the data is already populated unless overridden.
338-
* - Retrieves the license key and generates a unique cache key for the status request.
339-
* - Calls the API to check the license activation status.
340-
* - Updates the license status cache with the API response.
341-
* - Based on the license validation result, either removes or adds admin notices.
355+
* Checks the plugin's license status using the centralized Hub cache.
342356
*
343-
* @param stdClass $_transient_data Transient data object containing update information.
344-
* If not provided as an object, it initializes a new stdClass object.
357+
* Since v2.1.0: Reads from Hub_Manager's shared cache instead of
358+
* making individual API calls. This is the key optimization —
359+
* one hub request serves ALL plugins.
345360
*
346-
* @global string $pagenow Current admin page identifier.
361+
* @param stdClass $_transient_data Transient data object.
347362
*
348363
* @return stdClass The (possibly modified) transient data object.
349-
*
350-
* @uses request_is_activate() Makes an API request to validate the license key.
351-
* @uses set_version_info_cache() Caches the result of the license key validation.
352-
* @uses gwp_is_valid() Validates the license key using cached data or via API.
353364
*/
354-
public function check_update_license($_transient_data) {
365+
public function check_update_license( $_transient_data ) {
355366
global $pagenow;
356367

357-
// Ensure $_transient_data is an object.
358-
if (!is_object($_transient_data)) {
368+
if ( ! is_object( $_transient_data ) ) {
359369
$_transient_data = new stdClass();
360370
}
361371

362-
// Return early if on the plugins page in a multisite network.
363-
if ('plugins.php' === $pagenow && is_multisite()) {
372+
if ( 'plugins.php' === $pagenow && is_multisite() ) {
364373
return $_transient_data;
365374
}
366375

367-
// Check if the transient data already has a response and is not being overridden.
368-
if (!empty($_transient_data->response) && !empty($_transient_data->response[$this->name]) && false === $this->wp_override) {
376+
if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
369377
return $_transient_data;
370378
}
371379

372-
// Retrieve license key and generate a unique cache key.
373-
$license_key = $this->api_data['license_key'];
374-
$status_cache_key = 'paddlepress_status_request_' . md5(serialize($this->slug . $this->api_data['license_key'] . $this->beta)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
380+
// Use Hub_Manager if available (v2.1.0+).
381+
if ( class_exists( '\GravityWP\Shared\Hub_Manager' ) ) {
382+
$has_access = \GravityWP\Shared\Hub_Manager::has_access( $this->slug );
375383

376-
// Validate the license key through the API and update the cache.
377-
$status = $this->request_is_activate($license_key);
378-
$this->set_version_info_cache($status, $status_cache_key);
384+
if ( $has_access ) {
385+
remove_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
386+
} else {
387+
add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
388+
}
389+
390+
return $_transient_data;
391+
}
392+
393+
// Fallback to direct API call if Hub_Manager not loaded yet.
394+
$license_key = get_option( 'gravitywp_global_license_key', '' );
395+
396+
if ( empty( $license_key ) ) {
397+
add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
398+
return $_transient_data;
399+
}
379400

380-
// Validate the license key and manage admin notices based on validity.
381-
if ($this->gwp_is_valid(false, $license_key)) {
382-
remove_action('admin_notices', array($this->handler_class, 'action_admin_notices'));
401+
$status_cache_key = 'paddlepress_status_request_' . md5( serialize( $this->slug . $license_key . $this->beta ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
402+
$status = $this->request_is_activate( $license_key );
403+
$this->set_version_info_cache( $status, $status_cache_key );
404+
405+
if ( $this->gwp_is_valid( false, $license_key ) ) {
406+
remove_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
383407
} else {
384-
add_action('admin_notices', array($this->handler_class, 'action_admin_notices'));
408+
add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) );
385409
}
386410

387411
return $_transient_data;
@@ -418,8 +442,35 @@ public function check_update( $_transient_data ) {
418442
return $_transient_data;
419443
}
420444

421-
$version_info = false; // $this->get_cached_version_info();
445+
$version_info = false;
446+
447+
// Try Hub_Manager first (no extra API call needed).
448+
if ( class_exists( '\GravityWP\Shared\Hub_Manager' ) ) {
449+
$hub_plugin_data = \GravityWP\Shared\Hub_Manager::get_plugin_data( $this->slug );
450+
451+
if ( ! empty( $hub_plugin_data ) && ! empty( $hub_plugin_data['new_version'] ) ) {
452+
$version_info = new stdClass();
453+
$version_info->name = $hub_plugin_data['name'] ?? '';
454+
$version_info->slug = $hub_plugin_data['slug'] ?? $this->slug;
455+
$version_info->new_version = $hub_plugin_data['new_version'];
456+
$version_info->url = '';
457+
$version_info->plugin = $this->name;
458+
$version_info->icons = $hub_plugin_data['icons'] ?? array();
459+
$version_info->banners = $hub_plugin_data['banners'] ?? array();
460+
$version_info->sections = $hub_plugin_data['sections'] ?? array();
461+
$version_info->requires = $hub_plugin_data['requires'] ?? '';
462+
$version_info->tested = $hub_plugin_data['tested'] ?? '';
463+
$version_info->requires_php = $hub_plugin_data['requires_php'] ?? '';
464+
465+
// Only include download link if license covers this plugin.
466+
if ( ! empty( $hub_plugin_data['has_access'] ) && ! empty( $hub_plugin_data['download_link'] ) ) {
467+
$version_info->package = $hub_plugin_data['download_link'];
468+
$version_info->download_link = $hub_plugin_data['download_link'];
469+
}
470+
}
471+
}
422472

473+
// Fallback to direct API call if Hub didn't provide data.
423474
if ( false === $version_info ) {
424475
$version_info = $this->api_request(
425476
'get_version',
@@ -430,7 +481,6 @@ public function check_update( $_transient_data ) {
430481
)
431482
);
432483

433-
// Since we disabled our filter for the transient, we aren't running our object conversion on banners, sections, or icons. Do this now:
434484
if ( isset( $version_info->banners ) && ! is_array( $version_info->banners ) ) {
435485
$version_info->banners = $this->convert_object_to_array( $version_info->banners );
436486
}
@@ -455,9 +505,7 @@ public function check_update( $_transient_data ) {
455505
$no_update = false;
456506
if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
457507

458-
$_transient_data->response[ $this->name ] = $version_info;
459-
460-
// Make sure the plugin property is set to the plugin's name/location.
508+
$_transient_data->response[ $this->name ] = $version_info;
461509
$_transient_data->response[ $this->name ]->plugin = $this->name;
462510
} else {
463511
$no_update = new stdClass();
@@ -522,7 +570,7 @@ public function show_update_notification( $file, $plugin ) {
522570

523571
if ( empty( $update_cache->response ) || empty( $update_cache->response[ $this->name ] ) ) {
524572

525-
$version_info = $this->get_cached_version_info(); // || false;
573+
$version_info = $this->get_cached_version_info(); // || false;
526574

527575
if ( false === $version_info ) {
528576
$version_info = $this->api_request(

0 commit comments

Comments
 (0)