$method, 'timeout' => 12, 'headers' => [ 'Authorization' => 'Basic ' . base64_encode( 'chargeforms:' . $api_key ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- HTTP Basic auth encoding, not obfuscation; Mailchimp accepts any non-empty username. 'Content-Type' => 'application/json', ], ]; if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( "https://{$dc}.api.mailchimp.com/3.0{$path}", $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code >= 400 ) { // Mailchimp's error payload always has a human-readable "detail" // (e.g. "This account is set to only accept subscribed members // through opt-in forms") — surfacing that instead of a bare // status code is what makes Api_Log actually useful for // diagnosing WHY a subscribe failed. $message = is_array( $data ) && ! empty( $data['detail'] ) ? $data['detail'] : sprintf( /* translators: %d: HTTP status code */ __( 'Mailchimp returned an unexpected error (HTTP %d).', 'chargeforms' ), $code ); return new \WP_Error( 'chargeforms_mailchimp_api_error', $message, [ 'status' => $code, 'body' => $data ] ); } return is_array( $data ) ? $data : []; } /** Connection test — used by the "Test Connection" button on Global Settings > Integrations and to gate whether the per-form list dropdown even attempts to load. */ public static function ping( $api_key ) { $result = self::request( $api_key, 'GET', '/ping' ); return is_wp_error( $result ) ? $result : true; } /** @return array|\WP_Error */ public static function get_lists( $api_key ) { $result = self::request( $api_key, 'GET', '/lists?count=100&fields=lists.id,lists.name' ); if ( is_wp_error( $result ) ) { return $result; } $lists = []; foreach ( $result['lists'] ?? [] as $list ) { $lists[] = [ 'id' => $list['id'] ?? '', 'name' => $list['name'] ?? '' ]; } return $lists; } /** * Creates the member if new, updates them if they already exist — * idempotent by design (PUT to the subscriber_hash resource), so a * resubmission or an autosaved draft that later converts never creates * a duplicate contact. `status_if_new` only takes effect on first * creation; resubmitting never silently downgrades an existing member * who has since unsubscribed (Mailchimp rejects mismatched `status` * changes made this way on purpose — a compliance safeguard this * client deliberately doesn't try to work around). */ public static function upsert_member( $api_key, $list_id, $email, array $merge_fields, $status_if_new ) { $body = [ 'email_address' => $email, 'status_if_new' => $status_if_new, ]; if ( ! empty( $merge_fields ) ) { $body['merge_fields'] = $merge_fields; } return self::request( $api_key, 'PUT', '/lists/' . rawurlencode( $list_id ) . '/members/' . self::subscriber_hash( $email ), $body ); } /** * Separate endpoint, not part of upsert_member()'s body — Mailchimp's * member-upsert PUT does not reliably apply a `tags` array to an * EXISTING member (only ever on first creation in some API versions), * so tags are always applied via this dedicated call to work * correctly on both new and returning subscribers alike. */ public static function set_tags( $api_key, $list_id, $email, array $tag_names ) { if ( empty( $tag_names ) ) { return true; } $tags = array_map( function ( $name ) { return [ 'name' => $name, 'status' => 'active' ]; }, $tag_names ); $result = self::request( $api_key, 'POST', '/lists/' . rawurlencode( $list_id ) . '/members/' . self::subscriber_hash( $email ) . '/tags', [ 'tags' => $tags ] ); return is_wp_error( $result ) ? $result : true; } }