using System.Diagnostics.CodeAnalysis; using AIStudio.Tools.Services; namespace AIStudio.Tools.PluginSystem; /// /// Recognizes the links in the metadata of plugins. /// /// /// The source URL of a plugin has to be such a link, and its support contact may be one. A plugin may /// only link to a website or to a mail address: another scheme, such as file: or javascript:, would let a /// plugin open anything on the computer of the user with a single click. Loading a plugin and showing its /// links both decide with this class, so that AI Studio never offers a link which loading would refuse. /// public static class PluginLinks { /// /// Tries to read a text as a link which a plugin may offer. /// /// The text to read. Whitespace around it does not count. /// The link, when the text is one. /// True, when the text is an absolute http, https, or mailto link, and a mailto link names a recipient. public static bool TryParse(string text, [NotNullWhen(true)] out Uri? uri) { if (Uri.TryCreate(text.Trim(), UriKind.Absolute, out uri) && IsAllowed(uri)) return true; uri = null; return false; } /// /// Checks whether a link writes a mail instead of opening a website. /// /// The link to check. /// True, when the link is a mailto link. public static bool IsMail(Uri uri) => uri.Scheme.Equals(Uri.UriSchemeMailto, StringComparison.OrdinalIgnoreCase); /// /// Checks whether a text is one of the links which the Assistant Builder writes into the plugins it /// generates. /// /// /// Every plugin has to state a support contact and a source URL, so the Assistant Builder fills in /// links to MindWork AI. They tell nothing about the plugin itself, so AI Studio treats them as not /// stated: it neither shows them nor offers a button for them. The check goes by the text alone, so /// these links count as not stated in any other plugin as well. /// /// The text to check. Whitespace around it does not count. /// True, when the text is one of these placeholders. public static bool IsPlaceholder(string text) => text.Trim() is AssistantPluginGenerationService.DEFAULT_SUPPORT_CONTACT or AssistantPluginGenerationService.DEFAULT_SOURCE_URL; /// /// Gets the text by which the user recognizes a link. /// /// The link. /// For a mailto link its recipient, without the scheme and without a subject. For a website the link as the plugin states it. public static string GetDisplayText(Uri uri) => IsMail(uri) ? Uri.UnescapeDataString(GetRecipient(uri).Trim().ToString()) : uri.OriginalString; private static bool IsAllowed(Uri uri) { // Without a recipient, the mail program would open a mail to nobody: if (IsMail(uri)) return !GetRecipient(uri).IsWhiteSpace(); return uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); } /// /// Gets the recipient of a mailto link, which is everything between the colon and the first /// question mark. /// /// The mailto link. /// The recipient as the link states it, which may be empty. private static ReadOnlySpan GetRecipient(Uri mailUri) { var text = mailUri.OriginalString.AsSpan(); var recipient = text[(text.IndexOf(':') + 1)..]; var queryStart = recipient.IndexOf('?'); return queryStart >= 0 ? recipient[..queryStart] : recipient; } }