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 tera.register_filter("date", tera_contrib::dates::date);
140 tera.register_test("is_after", tera_contrib::dates::is_after);
141 tera.register_test("is_before", tera_contrib::dates::is_before);
142 tera.register_function("now", tera_contrib::dates::now);
143
144 tera.register_filter(
145 "filesize_format",
146 tera_contrib::filesize_format::filesize_format,
147 );
148
149 if let Some(ref language_engine) = language_engine {
150 let text_function = TextFunction::new(language_engine.clone(), |_, _| {
151 if let Some(context) = TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.take() {
152 TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.set(Some(context.clone()));
153 Ok(context)
154 } else {
155 Err(tera::Error::message(
156 "No context stored while rendering template, this is a bug in lib-humus!",
157 ))
158 }
159 });
160 tera.register_function("text", text_function);
161 }
162
163 let mut templates_to_load: Vec<(PathBuf, Option<String>)> = format_desc
164 .additional_templates
165 .iter()
166 .map(|file| (template_directory.join(file), Some(file.clone())))
167 .collect();
168 let full_file_extension = format!(".{}", format_desc.extension);
169 for dir_entry in std::fs::read_dir(&template_directory).map_err(|e| {
171 TemplatingEngineLoaderError::TemplateDirectoryIoError {
172 path: template_directory.clone(),
173 error: e,
174 }
175 })? {
176 let dir_entry = dir_entry.map_err(|e| {
177 TemplatingEngineLoaderError::TemplateDirectoryIoError {
178 path: template_directory.clone(),
179 error: e,
180 }
181 })?;
182 let path = dir_entry.path();
183 if !path.is_file() {
184 continue;
185 }
186 let name = String::from_utf8_lossy(dir_entry.file_name().as_bytes()).to_string();
187 if name.ends_with(&full_file_extension) {
188 log::debug!("Discovered template: {name:?}");
189 templates_to_load.push((path, Some(name)));
190 }
191 }
192 if let Err(e) = tera.add_template_files(templates_to_load) {
193 error!("Error Parsing Template: {e}");
194 return Err(TemplatingEngineLoaderError::TemplateParseError {
195 path: path.to_path_buf(),
196 format: *format_id,
197 tera_error: e,
198 });
199 }
200 templating_engines.insert(*format_id, tera);
201 }
202
203 Ok(Self {
204 tera: templating_engines,
205 templates_manifest,
206 template_config: extra_config,
207
208 phantom_api_format: PhantomData,
209 })
210 }
211
212 pub fn render_view<S: HumusQuerySettings>(
266 &self,
267 settings: &S,
268 view: impl HumusView<S, ApiFormat>,
269 language_engine: &LanguageEngine,
270 ) -> Response {
271 let format = settings.get_format();
272 let status_code = view.get_status_code(settings);
273
274 let mut response = if let Some(api_format) = ApiFormat::from_name(&format) {
275 view.into_api_response(settings, api_format)
276 } else if let Some(format_description) = self.templates_manifest.format.get(&format) {
277 let template_name = view.get_template_name();
278 let mime_type = &format_description.media_type;
279
280 let mut context = tera::Context::new();
281 context.insert("view", &template_name);
282 context.insert("format", &format);
284 let language = settings
285 .get_preferred_language()
286 .unwrap_or_else(|| language_engine.language_manifest().default_language.clone());
287 context.insert("lang", &language);
288 context.insert("language_manifest", language_engine.language_manifest());
289 context.insert("media_type", &mime_type.to_string());
290 context.insert("http_status", &status_code.as_u16());
291 context.insert("data", &view);
292 context.insert("extra", &self.template_config);
293 view.initalize_template_context(&mut context, settings);
294
295 TEMPLATE_BYPASSING_TEXT_FUNCTION_CONTEXT.set(Some(TextFunctionContext {
297 language: Some(language),
298 escape_function: tera::escape_html, safe_suffix: language_engine
300 .language_manifest()
301 .get_safe_suffix_for_format(format),
302 }));
303 let Some(tera) = self.tera.get(&format) else {
304 error!("Problem finding tera instance for format {format:?}");
305 return (
306 StatusCode::INTERNAL_SERVER_ERROR,
307 format!("Unknown template output format: {format:?}"),
308 )
309 .into_response();
310 };
311 match tera.render(
312 &format!("{template_name}.{}", format_description.extension),
313 &context,
314 ) {
315 Ok(text) => {
316 let response = (
317 [(
318 header::CONTENT_TYPE,
319 HeaderValue::from_str(mime_type.as_ref())
320 .expect("MimeType should always be a valid header value."),
321 )],
322 Into::<Body>::into(text),
323 )
324 .into_response();
325 view.update_response(response, settings)
326 }
327 Err(e) => {
328 error!(
329 "There was an error while rendering template {template_name}:\n{}",
330 render_tera_error(e)
331 );
332 (
333 StatusCode::INTERNAL_SERVER_ERROR,
334 format!("Template error in {template_name}, contact owner or see logs.\n"),
335 )
336 .into_response()
337 }
338 }
339 } else {
341 (
342 StatusCode::BAD_REQUEST,
343 "(unknown format) You have requested an unknown template or API format.\n"
344 .to_string(),
345 )
346 .into_response()
347 };
348
349 if response.status() == StatusCode::OK {
351 *response.status_mut() = status_code;
353 }
354
355 response
357 }
358}
359
360fn render_tera_error(error: tera::Error) -> String {
361 let mut text = error.to_string();
362 let mut error: &(dyn core::error::Error + 'static) = &error;
363 while let Some(source) = error.source() {
364 text = format!("{text}\n* {source}");
365 error = source;
366 }
367 text
368}