using System.Text; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Nop.Core; using Nop.Plugin.Payments.PayPalCommerce.Services.Api; using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Authentication; using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Models; using Nop.Plugin.Payments.PayPalCommerce.Services.Api.Onboarding; namespace Nop.Plugin.Payments.PayPalCommerce.Services; /// /// Represents the HTTP client to request PayPal API /// public class PayPalCommerceHttpClient { #region Fields private readonly HttpClient _httpClient; private static Dictionary _accessTokens = new(); #endregion #region Ctor public PayPalCommerceHttpClient(HttpClient httpClient) { _httpClient = httpClient; } #endregion #region Utilities /// /// Get access token /// /// Plugin settings /// /// A task that represents the asynchronous operation /// The task result contains the access token /// private async Task GetAccessTokenAsync(PayPalCommerceSettings settings) { if (!PayPalCommerceServiceManager.IsConfigured(settings)) throw new NopException("Plugin is not configured"); //no need to request a token if there is already a cached one and it has not expired (lifetime is about 9 hours) if (!_accessTokens.TryGetValue(settings.ClientId, out var accessToken) || string.IsNullOrEmpty(accessToken?.Token) || accessToken.IsExpired) { //get new access token accessToken = await RequestAsync(new() { ClientId = settings.ClientId, Secret = settings.SecretKey, GrantType = "client_credentials" }, settings); _accessTokens[settings.ClientId] = accessToken; } return accessToken.Token; } #endregion #region Methods /// /// Request remote service /// /// Request type /// Response type /// Request /// Plugin settings /// /// A task that represents the asynchronous operation /// The task result contains the response details /// public async Task RequestAsync(TRequest request, PayPalCommerceSettings settings) where TRequest : IApiRequest where TResponse : IApiResponse { //prepare request body, content is always JSON except for access token requests var requestString = JsonConvert.SerializeObject(request, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); var requestContent = request is GetAccessTokenRequest accessTokenRequest ? new FormUrlEncodedContent(PayPalCommerceServiceManager.ObjectToDictionary(accessTokenRequest)) : (ByteArrayContent)new StringContent(requestString, Encoding.Default, MimeTypes.ApplicationJson); //URL depends on environment var baseUrl = settings.UseSandbox ? PayPalCommerceDefaults.ServiceUrl.Sandbox : PayPalCommerceDefaults.ServiceUrl.Live; var requestMessage = new HttpRequestMessage(new HttpMethod(request.Method), new Uri(new Uri(baseUrl), request.Path)) { Content = requestContent }; //set timeout try { var timeout = TimeSpan.FromSeconds(settings.RequestTimeout ?? PayPalCommerceDefaults.RequestTimeout); if (_httpClient.Timeout != timeout) _httpClient.Timeout = timeout; } catch { } //add authorization and some custom headers var authorization = request switch { IAuthorizedRequest => $"Bearer {await GetAccessTokenAsync(settings)}", GetCredentialsRequest credentialsRequest => $"Bearer {credentialsRequest.AccessToken}", GetAccessTokenRequest tokenRequest => $"Basic {Convert.ToBase64String(Encoding.Default.GetBytes($"{tokenRequest.ClientId}:{tokenRequest.Secret}"))}", _ => null }; if (!string.IsNullOrEmpty(authorization)) requestMessage.Headers.Add(HeaderNames.Authorization, authorization); requestMessage.Headers.Add(HeaderNames.UserAgent, PayPalCommerceDefaults.UserAgent); requestMessage.Headers.Add(HeaderNames.Accept, MimeTypes.ApplicationJson); requestMessage.Headers.Add(PayPalCommerceDefaults.PartnerHeader.Name, PayPalCommerceDefaults.PartnerHeader.Value); requestMessage.Headers.Add("PayPal-Request-Id", Guid.NewGuid().ToString()); requestMessage.Headers.Add("Prefer", "return=representation"); //execute the request and get a result var httpResponse = await _httpClient.SendAsync(requestMessage); var responseString = await httpResponse.Content.ReadAsStringAsync(); //successful request processing if (httpResponse.IsSuccessStatusCode) { if (typeof(TResponse) == typeof(EmptyResponse)) return default; return JsonConvert.DeserializeObject(responseString ?? string.Empty) ?? default; } //failed request processing var error = $"Failed request ({httpResponse.StatusCode})"; var identityErrorResponse = JsonConvert.DeserializeObject(responseString ?? string.Empty); if (!string.IsNullOrEmpty(identityErrorResponse?.Error)) { var description = !string.IsNullOrEmpty(identityErrorResponse.ErrorDescription) ? identityErrorResponse.ErrorDescription : identityErrorResponse.Error; error += $": {description}"; } var errorResponse = JsonConvert.DeserializeObject(responseString ?? string.Empty); if (!string.IsNullOrEmpty(errorResponse?.Name)) { error += $": {(!string.IsNullOrEmpty(errorResponse.Message) ? errorResponse.Message : errorResponse.Name)}"; error += $"{Environment.NewLine}{JsonConvert.SerializeObject(errorResponse, Formatting.Indented)}"; } throw new NopException("Failed request", new NopException(error)); } #endregion }