//******************************************************************************************************
// CsvDownloadHandler.ashx.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:
// ----------------------------------------------------------------------------------------------------
// 07/29/2016 - Billy Ernest
// Generated original version of source code.
// 08/10/2016 - J. Ritchie Carroll
// Combined ASP.NET and self-hosted handlers into a single shared embedded resource.
//
//******************************************************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using GSF.Collections;
using GSF.Data.Model;
using GSF.Reflection;
using GSF.Security;
using GSF.Threading;
using GSF.Web.Hosting;
using GSF.Web.Hubs;
using GSF.Web.Security;
using CancellationToken = System.Threading.CancellationToken;
// ReSharper disable once AccessToDisposedClosure
namespace GSF.Web.Model.Handlers
{
///
/// Handles downloading of modeled table data as a comma-separated value file.
///
public class CsvDownloadHandler : IHttpHandler, IHostedHttpHandler
{
#region [ Members ]
// Nested Types
private class HttpResponseCancellationToken : CompatibleCancellationToken
{
private readonly HttpResponse m_reponse;
public HttpResponseCancellationToken(HttpResponse response) : base(CancellationToken.None) =>
m_reponse = response;
public override bool IsCancelled => !m_reponse.IsClientConnected;
}
// Constants
private const string CsvContentType = "text/csv";
#endregion
#region [ Properties ]
///
/// Gets a value indicating whether another request can use the instance.
///
///
/// true if the instance is reusable; otherwise, false.
///
public bool IsReusable => false;
///
/// Determines if client cache should be enabled for rendered handler content.
///
///
/// If rendered handler content does not change often, the server and client will use the
/// to determine if the client needs to refresh the content.
///
public bool UseClientCache => false;
#endregion
#region [ Methods ]
///
/// Gets hash of response content based on any parameters.
///
/// HTTP request message.
///
/// Value is only used when is true.
///
public long GetContentHash(HttpRequestMessage request) => 0;
///
/// Enables processing of HTTP Web requests by a custom HttpHandler that implements the interface.
///
/// An object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.
public void ProcessRequest(HttpContext context)
{
HttpResponse response = context.Response;
HttpResponseCancellationToken cancellationToken = new HttpResponseCancellationToken(response);
NameValueCollection requestParameters = context.Request.QueryString;
SecurityPrincipal securityPrincipal = context.User as SecurityPrincipal;
response.ClearContent();
response.Clear();
response.AddHeader("Content-Type", CsvContentType);
response.AddHeader("Content-Disposition", "attachment;filename=" + GetExportFileName(requestParameters));
response.BufferOutput = true;
try
{
CopyModelAsCsvToStream(securityPrincipal, requestParameters, response.OutputStream, response.Flush, cancellationToken);
}
catch (Exception ex)
{
LogExceptionHandler?.Invoke(ex);
throw;
}
finally
{
response.End();
}
}
///
/// Enables processing of HTTP web requests by a custom handler that implements the interface.
///
/// HTTP request message.
/// HTTP response message.
/// Propagates notification from client that operations should be canceled.
public Task ProcessRequestAsync(HttpRequestMessage request, HttpResponseMessage response, CancellationToken cancellationToken)
{
NameValueCollection requestParameters = request.RequestUri.ParseQueryString();
response.Content = new PushStreamContent((stream, _, _) =>
{
try
{
SecurityPrincipal securityPrincipal = request.GetRequestContext().Principal as SecurityPrincipal;
CopyModelAsCsvToStream(securityPrincipal, requestParameters, stream, null, cancellationToken);
}
catch (Exception ex)
{
LogExceptionHandler?.Invoke(ex);
throw;
}
finally
{
stream.Close();
}
},
new MediaTypeHeaderValue(CsvContentType));
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = GetExportFileName(requestParameters)
};
#if MONO
return Task.FromResult(false);
#else
return Task.CompletedTask;
#endif
}
private static void CopyModelAsCsvToStream(SecurityPrincipal securityPrincipal, NameValueCollection requestParameters, Stream responseStream, Action flushResponse, CompatibleCancellationToken cancellationToken)
{
string modelName = requestParameters["ModelName"];
string hubName = requestParameters["HubName"];
string connectionID = requestParameters["ConnectionID"];
string filterText = requestParameters["FilterText"];
string sortField = requestParameters["SortField"];
bool sortAscending = requestParameters["SortAscending"].ParseBoolean();
bool showDeleted = requestParameters["ShowDeleted"].ParseBoolean();
string[] parentKeys = requestParameters["ParentKeys"].Split(',').Select(parentKey => parentKey.Replace(','.RegexEncode(), ",")).ToArray();
if (string.IsNullOrEmpty(modelName))
throw new ArgumentNullException(nameof(modelName), "Cannot download CSV data: no model type name was specified.");
if (string.IsNullOrEmpty(hubName))
throw new ArgumentNullException(nameof(hubName), "Cannot download CSV data: no hub type name was specified.");
Type modelType = AssemblyInfo.FindType(modelName);
if (modelType is null)
throw new InvalidOperationException($"Cannot download CSV data: failed to find model type \"{modelName}\" in loaded assemblies.");
Type hubType = AssemblyInfo.FindType(hubName);
if (hubType is null)
throw new InvalidOperationException($"Cannot download CSV data: failed to find hub type \"{hubName}\" in loaded assemblies.");
IRecordOperationsHub hub;
// Record operation tuple defines method name and allowed roles
Tuple queryRecordCountOperation;
Tuple queryRecordsOperation;
string queryRoles;
try
{
// Create a local record operations hub instance so that CSV export can query same record set that is visible in active hub context
hub = Activator.CreateInstance(hubType) as IRecordOperationsHub;
if (hub is null)
throw new SecurityException($"Cannot download CSV data: hub type \"{hubName}\" is not a IRecordOperationsHub, access cannot be validated.");
// Assign provided connection ID from active hub context to our local hub instance so that any session based data will be available to query functions
hub.ConnectionID = connectionID;
Tuple[] recordOperations;
try
{
// Get any authorized query roles as defined in hub records operations for modeled table, default to read allowed for query
recordOperations = hub.RecordOperationsCache.GetRecordOperations(modelType);
if (recordOperations is null)
throw new NullReferenceException();
}
catch (KeyNotFoundException ex)
{
throw new SecurityException($"Cannot download CSV data: hub type \"{hubName}\" does not define record operations for \"{modelName}\", access cannot be validated.", ex);
}
// Get record operation for querying record count
queryRecordCountOperation = recordOperations[(int)RecordOperation.QueryRecordCount];
if (queryRecordCountOperation is null)
throw new NullReferenceException();
// Get record operation for querying records
queryRecordsOperation = recordOperations[(int)RecordOperation.QueryRecords];
if (queryRecordsOperation is null)
throw new NullReferenceException();
// Get any defined role restrictions for record query operation - access to CSV download will based on these roles
queryRoles = string.IsNullOrEmpty(queryRecordsOperation.Item1) ? "*" : queryRecordsOperation.Item2 ?? "*";
}
catch (Exception ex)
{
throw new SecurityException($"Cannot download CSV data: failed to instantiate hub type \"{hubName}\" or access record operations, access cannot be validated.", ex);
}
DataContext dataContext = hub.DataContext;
// Validate current user has access to requested data
if (!dataContext.UserIsInRole(securityPrincipal, queryRoles))
throw new SecurityException($"Cannot download CSV data: access is denied for user \"{securityPrincipal?.Identity.Name ?? "Undefined"}\", minimum required roles = {queryRoles.ToDelimitedString(", ")}.");
const int TargetBufferSize = 524288;
StringBuilder readBuffer = new StringBuilder(TargetBufferSize * 2);
ManualResetEventSlim bufferReady = new ManualResetEventSlim(false);
List writeBuffer = new List();
object writeBufferLock = new object();
bool readComplete = false;
ITableOperations table = dataContext.Table(modelType);
string[] fieldNames = table.GetFieldNames(false).Where(field => !table.FieldHasAttribute(field)).ToArray();
bool hasDeletedField = !string.IsNullOrEmpty(dataContext.GetIsDeletedFlag(modelType));
Task readTask = Task.Factory.StartNew(() =>
{
try
{
const int PageSize = 250;
// Get query operation methods
MethodInfo queryRecordCount = hubType.GetMethod(queryRecordCountOperation.Item1);
MethodInfo queryRecords = hubType.GetMethod(queryRecordsOperation.Item1);
if (queryRecordCount is null || queryRecords is null)
throw new NullReferenceException("Query record operations are null");
// Setup query parameters
List