From 9d7c575dd189ceafb05a0daeec134419e8d336df Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Tue, 18 Feb 2025 21:08:56 +0000 Subject: [PATCH] case 4632 --- docs/8.0/ayanova/docs/changelog.md | 12 +- docs/8.0/ayanova/mkdocs.yml | 2 +- server/AyaNova/biz/ReportBiz.cs | 651 +++++++++++++----- .../generator/CoreJobCustomerNotify.cs | 338 ++++++--- server/AyaNova/util/ServerBootConfig.cs | 155 +++-- 5 files changed, 813 insertions(+), 345 deletions(-) diff --git a/docs/8.0/ayanova/docs/changelog.md b/docs/8.0/ayanova/docs/changelog.md index f4595fa3..c49ca8a3 100644 --- a/docs/8.0/ayanova/docs/changelog.md +++ b/docs/8.0/ayanova/docs/changelog.md @@ -6,15 +6,21 @@ If you are viewing this page from your local copy of AyaNova be advised that it See the [upgrade instructions](ops-upgrade.md) section of this manual for details. -## 2024 +## 2025 ### AyaNova 8.2.3 -In progress... +Released 2025-02-18 + +**Fixed** + +- Server: resolved edge case issue with Customer Notifications attached reports could fail to generate when platform is Windows / IIS and AYANOVA_REPORT_RENDER_API_URL_OVERRIDE configuration setting override in effect - Docs: fixed outdated and missing installation steps for setting directory rights and ownership in [linux server install](ops-install-linux-server.md) for post 8.2.0 changes (hat tip to Steven L. for spotting this) -- Docs: improved PO part requests documentation section to specify that only Parts that match the Vendor selected on the PO will be offered for selection in the Part Requests list. +- Docs: improved PO part requests documentation section to specify that only Parts that match the Vendor selected on the PO will be offered for selection in the Part Requests list + +## 2024 ### AyaNova 8.2.2 diff --git a/docs/8.0/ayanova/mkdocs.yml b/docs/8.0/ayanova/mkdocs.yml index 4acc2372..2dfe1de2 100644 --- a/docs/8.0/ayanova/mkdocs.yml +++ b/docs/8.0/ayanova/mkdocs.yml @@ -8,7 +8,7 @@ site_name: AyaNova manual site_dir: "../../../server/AyaNova/wwwroot/docs" site_url: https://ayanova.com/docs/ strict: true -copyright: Copyright © 2022-2025 Ground Zero Tech-Works Inc. REV-2025-02-10 +copyright: Copyright © 2022-2025 Ground Zero Tech-Works Inc. REV-2025-02-18 extra: generator: false # Extensions diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index d2a7c49a..975bced5 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -1,23 +1,28 @@ -using System.Threading.Tasks; -using System.Linq; -using System.IO; +using System; using System.Collections.Generic; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using AyaNova.Util; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; +using AyaNova.Util; using EnumsNET; -using PuppeteerSharp; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -using System; +using PuppeteerSharp; using StackExchange.Profiling.Internal; namespace AyaNova.Biz { internal class ReportBiz : BizObject, IJobObject, ISearchAbleObject { - internal ReportBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + internal ReportBiz( + AyContext dbcontext, + long currentUserId, + long userTranslationId, + AuthorizationRoles UserRoles + ) { ct = dbcontext; UserId = currentUserId; @@ -26,23 +31,36 @@ namespace AyaNova.Biz BizType = AyaType.Report; } - internal static ReportBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + internal static ReportBiz GetBiz( + AyContext ct, + Microsoft.AspNetCore.Http.HttpContext httpContext = null + ) { if (httpContext != null) - return new ReportBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + return new ReportBiz( + ct, + UserIdFromContext.Id(httpContext.Items), + UserTranslationIdFromContext.Id(httpContext.Items), + UserRolesFromContext.Roles(httpContext.Items) + ); else - return new ReportBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); + return new ReportBiz( + ct, + 1, + ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, + AuthorizationRoles.BizAdmin + ); } //////////////////////////////////////////////////////////////////////////////////////////////// - //EXISTS + //EXISTS internal async Task ExistsAsync(long id) { return await ct.Report.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// - //CREATE + //CREATE // internal async Task CreateAsync(Report newObject) { @@ -53,23 +71,23 @@ namespace AyaNova.Biz { await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); + await EventLogProcessor.LogEventToDatabaseAsync( + new Event(UserId, newObject.Id, BizType, AyaEvent.Created), + ct + ); await SearchIndexAsync(newObject, true); return newObject; } } - //////////////////////////////////////////////////////////////////////////////////////////////// //IMPORT // internal async Task ImportAsync(JObject o, bool skipIfAlreadyPresent = false) { - //Report newObject = new Report(); var newObject = o.ToObject(); - var proposedName = (string)o["Name"]; string newUniqueName = proposedName; bool NotUnique = true; @@ -86,16 +104,19 @@ namespace AyaNova.Biz return true; } } - } while (NotUnique); newObject.Name = newUniqueName; await ValidateAsync(newObject, null); - if (HasErrors) return false; + if (HasErrors) + return false; await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); + await EventLogProcessor.LogEventToDatabaseAsync( + new Event(UserId, newObject.Id, BizType, AyaEvent.Created), + ct + ); if (skipIfAlreadyPresent) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); @@ -105,13 +126,16 @@ namespace AyaNova.Biz } //////////////////////////////////////////////////////////////////////////////////////////////// - //GET - // + //GET + // internal async Task GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.Report.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); + await EventLogProcessor.LogEventToDatabaseAsync( + new Event(UserId, id, BizType, AyaEvent.Retrieved), + ct + ); return ret; } @@ -120,7 +144,6 @@ namespace AyaNova.Biz // internal async Task PutAsync(Report putObject) { - var dbObject = await GetAsync(putObject.Id, false); if (dbObject == null) { @@ -133,9 +156,9 @@ namespace AyaNova.Biz return null; } - await ValidateAsync(putObject, dbObject); - if (HasErrors) return null; + if (HasErrors) + return null; ct.Replace(dbObject, putObject); try { @@ -149,7 +172,10 @@ namespace AyaNova.Biz AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + await EventLogProcessor.LogEventToDatabaseAsync( + new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), + ct + ); await SearchIndexAsync(putObject, false); return putObject; } @@ -171,21 +197,40 @@ namespace AyaNova.Biz if (HasErrors) return false; { - var IDList = await ct.Review.AsNoTracking().Where(x => x.AType == AyaType.Report && x.ObjectId == id).Select(x => x.Id).ToListAsync(); + var IDList = await ct + .Review.AsNoTracking() + .Where(x => x.AType == AyaType.Report && x.ObjectId == id) + .Select(x => x.Id) + .ToListAsync(); if (IDList.Count() > 0) { - ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); + ReviewBiz b = new ReviewBiz( + ct, + UserId, + UserTranslationId, + CurrentUserRoles + ); foreach (long ItemId in IDList) if (!await b.DeleteAsync(ItemId, transaction)) { - AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}"); + AddError( + ApiErrorCode.CHILD_OBJECT_ERROR, + null, + $"Review [{ItemId}]: {b.GetErrorsAsString()}" + ); return false; } } } ct.Report.Remove(dbObject); await ct.SaveChangesAsync(); - await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); + await EventLogProcessor.DeleteObjectLogAsync( + UserId, + BizType, + dbObject.Id, + dbObject.Name, + ct + ); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await transaction.CommitAsync(); @@ -193,13 +238,22 @@ namespace AyaNova.Biz } } - //////////////////////////////////////////////////////////////////////////////////////////////// //GET LIST - // + // internal async Task> GetReportListAsync(AyaType aType) { - var rpts = await ct.Report.AsNoTracking().Where(z => z.AType == aType && z.Active == true).Select(z => new { id = z.Id, name = z.Name, roles = z.Roles }).OrderBy(z => z.name).ToListAsync(); + var rpts = await ct + .Report.AsNoTracking() + .Where(z => z.AType == aType && z.Active == true) + .Select(z => new + { + id = z.Id, + name = z.Name, + roles = z.Roles, + }) + .OrderBy(z => z.name) + .ToListAsync(); var ret = new List(); foreach (var item in rpts) { @@ -211,13 +265,16 @@ namespace AyaNova.Biz return ret; } - //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task SearchIndexAsync(Report obj, bool isNew) { - var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); + var SearchParams = new Search.SearchIndexProcessObjectParameters( + UserTranslationId, + obj.Id, + BizType + ); DigestSearchText(obj, SearchParams); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); @@ -225,7 +282,10 @@ namespace AyaNova.Biz await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } - public async Task GetSearchResultSummary(long id, AyaType specificType) + public async Task GetSearchResultSummary( + long id, + AyaType specificType + ) { var obj = await GetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); @@ -233,47 +293,51 @@ namespace AyaNova.Biz return SearchParams; } - public void DigestSearchText(Report obj, Search.SearchIndexProcessObjectParameters searchParams) + public void DigestSearchText( + Report obj, + Search.SearchIndexProcessObjectParameters searchParams + ) { if (obj != null) - searchParams.AddText(obj.Notes) - .AddText(obj.Name) - .AddText(obj.Template) - .AddText(obj.Style) - .AddText(obj.JsPrerender) - .AddText(obj.JsHelpers) - .AddText(obj.HeaderTemplate) - .AddText(obj.FooterTemplate); + searchParams + .AddText(obj.Notes) + .AddText(obj.Name) + .AddText(obj.Template) + .AddText(obj.Style) + .AddText(obj.JsPrerender) + .AddText(obj.JsHelpers) + .AddText(obj.HeaderTemplate) + .AddText(obj.FooterTemplate); } - - //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ValidateAsync(Report proposedObj, Report currentObj) { - bool isNew = currentObj == null; //Name required if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); - - //If name is otherwise OK, check that name is unique if (!PropertyHasErrors("Name")) { //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false //NOTE: unlike other objects reports can have the same name as long as the type differs - if (await ct.Report.AnyAsync(z => z.Name == proposedObj.Name && z.AType == proposedObj.AType && z.Id != proposedObj.Id)) + if ( + await ct.Report.AnyAsync(z => + z.Name == proposedObj.Name + && z.AType == proposedObj.AType + && z.Id != proposedObj.Id + ) + ) { AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); } } - } private async Task ValidateCanDelete(Report inObj) @@ -283,22 +347,36 @@ namespace AyaNova.Biz { //Note: errorbox will ensure it appears in the general errror box and not field specific //the translation key is to indicate what the linked object is that is causing the error - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("NotifySubscription")); + AddError( + ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, + "generalerror", + await Translate("NotifySubscription") + ); } - if (await ct.GlobalBizSettings.AnyAsync(z => z.CustomerDefaultWorkOrderReportId == inObj.Id) == true) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("GlobalSettings")); + if ( + await ct.GlobalBizSettings.AnyAsync(z => + z.CustomerDefaultWorkOrderReportId == inObj.Id + ) == true + ) + AddError( + ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, + "generalerror", + await Translate("GlobalSettings") + ); } - - //////////////////////////////////////////////////////////////////////////////////////////////// //REPORT DATA //Data fetched to return to report render or for designer for Client report design usage - public async Task GetReportDataForReportDesigner(DataListSelectedRequest selectedRequest) + public async Task GetReportDataForReportDesigner( + DataListSelectedRequest selectedRequest + ) { - var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportDataForReportDesigner"); + var log = AyaNova.Util.ApplicationLogging.CreateLogger( + "ReportBiz::GetReportDataForReportDesigner" + ); AuthorizationRoles effectiveRoles = CurrentUserRoles; if (selectedRequest.AType == AyaType.NoType) @@ -309,7 +387,15 @@ namespace AyaNova.Biz //Do we need to rehydrate the ID List from a DataList? if (selectedRequest.SelectedRowIds.Length == 0) - selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId); + selectedRequest.SelectedRowIds = + await DataListSelectedProcessingOptions.RehydrateIdList( + selectedRequest, + ct, + effectiveRoles, + log, + UserId, + UserTranslationId + ); //case 4568 get translation id ultimately without failing so as to not affect any existing callers that this might not work with long RequestorUserTranslationId = UserTranslationId; @@ -318,24 +404,38 @@ namespace AyaNova.Biz JToken clientMeta = ((DataListReportRequest)selectedRequest).ClientMeta; if (clientMeta["UserId"] != null) { - RequestorUserTranslationId = await ct.User.AsNoTracking().Where(a => a.Id == clientMeta["UserId"].ToObject()).Select(m => m.UserOptions.TranslationId).FirstAsync();//m => new { roles = m.Roles, name = m.Name, m.UserType, id = m.Id, translationId = m.UserOptions.TranslationId, currentAuthToken = m.CurrentAuthToken }).FirstAsync(); + RequestorUserTranslationId = await ct + .User.AsNoTracking() + .Where(a => a.Id == clientMeta["UserId"].ToObject()) + .Select(m => m.UserOptions.TranslationId) + .FirstAsync(); //m => new { roles = m.Roles, name = m.Name, m.UserType, id = m.Id, translationId = m.UserOptions.TranslationId, currentAuthToken = m.CurrentAuthToken }).FirstAsync(); } } catch (Exception exx) { log.LogDebug(exx, $"Failed to get RequestionUserTranslationId from ClientMeta"); - }; - + } + ; log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}"); - var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, CurrentUserRoles, RequestorUserTranslationId); - log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items"); - return await ((IReportAbleObject)biz).GetReportData(selectedRequest, Guid.Empty);//Guid.empty signifies it's not a job calling it + var biz = BizObjectFactory.GetBizObject( + selectedRequest.AType, + ct, + UserId, + CurrentUserRoles, + RequestorUserTranslationId + ); + log.LogDebug( + $"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items" + ); + return await ((IReportAbleObject)biz).GetReportData(selectedRequest, Guid.Empty); //Guid.empty signifies it's not a job calling it } - - - public async Task GetReportData(DataListSelectedRequest selectedRequest, Guid jobId, bool requestIsCustomerWorkOrderReport = false) + public async Task GetReportData( + DataListSelectedRequest selectedRequest, + Guid jobId, + bool requestIsCustomerWorkOrderReport = false + ) { var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportData"); AuthorizationRoles effectiveRoles = CurrentUserRoles; @@ -346,15 +446,33 @@ namespace AyaNova.Biz return null; } - if (!requestIsCustomerWorkOrderReport && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, selectedRequest.AType)) + if ( + !requestIsCustomerWorkOrderReport + && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole( + effectiveRoles, + selectedRequest.AType + ) + ) { - AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {selectedRequest.AType} type object"); + AddError( + ApiErrorCode.NOT_AUTHORIZED, + null, + $"User not authorized for {selectedRequest.AType} type object" + ); return null; } //Do we need to rehydrate the ID List from a DataList? if (selectedRequest.SelectedRowIds.Length == 0) - selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId); + selectedRequest.SelectedRowIds = + await DataListSelectedProcessingOptions.RehydrateIdList( + selectedRequest, + ct, + effectiveRoles, + log, + UserId, + UserTranslationId + ); if (!ReportRenderManager.KeepGoing(jobId)) return null; @@ -366,35 +484,62 @@ namespace AyaNova.Biz JToken clientMeta = ((DataListReportRequest)selectedRequest).ClientMeta; if (clientMeta["UserId"] != null) { - RequestorUserTranslationId = await ct.User.AsNoTracking().Where(a => a.Id == clientMeta["UserId"].ToObject()).Select(m => m.UserOptions.TranslationId).FirstAsync();//m => new { roles = m.Roles, name = m.Name, m.UserType, id = m.Id, translationId = m.UserOptions.TranslationId, currentAuthToken = m.CurrentAuthToken }).FirstAsync(); + RequestorUserTranslationId = await ct + .User.AsNoTracking() + .Where(a => a.Id == clientMeta["UserId"].ToObject()) + .Select(m => m.UserOptions.TranslationId) + .FirstAsync(); //m => new { roles = m.Roles, name = m.Name, m.UserType, id = m.Id, translationId = m.UserOptions.TranslationId, currentAuthToken = m.CurrentAuthToken }).FirstAsync(); } } catch (Exception exx) { log.LogDebug(exx, $"Failed to get RequestionUserTranslationId from ClientMeta"); - }; - + } + ; log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}"); - var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, CurrentUserRoles, RequestorUserTranslationId); - log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items"); + var biz = BizObjectFactory.GetBizObject( + selectedRequest.AType, + ct, + UserId, + CurrentUserRoles, + RequestorUserTranslationId + ); + log.LogDebug( + $"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items" + ); return await ((IReportAbleObject)biz).GetReportData(selectedRequest, jobId); } - //////////////////////////////////////////////////////////////////////////////////////////////// //RENDER // - public async Task RequestRenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl, string userName) + public async Task RequestRenderReport( + DataListReportRequest reportRequest, + DateTime renderTimeOutExpiry, + string apiUrl, + string userName + ) { - var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::RequestRenderReport"); - log.LogDebug($"report id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}"); + var log = AyaNova.Util.ApplicationLogging.CreateLogger( + "ReportBiz::RequestRenderReport" + ); + log.LogDebug( + $"report id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}" + ); //Is reporting api url overridden in CORS issue scenario (case 4398) if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_API_URL_OVERRIDE)) { - apiUrl = $"{ServerBootConfig.AYANOVA_REPORT_RENDER_API_URL_OVERRIDE.TrimEnd().TrimEnd('/')}/api/{AyaNovaVersion.CurrentApiVersion}/"; + //case 4632, override breaks special api url when processing customer notification + //in order to minimize side effects will go by special user name used here + if ( + userName + != ServerBootConfig.CUSTOMER_NOTIFICATION_ATTACHED_REPORT_RENDER_USERNAME + ) + apiUrl = + $"{ServerBootConfig.AYANOVA_REPORT_RENDER_API_URL_OVERRIDE.TrimEnd().TrimEnd('/')}/api/{AyaNovaVersion.CurrentApiVersion}/"; } //Customer User Report? @@ -403,10 +548,16 @@ namespace AyaNova.Biz { log.LogDebug("customer user report requested"); //get the user and workorder data and set the actual report id or return null if not found - var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == reportRequest.SelectedRowIds[0]).Select(x => x.Tags).FirstOrDefaultAsync(); - if (woTags == null) return null; + var woTags = await ct + .WorkOrder.AsNoTracking() + .Where(x => x.Id == reportRequest.SelectedRowIds[0]) + .Select(x => x.Tags) + .FirstOrDefaultAsync(); + if (woTags == null) + return null; var cr = await UserBiz.CustomerUserEffectiveRightsAsync(UserId, woTags); - if (cr.ThisWOEffectiveWOReportId == null) return null; + if (cr.ThisWOEffectiveWOReportId == null) + return null; reportRequest.ReportId = (long)cr.ThisWOEffectiveWOReportId; RequestIsCustomerWorkOrderReport = true; } @@ -426,14 +577,22 @@ namespace AyaNova.Biz reportRequest.AType = report.AType; } - AuthorizationRoles effectiveRoles = CurrentUserRoles; - - if (!RequestIsCustomerWorkOrderReport && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.AType)) + if ( + !RequestIsCustomerWorkOrderReport + && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole( + effectiveRoles, + report.AType + ) + ) { log.LogDebug($"bail: user unauthorized"); - AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.AType} type object"); + AddError( + ApiErrorCode.NOT_AUTHORIZED, + null, + $"User not authorized for {report.AType} type object" + ); return null; } @@ -441,7 +600,11 @@ namespace AyaNova.Biz if (reportRequest.ClientMeta == null) { log.LogDebug($"bail: ClientMeta parameter is missing"); - AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "ClientMeta parameter is missing and required to render report"); + AddError( + ApiErrorCode.VALIDATION_MISSING_PROPERTY, + null, + "ClientMeta parameter is missing and required to render report" + ); return null; } @@ -450,16 +613,27 @@ namespace AyaNova.Biz //Do we need to rehydrate the ID List from a DataList? if (reportRequest.SelectedRowIds.Length == 0) - reportRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(reportRequest, ct, effectiveRoles, log, UserId, UserTranslationId); + reportRequest.SelectedRowIds = + await DataListSelectedProcessingOptions.RehydrateIdList( + reportRequest, + ct, + effectiveRoles, + log, + UserId, + UserTranslationId + ); - var JobName = $"LT:Report id: \"{reportRequest.ReportId}\" LT:{reportRequest.AType} ({reportRequest.SelectedRowIds.LongLength}) LT:User {userName}"; - JObject o = JObject.FromObject(new - { - reportRequest = reportRequest, - requestIsCustomerWorkOrderReport = RequestIsCustomerWorkOrderReport, - apiUrl = apiUrl, - userName = userName - }); + var JobName = + $"LT:Report id: \"{reportRequest.ReportId}\" LT:{reportRequest.AType} ({reportRequest.SelectedRowIds.LongLength}) LT:User {userName}"; + JObject o = JObject.FromObject( + new + { + reportRequest = reportRequest, + requestIsCustomerWorkOrderReport = RequestIsCustomerWorkOrderReport, + apiUrl = apiUrl, + userName = userName, + } + ); OpsJob j = new OpsJob(); j.Name = JobName; @@ -469,17 +643,17 @@ namespace AyaNova.Biz j.Exclusive = false; j.JobInfo = o.ToString(); await JobsBiz.AddJobAsync(j); - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 0, AyaType.ServerJob, AyaEvent.Created, JobName), ct); + await EventLogProcessor.LogEventToDatabaseAsync( + new Event(UserId, 0, AyaType.ServerJob, AyaEvent.Created, JobName), + ct + ); return j.GId; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - } - public async Task DoRenderJob(OpsJob job) { - var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::DoRenderJob"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); @@ -489,27 +663,33 @@ namespace AyaNova.Biz log.LogDebug($"Start; rehydrate job {job.Name}"); JObject jobData = JObject.Parse(job.JobInfo); var reportRequest = jobData["reportRequest"].ToObject(); - var RequestIsCustomerWorkOrderReport = jobData["requestIsCustomerWorkOrderReport"].ToObject(); + var RequestIsCustomerWorkOrderReport = jobData["requestIsCustomerWorkOrderReport"] + .ToObject(); var apiUrl = jobData["apiUrl"].ToObject(); var userName = jobData["userName"].ToObject(); - - var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId); if (report == null) { - await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"LT:ErrorAPI2010 LT:Report({reportRequest.ReportId})\""); + await JobsBiz.LogJobAsync( + job.GId, + $"rendererror:error,\"LT:ErrorAPI2010 LT:Report({reportRequest.ReportId})\"" + ); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); return; } if (!ReportRenderManager.KeepGoing(job.GId)) return; - //Get data + //Get data log.LogDebug("Getting report data now"); // var watch = new Stopwatch(); // watch.Start(); - var ReportData = await GetReportData(reportRequest, job.GId, RequestIsCustomerWorkOrderReport); + var ReportData = await GetReportData( + reportRequest, + job.GId, + RequestIsCustomerWorkOrderReport + ); // watch.Stop(); // log.LogInformation($"GetReportData took {watch.ElapsedMilliseconds}ms to execute"); @@ -521,7 +701,10 @@ namespace AyaNova.Biz if (ReportData == null) { log.LogDebug($"bail: ReportData == null"); - await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"{this.GetErrorsAsString()}\"");//?? circle back on this one + await JobsBiz.LogJobAsync( + job.GId, + $"rendererror:error,\"{this.GetErrorsAsString()}\"" + ); //?? circle back on this one await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); } @@ -537,18 +720,22 @@ namespace AyaNova.Biz } else { - log.LogDebug($"Using user specified browser at {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH}"); + log.LogDebug( + $"Using user specified browser at {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH}" + ); AutoDownloadBrowser = false; } - var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); - + var ReportJSFolderPath = Path.Combine( + ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, + "resource", + "rpt" + ); //Keep for debugging headfully //var lo = new LaunchOptions { Headless = false }; var lo = new LaunchOptions { Headless = true }; - if (!AutoDownloadBrowser) { lo.ExecutablePath = ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH; @@ -572,17 +759,16 @@ namespace AyaNova.Biz { log.LogDebug($"Calling browserFetcher download async now:"); //await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); - + //case 4586 //puppeteer has gone chrome dev by default and so does puppeteersharp which tests ok so I'm going to allow the default //this means the files go into binary folder under Chrome and ChromeHeadlessShell await new BrowserFetcher().DownloadAsync(); } - //Set Chromium args //*** DANGER: --disable-dev-shm-usage will crash linux ayanova when it runs out of memory **** - //that was only suitable for dockerized scenario as it had an alt swap system + //that was only suitable for dockerized scenario as it had an alt swap system //var OriginalDefaultArgs = "--disable-dev-shm-usage --single-process --no-sandbox --disable-gpu --no-zygote "; @@ -593,12 +779,19 @@ namespace AyaNova.Biz if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS)) { - log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}"); - lo.Args = new string[] { $"{ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}" };// AYANOVA_REPORT_RENDER_BROWSER_PARAMS + log.LogDebug( + $"AYANOVA_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}" + ); + lo.Args = new string[] + { + $"{ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}", + }; // AYANOVA_REPORT_RENDER_BROWSER_PARAMS } else { - log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'"); + log.LogDebug( + $"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'" + ); lo.Args = new string[] { DefaultArgs }; } System.Text.StringBuilder PageLog = new System.Text.StringBuilder(); @@ -613,7 +806,7 @@ namespace AyaNova.Biz { //track this process for timeout purposes ReportRenderManager.SetProcess(job.GId, browser.Process.Id, log); - page.DefaultTimeout = 0;//infinite timeout as we are controlling how long the process can live for with the reportprocessmanager + page.DefaultTimeout = 0; //infinite timeout as we are controlling how long the process can live for with the reportprocessmanager try { //info and error logging @@ -625,7 +818,6 @@ namespace AyaNova.Biz case ConsoleType.Error: try { - //case 4586 todo force an error here and see what we can do with it old vs new behaviour test on current release same report with erorr triggered //ok, I tested it with an exception and it worked just find so shrug emoji PageLog.AppendLine($"ERROR: {args.Message.Text}"); @@ -641,7 +833,6 @@ namespace AyaNova.Biz default: PageLog.AppendLine($"INFO: {args.Message.Text}"); - break; } }; @@ -650,41 +841,64 @@ namespace AyaNova.Biz if (!ReportRenderManager.KeepGoing(job.GId)) return; - //Add Handlebars JS for compiling and presenting + //Add Handlebars JS for compiling and presenting //https://handlebarsjs.com/ - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") }); + await page.AddScriptTagAsync( + new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") } + ); //add Marked for markdown processing //https://github.com/markedjs/marked - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") }); + await page.AddScriptTagAsync( + new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") } + ); //add DOM Purify for markdown template sanitization processing //https://github.com/cure53/DOMPurify - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") }); + await page.AddScriptTagAsync( + new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") } + ); //add Bar code library if our bar code helper is referenced //https://github.com/metafloor/bwip-js if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC ")) - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-bc.js") }); + await page.AddScriptTagAsync( + new AddTagOptions() + { + Path = Path.Combine(ReportJSFolderPath, "ay-bc.js"), + } + ); //add stock helpers - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); + await page.AddScriptTagAsync( + new AddTagOptions() + { + Path = Path.Combine(ReportJSFolderPath, "ay-report.js"), + } + ); //execute to add to handlebars await page.EvaluateExpressionAsync("ayRegisterHelpers();"); if (!ReportRenderManager.KeepGoing(job.GId)) return; - log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page"); + log.LogDebug( + $"Preparing page: adding this report's scripts, style and templates to page" + ); - //add report pre-render, helpers and style + //add report pre-render, helpers and style if (string.IsNullOrWhiteSpace(report.JsPrerender)) { - report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}"; + report.JsPrerender = + "async function ayPrepareData(reportData){return reportData;}"; } - await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender }); + await page.AddScriptTagAsync( + new AddTagOptions() { Content = report.JsPrerender } + ); if (!string.IsNullOrWhiteSpace(report.JsHelpers)) - await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers }); + await page.AddScriptTagAsync( + new AddTagOptions() { Content = report.JsHelpers } + ); log.LogDebug($"Preparing page: adding Client meta data"); //Client meta data to JSON string @@ -699,12 +913,23 @@ namespace AyaNova.Biz var HasLargeLogo = "false"; if (logo != null) { - if (logo.Small != null) HasSmallLogo = "true"; - if (logo.Medium != null) HasMediumLogo = "true"; - if (logo.Large != null) HasLargeLogo = "true"; + if (logo.Small != null) + HasSmallLogo = "true"; + if (logo.Medium != null) + HasMediumLogo = "true"; + if (logo.Large != null) + HasLargeLogo = "true"; } - var HasPostalAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.PostAddress) ? "true" : "false"; - var HasStreetAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.Address) ? "true" : "false"; + var HasPostalAddress = !string.IsNullOrWhiteSpace( + ServerGlobalBizSettings.Cache.PostAddress + ) + ? "true" + : "false"; + var HasStreetAddress = !string.IsNullOrWhiteSpace( + ServerGlobalBizSettings.Cache.Address + ) + ? "true" + : "false"; //case 4209 //latitude and longitude are the only nullable fields in global biz settings and need to be converted to empty strings if null @@ -715,36 +940,47 @@ namespace AyaNova.Biz if (ServerGlobalBizSettings.Cache.Longitude != null) sLongitude = ServerGlobalBizSettings.Cache.Longitude.ToString(); - - var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `{AyaNova.Core.License.ActiveKey.RegisteredTo}`,CompanyWebAddress:`{ServerGlobalBizSettings.Cache.WebAddress}`,CompanyEmailAddress:`{ServerGlobalBizSettings.Cache.EmailAddress}`,CompanyPhone1:`{ServerGlobalBizSettings.Cache.Phone1}`,CompanyPhone2:`{ServerGlobalBizSettings.Cache.Phone2}`,HasPostalAddress:{HasPostalAddress},CompanyPostAddress:`{ServerGlobalBizSettings.Cache.PostAddress}`,CompanyPostCity:`{ServerGlobalBizSettings.Cache.PostCity}`,CompanyPostRegion:`{ServerGlobalBizSettings.Cache.PostRegion}`,CompanyPostCountry:`{ServerGlobalBizSettings.Cache.PostCountry}`,CompanyPostCode:`{ServerGlobalBizSettings.Cache.PostCode}`,HasStreetAddress:{HasStreetAddress},CompanyAddress:`{ServerGlobalBizSettings.Cache.Address}`,CompanyCity:`{ServerGlobalBizSettings.Cache.City}`,CompanyRegion:`{ServerGlobalBizSettings.Cache.Region}`,CompanyCountry:`{ServerGlobalBizSettings.Cache.Country}`,CompanyAddressPostal:`{ServerGlobalBizSettings.Cache.AddressPostal}`,CompanyLatitude:{sLatitude},CompanyLongitude:{sLongitude}}}"; + var serverMeta = + $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `{AyaNova.Core.License.ActiveKey.RegisteredTo}`,CompanyWebAddress:`{ServerGlobalBizSettings.Cache.WebAddress}`,CompanyEmailAddress:`{ServerGlobalBizSettings.Cache.EmailAddress}`,CompanyPhone1:`{ServerGlobalBizSettings.Cache.Phone1}`,CompanyPhone2:`{ServerGlobalBizSettings.Cache.Phone2}`,HasPostalAddress:{HasPostalAddress},CompanyPostAddress:`{ServerGlobalBizSettings.Cache.PostAddress}`,CompanyPostCity:`{ServerGlobalBizSettings.Cache.PostCity}`,CompanyPostRegion:`{ServerGlobalBizSettings.Cache.PostRegion}`,CompanyPostCountry:`{ServerGlobalBizSettings.Cache.PostCountry}`,CompanyPostCode:`{ServerGlobalBizSettings.Cache.PostCode}`,HasStreetAddress:{HasStreetAddress},CompanyAddress:`{ServerGlobalBizSettings.Cache.Address}`,CompanyCity:`{ServerGlobalBizSettings.Cache.City}`,CompanyRegion:`{ServerGlobalBizSettings.Cache.Region}`,CompanyCountry:`{ServerGlobalBizSettings.Cache.Country}`,CompanyAddressPostal:`{ServerGlobalBizSettings.Cache.AddressPostal}`,CompanyLatitude:{sLatitude},CompanyLongitude:{sLongitude}}}"; log.LogDebug($"Preparing page: adding Report meta data"); - //Custom fields definition for report usage + //Custom fields definition for report usage string CustomFieldsTemplate = "null"; - var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.AType.ToString()); + var FormCustomization = await ct + .FormCustom.AsNoTracking() + .SingleOrDefaultAsync(z => z.FormKey == report.AType.ToString()); if (FormCustomization != null) { CustomFieldsTemplate = FormCustomization.Template; } - //Report meta data + //Report meta data - var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,AType:`{report.AType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportRequest.DataListKey}`,SelectedRowIds: `{string.Join(",", reportRequest.SelectedRowIds)}`}}"; - + var reportMeta = + $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,AType:`{report.AType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportRequest.DataListKey}`,SelectedRowIds: `{string.Join(",", reportRequest.SelectedRowIds)}`}}"; //duplicate meta data in report page wide variable for use by our internal functions - await page.AddScriptTagAsync(new AddTagOptions() { Content = $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}" }); + await page.AddScriptTagAsync( + new AddTagOptions() + { + Content = + $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}", + } + ); if (!ReportRenderManager.KeepGoing(job.GId)) return; //prePareData / preRender - var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; + var ReportDataObject = + $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; //case 4209 - log.LogDebug($"ReportData object about to be pre-rendered is:{ReportDataObject}"); + log.LogDebug( + $"ReportData object about to be pre-rendered is:{ReportDataObject}" + ); log.LogDebug($"PageLog before render:{PageLog.ToString()}"); log.LogDebug($"Calling ayPreRender..."); @@ -756,21 +992,25 @@ namespace AyaNova.Biz //compile the template log.LogDebug($"Calling Handlebars.compile..."); - var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; + var compileScript = + $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; if (!ReportRenderManager.KeepGoing(job.GId)) return; var compiledHTML = await page.EvaluateExpressionAsync(compileScript); if (!ReportRenderManager.KeepGoing(job.GId)) return; - //render report as HTML + //render report as HTML log.LogDebug($"Setting render page content style and compiled HTML"); await page.SetContentAsync($"{compiledHTML}"); - - string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; - string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); - + string outputFileName = + StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + + ".pdf"; + string outputFullPath = System.IO.Path.Combine( + FileUtil.TemporaryFilesFolder, + outputFileName + ); //Set PDF options //https://pptr.dev/#?product=Puppeteer&version=v5.3.0&show=api-pagepdfoptions @@ -783,8 +1023,12 @@ namespace AyaNova.Biz { var ClientPDFDate = reportRequest.ClientMeta["PDFDate"].Value(); var ClientPDFTime = reportRequest.ClientMeta["PDFTime"].Value(); - PdfOptions.HeaderTemplate = report.HeaderTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); - PdfOptions.FooterTemplate = report.FooterTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); + PdfOptions.HeaderTemplate = report + .HeaderTemplate.Replace("PDFDate", ClientPDFDate) + .Replace("PDFTime", ClientPDFTime); + PdfOptions.FooterTemplate = report + .FooterTemplate.Replace("PDFDate", ClientPDFDate) + .Replace("PDFTime", ClientPDFTime); } if (report.PaperFormat != ReportPaperFormat.NotSet) @@ -855,8 +1099,13 @@ namespace AyaNova.Biz log.LogDebug($"Closing Browser"); await browser.CloseAsync(); - log.LogDebug($"Render completed successfully, output filename is: {outputFileName}, logging to job for client"); - var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { reportfilename = outputFileName }, Newtonsoft.Json.Formatting.None); + log.LogDebug( + $"Render completed successfully, output filename is: {outputFileName}, logging to job for client" + ); + var json = Newtonsoft.Json.JsonConvert.SerializeObject( + new { reportfilename = outputFileName }, + Newtonsoft.Json.Formatting.None + ); await JobsBiz.LogJobAsync(job.GId, json); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); return; @@ -868,7 +1117,9 @@ namespace AyaNova.Biz } catch (PuppeteerSharp.TargetClosedException) { - log.LogDebug("Caught PuppeteerSharp.TargetClosedException - report was cancelled by user OR timed out"); + log.LogDebug( + "Caught PuppeteerSharp.TargetClosedException - report was cancelled by user OR timed out" + ); //we closed it because the timeout hit and the CoreJobReportRenderEngineProcessCleanup job cleaned it out //so return the error the client expects in this scenario await HandleTimeOut(job, log, reportRequest, userName); @@ -882,19 +1133,38 @@ namespace AyaNova.Biz //(it might also mean other things wrong with template) if (PageLog.Length > 0) { - log.LogInformation($"Exception caught while rendering report \"{report.Name}\", report Page console log:"); + log.LogInformation( + $"Exception caught while rendering report \"{report.Name}\", report Page console log:" + ); log.LogInformation(PageLog.ToString()); - var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { pagelog = PageLog.ToString(), exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None); + var json = Newtonsoft.Json.JsonConvert.SerializeObject( + new + { + rendererror = new + { + pagelog = PageLog.ToString(), + exception = ExceptionUtil.ExtractAllExceptionMessages(ex), + }, + }, + Newtonsoft.Json.Formatting.None + ); await JobsBiz.LogJobAsync(job.GId, json); } else { - var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None); + var json = Newtonsoft.Json.JsonConvert.SerializeObject( + new + { + rendererror = new + { + exception = ExceptionUtil.ExtractAllExceptionMessages(ex), + }, + }, + Newtonsoft.Json.Formatting.None + ); await JobsBiz.LogJobAsync(job.GId, json); } - - await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); // var v=await page.GetContentAsync();//for debugging purposes @@ -913,15 +1183,34 @@ namespace AyaNova.Biz log.LogDebug($"Browser not closed in finally block, closing now"); await browser.CloseAsync(); } - log.LogDebug($"Calling ReportRenderManager.RemoveJob to stop tracking this job/process"); + log.LogDebug( + $"Calling ReportRenderManager.RemoveJob to stop tracking this job/process" + ); await ReportRenderManager.RemoveJob(job.GId, log, false); } } - static async Task HandleTimeOut(OpsJob job, ILogger log, DataListReportRequest reportRequest, string userName) + static async Task HandleTimeOut( + OpsJob job, + ILogger log, + DataListReportRequest reportRequest, + string userName + ) { - log.LogDebug($"Report render cancelled by user OR exceeded timeout setting of {ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{userName}"); - var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { timeout = true, timeoutsetting = ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT } }, Newtonsoft.Json.Formatting.None); + log.LogDebug( + $"Report render cancelled by user OR exceeded timeout setting of {ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{userName}" + ); + var json = Newtonsoft.Json.JsonConvert.SerializeObject( + new + { + rendererror = new + { + timeout = true, + timeoutsetting = ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT, + }, + }, + Newtonsoft.Json.Formatting.None + ); //"{\"rendererror\":{\"timeout\":1}}" await JobsBiz.LogJobAsync(job.GId, json); @@ -929,11 +1218,15 @@ namespace AyaNova.Biz } } - public async Task CancelJob(Guid jobId) { - await ReportRenderManager.RemoveJob(jobId, AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::CancelJob"), true); + await ReportRenderManager.RemoveJob( + jobId, + AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::CancelJob"), + true + ); } + //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // @@ -945,7 +1238,9 @@ namespace AyaNova.Biz await DoRenderJob(job); break; default: - throw new System.ArgumentOutOfRangeException($"ReportBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); + throw new System.ArgumentOutOfRangeException( + $"ReportBiz.HandleJob-> Invalid job type{job.JobType.ToString()}" + ); } } @@ -954,9 +1249,5 @@ namespace AyaNova.Biz ///////////////////////////////////////////////////////////////////// - - }//eoc - - -}//eons - + } //eoc +} //eons diff --git a/server/AyaNova/generator/CoreJobCustomerNotify.cs b/server/AyaNova/generator/CoreJobCustomerNotify.cs index a9df97e3..1e4e16bb 100644 --- a/server/AyaNova/generator/CoreJobCustomerNotify.cs +++ b/server/AyaNova/generator/CoreJobCustomerNotify.cs @@ -1,32 +1,33 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using AyaNova.Models; using AyaNova.Util; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -using System.Text.RegularExpressions; namespace AyaNova.Biz { - /// /// Notification processor /// turn notifyEvent records into inappnotification records for in app viewing and / or deliver smtp notifications seperately - /// + /// /// internal static class CoreJobCustomerNotify { private static bool NotifyIsRunning = false; - private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobCustomerNotify"); + private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger( + "CoreJobCustomerNotify" + ); private static DateTime lastRun = DateTime.MinValue; -#if (DEBUG) - private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 0, 21);//no more frequently than once every 20 seconds -#else - private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 1, 1);//no more frequently than once every 1 minute +#if (DEBUG) + private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 0, 21); //no more frequently than once every 20 seconds +#else + private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 1, 1); //no more frequently than once every 1 minute #endif //////////////////////////////////////////////////////////////////////////////////////////////// @@ -43,7 +44,9 @@ namespace AyaNova.Biz //This will get triggered roughly every minute, but we don't want to deliver that frequently if (DateTime.UtcNow - lastRun < RUN_EVERY_INTERVAL) { - log.LogTrace($"CustomerNotify ran less than {RUN_EVERY_INTERVAL} ago, exiting this cycle"); + log.LogTrace( + $"CustomerNotify ran less than {RUN_EVERY_INTERVAL} ago, exiting this cycle" + ); return; } try @@ -54,18 +57,31 @@ namespace AyaNova.Biz using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) { var customerevents = await ct.CustomerNotifyEvent.AsNoTracking().ToListAsync(); - log.LogDebug($"Found {customerevents.Count} CustomerNotifyEvents to examine for potential delivery"); + log.LogDebug( + $"Found {customerevents.Count} CustomerNotifyEvents to examine for potential delivery" + ); //iterate and deliver foreach (var customernotifyevent in customerevents) { - //no notifications for inactive users, just delete it as if it was delivered - var CustInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == customernotifyevent.CustomerId).Select(x => new { x.Name, x.Active, x.Tags, x.EmailAddress }).FirstOrDefaultAsync(); - + //no notifications for inactive users, just delete it as if it was delivered + var CustInfo = await ct + .Customer.AsNoTracking() + .Where(x => x.Id == customernotifyevent.CustomerId) + .Select(x => new + { + x.Name, + x.Active, + x.Tags, + x.EmailAddress, + }) + .FirstOrDefaultAsync(); if (!CustInfo.Active) { - log.LogDebug($"Inactive Customer {CustInfo.Name}, removing notify rather than delivering it: {customernotifyevent}"); + log.LogDebug( + $"Inactive Customer {CustInfo.Name}, removing notify rather than delivering it: {customernotifyevent}" + ); ct.CustomerNotifyEvent.Remove(customernotifyevent); await ct.SaveChangesAsync(); continue; @@ -73,29 +89,42 @@ namespace AyaNova.Biz if (string.IsNullOrWhiteSpace(CustInfo.EmailAddress)) { - log.LogDebug($"Customer {CustInfo.Name} has no email address, removing notify rather than delivering it: {customernotifyevent}"); + log.LogDebug( + $"Customer {CustInfo.Name} has no email address, removing notify rather than delivering it: {customernotifyevent}" + ); ct.CustomerNotifyEvent.Remove(customernotifyevent); await ct.SaveChangesAsync(); continue; } //Get subscription for delivery - var Subscription = await ct.CustomerNotifySubscription.AsNoTracking().FirstOrDefaultAsync(x => x.Id == customernotifyevent.CustomerNotifySubscriptionId); + var Subscription = await ct + .CustomerNotifySubscription.AsNoTracking() + .FirstOrDefaultAsync(x => + x.Id == customernotifyevent.CustomerNotifySubscriptionId + ); //NOTE: There is no need to separate out future delivery and immediate delivery because // All events have an event date, it's either immediate upon creation or it's future - // but not all events have an age value including ones with future event dates, - // and the default agevalue and advancenotice are both zero regardless so the block below works for either future or immediate deliveries + // but not all events have an age value including ones with future event dates, + // and the default agevalue and advancenotice are both zero regardless so the block below works for either future or immediate deliveries - var deliverAfter = customernotifyevent.EventDate + Subscription.AgeValue - Subscription.AdvanceNotice; + var deliverAfter = + customernotifyevent.EventDate + + Subscription.AgeValue + - Subscription.AdvanceNotice; if (deliverAfter < DateTime.UtcNow) { //Do the delivery, it's kosher - await DeliverCustomerNotificationSMTP(customernotifyevent, Subscription, CustInfo.EmailAddress, ct); + await DeliverCustomerNotificationSMTP( + customernotifyevent, + Subscription, + CustInfo.EmailAddress, + ct + ); } } } - } catch (Exception ex) { @@ -104,25 +133,28 @@ namespace AyaNova.Biz } finally { - log.LogDebug("CustomerNotify is done setting to not running state and tagging lastRun timestamp"); + log.LogDebug( + "CustomerNotify is done setting to not running state and tagging lastRun timestamp" + ); lastRun = DateTime.UtcNow; NotifyIsRunning = false; - } - } - //=== - private static async Task DeliverCustomerNotificationSMTP(CustomerNotifyEvent ne, CustomerNotifySubscription subscription, string deliveryAddress, AyContext ct) + private static async Task DeliverCustomerNotificationSMTP( + CustomerNotifyEvent ne, + CustomerNotifySubscription subscription, + string deliveryAddress, + AyContext ct + ) { - var DeliveryLogItem = new CustomerNotifyDeliveryLog() { Processed = DateTime.UtcNow, ObjectId = ne.ObjectId, CustomerNotifySubscriptionId = ne.CustomerNotifySubscriptionId, - Fail = false + Fail = false, }; try @@ -131,21 +163,25 @@ namespace AyaNova.Biz if (string.IsNullOrWhiteSpace(deliveryAddress)) { DeliveryLogItem.Fail = true; - DeliveryLogItem.Error = $"No email address provided for smtp delivery; event: {ne}"; + DeliveryLogItem.Error = + $"No email address provided for smtp delivery; event: {ne}"; } else { - if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive) { - await NotifyEventHelper.AddOpsProblemEvent($"Email notifications are set to OFF at server, unable to send Customer email notification for this event:{ne}"); - log.LogInformation($"** WARNING: SMTP notification is currently set to Active=False; unable to deliver Customer email notification, [CustomerId={ne.CustomerId}, Customer Notify subscription={ne.CustomerNotifySubscriptionId}]. Change this setting or remove all Customer notifications if this is permanent **"); + await NotifyEventHelper.AddOpsProblemEvent( + $"Email notifications are set to OFF at server, unable to send Customer email notification for this event:{ne}" + ); + log.LogInformation( + $"** WARNING: SMTP notification is currently set to Active=False; unable to deliver Customer email notification, [CustomerId={ne.CustomerId}, Customer Notify subscription={ne.CustomerNotifySubscriptionId}]. Change this setting or remove all Customer notifications if this is permanent **" + ); DeliveryLogItem.Fail = true; - DeliveryLogItem.Error = $"Email notifications are set to OFF at server, unable to send Customer email notification for this event: {ne}"; + DeliveryLogItem.Error = + $"Email notifications are set to OFF at server, unable to send Customer email notification for this event: {ne}"; } else { - //BUILD SUBJECT AND BODY FROM TOKENS IF REQUIRED var Subject = subscription.Subject; var Body = subscription.Template; @@ -157,54 +193,98 @@ namespace AyaNova.Biz { case AyaType.Quote: { - var qt = await ct.Quote.AsNoTracking().FirstOrDefaultAsync(z => z.Id == ne.ObjectId); + var qt = await ct + .Quote.AsNoTracking() + .FirstOrDefaultAsync(z => z.Id == ne.ObjectId); if (qt == null) { //maybe deleted, this can't proceed - throw new ApplicationException($"Unable to make delivery for customer notify event as Quote {ne.Name} was not found during delivery, deleted?"); + throw new ApplicationException( + $"Unable to make delivery for customer notify event as Quote {ne.Name} was not found during delivery, deleted?" + ); } - var CustomerName = await ct.Customer.AsNoTracking().Where(x => x.Id == qt.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(); + var CustomerName = await ct + .Customer.AsNoTracking() + .Where(x => x.Id == qt.CustomerId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); Subject = SetQuoteTokens(Subject, qt, CustomerName); Body = SetQuoteTokens(Body, qt, CustomerName); } break; case AyaType.WorkOrder: { - var wo = await ct.WorkOrder.AsNoTracking().FirstOrDefaultAsync(z => z.Id == ne.ObjectId); + var wo = await ct + .WorkOrder.AsNoTracking() + .FirstOrDefaultAsync(z => z.Id == ne.ObjectId); if (wo == null) { //maybe deleted, this can't proceed - throw new ApplicationException($"Unable to make delivery for customer notify event as WorkOrder {ne.Name} was not found during delivery, deleted?"); + throw new ApplicationException( + $"Unable to make delivery for customer notify event as WorkOrder {ne.Name} was not found during delivery, deleted?" + ); } - var CustomerName = await ct.Customer.AsNoTracking().Where(x => x.Id == wo.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(); - var StatusName = await ct.WorkOrderStatus.AsNoTracking().Where(x => x.Id == wo.LastStatusId).Select(x => x.Name).FirstOrDefaultAsync(); - Subject = SetWorkOrderTokens(Subject, wo, CustomerName, StatusName); - Body = SetWorkOrderTokens(Body, wo, CustomerName, StatusName); + var CustomerName = await ct + .Customer.AsNoTracking() + .Where(x => x.Id == wo.CustomerId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + var StatusName = await ct + .WorkOrderStatus.AsNoTracking() + .Where(x => x.Id == wo.LastStatusId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + Subject = SetWorkOrderTokens( + Subject, + wo, + CustomerName, + StatusName + ); + Body = SetWorkOrderTokens( + Body, + wo, + CustomerName, + StatusName + ); } break; case AyaType.CustomerServiceRequest: { - var csr = await ct.CustomerServiceRequest.AsNoTracking().FirstOrDefaultAsync(z => z.Id == ne.ObjectId); + var csr = await ct + .CustomerServiceRequest.AsNoTracking() + .FirstOrDefaultAsync(z => z.Id == ne.ObjectId); if (csr == null) { //maybe deleted, this can't proceed - throw new ApplicationException($"Unable to make delivery for customer notify event as CustomerServiceRequest {ne.Name} was not found during delivery, deleted?"); + throw new ApplicationException( + $"Unable to make delivery for customer notify event as CustomerServiceRequest {ne.Name} was not found during delivery, deleted?" + ); } - var CustomerName = await ct.Customer.AsNoTracking().Where(x => x.Id == csr.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(); - var UserName = await ct.User.AsNoTracking().Where(x => x.Id == csr.RequestedByUserId).Select(x => x.Name).FirstOrDefaultAsync(); - Subject = SetCSRTokens(Subject, csr, CustomerName, UserName); + var CustomerName = await ct + .Customer.AsNoTracking() + .Where(x => x.Id == csr.CustomerId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + var UserName = await ct + .User.AsNoTracking() + .Where(x => x.Id == csr.RequestedByUserId) + .Select(x => x.Name) + .FirstOrDefaultAsync(); + Subject = SetCSRTokens( + Subject, + csr, + CustomerName, + UserName + ); Body = SetCSRTokens(Body, csr, CustomerName, UserName); } break; } - - } - IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer; //generate report if applicable bool isReportableEvent = false; @@ -220,7 +300,12 @@ namespace AyaNova.Biz { long subTranslationId = (long)subscription.TranslationId; - ReportBiz biz = new ReportBiz(ct, 1, subTranslationId, AuthorizationRoles.BizAdmin); + ReportBiz biz = new ReportBiz( + ct, + 1, + subTranslationId, + AuthorizationRoles.BizAdmin + ); //example with workorder report //{"AType":34,"selectedRowIds":[355],"ReportId":9,"ClientMeta":{"UserName":"AyaNova SuperUser","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjQ2NzgyNTc4IiwiaXNzIjoiYXlhbm92YS5jb20iLCJpZCI6IjEifQ.ad7Acq54JCRGitDWKDJFFnqKkidbdaKaFmj-RA_RG5E","DownloadToken":"NdoU8ca3LG4L39Tj2oi3UReeeM7FLevTgbgopTPhGbA","TimeZoneName":"America/Los_Angeles","LanguageName":"en-US","Hour12":true,"CurrencyName":"USD","DefaultLocale":"en","PDFDate":"3/3/22","PDFTime":"3:38 PM"}} @@ -235,61 +320,106 @@ namespace AyaNova.Biz var pdfDate = new DateTime().ToShortDateString(); var pdfTime = new DateTime().ToShortTimeString(); var h12 = subscription.Hour12 ? "true" : "false"; - reportRequest.ClientMeta = JToken.Parse($"{{'UserName':'-','Authorization':'Bearer {jwt}','TimeZoneName':'{subscription.TimeZoneOverride}','LanguageName':'{subscription.LanguageOverride}','Hour12':{h12},'CurrencyName':'{subscription.CurrencyName}','DefaultLocale':'en','PDFDate':'{pdfDate}','PDFTime':'{pdfTime}'}}"); + reportRequest.ClientMeta = JToken.Parse( + $"{{'UserName':'-','Authorization':'Bearer {jwt}','TimeZoneName':'{subscription.TimeZoneOverride}','LanguageName':'{subscription.LanguageOverride}','Hour12':{h12},'CurrencyName':'{subscription.CurrencyName}','DefaultLocale':'en','PDFDate':'{pdfDate}','PDFTime':'{pdfTime}'}}" + ); //get port number - var match = System.Text.RegularExpressions.Regex.Match(ServerBootConfig.AYANOVA_USE_URLS, "[0-9]+"); - var API_URL = $"http://127.0.0.1:{match.Value}/api/{AyaNovaVersion.CurrentApiVersion}/"; - var jobid = await biz.RequestRenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL, "CUSTOMER NOTIFICATION - NO USER"); + var match = System.Text.RegularExpressions.Regex.Match( + ServerBootConfig.AYANOVA_USE_URLS, + "[0-9]+" + ); + var API_URL = + $"http://127.0.0.1:{match.Value}/api/{AyaNovaVersion.CurrentApiVersion}/"; + var jobid = await biz.RequestRenderReport( + reportRequest, + DateTime.UtcNow.AddMinutes( + ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT + ), + API_URL, + ServerBootConfig.CUSTOMER_NOTIFICATION_ATTACHED_REPORT_RENDER_USERNAME + ); if (jobid == null) { - throw new ApplicationException($"Report render job id is null failed to start"); + throw new ApplicationException( + $"Report render job id is null failed to start" + ); } else { bool done = false; - DateTime bailAfter = DateTime.Now.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT); + DateTime bailAfter = DateTime.Now.AddMinutes( + ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT + ); while (!done && DateTime.Now < bailAfter) { var status = await JobsBiz.GetJobStatusAsync((Guid)jobid); switch (status) { case JobStatus.Completed: - { - done = true; - //get job logs and parse file name from it - JobOperationsBiz jobopsbiz = new JobOperationsBiz(ct, 1, AuthorizationRoles.BizAdmin); - List log = await jobopsbiz.GetJobLogListAsync((Guid)jobid); - var lastLog = log[log.Count - 1]; - var lastLogJ = JObject.Parse(lastLog.StatusText); - var path = (string)lastLogJ["reportfilename"]; - var FilePath = FileUtil.GetFullPathForTemporaryFile(path); - var FileName = FileUtil.StringToSafeFileName(await TranslationBiz.GetTranslationStaticAsync(ne.AyaType.ToString(), subTranslationId, ct) + $"-{ne.Name}.pdf").ToLowerInvariant(); - await m.SendEmailAsync(deliveryAddress, Subject, Body, ServerGlobalOpsSettingsCache.Notify, FilePath, FileName); - break; - } + { + done = true; + //get job logs and parse file name from it + JobOperationsBiz jobopsbiz = new JobOperationsBiz( + ct, + 1, + AuthorizationRoles.BizAdmin + ); + List log = + await jobopsbiz.GetJobLogListAsync((Guid)jobid); + var lastLog = log[log.Count - 1]; + var lastLogJ = JObject.Parse(lastLog.StatusText); + var path = (string)lastLogJ["reportfilename"]; + var FilePath = FileUtil.GetFullPathForTemporaryFile( + path + ); + var FileName = FileUtil + .StringToSafeFileName( + await TranslationBiz.GetTranslationStaticAsync( + ne.AyaType.ToString(), + subTranslationId, + ct + ) + $"-{ne.Name}.pdf" + ) + .ToLowerInvariant(); + await m.SendEmailAsync( + deliveryAddress, + Subject, + Body, + ServerGlobalOpsSettingsCache.Notify, + FilePath, + FileName + ); + break; + } case JobStatus.Failed: case JobStatus.Absent: - throw new ApplicationException($"REPORT RENDER JOB {jobid} started but failed"); - + throw new ApplicationException( + $"REPORT RENDER JOB {jobid} started but failed" + ); } - } if (!done) - throw new TimeoutException("JOB FAILED DUE TO REPORT RENDER TIMEOUT"); + throw new TimeoutException( + "JOB FAILED DUE TO REPORT RENDER TIMEOUT" + ); } } else - await m.SendEmailAsync(deliveryAddress, Subject, Body, ServerGlobalOpsSettingsCache.Notify); - + await m.SendEmailAsync( + deliveryAddress, + Subject, + Body, + ServerGlobalOpsSettingsCache.Notify + ); } - } } catch (Exception ex) { await NotifyEventHelper.AddOpsProblemEvent("SMTP Customer Notification failed", ex); DeliveryLogItem.Fail = true; - DeliveryLogItem.Error = $"SMTP Notification failed to deliver for this Customer notify event: {ne}, message: {ex.Message}"; + DeliveryLogItem.Error = + $"SMTP Notification failed to deliver for this Customer notify event: {ne}, message: {ex.Message}"; log.LogDebug(ex, $"DeliverSMTP Failure delivering Customer notify event: {ne}"); } finally @@ -305,7 +435,11 @@ namespace AyaNova.Biz private static string SetQuoteTokens(string TheField, Quote qt, string CustomerName) { - MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + MatchCollection matches = Regex.Matches( + TheField, + @"\{{(.|\n)*?\}}", + RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled + ); //{{.*?}} foreach (Match KeyMatch in matches) { @@ -329,17 +463,24 @@ namespace AyaNova.Biz case "{{QuoteSerialNumber}}": TheField = TheField.Replace(KeyMatch.Value, qt.Serial.ToString()); break; - } } return TheField; } - - private static string SetWorkOrderTokens(string TheField, WorkOrder wo, string CustomerName, string statusName) + private static string SetWorkOrderTokens( + string TheField, + WorkOrder wo, + string CustomerName, + string statusName + ) { - MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + MatchCollection matches = Regex.Matches( + TheField, + @"\{{(.|\n)*?\}}", + RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled + ); //{{.*?}} foreach (Match KeyMatch in matches) { @@ -363,17 +504,24 @@ namespace AyaNova.Biz case "{{WorkOrderSerialNumber}}": TheField = TheField.Replace(KeyMatch.Value, wo.Serial.ToString()); break; - } } return TheField; } - - private static string SetCSRTokens(string TheField, CustomerServiceRequest csr, string CustomerName, string requestedBy) + private static string SetCSRTokens( + string TheField, + CustomerServiceRequest csr, + string CustomerName, + string requestedBy + ) { - MatchCollection matches = Regex.Matches(TheField, @"\{{(.|\n)*?\}}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + MatchCollection matches = Regex.Matches( + TheField, + @"\{{(.|\n)*?\}}", + RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled + ); //{{.*?}} foreach (Match KeyMatch in matches) { @@ -407,9 +555,5 @@ namespace AyaNova.Biz ///////////////////////////////////////////////////////////////////// - - }//eoc - - -}//eons - + } //eoc +} //eons diff --git a/server/AyaNova/util/ServerBootConfig.cs b/server/AyaNova/util/ServerBootConfig.cs index 721ea6d3..dacd7737 100644 --- a/server/AyaNova/util/ServerBootConfig.cs +++ b/server/AyaNova/util/ServerBootConfig.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.IO; using Microsoft.Extensions.Configuration; - namespace AyaNova.Util { - /// /// Contains config values from bootup /// @@ -14,23 +12,29 @@ namespace AyaNova.Util { //############################################################################################################ //STATIC HARD CODED COMPILE TIME DEFAULTS NOT SET THROUGH CONFIG - internal const int FAILED_AUTH_DELAY = 3000;//ms - internal const int JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY = 200;//ms this delay is a temporary measure to ensure super big time consuming batch jobs don't use all server CPU resources - internal const int JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS = 5;//seconds between progress updates and checks for cancellation of long running jobs - internal const int JOB_OBJECT_EMAIL_LOOP_DELAY = 500;//ms this delay ensures multiple email sendings in a job don't overwhelm the mail server + internal const int FAILED_AUTH_DELAY = 3000; //ms + internal const int JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY = 200; //ms this delay is a temporary measure to ensure super big time consuming batch jobs don't use all server CPU resources + internal const int JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS = 5; //seconds between progress updates and checks for cancellation of long running jobs + internal const int JOB_OBJECT_EMAIL_LOOP_DELAY = 500; //ms this delay ensures multiple email sendings in a job don't overwhelm the mail server //UPLOAD LIMITS 1048576 = 1MiB for testing 10737420000 10737418240 10,737,418,240 - internal const long MAX_ATTACHMENT_UPLOAD_BYTES = 10737420000;//slight bit of overage as 10737418241=10GiB - internal const long MAX_LOGO_UPLOAD_BYTES = 512000;//500KiB limit - internal const long MAX_IMPORT_FILE_UPLOAD_BYTES = 104857600;//100MiB limit - internal const long MAX_REPORT_TEMPLATE_UPLOAD_BYTES = 15728640;//15MiB limit; currently the largest v7 export for a report template is 828kb, I'm guessing 15mb is more than enough - internal const long MAX_TRANSLATION_UPLOAD_BYTES = 15728640;//15MiB limit; currently export file is 200kb * 50 maximum at a time = 15mb + internal const long MAX_ATTACHMENT_UPLOAD_BYTES = 10737420000; //slight bit of overage as 10737418241=10GiB + internal const long MAX_LOGO_UPLOAD_BYTES = 512000; //500KiB limit + internal const long MAX_IMPORT_FILE_UPLOAD_BYTES = 104857600; //100MiB limit + internal const long MAX_REPORT_TEMPLATE_UPLOAD_BYTES = 15728640; //15MiB limit; currently the largest v7 export for a report template is 828kb, I'm guessing 15mb is more than enough + internal const long MAX_TRANSLATION_UPLOAD_BYTES = 15728640; //15MiB limit; currently export file is 200kb * 50 maximum at a time = 15mb + + //case 4632 safety constant as it's now referenced in multiple places + internal const string CUSTOMER_NOTIFICATION_ATTACHED_REPORT_RENDER_USERNAME = + "CUSTOMER NOTIFICATION - NO USER"; + //############################################################################################################ //############################ //SEEDING FLAG INTERNAL ONLY //used to speed up seeding with bypasses to normal validation etc internal static bool SEEDING { get; set; } + //############################ //############################ @@ -42,17 +46,15 @@ namespace AyaNova.Util //Diagnostic static values used during development, may not be related to config at all, this is just a convenient class to put them in #if (DEBUG) internal static List TranslationKeysRequested { get; set; } - #endif - //CONTENTROOTPATH //** Not intended for end users but required in release mode internal static string AYANOVA_CONTENT_ROOT_PATH { get; set; } //Note: set in startup.cs, not in program.cs as it requires startup IHostingEnvironment - //LANGUAGE / Translation internal static string AYANOVA_DEFAULT_TRANSLATION { get; set; } + //** Not intended for end users internal static long AYANOVA_DEFAULT_TRANSLATION_ID { get; set; } //internal setting set at boot by TranslationBiz::ValidateTranslations @@ -61,9 +63,9 @@ namespace AyaNova.Util internal static string AYANOVA_USE_URLS { get; set; } internal static int AYANOVA_REPORT_RENDERING_TIMEOUT { get; set; } - //DATABASE internal static string AYANOVA_DB_CONNECTION { get; set; } + //** Not intended for end users internal static bool AYANOVA_PERMANENTLY_ERASE_DATABASE { get; set; } @@ -81,13 +83,13 @@ namespace AyaNova.Util //REPORT RENDERING BROWSER PATH (if not set then will attempt to auto-download on first render) internal static string AYANOVA_REPORT_RENDER_BROWSER_PATH { get; set; } - //REPORT RENDERING BROWSER PARAMS + + //REPORT RENDERING BROWSER PARAMS internal static string AYANOVA_REPORT_RENDER_BROWSER_PARAMS { get; set; } //REPORT RENDERING API URL OVERRIDE internal static string AYANOVA_REPORT_RENDER_API_URL_OVERRIDE { get; set; } - //LOGGING internal static string AYANOVA_LOG_PATH { get; set; } internal static string AYANOVA_LOG_LEVEL { get; set; } @@ -97,9 +99,10 @@ namespace AyaNova.Util internal static string AYANOVA_SET_SUPERUSER_PW { get; set; } //HELPFUL INFORMATION FOR DIAGNOSTICS - internal static Dictionary BOOT_DIAGNOSTIC_INFO { get; set; } = new Dictionary(); - internal static Dictionary DBSERVER_DIAGNOSTIC_INFO { get; set; } = new Dictionary(); - + internal static Dictionary BOOT_DIAGNOSTIC_INFO { get; set; } = + new Dictionary(); + internal static Dictionary DBSERVER_DIAGNOSTIC_INFO { get; set; } = + new Dictionary(); /// /// Populate the config from the configuration found at boot @@ -108,7 +111,6 @@ namespace AyaNova.Util /// internal static void SetConfiguration(IConfigurationRoot config) { - #if (DEBUG) TranslationKeysRequested = new List(); #endif @@ -121,7 +123,9 @@ namespace AyaNova.Util //LANGUAGE //TranslationBiz will validate this later at boot pfc and ensure a sane default is set (English) AYANOVA_DEFAULT_TRANSLATION = config.GetValue("AYANOVA_DEFAULT_TRANSLATION"); - AYANOVA_DEFAULT_TRANSLATION = string.IsNullOrWhiteSpace(AYANOVA_DEFAULT_TRANSLATION) ? "en" : AYANOVA_DEFAULT_TRANSLATION; + AYANOVA_DEFAULT_TRANSLATION = string.IsNullOrWhiteSpace(AYANOVA_DEFAULT_TRANSLATION) + ? "en" + : AYANOVA_DEFAULT_TRANSLATION; string lowTranslation = AYANOVA_DEFAULT_TRANSLATION.ToLowerInvariant(); switch (lowTranslation) { @@ -149,12 +153,11 @@ namespace AyaNova.Util break; } - - - //LOGLEVEL AYANOVA_LOG_LEVEL = config.GetValue("AYANOVA_LOG_LEVEL"); - AYANOVA_LOG_LEVEL = string.IsNullOrWhiteSpace(AYANOVA_LOG_LEVEL) ? "Info" : AYANOVA_LOG_LEVEL; + AYANOVA_LOG_LEVEL = string.IsNullOrWhiteSpace(AYANOVA_LOG_LEVEL) + ? "Info" + : AYANOVA_LOG_LEVEL; //LOGGING DIAGNOSTIC LOG bTemp = config.GetValue("AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG"); @@ -162,30 +165,37 @@ namespace AyaNova.Util //PORT / API AYANOVA_USE_URLS = config.GetValue("AYANOVA_USE_URLS"); - AYANOVA_USE_URLS = string.IsNullOrWhiteSpace(AYANOVA_USE_URLS) ? "http://*:7575" : AYANOVA_USE_URLS; + AYANOVA_USE_URLS = string.IsNullOrWhiteSpace(AYANOVA_USE_URLS) + ? "http://*:7575" + : AYANOVA_USE_URLS; AYANOVA_JWT_SECRET = config.GetValue("AYANOVA_JWT_SECRET"); //backdoor back door password superuser reset AYANOVA_SET_SUPERUSER_PW = config.GetValue("AYANOVA_SET_SUPERUSER_PW"); - //REPORT RENDERING + //REPORT RENDERING //RENDER OVERRIDE URL FOR CORS ISSUES BEHIND IIS (case 4398) - AYANOVA_REPORT_RENDER_API_URL_OVERRIDE = config.GetValue("AYANOVA_REPORT_RENDER_API_URL_OVERRIDE"); + AYANOVA_REPORT_RENDER_API_URL_OVERRIDE = config.GetValue( + "AYANOVA_REPORT_RENDER_API_URL_OVERRIDE" + ); - //RENDER ENGINE PATH - AYANOVA_REPORT_RENDER_BROWSER_PATH = ActualFullPath(config.GetValue("AYANOVA_REPORT_RENDER_BROWSER_PATH")); + //RENDER ENGINE PATH + AYANOVA_REPORT_RENDER_BROWSER_PATH = ActualFullPath( + config.GetValue("AYANOVA_REPORT_RENDER_BROWSER_PATH") + ); //RENDER ENGINE PARAMS - AYANOVA_REPORT_RENDER_BROWSER_PARAMS = config.GetValue("AYANOVA_REPORT_RENDER_BROWSER_PARAMS"); + AYANOVA_REPORT_RENDER_BROWSER_PARAMS = config.GetValue( + "AYANOVA_REPORT_RENDER_BROWSER_PARAMS" + ); //PROCESS CONTROL int? nTemp = config.GetValue("AYANOVA_REPORT_RENDERING_TIMEOUT"); - AYANOVA_REPORT_RENDERING_TIMEOUT = (null == nTemp) ? 5 : (int)nTemp;//default - if (AYANOVA_REPORT_RENDERING_TIMEOUT < 1) AYANOVA_REPORT_RENDERING_TIMEOUT = 1; //one minute minimum timeout - - + AYANOVA_REPORT_RENDERING_TIMEOUT = (null == nTemp) ? 5 : (int)nTemp; //default + if (AYANOVA_REPORT_RENDERING_TIMEOUT < 1) + AYANOVA_REPORT_RENDERING_TIMEOUT = 1; //one minute minimum timeout //DB AYANOVA_DB_CONNECTION = config.GetValue("AYANOVA_DB_CONNECTION"); @@ -197,41 +207,64 @@ namespace AyaNova.Util bTemp = config.GetValue("AYANOVA_REMOVE_LICENSE_FROM_DB"); AYANOVA_REMOVE_LICENSE_FROM_DB = (null == bTemp) ? false : (bool)bTemp; - //FOLDERS string DataFolderPath = ActualFullPath(config.GetValue("AYANOVA_DATA_PATH")); string LogPath = ActualFullPath(config.GetValue("AYANOVA_LOG_PATH")); - string AttachmentFilesPath = ActualFullPath(config.GetValue("AYANOVA_ATTACHMENT_FILES_PATH")); - string BackupFilesPath = ActualFullPath(config.GetValue("AYANOVA_BACKUP_FILES_PATH")); - string TempFilesPath = ActualFullPath(config.GetValue("AYANOVA_TEMP_FILES_PATH")); - AYANOVA_BACKUP_PG_DUMP_PATH = ActualFullPath(config.GetValue("AYANOVA_BACKUP_PG_DUMP_PATH")); + string AttachmentFilesPath = ActualFullPath( + config.GetValue("AYANOVA_ATTACHMENT_FILES_PATH") + ); + string BackupFilesPath = ActualFullPath( + config.GetValue("AYANOVA_BACKUP_FILES_PATH") + ); + string TempFilesPath = ActualFullPath( + config.GetValue("AYANOVA_TEMP_FILES_PATH") + ); + AYANOVA_BACKUP_PG_DUMP_PATH = ActualFullPath( + config.GetValue("AYANOVA_BACKUP_PG_DUMP_PATH") + ); if (string.IsNullOrWhiteSpace(DataFolderPath)) { //In this case *must* have paths for *everything* specified if (string.IsNullOrWhiteSpace(LogPath)) - throw new System.ArgumentNullException("AYANOVA_LOG_PATH configuration setting missing and required"); + throw new System.ArgumentNullException( + "AYANOVA_LOG_PATH configuration setting missing and required" + ); if (string.IsNullOrWhiteSpace(AttachmentFilesPath)) - throw new System.ArgumentNullException("AYANOVA_ATTACHMENT_FILES_PATH configuration setting missing and required"); + throw new System.ArgumentNullException( + "AYANOVA_ATTACHMENT_FILES_PATH configuration setting missing and required" + ); if (string.IsNullOrWhiteSpace(BackupFilesPath)) - throw new System.ArgumentNullException("AYANOVA_BACKUP_FILES_PATH configuration setting missing and required"); + throw new System.ArgumentNullException( + "AYANOVA_BACKUP_FILES_PATH configuration setting missing and required" + ); if (string.IsNullOrWhiteSpace(TempFilesPath)) - throw new System.ArgumentNullException("AYANOVA_TEMP_FILES_PATH configuration setting missing and required"); - + throw new System.ArgumentNullException( + "AYANOVA_TEMP_FILES_PATH configuration setting missing and required" + ); } //set paths - AYANOVA_LOG_PATH = (string.IsNullOrWhiteSpace(LogPath)) ? Path.Combine(DataFolderPath, "logs") : LogPath; - AYANOVA_ATTACHMENT_FILES_PATH = (string.IsNullOrWhiteSpace(AttachmentFilesPath)) ? Path.Combine(DataFolderPath, "attachments") : AttachmentFilesPath; - AYANOVA_BACKUP_FILES_PATH = (string.IsNullOrWhiteSpace(BackupFilesPath)) ? Path.Combine(DataFolderPath, "backups") : BackupFilesPath; - AYANOVA_TEMP_FILES_PATH = (string.IsNullOrWhiteSpace(TempFilesPath)) ? Path.Combine(DataFolderPath, "temp") : TempFilesPath; - + AYANOVA_LOG_PATH = + (string.IsNullOrWhiteSpace(LogPath)) + ? Path.Combine(DataFolderPath, "logs") + : LogPath; + AYANOVA_ATTACHMENT_FILES_PATH = + (string.IsNullOrWhiteSpace(AttachmentFilesPath)) + ? Path.Combine(DataFolderPath, "attachments") + : AttachmentFilesPath; + AYANOVA_BACKUP_FILES_PATH = + (string.IsNullOrWhiteSpace(BackupFilesPath)) + ? Path.Combine(DataFolderPath, "backups") + : BackupFilesPath; + AYANOVA_TEMP_FILES_PATH = + (string.IsNullOrWhiteSpace(TempFilesPath)) + ? Path.Combine(DataFolderPath, "temp") + : TempFilesPath; #endregion server BASICS - } - internal static string ActualFullPath(string p) { if (string.IsNullOrWhiteSpace(p)) @@ -245,7 +278,9 @@ namespace AyaNova.Util get { if (string.IsNullOrWhiteSpace(AYANOVA_USE_URLS)) - { return null; } + { + return null; + } if (!AYANOVA_USE_URLS.Contains(";")) { @@ -253,15 +288,7 @@ namespace AyaNova.Util } var s = AYANOVA_USE_URLS.Split(';'); return s[0].Replace("*", "localhost"); - } } - - - - - - }//eoc - - -}//eons \ No newline at end of file + } //eoc +} //eons