Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.47% covered (danger)
3.47%
5 / 144
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailPoet_Integration
3.47% covered (danger)
3.47%
5 / 144
0.00% covered (danger)
0.00%
0 / 7
5982.02
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 / 23
0.00% covered (danger)
0.00%
0 / 1
306
 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 ( $list['id'] === $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 ) { // phpcs:ignore Squiz.PHP.EmptyCatchComment
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            $id    = strtolower( str_replace( array( ' ', '_' ), '', $field->get_attribute( 'id' ) ) );
179            $label = strtolower( str_replace( array( ' ', '_' ), '', $field->get_attribute( 'label' ) ) );
180
181            // If value is not a string, we already know it's not a valid name or email.
182            if ( ! is_string( $field->value ) ) {
183                continue;
184            }
185
186            $value = trim( $field->value );
187
188            if ( ( $id === 'email' || $label === 'email' ) && ! empty( $value ) ) {
189                $subscriber_data['email'] = $value;
190            } elseif ( ( $id === 'firstname' || $label === 'firstname' ) && ! empty( $value ) ) {
191                $subscriber_data['first_name'] = $value;
192            } elseif ( ( $id === 'lastname' || $label === 'lastname' ) && ! empty( $value ) ) {
193                $subscriber_data['last_name'] = $value;
194            }
195        }
196
197        if ( empty( $subscriber_data['email'] ) ) {
198            return array();
199        }
200
201        return $subscriber_data;
202    }
203
204    /**
205     * Extract subscriber data (email, first_name, last_name) from form fields.
206     *
207     * @param Feedback $feedback Feedback object for the submission.
208     * @return array Associative array with at least 'email', optionally 'first_name', 'last_name'. Empty array if no email found.
209     */
210    protected static function get_subscriber_data( $feedback ) {
211        if ( ! $feedback->get_author_email() ) {
212            return array();
213        }
214
215        // Get email using new Feedback API.
216        $subscriber_data          = array();
217        $subscriber_data['email'] = $feedback->get_author_email();
218
219        // Try getting first and name from Feedback API.
220        if ( $feedback->get_field_value_by_label( 'First Name' ) ) {
221            $subscriber_data['first_name'] = $feedback->get_field_value_by_label( 'First Name' );
222        } elseif ( $feedback->get_field_value_by_form_field_id( 'firstname' ) ) {
223            $subscriber_data['first_name'] = $feedback->get_field_value_by_form_field_id( 'firstname' );
224        } elseif ( $feedback->get_field_value_by_form_field_id( 'first-name' ) ) {
225            $subscriber_data['first_name'] = $feedback->get_field_value_by_form_field_id( 'first-name' );
226        }
227        if ( $feedback->get_field_value_by_label( 'Last Name' ) ) {
228            $subscriber_data['last_name'] = $feedback->get_field_value_by_label( 'Last Name' );
229        } elseif ( $feedback->get_field_value_by_form_field_id( 'lastname' ) ) {
230            $subscriber_data['last_name'] = $feedback->get_field_value_by_form_field_id( 'lastname' );
231        } elseif ( $feedback->get_field_value_by_form_field_id( 'last-name' ) ) {
232            $subscriber_data['last_name'] = $feedback->get_field_value_by_form_field_id( 'last-name' );
233        }
234
235        return $subscriber_data;
236    }
237
238    /**
239     * Handle MailPoet integration after feedback post is inserted.
240     *
241     * @param int   $post_id      The post ID for the feedback CPT.
242     * @param array $fields       Collection of Contact_Form_Field instances.
243     * @param bool  $is_spam      Whether the submission is spam.
244     */
245    public static function handle_mailpoet_integration( $post_id, $fields, $is_spam ) {
246        if ( $is_spam ) {
247            return;
248        }
249
250        // Try and get the form from any of the fields
251        $form = null;
252        foreach ( $fields as $field ) {
253            if ( ! empty( $field->form ) ) {
254                $form = $field->form;
255                break;
256            }
257        }
258        if ( ! $form || ! is_a( $form, 'Automattic\Jetpack\Forms\ContactForm\Contact_Form' ) ) {
259            return;
260        }
261
262        if ( empty( $form->attributes['mailpoet']['enabledForForm'] ?? null ) ) {
263            return;
264        }
265
266        $feedback = Feedback::get( $post_id );
267        if ( ! $feedback ) {
268            return;
269        }
270
271        $post       = get_post( $post_id );
272        $is_v2_data = ( $post && $post->post_mime_type === 'v2' );
273
274        if ( $is_v2_data ) {
275            if ( $feedback->has_field_type( 'consent' ) && ! $feedback->has_consent() ) {
276                return;
277            }
278        } else {
279            $consent_field = null;
280            if ( is_array( $form->fields ) ) {
281                foreach ( $form->fields as $form_field ) {
282                    if ( 'consent' === $form_field->get_attribute( 'type' ) ) {
283                        $consent_field = $form_field;
284                        break;
285                    }
286                }
287            }
288            if ( $consent_field ) {
289                $consent_type = strtolower( (string) $consent_field->get_attribute( 'consenttype' ) );
290                if ( 'explicit' === $consent_type && ! $consent_field->value ) {
291                    return;
292                }
293            }
294        }
295
296        $mailpoet_api = self::get_api();
297        if ( ! $mailpoet_api ) {
298            // MailPoet is not active or not loaded.
299            return;
300        }
301
302        // Get listId and listName from the mailpoet attribute
303        $mailpoet_attr = is_array( $form->attributes['mailpoet'] ) ? $form->attributes['mailpoet'] : array();
304        $list_id       = $mailpoet_attr['listId'] ?? null;
305        $list_name     = $mailpoet_attr['listName'] ?? null;
306
307        $list_id = self::get_or_create_list_id( $mailpoet_api, $list_id, $list_name );
308        if ( ! $list_id ) {
309            // Could not get or create the list; bail out.
310            return;
311        }
312
313        $subscriber_data = $is_v2_data ? self::get_subscriber_data( $feedback ) : self::get_subscriber_data_from_fields( $fields );
314        if ( empty( $subscriber_data ) ) {
315            // Email is required for MailPoet subscribers.
316            return;
317        }
318
319        self::add_subscriber_to_list( $mailpoet_api, $list_id, $subscriber_data );
320    }
321
322    /**
323     * Get all MailPoet lists.
324     *
325     * @return array List of MailPoet lists, or empty array on failure.
326     */
327    public static function get_all_lists() {
328        $mailpoet_api = self::get_api();
329        if ( ! $mailpoet_api ) {
330            return array();
331        }
332        try {
333            return $mailpoet_api->getLists();
334        } catch ( \Exception $e ) {
335            return array();
336        }
337    }
338}