/*
This file is part of TALER
Copyright (C) 2020, 2022 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see
*/
/**
* @file templating_api.c
* @brief logic to load and complete HTML templates
* @author Christian Grothoff
*/
#include "platform.h"
#include
#include "taler_util.h"
#include "taler_mhd_lib.h"
#include "taler_templating_lib.h"
#include "mustach.h"
#include "mustach-jansson.h"
#include
/**
* Entry in a key-value array we use to cache templates.
*/
struct TVE
{
/**
* A name, used as the key. NULL for the last entry.
*/
char *name;
/**
* Language the template is in.
*/
char *lang;
/**
* 0-terminated (!) file data to return for @e name and @e lang.
*/
char *value;
};
/**
* Array of templates loaded into RAM.
*/
static struct TVE *loaded;
/**
* Length of the #loaded array.
*/
static unsigned int loaded_length;
/**
* Load Mustach template into memory. Note that we intentionally cache
* failures, that is if we ever failed to load a template, we will never try
* again.
*
* @param connection the connection we act upon
* @param name name of the template file to load
* (MUST be a 'static' string in memory!)
* @return NULL on error, otherwise the template
*/
static const char *
lookup_template (struct MHD_Connection *connection,
const char *name)
{
struct TVE *best = NULL;
const char *lang;
lang = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
if (NULL == lang)
lang = "en";
/* find best match by language */
for (unsigned int i = 0; i
TALER_language_matches (lang,
best->lang) ) )
best = &loaded[i];
}
if (NULL == best)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"No templates found in `%s'\n",
name);
return NULL;
}
return best->value;
}
/**
* Get the base URL for static resources.
*
* @param con the MHD connection
* @param instance_id the instance ID
* @returns the static files base URL, guaranteed
* to have a trailing slash.
*/
static char *
make_static_url (struct MHD_Connection *con,
const char *instance_id)
{
const char *host;
const char *forwarded_host;
const char *uri_path;
struct GNUNET_Buffer buf = { 0 };
host = MHD_lookup_connection_value (con,
MHD_HEADER_KIND,
"Host");
forwarded_host = MHD_lookup_connection_value (con,
MHD_HEADER_KIND,
"X-Forwarded-Host");
uri_path = MHD_lookup_connection_value (con,
MHD_HEADER_KIND,
"X-Forwarded-Prefix");
if (NULL != forwarded_host)
host = forwarded_host;
if (NULL == host)
{
GNUNET_break (0);
return NULL;
}
GNUNET_assert (NULL != instance_id);
if (GNUNET_NO == TALER_mhd_is_https (con))
GNUNET_buffer_write_str (&buf,
"http://");
else
GNUNET_buffer_write_str (&buf,
"https://");
GNUNET_buffer_write_str (&buf,
host);
if (NULL != uri_path)
GNUNET_buffer_write_path (&buf,
uri_path);
if (0 != strcmp ("default",
instance_id))
{
GNUNET_buffer_write_path (&buf,
"instances");
GNUNET_buffer_write_path (&buf,
instance_id);
}
GNUNET_buffer_write_path (&buf,
"static/");
return GNUNET_buffer_reap_str (&buf);
}
int
TALER_TEMPLATING_fill (const char *tmpl,
const json_t *root,
void **result,
size_t *result_size)
{
int eno;
if (0 !=
(eno = mustach_jansson (tmpl,
(json_t *) root,
(char **) result,
result_size)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"mustach failed on template with error %d\n",
eno);
*result = NULL;
*result_size = 0;
return eno;
}
return eno;
}
enum GNUNET_GenericReturnValue
TALER_TEMPLATING_build (struct MHD_Connection *connection,
unsigned int *http_status,
const char *template,
const char *instance_id,
const char *taler_uri,
const json_t *root,
struct MHD_Response **reply)
{
char *body;
size_t body_size;
{
const char *tmpl;
int eno;
tmpl = lookup_template (connection,
template);
if (NULL == tmpl)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to load template `%s'\n",
template);
*http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
*reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_LOAD_TEMPLATE,
template);
return GNUNET_NO;
}
/* Add default values to the context */
if (NULL != instance_id)
{
char *static_url = make_static_url (connection,
instance_id);
GNUNET_break (0 ==
json_object_set_new ((json_t *) root,
"static_url",
json_string (static_url)));
GNUNET_free (static_url);
}
if (0 !=
(eno = mustach_jansson (tmpl,
(json_t *) root,
&body,
&body_size)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"mustach failed on template `%s' with error %d\n",
template,
eno);
*http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
*reply = TALER_MHD_make_error (TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE,
template);
return GNUNET_NO;
}
}
/* try to compress reply if client allows it */
{
bool compressed = false;
if (MHD_YES ==
TALER_MHD_can_compress (connection))
{
compressed = TALER_MHD_body_compress ((void **) &body,
&body_size);
}
*reply = MHD_create_response_from_buffer (body_size,
body,
MHD_RESPMEM_MUST_FREE);
if (NULL == *reply)
{
GNUNET_break (0);
return GNUNET_SYSERR;
}
if (compressed)
{
if (MHD_NO ==
MHD_add_response_header (*reply,
MHD_HTTP_HEADER_CONTENT_ENCODING,
"deflate"))
{
GNUNET_break (0);
MHD_destroy_response (*reply);
*reply = NULL;
return GNUNET_SYSERR;
}
}
}
/* Add standard headers */
if (NULL != taler_uri)
GNUNET_break (MHD_NO !=
MHD_add_response_header (*reply,
"Taler",
taler_uri));
GNUNET_break (MHD_NO !=
MHD_add_response_header (*reply,
MHD_HTTP_HEADER_CONTENT_TYPE,
"text/html"));
return GNUNET_OK;
}
enum GNUNET_GenericReturnValue
TALER_TEMPLATING_reply (struct MHD_Connection *connection,
unsigned int http_status,
const char *template,
const char *instance_id,
const char *taler_uri,
const json_t *root)
{
enum GNUNET_GenericReturnValue res;
struct MHD_Response *reply;
MHD_RESULT ret;
res = TALER_TEMPLATING_build (connection,
&http_status,
template,
instance_id,
taler_uri,
root,
&reply);
if (GNUNET_SYSERR == res)
return res;
ret = MHD_queue_response (connection,
http_status,
reply);
MHD_destroy_response (reply);
if (MHD_NO == ret)
return GNUNET_SYSERR;
return (res == GNUNET_OK)
? GNUNET_OK
: GNUNET_NO;
}
/**
* Function called with a template's filename.
*
* @param cls closure, NULL
* @param filename complete filename (absolute path)
* @return #GNUNET_OK to continue to iterate,
* #GNUNET_NO to stop iteration with no error,
* #GNUNET_SYSERR to abort iteration with error!
*/
static enum GNUNET_GenericReturnValue
load_template (void *cls,
const char *filename)
{
char *lang;
char *end;
int fd;
struct stat sb;
char *map;
const char *name;
(void) cls;
if ('.' == filename[0])
return GNUNET_OK;
name = strrchr (filename,
'/');
if (NULL == name)
name = filename;
else
name++;
lang = strchr (name,
'.');
if (NULL == lang)
return GNUNET_OK; /* name must include .$LANG */
lang++;
end = strchr (lang,
'.');
if ( (NULL == end) ||
(0 != strcmp (end,
".must")) )
return GNUNET_OK; /* name must end with '.must' */
/* finally open template */
fd = open (filename,
O_RDONLY);
if (-1 == fd)
{
GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
"open",
filename);
return GNUNET_SYSERR;
}
if (0 !=
fstat (fd,
&sb))
{
GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
"open",
filename);
GNUNET_break (0 == close (fd));
return GNUNET_OK;
}
map = GNUNET_malloc_large (sb.st_size + 1);
if (NULL == map)
{
GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
"malloc");
GNUNET_break (0 == close (fd));
return GNUNET_SYSERR;
}
if (sb.st_size !=
read (fd,
map,
sb.st_size))
{
GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
"read",
filename);
GNUNET_break (0 == close (fd));
return GNUNET_OK;
}
GNUNET_break (0 == close (fd));
GNUNET_array_grow (loaded,
loaded_length,
loaded_length + 1);
loaded[loaded_length - 1].name = GNUNET_strndup (name,
(lang - 1) - name);
loaded[loaded_length - 1].lang = GNUNET_strndup (lang,
end - lang);
loaded[loaded_length - 1].value = map;
return GNUNET_OK;
}
MHD_RESULT
TALER_TEMPLATING_reply_error (struct MHD_Connection *connection,
const char *template_basename,
unsigned int http_status,
enum TALER_ErrorCode ec,
const char *detail)
{
json_t *data;
enum GNUNET_GenericReturnValue ret;
data = GNUNET_JSON_PACK (
GNUNET_JSON_pack_uint64 ("ec",
ec),
GNUNET_JSON_pack_string ("hint",
TALER_ErrorCode_get_hint (ec)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("detail",
detail))
);
ret = TALER_TEMPLATING_reply (connection,
http_status,
template_basename,
NULL,
NULL,
data);
json_decref (data);
switch (ret)
{
case GNUNET_OK:
return MHD_YES;
case GNUNET_NO:
return MHD_YES;
case GNUNET_SYSERR:
return MHD_NO;
}
GNUNET_assert (0);
return MHD_NO;
}
enum GNUNET_GenericReturnValue
TALER_TEMPLATING_init (const char *subsystem)
{
char *dn;
int ret;
{
char *path;
path = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR);
if (NULL == path)
{
GNUNET_break (0);
return GNUNET_SYSERR;
}
GNUNET_asprintf (&dn,
"%s/%s/templates/",
path,
subsystem);
GNUNET_free (path);
}
ret = GNUNET_DISK_directory_scan (dn,
&load_template,
NULL);
GNUNET_free (dn);
if (-1 == ret)
{
GNUNET_break (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
void
TALER_TEMPLATING_done (void)
{
for (unsigned int i = 0; i