$connection, 'attempts' => 0, 'due' => time(), 'created' => time()]; Store::set('disconnects', $pending); self::send($connection['site_id']); return ! isset(Store::get('disconnects')[$connection['site_id']]); } public static function ensure(): void { $pending = Store::get('disconnects'); $retrying = array_filter($pending, static fn ($item) => empty($item['terminal'])); if (! $retrying) { wp_clear_scheduled_hook('flx_security_disconnect'); return; } $due = min(array_column($retrying, 'due')); $next = wp_next_scheduled('flx_security_disconnect'); if (! $next || $next > $due) { if ($next) wp_clear_scheduled_hook('flx_security_disconnect'); wp_schedule_single_event(max(time() + 1, $due), 'flx_security_disconnect'); } } public static function tick(): void { $owner = Store::lock(); if ($owner === false) { self::ensure(); return; } try { foreach (Store::get('disconnects') as $site => $pending) { if (empty($pending['terminal']) && $pending['due'] <= time()) { self::send($site); break; } } } finally { Store::unlock($owner); } self::ensure(); } private static function send(string $site): void { $pending = Store::get('disconnects'); $item = $pending[$site] ?? null; if (! $item) return; $connection = $item['connection']; try { if (($item['created'] ?? time()) < time() - 7 * DAY_IN_SECONDS || $item['attempts'] >= 168) throw new \RuntimeException('request_rejected'); if (! empty($connection['disconnect_receipt'])) { $result = Client::request('POST', '/disconnect', ['site_id' => $site, 'receipt' => Store::decrypt($connection['disconnect_receipt'])], public: true, timeout: 5); } else { $result = Client::request('POST', '/sites/'.$site.'/disconnect', token: Store::decrypt($connection['secret']), timeout: 5); } if (($result['data']['connected'] ?? null) !== false) throw new \RuntimeException('invalid_response'); unset($pending[$site]); } catch (\Throwable $error) { if ($error->getMessage() === 'site_disconnected') { unset($pending[$site]); } elseif (in_array($error->getMessage(), ['invalid_authorization', 'request_rejected', 'invalid_token', 'insufficient_scope', 'connection_required', 'crypto_unavailable', 'invalid_request'], true)) { // Keep the unconfirmed outcome, without retaining credentials or scheduling a futile request. $pending[$site] = ['terminal' => true, 'attempts' => $item['attempts'], 'created' => $item['created'] ?? time(), 'due' => 0]; } else { $item['created'] ??= time(); $item['attempts']++; $item['due'] = max(time() + min(HOUR_IN_SECONDS, 15 * (2 ** min(8, $item['attempts']))), (int) Store::get('retry_after', 0)); $pending[$site] = $item; } } if ($pending) Store::set('disconnects', $pending); else Store::remove('disconnects'); self::ensure(); } }