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