PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 1.6.2
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v1.6.2
2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 1.8.1 All 42 releases
sellkit / includes / contact-segmentation / contact-data-updater.php

contact-data-updater.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster 1.6.2, at includes/contact-segmentation/contact-data-updater.php

529 lines 13.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sellkit\Contact_Segmentation;
4
5 use Sellkit\Contact_Segmentation\libraries\Mobile_Detect;
6 use Sellkit\Database;
7
8 defined( 'ABSPATH' ) || die();
9
10 /**
11 * Class Contact Data Updater.
12 *
13 * @package Sellkit\Contact_Segmentation
14 * @SuppressWarnings(ExcessiveClassComplexity)
15 * @since 1.1.0
16 */
17 class Contact_Data_Updater {
18
19 const SELLKIT_CONTACT_SEGMENTATION_LOCATION_API = 'sjYudEYJDjBMDDoKyxHd'; // TODO we should get it dynamically.
20 const SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE = 'sellkit_contact_segmentation_force_update';
21
22 /**
23 * Class instance.
24 *
25 * @since 1.1.0
26 * @var Contact_Data_Updater
27 */
28 private static $instance = null;
29
30 /**
31 * Contact data.
32 *
33 * @since 1.1.0
34 * @var array Contact data.
35 */
36 public $data;
37
38 /**
39 * New contact data.
40 *
41 * @since 1.1.0
42 * @var array Contact data.
43 */
44 public static $new_data = [];
45
46 /**
47 * Check if data is expired or not.
48 *
49 * @since 1.1.0
50 * @var boolean Is data expired.
51 */
52 public $is_expired = false;
53
54 /**
55 * Contact_Data constructor.
56 *
57 * @since 1.1.0
58 */
59 public function __construct() {
60 if ( wp_doing_ajax() ) {
61 return;
62 }
63
64 $this->data = Contact_Data::$historical_data;
65
66 if ( ! is_user_logged_in() ) {
67 $this->update_browser_language();
68 $this->maybe_update_device();
69 $this->update_utm_data();
70 $this->maybe_update_ip();
71 $this->maybe_update_locations();
72 $this->update_user_type();
73 $this->update_url_query_string();
74
75 add_action( 'wp', [ $this, 'update_viewed_details' ], 10 );
76 add_action( 'wp', [ $this, 'maybe_update_data' ], 9999 );
77 return;
78 }
79
80 $this->update_utm_data();
81 $this->update_url_query_string();
82 $this->update_browser_language();
83
84 add_action( 'wp', [ $this, 'update_viewed_details' ], 10 );
85
86 $force_update = get_user_meta( get_current_user_id(), self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, true );
87
88 if ( empty( $this->data ) ) {
89 $force_update = true;
90 }
91
92 $this->maybe_update_ip();
93 $this->maybe_update_locations();
94 $this->maybe_update_device();
95
96 if ( true == $force_update ) { //phpcs:ignore
97 $this->update_order_info();
98 }
99
100 add_action( 'wp', [ $this, 'maybe_update_data' ], 9999 );
101 }
102
103 /**
104 * If updating is required start to update.
105 *
106 * @since 1.1.0
107 */
108 public function maybe_update_data() {
109 if ( empty( self::$new_data ) ) {
110 return;
111 }
112
113 if ( ! is_user_logged_in() && is_array( self::$new_data ) ) {
114 setcookie( 'sellkit_contact_segmentation', wp_json_encode( array_merge( Contact_Data::$historical_data, self::$new_data ) ), 2147483647, '/' );
115 return;
116 }
117
118 if ( empty( Contact_Data::$historical_data ) && ! empty( self::$new_data ) ) {
119 self::$new_data['email'] = Contact_Data::get_user_email();
120
121 sellkit()->db->insert( 'contact_segmentation', self::$new_data );
122
123 update_user_meta( get_current_user_id(), self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, false );
124
125 return;
126 }
127
128 self::update_contact_segmentation( self::$new_data, Contact_Data::get_user_email() );
129 }
130
131 /**
132 * Get the class instance.
133 *
134 * @since 1.1.0
135 */
136 public static function get_instance() {
137 if ( null === self::$instance ) {
138 self::$instance = new self();
139 }
140
141 return self::$instance;
142 }
143
144 /**
145 * Update utm data.
146 *
147 * @since 1.1.0
148 */
149 private function update_utm_data() {
150 $utm_source = $this->check_utm_data( 'utm_source' );
151 $utm_campaign = $this->check_utm_data( 'utm_campaign' );
152 $utm_medium = $this->check_utm_data( 'utm_medium' );
153 $utm_content = $this->check_utm_data( 'utm_content' );
154 $utm_term = $this->check_utm_data( 'utm_term' );
155
156 if ( $this->check_utm_is_new( $utm_source, 'utm_source' ) ) {
157 self::$new_data['utm_source'] = $utm_source;
158 }
159
160 if ( $this->check_utm_is_new( $utm_campaign, 'utm_campaign' ) ) {
161 self::$new_data['utm_campaign'] = $utm_campaign;
162 }
163
164 if ( $this->check_utm_is_new( $utm_content, 'utm_content' ) ) {
165 self::$new_data['utm_content'] = $utm_content;
166 }
167
168 if ( $this->check_utm_is_new( $utm_term, 'utm_term' ) ) {
169 self::$new_data['utm_term'] = $utm_term;
170 }
171
172 if ( $this->check_utm_is_new( $utm_medium, 'utm_medium' ) ) {
173 self::$new_data['utm_medium'] = $utm_medium;
174 }
175 }
176
177 /**
178 * Check utm data.
179 *
180 * @param string $utm Utm name.
181 * @since 1.2.8
182 * @return string|null
183 */
184 private function check_utm_data( $utm ) {
185 $value = sellkit_htmlspecialchars( INPUT_GET, $utm );
186
187 if ( ! empty( $value ) ) {
188 return $value;
189 }
190
191 $data = array_change_key_case( $_GET, CASE_LOWER ); //phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification
192
193 if ( empty( $data[ $utm ] ) ) {
194 return $value;
195 }
196
197 return sanitize_text_field( $data[ $utm ] );
198 }
199
200 /**
201 * Update locations data from API.
202 *
203 * @since 1.1.0
204 */
205 private function maybe_update_locations() {
206 $ip = sellkit_get_ip();
207
208 if ( ! empty( $this->data['ip'] ) && $ip === $this->data['ip'] ) {
209 return;
210 }
211
212 $api_key = sellkit_get_option( 'geolocation_api_key' );
213
214 if ( empty( $api_key ) ) {
215 return;
216 }
217
218 $response = wp_remote_get( 'https://location.stg.growmatik.ai/v1', [
219 'timeout' => 10,
220 'body' => [
221 'apiKey' => $api_key,
222 'ip' => $ip,
223 ],
224 ] );
225
226 $response_code = wp_remote_retrieve_response_code( $response );
227
228 if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) && 200 === (int) $response_code ) {
229 $location_data = json_decode( $response['body'] );
230 self::$new_data['visitor_country'] = strtolower( $location_data->country );
231 self::$new_data['visitor_city'] = strtolower( $location_data->city );
232 }
233
234 self::$new_data['updated_at'] = time();
235 }
236
237 /**
238 * Update user type.
239 *
240 * @since 1.2.3
241 */
242 private function update_user_type() {
243 if ( empty( $this->data['user_type'] ) ) {
244 self::$new_data['user_type'] = 'first_time_visitor';
245 }
246
247 if ( ! empty( $this->data['user_type'] ) ) {
248 self::$new_data['user_type'] = 'returning_visitor';
249 }
250 }
251
252 /**
253 * Updates device types.
254 *
255 * @since 1.1.0
256 */
257 private function maybe_update_device() {
258 $detector = new Mobile_Detect();
259 $current_display = 'desktop';
260
261 if ( $detector->isMobile() ) {
262 $current_display = 'mobile';
263 }
264
265 if ( $detector->isTablet() ) {
266 $current_display = 'tablet';
267 }
268
269 if ( ! empty( $this->data['user_device'] ) && $this->data['user_device'] === $current_display ) {
270 return;
271 }
272
273 self::$new_data['user_device'] = $current_display;
274 }
275
276 /**
277 * Update browser language.
278 *
279 * @since 1.1.0
280 */
281 private function update_browser_language() {
282 $data = [];
283
284 if ( ! empty( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
285 $data = explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ); // phpcs:ignore
286 }
287
288 $language = ! empty( $data[0] ) ? $data[0] : '';
289
290 if ( empty( $language ) ) {
291 return;
292 }
293
294 if ( empty( $this->data['browser_language'] ) || ( $this->data['browser_language'] !== $language ) ) {
295 self::$new_data['browser_language'] = $language;
296 }
297 }
298
299 /**
300 * Updates the IP if it's changed.
301 *
302 * @since 1.2.1
303 */
304 private function maybe_update_ip() {
305 $ip = sellkit_get_ip();
306
307 if ( ! empty( $this->data['ip'] ) && $this->data['ip'] === $ip ) {
308 return;
309 }
310
311 self::$new_data['ip'] = $ip;
312 }
313
314 /**
315 * Checks utm value.
316 *
317 * @param string $new_utm New Utm.
318 * @param string $type Utm type.
319 * @return bool
320 */
321 private function check_utm_is_new( $new_utm, $type ) {
322 if ( empty( $new_utm ) ) {
323 return false;
324 }
325
326 if ( empty( $this->data[ $type ] ) ) {
327 return true;
328 }
329
330 if ( $new_utm !== $this->data[ $type ] ) {
331 return true;
332 }
333 }
334
335 /**
336 * Updates user information.
337 *
338 * @since 1.1.0
339 */
340 public function update_order_info() {
341 $current_user = wp_get_current_user();
342
343 $args = [
344 'customer_id' => $current_user->ID,
345 'post_status' => 'completed',
346 'post_type' => 'shop_order',
347 'limit' => -1,
348 'orderby' => 'id',
349 'order' => 'DESC',
350 ];
351
352 if ( ! sellkit()->has_valid_dependencies() ) {
353 return;
354 }
355
356 $orders = wc_get_orders( $args );
357 $order_number = count( $orders );
358 $total_spent = 0;
359 $first_order = ! empty( end( $orders ) ) ? end( $orders ) : '';
360 $last_order = ! empty( $orders[0] ) ? $orders[0] : '';
361 $purchased_products = [];
362 $purchased_categories = [];
363 $billing_countries = [];
364 $billing_cities = [];
365 $shipping_countries = [];
366 $shipping_cities = [];
367
368 foreach ( $orders as $order ) {
369 $total_spent += $order->get_total();
370 $billing_countries[] = $order->get_billing_country();
371 $billing_cities[] = $order->get_billing_city();
372 $shipping_countries[] = $order->get_shipping_country();
373 $shipping_cities[] = $order->get_shipping_city();
374
375 foreach ( $order->get_items() as $item ) {
376 $purchased_products[] = $item['product_id'];
377
378 $term_list = wp_get_post_terms( $item['product_id'], 'product_cat', [ 'fields' => 'ids' ] );
379 $purchased_categories = array_merge( $purchased_categories, $term_list );
380 }
381 }
382
383 $first_order_date = $first_order && ! empty( $first_order->get_date_completed() ) ? strtotime( $first_order->get_date_completed()->date( 'Y-m-d H:i:s' ) ) : '';
384 $last_order_date = $last_order && ! empty( $last_order->get_date_completed() ) ? strtotime( $last_order->get_date_completed()->date( 'Y-m-d H:i:s' ) ) : '';
385
386 self::$new_data['total_order_count'] = $order_number;
387 self::$new_data['total_spent'] = $total_spent;
388 self::$new_data['first_order_date'] = $first_order_date;
389 self::$new_data['last_order_date'] = $last_order_date;
390 self::$new_data['purchased_product'] = array_unique( $purchased_products );
391 self::$new_data['billing_country'] = array_unique( $billing_countries );
392 self::$new_data['billing_city'] = array_unique( $billing_cities );
393 self::$new_data['shipping_country'] = array_unique( $shipping_countries );
394 self::$new_data['shipping_city'] = array_unique( $shipping_cities );
395 self::$new_data['purchased_category'] = array_unique( $purchased_categories );
396
397 if ( $order_number > 0 ) {
398 self::$new_data['user_type'] = 'customer';
399 return;
400 }
401
402 self::$new_data['user_type'] = 'lead';
403 }
404
405 /**
406 * Force update data.
407 *
408 * @since 1.1.0
409 * @param string $order_id Order id.
410 */
411 public static function force_update_data( $order_id ) {
412 $user_id = get_post_meta( $order_id, '_customer_user', true );
413
414 if ( empty( $user_id ) ) {
415 $order = wc_get_order( $order_id );
416 $user = $order->get_billing_email();
417 $user_id = email_exists( $user );
418 }
419
420 if ( ! empty( $user_id ) ) {
421 update_user_meta( $user_id, self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, true );
422 }
423 }
424
425 /**
426 * Update url query string.
427 *
428 * @since 1.2.3
429 */
430 public function update_url_query_string() {
431 $old_vars = (array) Contact_Data::$historical_data;
432
433 $invalid_vars = [
434 'doing_wp_cron',
435 'customize_changeset_uuid',
436 'customize_theme',
437 'customize_messenger_channel',
438 'elementor_library',
439 'elementor-preview',
440 'ver',
441 'jupiterx-layout-builder-type',
442 'jupiterx-layout-builder-preview',
443 'preview-id',
444 ];
445
446 $valid_vars = [];
447 $new_vars = [];
448
449 $query_vars = $_GET; //phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification
450
451 if ( ! isset( $old_vars['url_query_string'] ) ) {
452 $old_vars['url_query_string'] = [];
453 }
454
455 foreach ( $query_vars as $key => $value ) {
456 if ( is_object( $old_vars['url_query_string'] ) ) {
457 $old_vars['url_query_string'] = (array) $old_vars['url_query_string'];
458 }
459
460 if (
461 in_array( $key, $invalid_vars, true ) ||
462 in_array( $value, $old_vars['url_query_string'], true )
463 ) {
464 continue;
465 }
466
467 $valid_vars[] = sanitize_text_field( $key );
468 }
469
470 if ( ! empty( $old_vars['url_query_string'] ) ) {
471 $new_vars = $old_vars['url_query_string'];
472 }
473
474 if ( ! empty( $valid_vars ) ) {
475 self::$new_data['url_query_string'] = array_unique( array_merge( $valid_vars, $new_vars ) );
476 }
477 }
478
479 /**
480 * Update viewed products. it should be done in wp hook so we have to update the data directly.
481 * it would be better to update these data with the main updating process.
482 *
483 * @since 1.1.0
484 */
485 public function update_viewed_details() {
486 $old_data = Contact_Data::$historical_data;
487
488 if ( is_singular( 'product' ) ) {
489 $product_id = get_the_ID();
490 $new_products = [];
491
492 if ( ! empty( $old_data['viewed_product'] ) ) {
493 $new_products = $old_data['viewed_product'];
494 }
495
496 if ( empty( $old_data['viewed_product'] ) || ! in_array( $product_id, $old_data['viewed_product'], false ) ) { // phpcs:ignore
497 self::$new_data['viewed_product'] = array_unique( array_merge( [ $product_id ], $new_products ) );
498 }
499 }
500
501 $queries = get_queried_object();
502 if ( ! empty( $queries->taxonomy ) && 'product_cat' === $queries->taxonomy ) {
503 $current_cat_id = $queries->term_id;
504 $new_cats = [];
505
506 if ( ! empty( $old_data['viewed_category'] ) ) {
507 $new_cats = $old_data['viewed_category'];
508 }
509
510 if ( empty( $old_data['viewed_category'] ) || ! in_array( $current_cat_id, $old_data['viewed_category'], false ) ) { // phpcs:ignore
511 self::$new_data['viewed_category'] = array_unique( array_merge( [ $current_cat_id ], $new_cats ) );
512 }
513 }
514 }
515
516 /**
517 * Updates contact segmentation data.
518 *
519 * @since 1.1.0
520 * @param array $data Data.
521 * @param string $email Email.
522 */
523 public static function update_contact_segmentation( $data, $email ) {
524 sellkit()->db->update( 'contact_segmentation', $data, [ 'email' => $email ] );
525
526 update_user_meta( get_current_user_id(), self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, false );
527 }
528 }
529