Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 165
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mailpoet_Background_Sync_Job
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 15
3080
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
 mailpoet
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 settings
0.00% covered (danger)
0.00%
0 / 1
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
 run_sync
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
90
 import_page_of_subscribers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 import_local_subscribers
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 import_subscriber
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
380
 get_total_page_count
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 set_first_import_status
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 first_import_completed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_resume_from_page
0.00% covered (danger)
0.00%
0 / 1
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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 percentage_completed
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 *
6 * MailPoet Sync: Background Sync Job (per run, site connection, currently only 1 local site)
7 *
8 */
9namespace Automattic\JetpackCRM;
10
11// block direct access
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/**
15 * MailPoet Background Sync Job class
16 */
17class Mailpoet_Background_Sync_Job {
18
19    /**
20     * Paused state
21     */
22    private $paused = false;
23
24    /**
25     * Mode (Local/API)
26     */
27    private $mode = JPCRM_MAILPOET_MODE_LOCAL;
28
29    /**
30     * Number of subscribers to process per job
31     */
32    private $subscribers_per_page = 500;
33    private $pages_per_job        = 1;
34
35    /**
36     * Current page the job is working on
37     */
38    private $current_page = 1;
39
40    /**
41     * Number of pages in MailPoet
42     */
43    private $mailpoet_total_pages = 0;
44
45    /**
46     * Number of subscribers in MailPoet
47     */
48    private $mailpoet_total_subscribers = 0;
49
50    /**
51     * A per-job cached list of MailPoet Segment data
52     */
53    public $segment_list = false;
54
55    /**
56     * If set to true this will echo progress of a sync job.
57     */
58    public $debug = false;
59
60    /**
61     * Setup MailPoet Background Sync Job
62     */
63    public function __construct( $debug = false, $subscribers_per_page = 50, $pages_per_job = 1 ) {
64
65        // set vars
66        $this->debug                = $debug;
67        $this->subscribers_per_page = $subscribers_per_page;
68        $this->pages_per_job        = $pages_per_job;
69
70        // promote paused state
71        // <for now we're pausing on this pause functionality>
72    }
73
74    /**
75     * Returns main class instance
76     */
77    public function mailpoet() {
78
79        global $zbs;
80        return $zbs->modules->mailpoet;
81    }
82
83    /**
84     * Returns full settings array from main settings class
85     */
86    public function settings() {
87
88        return $this->mailpoet()->settings->getAll();
89    }
90
91    /**
92     * If $this->debug is true, outputs passed string
93     *
94     * @param string - Debug string
95     */
96    private function debug( $str ) {
97
98        if ( $this->debug ) {
99
100            echo '[' . zeroBSCRM_locale_utsToDatetime( time() ) . '] ' . $str . '<br>';
101
102        }
103    }
104
105    /**
106     * Main job function: this will retrieve and import subscribers from MailPoet into CRM.
107     *
108     * @return mixed (int|json)
109     *   - if cron originated: a count of subscribers imported is returned
110     *   - if not cron originated (assumes AJAX):
111     *      - if completed sync: JSON summary info is output and then exit() is called
112     *      - else count of subscribers imported is returned
113     */
114    public function run_sync() {
115
116        global $zbs;
117
118        $this->debug( 'Fired `run_sync()`' );
119
120        // prep vars
121        $run_sync_job          = true;
122        $total_remaining_pages = 0;
123        $total_pages           = 0;
124        $errors                = array();
125        $subscribers_synced    = 0;
126
127        // check not marked 'paused'
128        if ( $this->paused ) {
129
130            // skip it
131            $this->debug( 'Skipping Sync (mode: ' . $this->mode . ') - Paused' );
132            $run_sync_job = false;
133
134        }
135
136        $this->debug( 'Starting Sync (mode: ' . $this->mode . ')' );
137
138        // switch by mode
139        if ( $this->mode == JPCRM_MAILPOET_MODE_LOCAL ) {
140
141            // local install
142
143            // verify mailpoet installed
144            if ( ! $zbs->mailpoet_is_active() ) {
145
146                $status_short_text = __( 'Missing MailPoet', 'zero-bs-crm' );
147
148                $this->debug( $status_short_text );
149
150                $errors[] = array(
151                    'status'            => 'error',
152                    'status_short_text' => $status_short_text,
153                    'status_long_text'  => __( 'MailPoet Sync will start importing data when you have installed the MailPoet plugin.', 'zero-bs-crm' ),
154                    'error'             => 'local_no_mailpoet',
155                );
156
157                // skip this site connection
158                $run_sync_job = false;
159
160            }
161        } else {
162
163            // no mode, or a faulty one!
164            $this->debug( 'Mode unacceptable' );
165
166            $errors[] = array(
167                'status'            => 'error',
168                'status_short_text' => $status_short_text,
169                'status_long_text'  => __( 'MailPoet Sync could not sync because it is in an unacceptable mode.', 'zero-bs-crm' ),
170                'error'             => 'mode_error',
171            );
172
173            // skip this site connection
174            $run_sync_job = false;
175
176        }
177
178        if ( $run_sync_job ) {
179
180            $this->debug( 'Running Import of ' . $this->pages_per_job . ' pages' );
181
182            // retrieve segment (list) summary data (used to tag contacts with their list participation if option)
183            $this->segment_list = $this->mailpoet()->get_mailpoet_lists_summary( true );
184
185            // do x pages
186            for ( $i = 0; $i < $this->pages_per_job; $i++ ) {
187
188                // Get last working position. The page range is 0..(total_pages - 1)
189                $page_to_retrieve = $this->resume_from_page();
190
191                // ... if for some reason we've got a negative, start from scratch.
192                if ( $page_to_retrieve < 0 ) {
193                    $page_to_retrieve = 0;
194                }
195
196                $this->current_page = $page_to_retrieve;
197
198                // import the page of subscribers
199                // This always returns the count of imported subscribers,
200                // unless 100% sync is reached, at which point it will exit (if called via AJAX)
201                // for now, we don't need to track the return
202                $subscribers_synced += (int) $this->import_page_of_subscribers( $page_to_retrieve );
203
204                $this->debug( 'Subscribers completed: ' . min( ( $this->current_page * $this->subscribers_per_page ) + $subscribers_synced, $this->mailpoet_total_subscribers ) . ' / ' . $this->mailpoet_total_subscribers );
205            }
206
207            // mark the pass
208            $this->mailpoet()->settings->update( 'last_sync_fired', time() );
209            $this->debug( 'Sync Job finished with percentage complete: ' . $this->percentage_completed( false ) . '% complete.' );
210
211        }
212
213        // return overall % counts later used to provide a summary % across sync site connections
214        $percentage_counts = $this->percentage_completed( true );
215        if ( is_array( $percentage_counts ) ) {
216            $total_pages           = (int) $percentage_counts['total_pages'];
217            $total_remaining_pages = $percentage_counts['total_pages'] - ( $percentage_counts['page_no'] + 1 );
218        }
219
220        // We should never have less than zero here
221        // (seems to happen when site connections error out)
222        if ( $total_remaining_pages < 0 ) {
223            $total_remaining_pages = 0;
224        }
225
226        return array(
227            'total_pages'           => $total_pages,
228            'total_remaining_pages' => $total_remaining_pages,
229            'errors'                => $errors,
230            'subscribers_synced'    => $subscribers_synced,
231        );
232    }
233
234    /**
235     * Retrieve and process 1 page of MailPoet Subscribers from local install (or later, API)
236     *
237     * @param int $page_no - the page number to start from. Position: 0..(total_pages - 1).
238     *
239     * @return mixed (int|json)
240     *   - if cron originated: a count of subscribers imported is returned
241     *   - if not cron originated (assumes AJAX):
242     *      - if completed sync: JSON summary info is output and then exit() is called
243     *      - else count of subscribers imported is returned
244     */
245    private function import_page_of_subscribers( $page_no ) {
246
247        $this->debug( 'Fired `import_page_of_subscribers( ' . $page_no . ' )`, importing from ' . $this->import_mode( true ) . '.' );
248
249        // store/api switch
250        if ( $this->import_mode() === JPCRM_MAILPOET_MODE_LOCAL ) {
251
252            // Local
253            return $this->import_local_subscribers( $page_no );
254
255        } else {
256
257            // not yet implemented, external import (JPCRM_MAILPOET_MODE_API)
258            return false;
259
260        }
261    }
262
263    /**
264     * Retrieve and process a page of MailPoet Subscribers from local install
265     *
266     * @param int $page_no
267     *
268     * @return mixed (int|json)
269     *   - if cron originated: a count of subscribers imported is returned
270     *   - if not cron originated (assumes AJAX):
271     *      - if completed sync: JSON summary info is output and then exit() is called
272     *      - else count of subscribers imported is returned
273     */
274    public function import_local_subscribers( $page_no = -1 ) {
275
276        // Where we're trying to run without MailPoet, fail.
277        // In theory we shouldn't ever hit this, as we catch it earlier.
278        global $zbs;
279        if ( ! $zbs->mailpoet_is_active() ) {
280            $this->debug( 'Unable to import as it appears the MailPoet plugin is not installed.' );
281            return false;
282        }
283
284        // catch paging
285        if ( $page_no < 0 ) {
286            $page_no = 0;
287        }
288        $limit  = $this->subscribers_per_page;
289        $offset = $this->subscribers_per_page * $page_no;
290
291        $this->debug( 'Retrieving page ' . $page_no . ' of subscribers (limit=' . $limit . ', offset=' . $offset . ')' );
292
293        $subscribers_synced = 0;
294
295        // Later: Allow per-list syncing, (for now this grabs all)
296        $mailpoet_subscribers = $this->mailpoet()->get_all_mailpoet_subscribers( $limit, $offset, false, true, true );
297
298        // got subs?
299        if ( is_array( $mailpoet_subscribers ) ) {
300
301            // cycle through and import
302            foreach ( $mailpoet_subscribers as $subscriber ) {
303
304                // will be an assoc arr of sub details
305                $this->import_subscriber( $subscriber );
306
307                ++$subscribers_synced;
308
309            }
310        } else {
311
312            $this->debug( 'No MailPoet subscribers found (have we reached the end of the list?)' );
313            return false;
314
315        }
316
317        // check for completion
318        $total_page_count = $this->get_total_page_count();
319        if ( $page_no >= ( $total_page_count - 1 ) ) {
320
321            $this->debug( 'MailPoet subscriber import complete!' );
322
323            // we're at 100%, mark sync complete
324            $this->set_first_import_status( true );
325
326            // set pointer to last processed page
327            $this->set_resume_from_page( $page_no );
328
329            // return count
330            return $subscribers_synced;
331
332        }
333
334        // There's still pages to go then:
335
336        // increase pointer by one, (only if we've got a full pages worth)
337        if ( $subscribers_synced === $this->subscribers_per_page ) {
338            $this->set_resume_from_page( $page_no + 1 );
339        }
340
341        // return the count
342        return $subscribers_synced;
343    }
344
345    /**
346     * Takes a MailPoet associative array for a subscriber and adds/updates a crm contact
347     *
348     * @param array $subscriber
349     *
350     * @return int $contact_id
351     */
352    public function import_subscriber( $subscriber ) {
353
354        global $zbs;
355
356        // get settings
357        $settings = $this->mailpoet()->settings->getAll();
358
359        // get wpid where passed
360        $wpid = -1;
361        if ( isset( $subscriber['wp_user_id'] ) && ! empty( $subscriber['wp_user_id'] ) ) {
362
363            $wpid = $subscriber['wp_user_id'];
364
365        }
366
367        // unused subscriber attributes:
368        // is_woocommerce_user, status, subscribed_ip, confirmed_ip, confirmed_at, last_subscribed_at,
369        // updated_at, deleted_at, unconfirmed_data, source (e.g. WordPress user), count_confirmations,
370        // unsubscribe_token, link_token, engagement_score, engagement_score_updated_at, last_engagement_at
371        // woocommerce_synced_at, email_count
372        $contact_args = array(
373
374            'data' => array(
375
376                'email'           => $subscriber['email'],
377                'fname'           => $subscriber['first_name'],
378                'lname'           => $subscriber['last_name'],
379                'wpid'            => $wpid,
380
381                'externalSources' => array(
382                    array(
383                        'source' => 'mailpoet',
384                        'uid'    => $subscriber['id'],
385                        'origin' => '', // for now this is always same-site
386                        'owner'  => 0, // for now we hard-type no owner to avoid ownership issues. As we roll out fuller ownership we may want to adapt this.
387                    ),
388                ),
389
390                'created'         => strtotime( $subscriber['created_at'] ),
391
392            ),
393            // 'extraMeta' => array()
394
395        );
396
397        // tags
398        $tags = array();
399        if ( $settings['tag_with_list'] == 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
400
401            // tag with subscriber list
402            if ( isset( $subscriber['subscriptions'] ) && is_array( $subscriber['subscriptions'] ) ) {
403
404                foreach ( $subscriber['subscriptions'] as $subscription ) {
405
406                    // only for subs
407                    if ( $subscription['status'] === 'subscribed' ) {
408
409                        /* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
410                                [id] => 1
411                                [subscriber_id] => 1
412                                [created_at] => 2022-10-11 07:55:51
413                                [segment_id] => 1
414                                [status] => subscribed
415                                [updated_at] => 2022-10-11 07:55:51
416                        */
417
418                        // find segment (list) name from our summary data
419                        // note: if this doesn't find a match it's likely
420                        // id 1+2 which seem to be WP and Woo user data sets
421                        // which are not returned by the API `getLists()`
422                        $segment_name = ( isset( $this->segment_list[ $subscription['segment_id'] ] ) ? $this->segment_list[ $subscription['segment_id'] ]['name'] : '' );
423
424                        // valid list name?
425                        // here we sidestep any with suffix `| CRM` or `| Jetpack CRM`
426                        // .. to avoid us exporting a CRM segment into a list, then reimporting and adding a tag we created
427                        if ( ! empty( $segment_name ) && ! str_ends_with( $segment_name, '| CRM' ) && ! str_ends_with( $segment_name, '| Jetpack CRM' ) ) {
428
429                            $tags[] = $settings['tag_list_prefix'] . $segment_name;
430
431                        }
432                    }
433                }
434            }
435        }
436
437        if ( $settings['tag_with_tags'] == 1 ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
438
439            // tag with subscriber tags
440            if ( isset( $subscriber['tags'] ) && is_array( $subscriber['tags'] ) ) {
441
442                foreach ( $subscriber['tags'] as $tag ) {
443
444                    /* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
445                        [id] => 54
446                        [subscriber_id] => 1
447                        [tag_id] => 4
448                        [created_at] => 2022-11-03 13:23:38
449                        [updated_at] => 2022-11-03 13:23:38
450                        [name] => xxx
451                    */
452
453                    // here we sidestep any with suffix `| CRM` or `| Jetpack CRM`
454                    // .. to avoid us exporting a CRM segment into a list, then reimporting and adding a tag we created
455                    if ( ! str_ends_with( $tag['name'], '| CRM' ) && ! str_ends_with( $tag['name'], '| Jetpack CRM' ) ) {
456
457                        $tags[] = $settings['tag_tag_prefix'] . $tag['name'];
458
459                    }
460                }
461            }
462        }
463
464        // got tags?
465        if ( count( $tags ) > 0 ) {
466
467            $contact_args['data']['tags']     = $tags;
468            $contact_args['data']['tag_mode'] = 'append';
469
470        }
471
472        // $this->debug( 'Add/Update contact: <pre>' . var_export( $contact_args ) . '</pre>' );
473
474        // Add/update the contact & return id
475        return $zbs->DAL->contacts->addUpdateContact( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
476            array(
477                'data'                 => $contact_args['data'],
478                'do_not_update_blanks' => true,
479            )
480        );
481    }
482
483    /**
484     * Returns a number for total pages to process (with current $this->subscribers_per_page)
485     *
486     * @return int $total_pages
487     */
488    public function get_total_page_count() {
489
490        // calculate it
491        $this->mailpoet_total_subscribers = $this->mailpoet()->get_all_mailpoet_subscribers_count();
492        $total_pages                      = 0;
493        if ( $this->subscribers_per_page > 0 && $this->mailpoet_total_subscribers > 0 ) {
494
495            $total_pages = ceil( $this->mailpoet_total_subscribers / $this->subscribers_per_page );
496
497        }
498
499        $this->mailpoet_total_pages = $total_pages;
500
501        return $total_pages;
502    }
503
504    /**
505     * Set's a completion status for subscriber imports
506     * (Wrapper)
507     *
508     * @param string|bool $status = 'yes|no' (#legacy) or 'true|false'
509     *
510     * @return bool $status
511     */
512    public function set_first_import_status( $status ) {
513
514        return $this->mailpoet()->background_sync->set_first_import_status( $status );
515    }
516
517    /**
518     * Returns a completion status for subscriber imports
519     * (Wrapper)
520     *
521     * @return bool $status
522     */
523    public function first_import_completed() {
524
525        return $this->mailpoet()->background_sync->first_import_completed();
526    }
527
528    /**
529     * Sets current working page index (to resume from)
530     * (Wrapper)
531     *
532     * @return int $page
533     */
534    public function set_resume_from_page( $page_no ) {
535
536        return $this->mailpoet()->background_sync->set_resume_from_page( $page_no );
537    }
538
539    /**
540     * Return current working page index (to resume from)
541     * (Wrapper)
542     *
543     * @return int $page
544     */
545    public function resume_from_page() {
546
547        return $this->mailpoet()->background_sync->resume_from_page();
548    }
549
550    /**
551     * Returns 'local' or 'api'
552     *  (whichever mode is selected in settings)
553     * (Wrapper)
554     */
555    public function import_mode( $str_mode = false ) {
556
557        return $this->mailpoet()->background_sync->import_mode( $str_mode );
558    }
559
560    /**
561     * Attempts to return the percentage completed of a sync
562     *
563     * @param bool $return_counts - Return counts (if true returns an array inc % completed, x of y pages)
564     * @param bool $use_cache - use values cached in object instead of retrieving them directly from MailPoet
565     *
566     * @return int|bool - percentage completed, or false if not attainable
567     */
568    public function percentage_completed( $return_counts = false, $use_cache = true ) {
569
570        // if not using cache, retrieve values from MailPoet
571        if ( ! $use_cache ) {
572
573            // could probably abstract the retrieval of subscribers for more nesting. For now it's fairly DRY as only in 2 places.
574
575                // Local store
576                $this->mailpoet_total_pages = $this->get_total_page_count();
577
578        }
579
580        // calculate completeness
581        if ( $this->mailpoet_total_pages === 0 ) {
582
583            // no subscribers to sync, so complete
584            $percentage_completed = 100;
585
586        } else {
587            $percentage_completed = 100 * ( $this->current_page + 1 ) / $this->mailpoet_total_pages;
588        }
589
590        $this->debug( 'Percentage completed: ' . $percentage_completed . '%' );
591        $this->debug( 'Pages completed: ' . ( $this->current_page + 1 ) . ' / ' . $this->mailpoet_total_pages );
592
593        if ( $return_counts ) {
594
595            return array(
596                'page_no'              => $this->current_page,
597                'total_pages'          => $this->mailpoet_total_pages,
598                'percentage_completed' => $percentage_completed,
599            );
600
601        }
602
603        // return
604        if ( $percentage_completed >= 0 ) {
605
606            return $percentage_completed;
607
608        }
609
610        return false;
611    }
612}