Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
87.10% |
27 / 31 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
| LicenseHeartbeat | |
87.10% |
27 / 31 |
|
25.00% |
1 / 4 |
13.36 | |
0.00% |
0 / 1 |
| register | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| deregister | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| run | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
7.04 | |||
| lastHeartbeatAt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * License Heartbeat (WP Cron). |
| 4 | * |
| 5 | * Calls the server's /v1/auth/heartbeat on a schedule to keep the |
| 6 | * license marked as active and to update last-seen timestamps. |
| 7 | * |
| 8 | * Runs every 12 hours via WP Cron. If the server returns inactive, |
| 9 | * the client marks the license as inactive locally too. |
| 10 | * |
| 11 | * @package SwapAds\Client\License |
| 12 | * @since 1.0.0 |
| 13 | */ |
| 14 | |
| 15 | declare(strict_types=1); |
| 16 | |
| 17 | namespace SwapAds\Client\License; |
| 18 | |
| 19 | use SwapAds\Client\Api\RestClient; |
| 20 | use SwapAds\Client\License\LicenseManager; |
| 21 | |
| 22 | /** |
| 23 | * Class LicenseHeartbeat. |
| 24 | * |
| 25 | * @since 1.0.0 |
| 26 | */ |
| 27 | final class LicenseHeartbeat |
| 28 | { |
| 29 | public const HOOK_NAME = 'swapads_client_license_heartbeat'; |
| 30 | public const SCHEDULE_NAME = 'twicedaily'; |
| 31 | public const OPTION_KEY = 'swapads_client_last_heartbeat_at'; |
| 32 | |
| 33 | /** |
| 34 | * Register WP Cron hook + schedule. |
| 35 | * |
| 36 | * @since 1.0.0 |
| 37 | */ |
| 38 | public static function register(): void |
| 39 | { |
| 40 | if (!function_exists('add_action')) { |
| 41 | return; |
| 42 | } |
| 43 | add_action(self::HOOK_NAME, [self::class, 'run']); |
| 44 | if (!wp_next_scheduled(self::HOOK_NAME)) { |
| 45 | wp_schedule_event(time(), self::SCHEDULE_NAME, self::HOOK_NAME); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Deregister WP Cron hook + clear schedule. |
| 51 | * |
| 52 | * @since 1.0.0 |
| 53 | */ |
| 54 | public static function deregister(): void |
| 55 | { |
| 56 | $timestamp = wp_next_scheduled(self::HOOK_NAME); |
| 57 | if ($timestamp) { |
| 58 | wp_unschedule_event($timestamp, self::HOOK_NAME); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Run the heartbeat: call /v1/auth/heartbeat and update local state. |
| 64 | * |
| 65 | * @return array<string, mixed> Result {success, status, error?} |
| 66 | * |
| 67 | * @since 1.0.0 |
| 68 | */ |
| 69 | public static function run(): array |
| 70 | { |
| 71 | if (!LicenseManager::isLicensed()) { |
| 72 | return ['success' => false, 'error' => 'NOT_LICENSED', 'status' => 'inactive']; |
| 73 | } |
| 74 | $serverUrl = (string) get_option('swapads_client_server_url', ''); |
| 75 | $licenseKey = (string) LicenseManager::key(); |
| 76 | $secret = (string) LicenseManager::secret(); |
| 77 | $siteId = (int) LicenseManager::siteId(); |
| 78 | |
| 79 | if ($serverUrl === '' || $licenseKey === '' || $secret === '') { |
| 80 | return ['success' => false, 'error' => 'INCOMPLETE_CONFIG', 'status' => 'inactive']; |
| 81 | } |
| 82 | |
| 83 | try { |
| 84 | $client = new RestClient($serverUrl); |
| 85 | $response = $client->post('/v1/auth/heartbeat', [ |
| 86 | 'license_key' => $licenseKey, |
| 87 | 'site_id' => $siteId, |
| 88 | ]); |
| 89 | $data = $response['data'] ?? []; |
| 90 | $status = (string) ($data['status'] ?? 'unknown'); |
| 91 | update_option(self::OPTION_KEY, time()); |
| 92 | if ($status === 'inactive') { |
| 93 | LicenseManager::store($licenseKey, $secret, $siteId, (string) LicenseManager::domain(), 'inactive'); |
| 94 | } else { |
| 95 | LicenseManager::store($licenseKey, $secret, $siteId, (string) LicenseManager::domain(), 'active'); |
| 96 | } |
| 97 | return ['success' => true, 'status' => $status]; |
| 98 | } catch (\Throwable $e) { |
| 99 | return ['success' => false, 'error' => 'REQUEST_FAILED', 'message' => $e->getMessage()]; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Get timestamp of last successful heartbeat (or 0). |
| 105 | * |
| 106 | * @return int Unix timestamp. |
| 107 | * |
| 108 | * @since 1.0.0 |
| 109 | */ |
| 110 | public static function lastHeartbeatAt(): int |
| 111 | { |
| 112 | return (int) get_option(self::OPTION_KEY, 0); |
| 113 | } |
| 114 | } |