/** * 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; }