// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using Microsoft.ClearScript.Util; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; namespace Microsoft.ClearScript { ///

/// Represents a document loader. /// public abstract class DocumentLoader { // ReSharper disable EmptyConstructor /// /// Initializes a new instance. /// protected DocumentLoader() { // the help file builder (SHFB) insists on an empty constructor here } // ReSharper restore EmptyConstructor /// /// Gets the default document loader. /// public static DocumentLoader Default => DefaultImpl.Instance; /// /// Gets or sets the maximum size of the document loader's cache. /// /// /// This property specifies the maximum number of documents to be cached by the document /// loader. For the default document loader, its initial value is 1024. /// /// public virtual uint MaxCacheSize { get => 0; set => throw new NotSupportedException("Loader does not support caching"); } /// /// Loads a document. /// /// Document access settings for the operation. /// An optional structure containing meta-information for the requesting document. /// A string specifying the document to be loaded. /// An optional category for the requested document. /// An optional context callback for the requested document. /// A instance that represents the loaded document. /// /// A loaded document must have an absolute URI. Once a /// load operation has completed successfully, subsequent requests that resolve to the same /// URI are expected to return the same reference, although loaders /// are not required to manage document caches of unlimited size. /// public virtual Document LoadDocument(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback) { MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier"); try { return LoadDocumentAsync(settings, sourceInfo, specifier, category, contextCallback).Result; } catch (AggregateException exception) { exception = exception.Flatten(); if (exception.InnerExceptions.Count == 1) { throw new FileLoadException(null, specifier, exception.InnerExceptions[0]); } throw new FileLoadException(null, specifier, exception); } } /// /// Loads a document asynchronously. /// /// Document access settings for the operation. /// An optional structure containing meta-information for the requesting document. /// A string specifying the document to be loaded. /// An optional category for the requested document. /// An optional context callback for the requested document. /// A task that represents the asynchronous operation. Upon completion, the task's result is a instance that represents the loaded document. /// /// A loaded document must have an absolute URI. Once a /// load operation has completed successfully, subsequent requests that resolve to the same /// URI are expected to return the same reference, although loaders /// are not required to manage document caches of unlimited size. /// public abstract Task LoadDocumentAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback); /// /// Discards all cached documents. /// public virtual void DiscardCachedDocuments() { } #region Nested type: DefaultImpl private sealed class DefaultImpl : DocumentLoader { public static readonly DefaultImpl Instance = new DefaultImpl(); private static readonly IReadOnlyCollection relativePrefixes = new List { "." + Path.DirectorySeparatorChar, "." + Path.AltDirectorySeparatorChar, ".." + Path.DirectorySeparatorChar, ".." + Path.AltDirectorySeparatorChar, }; private readonly List cache = new List(); private DefaultImpl() { MaxCacheSize = 1024; } private static async Task> GetCandidateUrisAsync(DocumentSettings settings, DocumentInfo? sourceInfo, Uri uri) { var candidateUris = new List(); if (string.IsNullOrWhiteSpace(settings.FileNameExtensions)) { candidateUris.Add(uri); } else { foreach (var testUri in ApplyFileNameExtensions(sourceInfo, uri, settings.FileNameExtensions)) { if (await IsCandidateUriAsync(settings, testUri).ConfigureAwait(false)) { candidateUris.Add(testUri); } } } return candidateUris; } private static async Task> GetCandidateUrisAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier) { var candidateUris = new List(); var rawUris = GetRawUris(settings, sourceInfo, specifier).Distinct(); if (!string.IsNullOrWhiteSpace(settings.FileNameExtensions)) { rawUris = rawUris.SelectMany(uri => ApplyFileNameExtensions(sourceInfo, uri, settings.FileNameExtensions)); } foreach (var testUri in rawUris) { if (await IsCandidateUriAsync(settings, testUri).ConfigureAwait(false)) { candidateUris.Add(testUri); } } return candidateUris; } private static IEnumerable GetRawUris(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier) { Uri baseUri; Uri uri; if (sourceInfo.HasValue && SpecifierMayBeRelative(settings, specifier)) { baseUri = GetBaseUri(sourceInfo.Value); if ((baseUri != null) && Uri.TryCreate(baseUri, specifier, out uri)) { yield return uri; } } var searchPath = settings.SearchPath; if (!string.IsNullOrWhiteSpace(searchPath)) { foreach (var url in searchPath.SplitSearchPath()) { if (Uri.TryCreate(url, UriKind.Absolute, out baseUri) && TryCombineSearchUri(baseUri, specifier, out uri)) { yield return uri; } } } if (MiscHelpers.Try(out var path, () => Path.Combine(Directory.GetCurrentDirectory(), specifier)) && Uri.TryCreate(path, UriKind.Absolute, out uri)) { yield return uri; } if (MiscHelpers.Try(out path, () => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, specifier)) && Uri.TryCreate(path, UriKind.Absolute, out uri)) { yield return uri; } using (var process = Process.GetCurrentProcess()) { var module = process.MainModule; if ((module != null) && Uri.TryCreate(module.FileName, UriKind.Absolute, out baseUri) && Uri.TryCreate(baseUri, specifier, out uri)) { yield return uri; } } } private static IEnumerable ApplyFileNameExtensions(DocumentInfo? sourceInfo, Uri uri, string fileNameExtensions) { yield return uri; var builder = new UriBuilder(uri); var path = builder.Path; if (!string.IsNullOrEmpty(Path.GetFileName(path)) && !Path.HasExtension(path)) { string sourceFileNameExtension = null; if (sourceInfo.HasValue) { sourceFileNameExtension = Path.GetExtension((sourceInfo.Value.Uri != null) ? new UriBuilder(sourceInfo.Value.Uri).Path : sourceInfo.Value.Name); if (!string.IsNullOrEmpty(sourceFileNameExtension)) { builder.Path = Path.ChangeExtension(path, sourceFileNameExtension); yield return builder.Uri; } } foreach (var fileNameExtension in fileNameExtensions.SplitSearchPath()) { var testFileNameExtension = fileNameExtension.StartsWith(".", StringComparison.Ordinal) ? fileNameExtension : "." + fileNameExtension; if (!testFileNameExtension.Equals(sourceFileNameExtension, StringComparison.OrdinalIgnoreCase)) { builder.Path = Path.ChangeExtension(path, testFileNameExtension); yield return builder.Uri; } } } } private static bool SpecifierMayBeRelative(DocumentSettings settings, string specifier) { return !settings.AccessFlags.HasFlag(DocumentAccessFlags.EnforceRelativePrefix) || relativePrefixes.Any(specifier.StartsWith); } private static Uri GetBaseUri(DocumentInfo sourceInfo) { var sourceUri = sourceInfo.Uri; if ((sourceUri == null) && !Uri.TryCreate(sourceInfo.Name, UriKind.RelativeOrAbsolute, out sourceUri)) { return null; } if (!sourceUri.IsAbsoluteUri) { return null; } return sourceUri; } private static bool TryCombineSearchUri(Uri searchUri, string specifier, out Uri uri) { var searchUrl = searchUri.AbsoluteUri; if (!searchUrl.EndsWith("/", StringComparison.Ordinal)) { searchUri = new Uri(searchUrl + "/"); } return Uri.TryCreate(searchUri, specifier, out uri); } private static async Task IsCandidateUriAsync(DocumentSettings settings, Uri uri) { return uri.IsFile ? settings.AccessFlags.HasFlag(DocumentAccessFlags.EnableFileLoading) && File.Exists(uri.LocalPath) : settings.AccessFlags.HasFlag(DocumentAccessFlags.EnableWebLoading) && await WebDocumentExistsAsync(uri).ConfigureAwait(false); } private static async Task WebDocumentExistsAsync(Uri uri) { using (var client = new HttpClient()) { using (var request = new HttpRequestMessage(HttpMethod.Head, uri)) { using (var response = await client.SendAsync(request).ConfigureAwait(false)) { return response.IsSuccessStatusCode; } } } } private async Task LoadDocumentAsync(DocumentSettings settings, Uri uri, DocumentCategory category, DocumentContextCallback contextCallback) { var cachedDocument = GetCachedDocument(uri); if (cachedDocument != null) { return cachedDocument; } string contents; var flags = settings.AccessFlags; if (uri.IsFile) { if (!flags.HasFlag(DocumentAccessFlags.EnableFileLoading)) { throw new UnauthorizedAccessException("The script engine is not configured for loading documents from the file system"); } using (var reader = new StreamReader(uri.LocalPath)) { contents = await reader.ReadToEndAsync().ConfigureAwait(false); } } else { if (!flags.HasFlag(DocumentAccessFlags.EnableWebLoading)) { throw new UnauthorizedAccessException("The script engine is not configured for downloading documents from the Web"); } using (var client = new WebClient()) { contents = await client.DownloadStringTaskAsync(uri).ConfigureAwait(false); } } var documentInfo = new DocumentInfo(uri) { Category = category, ContextCallback = contextCallback }; var callback = settings.LoadCallback; callback?.Invoke(ref documentInfo); return CacheDocument(new StringDocument(documentInfo, contents)); } private Document GetCachedDocument(Uri uri) { lock (cache) { for (var index = 0; index < cache.Count; index++) { var cachedDocument = cache[index]; if (cachedDocument.Info.Uri == uri) { cache.RemoveAt(index); cache.Insert(0, cachedDocument); return cachedDocument; } } return null; } } private Document CacheDocument(Document document) { lock (cache) { var cachedDocument = cache.FirstOrDefault(testDocument => testDocument.Info.Uri == document.Info.Uri); if (cachedDocument != null) { Debug.Assert(cachedDocument.Contents.ReadToEnd().SequenceEqual(document.Contents.ReadToEnd())); return cachedDocument; } var maxCacheSize = Math.Max(16, Convert.ToInt32(Math.Min(MaxCacheSize, int.MaxValue))); while (cache.Count >= maxCacheSize) { cache.RemoveAt(cache.Count - 1); } cache.Insert(0, document); return document; } } #region DocumentLoader overrides public override uint MaxCacheSize { get; set; } public override async Task LoadDocumentAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback) { MiscHelpers.VerifyNonNullArgument(settings, nameof(settings)); MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier"); if ((settings.AccessFlags & DocumentAccessFlags.EnableAllLoading) == DocumentAccessFlags.None) { throw new UnauthorizedAccessException("The script engine is not configured for loading documents"); } if (category == null) { category = sourceInfo.HasValue ? sourceInfo.Value.Category : DocumentCategory.Script; } List candidateUris; if (Uri.TryCreate(specifier, UriKind.RelativeOrAbsolute, out var uri) && uri.IsAbsoluteUri) { candidateUris = await GetCandidateUrisAsync(settings, sourceInfo, uri).ConfigureAwait(false); } else { candidateUris = await GetCandidateUrisAsync(settings, sourceInfo, specifier).ConfigureAwait(false); } if (candidateUris.Count < 1) { throw new FileNotFoundException(null, specifier); } if (candidateUris.Count == 1) { return await LoadDocumentAsync(settings, candidateUris[0], category, contextCallback).ConfigureAwait(false); } var exceptions = new List(candidateUris.Count); foreach (var candidateUri in candidateUris) { var task = LoadDocumentAsync(settings, candidateUri, category, contextCallback); try { return await task.ConfigureAwait(false); } catch (Exception exception) { if ((task.Exception != null) && task.Exception.InnerExceptions.Count == 1) { Debug.Assert(ReferenceEquals(task.Exception.InnerExceptions[0], exception)); exceptions.Add(exception); } else { exceptions.Add(task.Exception); } } } if (exceptions.Count < 1) { MiscHelpers.AssertUnreachable(); throw new FileNotFoundException(null, specifier); } if (exceptions.Count == 1) { MiscHelpers.AssertUnreachable(); throw new FileLoadException(exceptions[0].Message, specifier, exceptions[0]); } throw new AggregateException(exceptions).Flatten(); } public override void DiscardCachedDocuments() { lock (cache) { cache.Clear(); } base.DiscardCachedDocuments(); } #endregion } #endregion } }