PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 2.7.0
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v2.7.0
2.7.0 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 All 43 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 2.7.0, at includes/contact-segmentation/contact-data-updater.php

556 lines 14.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 || empty( self::$new_data['user_type'] ) ) { //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 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public UTM/analytics query parameters.
192 $data = array_change_key_case( $_GET, CASE_LOWER );
193
194 if ( empty( $data[ $utm ] ) ) {
195 return $value;
196 }
197
198 return sanitize_text_field( $data[ $utm ] );
199 }
200
201 /**
202 * Update locations data from API.
203 *
204 * @since 1.1.0
205 */
206 private function maybe_update_locations() {
207 $ip = sellkit_get_ip();
208
209 if ( ! empty( $this->data['ip'] ) && $ip === $this->data['ip'] && isset( $this->data['visitor_country'] ) && ! empty( $this->data['visitor_country'] ) ) {
210 return;
211 }
212
213 $account_id = sellkit_get_option( 'geolocation_account_id' );
214 $api_key = sellkit_get_option( 'geolocation_licence_key' );
215 $maxmind_api_url = $this->get_maxmind_url();
216
217 if ( empty( $api_key ) || empty( $account_id ) ) {
218 return;
219 }
220
221 $auth = base64_encode( $account_id . ':' . $api_key );
222
223 $response = wp_remote_get( $maxmind_api_url . $ip, [
224 'timeout' => 10,
225 'headers' => [
226 'Authorization' => 'Basic ' . $auth,
227 'Accept' => 'application/json',
228 ],
229 ] );
230
231 $response_code = wp_remote_retrieve_response_code( $response );
232
233 if ( ! is_wp_error( $response ) && ! empty( $response['body'] ) && 200 === (int) $response_code ) {
234 $location_data = json_decode( $response['body'] );
235
236 if ( isset( $location_data->country->names->en ) ) {
237 self::$new_data['visitor_country'] = strtolower( $location_data->country->names->en );
238 }
239
240 if ( isset( $location_data->city->names->en ) ) {
241 self::$new_data['visitor_city'] = strtolower( $location_data->city->names->en );
242 }
243 }
244
245 self::$new_data['updated_at'] = time();
246 }
247
248 /**
249 * Maxmind Api Url.
250 *
251 * @since 2.3.4
252 */
253 private function get_maxmind_url() {
254 $is_free_maxmind = sellkit_get_option( 'maxmind_is_free_service' ) === '1' ? true : false;
255 $maxmind_api_url = $is_free_maxmind ? 'https://geolite.info/geoip/v2.1/city/' : 'https://geoip.maxmind.com/geoip/v2.1/city/';
256
257 return $maxmind_api_url;
258 }
259
260 /**
261 * Update user type.
262 *
263 * @since 1.2.3
264 */
265 private function update_user_type() {
266 if ( empty( $this->data['user_type'] ) ) {
267 self::$new_data['user_type'] = 'first_time_visitor';
268 }
269
270 if ( ! empty( $this->data['user_type'] ) ) {
271 self::$new_data['user_type'] = 'returning_visitor';
272 }
273 }
274
275 /**
276 * Updates device types.
277 *
278 * @since 1.1.0
279 */
280 private function maybe_update_device() {
281 if ( ! class_exists( 'Mobile_Detect' ) ) {
282 require_once __DIR__ . '/libraries/mobile-detect.php';
283 }
284 $detector = new Mobile_Detect();
285 $current_display = 'desktop';
286
287 if ( $detector->isMobile() ) {
288 $current_display = 'mobile';
289 }
290
291 if ( $detector->isTablet() ) {
292 $current_display = 'tablet';
293 }
294
295 if ( ! empty( $this->data['user_device'] ) && $this->data['user_device'] === $current_display ) {
296 return;
297 }
298
299 self::$new_data['user_device'] = $current_display;
300 }
301
302 /**
303 * Update browser language.
304 *
305 * @since 1.1.0
306 */
307 private function update_browser_language() {
308 $data = [];
309
310 if ( ! empty( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
311 $data = explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ); // phpcs:ignore
312 }
313
314 $language = ! empty( $data[0] ) ? $data[0] : '';
315
316 if ( empty( $language ) ) {
317 return;
318 }
319
320 if ( empty( $this->data['browser_language'] ) || ( $this->data['browser_language'] !== $language ) ) {
321 self::$new_data['browser_language'] = $language;
322 }
323 }
324
325 /**
326 * Updates the IP if it's changed.
327 *
328 * @since 1.2.1
329 */
330 private function maybe_update_ip() {
331 $ip = sellkit_get_ip();
332
333 if ( ! empty( $this->data['ip'] ) && $this->data['ip'] === $ip ) {
334 return;
335 }
336
337 self::$new_data['ip'] = $ip;
338 }
339
340 /**
341 * Checks utm value.
342 *
343 * @param string $new_utm New Utm.
344 * @param string $type Utm type.
345 * @return bool
346 */
347 private function check_utm_is_new( $new_utm, $type ) {
348 if ( empty( $new_utm ) ) {
349 return false;
350 }
351
352 if ( empty( $this->data[ $type ] ) ) {
353 return true;
354 }
355
356 if ( $new_utm !== $this->data[ $type ] ) {
357 return true;
358 }
359 }
360
361 /**
362 * Updates user information.
363 *
364 * @since 1.1.0
365 */
366 public function update_order_info() {
367 $current_user = wp_get_current_user();
368
369 $args = [
370 'customer_id' => $current_user->ID,
371 'post_status' => 'completed',
372 'post_type' => 'shop_order',
373 'limit' => -1,
374 'orderby' => 'id',
375 'order' => 'DESC',
376 ];
377
378 if ( ! sellkit()->has_valid_dependencies() ) {
379 return;
380 }
381
382 $orders = wc_get_orders( $args );
383 $order_number = count( $orders );
384 $total_spent = 0;
385 $first_order = ! empty( end( $orders ) ) ? end( $orders ) : '';
386 $last_order = ! empty( $orders[0] ) ? $orders[0] : '';
387 $purchased_products = [];
388 $purchased_categories = [];
389 $billing_countries = [];
390 $billing_cities = [];
391 $shipping_countries = [];
392 $shipping_cities = [];
393
394 foreach ( $orders as $order ) {
395 $total_spent += $order->get_total();
396 $billing_countries[] = $order->get_billing_country();
397 $billing_cities[] = $order->get_billing_city();
398 $shipping_countries[] = $order->get_shipping_country();
399 $shipping_cities[] = $order->get_shipping_city();
400
401 foreach ( $order->get_items() as $item ) {
402 $purchased_products[] = $item['product_id'];
403
404 $term_list = wp_get_post_terms( $item['product_id'], 'product_cat', [ 'fields' => 'ids' ] );
405 $purchased_categories = array_merge( $purchased_categories, $term_list );
406 }
407 }
408
409 $first_order_date = $first_order && ! empty( $first_order->get_date_completed() ) ? strtotime( $first_order->get_date_completed()->date( 'Y-m-d H:i:s' ) ) : '';
410 $last_order_date = $last_order && ! empty( $last_order->get_date_completed() ) ? strtotime( $last_order->get_date_completed()->date( 'Y-m-d H:i:s' ) ) : '';
411
412 self::$new_data['total_order_count'] = $order_number;
413 self::$new_data['total_spent'] = $total_spent;
414 self::$new_data['first_order_date'] = $first_order_date;
415 self::$new_data['last_order_date'] = $last_order_date;
416 self::$new_data['purchased_product'] = array_unique( $purchased_products );
417 self::$new_data['billing_country'] = array_unique( $billing_countries );
418 self::$new_data['billing_city'] = array_unique( $billing_cities );
419 self::$new_data['shipping_country'] = array_unique( $shipping_countries );
420 self::$new_data['shipping_city'] = array_unique( $shipping_cities );
421 self::$new_data['purchased_category'] = array_unique( $purchased_categories );
422
423 if ( $order_number > 0 ) {
424 self::$new_data['user_type'] = 'customer';
425 return;
426 }
427
428 self::$new_data['user_type'] = 'lead';
429 }
430
431 /**
432 * Force update data.
433 *
434 * @since 1.1.0
435 * @param string $order_id Order id.
436 */
437 public static function force_update_data( $order_id ) {
438 $user_id = get_post_meta( $order_id, '_customer_user', true );
439
440 if ( empty( $user_id ) ) {
441 $order = wc_get_order( $order_id );
442 $user = $order->get_billing_email();
443 $user_id = email_exists( $user );
444 }
445
446 if ( ! empty( $user_id ) ) {
447 update_user_meta( $user_id, self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, true );
448 }
449 }
450
451 /**
452 * Update url query string.
453 *
454 * @since 1.2.3
455 */
456 public function update_url_query_string() {
457 $old_vars = (array) Contact_Data::$historical_data;
458
459 $invalid_vars = [
460 'doing_wp_cron',
461 'customize_changeset_uuid',
462 'customize_theme',
463 'customize_messenger_channel',
464 'elementor_library',
465 'elementor-preview',
466 'ver',
467 'jupiterx-layout-builder-type',
468 'jupiterx-layout-builder-preview',
469 'preview-id',
470 ];
471
472 $valid_vars = [];
473 $new_vars = [];
474
475 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Merging stored URL query keys with current request for segmentation.
476 $query_vars = $_GET;
477
478 if ( ! isset( $old_vars['url_query_string'] ) ) {
479 $old_vars['url_query_string'] = [];
480 }
481
482 foreach ( $query_vars as $key => $value ) {
483 if ( is_object( $old_vars['url_query_string'] ) ) {
484 $old_vars['url_query_string'] = (array) $old_vars['url_query_string'];
485 }
486
487 if (
488 in_array( $key, $invalid_vars, true ) ||
489 in_array( $value, $old_vars['url_query_string'], true )
490 ) {
491 continue;
492 }
493
494 $valid_vars[] = sanitize_text_field( $key );
495 }
496
497 if ( ! empty( $old_vars['url_query_string'] ) ) {
498 $new_vars = $old_vars['url_query_string'];
499 }
500
501 if ( ! empty( $valid_vars ) ) {
502 self::$new_data['url_query_string'] = array_unique( array_merge( $valid_vars, $new_vars ) );
503 }
504 }
505
506 /**
507 * Update viewed products. it should be done in wp hook so we have to update the data directly.
508 * it would be better to update these data with the main updating process.
509 *
510 * @since 1.1.0
511 */
512 public function update_viewed_details() {
513 $old_data = Contact_Data::$historical_data;
514
515 if ( is_singular( 'product' ) ) {
516 $product_id = get_the_ID();
517 $new_products = [];
518
519 if ( ! empty( $old_data['viewed_product'] ) ) {
520 $new_products = $old_data['viewed_product'];
521 }
522
523 if ( empty( $old_data['viewed_product'] ) || ! in_array( $product_id, $old_data['viewed_product'], false ) ) { // phpcs:ignore
524 self::$new_data['viewed_product'] = array_unique( array_merge( [ $product_id ], $new_products ) );
525 }
526 }
527
528 $queries = get_queried_object();
529 if ( ! empty( $queries->taxonomy ) && 'product_cat' === $queries->taxonomy ) {
530 $current_cat_id = $queries->term_id;
531 $new_cats = [];
532
533 if ( ! empty( $old_data['viewed_category'] ) ) {
534 $new_cats = $old_data['viewed_category'];
535 }
536
537 if ( empty( $old_data['viewed_category'] ) || ! in_array( $current_cat_id, $old_data['viewed_category'], false ) ) { // phpcs:ignore
538 self::$new_data['viewed_category'] = array_unique( array_merge( [ $current_cat_id ], $new_cats ) );
539 }
540 }
541 }
542
543 /**
544 * Updates contact segmentation data.
545 *
546 * @since 1.1.0
547 * @param array $data Data.
548 * @param string $email Email.
549 */
550 public static function update_contact_segmentation( $data, $email ) {
551 sellkit()->db->update( 'contact_segmentation', $data, [ 'email' => $email ] );
552
553 update_user_meta( get_current_user_id(), self::SELLKIT_CONTACT_SEGMENTATION_FORCE_UPDATE, false );
554 }
555 }
556