Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 204
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mailpoet_Background_Sync
0.00% covered (danger)
0.00%
0 / 203
0.00% covered (danger)
0.00%
0 / 19
4692
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 instance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 mailpoet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 debug
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 init_hooks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 schedule_cron
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 cron_job
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_cron
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_cron_monitor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 sync_subscribers
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
110
 set_first_import_status
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 first_import_completed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 set_resume_from_page
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 resume_from_page
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 import_mode
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 add_update_subscriber_by_id
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
506
 delete_subscriber_by_id
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 add_update_subscribers_by_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 delete_subscribers_by_id
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 *
6 * MailPoet Sync: Background Sync
7 *
8 */
9namespace Automattic\JetpackCRM;
10
11// block direct access
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/**
15 * MailPoet Background Sync class
16 */
17class Mailpoet_Background_Sync {
18
19    /**
20     * If set to true this will echo progress of a sync job.
21     */
22    public $debug = false;
23
24    /**
25     * Future proofing multi-connections
26     */
27    public $mode = JPCRM_MAILPOET_MODE_LOCAL;
28
29    /**
30     * The single instance of the class.
31     */
32    protected static $_instance = null;
33
34    /**
35     * Setup MailPoet Background Sync
36     */
37    public function __construct() {
38
39        // load job class
40        require_once JPCRM_MAILPOET_ROOT_PATH . 'includes/class-mailpoet-background-sync-job.php';
41
42        // Initialise Hooks
43        $this->init_hooks();
44
45        // Schedule cron
46        $this->schedule_cron();
47    }
48
49    /**
50     * Main Class Instance.
51     *
52     * Ensures only one instance of Mailpoet_Background_Sync is loaded or can be loaded.
53     *
54     * @since 2.0
55     * @static
56     * @see
57     * @return Mailpoet_Background_Sync main instance
58     */
59    public static function instance() {
60        if ( self::$_instance === null ) {
61            self::$_instance = new self();
62        }
63        return self::$_instance;
64    }
65
66    /**
67     * Returns main class instance
68     */
69    public function mailpoet() {
70
71        global $zbs;
72        return $zbs->modules->mailpoet;
73    }
74
75    /**
76     * If $this->debug is true, outputs passed string
77     *
78     * @param string - Debug string
79     */
80    private function debug( $str ) {
81
82        if ( $this->debug ) {
83
84            echo '[' . zeroBSCRM_locale_utsToDatetime( time() ) . '] ' . $str . '<br>';
85
86        }
87    }
88
89    /**
90     * Initialise Hooks
91     */
92    private function init_hooks() {
93
94        // cron
95        add_action( 'jpcrm_mailpoet_sync', array( $this, 'cron_job' ) );
96
97        // Syncing based on MailPoet hooks:
98
99        // Subscriber edits/changes:
100        add_action( 'mailpoet_subscriber_created', array( $this, 'add_update_subscriber_by_id' ), 1, 1 );
101        add_action( 'mailpoet_subscriber_updated', array( $this, 'add_update_subscriber_by_id' ), 1, 1 );
102        add_action( 'mailpoet_subscriber_deleted', array( $this, 'delete_subscriber_by_id' ), 1, 1 );
103        add_action( 'mailpoet_multiple_subscribers_created', array( $this, 'add_update_subscribers_by_id' ), 1, 1 );
104        add_action( 'mailpoet_multiple_subscribers_updated', array( $this, 'add_update_subscribers_by_id' ), 1, 1 );
105        add_action( 'mailpoet_multiple_subscribers_deleted', array( $this, 'delete_subscribers_by_id' ), 1, 1 );
106
107        // add our cron task to the core crm cron monitor list
108        add_filter( 'jpcrm_cron_to_monitor', array( $this, 'add_cron_monitor' ) );
109    }
110
111    /**
112     * Setup cron schedule
113     */
114    private function schedule_cron() {
115
116        // schedule it
117        if ( ! wp_next_scheduled( 'jpcrm_mailpoet_sync' ) ) {
118            wp_schedule_event( time(), '5min', 'jpcrm_mailpoet_sync' );
119        }
120    }
121
122    /**
123     * Run cron job
124     */
125    public function cron_job() {
126
127        // define global to mark this as a cron call
128        define( 'jpcrm_mailpoet_cron_running', 1 );
129
130        // fire job
131        $this->sync_subscribers();
132    }
133
134    /**
135     * Returns bool as to whether or not the current call was made via cron
136     */
137    private function is_cron() {
138
139        return defined( 'jpcrm_mailpoet_cron_running' );
140    }
141
142    /**
143     * Filter call to add the cron zbssendbot to the watcher system
144     *
145     * @param array $crons
146     * @return array
147     */
148    function add_cron_monitor( $crons ) {
149
150        if ( is_array( $crons ) ) {
151
152            $crons['jpcrm_mailpoet_sync'] = '5min';
153        }
154
155        return $crons;
156    }
157
158    /**
159     * Main job function: this will retrieve and import subscribers from MailPoet
160     *  This can be called in three 'modes'
161     *    - via cron (as defined by `jpcrm_mailpoet_cron_running`)
162     *    - via AJAX (if not via cron and not in debug mode)
163     *    - for debug (if $this->debug is set) This is designed to be called inline and will output progress of sync job
164     *
165     * @param bool $silent - if true no output will be returned (for use where we call after `add_update_subscribers_by_id()`)
166     *
167     * @return mixed (int|json)
168     *   - if cron originated: a count of orers imported is returned
169     *   - if not cron originated (assumes AJAX):
170     *      - if completed sync: JSON summary info is output and then exit() is called
171     *      - else count of subscribers imported is returned
172     */
173    public function sync_subscribers( $silent = false ) {
174
175        global $zbs;
176
177        $this->debug( 'Fired `sync_subscribers()`.' );
178
179        // check not currently running
180        if ( defined( 'jpcrm_mailpoet_running' ) ) {
181
182            $this->debug( 'Attempted to run `sync_subscribers()` when job already in progress.' );
183
184            // return blocker error
185            return array( 'status' => 'job_in_progress' );
186
187        }
188
189        $this->debug( 'Commencing syncing...' );
190
191        // prep silos
192        $total_remaining_pages = 0;
193        $total_pages           = 0;
194        $errors                = array();
195        $subscribers_synced    = 0;
196
197        // blocker
198        if ( ! defined( 'jpcrm_mailpoet_running' ) ) {
199
200            define( 'jpcrm_mailpoet_running', 1 );
201
202        }
203
204        // init class
205        $sync_job = new Mailpoet_Background_Sync_Job( $this->debug );
206
207        // start sync job
208        $sync_result = $sync_job->run_sync();
209
210        $this->debug( 'Sync Result:<pre>' . print_r( $sync_result, 1 ) . '</pre>' );
211
212        /*
213        will be
214        false
215
216        or
217
218        array(
219
220            'total_pages'           => $total_pages,
221            'total_remaining_pages' => $total_remaining_pages,
222            'errors'                => $errors,
223
224        );*/
225
226        if ( is_array( $sync_result ) && isset( $sync_result['total_pages'] ) && isset( $sync_result['total_remaining_pages'] ) ) {
227
228            // maintain overall % counts later used to provide a summary % across sync site connections
229            $total_pages           += (int) $sync_result['total_pages'];
230            $total_remaining_pages += $sync_result['total_remaining_pages'];
231            $subscribers_synced     = (int) $sync_result['subscribers_synced'];
232
233        }
234
235        // discern completeness
236        // either maxxed pages, or more likely x no = y no
237        if ( $total_remaining_pages == 0 || $this->mailpoet()->get_all_mailpoet_subscribers_count() <= $this->mailpoet()->get_crm_mailpoet_contact_count() ) {
238
239            $sync_status        = 'sync_completed';
240            $overall_percentage = 100;
241            $status_short_text  = __( 'Sync Completed', 'zero-bs-crm' );
242            $status_long_text   = __( 'MailPoet Sync has imported all existing subscribers and will continue to import future subscribers.', 'zero-bs-crm' );
243
244        } else {
245
246            $sync_status        = 'sync_part_complete';
247            $overall_percentage = (int) ( ( $total_pages - $total_remaining_pages ) / $total_pages * 100 );
248            $status_short_text  = __( 'Syncing subscribers from MailPoet...', 'zero-bs-crm' );
249            $status_long_text   = '';
250
251        }
252
253        // if cron, we just return count
254        if ( $this->is_cron() || $silent ) {
255
256            return array(
257
258                'status'               => $sync_status, // sync_completed sync_part_complete job_in_progress error
259                'status_short_text'    => $status_short_text,
260                'percentage_completed' => $overall_percentage,
261
262            );
263
264        } else {
265
266            $this->debug( 'Completed Subscriber Sync Job: ' . $sync_status );
267            $mailpoetsync_status_array = array(
268                'status'                           => $sync_status,
269                'status_short_text'                => $status_short_text,
270                'status_long_text'                 => $status_long_text,
271                'page_no'                          => ( $total_pages - $total_remaining_pages ),
272                'subscribers_synced'               => $subscribers_synced,
273                'percentage_completed'             => $overall_percentage,
274                'total_crm_contacts_from_mailpoet' => $this->mailpoet()->get_crm_mailpoet_contact_count(),
275            );
276            $mailpoet_latest_stats     = $this->mailpoet()->get_jpcrm_mailpoet_latest_stats();
277            wp_send_json( array_merge( $mailpoet_latest_stats, $mailpoetsync_status_array ) );
278        }
279    }
280
281    /**
282     * Set's a completion status for MailPoet Subscriber imports
283     *
284     * @param string|bool $status = 'yes|no' (#legacy) or 'true|false'
285     *
286     * @return bool $status
287     */
288    public function set_first_import_status( $status ) {
289
290        $status_bool = false;
291
292        if ( $status == 'yes' || $status === true ) {
293
294            $status_bool = true;
295
296        }
297
298        // set it
299        $this->mailpoet()->settings->update( 'first_import_complete', $status_bool );
300
301        return $status_bool;
302    }
303
304    /**
305     * Returns a completion status for MailPoet Subscriber imports
306     *
307     * @return bool $status
308     */
309    public function first_import_completed() {
310
311        $status_bool = false;
312
313        // get
314        $first_import_complete = $this->mailpoet()->settings->get( 'first_import_complete', false );
315
316        if ( $first_import_complete == 'yes' || $first_import_complete === true || $first_import_complete == 1 ) {
317
318            $status_bool = true;
319
320        }
321
322        return $status_bool;
323    }
324
325    /**
326     * Sets current working page index (to resume from)
327     *
328     * @return int $page
329     */
330    public function set_resume_from_page( $page_no ) {
331
332        $this->mailpoet()->settings->update( 'resume_from_page', $page_no );
333
334        return $page_no;
335    }
336
337    /**
338     * Return current working page index (to resume from)
339     *
340     * @return int $page
341     */
342    public function resume_from_page() {
343
344        return $this->mailpoet()->settings->get( 'resume_from_page', 0 );
345    }
346
347    /**
348     * Returns 'local' or 'api'
349     *  (whichever mode is selected in settings)
350     */
351    public function import_mode( $str_mode = false ) {
352
353        // import mode
354        $mode = (int) $this->mode;
355
356        // debug/string mode
357        if ( $str_mode ) {
358            if ( $mode === 0 ) {
359                return 'JPCRM_MAILPOET_MODE_LOCAL';
360            } else {
361                return 'JPCRM_MAILPOET_MODE_API';
362            }
363        }
364
365        return $mode;
366    }
367
368    /**
369     * Add or Update subscriber
370     * Fired by hooks: mailpoet_subscriber_created, mailpoet_subscriber_updated, mailpoet_subscriber_deleted
371     * Changes caught here:
372     * - first, last names
373     * - email
374     * - doens't seem to fire on: change of newsletter, change of tags
375     */
376    public function add_update_subscriber_by_id( int $subscriberId ) {
377
378        global $zbs;
379
380        // should we log changes via contact note?
381        $autolog_changes = $this->mailpoet()->settings->get( 'autolog_changes', false );
382
383        // retrieve records
384        $potential_subscriber = $this->mailpoet()->get_mailpoet_subscriber_by_subscriber_id( $subscriberId );
385        $potential_contact    = $zbs->DAL->contacts->getContact(
386            -1,
387            array(
388
389                'externalSource'    => 'mailpoet',
390                'externalSourceUID' => $subscriberId,
391
392            )
393        );
394
395        // got records?
396        if (
397                is_array( $potential_subscriber ) && isset( $potential_subscriber['email'] )
398        ) {
399
400            // Update:
401            if ( is_array( $potential_contact ) && isset( $potential_contact['id'] ) ) {
402
403                // note changes
404                $previous_data   = $potential_contact;
405                $contact_changes = array();
406
407                // email (will always be the same until https://github.com/Automattic/zero-bs-crm/issues/2565)
408                // ... in fact this next block is defunct as it stands because getSubscriber above gets the subscriber
409                // ... AFTER email change.
410                if ( $potential_subscriber['email'] != $potential_contact['email'] ) {
411
412                    $contact_changes['email'] = $potential_subscriber->data->email;
413
414                    // if email changed, add old as an alias
415                    // for that we need the old alias list to append to
416                    $contact_aliases = is_array( $potential_contact['aliases'] ) ? $potential_contact['aliases'] : array();
417                    if ( ! in_array( $previous_data['email'], $contact_aliases ) ) {
418
419                        $contact_aliases[] = $previous_data['email'];
420
421                    }
422                }
423
424                // first name
425                if ( $potential_subscriber['first_name'] != $potential_contact['fname'] ) {
426
427                    $contact_changes['fname'] = $potential_subscriber['first_name'];
428
429                }
430
431                // last name
432                if ( $potential_subscriber['last_name'] != $potential_contact['lname'] ) {
433
434                    $contact_changes['lname'] = $potential_subscriber['last_name'];
435
436                }
437
438                // enact changes
439                if ( count( $contact_changes ) > 0 ) {
440
441                    // we split this into field + contact_aliases changes, because then we can use limitedFields support
442
443                    // build limited fields:
444                    $contact_changes_as_limited_fields = array();
445                    foreach ( $contact_changes as $key => $value ) {
446
447                        $contact_changes_as_limited_fields[] = array(
448
449                            'key'  => 'zbsc_' . $key,
450                            'val'  => $value,
451                            'type' => '%s', // all are strings here
452
453                        );
454
455                    }
456
457                    // enact
458                    $zbs->DAL->contacts->addUpdateContact(
459                        array(
460
461                            'id'            => $potential_contact['id'],
462                            'limitedFields' => $contact_changes_as_limited_fields,
463
464                        )
465                    );
466
467                    // any aliases to add?
468                    if ( isset( $contact_aliases ) && count( $contact_aliases ) ) {
469
470                        foreach ( $contact_aliases as $alias ) {
471
472                            zeroBS_addObjAlias( ZBS_TYPE_CONTACT, $potential_contact['id'], $alias );
473                        }
474                    }
475
476                    // do we add logs?
477                    if ( $autolog_changes == '1' ) {
478
479                        // build log
480                        $object_change_str = '';
481                        if ( isset( $contact_changes['email'] ) ) {
482
483                            $object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'Email', 'zero-bs-crm' ), $previous_data['email'], $contact_changes['email'] );
484
485                        }
486                        if ( isset( $contact_changes['fname'] ) ) {
487
488                            $object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'First name', 'zero-bs-crm' ), $previous_data['fname'], $contact_changes['fname'] );
489
490                        }
491                        if ( isset( $contact_changes['lname'] ) ) {
492
493                            $object_change_str .= sprintf( '%s: <code>%s</code> → <code>%s</code><br>', __( 'Last name', 'zero-bs-crm' ), $previous_data['lname'], $contact_changes['lname'] );
494
495                        }
496
497                        // add log
498                        if ( ! empty( $object_change_str ) ) {
499
500                            zeroBS_addUpdateLog(
501                                $potential_contact['id'],
502                                -1,
503                                -1,
504                                array(
505                                    'type'      => __( 'Contact Changed via MailPoet', 'zero-bs-crm' ),
506                                    'shortdesc' => __( 'Contact details changed via connected MailPoet subscriber', 'zero-bs-crm' ),
507                                    'longdesc'  => $object_change_str,
508                                ),
509                                'zerobs_customer'
510                            );
511
512                        }
513                    }
514                }
515
516                return;
517
518            } else {
519
520                // New addition
521
522                // Note we can't act on this hook because currently the only thing passed is the
523                // MailPoet ID, from which the user can't currently (via MailPoet API) be retrieved
524                // ... so when they add that we can use $this->mailpoet()->get_mailpoet_subscriber_by_subscriber_id
525                // in it's real sense and write logic here to import the addition.
526                // For now we hack around this below using their GetSubscribers endpoint with a filter of `minUpdatedAt`
527                // ... though that's not a sure bet by any means.
528                // see #temporary-workaround
529                // gh-2565
530
531            }
532        }
533
534        // Temporary workaround for lack of accessibility to getSubscriberByID in MP API
535        // in the instance of newly added subs
536        // #temporary-workaround
537        // Attempts to grab the last inserted sub. This will be hit and miss, but will work smoothly for
538        // small, infrequently updated lists
539        $last_updated_guess_timestamp = time() + jpcrm_get_wp_timezone_offset_in_seconds() - 1;
540        $potential_subscribers        = $this->mailpoet()->get_mailpoet_subscribers( false, false, $last_updated_guess_timestamp, 1, 0, false, true, true );
541
542        if ( is_array( $potential_subscribers ) && count( $potential_subscribers ) > 0 ) {
543
544            // push this sub through our sync import function:
545
546            // init class
547            $sync_job = new Mailpoet_Background_Sync_Job( false );
548            $sync_job->import_subscriber( $potential_subscribers[0] );
549
550        }
551    }
552
553    /**
554     * Delete subscriber
555     * Fired by hooks: mailpoet_subscriber_created, mailpoet_subscriber_updated, mailpoet_subscriber_deleted
556     */
557    public function delete_subscriber_by_id( int $subscriberId ) {
558
559        global $zbs;
560
561        // what's the delete action?
562        $delete_action = $this->mailpoet()->settings->get( 'delete_action', 'none' );
563
564        // shall we delete the related crm contact?
565        if ( $delete_action == 'delete' || $delete_action == 'delete_save_related_objects' ) {
566
567            // retrieve record
568            $potential_contact_id = $zbs->DAL->contacts->getContact(
569                -1,
570                array(
571
572                    'externalSource'    => 'mailpoet',
573                    'externalSourceUID' => $subscriberId,
574
575                    'onlyID'            => true,
576
577                )
578            );
579
580            // got record?
581            if ( $potential_contact_id ) {
582
583                $save_orphans = false;
584                if ( $delete_action == 'delete_save_related_objects' ) {
585
586                    $save_orphans = true;
587
588                }
589
590                // delete the contact
591                $zbs->DAL->contacts->deleteContact(
592                    array(
593
594                        'id'          => $potential_contact_id,
595                        'saveOrphans' => $save_orphans,
596
597                    )
598                );
599
600            }
601        } elseif ( $delete_action == 'add_note' ) {
602
603            // if it was deleted in MailPoet but user has 'add_note' selected as delete action, we add a log to contact
604
605            // retrieve record
606            $potential_contact_id = $zbs->DAL->contacts->getContact(
607                -1,
608                array(
609
610                    'externalSource'    => 'mailpoet',
611                    'externalSourceUID' => $subscriberId,
612
613                    'onlyID'            => true,
614
615                )
616            );
617
618            // got record?
619            if ( $potential_contact_id ) {
620
621                zeroBS_addUpdateLog(
622                    $potential_contact_id,
623                    -1,
624                    -1,
625                    array(
626                        'type'      => __( 'Subscriber deleted in MailPoet', 'zero-bs-crm' ),
627                        'shortdesc' => __( 'Associated MailPoet subscriber was deleted in MailPoet', 'zero-bs-crm' ),
628                        'longdesc'  => '',
629                    ),
630                    'zerobs_customer'
631                );
632
633            }
634        }
635
636        // if we're not deleting the contact, we need to remove the external source record
637        // for the contact, because there's no link any more.
638        // ... actually if we leave it in tact it still records useful info (the fact the source was MP)
639    }
640
641    /**
642     * Add or Update subscribers
643     * Fired by hooks: mailpoet_multiple_subscribers_created, mailpoet_multiple_subscribers_updated
644     */
645    public function add_update_subscribers_by_id( int $minActionTimestamp ) {
646
647        // catch these via sync
648        $this->sync_subscribers( true );
649    }
650
651    /**
652     * Delete subscribers
653     * Fired by hook: mailpoet_multiple_subscribers_deleted
654     */
655    public function delete_subscribers_by_id( array $subscriberIds ) {
656
657        // here we rely on our other function `delete_subscriber_by_id()`
658        // which has all of the settings-based delete actions
659        if ( count( $subscriberIds ) > 0 ) {
660
661            foreach ( $subscriberIds as $subscriber_id ) {
662
663                $this->delete_subscriber_by_id( $subscriber_id );
664
665            }
666        }
667    }
668}