1use axum::{
6 body::Body, http::StatusCode, http::header, response::IntoResponse, response::Response,
7};
8use axum_extra::headers::HeaderValue;
9use lib_humus_configuration::ErrorCause;
10use lib_humus_configuration::read_from_toml_file;
11use log::{error, info};
12use tera::Tera;
13
14use std::cell::Cell;
15use std::collections::HashMap;
16use std::marker::PhantomData;
17use std::os::unix::ffi::OsStrExt;
18use std::path::Path;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22use crate::HumusQuerySettings;
23use crate::HumusView;
24use crate::language::LanguageEngine;
25use crate::language::TextFunction;
26use crate::language::TextFunctionContext;
27use crate::templating::HumusApiFormat;
28use crate::templating::HumusFormatIdentifier;
29use crate::templating::TemplatesManifest;
30use crate::templating::{ExtraConfig, TemplatingEngineLoaderError};
31
32thread_local! (
33 static TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT: Cell<Option<TextFunctionContext>> = const { Cell::new(None) }
35);
36
37#[derive(Debug, Clone)]
39pub struct TemplatingEngine<ApiFormat>
40where
41 ApiFormat: HumusApiFormat,
42{
43 pub tera: HashMap<HumusFormatIdentifier, Tera>,
45
46 pub template_config: Option<ExtraConfig>,
48
49 pub templates_manifest: TemplatesManifest,
51
52 phantom_api_format: PhantomData<ApiFormat>,
53}
54
55impl<ApiFormat> TemplatingEngine<ApiFormat>
56where
57 ApiFormat: HumusApiFormat,
58{
59 pub fn load_from_directory(
72 path: impl AsRef<Path>,
73 extra_config_path: Option<&Path>,
74 template_tera: Tera,
75 language_engine: Option<Arc<LanguageEngine>>,
76 ) -> Result<Self, TemplatingEngineLoaderError> {
77 let path = path.as_ref();
78
79 let extra_config: Option<ExtraConfig> = read_from_toml_file(
80 extra_config_path
81 .map(|p| p.to_path_buf())
82 .unwrap_or_else(|| path.join("extra.toml")),
83 )
84 .or_else(|e| match &e.cause {
85 ErrorCause::FileRead { .. } => {
86 if extra_config_path.is_some() {
90 return Err(TemplatingEngineLoaderError::ConfigurationError(e));
91 }
92 Ok(None)
93 }
94 _ => {
95 return Err(TemplatingEngineLoaderError::ConfigurationError(e));
96 }
97 })?;
98 let templates_manifest: TemplatesManifest =
100 read_from_toml_file(path.join("templates.toml"))
101 .map_err(TemplatingEngineLoaderError::TemplatesManifestError)?;
102
103 let problems = templates_manifest.find_problems::<ApiFormat>();
104 if !problems.is_empty() {
105 return Err(TemplatingEngineLoaderError::TemplatesManifestProblems(
106 problems,
107 ));
108 }
109
110 let template_directory = path.join("tera2");
111
112 if !template_directory.is_dir() {
113 return Err(TemplatingEngineLoaderError::TemplateDirectoryNotFound {
114 path: template_directory,
115 });
116 }
117
118 let mut templating_engines: HashMap<HumusFormatIdentifier, Tera> = HashMap::new();
119 for (format_id, format_desc) in templates_manifest.format.iter() {
120 info!("Loading templates for format {format_id:?} ...");
121 let mut tera = template_tera.clone();
122 tera.register_filter("urlencode", tera_contrib::urlencode::urlencode);
124 tera.register_filter(
125 "urlencode_strict",
126 tera_contrib::urlencode::urlencode_strict,
127 );
128
129 tera.register_test("matching", tera_contrib::regex::Matching::default());
130 tera.register_filter(
131 "regex_replace",
132 tera_contrib::regex::RegexReplace::default(),
133 );
134 tera.register_filter("spaceless", tera_contrib::regex::spaceless);
135 tera.register_filter("striptags", tera_contrib::regex::striptags);
136
137 tera.register_filter("json_encode", tera_contrib::json::json_encode);
138
139 if let Some(ref language_engine) = language_engine {
140 let text_function = TextFunction::new(language_engine.clone(), |_, _| {
141 if let Some(context) = TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.take() {
142 TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.set(Some(context.clone()));
143 Ok(context)
144 } else {
145 Err(tera::Error::message(
146 "No context stored while rendering template, this is a bug in lib-humus!",
147 ))
148 }
149 });
150 tera.register_function("text", text_function);
151 }
152
153 let mut templates_to_load: Vec<(PathBuf, Option<String>)> = format_desc
154 .additional_templates
155 .iter()
156 .map(|file| (template_directory.join(file), Some(file.clone())))
157 .collect();
158 let full_file_extension = format!(".{}", format_desc.extension);
159 for dir_entry in std::fs::read_dir(&template_directory).map_err(|e| {
161 TemplatingEngineLoaderError::TemplateDirectoryIoError {
162 path: template_directory.clone(),
163 error: e,
164 }
165 })? {
166 let dir_entry = dir_entry.map_err(|e| {
167 TemplatingEngineLoaderError::TemplateDirectoryIoError {
168 path: template_directory.clone(),
169 error: e,
170 }
171 })?;
172 let path = dir_entry.path();
173 if !path.is_file() {
174 continue;
175 }
176 let name = String::from_utf8_lossy(dir_entry.file_name().as_bytes()).to_string();
177 if name.ends_with(&full_file_extension) {
178 log::debug!("Discovered template: {name:?}");
179 templates_to_load.push((path, Some(name)));
180 }
181 }
182 if let Err(e) = tera.add_template_files(templates_to_load) {
183 error!("Error Parsing Template: {e}");
184 return Err(TemplatingEngineLoaderError::TemplateParseError {
185 path: path.to_path_buf(),
186 format: *format_id,
187 tera_error: e,
188 });
189 }
190 templating_engines.insert(*format_id, tera);
191 }
192
193 Ok(Self {
194 tera: templating_engines,
195 templates_manifest,
196 template_config: extra_config,
197
198 phantom_api_format: PhantomData,
199 })
200 }
201
202 pub fn render_view<S: HumusQuerySettings>(
256 &self,
257 settings: &S,
258 view: impl HumusView<S, ApiFormat>,
259 language_engine: &LanguageEngine,
260 ) -> Response {
261 let format = settings.get_format();
262 let status_code = view.get_status_code(settings);
263
264 let mut response = if let Some(api_format) = ApiFormat::from_name(&format) {
265 view.into_api_response(settings, api_format)
266 } else if let Some(format_description) = self.templates_manifest.format.get(&format) {
267 let template_name = view.get_template_name();
268 let mime_type = &format_description.media_type;
269
270 let mut context = tera::Context::new();
271 context.insert("view", &template_name);
272 context.insert("format", &format);
274 let language = settings
275 .get_preferred_language()
276 .unwrap_or_else(|| language_engine.language_manifest().default_language.clone());
277 context.insert("lang", &language);
278 context.insert("language_manifest", language_engine.language_manifest());
279 context.insert("media_type", &mime_type.to_string());
280 context.insert("http_status", &status_code.as_u16());
281 context.insert("data", &view);
282 context.insert("extra", &self.template_config);
283 view.initalize_template_context(&mut context, settings);
284
285 TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.set(Some(TextFunctionContext {
287 language: Some(language),
288 escape_function: tera::escape_html, safe_suffix: language_engine
290 .language_manifest()
291 .get_safe_suffix_for_format(format),
292 }));
293 let Some(tera) = self.tera.get(&format) else {
294 error!("Problem finding tera instance for format {format:?}");
295 return (
296 StatusCode::INTERNAL_SERVER_ERROR,
297 format!("Unknown template output format: {format:?}"),
298 )
299 .into_response();
300 };
301 match tera.render(
302 &format!("{template_name}.{}", format_description.extension),
303 &context,
304 ) {
305 Ok(text) => {
306 let response = (
307 [(
308 header::CONTENT_TYPE,
309 HeaderValue::from_str(mime_type.as_ref())
310 .expect("MimeType should always be a valid header value."),
311 )],
312 Into::<Body>::into(text),
313 )
314 .into_response();
315 view.update_response(response, settings)
316 }
317 Err(e) => {
318 error!(
319 "There was an error while rendering template {template_name}:\n{}",
320 render_tera_error(e)
321 );
322 (
323 StatusCode::INTERNAL_SERVER_ERROR,
324 format!("Template error in {template_name}, contact owner or see logs.\n"),
325 )
326 .into_response()
327 }
328 }
329 } else {
331 (
332 StatusCode::BAD_REQUEST,
333 "(unknown format) You have requested an unknown template or API format.\n"
334 .to_string(),
335 )
336 .into_response()
337 };
338
339 if response.status() == StatusCode::OK {
341 *response.status_mut() = status_code;
343 }
344
345 response
347 }
348}
349
350fn render_tera_error(error: tera::Error) -> String {
351 let mut text = error.to_string();
352 let mut error: &(dyn core::error::Error + 'static) = &error;
353 while let Some(source) = error.source() {
354 text = format!("{text}\n* {source}");
355 error = source;
356 }
357 text
358}