$value) { $value = $key == 'refer_url' ? self::normalizeReferUrl($value) : sanitize_text_field($value); if ($value !== '' && $value !== null) { $directValues[$key] = $value; } } $allowedKeys = self::allowedUtmParameterKey(); $metaValues = []; foreach (Arr::except($data, $directValueKeys) as $key => $value) { if (!in_array($key, $allowedKeys, true)) { continue; } $value = sanitize_text_field($value); if ($value !== '' && $value !== null) { $metaValues[$key] = $value; } } // A cart hash on its own is reason enough: the row is the order's companion and // downstream code reads it whether or not the visit carried a source. if (!$directValues && !$metaValues && !$cartHash) { return null; } $operation = OrderOperation::query()->where('order_id', $orderId)->first(); if (!$operation) { $attributes = array_merge($directValues, ['order_id' => $orderId]); if ($metaValues) { $attributes['meta'] = $metaValues; } if ($cartHash) { $attributes['cart_hash'] = $cartHash; } return OrderOperation::query()->create($attributes); } $attributes = $directValues; if ($metaValues) { $existingMeta = $operation->meta; $attributes['meta'] = Arr::mergeMissingValues( $metaValues, is_array($existingMeta) ? $existingMeta : [] ); } if ($cartHash && !$operation->cart_hash) { $attributes['cart_hash'] = $cartHash; } if ($attributes) { $operation->update($attributes); } return $operation; } /** * Reduce a referrer (full URL or bare host) to its bare domain: * no scheme, no www. prefix, no path — e.g. "google.com" */ public static function normalizeReferUrl($value): string { $value = trim((string)$value); if (!$value) { return ''; } if (strpos($value, '//') !== false) { $host = wp_parse_url($value, PHP_URL_HOST); if ($host) { $value = $host; } } else { $value = explode('/', $value)[0]; } $value = strtolower($value); if (strpos($value, 'www.') === 0) { $value = substr($value, 4); } return sanitize_text_field($value); } /** * Choose the attribution block to record against an order. * * The browser resolves attribution before posting — UTMManager replaces the * attribution block on a fresh marketing touch and carries ad click identifiers * forward on their own longer window — so the posted block is already the * finished answer and is taken whole. Merging it with the cart column key by * key would re-introduce fields from a touch the browser deliberately dropped, * because a cart row is reused across visits and only refreshes when the * customer edits a checkout field. * * The cart remains the fallback for order creation that never went through a * browser, where it is the only source available. * * @param array $requestUtmData Attribution posted with the current request. * @param mixed $cartUtmData The cart's stored block, or null. * * @return array */ public static function resolveUtmData(array $requestUtmData, $cartUtmData = []): array { if ($requestUtmData) { return $requestUtmData; } return is_array($cartUtmData) ? $cartUtmData : []; } public static function getUtmDataOfRequest(): array { $requestData = App::request()->all(); $requestUtmData = Arr::get($requestData, 'utm_data', []); $sanitizedUtmData = []; // Sanitize UTM data foreach ($requestUtmData as $utmKey => $utmValue) { $sanitizedKey = sanitize_text_field($utmKey); $sanitizedUtmData[$sanitizedKey] = sanitize_text_field($utmValue); } return $sanitizedUtmData; } }