Forge
csharpdeeb25a2
1using System.Net;
2
3namespace CascadeIDE.Features.WebAiPortal.Application;
4
5/// <summary>Нормализация строки адреса для встроенного WebView: без схемы подставляется <c>https://</c>, для loopback — <c>http://</c>.</summary>
6public static class WebAiPortalUrlNormalize
7{
8 /// <summary>Построить абсолютный URI для веб-навигации и каноническую строку для поля URL.</summary>
9 public static bool TryBuildNavigationUri(string? raw, out Uri? uri, out string normalizedText)
10 {
11 uri = null;
12 normalizedText = "";
13
14 var s = raw?.Trim() ?? "";
15 if (s.Length == 0)
16 {
17 uri = new Uri("about:blank");
18 normalizedText = "about:blank";
19 return true;
20 }
21
22 // «//host/path» без схемы .NET часто превращает в file: — обрабатываем до общего TryCreate.
23 if (s.StartsWith("//", StringComparison.Ordinal))
24 {
25 var schemeRelative = "https:" + s;
26 return Uri.TryCreate(schemeRelative, UriKind.Absolute, out var abs) &&
27 IsHttpFamily(abs.Scheme) &&
28 AssignResult(abs, out uri, out normalizedText);
29 }
30
31 if (TryParseBrowserAbsoluteUri(s, out var absolute))
32 return AssignResult(absolute, out uri, out normalizedText);
33
34 string[] prefixes = PreferHttpScheme(s)
35 ? ["http://", "https://"]
36 : ["https://", "http://"];
37 foreach (var prefix in prefixes)
38 {
39 var candidate = prefix + TrimLeadingAuthoritySlashes(s);
40 if (Uri.TryCreate(candidate, UriKind.Absolute, out absolute) && IsHttpFamily(absolute.Scheme))
41 return AssignResult(absolute, out uri, out normalizedText);
42 }
43
44 return false;
45 }
46
47 /// <summary>
48 /// Абсолютный URI, который можно открыть во встроенном браузере без доработки строки.
49 /// «localhost:5000» и прочие записи без «://» здесь не считаются валидными (пойдут в ветку автопрефикса).
50 /// </summary>
51 private static bool TryParseBrowserAbsoluteUri(string s, out Uri absolute)
52 {
53 if (!Uri.TryCreate(s, UriKind.Absolute, out absolute!))
54 return false;
55
56 switch (absolute.Scheme)
57 {
58 case "http":
59 case "https":
60 case "about":
61 case "file":
62 return true;
63 default:
64 // vscode:, mailto:, ftp:… — только если пользователь явно указал схему.
65 return s.Contains("://", StringComparison.Ordinal);
66 }
67 }
68
69 private static bool AssignResult(Uri absolute, out Uri? uri, out string normalizedText)
70 {
71 uri = absolute;
72 normalizedText = absolute.AbsoluteUri;
73 return true;
74 }
75
76 private static bool IsHttpFamily(string scheme) =>
77 string.Equals(scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
78 string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
79
80 /// <summary>Loopback без схемы — чаще <c>http</c> (локальные dev-серверы).</summary>
81 private static bool PreferHttpScheme(string userInputTrimmed)
82 {
83 var authority = TrimLeadingAuthoritySlashes(userInputTrimmed);
84 var slashIdx = authority.IndexOf('/');
85 if (slashIdx >= 0)
86 authority = authority[..slashIdx];
87
88 if (authority.StartsWith("[::1]", StringComparison.OrdinalIgnoreCase))
89 return true;
90 if (authority.Equals("localhost", StringComparison.OrdinalIgnoreCase))
91 return true;
92 if (authority.StartsWith("localhost:", StringComparison.OrdinalIgnoreCase))
93 return true;
94
95 var lastColon = authority.LastIndexOf(':');
96 if (lastColon > 0)
97 {
98 var hostPart = authority[..lastColon];
99 if (IPAddress.TryParse(hostPart, out var addr) && IPAddress.IsLoopback(addr))
100 return true;
101 }
102 else if (IPAddress.TryParse(authority, out var addr) && IPAddress.IsLoopback(addr))
103 return true;
104
105 return false;
106 }
107
108 private static string TrimLeadingAuthoritySlashes(string s) =>
109 s.StartsWith('/') ? s.TrimStart('/') : s;
110}
111
View only · write via MCP/CIDE