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