sqlpage/
render.rs

1//! Handles the rendering of SQL query results into HTTP responses using components.
2//!
3//! This module is responsible for transforming database query results into formatted HTTP responses
4//! by utilizing a component-based rendering system. It supports multiple output formats including HTML,
5//! JSON, and CSV.
6//!
7//! # Components
8//!
9//! Components are small user interface elements that display data in specific ways. The rendering
10//! system supports two types of parameters for components:
11//!
12//! * **Top-level parameters**: Properties that customize the component's appearance and behavior
13//! * **Row-level parameters**: The actual data to be displayed within the component
14//!
15//! # Page Context States
16//!
17//! The rendering process moves through different states represented by [`PageContext`]:
18//!
19//! * `Header`: Initial state for processing HTTP headers and response setup
20//! * `Body`: Active rendering state where component output is generated
21//! * `Close`: Final state indicating the response is complete
22//!
23//! # Header Components
24//!
25//! Some components must be processed before any response body is sent:
26//!
27//! * [`status_code`](https://sql-page.com/component.sql?component=status_code): Sets the HTTP response status
28//! * [`http_header`](https://sql-page.com/component.sql?component=http_header): Sets custom HTTP headers
29//! * [`redirect`](https://sql-page.com/component.sql?component=redirect): Performs HTTP redirects
30//! * `authentication`: Handles password-protected access
31//! * `cookie`: Manages browser cookies
32//!
33//! # Body Components
34//!
35//! The module supports multiple output formats through different renderers:
36//!
37//! * HTML: Renders templated HTML output using components
38//! * JSON: Generates JSON responses for API endpoints
39//! * CSV: Creates downloadable CSV files
40//!
41//! For more details on available components and their usage, see the
42//! [SQLPage documentation](https://sql-page.com/documentation.sql).
43
44use 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    /// Indicates that we should stay in the header context
65    Header(HeaderContext),
66
67    /// Indicates that we should start rendering the body
68    Body {
69        http_response: HttpResponseBuilder,
70        renderer: AnyRenderBodyContext,
71    },
72
73    /// The response is ready, and should be sent as is. No further statements should be executed
74    Close(HttpResponse),
75}
76
77/// Handles the first SQL statements, before the headers have been sent to
78pub 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, // strict by default
209            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    /// Answers to the HTTP request with a single json object
246    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        // The authentication failed
312        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
384/**
385 * Can receive rows, and write them in a given format to an `io::Write`
386 */
387pub enum AnyRenderBodyContext {
388    Html(HtmlRenderContext<ResponseWriter>),
389    Json(JsonBodyRenderer<ResponseWriter>),
390    Csv(CsvBodyRenderer),
391}
392
393/**
394 * Dummy impl to dispatch method calls to the underlying renderer
395 */
396impl 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    // The writer is a large struct, so we store it on the heap
515    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    /// Handles the rendering of an error.
740    /// Returns whether the error is irrecoverable and the rendering must stop
741    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    /// Set a new current component and return the old one
810    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    // LocalVars is a large struct, so we store it on the heap
876    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}