using System.Threading.Tasks; using System.Linq; using System.IO; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using EnumsNET; using PuppeteerSharp; using Newtonsoft.Json.Linq; namespace AyaNova.Biz { internal class ReportBiz : BizObject, ISearchAbleObject { internal ReportBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.Report; } 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)); else return new ReportBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Report.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreateAsync(Report newObject) { await ValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await SearchIndexAsync(newObject, true); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task DuplicateAsync(long id) { Report dbObject = await GetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } Report newObject = new Report(); CopyObject.Copy(dbObject, newObject); string newUniqueName = string.Empty; bool NotUnique = true; long l = 1; do { newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Name, l++, 255); NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName); } while (NotUnique); newObject.Name = newUniqueName; newObject.Id = 0; newObject.Concurrency = 0; await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); 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) { Report newObject = new Report(); var proposedName = (string)o["Name"]; string newUniqueName = proposedName; bool NotUnique = true; long l = 1; do { NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName); if (NotUnique) newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255); } while (NotUnique); newObject.Name = newUniqueName; newObject.Active = (bool)o["Active"]; newObject.JsHelpers = (string)o["JsHelpers"]; newObject.JsPrerender = (string)o["JsPrerender"]; newObject.Notes = (string)o["Notes"]; newObject.ObjectType = (AyaType)(int)o["ObjectType"]; newObject.RenderType = (ReportRenderType)(int)o["RenderType"]; newObject.Roles = (AuthorizationRoles)(int)o["Roles"]; newObject.Style = (string)o["Style"]; newObject.Template = (string)o["Template"]; await ValidateAsync(newObject, null); if (HasErrors) return false; await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //GET // internal async Task GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.Report.SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PutAsync(Report putObject) { Report dbObject = await ct.Report.SingleOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } Report SnapshotOfOriginalDBObj = new Report(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(putObject, dbObject, "Id"); ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); await SearchIndexAsync(dbObject, false); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task DeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { try { Report dbObject = await ct.Report.SingleOrDefaultAsync(z => z.Id == id); ValidateCanDelete(dbObject); if (HasErrors) return false; if (HasErrors) return false; ct.Report.Remove(dbObject); await ct.SaveChangesAsync(); 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(); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } return true; } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET LIST // internal async Task> GetReportListAsync(AyaType ayType) { var rpts = await ct.Report.AsNoTracking().Where(z => z.ObjectType == ayType && z.Active == true).Select(z => new { id = z.Id, name = z.Name, roles = z.Roles }).ToListAsync(); var ret = new List(); foreach (var item in rpts) { if (CurrentUserRoles.HasAnyFlags(item.roles)) { ret.Add(new NameIdItem() { Name = item.name, Id = item.id }); } } //Sort by name return ret.OrderBy(z => z.Name).ToList(); } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task SearchIndexAsync(Report obj, bool isNew) { var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); SearchParams.AddText(obj.Notes).AddText(obj.Name); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id) { var obj = await ct.Report.SingleOrDefaultAsync(z => z.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Name); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //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"); //Name must be less than 255 characters if (proposedObj.Name.Length > 255) AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max"); //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 if (await ct.Report.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id)) { AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); } } } private async void ValidateCanDelete(Report inObj) { //TODO: this is shitty, needs to mention notifications to be maximally useful if (await ct.NotifySubscription.AnyAsync(z => z.LinkReportId == inObj.Id) == true) { AddError(ApiErrorCode.INVALID_OPERATION, null, "LT:ErrorDBForeignKeyViolation"); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// //REPORT DATA //Data fetched to return to report designer for Client report design usage public async Task GetReportData(ReportDataParameter reportDataParam, AuthorizationRoles overrideRoles = AuthorizationRoles.NoRole) { var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportData"); AuthorizationRoles effectiveRoles = CurrentUserRoles; if (overrideRoles != AuthorizationRoles.NoRole) effectiveRoles = overrideRoles; if (!AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, reportDataParam.ObjectType)) { AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {reportDataParam.ObjectType} type object"); return null; } //Do we need to rehydrate the ID List from a DataList? if (reportDataParam.SelectedRowIds.Length == 0) reportDataParam.SelectedRowIds = await AyaNova.DataList.DataListFetcher.GetIdListResponseAsync(reportDataParam.DataListKey, reportDataParam.ListView, ct, effectiveRoles, log); log.LogDebug($"Instantiating biz object handler for {reportDataParam.ObjectType}"); var biz = BizObjectFactory.GetBizObject(reportDataParam.ObjectType, ct); log.LogDebug($"Fetching data for {reportDataParam.SelectedRowIds.Length} {reportDataParam.ObjectType} items"); return await ((IReportAbleObject)biz).GetReportData(reportDataParam.SelectedRowIds); } //////////////////////////////////////////////////////////////////////////////////////////////// //RENDER // public async Task RenderReport(RenderReportParameter reportParam, string apiUrl, AuthorizationRoles overrideRoles = AuthorizationRoles.NoRole) { var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::RenderReport"); //get report, vet security, see what we need before init in case of issue var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportParam.ReportId); if (report == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } AuthorizationRoles effectiveRoles = CurrentUserRoles; if (overrideRoles != AuthorizationRoles.NoRole) effectiveRoles = overrideRoles; if (!AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.ObjectType)) { AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.ObjectType} type object"); return null; } //Get data var ReportData = await GetReportData(new ReportDataParameter() { ObjectType = report.ObjectType, SelectedRowIds = reportParam.SelectedRowIds, DataListKey = reportParam.DataListKey, ListView = reportParam.ListView }); //initialization log.LogDebug("Initializing report system"); var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); var lo = new LaunchOptions { Headless = true }; bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); if (!isWindows) { log.LogDebug($"Not Windows: setting executable path for chrome to expected '/usr/bin/chromium-browser'"); lo.ExecutablePath = "/usr/bin/chromium-browser";//this is the default path for docker based alpine dist, but maybe not others, need to make a config setting likely lo.Args = new string[] { "--no-sandbox" }; } else { log.LogDebug($"Windows: Calling browserFetcher download async now:"); await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); } //https://stackoverflow.com/questions/53367966/puppeteer-sharp-still-appear-many-chromium-instance-in-process-task-manager-when log.LogDebug($"Launching headless Chrome now:"); using (var browser = await Puppeteer.LaunchAsync(lo)) using (var page = await browser.NewPageAsync()) { try { log.LogDebug($"Preparing page: adding base reporting scripts to page"); //Add Handlebars JS for compiling and presenting //https://handlebarsjs.com/ 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") }); //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") }); //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") }); //add stock helpers await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); //execute to add to handlebars await page.EvaluateExpressionAsync("ayRegisterHelpers();"); log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page"); //add report pre-render, helpers and style if (string.IsNullOrWhiteSpace(report.JsPrerender)) { report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}"; } await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender }); if (!string.IsNullOrWhiteSpace(report.JsHelpers)) await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers }); if (!string.IsNullOrWhiteSpace(report.Style)) await page.AddStyleTagAsync(new AddTagOptions() { Content = report.Style }); log.LogDebug($"Preparing page: adding Client meta data"); //Client meta data var clientMeta = "{}"; if (reportParam.ClientMeta != null) clientMeta = reportParam.ClientMeta.ToString(); log.LogDebug($"Preparing page: adding Server meta data"); //Server meta data var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync(); var HasSmallLogo = "false"; var HasMediumLogo = "false"; var HasLargeLogo = "false"; if (logo != null) { if (logo.Small != null) HasSmallLogo = "true"; if (logo.Medium != null) HasMediumLogo = "true"; if (logo.Large != null) HasLargeLogo = "true"; } var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},}}"; log.LogDebug($"Preparing page: adding Report meta data"); //Custom fields definition for report usage string CustomFieldsTemplate = "null"; var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.ObjectType.ToString()); if (FormCustomization != null) { CustomFieldsTemplate = FormCustomization.Template; } //Report meta data var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,ObjectType:`{report.ObjectType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportParam.DataListKey}`,ListView:`{reportParam.ListView}`,SelectedRowIds: `{string.Join(",", reportParam.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} }}" }); #if (DEBUG) //view page contents var pagecontent = await page.GetContentAsync(); #endif //prePareData / preRender var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; log.LogDebug($"Calling ayPreRender..."); var PreParedReportDataObject = await page.EvaluateExpressionAsync($"ayPreRender({ReportDataObject});");//note ayPreRender is async but dont' use await to call it as the EvaluateExpressionAsync function knows how to handle that already //compile the template log.LogDebug($"Calling Handlebars.compile..."); var compileScript = $"Handlebars.compile(`{report.Template}`)({PreParedReportDataObject});"; var compiledHTML = await page.EvaluateExpressionAsync(compileScript); //render report as HTML log.LogDebug($"Setting page content to compiled HTML"); await page.SetContentAsync(compiledHTML); //add style (after page or it won't work) if (!string.IsNullOrWhiteSpace(report.Style)) { log.LogDebug($"Adding report template Style CSS"); await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style }); } string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); //render to pdf and return log.LogDebug($"Calling render page contents to PDF"); await page.PdfAsync(outputFullPath); log.LogDebug($"Completed, returning results"); return outputFileName; } catch (System.Exception ex) { //This is the error when a helper is used on the template but doesn't exist: //Evaluation failed: d //(it might also mean other things wrong with template) // var v=await page.GetContentAsync(); throw ex; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons