//****************************************************************************************************** // WebExtensions.cs - Gbtc // // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this // file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 02/17/2016 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Dynamic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Xml; using GSF.IO; using GSF.Web.Model; using Newtonsoft.Json; using RazorEngine.Templating; using HtmlHelper = System.Web.Mvc.HtmlHelper; namespace GSF.Web { /// /// Defines extension methods useful for web applications. /// public static class WebExtensions { // Nested Types // Defines a multi-part stream provider that will parse out files and form data private class PostDataStreamProvider : MultipartStreamProvider { #region [ Members ] // Fields private readonly PostData m_postData; private readonly List m_isFormData; #endregion #region [ Constructors ] public PostDataStreamProvider() { m_postData = new PostData(); m_isFormData = new List(); } #endregion #region [ Properties ] // Gets any data passed as part of the multi-part form data public PostData PostData => m_postData; #endregion #region [ Methods ] // Gets the stream where to write the body part to. This method is called when a MIME multi-part body part has been parsed public override Stream GetStream(HttpContent parent, HttpContentHeaders headers) { // For form data, Content-Disposition header is a requirement ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition; if ((object)contentDisposition == null) throw new InvalidOperationException("Expected \"Content-Disposition\" header field not found in MIME multi-part body."); // Post process this body part as form data if no file name is specified m_isFormData.Add(string.IsNullOrEmpty(contentDisposition.FileName)); return new MemoryStream(); } // Executes the post processing operation reading the non-file contents as form data public override async Task ExecutePostProcessingAsync() { for (int i = 0; i < Contents.Count; i++) { if (m_isFormData[i]) { HttpContent formContent = Contents[i]; ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition; string fieldName = UnquoteToken(contentDisposition.Name) ?? ""; // Read out any form data string fieldValue = await formContent.ReadAsStringAsync(); m_postData.FormData.Add(fieldName, fieldValue); } else { m_postData.FileData.Add(Contents[i]); } } } #endregion #region [ Static ] // Static Methods private static string UnquoteToken(string token) { if (string.IsNullOrWhiteSpace(token)) return token; if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) return token.Substring(1, token.Length - 2); return token; } #endregion } // Defines a GetMemberBinder implementation to get DynamicViewBag properties by name private class MemberBinder : GetMemberBinder { public MemberBinder(string name) : base(name, false) { } public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion) => null; } /// /// Key name for dictionary used to cache parsed . /// public const string PostDataKey = "GSF_" + nameof(PostData); // Static Fields private static readonly HashSet s_executingAssemblyResources; private static readonly HashSet s_callingAssemblyResources; private static readonly HashSet s_entryAssemblyResources; private static readonly Dictionary> s_embeddedResourceAssemblies; private static readonly string[] s_newLineDelimeters = { "\r\n", "\r", "\n" }; // Static Constructor static WebExtensions() { s_executingAssemblyResources = new HashSet(Assembly.GetExecutingAssembly().GetManifestResourceNames(), StringComparer.Ordinal); s_callingAssemblyResources = new HashSet(Assembly.GetCallingAssembly().GetManifestResourceNames(), StringComparer.Ordinal); s_entryAssemblyResources = new HashSet(Assembly.GetEntryAssembly()?.GetManifestResourceNames() ?? new[] { "" }, StringComparer.Ordinal); s_embeddedResourceAssemblies = new Dictionary>(); } // Static Methods /// /// Performs JavaScript encoding on given string. /// /// The string to be encoded. public static string JavaScriptEncode(this string text) { return HttpUtility.JavaScriptStringEncode(text.ToNonNullString()); } /// /// Performs HTML encoding on the given string. /// /// The string to be encoded. public static string HtmlEncode(this string text) { return HttpUtility.HtmlEncode(text.ToNonNullString()); } /// /// Performs URL encoding on the given string. /// /// The string to be encoded. public static string UrlEncode(this string text) { return HttpUtility.UrlEncode(text.ToNonNullString()); } /// /// Parses a JSON timestamp into a .NET . /// /// JSON timestamp. /// .NET parsed from JSON . public static DateTime ParseJsonTimestamp(this string timestamp) { DateTimeOffset dto = JsonConvert.DeserializeObject($"\"{timestamp}\""); return dto.UtcDateTime; } /// /// Corrects script alignment with a desired number of forward spaces. /// /// Script text. /// Desired forward spaces. /// Determines if exact forward spacing should forced on each line - when true, existing hierarchy will not be preserved. /// Script with corrected forward alignment. public static string FixForwardSpacing(this string script, int spaces = 4, bool forceFixed = false) { string forwardSpacing = new string(' ', spaces); if (forceFixed) { string[] lines = script.Split(s_newLineDelimeters, StringSplitOptions.None); return string.Join(Environment.NewLine, lines.Select(line => $"{forwardSpacing}{line.TrimStart(null)}")); } Tuple[] linesAndLengths = script .Split(s_newLineDelimeters, StringSplitOptions.None) .Select(line => new Tuple(line, line.Length - line.TrimStart(null).Length)) .ToArray(); int minLength = linesAndLengths .Select(lineAndLength => (int?)lineAndLength.Item2) .Where(length => length.GetValueOrDefault() > 0) .Min() ?? 0; return string.Join(Environment.NewLine, linesAndLengths.Select(lineAndLength => lineAndLength.Item2 > 0 ? $"{forwardSpacing}{(lineAndLength.Item2 > minLength ? lineAndLength.Item1.Substring(minLength) : lineAndLength.Item1)}" : lineAndLength.Item1.ToNonNullNorEmptyString())); } /// /// Adds basic authentication header to an /// /// instance to add authentication header. /// User name for basic authentication. /// Password for basic authentication. public static void AddBasicAuthenticationHeader(this HttpClient client, string userName, string password) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userName}:{password}"))); } /// /// Gets query parameters for current request message /// /// /// public static Dictionary QueryParameters(this HttpRequestMessage request) { return request.GetQueryNameValuePairs().ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase); } /// /// Gets a collection of uploaded post data from an . /// /// request data that contains form data and/or uploaded files. /// Parsed post data. public static PostData GetPostData(this HttpRequestMessage request) { // Providing non-async version to simplify processing within Razor script Task getPostDataTask = GetPostDataAsync(request, CancellationToken.None); getPostDataTask.ContinueWith(task => { if ((object)task.Exception != null) throw task.Exception; }, TaskContinuationOptions.OnlyOnFaulted); return getPostDataTask.Result; } /// /// Asynchronously gets a collection of uploaded post data from an . /// /// request data that contains form data and/or uploaded files. /// The token to monitor for cancellation requests. /// Parsed post data. public static async Task GetPostDataAsync(this HttpRequestMessage request, CancellationToken cancellationToken) { if ((object)request?.Content == null) return new PostData(); PostData postData; if (request.Properties.TryGetValue(PostDataKey, out object cachedPostData)) { postData = cachedPostData as PostData; } else { if (request.Content.IsMimeMultipartContent()) postData = (await request.Content.ReadAsMultipartAsync(new PostDataStreamProvider(), cancellationToken)).PostData; else if (request.Content.IsFormData()) postData = new PostData(await request.Content.ReadAsFormDataAsync(cancellationToken)); else postData = new PostData(); // Cache parsed post data with request so it doesn't have to be parsed again request.Properties[PostDataKey] = postData; } return postData; } /// /// Converts relative URL to an absolute URL. /// /// MVC HTML helper instance. /// Relative URL. /// Absolute URL. public static string ToAbsoluteUrl(this HtmlHelper helper, string url) { if (string.IsNullOrEmpty(url)) return ""; if (HttpContext.Current == null) return ""; if (url.StartsWith("/")) url = url.Insert(0, "~"); if (!url.StartsWith("~/")) url = url.Insert(0, "~/"); Uri uri = HttpContext.Current.Request.Url; string port = uri.Port == 80 ? "" : ":" + uri.Port; return $"{uri.Scheme}://{uri.Host}{port}{VirtualPathUtility.ToAbsolute(url)}"; } /// /// Includes an HTTP resource directly into an MVC view page. /// /// MVC HTML helper instance. /// URL of resource to include. /// Resolved URL resource as an MvcHtmlString. public static MvcHtmlString IncludeUrl(this HtmlHelper helper, string url) { if (string.IsNullOrEmpty(url)) return new MvcHtmlString(""); HttpWebRequest request = WebRequest.CreateHttp(url); request.Credentials = CredentialCache.DefaultCredentials; using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) using (Stream stream = response?.GetResponseStream()) { if ((object)stream == null) return new MvcHtmlString($""); using (StreamReader reader = new StreamReader(stream)) return new MvcHtmlString(reader.ReadToEnd()); } } /// /// Includes an embedded resource directly into an MVC view page. /// /// MVC HTML helper instance. /// Resource to include. /// Resource as an MvcHtmlString. public static MvcHtmlString IncludeResouce(this HtmlHelper helper, string resourceName) { if (string.IsNullOrEmpty(resourceName)) return new MvcHtmlString(""); Stream stream = OpenEmbeddedResourceStream(resourceName); if ((object)stream == null) return new MvcHtmlString(""); using (StreamReader reader = new StreamReader(stream)) return new MvcHtmlString(reader.ReadToEnd()); } /// /// Renders a Razor view from an embedded resource. /// /// MVC HTML helper instance. /// Embedded resource name. /// Type of model. /// Model instance. /// Rendered resource as an MvcHtmlString. /// /// If needed by view, define ViewBag.DataContext before rendering resource. /// public static MvcHtmlString RenderResource(this HtmlHelper helper, string resourceName, Type modelType = null, object model = null) { MvcHtmlString result; DynamicViewBag viewBag = new DynamicViewBag(helper.ViewData); bool isPost = HttpContext.Current.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase); viewBag.AddValue("IsPost", isPost); viewBag.AddValue("Request", HttpContext.Current.Items["MS_HttpRequestMessage"] ?? new HttpRequestMessage(isPost ? HttpMethod.Post : HttpMethod.Get, HttpContext.Current.Request.Url)); if (FilePath.GetExtension(resourceName).Equals(".vbhtml", StringComparison.OrdinalIgnoreCase)) result = new MvcHtmlString(RazorEngine.Default.RunCompile(resourceName, modelType, model, viewBag)); else result = new MvcHtmlString(RazorEngine.Default.RunCompile(resourceName, modelType, model, viewBag)); // Copy any new or updated view bag elements back to MVC view data dictionary foreach (string memberName in viewBag.GetDynamicMemberNames()) { viewBag.TryGetMember(new MemberBinder(memberName), out object value); if (value != null) helper.ViewData[memberName] = value; } return result; } /// /// Renders a Razor view from an embedded resource. /// /// Type of model. /// MVC HTML helper instance. /// Embedded resource name. /// Model instance. /// Rendered resource as an MvcHtmlString. /// /// If needed by view, define ViewBag.DataContext before rendering resource. /// public static MvcHtmlString RenderResource(this HtmlHelper helper, string resourceName, T model) { return RenderResource(helper, resourceName, typeof(T), model); } /// /// Determines if specified embedded resource exists. /// /// Fully qualified name of resource to check. /// true if resource exists; otherwise, false. public static bool EmbeddedResourceExists(string resourceName) { return s_executingAssemblyResources.Contains(resourceName) || s_callingAssemblyResources.Contains(resourceName) || s_entryAssemblyResources.Contains(resourceName) || s_embeddedResourceAssemblies.Any(resources => resources.Value.Contains(resourceName)); } /// /// Opens a stream to an embedded resource. /// /// Fully qualified name of resource to open. /// Stream to embedded resource if found; otherwise, null. public static Stream OpenEmbeddedResourceStream(string resourceName) { Stream resourceStream = null; if (s_executingAssemblyResources.Contains(resourceName)) resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); if (resourceStream != null) return resourceStream; if (s_callingAssemblyResources.Contains(resourceName)) resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); if (resourceStream != null) return resourceStream; if (s_entryAssemblyResources.Contains(resourceName)) resourceStream = Assembly.GetEntryAssembly()?.GetManifestResourceStream(resourceName); if (resourceStream != null) return resourceStream; foreach (KeyValuePair> resources in s_embeddedResourceAssemblies) { Assembly assembly = resources.Key; HashSet resourceNames = resources.Value; if (resourceNames.Contains(resourceName)) return assembly.GetManifestResourceStream(resourceName); } return null; } /// /// Adds embedded resources from the specified assembly. /// /// Assembly to add resources from. public static void AddEmbeddedResourceAssembly(Assembly assembly) { string[] resourceNames = assembly?.GetManifestResourceNames(); if ((object)resourceNames != null && resourceNames.Length > 0) { HashSet embeddedResourceNames = new HashSet(StringComparer.Ordinal); foreach (string resourceName in resourceNames) embeddedResourceNames.Add(resourceName); s_embeddedResourceAssemblies[assembly] = embeddedResourceNames; } } // Code replaced by full dependent assembly scan withing ValidateAssemblyBindings tool ///// ///// Validates the assembly bindings for the specified application . ///// ///// Application configuration file to validate for needed assembly bindings. ///// Stream to assembly bindings to add; defaults to embedded "GSF.Web.AssemblyBindings.xml" resource. ///// true if assembly bindings were updated; otherwise, false. ///// Any stream passed in via will be closed if function succeeds. //public static bool ValidateAssemblyBindings(string configFileName, Stream assemblyBindingsSource = null) //{ // if (!File.Exists(configFileName)) // return false; // XmlDocument configFile = new XmlDocument(); // configFile.Load(configFileName); // XmlNode runTime = configFile.SelectSingleNode("configuration/runtime"); // if ((object)runTime == null) // { // XmlNode config = configFile.SelectSingleNode("configuration"); // // This is expected to already exist... // if ((object)config == null) // return false; // runTime = configFile.CreateElement("runtime"); // config.AppendChild(runTime); // XmlElement gcServer = configFile.CreateElement("gcServer"); // XmlAttribute enabled = configFile.CreateAttribute("enabled"); // enabled.Value = "true"; // gcServer.Attributes.Append(enabled); // runTime.AppendChild(gcServer); // XmlElement gcConcurrent = configFile.CreateElement("gcConcurrent"); // enabled = configFile.CreateAttribute("enabled"); // enabled.Value = "true"; // gcConcurrent.Attributes.Append(enabled); // runTime.AppendChild(gcConcurrent); // } // using (Stream stream = assemblyBindingsSource ?? OpenEmbeddedResourceStream("GSF.Web.AssemblyBindings.xml")) // { // XmlNamespaceManager nsmgr = new XmlNamespaceManager(configFile.NameTable); // nsmgr.AddNamespace("s", "urn:schemas-microsoft-com:asm.v1"); // XmlDocument assemblyBindingsXml = new XmlDocument(); // assemblyBindingsXml.Load(stream); // XmlDocumentFragment assemblyBindings = configFile.CreateDocumentFragment(); // assemblyBindings.InnerXml = assemblyBindingsXml.InnerXml; // XmlNode oldAssemblyBindings = configFile.SelectSingleNode("configuration/runtime/s:assemblyBinding", nsmgr); // if ((object)oldAssemblyBindings != null) // runTime.ReplaceChild(assemblyBindings, oldAssemblyBindings); // else // runTime.AppendChild(assemblyBindings); // } // configFile.Save(configFileName); // return true; //} } }