Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Klaviyo
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 4
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 is_querystring_valid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_wp_user_array
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 get_user_data
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Links in email sent from Klaviyo each have a tracking parameter (`_kx=...`) which can be queried against
4 * the Klaviyo API to get the user details, then the email address is used to find any corresponding
5 * WordPress user account.
6 *
7 * @see https://developers.klaviyo.com/en/reference/exchange
8 *
9 * @package brianhenryie/bh-wp-autologin-urls
10 */
11
12namespace BrianHenryIE\WP_Autologin_URLs\API\Integrations;
13
14use BrianHenryIE\WP_Autologin_URLs\Klaviyo\ApiException;
15use BrianHenryIE\WP_Autologin_URLs\Settings_Interface;
16use BrianHenryIE\WP_Autologin_URLs\API\User_Finder_Interface;
17use BrianHenryIE\WP_Autologin_URLs\Klaviyo\API\ProfilesApi;
18use BrianHenryIE\WP_Autologin_URLs\Klaviyo\Client;
19use Exception;
20use Psr\Log\LoggerAwareInterface;
21use Psr\Log\LoggerAwareTrait;
22use Psr\Log\LoggerInterface;
23use WP_User;
24
25/**
26 * The $_GET data is coming from links clicked outside WordPress; it will not have a nonce.
27 *
28 * phpcs:disable WordPress.Security.NonceVerification.Recommended
29 */
30class Klaviyo implements User_Finder_Interface, LoggerAwareInterface {
31    use LoggerAwareTrait;
32
33    const QUERYSTRING_PARAMETER_NAME = '_kx';
34
35    protected Client $client;
36
37    /**
38     * Constructor.
39     *
40     * @param Settings_Interface $settings The plugin settings, for the Klaviyo private key.
41     * @param LoggerInterface    $logger A PSR logger.
42     * @param ?Client            $client The Klaviyo SDK.
43     *
44     * @throws Exception When Klaviyo private key has not been set.
45     */
46    public function __construct( Settings_Interface $settings, LoggerInterface $logger, ?Client $client = null ) {
47
48        $this->setLogger( $logger );
49
50        $private_key = $settings->get_klaviyo_private_api_key();
51
52        if ( empty( $private_key ) ) {
53            throw new Exception( 'Klaviyo private key not set' );
54        }
55
56        $this->setLogger( $logger );
57        $this->client = $client ?? new Client(
58            $private_key,
59            0,
60            3
61        );
62    }
63
64    /**
65     * Determine is the querystring needed for this integration present.
66     */
67    public function is_querystring_valid(): bool {
68        return ! empty( $_GET[ self::QUERYSTRING_PARAMETER_NAME ] );
69    }
70
71    /**
72     *
73     *
74     * @return array{source:string, wp_user:\WP_User|null, user_data:array<void>|array<string,string>}
75     */
76    public function get_wp_user_array(): array {
77
78        $result              = array();
79        $result['source']    = 'BrianHenryIE\WP_Autologin_URLs\Klaviyo';
80        $result['wp_user']   = null;
81        $result['user_data'] = array();
82
83        /**
84         * This was checked above.
85         *
86         * @see Klaviyo::is_querystring_valid()
87         */
88        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
89        $klaviyo_parameter = sanitize_text_field( wp_unslash( $_GET[ self::QUERYSTRING_PARAMETER_NAME ] ) );
90
91        $user_data = $this->get_user_data( $klaviyo_parameter );
92
93        if ( empty( $user_data ) ) {
94            $this->logger->debug( 'Email not returned from Klaviyo.' );
95            return $result;
96        }
97
98        $result['user_data'] = $user_data;
99
100        if ( ! isset( $user_data['email'] ) ) {
101            return $result;
102        }
103
104        $user_email = $user_data['email'];
105
106        $user = get_user_by( 'email', $user_email );
107
108        if ( ! ( $user instanceof WP_User ) ) {
109            $this->logger->debug( "No WP_User account found for Klaviyo user {$user_data['klaviyo_user_id']}" );
110            return $result;
111        }
112
113        $this->logger->info( "User wp_user:{$user->ID}, klaviyo:{$user->ID} {$user_data['klaviyo_user_id']} found via Klaviyo Email URL." );
114
115        $result['wp_user'] = $user;
116
117        return $result;
118    }
119
120    /**
121     * Query the Klaviyo API for the user data.
122     * Map that data to an array using the WooCommerce field names.
123     *
124     * @see https://developers.klaviyo.com/en/reference/exchange
125     * @see https://developers.klaviyo.com/en/reference/get-profile
126     *
127     * @param string $kx_parameter The Klaviyo tracking URL parameter.
128     *
129     * @return array<void>|array{klaviyo_user_id:string,address:string,address_2:string,city:string,country:string,state:string,postcode:string,company:string,first_name:string,email:string,billing_phone:string,last_name:string}
130     * @throws \BrianHenryIE\WP_Autologin_URLs\Klaviyo\ApiException
131     */
132    protected function get_user_data( string $kx_parameter ): array {
133
134        /** @var ProfilesApi $profiles */
135        $profiles = $this->client->Profiles;
136
137        try {
138            /**
139             * Get the Klaviyo profile id from the tracking URL parameter.
140             *
141             * @see https://developers.klaviyo.com/en/reference/exchange
142             *
143             * @var array{id?:string} $response
144             */
145            $response = $profiles->exchange( array( 'exchange_id' => $kx_parameter ) );
146        } catch ( ApiException $exception ) { // ApiException seemingly not catching 429 errors.
147            $this->logger->error( $exception->getMessage(), array( 'exception' => $exception ) );
148            return array();
149        }
150
151        if ( ! isset( $response['id'] ) ) {
152            $this->logger->debug( 'No Klaviyo profile id found for _kx ' . $kx_parameter );
153            return array();
154        }
155
156        $klaviyo_user_id = $response['id'];
157
158        /**
159         * Query the Klaviyo API for the profile data.
160         *
161         * @see https://developers.klaviyo.com/en/reference/get-profile
162         *
163         * @var array{'$address1':string,'$address2':string,'$city':string,'$country':string,'$region':string,'$zip':string,'$organization':string,'$first_name':string,'$email':string,'$phone_number':string,'$title':string,'$last_name':string} $klaviyo_user
164         */
165        $klaviyo_user = $profiles->getProfile( $klaviyo_user_id );
166
167        $user_data_map = array(
168            '$address1'     => 'address',
169            '$address2'     => 'address_2',
170            '$city'         => 'city',
171            '$country'      => 'country',
172            '$region'       => 'state',
173            '$zip'          => 'postcode',
174            '$organization' => 'company',
175            '$first_name'   => 'first_name',
176            '$email'        => 'email',
177            '$phone_number' => 'billing_phone',
178            '$last_name'    => 'last_name',
179        );
180
181        $user_data = array();
182
183        $user_data['klaviyo_user_id'] = $klaviyo_user_id;
184
185        foreach ( $klaviyo_user as $key => $value ) {
186            if ( isset( $user_data_map[ $key ] ) ) {
187                $user_data[ $user_data_map[ $key ] ] = (string) $value;
188            }
189        }
190
191        if ( isset( $user_data['email'] ) ) {
192            $user_data['billing_email'] = $user_data['email'];
193        }
194
195        return $user_data;
196    }
197}