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 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(); } //====================================================================================================== public class ObjectReportDataParameter { public AyaType ObjectType { get; set; } public long[] ObjectIdArray { get; set; } } /// /// Get data from id list in format used by report designer /// /// report id and object id values for object type specified in report template /// From route path /// [HttpPost("object-report-data")] public async Task GetReportData([FromBody] ObjectReportDataParameter reportDataParam, ApiVersion apiVersion) { 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)); var reportData = await biz.GetReportData(reportDataParam.ObjectType, reportDataParam.ObjectIdArray); 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 /// [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/"; var result = await biz.RenderReport(reportParam.ReportId, reportParam.ObjectIdArray, API_URL); if (result == null) return BadRequest(new ApiErrorResponse(biz.Errors)); else return new FileContentResult(result.RenderedOutput, result.MimeType); } public class RenderReportParameter { public long ReportId { get; set; } public long[] ObjectIdArray { get; set; } } [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"); } } //----------------------------------------- //------------ /* NOTES/TODO during testing Need job to automatically erase any temp files older than 5 minutes (or whatever) */ }//eoc }//eons