'woocommerce', 'external_id' => self::store_id(), 'name' => get_bloginfo('name'), 'site_url' => home_url(), 'version' => AFFCLIX_WC_VERSION, 'platform_version' => defined('WC_VERSION') ? WC_VERSION : '', ]; // The read-only REST key is created only when the merchant ticked the // box for it. When they have, it is present only on the handshake that // first mints the key: WooCommerce stores a hash, so the plaintext // cannot be read back afterwards. Every later handshake omits it and // the server keeps the credentials it already holds. $creds = Affclix_Settings::credentials(); if (! empty($creds['allow_rest'])) { $rest_key = Affclix_Rest_Key::provision(); if (is_array($rest_key)) { $payload['credentials'] = $rest_key; } } else { Affclix_Rest_Key::revoke(); } $response = self::request('POST', '/connect', $payload); if (! $response['ok']) { return [ 'ok' => false, 'message' => $response['message'], 'manifest' => [], ]; } $manifest = isset($response['body']['data']) ? $response['body']['data'] : []; Affclix_Settings::save_manifest($manifest); return [ 'ok' => true, 'message' => __('Connected to AffClix.', 'affclix-for-woocommerce'), 'manifest' => $manifest, ]; } /** * Tell AffClix this store is no longer sending orders. * * @return bool */ public static function disconnect() { $response = self::request( 'POST', '/disconnect', [ 'channel' => 'woocommerce', 'external_id' => self::store_id(), ] ); Affclix_Settings::clear_manifest(); return $response['ok']; } /** * This store's stable identifier. * * The home URL host, not the full URL: a merchant moving from http to https * or adding a trailing slash must land on the same install row rather than * creating a second store that splits their reporting in half. * * @return string */ public static function store_id() { $host = wp_parse_url(home_url(), PHP_URL_HOST); return $host ? strtolower($host) : home_url(); } /** * Scheme + host for the handshake API. * * The stored host is normally bare (`api.affclix.com`) and is assumed to be * https — never silently downgrade a merchant's credentials onto a plaintext * connection. A host entered *with* a scheme is honoured as typed, which is * what makes a self-hosted or local AffClix on http reachable at all. * * @param string $host Stored api_host setting. * @return string Base URL with no trailing slash. */ private static function api_base($host) { $host = trim((string) $host); $host = rtrim($host, '/'); if ($host === '') { return 'https://api.affclix.com'; } return preg_match('~^https?://~i', $host) ? $host : 'https://'.$host; } /** * Perform an authenticated request. * * @param string $method HTTP verb. * @param string $path Path under /v1/a/distribution. * @param array $body Payload; sent as a query string for GET. * @return array{ok:bool,message:string,body:array,status:int} */ private static function request($method, $path, array $body = []) { $creds = Affclix_Settings::credentials(); if (empty($creds['domain_id']) || empty($creds['api_key']) || empty($creds['api_secret'])) { return self::fail(__('Enter your AffClix account ID, API key and API secret first.', 'affclix-for-woocommerce')); } $url = self::api_base($creds['api_host']).'/v1/a/distribution'.$path; $args = [ // Longer than WordPress's 5s default: the handshake provisions a // store and generates a webhook secret on the far side, and a // timeout here leaves the merchant staring at a failure for a call // that actually succeeded. 'timeout' => 20, 'method' => $method, 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-Domain-Id' => (string) $creds['domain_id'], 'X-Api-Key' => (string) $creds['api_key'], 'X-Api-Secret' => (string) $creds['api_secret'], 'User-Agent' => 'AffClix-WooCommerce/'.AFFCLIX_WC_VERSION.'; '.home_url(), ], ]; if ($method === 'GET') { $url = add_query_arg($body, $url); } else { $args['body'] = wp_json_encode($body); } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { return self::fail($response->get_error_message()); } $status = (int) wp_remote_retrieve_response_code($response); $parsed = json_decode(wp_remote_retrieve_body($response), true); $parsed = is_array($parsed) ? $parsed : []; if ($status < 200 || $status >= 300 || empty($parsed['success'])) { $message = isset($parsed['message']) ? $parsed['message'] /* translators: %d: HTTP status code. */ : sprintf(__('AffClix returned HTTP %d.', 'affclix-for-woocommerce'), $status); return self::fail($message, $status); } return [ 'ok' => true, 'message' => '', 'body' => $parsed, 'status' => $status, ]; } /** * Build a failure result and log it. * * @param string $message Human-readable reason. * @param int $status HTTP status, when there was one. * @return array */ private static function fail($message, $status = 0) { if (function_exists('wc_get_logger')) { wc_get_logger()->error($message, ['source' => 'affclix']); } return [ 'ok' => false, 'message' => $message, 'body' => [], 'status' => $status, ]; } }