Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.45% covered (danger)
3.45%
5 / 145
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailPoet_Integration
3.45% covered (danger)
3.45%
5 / 145
0.00% covered (danger)
0.00%
0 / 7
6134.12
0.00% covered (danger)
0.00%
0 / 1
 get_api
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_or_create_list_id
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
 add_subscriber_to_list
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
240
 get_subscriber_data_from_fields
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
342
 get_subscriber_data
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 handle_mailpoet_integration
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
552
 get_all_lists
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
1<?php
2/**
3 * MailPoet Integration for Jetpack Contact Forms.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Forms\Service;
9
10use Automattic\Jetpack\Forms\ContactForm\Feedback;
11
12/**
13 * Class MailPoet_Integration
14 *
15 * Handles integration with MailPoet for Jetpack Contact Forms.
16 */
17class MailPoet_Integration {
18    /**
19     * MailPoet API instance
20     *
21     * @var mixed
22     */
23    protected static $mailpoet_api = null;
24
25    /**
26     * Get the MailPoet API instance (v1), instantiating if necessary.
27     *
28     * @return mixed
29     */
30    protected static function get_api() {
31        if ( null === self::$mailpoet_api && class_exists( '\MailPoet\API\API' ) ) {
32            // @phan-suppress-next-line PhanUndeclaredClassMethod
33            self::$mailpoet_api = \MailPoet\API\API::MP( 'v1' );
34        }
35        return self::$mailpoet_api;
36    }
37
38    /**
39     * Get or create a MailPoet list for Jetpack Forms.
40     *
41     * @param mixed       $mailpoet_api The MailPoet API instance.
42     * @param string|null $list_id Optional. The ID of the list to use if it exists.
43     * @param string|null $list_name Optional. The name of the list to create if no ID is provided. Defaults to 'Jetpack Form Subscribers'.
44     * @return string|null List ID or null on failure.
45     */
46    protected static function get_or_create_list_id( $mailpoet_api, $list_id = null, $list_name = null ) {
47        // 1. If listId is provided, check if it exists
48        if ( $list_id ) {
49            try {
50                $lists = $mailpoet_api->getLists();
51                foreach ( $lists as $list ) {
52                    if ( (string) $list['id'] === (string) $list_id && empty( $list['deleted_at'] ) ) {
53                        return $list['id'];
54                    }
55                }
56            } catch ( \Exception $e ) { // phpcs:ignore Squiz.PHP.EmptyCatchComment,Generic.CodeAnalysis.EmptyStatement.DetectedCatch
57                // Intentionally empty: fall through to next step
58            }
59        }
60
61        // 2. If listName is provided, create a new list
62        if ( $list_name ) {
63            try {
64                $new_list = $mailpoet_api->addList(
65                    array(
66                        'name'        => $list_name,
67                        'description' => 'Created by Jetpack Forms',
68                    )
69                );
70                return $new_list['id'];
71            } catch ( \Exception $e ) { // phpcs:ignore Squiz.PHP.EmptyCatchComment,Generic.CodeAnalysis.EmptyStatement.DetectedCatch
72                // Intentionally empty: fall through to default
73            }
74        }
75
76        // 3. Fallback: use or create the default list
77        $default_list_name        = 'Jetpack Forms';
78        $default_list_description = __( 'Subscribers from Jetpack Forms', 'jetpack-forms' );
79        try {
80            $lists = $mailpoet_api->getLists();
81            foreach ( $lists as $list ) {
82                if ( $list['name'] === $default_list_name && empty( $list['deleted_at'] ) ) {
83                    return $list['id'];
84                }
85            }
86            $new_list = $mailpoet_api->addList(
87                array(
88                    'name'        => $default_list_name,
89                    'description' => $default_list_description,
90                )
91            );
92            return $new_list['id'];
93        } catch ( \Exception $e ) {
94            return null;
95        }
96    }
97
98    /**
99     * Add a subscriber to a MailPoet list.
100     *
101     * @param mixed  $mailpoet_api The MailPoet API instance.
102     * @param string $list_id The MailPoet list ID.
103     * @param array  $subscriber_data Associative array with at least 'email', optionally 'first_name', 'last_name'.
104     * @return array|null Subscriber data on success, or null on failure.
105     */
106    protected static function add_subscriber_to_list( $mailpoet_api, $list_id, $subscriber_data ) {
107        $email = $subscriber_data['email'];
108        try {
109            $existing = $mailpoet_api->getSubscriber( $email );
110
111            // Normalize "subscribed" status using MailPoet constant when available.
112            $status_subscribed = class_exists( '\MailPoet\Entities\SubscriberEntity' )
113                // @phan-suppress-next-line PhanUndeclaredClassConstant
114                ? \MailPoet\Entities\SubscriberEntity::STATUS_SUBSCRIBED
115                : 'subscribed';
116
117            // If already subscribed to list, do nothing.
118            if ( ! empty( $existing['subscriptions'] ) && is_array( $existing['subscriptions'] ) ) {
119                foreach ( $existing['subscriptions'] as $subscription ) {
120                    if (
121                        isset( $subscription['segment_id'] ) && isset( $subscription['status'] ) &&
122                        (string) $subscription['segment_id'] === (string) $list_id &&
123                        $status_subscribed === $subscription['status']
124                    ) {
125                        return $existing;
126                    }
127                }
128            }
129
130            // Subscriber exists but is not on the target list, so add to list.
131            // If subscriber already confirmed ('subscribed'), do not resend confirmation.
132            $options = array();
133            if ( isset( $existing['status'] ) && $existing['status'] === $status_subscribed ) {
134                $options['send_confirmation_email'] = false;
135            }
136
137            return $mailpoet_api->subscribeToLists( $email, array( $list_id ), $options );
138        } catch ( \Exception $e ) {
139            // MailPoet returns APIException code 4 if "subscriber does not exist".
140            // In that case, take no action and next try statement will add subscriber.
141            // For other exceptions, return null.
142            $not_found_code = 4;
143            if ( method_exists( $e, 'getCode' ) && (int) $e->getCode() !== $not_found_code ) {
144                return null;
145            }
146        }
147
148        // Subscriber does not exist, so add new subscriber to list and send confirmation email.
149        try {
150            return $mailpoet_api->addSubscriber( $subscriber_data, array( $list_id ) );
151        } catch ( \Exception $e ) {
152            return null;
153        }
154    }
155
156    /**
157     * Extract subscriber data (email, first_name, last_name) from form fields.
158     * Once refactored form storage is in place, use get_subscriber_data() instead.
159     *
160     * @param array $fields Collection of Contact_Form_Field instances.
161     * @return array Associative array with at least 'email', optionally 'first_name', 'last_name'. Empty array if no email found.
162     */
163    protected static function get_subscriber_data_from_fields( $fields ) {
164        // Try and get the form from any of the fields
165        $form = null;
166        foreach ( $fields as $field ) {
167            if ( ! empty( $field->form ) ) {
168                $form = $field->form;
169                break;
170            }
171        }
172        if ( ! $form || ! is_a( $form, 'Automattic\Jetpack\Forms\ContactForm\Contact_Form' ) ) {
173            return array();
174        }
175
176        $subscriber_data = array();
177        foreach ( $form->fields as $field ) {
178            $type  = strtolower( (string) $field->get_attribute( 'type' ) );
179            $id    = strtolower( str_replace( array( ' ', '_' ), '', (string) $field->get_attribute( 'id' ) ) );
180            $label = strtolower( str_replace( array( ' ', '_' ), '', (string) $field->get_attribute( 'label' ) ) );
181
182            // If value is not a string, we already know it's not a valid name or email.
183            if ( ! is_string( $field->value ) ) {
184                continue;
185            }
186
187            $value = trim( $field->value );
188
189            if ( ( $type === 'email' || $id === 'email' || $label === 'email' ) && ! empty( $value ) ) {
190                $subscriber_data['email'] = $value;
191            } elseif ( ( $id === 'firstname' || $label === 'firstname' ) && ! empty( $value ) ) {
192                $subscriber_data['first_name'] = $value;
193            } elseif ( ( $id === 'lastname' || $label === 'lastname' ) && ! empty( $value ) ) {
194                $subscriber_data['last_name'] = $value;
195            }
196        }
197
198        if ( empty( $subscriber_data['email'] ) ) {
199            return array();
200        }
201
202        return $subscriber_data;
203    }
204
205    /**
206     * Extract subscriber data (email, first_name, last_name) from form fields.
207     *
208     * @param Feedback $feedback Feedback object for the submission.
209     * @return array Associative array with at least 'email', optionally 'first_name', 'last_name'. Empty array if no email found.
210     */
211    protected static function get_subscriber_data( $feedback ) {
212        if ( ! $feedback->get_author_email() ) {
213            return array();
214        }
215
216        // Get email using new Feedback API.
217        $subscriber_data          = array();
218        $subscriber_data['email'] = $feedback->get_author_email();
219
220        // Try getting first and name from Feedback API.
221        if ( $feedback->get_field_value_by_label( 'First Name' ) ) {
222            $subscriber_data['first_name'] = $feedback->get_field_value_by_label( 'First Name' );
223        } elseif ( $feedback->get_field_value_by_form_field_id( 'firstname' ) ) {
224            $subscriber_data['first_name'] = $feedback->get_field_value_by_form_field_id( 'firstname' );
225        } elseif ( $feedback->get_field_value_by_form_field_id( 'first-name' ) ) {
226            $subscriber_data['first_name'] = $feedback->get_field_value_by_form_field_id( 'first-name' );
227        }
228        if ( $feedback->get_field_value_by_label( 'Last Name' ) ) {
229            $subscriber_data['last_name'] = $feedback->get_field_value_by_label( 'Last Name' );
230        } elseif ( $feedback->get_field_value_by_form_field_id( 'lastname' ) ) {
231            $subscriber_data['last_name'] = $feedback->get_field_value_by_form_field_id( 'lastname' );
232        } elseif ( $feedback->get_field_value_by_form_field_id( 'last-name' ) ) {
233            $subscriber_data['last_name'] = $feedback->get_field_value_by_form_field_id( 'last-name' );
234        }
235
236        return $subscriber_data;
237    }
238
239    /**
240     * Handle MailPoet integration after feedback post is inserted.
241     *
242     * @param int   $post_id      The post ID for the feedback CPT.
243     * @param array $fields       Collection of Contact_Form_Field instances.
244     * @param bool  $is_spam      Whether the submission is spam.
245     */
246    public static function handle_mailpoet_integration( $post_id, $fields, $is_spam ) {
247        if ( $is_spam ) {
248            return;
249        }
250
251        // Try and get the form from any of the fields
252        $form = null;
253        foreach ( $fields as $field ) {
254            if ( ! empty( $field->form ) ) {
255                $form = $field->form;
256                break;
257            }
258        }
259        if ( ! $form || ! is_a( $form, 'Automattic\Jetpack\Forms\ContactForm\Contact_Form' ) ) {
260            return;
261        }
262
263        if ( empty( $form->attributes['mailpoet']['enabledForForm'] ?? null ) ) {
264            return;
265        }
266
267        $feedback = Feedback::get( $post_id );
268        if ( ! $feedback ) {
269            return;
270        }
271
272        $post       = get_post( $post_id );
273        $is_v2_data = ( $post && $post->post_mime_type === 'v2' );
274
275        if ( $is_v2_data ) {
276            if ( $feedback->has_field_type( 'consent' ) && ! $feedback->has_consent() ) {
277                return;
278            }
279        } else {
280            $consent_field = null;
281            if ( is_array( $form->fields ) ) {
282                foreach ( $form->fields as $form_field ) {
283                    if ( 'consent' === $form_field->get_attribute( 'type' ) ) {
284                        $consent_field = $form_field;
285                        break;
286                    }
287                }
288            }
289            if ( $consent_field ) {
290                $consent_type = strtolower( (string) $consent_field->get_attribute( 'consenttype' ) );
291                if ( 'explicit' === $consent_type && ! $consent_field->value ) {
292                    return;
293                }
294            }
295        }
296
297        $mailpoet_api = self::get_api();
298        if ( ! $mailpoet_api ) {
299            // MailPoet is not active or not loaded.
300            return;
301        }
302
303        // Get listId and listName from the mailpoet attribute
304        $mailpoet_attr = is_array( $form->attributes['mailpoet'] ) ? $form->attributes['mailpoet'] : array();
305        $list_id       = $mailpoet_attr['listId'] ?? null;
306        $list_name     = $mailpoet_attr['listName'] ?? null;
307
308        $list_id = self::get_or_create_list_id( $mailpoet_api, $list_id, $list_name );
309        if ( ! $list_id ) {
310            // Could not get or create the list; bail out.
311            return;
312        }
313
314        $subscriber_data = $is_v2_data ? self::get_subscriber_data( $feedback ) : self::get_subscriber_data_from_fields( $fields );
315        if ( empty( $subscriber_data ) ) {
316            // Email is required for MailPoet subscribers.
317            return;
318        }
319
320        self::add_subscriber_to_list( $mailpoet_api, $list_id, $subscriber_data );
321    }
322
323    /**
324     * Get all MailPoet lists.
325     *
326     * @return array List of MailPoet lists, or empty array on failure.
327     */
328    public static function get_all_lists() {
329        $mailpoet_api = self::get_api();
330        if ( ! $mailpoet_api ) {
331            return array();
332        }
333        try {
334            return $mailpoet_api->getLists();
335        } catch ( \Exception $e ) {
336            return array();
337        }
338    }
339}