use crate::templates::SplitTemplate; use crate::AppState; use actix_web::http::{header, StatusCode}; use actix_web::{HttpResponse, HttpResponseBuilder}; use anyhow::{bail, format_err, Context as AnyhowContext}; use async_recursion::async_recursion; use handlebars::{BlockContext, Context, JsonValue, RenderError, Renderable}; use serde::Serialize; use serde_json::{json, Value}; use std::borrow::Cow; use std::sync::Arc; pub enum PageContext { /// Indicates that we should stay in the header context Header(HeaderContext), /// Indicates that we should start rendering the body Body { http_response: HttpResponseBuilder, renderer: RenderContext, }, /// The response is ready, and should be sent as is. No further statements should be executed Close(HttpResponse), } /// Handles the first SQL statements, before the headers have been sent to pub struct HeaderContext { app_state: Arc, pub writer: W, response: HttpResponseBuilder, has_status: bool, } impl HeaderContext { pub fn new(app_state: Arc, writer: W) -> Self { let mut response = HttpResponseBuilder::new(StatusCode::OK); response.content_type("text/html; charset=utf-8"); Self { app_state, writer, response, has_status: false, } } pub async fn handle_row(self, data: JsonValue) -> anyhow::Result> { log::debug!("Handling header row: {data}"); match get_object_str(&data, "component") { Some("status_code") => self.status_code(&data).map(PageContext::Header), Some("http_header") => self.add_http_header(&data).map(PageContext::Header), Some("redirect") => self.redirect(&data).map(PageContext::Close), Some("json") => self.json(&data).map(PageContext::Close), Some("cookie") => self.add_cookie(&data).map(PageContext::Header), Some("authentication") => self.authentication(&data), _ => self.start_body(data).await, } } pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result> { log::debug!("Handling header error: {err}"); let data = json!({ "component": "error", "description": err.to_string(), "backtrace": get_backtrace(&err), }); self.start_body(data).await } fn status_code(mut self, data: &JsonValue) -> anyhow::Result { let status_code = data .as_object() .and_then(|m| m.get("status")) .with_context(|| "status_code component requires a status")? .as_u64() .with_context(|| "status must be a number")?; let code = u16::try_from(status_code) .with_context(|| format!("status must be a number between 0 and {}", u16::MAX))?; self.response.status(StatusCode::from_u16(code)?); self.has_status = true; Ok(self) } fn add_http_header(mut self, data: &JsonValue) -> anyhow::Result { let obj = data.as_object().with_context(|| "expected object")?; for (name, value) in obj { if name == "component" { continue; } let value_str = value .as_str() .with_context(|| "http header values must be strings")?; if name.eq_ignore_ascii_case("location") && !self.has_status { self.response.status(StatusCode::FOUND); self.has_status = true; } self.response.insert_header((name.as_str(), value_str)); } Ok(self) } fn add_cookie(mut self, data: &JsonValue) -> anyhow::Result { let obj = data.as_object().with_context(|| "expected object")?; let name = obj .get("name") .and_then(JsonValue::as_str) .with_context(|| "cookie name must be a string")?; let mut cookie = actix_web::cookie::Cookie::named(name); let remove = obj.get("remove"); if remove == Some(&json!(true)) || remove == Some(&json!(1)) { self.response.cookie(cookie); log::trace!("Removing cookie {}", name); return Ok(self); } let value = obj .get("value") .and_then(JsonValue::as_str) .with_context(|| "cookie value must be a string")?; cookie.set_value(value); let http_only = obj.get("http_only"); cookie.set_http_only(http_only != Some(&json!(false)) && http_only != Some(&json!(0))); let secure = obj.get("secure"); cookie.set_secure(secure != Some(&json!(false)) && secure != Some(&json!(0))); let path = obj.get("path").and_then(JsonValue::as_str); if let Some(path) = path { cookie.set_path(path); } let domain = obj.get("domain").and_then(JsonValue::as_str); if let Some(domain) = domain { cookie.set_domain(domain); } let expires = obj.get("expires").and_then(JsonValue::as_i64); if let Some(expires) = expires { cookie.set_expires(actix_web::cookie::Expiration::DateTime( actix_web::cookie::time::OffsetDateTime::from_unix_timestamp(expires)?, )); } log::trace!("Setting cookie {}", cookie); self.response .append_header((header::SET_COOKIE, cookie.encoded().to_string())); Ok(self) } fn redirect(mut self, data: &JsonValue) -> anyhow::Result { self.response.status(StatusCode::FOUND); self.has_status = true; let link = get_object_str(data, "link") .with_context(|| "The redirect component requires a 'link' property")?; self.response.insert_header((header::LOCATION, link)); let response = self.response.body(()); Ok(response) } /// Answers to the HTTP request with a single json object fn json(mut self, data: &JsonValue) -> anyhow::Result { let contents = data .get("contents") .with_context(|| "Missing 'contents' property for the json component")?; let json_response = if let Some(s) = contents.as_str() { s.as_bytes().to_owned() } else { serde_json::to_vec(contents)? }; self.response .insert_header((header::CONTENT_TYPE, "application/json")); Ok(self.response.body(json_response)) } fn authentication(mut self, data: &JsonValue) -> anyhow::Result> { use argon2::Argon2; use password_hash::PasswordHash; let password_hash = get_object_str(data, "password_hash"); let password = get_object_str(data, "password"); if let (Some(password), Some(password_hash)) = (password, password_hash) { match PasswordHash::new(password_hash) .map_err(|e| { anyhow::anyhow!("invalid value for the password_hash property: {}", e) })? .verify_password(&[&Argon2::default()], password) { Ok(()) => return Ok(PageContext::Header(self)), Err(e) => log::info!("User authentication failed: {}", e), } } log::debug!( "Authentication failed with password_hash = {:?}", password_hash ); // The authentication failed if let Some(link) = get_object_str(data, "link") { self.response.status(StatusCode::FOUND); self.response.insert_header((header::LOCATION, link)); self.has_status = true; } else { self.response.status(StatusCode::UNAUTHORIZED); self.response .insert_header((header::WWW_AUTHENTICATE, "Basic realm=\"Auth required\"")); self.has_status = true; } // Set an empty response body let http_response = self.response.body(()); Ok(PageContext::Close(http_response)) } async fn start_body(self, data: JsonValue) -> anyhow::Result> { let renderer = RenderContext::new(self.app_state, self.writer, data) .await .with_context(|| "Failed to create a render context from the header context.")?; let http_response = self.response; Ok(PageContext::Body { renderer, http_response, }) } pub fn close(mut self) -> HttpResponse { self.response.finish() } } fn get_backtrace(error: &anyhow::Error) -> Vec { let mut backtrace = vec![]; let mut source = error.source(); while let Some(s) = source { backtrace.push(format!("{s}")); source = s.source(); } backtrace } fn get_object_str<'a>(json: &'a JsonValue, key: &str) -> Option<&'a str> { json.as_object() .and_then(|obj| obj.get(key)) .and_then(JsonValue::as_str) } #[allow(clippy::module_name_repetitions)] pub struct RenderContext { app_state: Arc, pub writer: W, current_component: Option, shell_renderer: SplitTemplateRenderer, recursion_depth: usize, current_statement: usize, } const DEFAULT_COMPONENT: &str = "debug"; const SHELL_COMPONENT: &str = "shell"; const DYNAMIC_COMPONENT: &str = "dynamic"; const MAX_RECURSION_DEPTH: usize = 256; impl RenderContext { pub async fn new( app_state: Arc, mut writer: W, mut initial_row: JsonValue, ) -> anyhow::Result> { log::debug!("Creating the shell component for the page"); let mut shell_renderer = Self::create_renderer(SHELL_COMPONENT, Arc::clone(&app_state)) .await .with_context(|| "The shell component should always exist")?; let mut initial_component = Some(get_object_str(&initial_row, "component").unwrap_or(DEFAULT_COMPONENT)); let mut shell_properties = JsonValue::Null; match initial_component { Some(SHELL_COMPONENT) => { shell_properties = initial_row.take(); initial_component = None; }, Some(DYNAMIC_COMPONENT) => { let dynamic_properties = Self::extract_dynamic_properties(&initial_row)?; for prop in dynamic_properties { match get_object_str(&prop, "component") { None | Some(SHELL_COMPONENT) => { shell_properties = prop.into_owned(); initial_component = None; }, _ => bail!("Dynamic components at the top level are not supported, except for setting the shell component properties"), } } }, _ => log::trace!("The first row is not a shell component, so we will render a shell with default properties"), } log::debug!("Rendering the shell with properties: {shell_properties}"); shell_renderer.render_start(&mut writer, shell_properties)?; let mut initial_context = RenderContext { app_state, writer, current_component: None, shell_renderer, recursion_depth: 0, current_statement: 1, }; if let Some(component) = initial_component { log::trace!("The page starts with a component without a shell: {component}"); initial_context .open_component_with_data(component, &initial_row) .await?; } Ok(initial_context) } async fn current_component(&mut self) -> anyhow::Result<&mut SplitTemplateRenderer> { if self.current_component.is_none() { let _old = self.set_current_component(DEFAULT_COMPONENT).await?; } Ok(self.current_component.as_mut().unwrap()) } #[async_recursion(? Send)] pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> { log::debug!( "<- Processing database row: {}", serde_json::to_string(&data).unwrap_or_else(|e| e.to_string()) ); let new_component = get_object_str(data, "component"); let current_component = self.current_component().await?.name(); match (current_component, new_component) { (_current_component, Some(DYNAMIC_COMPONENT)) => { self.render_dynamic(data).await.with_context(|| { format!("Unable to render dynamic component with properties {data}") })?; } (_, Some("http_header")) => { bail!("The http_header component can not be used in the body of the page, only as the very first component in the page. \ The HTTP headers have already be sent for the current page, they cannot be changed now."); } (_current_component, Some(new_component)) => { self.open_component_with_data(new_component, &data).await?; } (_, _) => { self.render_current_template_with_data(&data).await?; } } Ok(()) } fn extract_dynamic_properties(data: &Value) -> anyhow::Result>> { let properties_key = "properties"; let properties_obj = data .get(properties_key) .with_context(|| format!("Missing '{properties_key}' key."))?; Ok(match properties_obj { Value::String(s) => match serde_json::from_str::(s) .with_context(|| "parsing json properties")? { Value::Array(values) => values.into_iter().map(Cow::Owned).collect(), obj @ Value::Object(_) => vec![Cow::Owned(obj)], other => bail!( "Expected properties string to parse as array or object, got {other} instead." ), }, obj @ Value::Object(_) => vec![Cow::Borrowed(obj)], Value::Array(values) => values.iter().map(Cow::Borrowed).collect(), other => bail!("Expected properties of type array or object, got {other} instead."), }) } async fn render_dynamic(&mut self, data: &Value) -> anyhow::Result<()> { anyhow::ensure!( self.recursion_depth <= MAX_RECURSION_DEPTH, "Maximum recursion depth exceeded in the dynamic component." ); for dynamic_row_obj in Self::extract_dynamic_properties(data)? { self.recursion_depth += 1; let res = self.handle_row(&dynamic_row_obj).await; self.recursion_depth -= 1; res?; } Ok(()) } #[allow(clippy::unused_async)] pub async fn finish_query(&mut self) -> anyhow::Result<()> { log::debug!("-> Query {} finished", self.current_statement); self.current_statement += 1; Ok(()) } /// Handles the rendering of an error. /// Returns whether the error is irrecoverable and the rendering must stop pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> { log::warn!("SQL error: {:?}", error); self.close_component()?; let description = error.to_string(); let data = json!({ "query_number": self.current_statement, "description": description, "backtrace": get_backtrace(error) }); let saved_component = self.open_component_with_data("error", &data).await?; self.close_component()?; self.current_component = saved_component; Ok(()) } pub async fn handle_result(&mut self, result: &anyhow::Result) -> anyhow::Result<()> { if let Err(error) = result { self.handle_error(error).await } else { Ok(()) } } pub async fn handle_result_and_log(&mut self, result: &anyhow::Result) { if let Err(e) = self.handle_result(result).await { log::error!("{}", e); } } async fn render_current_template_with_data( &mut self, data: &T, ) -> anyhow::Result<()> { if self.current_component.is_none() { self.set_current_component(DEFAULT_COMPONENT).await?; } self.current_component .as_mut() .expect("just set the current component") .render_item(&mut self.writer, json!(data))?; self.shell_renderer .render_item(&mut self.writer, JsonValue::Null)?; Ok(()) } async fn create_renderer( component: &str, app_state: Arc, ) -> anyhow::Result { let split_template = app_state .all_templates .get_template(&app_state, component) .await?; Ok(SplitTemplateRenderer::new(split_template, app_state)) } /// Set a new current component and return the old one async fn set_current_component( &mut self, component: &str, ) -> anyhow::Result> { let new_component = Self::create_renderer(component, Arc::clone(&self.app_state)).await?; Ok(self.current_component.replace(new_component)) } async fn open_component_with_data( &mut self, component: &str, data: &T, ) -> anyhow::Result> { self.close_component()?; let old_component = self.set_current_component(component).await?; self.current_component .as_mut() .expect("just set the current component") .render_start(&mut self.writer, json!(data))?; Ok(old_component) } fn close_component(&mut self) -> anyhow::Result<()> { if let Some(old_component) = self.current_component.as_mut().take() { old_component.render_end(&mut self.writer)?; } Ok(()) } pub async fn close(mut self) -> W { if let Some(old_component) = self.current_component.as_mut().take() { let res = old_component .render_end(&mut self.writer) .map_err(|e| format_err!("Unable to render the component closing: {e}")); self.handle_result_and_log(&res).await; } let res = self .shell_renderer .render_end(&mut self.writer) .map_err(|e| format_err!("Unable to render the shell closing: {e}")); self.handle_result_and_log(&res).await; self.writer } } struct HandlebarWriterOutput(W); impl handlebars::Output for HandlebarWriterOutput { fn write(&mut self, seg: &str) -> std::io::Result<()> { std::io::Write::write_all(&mut self.0, seg.as_bytes()) } } pub struct SplitTemplateRenderer { split_template: Arc, local_vars: Option, ctx: Context, app_state: Arc, row_index: usize, } impl SplitTemplateRenderer { fn new(split_template: Arc, app_state: Arc) -> Self { Self { split_template, local_vars: None, app_state, row_index: 0, ctx: Context::null(), } } fn name(&self) -> &str { self.split_template .list_content .name .as_deref() .unwrap_or_default() } fn render_start( &mut self, writer: W, data: JsonValue, ) -> Result<(), RenderError> { log::trace!( "Starting rendering of a template{} with the following top-level parameters: {data}", self.split_template .name() .map(|n| format!(" ('{n}')")) .unwrap_or_default(), ); let mut render_context = handlebars::RenderContext::new(None); *self.ctx.data_mut() = data; let mut output = HandlebarWriterOutput(writer); self.split_template.before_list.render( &self.app_state.all_templates.handlebars, &self.ctx, &mut render_context, &mut output, )?; self.local_vars = render_context .block_mut() .map(|blk| std::mem::take(blk.local_variables_mut())); self.row_index = 0; Ok(()) } fn render_item( &mut self, writer: W, data: JsonValue, ) -> Result<(), RenderError> { log::trace!("Rendering a new item in the page: {data:?}"); if let Some(local_vars) = self.local_vars.take() { let mut render_context = handlebars::RenderContext::new(None); let blk = render_context .block_mut() .expect("context created without block"); *blk.local_variables_mut() = local_vars; let mut blk = BlockContext::new(); blk.set_base_value(data); blk.set_local_var("row_index", JsonValue::Number(self.row_index.into())); render_context.push_block(blk); let mut output = HandlebarWriterOutput(writer); self.split_template.list_content.render( &self.app_state.all_templates.handlebars, &self.ctx, &mut render_context, &mut output, )?; render_context.pop_block(); self.local_vars = render_context .block_mut() .map(|blk| std::mem::take(blk.local_variables_mut())); self.row_index += 1; } Ok(()) } fn render_end(&mut self, writer: W) -> Result<(), RenderError> { log::trace!( "Closing a template {}", self.split_template .name() .map(|n| format!("('{n}')")) .unwrap_or_default(), ); if let Some(local_vars) = self.local_vars.take() { let mut render_context = handlebars::RenderContext::new(None); *render_context .block_mut() .expect("ctx created without block") .local_variables_mut() = local_vars; let mut output = HandlebarWriterOutput(writer); self.split_template.after_list.render( &self.app_state.all_templates.handlebars, &self.ctx, &mut render_context, &mut output, )?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::app_config; use crate::templates::split_template; use handlebars::Template; #[actix_web::test] async fn test_split_template_render() -> anyhow::Result<()> { let template = Template::compile( "Hello {{name}} !\ {{#each_row}} ({{x}} : {{../name}}) {{/each_row}}\ Goodbye {{name}}", )?; let split = split_template(template); let mut output = Vec::new(); let config = app_config::tests::test_config(); let app_state = Arc::new(AppState::init(&config).await.unwrap()); let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state); rdr.render_start(&mut output, json!({"name": "SQL"}))?; rdr.render_item(&mut output, json!({"x": 1}))?; rdr.render_item(&mut output, json!({"x": 2}))?; rdr.render_end(&mut output)?; assert_eq!( String::from_utf8_lossy(&output), "Hello SQL ! (1 : SQL) (2 : SQL) Goodbye SQL" ); Ok(()) } #[actix_web::test] async fn test_delayed() -> anyhow::Result<()> { let template = Template::compile( "{{#each_row}} {{x}} {{#delay}} {{x}} {{/delay}}{{/each_row}}{{flush_delayed}}", )?; let split = split_template(template); let mut output = Vec::new(); let config = app_config::tests::test_config(); let app_state = Arc::new(AppState::init(&config).await.unwrap()); let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state); rdr.render_start(&mut output, json!(null))?; rdr.render_item(&mut output, json!({"x": 1}))?; rdr.render_item(&mut output, json!({"x": 2}))?; rdr.render_end(&mut output)?; assert_eq!( String::from_utf8_lossy(&output), " 1 2 2 1 " ); Ok(()) } }