18using System.Collections.Generic;
24using System.Text.Json;
25using System.Threading.Tasks;
27using System.Windows.Forms;
28using System.Xml.Serialization;
31using YamlDotNet.Serialization;
36 internal class RESTHelper
38 internal int StatusCode {
get;
set; }
42 internal int RetryAfterSeconds {
get;
set; }
44 private RESTClient _restClient;
49 private HttpClient _httpClient {
get;
set; } =
new HttpClient(
new HttpClientHandler { UseCookies =
false })
54 Timeout = System.Threading.Timeout.InfiniteTimeSpan
57 private readonly
string _browserExtensionApiBase;
61 internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
65 internal TimeSpan HttpTimeout {
get;
set; } = DefaultTimeout;
66 internal static Dictionary<ApiEndpoint, string> Endpoints {
get;
set; } =
new Dictionary<ApiEndpoint, string>
68 [ApiEndpoint.Back] =
"back",
69 [ApiEndpoint.CaptureVisibleTab] =
"capture-visible-tab",
70 [ApiEndpoint.CastDesktop] =
"cast-desktop",
71 [ApiEndpoint.CastTab] =
"cast-tab",
72 [ApiEndpoint.CheckNetworkIdle] =
"check-network-idle",
73 [ApiEndpoint.CheckStatus] =
"status",
74 [ApiEndpoint.ClearReferrer] =
"clear-referrer",
75 [ApiEndpoint.CloseBrowser] =
"close-browser",
76 [ApiEndpoint.CloseTab] =
"close-tab",
77 [ApiEndpoint.DeleteStorage] =
"delete-storage",
78 [ApiEndpoint.DragAndDrop] =
"drag-and-drop",
79 [ApiEndpoint.Evaluate] =
"evaluate",
80 [ApiEndpoint.EvaluateAll] =
"evaluate-all",
81 [ApiEndpoint.EvaluatePersistent] =
"evaluate-persistent",
82 [ApiEndpoint.EvaluateAllPersistent] =
"evaluate-all-persistent",
83 [ApiEndpoint.ExecuteJavaScript] =
"execute-javascript",
84 [ApiEndpoint.Fetch] =
"fetch",
85 [ApiEndpoint.FillInAppend] =
"fill-in-append",
86 [ApiEndpoint.FillInInsert] =
"fill-in-insert",
87 [ApiEndpoint.FillInOverwrite] =
"fill-in-overwrite",
88 [ApiEndpoint.FireChangeEvent] =
"fire-change-event",
89 [ApiEndpoint.Focus] =
"focus",
90 [ApiEndpoint.Forward] =
"forward",
91 [ApiEndpoint.FullScreen] =
"fullscreen",
92 [ApiEndpoint.GetAttribute] =
"get-attribute",
93 [ApiEndpoint.GetBoundingClientRect] =
"get-bounding-client-rect",
94 [ApiEndpoint.ElementFromPoint] =
"element-from-point",
95 [ApiEndpoint.GetContentAndCss] =
"get-content-and-css",
96 [ApiEndpoint.GetCssAttributes] =
"get-css-attributes",
97 [ApiEndpoint.GetCurrentUrl] =
"get-current-url",
98 [ApiEndpoint.GetDocumentStamp] =
"get-document-stamp",
99 [ApiEndpoint.GetHydrationScripts] =
"get-hydration-scripts",
100 [ApiEndpoint.GetDomAttributes] =
"get-dom-attributes",
101 [ApiEndpoint.GetDomProperties] =
"get-dom-properties",
102 [ApiEndpoint.GetElementAttributeHash] =
"get-element-attribute-hash",
103 [ApiEndpoint.GetPageSource] =
"get-page-source",
104 [ApiEndpoint.GetParentNode] =
"get-parent-node",
105 [ApiEndpoint.GetReadyStatus] =
"get-ready-status",
106 [ApiEndpoint.GetShadowRoot] =
"get-shadow-root",
107 [ApiEndpoint.GetStorage] =
"get-storage",
108 [ApiEndpoint.GetLanguages] =
"get-languages",
109 [ApiEndpoint.CaptureCalls] =
"capture-calls",
110 [ApiEndpoint.GetCapturedCalls] =
"get-captured-calls",
111 [ApiEndpoint.GetUserAgent] =
"get-useragent",
112 [ApiEndpoint.GoTo] =
"goto",
113 [ApiEndpoint.GoToTab] =
"goto-tab",
114 [ApiEndpoint.GetWindowRectangle] =
"get-window-rectangle",
115 [ApiEndpoint.HideElement] =
"hide-element",
116 [ApiEndpoint.Hover] =
"hover",
117 [ApiEndpoint.InjectScript] =
"inject-script",
118 [ApiEndpoint.ClearInjectedScripts] =
"clear-injected-scripts",
119 [ApiEndpoint.IsClickable] =
"is-clickable",
120 [ApiEndpoint.IsDisplayed] =
"is-displayed",
121 [ApiEndpoint.IsEnabled] =
"is-enabled",
122 [ApiEndpoint.IsEndOfPage] =
"is-end-of-page",
123 [ApiEndpoint.IsVisibleInViewport] =
"is-visible-in-viewport",
124 [ApiEndpoint.LeftClick] =
"left-click",
125 [ApiEndpoint.LeftClickAndDownload] =
"left-click-download",
126 [ApiEndpoint.LeftDoubleClick] =
"left-double-click",
127 [ApiEndpoint.Maximize] =
"maximize",
128 [ApiEndpoint.MiddleClick] =
"middle-click",
129 [ApiEndpoint.Minimize] =
"minimize",
130 [ApiEndpoint.MoveTo] =
"move-to",
131 [ApiEndpoint.NewTab] =
"new-tab",
132 [ApiEndpoint.NextTab] =
"next-tab",
133 [ApiEndpoint.NextWindow] =
"next-window",
134 [ApiEndpoint.Normal] =
"normal",
135 [ApiEndpoint.OverrideReferrer] =
"override-referrer",
136 [ApiEndpoint.PageDown] =
"page-down",
137 [ApiEndpoint.PageEnd] =
"page-end",
138 [ApiEndpoint.PageTop] =
"page-top",
139 [ApiEndpoint.PageUp] =
"page-up",
140 [ApiEndpoint.PressModifierKey] =
"press-modifier-key",
141 [ApiEndpoint.PreviousTab] =
"previous-tab",
142 [ApiEndpoint.PreviousWindow] =
"previous-window",
143 [ApiEndpoint.QuerySelector] =
"query-selector",
144 [ApiEndpoint.QuerySelectors] =
"query-selectors",
145 [ApiEndpoint.QueryPersistentSelector] =
"query-persistent-selector",
146 [ApiEndpoint.QueryPersistentSelectors] =
"query-persistent-selectors",
147 [ApiEndpoint.Refresh] =
"refresh",
148 [ApiEndpoint.ReleaseModifierKey] =
"release-modifier-key",
149 [ApiEndpoint.Restore] =
"restore",
150 [ApiEndpoint.RightClick] =
"right-click",
151 [ApiEndpoint.ScrollElement] =
"scroll-element",
152 [ApiEndpoint.ScrollIntoView] =
"scroll-into-view",
153 [ApiEndpoint.ScrollWindowByHorizontal] =
"scroll-window",
154 [ApiEndpoint.ScrollWindowByVertical] =
"scroll-window",
155 [ApiEndpoint.ScrollWindow] =
"scroll-window",
156 [ApiEndpoint.SelectClick] =
"select-click",
157 [ApiEndpoint.SendKey] =
"send-key",
158 [ApiEndpoint.SendString] =
"send-string",
159 [ApiEndpoint.SetAttribute] =
"set-attribute",
160 [ApiEndpoint.SetDownloadFilename] =
"set-download-filename",
161 [ApiEndpoint.SetGoogleReferrer] =
"google-referrer",
162 [ApiEndpoint.SetRange] =
"set-range",
163 [ApiEndpoint.SetStorage] =
"set-storage",
164 [ApiEndpoint.SetUserAgent] =
"set-useragent",
165 [ApiEndpoint.SetValueFromElement] =
"set-value-from-element",
166 [ApiEndpoint.StopCasting] =
"stop-casting",
167 [ApiEndpoint.SwitchToDefaultContent] =
"switch-to-default-content",
168 [ApiEndpoint.SwitchToElement] =
"switch-to-element",
169 [ApiEndpoint.SwitchToFrame] =
"switch-to-frame",
170 [ApiEndpoint.SwitchToShadowRoot] =
"switch-to-shadow-root",
171 [ApiEndpoint.TabCount] =
"tab-count",
172 [ApiEndpoint.TopBrowser] =
"top-browser",
173 [ApiEndpoint.MoveToPoint] =
"move-to-point",
174 [ApiEndpoint.ClickPoint] =
"click-point",
175 [ApiEndpoint.TrustedLeftClick] =
"trusted-left-click",
176 [ApiEndpoint.TrustedMiddleClick] =
"trusted-middle-click",
177 [ApiEndpoint.Upload] =
"upload",
178 [ApiEndpoint.WindowInnerHeight] =
"window-inner-height",
179 [ApiEndpoint.WindowInnerWidth] =
"window-inner-width",
180 [ApiEndpoint.WindowOuterHeight] =
"window-outer-height",
181 [ApiEndpoint.WindowOuterWidth] =
"window-outer-width",
182 [ApiEndpoint.WindowPageOffsetX] =
"window-page-xoffset",
183 [ApiEndpoint.WindowPageOffsetY] =
"window-page-yoffset",
184 [ApiEndpoint.WindowScreenLeft] =
"window-screen-left",
185 [ApiEndpoint.WindowScreenTop] =
"window-screen-top",
186 [ApiEndpoint.OpenWindow] =
"open-window",
187 [ApiEndpoint.GoToWindow] =
"goto-window",
188 [ApiEndpoint.CloseWindow] =
"close-window",
189 [ApiEndpoint.GetCurrentWindow] =
"get-current-window",
191 [ApiEndpoint.GetWorkflow] =
"get-workflow",
192 [ApiEndpoint.GetSettings] =
"get-settings",
193 [ApiEndpoint.GetGpalSettings] =
"get-gpal-settings",
194 [ApiEndpoint.GetBrowserSettings] =
"get-browser-settings",
195 #endregion GPAL STUDIO
198 public RESTHelper(
string browserExtensionApiBase, RESTClient client, TimeSpan? httpTimeout =
null)
200 if (browserExtensionApiBase ==
null)
202 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
203 "Browser extension API base cannot be null.",
204 null, GPALObjectType.None);
206 _browserExtensionApiBase = browserExtensionApiBase;
211 HttpTimeout = httpTimeout ?? DefaultTimeout;
212 _restClient = client;
215 public string GetEndpointUrl(ApiEndpoint endpoint)
217 return $
"{_browserExtensionApiBase}{Endpoints[endpoint]}";
220 internal async Task<string> Call(ApiEndpoint endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
225 string url = GetEndpointUrl(endpoint);
227 string result = await InternalCall(url, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
232 internal async Task<string> Call(
string endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
237 string url =
true == endpoint?.StartsWith(
"http", StringComparison.OrdinalIgnoreCase)
239 : $
"{_browserExtensionApiBase}{endpoint}";
241 string result = await InternalCall(url, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
246 internal async Task<T> CallAndDeserialize<T>(ApiEndpoint endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
249 string jsonResponse = await Call(endpoint, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
250 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{endpoint}] result: [{jsonResponse}]");
252 return string.IsNullOrEmpty(jsonResponse)
254 : JsonConvert.DeserializeObject<T>(jsonResponse);
263 private static string DecodeJsonString(
string response)
265 string retVal = response;
267 while (
false ==
string.IsNullOrEmpty(retVal)
268 &&
true == retVal.StartsWith(
"\"") &&
true == retVal.EndsWith(
"\""))
272 string peeled = JsonConvert.DeserializeObject<
string>(retVal);
275 if (peeled == retVal)
280 catch (Newtonsoft.Json.JsonException)
289 internal async Task<T> CallAndDeserialize<T>(
string endpoint,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
291 string jsonResponse = await Call(endpoint, parameters, httpMethod, contentEncoding).ConfigureAwait(
false);
296 jsonResponse = DecodeJsonString(jsonResponse);
298 if (typeof(GPALElement) == typeof(T) || typeof(List<GPALElement>) == typeof(T))
300 var settings =
new JsonSerializerSettings
302 Converters =
new List<JsonConverter> {
new GPALElementConverter(),
new PointConverter(),
new SizeConverter() },
303 NullValueHandling = NullValueHandling.Ignore
307 return JsonConvert.DeserializeObject<T>(jsonResponse, settings);
311 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Exception calling [{endpoint}]",
null, GPALObjectType.None, ex);
314 else if(
string.IsNullOrEmpty(jsonResponse))
316 if (endpoint.Equals(Endpoints[ApiEndpoint.IsEndOfPage]) &&
317 typeof(T) == typeof(
bool))
319 return (T)(object)
true;
325 if (typeof(T) == typeof(
bool))
326 if (
"OK".Equals(jsonResponse) ||
"true".Equals(jsonResponse))
327 return (T)(object)
true;
328 else if (
"NOTOK".Equals(jsonResponse) ||
"false".Equals(jsonResponse))
329 return (T)(object)
false;
332 var msg = jsonResponse.Replace(
"\\",
"");
333 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"[{msg}]", parameters, GPALObjectType.Other);
334 return (T)(object)
false;
339 if (endpoint.Equals(Endpoints[ApiEndpoint.GetCssAttributes]) &&
"[]".Equals(jsonResponse?.Trim()))
344 if (
null != jsonResponse)
345 return JsonConvert.DeserializeObject<T>(jsonResponse);
349 catch (Newtonsoft.Json.JsonException ex)
354 if (typeof(T) == typeof(
string))
355 return (T)(object)jsonResponse;
357 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
358 $
"Could not deserialize [{jsonResponse?.Substring(0, Math.Min(80, jsonResponse?.Length ?? 0))}] as [{typeof(T).Name}]",
359 null, GPALObjectType.None, ex);
364 private static T HandleUnsupportedContentType<T>(
string contentType,
string response)
366 GPAL.PublishSimpleEvent(
367 GPALEventType.WARNING,
368 $
"Unsupported format: [{contentType}]. Unable to cast to [{typeof(T)}]. Returning raw response.",
370 GPALObjectType.Other);
376 private static readonly
string[] contentHeaders =
378 "content-type",
"content-length",
"content-encoding",
"content-language",
"content-location",
379 "content-md5",
"content-range",
"content-disposition",
"expires",
"last-modified",
"allow"
382 private static bool IsContentHeader(
string name)
384 return Array.Exists(contentHeaders, content => content.Equals(name, StringComparison.OrdinalIgnoreCase));
396 private static string AppendQueryString(
string route,
object parameters)
400 if (parameters is
string written)
402 query = written.TrimStart(
'?',
'&');
406 IEnumerable<KeyValuePair<string, object>> pairs;
408 if (parameters is IDictionary<string, object> dictionary)
411 pairs = parameters.GetType().GetProperties()
412 .Select(property =>
new KeyValuePair<string, object>(property.Name, property.GetValue(parameters)));
414 query =
string.Join(
"&", pairs
415 .Where(pair =>
null != pair.Value)
416 .Select(pair => $
"{HttpUtility.UrlEncode(pair.Key)}={HttpUtility.UrlEncode(pair.Value.ToString())}"));
419 string retVal = route;
421 if (
false ==
string.IsNullOrEmpty(query))
422 retVal =
true == route.Contains(
"?") ? $
"{route}&{query}" : $
"{route}?{query}";
432 private static int ReadRetryAfter(HttpResponseMessage response)
438 if (
null != response?.Headers?.RetryAfter)
440 if (
true == response.Headers.RetryAfter.Delta.HasValue)
441 seconds = (int)Math.Ceiling(response.Headers.RetryAfter.Delta.Value.TotalSeconds);
442 else if (
true == response.Headers.RetryAfter.Date.HasValue)
443 seconds = (int)Math.Ceiling((response.Headers.RetryAfter.Date.Value - DateTimeOffset.UtcNow).TotalSeconds);
446 return 0 < seconds ? seconds : 0;
449 internal async Task<string> InternalCall(
string route,
object parameters =
null,
string httpMethod =
"GET", ContentEncoding contentEncoding = ContentEncoding.Json)
451 string upperMethod = httpMethod.ToUpperInvariant();
457 if ((
"GET" == upperMethod ||
"HEAD" == upperMethod) &&
null != parameters)
459 route = AppendQueryString(route, parameters);
464 async Task<string> TryOnce()
466 using var cts =
new System.Threading.CancellationTokenSource(HttpTimeout);
467 HttpResponseMessage httpResponse =
new HttpResponseMessage();
468 string responseBody =
"";
472 if (httpMethod.ToUpper() ==
"POST" &&
473 route.EndsWith(
"upload", StringComparison.OrdinalIgnoreCase))
475 var paramsDict = parameters as IDictionary<string, object>
476 ?? parameters?.GetType().GetProperties()
477 .ToDictionary(p => p.Name, p => p.GetValue(parameters));
479 paramsDict.TryGetValue(
"uploadPath", out var uploadPathObj);
480 string uploadPath = uploadPathObj?.ToString();
482 paramsDict.TryGetValue(
"elementId", out var elementIdObj);
483 string elementId = elementIdObj?.ToString();
485 if (!File.Exists(uploadPath))
487 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
488 $
"upload file not found: [{uploadPath}]",
null, GPALObjectType.None);
492 byte[] fileBytes = File.ReadAllBytes(uploadPath);
493 string base64Data = Convert.ToBase64String(fileBytes);
494 string mimeType = MimeMapping.GetMimeMapping(uploadPath);
504 string jsonBody = System.Text.Json.JsonSerializer.Serialize(payload);
505 HttpContent content =
new StringContent(jsonBody, Encoding.UTF8,
"application/json");
507 var attachRequest =
new HttpRequestMessage(HttpMethod.Post, route);
508 attachRequest.Content = content;
510 httpResponse = await _httpClient.SendAsync(attachRequest, cts.Token).ConfigureAwait(
false);
514 var request =
new HttpRequestMessage(
new HttpMethod(httpMethod.ToUpperInvariant()), route);
516 request.Headers.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(
"application/json"));
518 foreach (var header
in _restClient._headers)
523 if (
false == IsContentHeader(header.Key) && !request.Headers.Contains(header.Key))
525 request.Headers.TryAddWithoutValidation(header.Key, header.Value);
529 if (parameters !=
null)
535 string contentType = RESTClient.GetContentType(contentEncoding);
537 foreach (var header
in _restClient._headers)
538 if (
true ==
"Content-Type".Equals(header.Key, StringComparison.OrdinalIgnoreCase)
539 &&
false ==
string.IsNullOrWhiteSpace(header.Value))
540 contentType = header.Value.Split(
';')[0].Trim();
542 object parms = parameters;
544 string parmsEncodingType = StringFormatDetector.DetectFormat(parameters.ToString());
546 if (
"none".Equals(parmsEncodingType) &&
"application/json".Equals(contentType))
548 parms = System.Text.Json.JsonSerializer.Serialize(parameters);
550 else if (
"none".Equals(parmsEncodingType) &&
"application/x-www-form-urlencoded".Equals(contentType))
552 if (!(parameters is
string))
553 parms = HttpUtility.UrlEncode(parameters.ToString());
556 string bodyText = parms?.ToString() ??
"";
557 request.Content =
new StringContent(bodyText, System.Text.Encoding.UTF8, contentType);
560 httpResponse = await _httpClient.SendAsync(request, cts.Token).ConfigureAwait(
false);
563 StatusCode = (int)httpResponse.StatusCode;
564 RetryAfterSeconds = ReadRetryAfter(httpResponse);
566 responseBody = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(
false);
571 const int maxRawLogChars = 200;
572 string rawForLog =
string.IsNullOrEmpty(responseBody) || responseBody.Length <= maxRawLogChars
574 : responseBody.Substring(0, maxRawLogChars) + $
"...[truncated, {responseBody.Length} chars total]";
576 GPAL.PublishSimpleEvent(GPALEventType.DEEPDEBUG, $
"raw result[{route}][{rawForLog}]",
this, GPALObjectType.RestHelper);
578 string upperMethod = httpMethod.ToUpperInvariant();
579 if (
string.IsNullOrEmpty(responseBody) &&
580 (upperMethod ==
"OPTIONS" || upperMethod ==
"HEAD"))
582 var headerDict =
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
584 foreach (var h
in httpResponse.Headers)
585 headerDict[h.Key] =
string.Join(
", ", h.Value);
587 foreach (var h
in httpResponse.Content.Headers)
588 if (!headerDict.ContainsKey(h.Key))
589 headerDict[h.Key] =
string.Join(
", ", h.Value);
591 responseBody = System.Text.Json.JsonSerializer.Serialize(headerDict,
592 new JsonSerializerOptions { WriteIndented =
true });
595 if (
false == httpResponse.IsSuccessStatusCode)
597 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
598 $
"Request [{route}] failed with status code: [{StatusCode}] [{httpResponse.ReasonPhrase}]",
599 responseBody, GPALObjectType.None);
606 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
607 $
"Error in TryOnce for [{httpMethod}] [{route}]",
null, GPALObjectType.RestClient, ex);
615 return await TryOnce().ConfigureAwait(
false);
620 catch (Exception first) when (
false == IsCancellation(first))
624 await Task.Delay(2_000).ConfigureAwait(
false);
625 return await TryOnce().ConfigureAwait(
false);
629 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
630 $
"Unable to communicate with: [{route}]",
631 null, GPALObjectType.None, ex);
636 if (
true == IsOurBrowser(route))
637 throw new GPALException($
"{GPAL.MyMethodName()}: The browser at [{_browserExtensionApiBase}] did not answer [{route}] on either attempt, so there is nothing to drive");
642 catch (Exception timedOut)
644 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
645 $
"Nothing answered [{route}] in [{TimeoutText()}], so the call was given up on",
646 null, GPALObjectType.None, timedOut);
648 if (
true == IsOurBrowser(route))
649 throw new GPALException($
"{GPAL.MyMethodName()}: The browser at [{_browserExtensionApiBase}] did not answer [{route}] in [{TimeoutText()}], so there is nothing to drive");
662 private static bool IsCancellation(Exception ex)
664 bool retVal = ex is OperationCanceledException || ex is TimeoutException;
666 if (
false == retVal && ex is HttpRequestException &&
null != ex.InnerException)
667 retVal = ex.InnerException is OperationCanceledException || ex.InnerException is TimeoutException;
676 private string TimeoutText()
678 return System.Threading.Timeout.InfiniteTimeSpan == HttpTimeout
680 : $
"{HttpTimeout.TotalSeconds:0.#} seconds";
693 private bool IsOurBrowser(
string route)
697 if (
false ==
string.IsNullOrEmpty(_browserExtensionApiBase)
698 &&
true == route.StartsWith(_browserExtensionApiBase, StringComparison.OrdinalIgnoreCase)
699 &&
true == Uri.TryCreate(_browserExtensionApiBase, UriKind.Absolute, out Uri baseUri)
700 &&
true == baseUri.IsLoopback)
702 string called = route.Substring(_browserExtensionApiBase.Length).Split(
'?')[0].Trim(
'/');
704 retVal = Endpoints.Values.Any(known => known.Trim(
'/').Equals(called, StringComparison.OrdinalIgnoreCase));
710 public async Task<bool> CheckAPIStatus()
712 string route = $
"{_browserExtensionApiBase}{Endpoints[ApiEndpoint.CheckStatus]}";
713 using (HttpClient client =
new HttpClient())
717 HttpResponseMessage response = await client.GetAsync(route).ConfigureAwait(
false);
718 string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(
false);
720 return responseBody.Equals(
"Server is running");
729 public static class StringFormatDetector
731 public static string DetectFormat(
string input)
733 if (
string.IsNullOrWhiteSpace(input))
737 var trimmed = input.Trim();
744 if (IsUrlEncoded(trimmed))
750 internal static bool IsJson(
string input)
752 if ((input.StartsWith(
"{") && input.EndsWith(
"}")) ||
753 (input.StartsWith(
"[") && input.EndsWith(
"]")))
757 using var doc = JsonDocument.Parse(input);
768 private static bool IsUrlEncoded(
string input)
772 var decoded = HttpUtility.UrlDecode(input);
773 var reencoded = HttpUtility.UrlEncode(decoded);
774 return string.Equals(input, reencoded, StringComparison.Ordinal);