/** * Sets a mock Mailchimp API that will pass the webhook is configured * correctly test. * * This code is used in many methods. * * @return Prophecy. */ protected function prepMockForWebhookConfig() { // Make mock API that will return a webhook with the sources.API setting // set, which is wrong. $api_prophecy = $this->prophesize('CRM_Mailchimp_Api3'); CRM_Mailchimp_Utils::setMailchimpApi($api_prophecy->reveal()); $url = CRM_Mailchimp_Utils::getWebhookUrl(); $api_prophecy->get("/lists/dummylistid/webhooks", Argument::any())->shouldBeCalled()->willReturn(json_decode('{"http_code":200,"data":{"webhooks":[{"url":"' . $url . '","sources":{"api":false}}]}}')); return $api_prophecy; }
/** * 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]; }
/** * Configure webhook with Mailchimp. * * Returns a list of messages to display to the user. * * @param string $list_id Mailchimp List Id. * @param bool $dry_run If set no changes are made. * @return array */ public static function configureList($list_id, $dry_run = FALSE) { $api = CRM_Mailchimp_Utils::getMailchimpApi(); $expected = ['url' => CRM_Mailchimp_Utils::getWebhookUrl(), 'events' => ['subscribe' => TRUE, 'unsubscribe' => TRUE, 'profile' => TRUE, 'cleaned' => TRUE, 'upemail' => TRUE, 'campaign' => FALSE], 'sources' => ['user' => TRUE, 'admin' => TRUE, 'api' => FALSE]]; $verb = $dry_run ? 'Need to change ' : 'Changed '; try { $result = $api->get("/lists/{$list_id}/webhooks"); $webhooks = empty($result->data->webhooks) ? NULL : $result->data->webhooks; //$webhooks = $api->get("/lists/$list_id/webhooks")->data->webhooks; if (empty($webhooks)) { $messages[] = ts(($dry_run ? 'Need to create' : 'Created') . ' a webhook at Mailchimp'); } else { // Existing webhook(s) - check thoroughly. if (count($webhooks) > 1) { // Unusual case, leave it alone. $messages[] = "Mailchimp list {$list_id} has more than one webhook configured. This is unusual, and so CiviCRM has not made any changes. Please ensure the webhook is set up correctly."; return $messages; } // Got a single webhook, check it looks right. $messages = []; // Correct URL? if ($webhooks[0]->url != $expected['url']) { $messages[] = ts($verb . 'webhook URL from %1 to %2', [1 => $webhooks[0]->url, 2 => $expected['url']]); } // Correct sources? foreach ($expected['sources'] as $source => $expected_value) { if ($webhooks[0]->sources->{$source} != $expected_value) { $messages[] = ts($verb . 'webhook source %1 from %2 to %3', [1 => $source, 2 => (int) $webhooks[0]->sources->{$source}, 3 => (int) $expected_value]); } } // Correct events? foreach ($expected['events'] as $event => $expected_value) { if ($webhooks[0]->events->{$event} != $expected_value) { $messages[] = ts($verb . 'webhook event %1 from %2 to %3', [1 => $event, 2 => (int) $webhooks[0]->events->{$event}, 3 => (int) $expected_value]); } } if (empty($messages)) { // All fine. return; } if (!$dry_run) { // As of May 2016, there doesn't seem to be an update method for // webhooks, so we just delete this and add another. $api->delete("/lists/{$list_id}/webhooks/" . $webhooks[0]->id); } } if (!$dry_run) { // Now create the proper one. $result = $api->post("/lists/{$list_id}/webhooks", $expected); } } catch (CRM_Mailchimp_RequestErrorException $e) { if ($e->request->method == 'GET' && $e->response->http_code == 404) { $messages[] = ts("The Mailchimp list that this once worked with has been deleted"); } else { $messages[] = ts("Problems updating or fetching from Mailchimp. Please manually check the configuration. ") . $e->getMessage(); } } catch (CRM_Mailchimp_NetworkErrorException $e) { $messages[] = ts("Problems (possibly temporary) talking to Mailchimp. ") . $e->getMessage(); } return $messages; }