1use crate::templates::SplitTemplate;
45use crate::webserver::http::RequestContext;
46use crate::webserver::response_writer::{AsyncResponseWriter, ResponseWriter};
47use crate::webserver::ErrorWithStatus;
48use crate::AppState;
49use actix_web::cookie::time::format_description::well_known::Rfc3339;
50use actix_web::cookie::time::OffsetDateTime;
51use actix_web::http::{header, StatusCode};
52use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
53use anyhow::{bail, format_err, Context as AnyhowContext};
54use awc::cookie::time::Duration;
55use handlebars::{BlockContext, Context, JsonValue, RenderError, Renderable};
56use serde::Serialize;
57use serde_json::{json, Value};
58use std::borrow::Cow;
59use std::convert::TryFrom;
60use std::io::Write;
61use std::sync::Arc;
62
63pub enum PageContext {
64 Header(HeaderContext),
66
67 Body {
69 http_response: HttpResponseBuilder,
70 renderer: AnyRenderBodyContext,
71 },
72
73 Close(HttpResponse),
75}
76
77pub struct HeaderContext {
79 app_state: Arc<AppState>,
80 request_context: RequestContext,
81 pub writer: ResponseWriter,
82 response: HttpResponseBuilder,
83 has_status: bool,
84}
85
86impl HeaderContext {
87 #[must_use]
88 pub fn new(
89 app_state: Arc<AppState>,
90 request_context: RequestContext,
91 writer: ResponseWriter,
92 ) -> Self {
93 let mut response = HttpResponseBuilder::new(StatusCode::OK);
94 response.content_type("text/html; charset=utf-8");
95 if app_state.config.content_security_policy.is_none() {
96 response.insert_header(&request_context.content_security_policy);
97 }
98 Self {
99 app_state,
100 request_context,
101 writer,
102 response,
103 has_status: false,
104 }
105 }
106 pub async fn handle_row(self, data: JsonValue) -> anyhow::Result<PageContext> {
107 log::debug!("Handling header row: {data}");
108 let comp_opt =
109 get_object_str(&data, "component").and_then(|s| HeaderComponent::try_from(s).ok());
110 match comp_opt {
111 Some(HeaderComponent::StatusCode) => self.status_code(&data).map(PageContext::Header),
112 Some(HeaderComponent::HttpHeader) => {
113 self.add_http_header(&data).map(PageContext::Header)
114 }
115 Some(HeaderComponent::Redirect) => self.redirect(&data).map(PageContext::Close),
116 Some(HeaderComponent::Json) => self.json(&data),
117 Some(HeaderComponent::Csv) => self.csv(&data).await,
118 Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
119 Some(HeaderComponent::Authentication) => self.authentication(data).await,
120 None => self.start_body(data).await,
121 }
122 }
123
124 pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result<PageContext> {
125 if self.app_state.config.environment.is_prod() {
126 return Err(err);
127 }
128 log::debug!("Handling header error: {err}");
129 let data = json!({
130 "component": "error",
131 "description": err.to_string(),
132 "backtrace": get_backtrace(&err),
133 });
134 self.start_body(data).await
135 }
136
137 fn status_code(mut self, data: &JsonValue) -> anyhow::Result<Self> {
138 let status_code = data
139 .as_object()
140 .and_then(|m| m.get("status"))
141 .with_context(|| "status_code component requires a status")?
142 .as_u64()
143 .with_context(|| "status must be a number")?;
144 let code = u16::try_from(status_code)
145 .with_context(|| format!("status must be a number between 0 and {}", u16::MAX))?;
146 self.response.status(StatusCode::from_u16(code)?);
147 self.has_status = true;
148 Ok(self)
149 }
150
151 fn add_http_header(mut self, data: &JsonValue) -> anyhow::Result<Self> {
152 let obj = data.as_object().with_context(|| "expected object")?;
153 for (name, value) in obj {
154 if name == "component" {
155 continue;
156 }
157 let value_str = value
158 .as_str()
159 .with_context(|| "http header values must be strings")?;
160 if name.eq_ignore_ascii_case("location") && !self.has_status {
161 self.response.status(StatusCode::FOUND);
162 self.has_status = true;
163 }
164 self.response.insert_header((name.as_str(), value_str));
165 }
166 Ok(self)
167 }
168
169 fn add_cookie(mut self, data: &JsonValue) -> anyhow::Result<Self> {
170 let obj = data.as_object().with_context(|| "expected object")?;
171 let name = obj
172 .get("name")
173 .and_then(JsonValue::as_str)
174 .with_context(|| "cookie name must be a string")?;
175 let mut cookie = actix_web::cookie::Cookie::named(name);
176
177 let path = obj.get("path").and_then(JsonValue::as_str);
178 if let Some(path) = path {
179 cookie.set_path(path);
180 } else {
181 cookie.set_path("/");
182 }
183 let domain = obj.get("domain").and_then(JsonValue::as_str);
184 if let Some(domain) = domain {
185 cookie.set_domain(domain);
186 }
187
188 let remove = obj.get("remove");
189 if remove == Some(&json!(true)) || remove == Some(&json!(1)) {
190 cookie.make_removal();
191 self.response.cookie(cookie);
192 log::trace!("Removing cookie {}", name);
193 return Ok(self);
194 }
195
196 let value = obj
197 .get("value")
198 .and_then(JsonValue::as_str)
199 .with_context(|| "The 'value' property of the cookie component is required (unless 'remove' is set) and must be a string.")?;
200 cookie.set_value(value);
201 let http_only = obj.get("http_only");
202 cookie.set_http_only(http_only != Some(&json!(false)) && http_only != Some(&json!(0)));
203 let same_site = obj.get("same_site").and_then(Value::as_str);
204 cookie.set_same_site(match same_site {
205 Some("none") => actix_web::cookie::SameSite::None,
206 Some("lax") => actix_web::cookie::SameSite::Lax,
207 None | Some("strict") => actix_web::cookie::SameSite::Strict, Some(other) => bail!("Cookie: invalid value for same_site: {}", other),
209 });
210 let secure = obj.get("secure");
211 cookie.set_secure(secure != Some(&json!(false)) && secure != Some(&json!(0)));
212 if let Some(max_age_json) = obj.get("max_age") {
213 let seconds = max_age_json
214 .as_i64()
215 .ok_or_else(|| anyhow::anyhow!("max_age must be a number, not {max_age_json}"))?;
216 cookie.set_max_age(Duration::seconds(seconds));
217 }
218 let expires = obj.get("expires");
219 if let Some(expires) = expires {
220 cookie.set_expires(actix_web::cookie::Expiration::DateTime(match expires {
221 JsonValue::String(s) => OffsetDateTime::parse(s, &Rfc3339)?,
222 JsonValue::Number(n) => OffsetDateTime::from_unix_timestamp(
223 n.as_i64().with_context(|| "expires must be a timestamp")?,
224 )?,
225 _ => bail!("expires must be a string or a number"),
226 }));
227 }
228 log::trace!("Setting cookie {}", cookie);
229 self.response
230 .append_header((header::SET_COOKIE, cookie.encoded().to_string()));
231 Ok(self)
232 }
233
234 fn redirect(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
235 self.response.status(StatusCode::FOUND);
236 self.has_status = true;
237 let link = get_object_str(data, "link")
238 .with_context(|| "The redirect component requires a 'link' property")?;
239 self.response.insert_header((header::LOCATION, link));
240 let response = self.response.body(());
241 Ok(response)
242 }
243
244 fn json(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
246 self.response
247 .insert_header((header::CONTENT_TYPE, "application/json"));
248 if let Some(contents) = data.get("contents") {
249 let json_response = if let Some(s) = contents.as_str() {
250 s.as_bytes().to_owned()
251 } else {
252 serde_json::to_vec(contents)?
253 };
254 Ok(PageContext::Close(self.response.body(json_response)))
255 } else {
256 let body_type = get_object_str(data, "type");
257 let json_renderer = match body_type {
258 None | Some("array") => JsonBodyRenderer::new_array(self.writer),
259 Some("jsonlines") => JsonBodyRenderer::new_jsonlines(self.writer),
260 Some("sse") => {
261 self.response
262 .insert_header((header::CONTENT_TYPE, "text/event-stream"));
263 JsonBodyRenderer::new_server_sent_events(self.writer)
264 }
265 _ => bail!(
266 "Invalid value for the 'type' property of the json component: {body_type:?}"
267 ),
268 };
269 let renderer = AnyRenderBodyContext::Json(json_renderer);
270 let http_response = self.response;
271 Ok(PageContext::Body {
272 http_response,
273 renderer,
274 })
275 }
276 }
277
278 async fn csv(mut self, options: &JsonValue) -> anyhow::Result<PageContext> {
279 self.response
280 .insert_header((header::CONTENT_TYPE, "text/csv; charset=utf-8"));
281 if let Some(filename) =
282 get_object_str(options, "filename").or_else(|| get_object_str(options, "title"))
283 {
284 let extension = if filename.contains('.') { "" } else { ".csv" };
285 self.response.insert_header((
286 header::CONTENT_DISPOSITION,
287 format!("attachment; filename={filename}{extension}"),
288 ));
289 }
290 let csv_renderer = CsvBodyRenderer::new(self.writer, options).await?;
291 let renderer = AnyRenderBodyContext::Csv(csv_renderer);
292 let http_response = self.response.take();
293 Ok(PageContext::Body {
294 renderer,
295 http_response,
296 })
297 }
298
299 async fn authentication(mut self, mut data: JsonValue) -> anyhow::Result<PageContext> {
300 let password_hash = take_object_str(&mut data, "password_hash");
301 let password = take_object_str(&mut data, "password");
302 if let (Some(password), Some(password_hash)) = (password, password_hash) {
303 log::debug!("Authentication with password_hash = {:?}", password_hash);
304 match verify_password_async(password_hash, password).await? {
305 Ok(()) => return Ok(PageContext::Header(self)),
306 Err(e) => log::info!("Password didn't match: {}", e),
307 }
308 }
309 log::debug!("Authentication failed");
310 let http_response: HttpResponse = if let Some(link) = get_object_str(&data, "link") {
312 self.response
313 .status(StatusCode::FOUND)
314 .insert_header((header::LOCATION, link))
315 .body(
316 "Sorry, but you are not authorized to access this page. \
317 Redirecting to the login page...",
318 )
319 } else {
320 ErrorWithStatus {
321 status: StatusCode::UNAUTHORIZED,
322 }
323 .error_response()
324 };
325 self.has_status = true;
326 Ok(PageContext::Close(http_response))
327 }
328
329 async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
330 let html_renderer =
331 HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
332 .await
333 .with_context(|| "Failed to create a render context from the header context.")?;
334 let renderer = AnyRenderBodyContext::Html(html_renderer);
335 let http_response = self.response;
336 Ok(PageContext::Body {
337 renderer,
338 http_response,
339 })
340 }
341
342 pub fn close(mut self) -> HttpResponse {
343 self.response.finish()
344 }
345}
346
347async fn verify_password_async(
348 password_hash: String,
349 password: String,
350) -> Result<Result<(), password_hash::Error>, anyhow::Error> {
351 tokio::task::spawn_blocking(move || {
352 let hash = password_hash::PasswordHash::new(&password_hash)
353 .map_err(|e| anyhow::anyhow!("invalid value for the password_hash property: {}", e))?;
354 let phfs = &[&argon2::Argon2::default() as &dyn password_hash::PasswordVerifier];
355 Ok(hash.verify_password(phfs, password))
356 })
357 .await?
358}
359
360fn get_backtrace(error: &anyhow::Error) -> Vec<String> {
361 let mut backtrace = vec![];
362 let mut source = error.source();
363 while let Some(s) = source {
364 backtrace.push(format!("{s}"));
365 source = s.source();
366 }
367 backtrace
368}
369
370fn get_object_str<'a>(json: &'a JsonValue, key: &str) -> Option<&'a str> {
371 json.as_object()
372 .and_then(|obj| obj.get(key))
373 .and_then(JsonValue::as_str)
374}
375
376fn take_object_str(json: &mut JsonValue, key: &str) -> Option<String> {
377 match json.get_mut(key)?.take() {
378 JsonValue::String(s) => Some(s),
379 _ => None,
380 }
381}
382
383pub enum AnyRenderBodyContext {
387 Html(HtmlRenderContext<ResponseWriter>),
388 Json(JsonBodyRenderer<ResponseWriter>),
389 Csv(CsvBodyRenderer),
390}
391
392impl AnyRenderBodyContext {
396 pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
397 log::debug!(
398 "<- Rendering properties: {}",
399 serde_json::to_string(&data).unwrap_or_else(|e| e.to_string())
400 );
401 match self {
402 AnyRenderBodyContext::Html(render_context) => render_context.handle_row(data).await,
403 AnyRenderBodyContext::Json(json_body_renderer) => json_body_renderer.handle_row(data),
404 AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.handle_row(data).await,
405 }
406 }
407 pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
408 log::error!("SQL error: {:?}", error);
409 match self {
410 AnyRenderBodyContext::Html(render_context) => render_context.handle_error(error).await,
411 AnyRenderBodyContext::Json(json_body_renderer) => {
412 json_body_renderer.handle_error(error)
413 }
414 AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.handle_error(error).await,
415 }
416 }
417 pub async fn finish_query(&mut self) -> anyhow::Result<()> {
418 match self {
419 AnyRenderBodyContext::Html(render_context) => render_context.finish_query().await,
420 AnyRenderBodyContext::Json(_json_body_renderer) => Ok(()),
421 AnyRenderBodyContext::Csv(_csv_renderer) => Ok(()),
422 }
423 }
424
425 pub async fn flush(&mut self) -> anyhow::Result<()> {
426 match self {
427 AnyRenderBodyContext::Html(HtmlRenderContext { writer, .. })
428 | AnyRenderBodyContext::Json(JsonBodyRenderer { writer, .. }) => {
429 writer.async_flush().await?;
430 }
431 AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.flush().await?,
432 }
433 Ok(())
434 }
435
436 pub async fn close(self) -> ResponseWriter {
437 match self {
438 AnyRenderBodyContext::Html(render_context) => render_context.close().await,
439 AnyRenderBodyContext::Json(json_body_renderer) => json_body_renderer.close(),
440 AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.close().await,
441 }
442 }
443}
444
445pub struct JsonBodyRenderer<W: std::io::Write> {
446 writer: W,
447 is_first: bool,
448 prefix: &'static [u8],
449 suffix: &'static [u8],
450 separator: &'static [u8],
451}
452
453impl<W: std::io::Write> JsonBodyRenderer<W> {
454 pub fn new_array(writer: W) -> JsonBodyRenderer<W> {
455 let mut renderer = Self {
456 writer,
457 is_first: true,
458 prefix: b"[\n",
459 suffix: b"\n]",
460 separator: b",\n",
461 };
462 let _ = renderer.write_prefix();
463 renderer
464 }
465 pub fn new_jsonlines(writer: W) -> JsonBodyRenderer<W> {
466 let mut renderer = Self {
467 writer,
468 is_first: true,
469 prefix: b"",
470 suffix: b"",
471 separator: b"\n",
472 };
473 renderer.write_prefix().unwrap();
474 renderer
475 }
476 pub fn new_server_sent_events(writer: W) -> JsonBodyRenderer<W> {
477 let mut renderer = Self {
478 writer,
479 is_first: true,
480 prefix: b"data: ",
481 suffix: b"\n\n",
482 separator: b"\n\ndata: ",
483 };
484 renderer.write_prefix().unwrap();
485 renderer
486 }
487 fn write_prefix(&mut self) -> anyhow::Result<()> {
488 self.writer.write_all(self.prefix)?;
489 Ok(())
490 }
491 pub fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
492 if self.is_first {
493 self.is_first = false;
494 } else {
495 let _ = self.writer.write_all(self.separator);
496 }
497 serde_json::to_writer(&mut self.writer, data)?;
498 Ok(())
499 }
500 pub fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
501 self.handle_row(&json!({
502 "error": error.to_string()
503 }))
504 }
505
506 pub fn close(mut self) -> W {
507 let _ = self.writer.write_all(self.suffix);
508 self.writer
509 }
510}
511
512pub struct CsvBodyRenderer {
513 writer: csv_async::AsyncWriter<AsyncResponseWriter>,
514 columns: Vec<String>,
515}
516
517impl CsvBodyRenderer {
518 pub async fn new(
519 mut writer: ResponseWriter,
520 options: &JsonValue,
521 ) -> anyhow::Result<CsvBodyRenderer> {
522 let mut builder = csv_async::AsyncWriterBuilder::new();
523 if let Some(separator) = get_object_str(options, "separator") {
524 let &[separator_byte] = separator.as_bytes() else {
525 bail!("Invalid csv separator: {separator:?}. It must be a single byte.");
526 };
527 builder.delimiter(separator_byte);
528 }
529 if let Some(quote) = get_object_str(options, "quote") {
530 let &[quote_byte] = quote.as_bytes() else {
531 bail!("Invalid csv quote: {quote:?}. It must be a single byte.");
532 };
533 builder.quote(quote_byte);
534 }
535 if let Some(escape) = get_object_str(options, "escape") {
536 let &[escape_byte] = escape.as_bytes() else {
537 bail!("Invalid csv escape: {escape:?}. It must be a single byte.");
538 };
539 builder.escape(escape_byte);
540 }
541 if options
542 .get("bom")
543 .and_then(JsonValue::as_bool)
544 .unwrap_or(false)
545 {
546 let utf8_bom = b"\xEF\xBB\xBF";
547 writer.write_all(utf8_bom)?;
548 }
549 let mut async_writer = AsyncResponseWriter::new(writer);
550 tokio::io::AsyncWriteExt::flush(&mut async_writer).await?;
551 let writer = builder.create_writer(async_writer);
552 Ok(CsvBodyRenderer {
553 writer,
554 columns: vec![],
555 })
556 }
557
558 pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
559 if self.columns.is_empty() {
560 if let Some(obj) = data.as_object() {
561 let headers: Vec<String> = obj.keys().map(String::to_owned).collect();
562 self.columns = headers;
563 self.writer.write_record(&self.columns).await?;
564 }
565 }
566
567 if let Some(obj) = data.as_object() {
568 let col2bytes = |s| {
569 let val = obj.get(s);
570 let Some(val) = val else {
571 return Cow::Borrowed(&b""[..]);
572 };
573 if let Some(s) = val.as_str() {
574 Cow::Borrowed(s.as_bytes())
575 } else {
576 Cow::Owned(val.to_string().into_bytes())
577 }
578 };
579 let record = self.columns.iter().map(col2bytes);
580 self.writer.write_record(record).await?;
581 }
582
583 Ok(())
584 }
585
586 pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
587 let err_str = error.to_string();
588 self.writer
589 .write_record(
590 self.columns
591 .iter()
592 .enumerate()
593 .map(|(i, _)| if i == 0 { &err_str } else { "" })
594 .collect::<Vec<_>>(),
595 )
596 .await?;
597 Ok(())
598 }
599
600 pub async fn flush(&mut self) -> anyhow::Result<()> {
601 self.writer.flush().await?;
602 Ok(())
603 }
604
605 pub async fn close(self) -> ResponseWriter {
606 self.writer
607 .into_inner()
608 .await
609 .expect("Failed to get inner writer")
610 .into_inner()
611 }
612}
613
614#[allow(clippy::module_name_repetitions)]
615pub struct HtmlRenderContext<W: std::io::Write> {
616 app_state: Arc<AppState>,
617 pub writer: W,
618 current_component: Option<SplitTemplateRenderer>,
619 shell_renderer: SplitTemplateRenderer,
620 current_statement: usize,
621 request_context: RequestContext,
622}
623
624const DEFAULT_COMPONENT: &str = "table";
625const PAGE_SHELL_COMPONENT: &str = "shell";
626const FRAGMENT_SHELL_COMPONENT: &str = "shell-empty";
627
628impl<W: std::io::Write> HtmlRenderContext<W> {
629 pub async fn new(
630 app_state: Arc<AppState>,
631 request_context: RequestContext,
632 mut writer: W,
633 initial_row: JsonValue,
634 ) -> anyhow::Result<HtmlRenderContext<W>> {
635 log::debug!("Creating the shell component for the page");
636
637 let mut initial_rows = vec![Cow::Borrowed(&initial_row)];
638
639 if !initial_rows
640 .first()
641 .and_then(|c| get_object_str(c, "component"))
642 .is_some_and(Self::is_shell_component)
643 {
644 let default_shell = if request_context.is_embedded {
645 FRAGMENT_SHELL_COMPONENT
646 } else {
647 PAGE_SHELL_COMPONENT
648 };
649 let added_row = json!({"component": default_shell});
650 log::trace!(
651 "No shell component found in the first row. Adding the default shell: {added_row}"
652 );
653 initial_rows.insert(0, Cow::Owned(added_row));
654 }
655 let mut rows_iter = initial_rows.into_iter().map(Cow::into_owned);
656
657 let shell_row = rows_iter
658 .next()
659 .expect("shell row should exist at this point");
660 let mut shell_component =
661 get_object_str(&shell_row, "component").expect("shell should exist");
662 if request_context.is_embedded && shell_component != FRAGMENT_SHELL_COMPONENT {
663 log::warn!(
664 "Embedded pages cannot use a shell component! Ignoring the '{shell_component}' component and its properties: {shell_row}"
665 );
666 shell_component = FRAGMENT_SHELL_COMPONENT;
667 }
668 let mut shell_renderer = Self::create_renderer(
669 shell_component,
670 Arc::clone(&app_state),
671 0,
672 request_context.content_security_policy.nonce,
673 )
674 .await
675 .with_context(|| "The shell component should always exist")?;
676 log::debug!("Rendering the shell with properties: {shell_row}");
677 shell_renderer.render_start(&mut writer, shell_row)?;
678
679 let mut initial_context = HtmlRenderContext {
680 app_state,
681 writer,
682 current_component: None,
683 shell_renderer,
684 current_statement: 1,
685 request_context,
686 };
687
688 for row in rows_iter {
689 initial_context.handle_row(&row).await?;
690 }
691
692 Ok(initial_context)
693 }
694
695 fn is_shell_component(component: &str) -> bool {
696 component.starts_with(PAGE_SHELL_COMPONENT)
697 }
698
699 pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
700 let new_component = get_object_str(data, "component");
701 let current_component = self
702 .current_component
703 .as_ref()
704 .map(SplitTemplateRenderer::name);
705 if let Some(comp_str) = new_component {
706 if Self::is_shell_component(comp_str) {
707 bail!("There cannot be more than a single shell per page. You are trying to open the {} component, but a shell component is already opened for the current page. You can fix this by removing the extra shell component, or by moving this component to the top of the SQL file, before any other component that displays data.", comp_str);
708 }
709
710 match self.open_component_with_data(comp_str, &data).await {
711 Ok(_) => (),
712 Err(err) => match HeaderComponent::try_from(comp_str) {
713 Ok(_) => bail!("The {comp_str} component cannot be used after data has already been sent to the client's browser. \n\
714 This component must be used before any other component. \n\
715 To fix this, either move the call to the '{comp_str}' component to the top of the SQL file, \n\
716 or create a new SQL file where '{comp_str}' is the first component."),
717 Err(()) => return Err(err),
718 },
719 }
720 } else if current_component.is_none() {
721 self.open_component_with_data(DEFAULT_COMPONENT, &JsonValue::Null)
722 .await?;
723 self.render_current_template_with_data(&data).await?;
724 } else {
725 self.render_current_template_with_data(&data).await?;
726 }
727 Ok(())
728 }
729
730 #[allow(clippy::unused_async)]
731 pub async fn finish_query(&mut self) -> anyhow::Result<()> {
732 log::debug!("-> Query {} finished", self.current_statement);
733 self.current_statement += 1;
734 Ok(())
735 }
736
737 pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
740 self.close_component()?;
741 let data = if self.app_state.config.environment.is_prod() {
742 json!({
743 "description": format!("Please contact the administrator for more information. The error has been logged."),
744 })
745 } else {
746 json!({
747 "query_number": self.current_statement,
748 "description": error.to_string(),
749 "backtrace": get_backtrace(error),
750 "note": "You can hide error messages like this one from your users by setting the 'environment' configuration option to 'production'."
751 })
752 };
753 let saved_component = self.open_component_with_data("error", &data).await?;
754 self.close_component()?;
755 self.current_component = saved_component;
756 Ok(())
757 }
758
759 pub async fn handle_result<R>(&mut self, result: &anyhow::Result<R>) -> anyhow::Result<()> {
760 if let Err(error) = result {
761 self.handle_error(error).await
762 } else {
763 Ok(())
764 }
765 }
766
767 pub async fn handle_result_and_log<R>(&mut self, result: &anyhow::Result<R>) {
768 if let Err(e) = self.handle_result(result).await {
769 log::error!("{}", e);
770 }
771 }
772
773 async fn render_current_template_with_data<T: Serialize>(
774 &mut self,
775 data: &T,
776 ) -> anyhow::Result<()> {
777 if self.current_component.is_none() {
778 self.set_current_component(DEFAULT_COMPONENT).await?;
779 }
780 self.current_component
781 .as_mut()
782 .expect("just set the current component")
783 .render_item(&mut self.writer, json!(data))?;
784 self.shell_renderer
785 .render_item(&mut self.writer, JsonValue::Null)?;
786 Ok(())
787 }
788
789 async fn create_renderer(
790 component: &str,
791 app_state: Arc<AppState>,
792 component_index: usize,
793 nonce: u64,
794 ) -> anyhow::Result<SplitTemplateRenderer> {
795 let split_template = app_state
796 .all_templates
797 .get_template(&app_state, component)
798 .await?;
799 Ok(SplitTemplateRenderer::new(
800 split_template,
801 app_state,
802 component_index,
803 nonce,
804 ))
805 }
806
807 async fn set_current_component(
809 &mut self,
810 component: &str,
811 ) -> anyhow::Result<Option<SplitTemplateRenderer>> {
812 let current_component_index = self
813 .current_component
814 .as_ref()
815 .map_or(1, |c| c.component_index);
816 let new_component = Self::create_renderer(
817 component,
818 Arc::clone(&self.app_state),
819 current_component_index + 1,
820 self.request_context.content_security_policy.nonce,
821 )
822 .await?;
823 Ok(self.current_component.replace(new_component))
824 }
825
826 async fn open_component_with_data<T: Serialize>(
827 &mut self,
828 component: &str,
829 data: &T,
830 ) -> anyhow::Result<Option<SplitTemplateRenderer>> {
831 self.close_component()?;
832 let old_component = self.set_current_component(component).await?;
833 self.current_component
834 .as_mut()
835 .expect("just set the current component")
836 .render_start(&mut self.writer, json!(data))?;
837 Ok(old_component)
838 }
839
840 fn close_component(&mut self) -> anyhow::Result<()> {
841 if let Some(old_component) = self.current_component.as_mut().take() {
842 old_component.render_end(&mut self.writer)?;
843 }
844 Ok(())
845 }
846
847 pub async fn close(mut self) -> W {
848 if let Some(old_component) = self.current_component.as_mut().take() {
849 let res = old_component
850 .render_end(&mut self.writer)
851 .map_err(|e| format_err!("Unable to render the component closing: {e}"));
852 self.handle_result_and_log(&res).await;
853 }
854 let res = self
855 .shell_renderer
856 .render_end(&mut self.writer)
857 .map_err(|e| format_err!("Unable to render the shell closing: {e}"));
858 self.handle_result_and_log(&res).await;
859 self.writer
860 }
861}
862
863struct HandlebarWriterOutput<W: std::io::Write>(W);
864
865impl<W: std::io::Write> handlebars::Output for HandlebarWriterOutput<W> {
866 fn write(&mut self, seg: &str) -> std::io::Result<()> {
867 std::io::Write::write_all(&mut self.0, seg.as_bytes())
868 }
869}
870
871pub struct SplitTemplateRenderer {
872 split_template: Arc<SplitTemplate>,
873 local_vars: Option<handlebars::LocalVars>,
874 ctx: Context,
875 app_state: Arc<AppState>,
876 row_index: usize,
877 component_index: usize,
878 nonce: JsonValue,
879}
880
881impl SplitTemplateRenderer {
882 fn new(
883 split_template: Arc<SplitTemplate>,
884 app_state: Arc<AppState>,
885 component_index: usize,
886 nonce: u64,
887 ) -> Self {
888 Self {
889 split_template,
890 local_vars: None,
891 app_state,
892 row_index: 0,
893 ctx: Context::null(),
894 component_index,
895 nonce: nonce.into(),
896 }
897 }
898 fn name(&self) -> &str {
899 self.split_template
900 .list_content
901 .name
902 .as_deref()
903 .unwrap_or_default()
904 }
905
906 fn render_start<W: std::io::Write>(
907 &mut self,
908 writer: W,
909 data: JsonValue,
910 ) -> Result<(), RenderError> {
911 log::trace!(
912 "Starting rendering of a template{} with the following top-level parameters: {data}",
913 self.split_template
914 .name()
915 .map(|n| format!(" ('{n}')"))
916 .unwrap_or_default(),
917 );
918 let mut render_context = handlebars::RenderContext::new(None);
919 let blk = render_context
920 .block_mut()
921 .expect("context created without block");
922 blk.set_local_var("component_index", self.component_index.into());
923 blk.set_local_var("csp_nonce", self.nonce.clone());
924
925 *self.ctx.data_mut() = data;
926 let mut output = HandlebarWriterOutput(writer);
927 self.split_template.before_list.render(
928 &self.app_state.all_templates.handlebars,
929 &self.ctx,
930 &mut render_context,
931 &mut output,
932 )?;
933 self.local_vars = render_context
934 .block_mut()
935 .map(|blk| std::mem::take(blk.local_variables_mut()));
936 self.row_index = 0;
937 Ok(())
938 }
939
940 fn render_item<W: std::io::Write>(
941 &mut self,
942 writer: W,
943 data: JsonValue,
944 ) -> Result<(), RenderError> {
945 log::trace!("Rendering a new item in the page: {data:?}");
946 if let Some(local_vars) = self.local_vars.take() {
947 let mut render_context = handlebars::RenderContext::new(None);
948 let blk = render_context
949 .block_mut()
950 .expect("context created without block");
951 *blk.local_variables_mut() = local_vars;
952 let mut blk = BlockContext::new();
953 blk.set_base_value(data);
954 blk.set_local_var("component_index", self.component_index.into());
955 blk.set_local_var("row_index", self.row_index.into());
956 blk.set_local_var("csp_nonce", self.nonce.clone());
957 render_context.push_block(blk);
958 let mut output = HandlebarWriterOutput(writer);
959 self.split_template.list_content.render(
960 &self.app_state.all_templates.handlebars,
961 &self.ctx,
962 &mut render_context,
963 &mut output,
964 )?;
965 render_context.pop_block();
966 self.local_vars = render_context
967 .block_mut()
968 .map(|blk| std::mem::take(blk.local_variables_mut()));
969 self.row_index += 1;
970 }
971 Ok(())
972 }
973
974 fn render_end<W: std::io::Write>(&mut self, writer: W) -> Result<(), RenderError> {
975 log::trace!(
976 "Closing a template {}",
977 self.split_template
978 .name()
979 .map(|name| format!("('{name}')"))
980 .unwrap_or_default(),
981 );
982 if let Some(mut local_vars) = self.local_vars.take() {
983 let mut render_context = handlebars::RenderContext::new(None);
984 local_vars.put("row_index", self.row_index.into());
985 local_vars.put("component_index", self.component_index.into());
986 local_vars.put("csp_nonce", self.nonce.clone());
987 log::trace!("Rendering the after_list template with the following local variables: {local_vars:?}");
988 *render_context
989 .block_mut()
990 .expect("ctx created without block")
991 .local_variables_mut() = local_vars;
992 let mut output = HandlebarWriterOutput(writer);
993 self.split_template.after_list.render(
994 &self.app_state.all_templates.handlebars,
995 &self.ctx,
996 &mut render_context,
997 &mut output,
998 )?;
999 }
1000 Ok(())
1001 }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007 use crate::app_config;
1008 use crate::templates::split_template;
1009 use handlebars::Template;
1010
1011 #[actix_web::test]
1012 async fn test_split_template_render() -> anyhow::Result<()> {
1013 let template = Template::compile(
1014 "Hello {{name}} !\
1015 {{#each_row}} ({{x}} : {{../name}}) {{/each_row}}\
1016 Goodbye {{name}}",
1017 )?;
1018 let split = split_template(template);
1019 let mut output = Vec::new();
1020 let config = app_config::tests::test_config();
1021 let app_state = Arc::new(AppState::init(&config).await.unwrap());
1022 let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state, 0, 0);
1023 rdr.render_start(&mut output, json!({"name": "SQL"}))?;
1024 rdr.render_item(&mut output, json!({"x": 1}))?;
1025 rdr.render_item(&mut output, json!({"x": 2}))?;
1026 rdr.render_end(&mut output)?;
1027 assert_eq!(
1028 String::from_utf8_lossy(&output),
1029 "Hello SQL ! (1 : SQL) (2 : SQL) Goodbye SQL"
1030 );
1031 Ok(())
1032 }
1033
1034 #[actix_web::test]
1035 async fn test_delayed() -> anyhow::Result<()> {
1036 let template = Template::compile(
1037 "{{#each_row}}<b> {{x}} {{#delay}} {{x}} </b>{{/delay}}{{/each_row}}{{flush_delayed}}",
1038 )?;
1039 let split = split_template(template);
1040 let mut output = Vec::new();
1041 let config = app_config::tests::test_config();
1042 let app_state = Arc::new(AppState::init(&config).await.unwrap());
1043 let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state, 0, 0);
1044 rdr.render_start(&mut output, json!(null))?;
1045 rdr.render_item(&mut output, json!({"x": 1}))?;
1046 rdr.render_item(&mut output, json!({"x": 2}))?;
1047 rdr.render_end(&mut output)?;
1048 assert_eq!(
1049 String::from_utf8_lossy(&output),
1050 "<b> 1 <b> 2 2 </b> 1 </b>"
1051 );
1052 Ok(())
1053 }
1054}
1055
1056#[derive(Copy, Clone, PartialEq, Eq)]
1057enum HeaderComponent {
1058 StatusCode,
1059 HttpHeader,
1060 Redirect,
1061 Json,
1062 Csv,
1063 Cookie,
1064 Authentication,
1065}
1066
1067impl TryFrom<&str> for HeaderComponent {
1068 type Error = ();
1069 fn try_from(s: &str) -> Result<Self, Self::Error> {
1070 match s {
1071 "status_code" => Ok(Self::StatusCode),
1072 "http_header" => Ok(Self::HttpHeader),
1073 "redirect" => Ok(Self::Redirect),
1074 "json" => Ok(Self::Json),
1075 "csv" => Ok(Self::Csv),
1076 "cookie" => Ok(Self::Cookie),
1077 "authentication" => Ok(Self::Authentication),
1078 _ => Err(()),
1079 }
1080 }
1081}