Connect License class to the real auroraplugins.com license server
- License.php now calls the aurora-licensing plugin's REST API (/wp-json/aurora-licensing/v1/validate|activate|deactivate) instead of treating any non-empty license_key as active. Result is cached in the aurora_admin_license_status option for 12 hours; is_active() and is_feature_available() read the cache instead of hitting the network on every admin page load. - Hooks update_option_aurora_admin_settings to activate/deactivate immediately when the license_key setting actually changes, rather than waiting out the cache. Instantiated in AuroraAdmin.php so that hook registers. - Settings page now surfaces the real status (active/invalid/unreachable, with the server's own message) as a status line under the License key field, instead of a field that silently does nothing. - Verified live against the real server: a fake test key correctly came back locked with the server's own "License key not found." message. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
eb03d3dd6c
commit
540083e6e4
@@ -2,6 +2,7 @@
|
||||
namespace AuroraAdmin\App;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
use AuroraAdmin\Licensing\License;
|
||||
use AuroraAdmin\Shell\Shell;
|
||||
use AuroraAdmin\Pages\DashboardTakeover;
|
||||
use AuroraAdmin\Pages\SettingsPage;
|
||||
@@ -35,6 +36,7 @@ class AuroraAdmin
|
||||
public function __construct()
|
||||
{
|
||||
new Settings();
|
||||
new License();
|
||||
new Shell();
|
||||
new DashboardTakeover();
|
||||
new SettingsPage();
|
||||
|
||||
@@ -9,12 +9,12 @@ defined("ABSPATH") || exit();
|
||||
* Single gate every paid feature funnels through, so the free/paid split
|
||||
* lives in one place instead of being re-decided in each feature's code.
|
||||
*
|
||||
* Today's is_active() is a placeholder: any non-empty license_key counts as
|
||||
* active, with no remote validation. There's no license server yet — when
|
||||
* one exists, swap is_active()'s internals for a real check (cached in a
|
||||
* transient so it isn't a remote call on every admin page load); nothing
|
||||
* else in the plugin needs to change, since every gated feature already
|
||||
* calls into this class rather than reading the setting directly.
|
||||
* Validates against the real license server: the "aurora-licensing" WP
|
||||
* plugin running on auroraplugins.com, REST namespace
|
||||
* /wp-json/aurora-licensing/v1/ (validate/activate/deactivate). Every
|
||||
* result is cached in the aurora_admin_license_status option for
|
||||
* STATUS_TTL, refreshed immediately (not just on expiry) whenever the
|
||||
* license_key setting actually changes — see on_settings_updated().
|
||||
*/
|
||||
class License
|
||||
{
|
||||
@@ -31,10 +31,47 @@ class License
|
||||
"remote-site-switcher" => "Remote Site Switcher",
|
||||
];
|
||||
|
||||
const API_BASE = "https://auroraplugins.com/wp-json/aurora-licensing/v1/";
|
||||
const STATUS_OPTION = "aurora_admin_license_status";
|
||||
const STATUS_TTL = 12 * HOUR_IN_SECONDS;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
add_action("update_option_aurora_admin_settings", [self::class, "on_settings_updated"], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires on every save of the aurora_admin_settings option (not just the
|
||||
* license field) — only acts when license_key actually changed, so
|
||||
* saving an unrelated settings category never triggers a remote call.
|
||||
* Deactivates the old key's activation on this domain before activating
|
||||
* the new one, so switching keys doesn't leak a stale activation slot.
|
||||
*/
|
||||
public static function on_settings_updated($old_value, $value)
|
||||
{
|
||||
$old_key = is_array($old_value) ? trim((string) ($old_value["license_key"] ?? "")) : "";
|
||||
$new_key = is_array($value) ? trim((string) ($value["license_key"] ?? "")) : "";
|
||||
|
||||
if ($old_key === $new_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($old_key !== "") {
|
||||
self::call("deactivate", ["license_key" => $old_key, "domain" => self::domain()]);
|
||||
}
|
||||
|
||||
if ($new_key === "") {
|
||||
delete_option(self::STATUS_OPTION);
|
||||
return;
|
||||
}
|
||||
|
||||
self::activate($new_key);
|
||||
}
|
||||
|
||||
public static function is_active()
|
||||
{
|
||||
$key = trim((string) Settings::get("license_key", ""));
|
||||
return $key !== "";
|
||||
$status = self::status();
|
||||
return !empty($status["valid"]) && ($status["status"] ?? "") === "active";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,4 +90,106 @@ class License
|
||||
{
|
||||
return isset(self::PAID_FEATURES[$feature]) ? self::PAID_FEATURES[$feature] : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* The license server's /validate response shape (see call()), cached in
|
||||
* aurora_admin_license_status for STATUS_TTL so every paid-feature check
|
||||
* across an admin page load — there are several per request — doesn't
|
||||
* each cost a remote HTTP round trip. Re-validated immediately (not just
|
||||
* on expiry) if the cached result was for a different key than what's
|
||||
* currently saved, so a key change is never served stale data even if
|
||||
* on_settings_updated() somehow didn't fire first.
|
||||
*/
|
||||
public static function status()
|
||||
{
|
||||
$key = trim((string) Settings::get("license_key", ""));
|
||||
if ($key === "") {
|
||||
return ["valid" => false, "status" => "none"];
|
||||
}
|
||||
|
||||
$cached = get_option(self::STATUS_OPTION);
|
||||
$fresh =
|
||||
is_array($cached) &&
|
||||
($cached["checked_key"] ?? null) === $key &&
|
||||
($cached["checked_at"] ?? 0) > time() - self::STATUS_TTL;
|
||||
|
||||
if ($fresh) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
return self::validate($key);
|
||||
}
|
||||
|
||||
public static function validate($key)
|
||||
{
|
||||
$result = self::call("validate", [
|
||||
"license_key" => $key,
|
||||
"domain" => self::domain(),
|
||||
]);
|
||||
|
||||
$result["checked_key"] = $key;
|
||||
$result["checked_at"] = time();
|
||||
update_option(self::STATUS_OPTION, $result, false);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function activate($key)
|
||||
{
|
||||
self::call("activate", [
|
||||
"license_key" => $key,
|
||||
"domain" => self::domain(),
|
||||
]);
|
||||
|
||||
// /activate's response is a narrower shape ({activated, plan, ...} or
|
||||
// an error) than /validate's ({valid, status, active_sites, ...}) —
|
||||
// re-validate right after so the cached status has the full shape
|
||||
// status()/is_active() and the Settings UI expect either way.
|
||||
self::validate($key);
|
||||
}
|
||||
|
||||
private static function domain()
|
||||
{
|
||||
// The server normalizes domain server-side (strips protocol/www/
|
||||
// trailing slash) — a bare host is enough, no need to duplicate that
|
||||
// normalization here.
|
||||
return (string) parse_url(home_url(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST $endpoint against the license server. Always returns a decoded
|
||||
* array — network failures and non-JSON responses collapse into the
|
||||
* same {valid:false, status:"unreachable", ...} shape as a real error
|
||||
* response, so callers only ever need to branch on the response fields,
|
||||
* never on WP_Error separately.
|
||||
*/
|
||||
private static function call($endpoint, $body)
|
||||
{
|
||||
$response = wp_remote_post(self::API_BASE . $endpoint, [
|
||||
"timeout" => 8,
|
||||
"headers" => ["Content-Type" => "application/json"],
|
||||
"body" => wp_json_encode($body),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
"valid" => false,
|
||||
"status" => "unreachable",
|
||||
"error" => "unreachable",
|
||||
"message" => $response->get_error_message(),
|
||||
];
|
||||
}
|
||||
|
||||
$decoded = json_decode(wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($decoded)) {
|
||||
return [
|
||||
"valid" => false,
|
||||
"status" => "unreachable",
|
||||
"error" => "invalid_response",
|
||||
"message" => "License server returned an unexpected response.",
|
||||
];
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace AuroraAdmin\Pages;
|
||||
|
||||
use AuroraAdmin\Utility\Assets;
|
||||
use AuroraAdmin\Options\Settings;
|
||||
use AuroraAdmin\Licensing\License;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
@@ -67,6 +68,10 @@ class SettingsPage
|
||||
"settings" => Settings::get(),
|
||||
"roles" => $roles,
|
||||
"postTypes" => $post_types,
|
||||
// Real-time result from the license server (see License::status()),
|
||||
// not just whether a key is present — drives the status line under
|
||||
// the License key field (active/invalid/site-limit-reached/etc.).
|
||||
"licenseStatus" => License::status(),
|
||||
];
|
||||
?>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user