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.AttachReportId == 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 await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") }); //add Marked for markdown processing await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.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 //TODO: Add code to vet / evaluate these scripts and see if they are safe and contain valid methods expected await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender }); await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers }); await page.AddStyleTagAsync(new AddTagOptions() { Content = report.Style }); //add Client meta data var clientMeta = "{}"; if (reportParam.ClientMeta != null) clientMeta = reportParam.ClientMeta.ToString(); //add Server meta data var serverMeta=$"{{ayApiUrl:`{apiUrl}`}}"; //this is how you view the contents of the page #if (DEBUG) var pagecontent = await page.GetContentAsync(); #endif //compile and run handlebars template //var compileScript = $"Handlebars.compile(`{report.Template}`)({{ ayReportData:ayPreRender({ReportData}), ayClientMetaData:{clientMeta}, ayServerMetaData:{{ayApiUrl:`{apiUrl}`}} }});"; var compileScript = $"Handlebars.compile(`{report.Template}`)(ayPreRender({{ ayReportData:{ReportData}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}));"; var resultHTML = await page.EvaluateExpressionAsync(compileScript); //render report as HTML await page.SetContentAsync(resultHTML); //add style (after page or it won't work) if (!string.IsNullOrWhiteSpace(report.Style)) { await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style }); } //If need the generated page content //var pagecontent = await page.GetContentAsync(); string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); //render to pdf and return await page.PdfAsync(outputFullPath); log.LogDebug($"returning results now:"); return outputFileName; } catch (System.Exception ex) { // var v=await page.GetContentAsync(); throw ex; } } } // 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 ObjectReportDataParameter() { 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); // } // log.LogDebug($"Launching headless Chrome now:"); // using (var browser = await Puppeteer.LaunchAsync(lo)) // using (var page = await browser.NewPageAsync()) // { // log.LogDebug($"Preparing page: adding base reporting scripts to page"); // //Add Handlebars JS for compiling and presenting // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") }); // //add Marked for markdown processing // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.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"); // //TODO: CUSTOM HELPERS // //compile and run handlebars template // var compileScript = $"let reportData=ayPreRender({ReportData});Handlebars.compile({report.Template})(reportData);"; // var resultHTML = await page.EvaluateExpressionAsync(compileScript); // //render report as HTML // await page.SetContentAsync(resultHTML); // //add style (after page or it won't work) // if (!string.IsNullOrWhiteSpace(report.Style)) // { // await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style }); // } // //If need the generated page content // //var pagecontent = await page.GetContentAsync(); // //render to pdf and return // var pdfBuffer = await page.PdfDataAsync(); // log.LogDebug($"returning results now:"); // return new RenderedReport() { MimeType = "application/pdf", RenderedOutput = pdfBuffer }; // } // } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons