.", with the site id + timestamp + signature in * the X-Adverships-* headers. * * The connect() call is the one exception — at connect time we have no secret * yet, so it posts the one-time provision key in the body (unsigned) and stores * the secret the server returns. */ if (!defined('ABSPATH')) { exit; } class Adverships_Affiliate_Client { /** @return array The stored settings (connection state + api_base). */ public static function settings() { $defaults = array( 'api_base' => ADVERSHIPS_AFF_DEFAULT_API, 'site_id' => '', 'site_secret' => '', 'business_username' => '', 'bizcode' => '', 'invite_url' => '', 'connected_at' => 0, 'alerts_enabled' => false, 'alerts_email' => '', 'alerts_last_seen' => 0, ); $saved = get_option(ADVERSHIPS_AFF_OPTION, array()); if (!is_array($saved)) { $saved = array(); } return array_merge($defaults, $saved); } /** Persist a partial settings update (merged over current). */ public static function update_settings(array $patch) { $current = self::settings(); update_option(ADVERSHIPS_AFF_OPTION, array_merge($current, $patch), false); } /** @return string The Adverships API base, trailing slash trimmed. */ public static function api_base() { $s = self::settings(); $base = !empty($s['api_base']) ? $s['api_base'] : ADVERSHIPS_AFF_DEFAULT_API; return rtrim($base, '/'); } /** @return bool True once a site_id + secret are stored. */ public static function is_connected() { $s = self::settings(); return $s['site_id'] !== '' && $s['site_secret'] !== ''; } /** * Exchange a one-time provision key for a permanent connection. On success * stores site_id + site_secret (+ bizcode, username) and returns the parsed * response array. On failure returns a WP_Error. * * @param string $provisionKey The key the merchant pasted (from the dashboard). * @param string $apiBase Optional API base override (else default/stored). * @return array|WP_Error */ public static function connect($provisionKey, $apiBase = '') { $provisionKey = trim($provisionKey); if ($provisionKey === '') { return new WP_Error('missing_key', __('Enter a connection key.', 'adverships-affiliate')); } $base = $apiBase !== '' ? rtrim($apiBase, '/') : self::api_base(); $body = wp_json_encode(array( 'provision_key' => $provisionKey, 'site_url' => home_url(), 'site_name' => get_bloginfo('name'), 'admin_email' => get_bloginfo('admin_email'), 'wc_version' => defined('WC_VERSION') ? WC_VERSION : '', 'wp_version' => get_bloginfo('version'), 'plugin_version' => ADVERSHIPS_AFF_VERSION, )); $resp = wp_remote_post($base . '/api/wordpress/register.php', array( 'timeout' => 20, 'headers' => array('Content-Type' => 'application/json'), 'body' => $body, )); if (is_wp_error($resp)) { return $resp; } $code = (int) wp_remote_retrieve_response_code($resp); $data = json_decode(wp_remote_retrieve_body($resp), true); if ($code !== 200 || !is_array($data) || empty($data['ok'])) { $err = is_array($data) && isset($data['error']) ? $data['error'] : ('http_' . $code); return new WP_Error('connect_failed', self::friendly_error($err)); } self::update_settings(array( 'api_base' => $base, 'site_id' => isset($data['site_id']) ? $data['site_id'] : '', 'site_secret' => isset($data['site_secret']) ? $data['site_secret'] : '', 'business_username' => isset($data['username']) ? $data['username'] : '', 'bizcode' => isset($data['bizcode']) ? $data['bizcode'] : '', 'invite_url' => isset($data['invite_url']) ? $data['invite_url'] : '', 'connected_at' => time(), )); return $data; } /** * Tell Adverships to detach this site (clears the server-side wp_* columns * and retires the secret). Best-effort: returns the result but callers * should clear local state regardless of the outcome. * * @return array|WP_Error */ public static function disconnect_remote() { if (!self::is_connected()) { return new WP_Error('not_connected', __('Not connected to Adverships.', 'adverships-affiliate')); } return self::post_signed('/api/wordpress/uninstall.php', array('site_url' => home_url()), true); } /** * Full disconnect: notify the server to detach the site, then clear local * connection state. Local state is cleared even if the remote call fails so * the merchant is never stuck "connected" locally to a dead link. */ public static function disconnect() { self::disconnect_remote(); // best-effort self::update_settings(array( 'site_id' => '', 'site_secret' => '', 'business_username' => '', 'bizcode' => '', 'invite_url' => '', 'connected_at' => 0, // Reset the alerts cursor so a future reconnect re-seeds it instead // of replaying history against a stale high-water mark. 'alerts_last_seen' => 0, )); // Stop the poll immediately; it would no-op while disconnected anyway, // but clearing it keeps wp-cron tidy. if (class_exists('Adverships_Affiliate_Alerts')) { Adverships_Affiliate_Alerts::unschedule(); } } /** * POST a signed request to an api/wordpress/*.php endpoint. * * @param string $path Endpoint path, e.g. '/api/wordpress/order_webhook.php'. * @param array $data Body payload (json-encoded + signed). * @param bool $blocking false for fire-and-forget (tracking); true for reads. * @return array|WP_Error ['code'=>int, 'data'=>array] on success. */ public static function post_signed($path, array $data, $blocking = true) { $s = self::settings(); if ($s['site_id'] === '' || $s['site_secret'] === '') { return new WP_Error('not_connected', __('Not connected to Adverships.', 'adverships-affiliate')); } $body = wp_json_encode($data); $ts = (string) time(); $sig = hash_hmac('sha256', $ts . '.' . $body, $s['site_secret']); $resp = wp_remote_post(self::api_base() . $path, array( 'timeout' => $blocking ? 20 : 5, 'blocking' => (bool) $blocking, 'headers' => array( 'Content-Type' => 'application/json', 'X-Adverships-Site' => $s['site_id'], 'X-Adverships-Ts' => $ts, 'X-Adverships-Sig' => $sig, ), 'body' => $body, )); if (is_wp_error($resp)) { return $resp; } if (!$blocking) { return array('code' => 0, 'data' => array()); } $code = (int) wp_remote_retrieve_response_code($resp); $parsed = json_decode(wp_remote_retrieve_body($resp), true); return array('code' => $code, 'data' => is_array($parsed) ? $parsed : array()); } /** Map server error codes to merchant-readable messages. */ private static function friendly_error($err) { $map = array( 'invalid_provision_key' => __('That connection key is invalid, expired, or already used. Generate a fresh one from your Adverships dashboard.', 'adverships-affiliate'), 'site_already_connected' => __('This store URL is already connected to a different Adverships account.', 'adverships-affiliate'), 'account_not_found' => __('The Adverships account for that key could not be found.', 'adverships-affiliate'), 'invalid_site_url' => __('Your site URL was rejected by Adverships.', 'adverships-affiliate'), ); if (isset($map[$err])) { return $map[$err]; } /* translators: %s: raw error code from the server. */ return sprintf(__('Connection failed (%s).', 'adverships-affiliate'), $err); } }