using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.IO; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using AyaNova.Models; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using AyaNova.Util; using PuppeteerSharp; namespace AyaNova.Api.Controllers { [ApiController] [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/report")] [Produces("application/json")] [Authorize] public class ReportController : ControllerBase { private readonly AyContext ct; private readonly ILogger log; private readonly ApiServerState serverState; /// /// ctor /// /// /// /// public ReportController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } /// /// Create Report /// /// /// From route path /// [HttpPost] public async Task PostReport([FromBody] Report newObject, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); Report o = await biz.CreateAsync(newObject); if (o == null) return BadRequest(new ApiErrorResponse(biz.Errors)); else return CreatedAtAction(nameof(ReportController.GetReport), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); } /// /// Duplicate Report /// (Wiki and Attachments are not duplicated) /// /// Source object id /// From route path /// Report [HttpPost("duplicate/{id}")] public async Task DuplicateReport([FromRoute] long id, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); Report o = await biz.DuplicateAsync(id); if (o == null) return BadRequest(new ApiErrorResponse(biz.Errors)); else return CreatedAtAction(nameof(ReportController.GetReport), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o)); } /// /// Get Report /// /// /// Report [HttpGet("{id}")] public async Task GetReport([FromRoute] long id) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); var o = await biz.GetAsync(id); if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); return Ok(ApiOkResponse.Response(o)); } /// /// Put (update) Report /// /// /// [HttpPut] public async Task PutReport([FromBody] Report updatedObject) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); var o = await biz.PutAsync(updatedObject);//In future may need to return entire object, for now just concurrency token if (o == null) { if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) return StatusCode(409, new ApiErrorResponse(biz.Errors)); else return BadRequest(new ApiErrorResponse(biz.Errors)); } return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); ; } /// /// Delete Report /// /// /// NoContent [HttpDelete("{id}")] public async Task DeleteReport([FromRoute] long id) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!await biz.DeleteAsync(id)) return BadRequest(new ApiErrorResponse(biz.Errors)); return NoContent(); } /// /// Get Report list for object /// /// Type of object /// Name / id report list of allowed reports for role of requester [HttpGet("list/{ayType}")] public async Task GetReportList([FromRoute] AyaType ayType) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); //extra check if they have rights to the type of object in question, this nips it in the bud before they even get to the fetch data stage later if (!Authorized.HasReadFullRole(HttpContext.Items, ayType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); var o = await biz.GetReportListAsync(ayType); if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); return Ok(ApiOkResponse.Response(o)); } //====================================================================================================== /// /// Get data from id list in format used by report designer /// /// Data required for report /// From route path /// [HttpPost("data")] public async Task GetReportData([FromBody] ObjectReportDataParameter reportDataParam, ApiVersion apiVersion) { /*{ public AyaType ObjectType { get; set; } public long[] SelectedRowIds { get; set; } public string DataListKey { get; set; } public string ListView { get; set; }//optional, if null or empty will use default list view built into DataList }*/ if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); // Newtonsoft.Json.Linq.JArray reportData = null; // if (reportDataParam.SelectedRowIds.Length > 0) // { // //pre-selected id values // reportData = await biz.GetReportData(reportDataParam.ObjectType, reportDataParam.SelectedRowIds); // } // else // { // //get from datalist values // //get the list of id's from the data list view // var rowIds = await AyaNova.DataList.DataListFetcher.GetIdListResponseAsync(reportDataParam.DataListKey, reportDataParam.ListView, ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items), log); // //now get the report data // reportData = await biz.GetReportData(reportDataParam.ObjectType, rowIds); // } var reportData = await biz.GetReportData(reportDataParam); if (reportData == null) return BadRequest(new ApiErrorResponse(biz.Errors)); else return Ok(ApiOkResponse.Response(reportData)); } /// /// Render Report /// /// report id and object id values for object type specified in report template /// From route path /// downloadable pdf name [HttpPost("render")] public async Task RenderReport([FromBody] RenderReportParameter reportParam, ApiVersion apiVersion) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); var httpConnectionFeature = HttpContext.Features.Get(); var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; try { var result = await biz.RenderReport(reportParam, API_URL); if (string.IsNullOrWhiteSpace(result)) return BadRequest(new ApiErrorResponse(biz.Errors)); else return Ok(ApiOkResponse.Response(result)); } catch (System.Exception ex) { //Don't send the full stack trace, just the initial error message from the javascript eval // "Evaluation failed: ReferenceError: reportdata is not defined\n at reportPreRender (:5:17)\n at ayPreRender (C:\\data\\code\\raven\\server\\AyaNova\\resource\\rpt\\ay-report.js:13:12)\n at __puppeteer_evaluation_script__:10:25" var v = ex.Message; int positionOfNewLine = v.IndexOf("\n at"); if (positionOfNewLine >= 0) v = v.Substring(0, positionOfNewLine); return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, v)); } } /// /// Download a report file /// /// /// download token /// [HttpGet("download/{fileName}")] [AllowAnonymous] public async Task DownloadAsync([FromRoute] string fileName, [FromQuery] string t) { int nFailedAuthDelay = 3000; if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (string.IsNullOrWhiteSpace(t)) { await Task.Delay(nFailedAuthDelay);//DOS protection return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } var DownloadUser = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.DlKey == t && z.Active == true); if (DownloadUser == null) { await Task.Delay(nFailedAuthDelay);//DOS protection return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero); if (DownloadUser.DlKeyExpire < utcNow.DateTime) { await Task.Delay(nFailedAuthDelay);//DOS protection return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } // if (!Authorized.HasModifyRole(DownloadUser.Roles, AyaType.Backup))//not technically modify but treating as such as a backup is very sensitive data // { // await Task.Delay(nFailedAuthDelay);//DOS protection // return StatusCode(403, new ApiNotAuthorizedResponse()); // } if (!FileUtil.TemporaryFileExists(fileName)) { await Task.Delay(nFailedAuthDelay);//fishing protection return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName); // await EventLogProcessor.LogEventToDatabaseAsync(new Event(DownloadUser.Id, 0, AyaType.NoType, AyaEvent.UtilityFileDownload, fileName), ct); return PhysicalFile(FilePath, "application/pdf"); } // [HttpGet("render-test")] // [AllowAnonymous] // public async Task GetTestReport([FromRoute] string test) // { // if (!serverState.IsOpen) // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); // string outputFile = FileUtil.NewRandomTempFilesFolderFileName; // switch (test) // { // case "chrome-reddit-to-pdf": // //first test, just render a web page to pdf and return it // //return PhysicalFile(filePath, mimetype, dbObject.DisplayFileName); // outputFile += ".pdf"; // //http://www.puppeteersharp.com/api/index.html // //https://github.com/hardkoded/puppeteer-sharp // await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); // var browser = await Puppeteer.LaunchAsync(new LaunchOptions // { // Headless = true // }); // var page = await browser.NewPageAsync(); // await page.GoToAsync("https://github.com/hardkoded/puppeteer-sharp"); // await page.PdfAsync(outputFile); // return PhysicalFile(outputFile, "application/pdf"); // } // return NotFound(test); // } //--------------------------------------------------------------------- [HttpGet("poc")] [AllowAnonymous] public async Task ProofOfConcept([FromRoute] string url) { //https://test.helloayanova.com/api/v8/report/poc //http://localhost:7575/api/v8/report/poc if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); var httpConnectionFeature = HttpContext.Features.Get(); var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; //var localIpAddress = httpConnectionFeature?.LocalIpAddress; //todo: validate files are present somehow? var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); if (!Directory.Exists(ReportJSFolderPath)) throw new System.Exception($"E1012: \"reportjs\" folder not found where expected: \"{ReportJSFolderPath}\", installation damaged?"); //sample CSS var reportCSS = @" @page { margin: 1cm; } @page :first { margin: 2cm; } .ay-red { color:red; } .ay-blue { color:blue; } "; //sample template //var reportTemplate = "'
{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
'"; var reportTemplate = "'test title " + "

Test page top

A blue paragraph

{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
" + "

Some markdown

{{{aymarkdown mdtest}}}
'"; //data object var aylogo = $"{{small:'{API_URL}logo/small',medium:'{API_URL}logo/medium',large:'{API_URL}logo/large'}}"; var reportData = "{ person: { firstname: 'Tyler', lastname: 'Mott' },aylogo:[AYLOGO], mdtest:'| CODE | MEANING |\\n| ----- | ------------------------------ |\\n| E1000 | Could not connect to the database specified in the [connection string](ops-config-db.md). |\\n| E1050 | XXXXXXXX |\\n| E1012 | Missing resource folder. AyaNova was started from the wrong location or was not installed properly. |\\n' }"; reportData = reportData.Replace("[AYLOGO]", aylogo); log.LogInformation($"setting Chrome launchoptions for os:"); var lo = new LaunchOptions { Headless = true }; bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); if (!isWindows) { log.LogInformation($"IS NOT WINDOWS: setting executable path for chrome"); 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.LogInformation($"IS WINDOWS: Calling browserFetcher download async now:"); await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); } log.LogInformation($"Calling browser.launchAsync now:"); using (var browser = await Puppeteer.LaunchAsync(lo)) using (var page = await browser.NewPageAsync()) { log.LogInformation($"In using for page ops adding scripts and report now:"); //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") }); //test add helpers await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); //execute to add to handlebars await page.EvaluateExpressionAsync("ayRegisterHelpers();"); //compile and run handlebars template var compileScript = $"Handlebars.compile({reportTemplate})({reportData});"; var resultHTML = await page.EvaluateExpressionAsync(compileScript); //render report as HTML await page.SetContentAsync(resultHTML); //add style (after page or it won't work) await page.AddStyleTagAsync(new AddTagOptions { Content = reportCSS }); //useful for debugging purposes only //var pagecontent = await page.GetContentAsync(); //render to pdf and return var pdfBuffer = await page.PdfDataAsync(); log.LogInformation($"returning results now:"); return new FileContentResult(pdfBuffer, "application/pdf"); } } [HttpPost("post-poc")] public async Task PostProofOfConcept([FromBody] NameItem nameItem) { //https://test.helloayanova.com/api/v8/report/poc //http://localhost:7575/api/v8/report/poc if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); var httpConnectionFeature = HttpContext.Features.Get(); var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; //var localIpAddress = httpConnectionFeature?.LocalIpAddress; //todo: validate files are present somehow? var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); if (!Directory.Exists(ReportJSFolderPath)) throw new System.Exception($"E1012: \"reportjs\" folder not found where expected: \"{ReportJSFolderPath}\", installation damaged?"); //sample CSS var reportCSS = @" @page { margin: 1cm; } @page :first { margin: 2cm; } .ay-red { color:red; } .ay-blue { color:blue; } "; //sample template //var reportTemplate = "'
{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
'"; var reportTemplate = "'test title " + "

Test page top

A blue paragraph

{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
" + "

Some markdown

{{{aymarkdown mdtest}}}
'"; //data object var aylogo = $"{{small:'{API_URL}logo/small',medium:'{API_URL}logo/medium',large:'{API_URL}logo/large'}}"; var reportData = "{ person: { firstname: 'Thatcher', lastname: '[NAME]' },aylogo:[AYLOGO], mdtest:'| CODE | MEANING |\\n| ----- | ------------------------------ |\\n| E1000 | Could not connect to the database specified in the [connection string](ops-config-db.md). |\\n| E1050 | XXXXXXXX |\\n| E1012 | Missing resource folder. AyaNova was started from the wrong location or was not installed properly. |\\n' }"; reportData = reportData.Replace("[AYLOGO]", aylogo); reportData = reportData.Replace("[NAME]", nameItem.Name); log.LogInformation($"setting Chrome launchoptions for os:"); var lo = new LaunchOptions { Headless = true }; bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); if (!isWindows) { log.LogInformation($"IS NOT WINDOWS: setting executable path for chrome"); 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.LogInformation($"IS WINDOWS: Calling browserFetcher download async now:"); await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); } log.LogInformation($"Calling browser.launchAsync now:"); using (var browser = await Puppeteer.LaunchAsync(lo)) using (var page = await browser.NewPageAsync()) { log.LogInformation($"In using for page ops adding scripts and report now:"); //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") }); //test add helpers await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); //execute to add to handlebars await page.EvaluateExpressionAsync("ayRegisterHelpers();"); //compile and run handlebars template var compileScript = $"Handlebars.compile({reportTemplate})({reportData});"; var resultHTML = await page.EvaluateExpressionAsync(compileScript); //render report as HTML await page.SetContentAsync(resultHTML); //add style (after page or it won't work) await page.AddStyleTagAsync(new AddTagOptions { Content = reportCSS }); 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:"); log.LogInformation($"returning results now:"); return Ok(ApiOkResponse.Response(outputFileName)); // return new FileContentResult(pdfBuffer, "application/pdf"); } } //----------------------------------------- //------------ /* NOTES/TODO during testing Need job to automatically erase any temp files older than 5 minutes (or whatever) */ }//eoc }//eons