diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 423e380e..bac559b4 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -402,6 +402,7 @@ + diff --git a/Disco.Services/Exporting/ExportHelpers.cs b/Disco.Services/Exporting/ExportHelpers.cs index f668a301..af265003 100644 --- a/Disco.Services/Exporting/ExportHelpers.cs +++ b/Disco.Services/Exporting/ExportHelpers.cs @@ -15,9 +15,6 @@ namespace Disco.Services { public static ExportResult WriteExport(IExportOptions options, IScheduledTaskStatus status, List> metadata, List records) where T : IExportRecord { - if (records.Count == 0) - return new ExportResult(); - var filenameWithoutExtension = $"{options.FilenamePrefix}-{status.StartedTimestamp.Value:yyyyMMdd-HHmmss}"; MemoryStream stream; string filename; diff --git a/Disco.Services/Logging/LogExport.cs b/Disco.Services/Logging/LogExport.cs new file mode 100644 index 00000000..501c4a7e --- /dev/null +++ b/Disco.Services/Logging/LogExport.cs @@ -0,0 +1,67 @@ +using Disco.Models.Exporting; +using Disco.Models.Services.Exporting; +using Disco.Services.Logging.Models; +using Disco.Services.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Disco.Services.Logging +{ + using Metadata = ExportFieldMetadata; + + public static class LogExport + { + public static ExportResult GenerateExport(ExportFormat format, List records) + { + var options = new LogExportOptions(format); + + const string DateFormat = "yyyy-MM-dd"; + const string DateTimeFormat = DateFormat + " HH:mm:ss"; + Func csvStringEncoded = (o) => o == null ? null : $"\"{((string)o).Replace("\"", "\"\"")}\""; + Func csvToStringEncoded = (o) => o == null ? null : o.ToString(); + Func csvCurrencyEncoded = (o) => ((decimal?)o).HasValue ? ((decimal?)o).Value.ToString("C") : null; + Func csvDateEncoded = (o) => ((DateTime)o).ToString(DateFormat); + Func csvDateTimeEncoded = (o) => ((DateTime)o).ToString(DateTimeFormat); + Func csvNullableDateEncoded = (o) => ((DateTime?)o).HasValue ? csvDateEncoded(o) : null; + Func csvNullableDateTimeEncoded = (o) => ((DateTime?)o).HasValue ? csvDateTimeEncoded(o) : null; + + var metadata = new List + { + new Metadata(nameof(LogLiveEvent.Timestamp), nameof(LogLiveEvent.Timestamp), typeof(DateTime), e => e.Timestamp, csvDateTimeEncoded), + new Metadata(nameof(LogLiveEvent.ModuleId), nameof(LogLiveEvent.ModuleId), typeof(int), e => e.ModuleId, csvToStringEncoded), + new Metadata(nameof(LogLiveEvent.ModuleName), nameof(LogLiveEvent.ModuleName), typeof(string), e => e.ModuleName, csvStringEncoded), + new Metadata(nameof(LogLiveEvent.ModuleDescription), nameof(LogLiveEvent.ModuleDescription), typeof(string), e => e.ModuleDescription, csvStringEncoded), + new Metadata(nameof(LogLiveEvent.EventTypeId), nameof(LogLiveEvent.EventTypeId), typeof(int), e => e.EventTypeId, csvToStringEncoded), + new Metadata(nameof(LogLiveEvent.EventTypeName), nameof(LogLiveEvent.EventTypeName), typeof(string), e => e.EventTypeName, csvStringEncoded), + new Metadata("Severity", "Severity", typeof(string), e => e.EventTypeSeverity, csvToStringEncoded), + new Metadata("Message", "Message", typeof(string), e => e.FormattedMessage, csvStringEncoded), + }; + if (records.Count > 0) + { + var argCount = records.Max(r => r.Arguments?.Length ?? 0); + for (var i = 0; i < argCount; i++) + { + var index = i; + var name = $"Data{i + 1:00}"; + metadata.Add(new Metadata(name, name, typeof(string), e => (e.Arguments?.Length ?? 0) > index ? (e.Arguments[index] ?? "null") : null, csvStringEncoded)); + } + } + + return ExportHelpers.WriteExport(options, ScheduledTaskMockStatus.Create("Export Disco ICT Logs"), metadata, records); + } + } + + public class LogExportOptions : IExportOptions + { + public ExportFormat Format { get; set; } + public string FilenamePrefix { get; } = "DiscoIctLogs"; + public string ExcelWorksheetName { get; set; } = "Disco ICT Logs"; + public string ExcelTableName { get; set; } = "DiscoIctLogs"; + + public LogExportOptions(ExportFormat format) + { + Format = format; + } + } +} diff --git a/Disco.Services/Logging/Models/LogLiveEvent.cs b/Disco.Services/Logging/Models/LogLiveEvent.cs index 79a26d48..e213ab62 100644 --- a/Disco.Services/Logging/Models/LogLiveEvent.cs +++ b/Disco.Services/Logging/Models/LogLiveEvent.cs @@ -1,9 +1,10 @@ using System; +using Disco.Models.Exporting; using Newtonsoft.Json; namespace Disco.Services.Logging.Models { - public class LogLiveEvent + public class LogLiveEvent : IExportRecord { public int ModuleId { get; set; } public string ModuleName { get; set; } diff --git a/Disco.Services/Logging/Utilities.cs b/Disco.Services/Logging/Utilities.cs index 0afe226e..1ec15294 100644 --- a/Disco.Services/Logging/Utilities.cs +++ b/Disco.Services/Logging/Utilities.cs @@ -11,56 +11,6 @@ namespace Disco.Services.Logging public static class Utilities { - public const string LogEventCSVHeader = "Timestamp,ModuleId,ModuleName,ModuleDescription,EventTypeId,EventTypeName,Severity,Message"; - public static void ToCsvLine(this Models.LogLiveEvent e, TextWriter writer) - { - writer.Write(e.Timestamp.ToString("yyy-MM-dd HH:mm:ss")); - writer.Write(","); - writer.Write(e.ModuleId); - writer.Write(",\""); - writer.Write(e.ModuleName); - writer.Write("\",\""); - writer.Write(e.ModuleDescription); - writer.Write("\","); - writer.Write(e.EventTypeId); - writer.Write(",\""); - writer.Write(e.EventTypeName); - writer.Write("\","); - writer.Write(e.EventTypeSeverity); - writer.Write(",\""); - writer.Write(e.FormattedMessage.Replace("\"", "'")); - writer.Write("\""); - if (e.Arguments != null) - { - foreach (var arg in e.Arguments) - { - writer.Write(",\""); - if (arg == null) - writer.Write("null"); - else - writer.Write(arg.ToString().Replace("\"", "'")); - writer.Write("\""); - } - } - writer.WriteLine(); - } - public static MemoryStream ToCsv(this List e) - { - var ms = new MemoryStream(); - StreamWriter sw = new StreamWriter(ms); - sw.WriteLine(LogEventCSVHeader); - if (e != null) - { - foreach (var le in e) - { - le.ToCsvLine(sw); - } - } - sw.Flush(); - ms.Position = 0; - return ms; - } - public static List ToSelectListItems(this List items) { return items.Select(et => new SelectListItem() { Value = et.Id.ToString(), Text = et.Name }).ToList(); diff --git a/Disco.Web/Areas/API/Controllers/LoggingController.cs b/Disco.Web/Areas/API/Controllers/LoggingController.cs index 01d825cc..91d9ee8f 100644 --- a/Disco.Web/Areas/API/Controllers/LoggingController.cs +++ b/Disco.Web/Areas/API/Controllers/LoggingController.cs @@ -1,4 +1,5 @@ -using Disco.Services.Authorization; +using Disco.Models.Exporting; +using Disco.Services.Authorization; using Disco.Services.Logging; using Disco.Services.Tasks; using Disco.Services.Web; @@ -19,7 +20,7 @@ namespace Disco.Web.Areas.API.Controllers return Json(m, JsonRequestBehavior.AllowGet); } - [DiscoAuthorize(Claims.Config.Logging.Show)] + [HttpPost, ValidateAntiForgeryToken, DiscoAuthorize(Claims.Config.Logging.Show)] public virtual ActionResult RetrieveEvents(string Format, DateTime? Start = null, DateTime? End = null, int? ModuleId = null, List EventTypeIds = null, int? Take = null) { var logRetriever = new ReadLogContext() @@ -32,22 +33,20 @@ namespace Disco.Web.Areas.API.Controllers }; var results = logRetriever.Query(Database); + var exportFormat = ExportFormat.Xlsx; + switch (Format.ToLower()) { case "json": - { return Json(results, JsonRequestBehavior.AllowGet); - } case "csv": - { - return File(results.ToCsv(), "text/csv", "DiscoLogs.csv"); - } - default: - { - throw new ArgumentException("Unknown Format", "Format"); - } + exportFormat = ExportFormat.Csv; + break; } + var export = LogExport.GenerateExport(exportFormat, results); + + return File(export.Result, export.MimeType, export.Filename); } public virtual ActionResult ScheduledTaskStatus(string id) diff --git a/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.cshtml b/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.cshtml index c5685135..fcdcec07 100644 --- a/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.cshtml +++ b/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.cshtml @@ -8,6 +8,7 @@

Documents Imported Today

+ @Html.AntiForgeryToken()

No imported documents today

@@ -280,7 +281,8 @@ Start: d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(), End: null, ModuleId: 40, - Take: 2000 + Take: 2000, + '__RequestVerificationToken': host.find('input[name="__RequestVerificationToken"]').val() }; $.ajax({ url: '@(Url.Action(MVC.API.Logging.RetrieveEvents()))', diff --git a/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.generated.cs b/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.generated.cs index 778ae62a..dd817b99 100644 --- a/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.generated.cs +++ b/Disco.Web/Areas/Config/Views/DocumentTemplate/ImportStatus.generated.cs @@ -59,7 +59,18 @@ WriteLiteral("\r\n

Documents Imported Today\r\n

\r\n\r\n \r\n"); + +WriteLiteral(" "); + + + #line 11 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" +Write(Html.AntiForgeryToken()); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n $(function () {\r\n var vm;\r\n var host = "var urlDeviceShow = \'"); - #line 103 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 104 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.Device.Show())); @@ -285,7 +296,7 @@ WriteLiteral(">\r\n $(function () {\r\n var vm;\r\n var host = WriteLiteral("/\'\r\n var urlJobShow = \'"); - #line 104 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 105 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.Job.Show())); @@ -294,7 +305,7 @@ WriteLiteral("/\'\r\n var urlJobShow = \'"); WriteLiteral("/\'\r\n var urlUserShow = \'"); - #line 105 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 106 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.User.Show())); @@ -303,7 +314,7 @@ WriteLiteral("/\'\r\n var urlUserShow = \'"); WriteLiteral("/\'\r\n var urlPageThumbnail = \'"); - #line 106 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 107 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.API.DocumentTemplate.ImporterThumbnail())); @@ -312,7 +323,7 @@ WriteLiteral("/\'\r\n var urlPageThumbnail = \'"); WriteLiteral("/\'\r\n var urlDocumentTemplate = \'"); - #line 107 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 108 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.Config.DocumentTemplate.Index())); @@ -321,7 +332,7 @@ WriteLiteral("/\'\r\n var urlDocumentTemplate = \'"); WriteLiteral("/\';\r\n var urlManuallyAssign = \'"); - #line 108 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 109 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.Config.DocumentTemplate.UndetectedPages())); @@ -428,11 +439,12 @@ WriteLiteral("\';\r\n var isLive = false;\r\n\r\n function pageVie "l();\r\n\r\n // Load Logs\r\n var d = new Date();\r\n v" + "ar loadData = {\r\n Format: \"json\",\r\n Start: d.getFu" + "llYear() + \'-\' + (d.getMonth() + 1) + \'-\' + d.getDate(),\r\n End: n" + -"ull,\r\n ModuleId: 40,\r\n Take: 2000\r\n };\r" + -"\n $.ajax({\r\n url: \'"); +"ull,\r\n ModuleId: 40,\r\n Take: 2000,\r\n " + +" \'__RequestVerificationToken\': host.find(\'input[name=\"__RequestVerificationToke" + +"n\"]\').val()\r\n };\r\n $.ajax({\r\n url: \'"); - #line 286 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 288 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Url.Action(MVC.API.Logging.RetrieveEvents())); @@ -464,7 +476,7 @@ WriteLiteral(@"', $.connection.hub.qs = { LogModules: '"); - #line 309 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" + #line 311 "..\..\Areas\Config\Views\DocumentTemplate\ImportStatus.cshtml" Write(Disco.Services.Documents.DocumentsLog.Current.LiveLogGroupName); diff --git a/Disco.Web/Areas/Config/Views/Enrolment/Status.cshtml b/Disco.Web/Areas/Config/Views/Enrolment/Status.cshtml index 0298fe5e..99b3cb48 100644 --- a/Disco.Web/Areas/Config/Views/Enrolment/Status.cshtml +++ b/Disco.Web/Areas/Config/Views/Enrolment/Status.cshtml @@ -7,6 +7,7 @@ Html.BundleDeferred("~/ClientScripts/Modules/jQuery-Isotope"); }
+ @Html.AntiForgeryToken();

No enrollment sessions today

@@ -16,7 +17,7 @@ - +

@@ -337,7 +338,8 @@ Start: d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(), End: null, ModuleId: 50, - Take: 2000 + Take: 2000, + '__RequestVerificationToken': host.find('input[name="__RequestVerificationToken"]').val() }; $.ajax({ url: '@(Url.Action(MVC.API.Logging.RetrieveEvents()))', diff --git a/Disco.Web/Areas/Config/Views/Enrolment/Status.generated.cs b/Disco.Web/Areas/Config/Views/Enrolment/Status.generated.cs index 15811730..f4331770 100644 --- a/Disco.Web/Areas/Config/Views/Enrolment/Status.generated.cs +++ b/Disco.Web/Areas/Config/Views/Enrolment/Status.generated.cs @@ -60,7 +60,18 @@ WriteLiteral("\r\n\r\n \r\n"); + +WriteLiteral(" "); + + + #line 10 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" +Write(Html.AntiForgeryToken()); + + + #line default + #line hidden +WriteLiteral(";\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n"); - #line 32 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 33 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" #line default #line hidden - #line 32 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 33 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" using (Html.BeginForm(MVC.API.Enrollment.ResolveSessionPending(), FormMethod.Post)) { @@ -180,20 +191,20 @@ WriteLiteral(" data-bind=\"text: pendingIdentifier\""); WriteLiteral(">\r\n"); - #line 35 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 36 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" #line default #line hidden - #line 35 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 36 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Html.AntiForgeryToken()); #line default #line hidden - #line 35 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 36 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" ; @@ -246,7 +257,7 @@ WriteLiteral(" class=\"button\""); WriteLiteral(">Reject\r\n
\r\n"); - #line 44 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 45 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" } @@ -426,7 +437,7 @@ WriteLiteral(@"> var deviceBaseUrl = '"); - #line 124 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 125 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Url.Action(MVC.Device.Show())); @@ -435,7 +446,7 @@ WriteLiteral(@"> WriteLiteral("/\'\r\n var deviceModelImageUrl = \'"); - #line 125 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 126 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Url.Action(MVC.API.DeviceModel.Image())); @@ -444,7 +455,7 @@ WriteLiteral("/\'\r\n var deviceModelImageUrl = \'"); WriteLiteral("/\'\r\n var iconWarningUrl = \'url("); - #line 126 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 127 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Links.ClientSource.Style.Images.Status.warning32_png); @@ -453,7 +464,7 @@ WriteLiteral("/\'\r\n var iconWarningUrl = \'url("); WriteLiteral(")\';\r\n var iconErrorUrl = \'url("); - #line 127 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 128 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Links.ClientSource.Style.Images.Status.fail32_png); @@ -576,7 +587,7 @@ WriteLiteral(")\';\r\n\r\n function pageViewModel() {\r\n var " url: \'"); - #line 318 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 319 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Url.Action(MVC.API.DeviceModel.Index())); @@ -604,13 +615,14 @@ WriteLiteral(@"', Start: d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate(), End: null, ModuleId: 50, - Take: 2000 + Take: 2000, + '__RequestVerificationToken': host.find('input[name=""__RequestVerificationToken""]').val() }; $.ajax({ url: '"); - #line 343 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 345 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Url.Action(MVC.API.Logging.RetrieveEvents())); @@ -649,7 +661,7 @@ WriteLiteral(@"', $.connection.hub.qs = { LogModules: '"); - #line 373 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" + #line 375 "..\..\Areas\Config\Views\Enrolment\Status.cshtml" Write(Disco.Services.Devices.Enrolment.EnrolmentLog.Current.LiveLogGroupName); diff --git a/Disco.Web/Areas/Config/Views/Logging/Index.cshtml b/Disco.Web/Areas/Config/Views/Logging/Index.cshtml index da25d551..ca698a38 100644 --- a/Disco.Web/Areas/Config/Views/Logging/Index.cshtml +++ b/Disco.Web/Areas/Config/Views/Logging/Index.cshtml @@ -4,31 +4,43 @@ Authorization.Require(Claims.Config.Logging.Show); ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Logging"); - Html.BundleDeferred("~/ClientScripts/Modules/jQueryUI-TimePicker"); } @using (Html.BeginForm(MVC.API.Logging.RetrieveEvents())) -{ +{ + @Html.AntiForgeryToken()

Export Logs

- + + + + - - - - - - - -
Start Filter + Format + +
+ Start Filter - + * Optional
End Filter + + End Filter - + * Optional
Limit Filter + + Limit Filter
Module Filter + + Module Filter
- @Html.Hidden("Format", "CSV") - -
+

+ +

\r\n
\r\n"); +"oxes.prop(\'checked\', true);\r\n }\r\n }\r\n " + +" return false;\r\n });\r\n $(\'#event" + +"TypesSelectNone\').click(function () {\r\n var selectedModule = " + +"moduleId.val();\r\n if (selectedModule) {\r\n " + +" var selectedModuleEventTypes = logModuleEventTypes.filter(\'[data-logmoduleid" + +"=\"\' + selectedModule + \'\"]\');\r\n if (selectedModuleEventTy" + +"pes.length > 0) {\r\n var selectedModuleEventTypeCheckb" + +"oxes = selectedModuleEventTypes.find(\'input[type=\"checkbox\"]\');\r\n " + +" selectedModuleEventTypeCheckboxes.prop(\'checked\', false);\r\n " + +" }\r\n }\r\n return false;\r\n " + +" });\r\n\r\n });\r\n \r\n
\r\n"); - #line 149 "..\..\Areas\Config\Views\Logging\Index.cshtml" + #line 148 "..\..\Areas\Config\Views\Logging\Index.cshtml" } @@ -372,7 +397,7 @@ WriteLiteral(">\r\n $(function () {\r\n var filterStar WriteLiteral("

Live Logging

\r\n"); - #line 151 "..\..\Areas\Config\Views\Logging\Index.cshtml" + #line 150 "..\..\Areas\Config\Views\Logging\Index.cshtml" Write(Html.Partial(MVC.Config.Shared.Views.LogEvents, new Disco.Web.Areas.Config.Models.Shared.LogEventsModel() { IsLive = true, diff --git a/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml b/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml index c40fd9be..49989539 100644 --- a/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml +++ b/Disco.Web/Areas/Config/Views/Shared/LogEvents.cshtml @@ -7,6 +7,7 @@ var uniqueId = Guid.NewGuid().ToString("N"); }
+ @Html.AntiForgeryToken() @@ -42,7 +43,7 @@ }