/** * Mailchimp Get Mailchimp Lists API * * @param array $params * @return array API result descriptor * @see civicrm_api3_create_success * @see civicrm_api3_create_error * @throws API_Exception */ function civicrm_api3_mailchimp_getlists($params) { $api = CRM_Mailchimp_Utils::getMailchimpApi(); $query = ['offset' => 0, 'count' => 100, 'fields' => 'lists.id,lists.name,total_items']; $lists = []; do { $data = $api->get('/lists', $query)->data; foreach ($data->lists as $list) { $lists[$list->id] = $list->name; } $query['offset'] += 100; } while ($query['offset'] * 100 < $data->total_items); return civicrm_api3_create_success($lists); }
/** * Check that the contact's email is not a member of the test list. * * @param array $contact e.g. static::$civicrm_contact_1 */ public function assertContactNotListMember($contact) { $api = CRM_Mailchimp_Utils::getMailchimpApi(); try { $subscriber_hash = static::$civicrm_contact_1['subscriber_hash']; $result = $api->get("/lists/" . static::$test_list_id . "/members/{$contact['subscriber_hash']}", ['fields' => 'status']); } catch (CRM_Mailchimp_RequestErrorException $e) { $this->assertEquals(404, $e->response->http_code); } }
/** * Mailchimp in their wisdom changed all the Ids for interests. * * So we have to map on names and then update our stored Ids. * * Also change cronjobs. */ public function upgrade_20() { $this->ctx->log->info('Applying update to v2.0 Updating Mailchimp Interest Ids to fit their new API'); // New $api = CRM_Mailchimp_Utils::getMailchimpApi(); // Old $mcLists = new Mailchimp_Lists(CRM_Mailchimp_Utils::mailchimp()); // Use new API to get lists. Allow for 10,000 lists so we don't bother // batching. $lists = []; foreach ($api->get("/lists", ['fields' => 'lists.id,lists.name', 'count' => 10000])->data->lists as $list) { $lists[$list->id] = ['name' => $list->name]; } $queries = []; // Loop lists. foreach (array_keys($lists) as $list_id) { // Fetch Interest categories. $categories = $api->get("/lists/{$list_id}/interest-categories", ['count' => 10000, 'fields' => 'categories.id,categories.title'])->data->categories; if (!$categories) { continue; } // Old: fetch all categories (groupings) and interests (groups) in one go: $old = $mcLists->interestGroupings($list_id); // New: fetch interests for each category. foreach ($categories as $category) { // $lists[$list_id]['categories'][$category->id] = ['name' => $category->title]; // Match this category by name with the old 'groupings' $matched_old_grouping = FALSE; foreach ($old as $old_grouping) { if ($old_grouping['name'] == $category->title) { $matched_old_grouping = $old_grouping; break; } } if ($matched_old_grouping) { // Found a match. $cat_queries[] = ['list_id' => $list_id, 'old' => $matched_old_grouping['id'], 'new' => $category->id]; // Now do interests (old: groups) $interests = $api->get("/lists/{$list_id}/interest-categories/{$category->id}/interests", ['fields' => 'interests.id,interests.name', 'count' => 10000])->data->interests; foreach ($interests as $interest) { // Can we find this interest by name? $matched_old_group = FALSE; foreach ($matched_old_grouping['groups'] as $old_group) { if ($old_group['name'] == $interest->name) { $int_queries[] = ['list_id' => $list_id, 'old' => $old_group['id'], 'new' => $interest->id]; break; } } } } } } foreach ($cat_queries as $params) { CRM_Core_DAO::executeQuery('UPDATE civicrm_value_mailchimp_settings ' . 'SET mc_grouping_id = %1 ' . 'WHERE mc_list_id = %2 AND mc_grouping_id = %3;', [1 => [$params['new'], 'String'], 2 => [$params['list_id'], 'String'], 3 => [$params['old'], 'String']]); } foreach ($int_queries as $params) { CRM_Core_DAO::executeQuery('UPDATE civicrm_value_mailchimp_settings ' . 'SET mc_group_id = %1 ' . 'WHERE mc_list_id = %2 AND mc_group_id = %3;', [1 => [$params['new'], 'String'], 2 => [$params['list_id'], 'String'], 3 => [$params['old'], 'String']]); } // Now cron jobs. Delete all mailchimp ones. $result = civicrm_api3('Job', 'get', array('sequential' => 1, 'api_entity' => "mailchimp")); if ($result['count']) { // Should only be one, but just in case... foreach ($result['values'] as $old) { // Double check id exists! if (!empty($old['id'])) { civicrm_api3('Job', 'delete', ['id' => $old['id']]); } } } // Create Push Sync job. $params = array('sequential' => 1, 'name' => 'Mailchimp Push Sync', 'description' => 'Sync contacts between CiviCRM and MailChimp, assuming CiviCRM to be correct. Please understand the implications before using this.', 'run_frequency' => 'Daily', 'api_entity' => 'Mailchimp', 'api_action' => 'pushsync', 'is_active' => 0); $result = civicrm_api3('job', 'create', $params); // Create Pull Sync job. $params = array('sequential' => 1, 'name' => 'Mailchimp Pull Sync', 'description' => 'Sync contacts between CiviCRM and MailChimp, assuming Mailchimp to be correct. Please understand the implications before using this.', 'run_frequency' => 'Daily', 'api_entity' => 'Mailchimp', 'api_action' => 'pullsync', 'is_active' => 0); $result = civicrm_api3('job', 'create', $params); return TRUE; }
/** * Function to process the form * * @access public * * @return None */ public function postProcess() { // Store the submitted values in an array. $params = $this->controller->exportValues($this->_name); // Save the API Key & Save the Security Key if (CRM_Utils_Array::value('api_key', $params) || CRM_Utils_Array::value('security_key', $params)) { CRM_Core_BAO_Setting::setItem($params['api_key'], self::MC_SETTING_GROUP, 'api_key'); CRM_Core_BAO_Setting::setItem($params['security_key'], self::MC_SETTING_GROUP, 'security_key'); CRM_Core_BAO_Setting::setItem($params['enable_debugging'], self::MC_SETTING_GROUP, 'enable_debugging'); try { $mcClient = CRM_Mailchimp_Utils::getMailchimpApi(TRUE); $response = $mcClient->get('/'); if (empty($response->data->account_name)) { throw new Exception("Could not retrieve account details, although a response was received. Somthing's not right."); } } catch (Exception $e) { CRM_Core_Session::setStatus($e->getMessage()); return FALSE; } $message = "Following is the account information received from API callback:<br/>\n <table class='mailchimp-table'>\n <tr><td>Account Name:</td><td>" . htmlspecialchars($response->data->account_name) . "</td></tr>\n <tr><td>Account Email:</td><td>" . htmlspecialchars($response->data->email) . "</td></tr>\n </table>"; CRM_Core_Session::setStatus($message); } }
/** * 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]; }
/** * Get interest groupings for given ListID (cached). * * Nb. general API function used by several other helper functions. * * Returns an array like { * [category_id] => array( * 'id' => category_id, * 'name' => Category name * 'interests' => array( * [interest_id] => array( * 'id' => interest_id, * 'name' => interest name * ), * ... * ), * ... * ) * */ public static function getMCInterestGroupings($listID) { if (empty($listID)) { CRM_Mailchimp_Utils::checkDebug('CRM_Mailchimp_Utils::getMCInterestGroupings called without list id'); return NULL; } $mapper =& static::$mailchimp_interest_details; if (!array_key_exists($listID, $mapper)) { $mapper[$listID] = array(); try { // Get list name. $api = CRM_Mailchimp_Utils::getMailchimpApi(); $categories = $api->get("/lists/{$listID}/interest-categories", ['fields' => 'categories.id,categories.title', 'count' => 10000])->data->categories; } catch (CRM_Mailchimp_RequestErrorException $e) { if ($e->response->http_code == 404) { // Controlled response CRM_Core_Error::debug_log_message("Mailchimp error: List {$listID} is not found."); return NULL; } else { CRM_Core_Error::debug_log_message('Unhandled Mailchimp error: ' . $e->getMessage()); throw $e; } } catch (CRM_Mailchimp_NetworkErrorException $e) { CRM_Core_Error::debug_log_message('Unhandled Mailchimp network error: ' . $e->getMessage()); throw $e; return NULL; } // Re-map $categories from this: // id = (string [10]) `f192c59e0d` // title = (string [7]) `CiviCRM` foreach ($categories as $category) { // Need to look up interests for this category. $interests = CRM_Mailchimp_Utils::getMailchimpApi()->get("/lists/{$listID}/interest-categories/{$category->id}/interests", ['fields' => 'interests.id,interests.name', 'count' => 10000])->data->interests; $mapper[$listID][$category->id] = ['id' => $category->id, 'name' => $category->title, 'interests' => []]; foreach ($interests as $interest) { $mapper[$listID][$category->id]['interests'][$interest->id] = ['id' => $interest->id, 'name' => $interest->name]; } } } CRM_Mailchimp_Utils::checkDebug("CRM_Mailchimp_Utils::getMCInterestGroupings for list '{$listID}' returning ", $mapper[$listID]); return $mapper[$listID]; }
/** * Sync a single contact's membership and interests for this list from their * details in CiviCRM. * */ public function updateMailchimpFromCiviSingleContact($contact_id) { // Get all the groups related to this list that the contact is currently in. // We have to use this dodgy API that concatenates the titles of the groups // with a comma (making it unsplittable if a group title has a comma in it). $contact = civicrm_api3('Contact', 'getsingle', ['contact_id' => $contact_id, 'return' => ['first_name', 'last_name', 'email_id', 'email', 'group'], 'sequential' => 1]); $in_groups = CRM_Mailchimp_Utils::getGroupIds($contact['groups'], $this->group_details); $currently_a_member = in_array($this->membership_group_id, $in_groups); if (empty($contact['email'])) { // Without an email we can't do anything. return; } $subscriber_hash = md5(strtolower($contact['email'])); $api = CRM_Mailchimp_Utils::getMailchimpApi(); if (!$currently_a_member) { // They are not currently a member. // // We should ensure they are unsubscribed from Mailchimp. They might // already be, but as we have no way of telling exactly what just changed // at our end, we have to make sure. // // Nb. we don't bother updating their interests for unsubscribes. try { $result = $api->patch("/lists/{$this->list_id}/members/{$subscriber_hash}", ['status' => 'unsubscribed']); } catch (CRM_Mailchimp_RequestErrorException $e) { if ($e->response->http_code == 404) { // OK. Mailchimp didn't know about them anyway. Fine. } else { CRM_Core_Session::setStatus(ts('There was a problem trying to unsubscribe this contact at Mailchimp; any differences will remain until a CiviCRM to Mailchimp Sync is done.')); } } catch (CRM_Mailchimp_NetworkErrorException $e) { CRM_Core_Session::setStatus(ts('There was a network problem trying to unsubscribe this contact at Mailchimp; any differences will remain until a CiviCRM to Mailchimp Sync is done.')); } return; } // Now left with 'subscribe' case. // // Do this with a PUT as this allows for both updating existing and // creating new members. $data = ['status' => 'subscribed', 'email_address' => $contact['email'], 'merge_fields' => ['FNAME' => $contact['first_name'], 'LNAME' => $contact['last_name']]]; // Do interest groups. $data['interests'] = $this->getComparableInterestsFromCiviCrmGroups($contact['groups'], 'push'); if (empty($data['interests'])) { unset($data['interests']); } try { $result = $api->put("/lists/{$this->list_id}/members/{$subscriber_hash}", $data); } catch (CRM_Mailchimp_RequestErrorException $e) { CRM_Core_Session::setStatus(ts('There was a problem trying to subscribe this contact at Mailchimp:') . $e->getMessage()); } catch (CRM_Mailchimp_NetworkErrorException $e) { CRM_Core_Session::setStatus(ts('There was a network problem trying to unsubscribe this contact at Mailchimp; any differences will remain until a CiviCRM to Mailchimp Sync is done.')); } }