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, 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    /// 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        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, // strict by default
208            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    /// Answers to the HTTP request with a single json object
245    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        // The authentication failed
311        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
383/**
384 * Can receive rows, and write them in a given format to an `io::Write`
385 */
386pub enum AnyRenderBodyContext {
387    Html(HtmlRenderContext<ResponseWriter>),
388    Json(JsonBodyRenderer<ResponseWriter>),
389    Csv(CsvBodyRenderer),
390}
391
392/**
393 * Dummy impl to dispatch method calls to the underlying renderer
394 */
395impl 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    /// Handles the rendering of an error.
738    /// Returns whether the error is irrecoverable and the rendering must stop
739    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    /// Set a new current component and return the old one
808    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}