using System.Globalization; using System.Text.Json.Nodes; using w4c_workflows.Models.Credentials; using w4c_workflows.Models.Nodes; using w4c_workflows.Services.Credentials; using w4c_workflows.Services.Nodes.Binary; using w4c_workflows.Services.Security; namespace w4c_workflows.Services.Nodes.Executors; // Pagination half of the HTTP Request node: turns the declarative plan into // per-page cursor/page-number/next-URL steps and merges query/header/body // overrides into the next request. public sealed partial class HttpRequestNodeExecutor { // ------------------------------------------------------------------ pagination private static JsonNode? ApplyStep( HttpPaginationStep step, HttpPaginationPlan plan, JsonNode? body, Dictionary queryOverrides, Dictionary headerOverrides) { if (step.Cursor != null) return ApplyParameter(plan.CursorPlace, plan.CursorName, step.Cursor, body, queryOverrides, headerOverrides); if (step.PageNumber is int pageNumber) return ApplyParameter( plan.PagePlace, plan.PageName, pageNumber.ToString(CultureInfo.InvariantCulture), body, queryOverrides, headerOverrides); return body; } private static JsonNode? ApplyParameter( string place, string name, string value, JsonNode? body, Dictionary queryOverrides, Dictionary headerOverrides) { if (string.IsNullOrEmpty(name)) return body; switch (place) { case HttpPaginationPlan.PlaceHeader: headerOverrides[name] = value; return body; case HttpPaginationPlan.PlaceBody: if (body is JsonObject obj) obj[name] = value; else if (body == null) body = new JsonObject { [name] = value }; return body; default: queryOverrides[name] = value; return body; } } /// Merges pagination overrides into the base URL query (same-name keys are replaced). private static Uri ApplyQuery(Uri baseUri, IReadOnlyDictionary overrides) { if (overrides.Count == 0) return baseUri; var pairs = new List>(); if (!string.IsNullOrEmpty(baseUri.Query)) { foreach (var part in baseUri.Query.TrimStart('?') .Split('&', StringSplitOptions.RemoveEmptyEntries)) { var separator = part.IndexOf('='); var key = separator < 0 ? part : part[..separator]; var value = separator < 0 ? string.Empty : part[(separator + 1)..]; pairs.Add(new KeyValuePair( Uri.UnescapeDataString(key), Uri.UnescapeDataString(value))); } } foreach (var (name, value) in overrides) { pairs.RemoveAll(pair => string.Equals(pair.Key, name, StringComparison.Ordinal)); pairs.Add(new KeyValuePair(name, value)); } var query = string.Join('&', pairs.Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")); return new UriBuilder(baseUri) { Query = query }.Uri; } /// Resolves a pagination next URL (absolute or relative) and keeps it HTTP(S). private static bool TryResolveNext(string nextUrl, Uri current, out Uri next) { next = null!; try { // A leading-slash path parses as an absolute "file" URI on Unix, so // only an explicitly http(s) absolute URL bypasses relative resolution. if (Uri.TryCreate(nextUrl, UriKind.Absolute, out var absolute) && absolute.Scheme is "http" or "https") { next = absolute; } else { next = new Uri(current, nextUrl); } return next.Scheme is "http" or "https"; } catch (UriFormatException) { return false; } } /// Accumulates a page under the item cap; returns true when the cap is reached. private static bool AddItems(List items, IReadOnlyList pageItems, int maxItems) { if (maxItems <= 0) { items.AddRange(pageItems); return false; } var remaining = maxItems - items.Count; if (remaining <= 0) return true; if (pageItems.Count > remaining) { items.AddRange(pageItems.Take(remaining)); return true; } items.AddRange(pageItems); return items.Count >= maxItems; } }