PageRenderTime 17ms CodeModel.GetById 2ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 1ms

/InternetStandards.oEmbed/oEmbedClient.cs

https://bitbucket.org/jkporter/internetstandards.oembed
C# | 267 lines | 238 code | 29 blank | 0 comment | 21 complexity | 92e75102210149d5e0c2e6061142053a MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.Globalization;
  4using System.IO;
  5using System.Linq;
  6using System.Net.Http;
  7using System.Threading;
  8using System.Threading.Tasks;
  9using System.Xml;
 10using System.Xml.Linq;
 11using System.Xml.Serialization;
 12using System.Xml.XPath;
 13using AngleSharp;
 14using AngleSharp.Dom;
 15using AngleSharp.Dom.Html;
 16using AngleSharp.Extensions;
 17using AngleSharp.Network;
 18using AngleSharp.Parser.Html;
 19using InternetStandards.oEmbed.Network;
 20using InternetStandards.WHATWG.Url;
 21using Newtonsoft.Json;
 22using Newtonsoft.Json.Linq;
 23using HttpMethod = System.Net.Http.HttpMethod;
 24
 25namespace InternetStandards.oEmbed
 26{
 27    public class oEmbedClient
 28    {
 29        private readonly HttpClient _httpClient;
 30
 31        public oEmbedClient(HttpClient httpClient)
 32        {
 33            _httpClient = httpClient;
 34        }
 35
 36        public oEmbedClient() : this(new HttpClient())
 37        { }
 38
 39        public Task<T> MakeRequest<T>(string endpointUrl, double? maxWidth = null, double? maxHeight = null,
 40            Dictionary<string, string> additionalArguments = null, CancellationToken cancellationToken = default)
 41            where T : oEmbedResponse
 42        {
 43            return MakeRequest<T>(endpointUrl, null, maxWidth, maxHeight, additionalArguments: additionalArguments,
 44                cancellationToken: cancellationToken);
 45        }
 46
 47        public async Task<T> MakeRequest<T>(string endpointUrl, string url = null,
 48            double? maxWidth = null,
 49            double? maxHeight = null, string format = null, Dictionary<string, string> additionalArguments = null,
 50            CancellationToken cancellationToken = default) where T : oEmbedResponse
 51        {
 52            List<(string name, string value)> arguments;
 53
 54            var queryDelimterIndex = endpointUrl.IndexOf("?", StringComparison.InvariantCulture);
 55            if (queryDelimterIndex != -1)
 56            {
 57                var query = endpointUrl.Substring(queryDelimterIndex).Remove(0, 1);
 58                endpointUrl = endpointUrl.Substring(0, queryDelimterIndex + 1);
 59                arguments = new List<(string name, string value)>(application_x_www_form_urlencoded.Parse(query));
 60            }
 61            else
 62            {
 63                arguments = new List<(string name, string value)>();
 64            }
 65
 66            void AddArgument(string name, string value)
 67            {
 68                if (value == null)
 69                    return;
 70
 71                var index = arguments.FindIndex(tuple => tuple.name == name);
 72                if (index == -1)
 73                {
 74                    arguments.Add((name, value));
 75                }
 76                else
 77                {
 78                    arguments[index] = (name, value);
 79                }
 80            }
 81
 82            AddArgument("url", url);
 83            AddArgument("maxwidth", maxWidth?.ToString(CultureInfo.InvariantCulture));
 84            AddArgument("maxheight", maxHeight?.ToString(CultureInfo.InvariantCulture));
 85            AddArgument("format", format);
 86
 87            if (additionalArguments != null)
 88                arguments.AddRange(additionalArguments.Select(argument => (argument.Key, argument.Value)));
 89
 90            if (arguments.Count > 0)
 91            {
 92                if (!endpointUrl.EndsWith("?")) endpointUrl += "?";
 93                endpointUrl += application_x_www_form_urlencoded.Serializer(arguments);
 94            }
 95
 96            var httpRequest = new HttpRequestMessage(HttpMethod.Get, endpointUrl);
 97            httpRequest.Headers.Accept.ParseAdd("application/json;q=0.003");
 98            httpRequest.Headers.Accept.ParseAdd("text/xml;q=0.002");
 99            httpRequest.Headers.Accept.ParseAdd("application/xml;q=0.001");
100
101            using (var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false))
102            {
103                using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
104                {
105                    Type GetoEmbedResponseType(string type)
106                    {
107                        switch (type)
108                        {
109                            case "photo":
110                                return typeof(PhotoTypeoEmbedResponse);
111                            case "video":
112                                return typeof(VideoTypeoEmbedResponse);
113                            case "link":
114                                return typeof(LinkTypeoEmbedResponse);
115                            case "rich":
116                                return typeof(RichTypeoEmbedResponse);
117                            default:
118                                throw new Exception();
119                        }
120                    }
121
122                    switch (response.Content.Headers.ContentType.MediaType.ToLowerInvariant())
123                    {
124                        case "application/json":
125                            using (var streamReader = new StreamReader(responseStream))
126                            {
127                                using (var reader = new JsonTextReader(streamReader))
128                                {
129                                    var serializer = new JsonSerializer();
130                                    var oEmbedResponse = serializer.Deserialize<JObject>(reader);
131                                    return (T)oEmbedResponse.ToObject(
132                                        GetoEmbedResponseType(oEmbedResponse["type"].Value<string>()));
133                                }
134                            }
135
136                        case "text/xml":
137                        case "application/xml":
138                            var document = XDocument.Load(responseStream);
139                            using (var xmlReader = document.CreateReader())
140                            {
141                                var xmlSerializer =
142                                    new XmlSerializer(
143                                        GetoEmbedResponseType(document.XPathSelectElement("oembed/type").Value));
144                                return (T)xmlSerializer.Deserialize(xmlReader);
145                            }
146                    }
147                }
148            }
149
150            throw new Exception();
151        }
152
153        public Task<oEmbedResponse> MakeRequest(string endpointUrl, string url = null,
154            double? maxWidth = null,
155            double? maxHeight = null, string format = null, Dictionary<string, string> additionalArguments = null,
156            CancellationToken cancellationToken = default)
157        {
158            return MakeRequest<oEmbedResponse>(endpointUrl, url, maxWidth, maxHeight, format, additionalArguments,
159                cancellationToken);
160        }
161
162        public static string[] oEmbedLinkTypes = { "application/json+oembed", "text/xml+oembed" };
163
164        private IConfiguration AngleSharpConfig()
165        {
166            var defaultConfig = AngleSharp.Configuration.Default;
167            var requesters = defaultConfig.WithDefaultLoader().Services.OfType<IRequester>().ToList();
168            var requesterIndex = 0;
169            for (; requesterIndex < requesters.Count; requesterIndex++)
170            {
171                var requester = requesters[requesterIndex];
172                if (requester.SupportsProtocol("http") || requester.SupportsProtocol("https")) break;
173            }
174            requesters.Insert(requesterIndex, new HttpClientRequester(_httpClient));
175
176            return defaultConfig.WithDefaultLoader(null, requesters);
177        }
178
179        public async Task<IEnumerable<(string Url, string Type)>> Discover(string uri,
180            CancellationToken cancellationToken = default)
181        {
182            var context = BrowsingContext.New(AngleSharpConfig());
183            var request = DocumentRequest.Get(new Url(uri));
184            if (context?.Active != null)
185            {
186                request.Referer = context.Active.DocumentUri;
187            }
188
189            var loader = context.Loader;
190            var download = loader.DownloadAsync(request);
191            cancellationToken.Register(download.Cancel);
192
193            using (var response = await download.Task.ConfigureAwait(false))
194            {
195                var syntax = response.GetContentType().Equals(new MimeType("application/xhtml+xml"))
196                    ? LanguageSyntax.Xhtml
197                    : LanguageSyntax.Html;
198                return await Discover(response.Content, context, response.Address.ToString(), syntax, cancellationToken)
199                    .ConfigureAwait(false);
200            }
201        }
202
203        public Task<IEnumerable<(string Url, string Type)>> Discover(TextReader source, string baseUri = null,
204            LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
205        {
206            return Discover(baseUri, syntax,
207                async (p, b, c) => await p.ParseAsync(await source.ReadToEndAsync().ConfigureAwait(false), c)
208                    .ConfigureAwait(false), null, (s, b) => XmlReader.Create(source, s, b), cancellationToken);
209        }
210
211        public Task<IEnumerable<(string Url, string Type)>> Discover(Stream source, string baseUri = null,
212            LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
213        {
214            return Discover(source, null, baseUri, syntax, cancellationToken);
215        }
216
217        private Task<IEnumerable<(string Url, string Type)>> Discover(Stream source, IBrowsingContext browsingContext, string baseUri = null,
218            LanguageSyntax syntax = LanguageSyntax.Html, CancellationToken cancellationToken = default)
219        {
220            return Discover(baseUri, syntax, async (p, b, c) => await p.ParseAsync(source, c).ConfigureAwait(false),
221                browsingContext, (s, b) => XmlReader.Create(source, s, b), cancellationToken);
222        }
223
224        private async Task<IEnumerable<(string Url, string Type)>> Discover(string baseUri, LanguageSyntax syntax,
225            Func<HtmlParser, string, CancellationToken, Task<IDocument>> htmlParseAsync, IBrowsingContext browsingContext,
226            Func<XmlReaderSettings, string, XmlReader> createReader, CancellationToken cancellationToken = default)
227        {
228            switch (syntax)
229            {
230                case LanguageSyntax.Html:
231                    var parser = browsingContext == null
232                        ? new HtmlParser(AngleSharpConfig())
233                        : new HtmlParser(new HtmlParserOptions(), browsingContext);
234                    return Discover(await htmlParseAsync(parser, baseUri, cancellationToken).ConfigureAwait(false));
235                case LanguageSyntax.Xhtml:
236                    using (var xmlReader = createReader(new XmlReaderSettings { DtdProcessing= DtdProcessing.Ignore}, baseUri))
237                    {
238                        return Discover(XDocument.Load(xmlReader, LoadOptions.SetBaseUri), xmlReader.NameTable);
239                    }
240                default:
241                    throw new ArgumentOutOfRangeException(nameof(syntax), syntax, null);
242            }
243        }
244
245        private static IEnumerable<(string Url, string Type)> Discover(IDocument document)
246        {
247            return document.Head.QuerySelectorAll("link[rel=alternate][type]").Cast<IHtmlLinkElement>()
248                .Where(
249                    link =>
250                        oEmbedLinkTypes.Any(type =>
251                            string.Compare(type, link.Type,
252                                StringComparison.InvariantCultureIgnoreCase) == 0))
253                .Select(link => (new Url(link.BaseUrl, link.Href).ToString(), link.Type));
254        }
255
256        private static IEnumerable<(string Url, string Type)> Discover(XDocument document, XmlNameTable nameTable)
257        {
258            var namespaceManager = new XmlNamespaceManager(nameTable);
259            namespaceManager.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml");
260            return document.XPathSelectElements("/xhtml:html/xhtml:head/xhtml:link[@rel='alternate']", namespaceManager)
261                .Where(link => oEmbedLinkTypes.Any(type =>
262                    string.Compare(type, link.Attribute("type").Value, StringComparison.InvariantCultureIgnoreCase) ==
263                    0)).Select(link => (new Url(new Url(link.BaseUri), link.Attribute("href").Value).ToString(),
264                    link.Attribute("type").Value));
265        }
266    }
267}