/**
  * Function to actually build the form
  *
  * @return None
  * @access public
  */
 public function buildQuickForm()
 {
     $this->addFormRule(array('CRM_Mailchimp_Form_Setting', 'formRule'), $this);
     CRM_Core_Resources::singleton()->addStyleFile('uk.co.vedaconsulting.mailchimp', 'css/mailchimp.css');
     $webhook_url = CRM_Utils_System::url('civicrm/mailchimp/webhook', 'reset=1', TRUE, NULL, FALSE, TRUE);
     $this->assign('webhook_url', 'Webhook URL - ' . $webhook_url);
     // Add the API Key Element
     $this->addElement('text', 'api_key', ts('API Key'), array('size' => 48));
     // Add the User Security Key Element
     $this->addElement('text', 'security_key', ts('Security Key'), array('size' => 24));
     // Add Enable or Disable Debugging
     $enableOptions = array(1 => ts('Yes'), 0 => ts('No'));
     $this->addRadio('enable_debugging', ts('Enable Debugging'), $enableOptions, NULL);
     // Create the Submit Button.
     $buttons = array(array('type' => 'submit', 'name' => ts('Save & Test')));
     $groups = CRM_Mailchimp_Utils::getGroupsToSync(array(), null, $membership_only = TRUE);
     foreach ($groups as $group_id => $details) {
         $list = new Mailchimp_Lists(CRM_Mailchimp_Utils::mailchimp());
         $webhookoutput = $list->webhooks($details['list_id']);
         if ($webhookoutput[0]['sources']['api'] == 1) {
             CRM_Mailchimp_Utils::checkDebug('CRM_Mailchimp_Form_Setting - API is set in Webhook setting for listID', $details['list_id']);
             $listID = $details['list_id'];
             CRM_Core_Session::setStatus(ts('API is set in Webhook setting for listID %1', array(1 => $listID)), ts('Error'), 'error');
             break;
         }
     }
     // Add the Buttons.
     $this->addButtons($buttons);
 }
 static function manageCiviCRMGroupSubcription($contactID = array(), $requestData, $action)
 {
     CRM_Mailchimp_Utils::checkDebug('Start- CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $contactID= ', $contactID);
     CRM_Mailchimp_Utils::checkDebug('Start- CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $requestData= ', $requestData);
     CRM_Mailchimp_Utils::checkDebug('Start- CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $requestType= ', $action);
     if (empty($contactID) || empty($requestData['list_id']) || empty($action)) {
         return NULL;
     }
     $listID = $requestData['list_id'];
     $groupContactRemoves = $groupContactAdditions = array();
     // Deal with subscribe/unsubscribe.
     // We need the CiviCRM membership group for this list.
     $groups = CRM_Mailchimp_Utils::getGroupsToSync(array(), $listID, $membership_only = TRUE);
     $allGroups = CRM_Mailchimp_Utils::getGroupsToSync(array(), $listID, $membership_only = FALSE);
     if (!$groups) {
         // This list is not mapped to a group in CiviCRM.
         return NULL;
     }
     $_ = array_keys($groups);
     $membershipGroupID = $_[0];
     if ($action == 'subscribe') {
         $groupContactAdditions[$membershipGroupID][] = $contactID;
     } elseif ($action == 'unsubscribe') {
         $groupContactRemoves[$membershipGroupID][] = $contactID;
         $mcGroupings = array();
         foreach (empty($requestData['merges']['GROUPINGS']) ? array() : $requestData['merges']['GROUPINGS'] as $grouping) {
             foreach (explode(', ', $grouping['groups']) as $group) {
                 $mcGroupings[$grouping['id']][$group] = 1;
             }
         }
         foreach ($allGroups as $groupID => $details) {
             if ($groupID != $membershipGroupID && $details['is_mc_update_grouping']) {
                 if (!empty($mcGroupings[$details['grouping_id']][$details['group_name']])) {
                     $groupContactRemoves[$groupID][] = $contactID;
                 }
             }
         }
     }
     // Now deal with all the groupings that are mapped to CiviCRM groups for this list
     // and that have the allow MC updates flag set.
     /* Sample groupings from MC:
      *
      *     [GROUPINGS] => Array(
      *       [0] => Array(
      *           [id] => 11365
      *           [name] => CiviCRM
      *           [groups] => special
      *       ))
      * Re-map to mcGroupings[grouping_id][group_name] = 1;
      */
     $mcGroupings = array();
     foreach (empty($requestData['merges']['GROUPINGS']) ? array() : $requestData['merges']['GROUPINGS'] as $grouping) {
         foreach (explode(', ', $grouping['groups']) as $group) {
             $mcGroupings[$grouping['id']][$group] = 1;
         }
     }
     $groups = CRM_Mailchimp_Utils::getGroupsToSync(array(), $listID, $membership_only = FALSE);
     CRM_Mailchimp_Utils::checkDebug('Middle- CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $groups ', $groups);
     CRM_Mailchimp_Utils::checkDebug('Middle- CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $mcGroupings ', $mcGroupings);
     foreach ($groups as $groupID => $details) {
         if ($groupID != $membershipGroupID && $details['is_mc_update_grouping']) {
             // This is a group we allow updates for.
             if (empty($mcGroupings[$details['grouping_id']][$details['group_name']])) {
                 $groupContactRemoves[$groupID][] = $contactID;
             } else {
                 $groupContactAdditions[$groupID][] = $contactID;
             }
         }
     }
     // Add contacts to groups, if anything to do.
     foreach ($groupContactAdditions as $groupID => $contactIDs) {
         CRM_Contact_BAO_GroupContact::addContactsToGroup($contactIDs, $groupID, 'Admin', 'Added');
     }
     // Remove contacts from groups, if anything to do.
     foreach ($groupContactRemoves as $groupID => $contactIDs) {
         CRM_Contact_BAO_GroupContact::removeContactsFromGroup($contactIDs, $groupID, 'Admin', 'Removed');
     }
     CRM_Mailchimp_Utils::checkDebug('End - CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $groupContactRemoves ', $groupContactRemoves);
     CRM_Mailchimp_Utils::checkDebug('End - CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $groupContactAdditions ', $groupContactAdditions);
     CRM_Mailchimp_Utils::checkDebug('End - CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $contactID= ', $contactID);
     CRM_Mailchimp_Utils::checkDebug('End - CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $requestData= ', $requestData);
     CRM_Mailchimp_Utils::checkDebug('End - CRM_Mailchimp_Page_WebHook manageCiviCRMGroupSubcription $requestType= ', $action);
 }
 /**
  * Validate and process the request.
  *
  * This is separated from the run() method for testing purposes.
  *
  * This method serves as a router to other methods named after the type of
  * webhook we're called with.
  *
  * Methods may return data for mailchimp, or may throw RuntimeException
  * objects, the error code of which will be used for the response.
  * So you can throw a `RuntimeException("Invalid webhook configuration", 500);`
  * to tell mailchimp the webhook failed, but you can equally throw a 
  * `RuntimeException("soft fail", 200)` which will not tell Mailchimp there
  * was any problem. Mailchimp retries if there was a problem.
  *
  * If an exception is thrown, it is logged. @todo where?
  *
  * @return array with two values: $response_code, $response_object.
  */
 public function processRequest($expected_key, $key, $request_data)
 {
     // Check CMS's permission for (presumably) anonymous users.
     if (CRM_Core_Config::singleton()->userPermissionClass->isModulePermissionSupported() && !CRM_Mailchimp_Permission::check('allow webhook posts')) {
         throw new RuntimeException("Missing allow webhook posts permission.", 500);
     }
     // Check the 2 keys exist and match.
     if (!$key || !$expected_key || $key != $expected_key) {
         throw new RuntimeException("Invalid security key.", 500);
     }
     if (empty($request_data['data']['list_id']) || empty($request_data['type']) || !in_array($request_data['type'], ['subscribe', 'unsubscribe', 'profile', 'upemail', 'cleaned'])) {
         // We are not programmed to respond to this type of request.
         // But maybe Mailchimp introduced something new, so we'll just say OK.
         throw new RuntimeException("Missing or invalid data in request: " . json_encode($request_data), 200);
     }
     $method = $request_data['type'];
     // Check list config at Mailchimp.
     $list_id = $request_data['data']['list_id'];
     $api = CRM_Mailchimp_Utils::getMailchimpApi();
     $result = $api->get("/lists/{$list_id}/webhooks")->data->webhooks;
     $url = CRM_Mailchimp_Utils::getWebhookUrl();
     // Find our webhook and check for a particularly silly configuration.
     foreach ($result as $webhook) {
         if ($webhook->url == $url) {
             if ($webhook->sources->api) {
                 // To continue could cause a nasty loop.
                 throw new RuntimeException("The list '{$list_id}' is not configured correctly at Mailchimp. It has the 'API' source set so processing this using the API could cause a loop.", 500);
             }
         }
     }
     // Disable post hooks. We're updating *from* Mailchimp so we don't want
     // to fire anything *at* Mailchimp.
     CRM_Mailchimp_Utils::$post_hook_enabled = FALSE;
     // Pretty much all the request methods use these:
     $this->sync = new CRM_Mailchimp_Sync($request_data['data']['list_id']);
     $this->request_data = $request_data['data'];
     // Call the appropriate handler method.
     CRM_Mailchimp_Utils::checkDebug("Webhook: {$method} with request data: " . json_encode($request_data));
     $this->{$method}();
     // re-set the post hooks.
     CRM_Mailchimp_Utils::$post_hook_enabled = TRUE;
     // Return OK response.
     return [200, NULL];
 }
 static function subscribeOrUnsubsribeToMailchimpList($groupDetails, $contactID, $action)
 {
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $groupDetails', $groupDetails);
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $contactID', $contactID);
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $action', $action);
     if (empty($groupDetails) || empty($contactID) || empty($action)) {
         return NULL;
     }
     // We need to get contact's email before subscribing in Mailchimp
     $contactParams = array('version' => 3, 'id' => $contactID);
     $contactResult = civicrm_api('Contact', 'get', $contactParams);
     // This is the primary email address of the contact
     $email = $contactResult['values'][$contactID]['email'];
     if (empty($email)) {
         // Its possible to have contacts in CiviCRM without email address
         // and add to group offline
         return;
     }
     // Optional merges for the email (FNAME, LNAME)
     $merge = array('FNAME' => $contactResult['values'][$contactID]['first_name'], 'LNAME' => $contactResult['values'][$contactID]['last_name']);
     $listID = $groupDetails['list_id'];
     $grouping_id = $groupDetails['grouping_id'];
     $group_id = $groupDetails['group_id'];
     if (!empty($grouping_id) and !empty($group_id)) {
         $merge_groups[$grouping_id] = array('id' => $groupDetails['grouping_id'], 'groups' => array());
         $merge_groups[$grouping_id]['groups'][] = CRM_Mailchimp_Utils::getMCGroupName($listID, $grouping_id, $group_id);
         // remove the significant array indexes, in case Mailchimp cares.
         $merge['groupings'] = array_values($merge_groups);
     }
     // Send Mailchimp Lists API Call.
     $list = new Mailchimp_Lists(CRM_Mailchimp_Utils::mailchimp());
     switch ($action) {
         case "subscribe":
             // http://apidocs.mailchimp.com/api/2.0/lists/subscribe.php
             try {
                 $result = $list->subscribe($listID, array('email' => $email), $merge, $email_type = 'html', $double_optin = FALSE, $update_existing = FALSE, $replace_interests = TRUE, $send_welcome = FALSE);
             } catch (Exception $e) {
                 // Don't display if the error is that we're already subscribed.
                 $message = $e->getMessage();
                 if ($message !== $email . ' is already subscribed to the list.') {
                     CRM_Core_Session::setStatus($message);
                 }
             }
             break;
         case "unsubscribe":
             // https://apidocs.mailchimp.com/api/2.0/lists/unsubscribe.php
             try {
                 $result = $list->unsubscribe($listID, array('email' => $email), $delete_member = false, $send_goodbye = false, $send_notify = false);
             } catch (Exception $e) {
                 CRM_Core_Session::setStatus($e->getMessage());
             }
             break;
     }
     CRM_Mailchimp_Utils::checkDebug('End-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $groupDetails', $groupDetails);
     CRM_Mailchimp_Utils::checkDebug('End-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $contactID', $contactID);
     CRM_Mailchimp_Utils::checkDebug('End-CRM_Mailchimp_Utils subscribeOrUnsubsribeToMailchimpList $action', $action);
 }
 /**
  * Removes from the temporary tables those records that do not need processing.
  */
 static function syncIdentical()
 {
     //CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Form_Sync syncIdentical $count= ', $count);
     // Delete records have the same hash - these do not need an update.
     // count
     $dao = CRM_Core_DAO::executeQuery("SELECT COUNT(c.email) co FROM tmp_mailchimp_push_m m\n      INNER JOIN tmp_mailchimp_push_c c ON m.email = c.email AND m.hash = c.hash;");
     $dao->fetch();
     $count = $dao->co;
     CRM_Core_DAO::executeQuery("DELETE m, c\n       FROM tmp_mailchimp_push_m m\n       INNER JOIN tmp_mailchimp_push_c c ON m.email = c.email AND m.hash = c.hash;");
     CRM_Mailchimp_Utils::checkDebug('End-CRM_Mailchimp_Form_Sync syncIdentical $count= ', $count);
     return $count;
 }
 /**
  * Update the push stats setting.
  */
 public static function updatePushStats($updates)
 {
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Form_Sync updatePushStats $updates= ', $updates);
     $stats = CRM_Core_BAO_Setting::getItem(CRM_Mailchimp_Form_Setting::MC_SETTING_GROUP, 'push_stats');
     foreach ($updates as $listId => $settings) {
         if ($listId == 'dry_run') {
             continue;
         }
         foreach ($settings as $key => $val) {
             $stats[$listId][$key] = $val;
         }
     }
     CRM_Core_BAO_Setting::setItem($stats, CRM_Mailchimp_Form_Setting::MC_SETTING_GROUP, 'push_stats');
 }
 /**
  * Get Mailchimp group ID group name
  */
 public static function getMailchimpGroupIdFromName($listID, $groupName)
 {
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Utils getMailchimpGroupIdFromName $listID', $listID);
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Utils getMailchimpGroupIdFromName $groupName', $groupName);
     if (empty($listID) || empty($groupName)) {
         return NULL;
     }
     $mcLists = new Mailchimp_Lists(CRM_Mailchimp_Utils::mailchimp());
     try {
         $results = $mcLists->interestGroupings($listID);
     } catch (Exception $e) {
         return NULL;
     }
     foreach ($results as $grouping) {
         foreach ($grouping['groups'] as $group) {
             if ($group['name'] == $groupName) {
                 CRM_Mailchimp_Utils::checkDebug('End-CRM_Mailchimp_Utils getMailchimpGroupIdFromName= ', $group['id']);
                 return $group['id'];
             }
         }
     }
 }
 /**
  * Remove anything that's the same.
  */
 public static function syncPullIgnoreInSync(CRM_Queue_TaskContext $ctx, $listID)
 {
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Form_Pull syncPullIgnoreInSync $listID= ', $listID);
     $sync = new CRM_Mailchimp_Sync($listID);
     $stats[$listID]['in_sync'] = $sync->removeInSync('pull');
     CRM_Mailchimp_Utils::checkDebug('Start-CRM_Mailchimp_Form_Pull syncPullIgnoreInSync in-sync= ', $stats[$listID]['in_sync']);
     static::updatePullStats($stats);
     return CRM_Queue_Task::TASK_SUCCESS;
 }
 /**
  * "Pull" sync.
  *
  * Updates CiviCRM from Mailchimp using the tmp_mailchimp_push_[cm] tables.
  *
  * It is assumed that collections (in 'pull' mode) and `removeInSync` have
  * already run.
  *
  * 1. Loop the full tmp_mailchimp_push_m table:
  *
  *    1. Contact identified by collectMailchimp()?
  *       - Yes: update name if different.
  *       - No:  Create or find-and-update the contact.
  *
  *    2. Check for changes in groups; record what needs to be changed for a
  *       batch update.
  *
  * 2. Batch add/remove contacts from groups.
  *
  * @return array With the following keys:
  *
  * - created: was in MC not CiviCRM so a new contact was created
  * - joined : email matched existing contact that was joined to the membership
  *            group.
  * - in_sync: was in MC and on membership group already.
  * - removed: was not in MC but was on membership group, so removed from
  *            membership group.
  * - updated: No. in_sync or joined contacts that were updated.
  *
  * The initials of these categories c, j, i, r correspond to this diagram:
  *
  *     From Mailchimp: ************
  *     From CiviCRM  :         ********
  *     Result        : ccccjjjjiiiirrrr
  *
  * Of the contacts known in both systems (j, i) we also record how many were
  * updated (e.g. name, interests).
  *
  * Work in pass 1:
  *
  * - create|find
  * - join
  * - update names
  * - update interests
  *
  * Work in pass 2:
  *
  * - remove
  */
 public function updateCiviFromMailchimp()
 {
     // Ensure posthooks don't trigger while we make GroupContact changes.
     CRM_Mailchimp_Utils::$post_hook_enabled = FALSE;
     // This is a functional variable, not a stats. one
     $changes = ['removals' => [], 'additions' => []];
     CRM_Mailchimp_Utils::checkDebug("updateCiviFromMailchimp for group #{$this->membership_group_id}");
     // Stats.
     $stats = ['created' => 0, 'joined' => 0, 'in_sync' => 0, 'removed' => 0, 'updated' => 0];
     // all Mailchimp table *except* titanics: where the contact matches multiple
     // contacts in CiviCRM.
     $dao = CRM_Core_DAO::executeQuery("SELECT m.*,\n      c.contact_id c_contact_id,\n      c.interests c_interests, c.first_name c_first_name, c.last_name c_last_name\n      FROM tmp_mailchimp_push_m m\n      LEFT JOIN tmp_mailchimp_push_c c ON m.cid_guess = c.contact_id\n      WHERE m.cid_guess IS NOT NULL\n      ;");
     // Create lookup hash to map Mailchimp Interest Ids to CiviCRM Groups.
     $interest_to_group_id = [];
     foreach ($this->interest_group_details as $group_id => $details) {
         $interest_to_group_id[$details['interest_id']] = $group_id;
     }
     // Loop records found at Mailchimp, creating/finding contacts in CiviCRM.
     while ($dao->fetch()) {
         $existing_contact_changed = FALSE;
         if (!empty($dao->cid_guess)) {
             // Matched existing contact: result: joined or in_sync
             $contact_id = $dao->cid_guess;
             if ($dao->c_contact_id) {
                 // Contact is already in the membership group.
                 $stats['in_sync']++;
             } else {
                 // Contact needs joining to the membership group.
                 $stats['joined']++;
                 if (!$this->dry_run) {
                     // Live.
                     $changes['additions'][$this->membership_group_id][] = $contact_id;
                 } else {
                     // Dry Run.
                     CRM_Mailchimp_Utils::checkDebug("Would add existing contact to membership group. Email: {$dao->email} Contact Id: {$dao->cid_guess}");
                 }
             }
             // Update the first name and last name of the contacts we know
             // if needed and making sure we don't overwrite
             // something with nothing. See issue #188.
             $edits = static::updateCiviFromMailchimpContactLogic(['first_name' => $dao->first_name, 'last_name' => $dao->last_name], ['first_name' => $dao->c_first_name, 'last_name' => $dao->c_last_name]);
             if ($edits) {
                 if (!$this->dry_run) {
                     // There are changes to be made so make them now.
                     civicrm_api3('Contact', 'create', ['id' => $contact_id] + $edits);
                 } else {
                     // Dry run.
                     CRM_Mailchimp_Utils::checkDebug("Would update CiviCRM contact {$dao->cid_guess} " . (empty($edits['first_name']) ? '' : "First name from {$dao->c_first_name} to {$dao->first_name} ") . (empty($edits['last_name']) ? '' : "Last name from {$dao->c_last_name} to {$dao->last_name} "));
                 }
                 $existing_contact_changed = TRUE;
             }
         } else {
             // Contact does not exist, create a new one.
             if (!$this->dry_run) {
                 // Live:
                 $result = civicrm_api3('Contact', 'create', ['contact_type' => 'Individual', 'first_name' => $dao->first_name, 'last_name' => $dao->last_name, 'email' => $dao->email, 'sequential' => 1]);
                 $contact_id = $result['values'][0]['id'];
                 $changes['additions'][$this->membership_group_id][] = $contact_id;
             } else {
                 // Dry Run:
                 CRM_Mailchimp_Utils::checkDebug("Would create new contact with email: {$dao->email}, name: {$dao->first_name} {$dao->last_name}");
                 $contact_id = 'dry-run';
             }
             $stats['created']++;
         }
         // Do interests need updating?
         if ($dao->c_interests && $dao->c_interests == $dao->interests) {
             // Nothing to change.
         } else {
             // Unpack the interests reported by MC
             $mc_interests = unserialize($dao->interests);
             if ($dao->c_interests) {
                 // Existing contact.
                 $existing_contact_changed = TRUE;
                 $civi_interests = unserialize($dao->c_interests);
             } else {
                 // Newly created contact is not in any interest groups.
                 $civi_interests = [];
             }
             // Discover what needs changing to bring CiviCRM inline with Mailchimp.
             foreach ($mc_interests as $interest => $member_has_interest) {
                 if ($member_has_interest && empty($civi_interests[$interest])) {
                     // Member is interested in something, but CiviCRM does not know yet.
                     if (!$this->dry_run) {
                         $changes['additions'][$interest_to_group_id[$interest]][] = $contact_id;
                     } else {
                         CRM_Mailchimp_Utils::checkDebug("Would add CiviCRM contact {$dao->cid_guess} to interest group " . $interest_to_group_id[$interest]);
                     }
                 } elseif (!$member_has_interest && !empty($civi_interests[$interest])) {
                     // Member is not interested in something, but CiviCRM thinks it is.
                     if (!$this->dry_run) {
                         $changes['removals'][$interest_to_group_id[$interest]][] = $contact_id;
                     } else {
                         CRM_Mailchimp_Utils::checkDebug("Would remove CiviCRM contact {$dao->cid_guess} from interest group " . $interest_to_group_id[$interest]);
                     }
                 }
             }
         }
         if ($existing_contact_changed) {
             $stats['updated']++;
         }
     }
     // And now, what if a contact is not in the Mailchimp list?
     // We must remove them from the membership group.
     // Accademic interest (#188): what's faster, this or a 'WHERE NOT EXISTS'
     // construct?
     $dao = CRM_Core_DAO::executeQuery("\n    SELECT c.contact_id\n      FROM tmp_mailchimp_push_c c\n      LEFT OUTER JOIN tmp_mailchimp_push_m m ON m.cid_guess = c.contact_id\n      WHERE m.email IS NULL;\n      ");
     // Collect the contact_ids that need removing from the membership group.
     while ($dao->fetch()) {
         if (!$this->dry_run) {
             $changes['removals'][$this->membership_group_id][] = $dao->contact_id;
         } else {
             CRM_Mailchimp_Utils::checkDebug("Would remove CiviCRM contact {$dao->contact_id} from membership group - no longer subscribed at Mailchimp.");
         }
         $stats['removed']++;
     }
     if (!$this->dry_run) {
         // Log group contacts which are going to be added/removed to/from CiviCRM
         CRM_Mailchimp_Utils::checkDebug('Mailchimp $changes', $changes);
         // Make the changes.
         if ($changes['additions']) {
             // We have some contacts to add into groups...
             foreach ($changes['additions'] as $groupID => $contactIDs) {
                 CRM_Contact_BAO_GroupContact::addContactsToGroup($contactIDs, $groupID, 'Admin', 'Added');
             }
         }
         if ($changes['removals']) {
             // We have some contacts to add into groups...
             foreach ($changes['removals'] as $groupID => $contactIDs) {
                 CRM_Contact_BAO_GroupContact::removeContactsFromGroup($contactIDs, $groupID, 'Admin', 'Removed');
             }
         }
     }
     // Re-enable the post hooks.
     CRM_Mailchimp_Utils::$post_hook_enabled = TRUE;
     return $stats;
 }