PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Helpers / UtmHelper.php

UtmHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Helpers/UtmHelper.php

246 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Helpers;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Models\OrderOperation;
7 use FluentCart\Framework\Support\Arr;
8
9 class UtmHelper
10 {
11
12 public static function allowedUtmParameterKey(): array
13 {
14 $keys = [
15 'utm_campaign',
16 'utm_content',
17 'utm_term',
18 'utm_source',
19 'utm_medium',
20 'utm_id',
21 'refer_url',
22 // Ad network click identifiers. gbraid and wbraid carry iOS clicks where gclid
23 // is absent, and gad_campaignid names the campaign on the url itself, which
24 // survives cookie loss.
25 'fbclid',
26 'gclid',
27 'gbraid',
28 'wbraid',
29 'gad_campaignid',
30 'gad_source',
31 'msclkid'
32 ];
33
34 return apply_filters('fluent_cart/utm/allowed_keys', $keys, []);
35 }
36
37 /**
38 * Hostnames that belong to the same store network (e.g. child/product sites
39 * that redirect visitors here for checkout). Referrers from these hosts are
40 * treated as internal navigation and never recorded as refer_url — the real
41 * source arrives as query params (refer_url, utm_*) appended by the child site.
42 */
43 public static function getInternalDomains(): array
44 {
45 $domains = apply_filters('fluent_cart/utm/internal_domains', []);
46
47 if (!is_array($domains)) {
48 return [];
49 }
50
51 $hosts = [];
52 foreach ($domains as $domain) {
53 if (!is_string($domain) || !$domain) {
54 continue;
55 }
56 $domain = strtolower(trim($domain));
57 if (strpos($domain, '//') !== false) {
58 $domain = (string)wp_parse_url($domain, PHP_URL_HOST);
59 }
60 $domain = trim($domain, '/');
61 if ($domain) {
62 $hosts[] = $domain;
63 }
64 }
65
66 return array_values(array_unique($hosts));
67 }
68
69 /**
70 * Record an order's traffic source on its operation row.
71 *
72 * The known UTM fields become columns; anything else on the allow list — click
73 * identifiers such as gclid — goes into meta, which is where reporting and any
74 * conversion upload will look for them.
75 *
76 * Safe to call more than once for the same order. Empty incoming values never
77 * overwrite something already recorded.
78 *
79 * An order arriving with no source at all still gets its row when a cart hash is
80 * given. Other code treats the operation row as the order's companion — ReceiptHandler
81 * reads sales_recorded off it to decide whether a receipt is being seen for the first
82 * time — so an untracked order must not be left without one.
83 *
84 * @param int $orderId
85 * @param array $data Raw source data, normally the cart's utm_data.
86 * @param string $cartHash Recorded alongside so the order can be traced back to its cart.
87 * @return OrderOperation|null The row, or null when there was nothing to record it against.
88 */
89 public static function addUtmToOrder($orderId, $data = [], $cartHash = '')
90 {
91 if (!$orderId) {
92 return null;
93 }
94
95 if (!is_array($data)) {
96 $data = [];
97 }
98
99 $directValueKeys = [
100 'utm_campaign',
101 'utm_content',
102 'utm_term',
103 'utm_source',
104 'utm_medium',
105 'utm_id',
106 'refer_url',
107 ];
108
109 $directValues = [];
110 foreach (Arr::only($data, $directValueKeys) as $key => $value) {
111 $value = $key == 'refer_url'
112 ? self::normalizeReferUrl($value)
113 : sanitize_text_field($value);
114
115 if ($value !== '' && $value !== null) {
116 $directValues[$key] = $value;
117 }
118 }
119
120 $allowedKeys = self::allowedUtmParameterKey();
121 $metaValues = [];
122 foreach (Arr::except($data, $directValueKeys) as $key => $value) {
123 if (!in_array($key, $allowedKeys, true)) {
124 continue;
125 }
126
127 $value = sanitize_text_field($value);
128 if ($value !== '' && $value !== null) {
129 $metaValues[$key] = $value;
130 }
131 }
132
133 // A cart hash on its own is reason enough: the row is the order's companion and
134 // downstream code reads it whether or not the visit carried a source.
135 if (!$directValues && !$metaValues && !$cartHash) {
136 return null;
137 }
138
139 $operation = OrderOperation::query()->where('order_id', $orderId)->first();
140
141 if (!$operation) {
142 $attributes = array_merge($directValues, ['order_id' => $orderId]);
143
144 if ($metaValues) {
145 $attributes['meta'] = $metaValues;
146 }
147
148 if ($cartHash) {
149 $attributes['cart_hash'] = $cartHash;
150 }
151
152 return OrderOperation::query()->create($attributes);
153 }
154
155 $attributes = $directValues;
156
157 if ($metaValues) {
158 $existingMeta = $operation->meta;
159 $attributes['meta'] = Arr::mergeMissingValues(
160 $metaValues,
161 is_array($existingMeta) ? $existingMeta : []
162 );
163 }
164
165 if ($cartHash && !$operation->cart_hash) {
166 $attributes['cart_hash'] = $cartHash;
167 }
168
169 if ($attributes) {
170 $operation->update($attributes);
171 }
172
173 return $operation;
174 }
175
176 /**
177 * Reduce a referrer (full URL or bare host) to its bare domain:
178 * no scheme, no www. prefix, no path — e.g. "google.com"
179 */
180 public static function normalizeReferUrl($value): string
181 {
182 $value = trim((string)$value);
183 if (!$value) {
184 return '';
185 }
186
187 if (strpos($value, '//') !== false) {
188 $host = wp_parse_url($value, PHP_URL_HOST);
189 if ($host) {
190 $value = $host;
191 }
192 } else {
193 $value = explode('/', $value)[0];
194 }
195
196 $value = strtolower($value);
197 if (strpos($value, 'www.') === 0) {
198 $value = substr($value, 4);
199 }
200
201 return sanitize_text_field($value);
202 }
203
204 /**
205 * Choose the attribution block to record against an order.
206 *
207 * The browser resolves attribution before posting — UTMManager replaces the
208 * attribution block on a fresh marketing touch and carries ad click identifiers
209 * forward on their own longer window — so the posted block is already the
210 * finished answer and is taken whole. Merging it with the cart column key by
211 * key would re-introduce fields from a touch the browser deliberately dropped,
212 * because a cart row is reused across visits and only refreshes when the
213 * customer edits a checkout field.
214 *
215 * The cart remains the fallback for order creation that never went through a
216 * browser, where it is the only source available.
217 *
218 * @param array $requestUtmData Attribution posted with the current request.
219 * @param mixed $cartUtmData The cart's stored block, or null.
220 *
221 * @return array
222 */
223 public static function resolveUtmData(array $requestUtmData, $cartUtmData = []): array
224 {
225 if ($requestUtmData) {
226 return $requestUtmData;
227 }
228
229 return is_array($cartUtmData) ? $cartUtmData : [];
230 }
231
232 public static function getUtmDataOfRequest(): array
233 {
234 $requestData = App::request()->all();
235 $requestUtmData = Arr::get($requestData, 'utm_data', []);
236 $sanitizedUtmData = [];
237 // Sanitize UTM data
238 foreach ($requestUtmData as $utmKey => $utmValue) {
239 $sanitizedKey = sanitize_text_field($utmKey);
240 $sanitizedUtmData[$sanitizedKey] = sanitize_text_field($utmValue);
241 }
242
243 return $sanitizedUtmData;
244 }
245
246 }