. * * In the head and at priority 1 so the click parameters are captured before * anything else on the page can redirect or rewrite the URL. The capture * script ships inside the plugin; only the optional AffClix web SDK is * loaded from AffClix, and only when the merchant has switched it on. */ public static function enqueue() { if (! Affclix_Settings::is_connected()) { return; } $attr = Affclix_Settings::attribution(); wp_register_script( 'affclix-attribution', plugins_url('assets/js/affclix-attribution.js', AFFCLIX_WC_FILE), [], AFFCLIX_WC_VERSION, ['in_footer' => false] ); wp_add_inline_script( 'affclix-attribution', 'window.affclixWcConfig = '.wp_json_encode( [ 'name' => $attr['cookie_name'], 'days' => (int) $attr['window_days'], 'model' => $attr['model'], 'domain' => $attr['cookie_domain'], 'consent' => function_exists('wp_has_consent'), ] ).';', 'before' ); wp_enqueue_script('affclix-attribution'); $creds = Affclix_Settings::credentials(); $sdk_url = Affclix_Settings::get('sdk_url', ''); // The hosted SDK adds page-view analytics on top of attribution. It is // remote code, so it is opt-in and never loaded by default. if (! empty($creds['load_sdk']) && $sdk_url !== '') { wp_enqueue_script( 'affclix-sdk', esc_url_raw($sdk_url), [], null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- versioned in the URL path by the service. [ 'in_footer' => true, 'strategy' => 'async', ] ); } } /** * The attribution payload for the current visitor, read server-side. * * Returns null when there is nothing to attribute, or when the stored touch * is older than the window. The window is re-checked here rather than * trusted from the browser: the cookie is attacker-controlled input, and an * expired-but-present cookie is exactly what a stale click looks like. * * @return array|null */ public static function current_touch() { $attr = Affclix_Settings::attribution(); $name = $attr['cookie_name']; $window = max(1, (int) $attr['window_days']) * DAY_IN_SECONDS; if (empty($_COOKIE[$name])) { return null; } // Unslashed and decoded by hand: the cookie holds JSON written by the // browser, so WordPress's slashing would corrupt the quoting. $raw = json_decode(wp_unslash($_COOKIE[$name]), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- decoded and field-sanitised below. if (! is_array($raw) || empty($raw['ts'])) { return null; } // ts is epoch milliseconds, written by the snippet above. $age = time() - (int) ($raw['ts'] / 1000); if ($age < 0 || $age > $window) { return null; } $clean = []; foreach (['campaign_id', 'affiliate_id', 'click_id', 'sub1', 'sub2', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_content'] as $field) { if (! empty($raw[$field])) { $clean[$field] = sanitize_text_field((string) $raw[$field]); } } if (empty($clean)) { return null; } $clean['click_ts'] = (string) (int) $raw['ts']; return $clean; } }