commit 43ba0c9d221a82157bbad4e4da9af313fa4f4eda
parent 2d23acc74baa3b77bd70989df7101d99e4a53b05
Author: Christian Grothoff <grothoff@gnunet.org>
Date: Tue, 20 Jan 2026 07:50:43 +0100
add basic Paivana support (#10806)
Diffstat:
4 files changed, 292 insertions(+), 81 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-using-templates.c b/src/backend/taler-merchant-httpd_post-using-templates.c
@@ -30,6 +30,8 @@
#include "taler-merchant-httpd_exchanges.h"
#include "taler_merchant_util.h"
#include <taler/taler_json_lib.h>
+#include <regex.h>
+
/**
* Item selected from inventory_selection.
@@ -176,16 +178,6 @@ struct UseContext
const char *summary;
/**
- * Fulfillment URL override from request, if any.
- */
- const char *fulfillment_url;
-
- /**
- * Fulfillment message override from request, if any.
- */
- const char *fulfillment_message;
-
- /**
* Amount provided by the client.
*/
struct TALER_Amount amount;
@@ -222,6 +214,26 @@ struct UseContext
} inventory;
+ /**
+ * Request details if this is a paivana instantiation.
+ */
+ struct
+ {
+
+ /**
+ * Target website for the request.
+ */
+ const char *website;
+
+ /**
+ * Unique client identifier, consisting of
+ * current time, "-", and the hash of a nonce,
+ * the website and the current time.
+ */
+ const char *paivana_id;
+
+ } paivana;
+
} parse_request;
/**
@@ -258,6 +270,11 @@ struct UseContext
*/
unsigned int totals_len;
+ /**
+ * Array of payment choices, used with Paviana.
+ */
+ json_t *choices;
+
} compute_price;
};
@@ -302,6 +319,7 @@ cleanup_use_context (void *cls)
uc->parse_request.inventory.items);
GNUNET_free (uc->compute_price.totals);
uc->compute_price.totals_len = 0;
+ json_decref (uc->compute_price.choices);
if (NULL != uc->ihc.cc)
uc->ihc.cc (uc->ihc.ctx);
GNUNET_free (uc->ihc.infix);
@@ -459,7 +477,57 @@ static enum GNUNET_GenericReturnValue
parse_using_templates_paivana_request (
struct UseContext *uc)
{
- /* FIXME: not implemented */
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("website",
+ &uc->parse_request.paivana.website),
+ GNUNET_JSON_spec_string ("paivana_id",
+ &uc->parse_request.paivana.paivana_id),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue res;
+ struct GNUNET_HashCode sh;
+ unsigned long long tv;
+ const char *dash;
+
+ GNUNET_assert (NULL == uc->ihc.request_body);
+ res = TALER_MHD_parse_json_data (uc->hc->connection,
+ uc->hc->request_body,
+ spec);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ use_finalize_parse (uc,
+ res);
+ return GNUNET_SYSERR;
+ }
+ if (1 !=
+ sscanf (uc->parse_request.paivana.paivana_id,
+ "%llu-",
+ &tv))
+ {
+ GNUNET_break_op (0);
+ use_reply_with_error (uc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "paivana_id");
+ return GNUNET_SYSERR;
+ }
+ dash = strchr (uc->parse_request.paivana.paivana_id,
+ '-');
+ GNUNET_assert (NULL != dash);
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (dash + 1,
+ strlen (dash + 1),
+ &sh,
+ sizeof (sh)))
+ {
+ GNUNET_break_op (0);
+ use_reply_with_error (uc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "paivana_id");
+ return GNUNET_SYSERR;
+ }
return GNUNET_OK;
}
@@ -754,7 +822,9 @@ category_allowed (const json_t *allowed_categories,
if (NULL == allowed_categories)
return false;
- json_array_foreach ((json_t *) allowed_categories, idx, entry)
+ json_array_foreach ((json_t *) allowed_categories,
+ idx,
+ entry)
{
uint64_t selected_id;
@@ -790,7 +860,7 @@ category_allowed (const json_t *allowed_categories,
static enum GNUNET_GenericReturnValue
verify_using_templates_inventory (struct UseContext *uc)
{
- if (uc->template_contract.inventory.choose_one &&
+ if (uc->template_contract.details.inventory.choose_one &&
(1 != uc->parse_request.inventory.items_len))
{
GNUNET_break_op (0);
@@ -800,7 +870,7 @@ verify_using_templates_inventory (struct UseContext *uc)
"inventory_selection");
return GNUNET_SYSERR;
}
- if (uc->template_contract.inventory.selected_all)
+ if (uc->template_contract.details.inventory.selected_all)
return GNUNET_OK;
for (unsigned int i = 0;
i < uc->parse_request.inventory.items_len;
@@ -848,11 +918,12 @@ verify_using_templates_inventory (struct UseContext *uc)
struct InventoryTemplateItemContext *item =
&uc->parse_request.inventory.items[i];
- if (product_id_allowed (uc->template_contract.inventory.selected_products,
+ if (product_id_allowed (uc->template_contract.details.inventory.
+ selected_products,
item->product_id))
continue;
if (category_allowed (
- uc->template_contract.inventory.selected_categories,
+ uc->template_contract.details.inventory.selected_categories,
item->num_categories,
item->categories))
continue;
@@ -914,8 +985,34 @@ static enum GNUNET_GenericReturnValue
verify_using_templates_paivana (
struct UseContext *uc)
{
- /* TODO: PAIVANA include paivana-specific verification. */
- return verify_using_templates_fixed (uc);
+ if (NULL != uc->template_contract.details.paivana.website_regex)
+ {
+ regex_t ex;
+ bool allowed = false;
+
+ if (0 != regcomp (&ex,
+ uc->template_contract.details.paivana.website_regex,
+ REG_NOSUB | REG_EXTENDED))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (0 ==
+ regexec (&ex,
+ uc->parse_request.paivana.website,
+ 0, NULL,
+ 0))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Website `%s' allowed by template\n",
+ uc->parse_request.paivana.website);
+ allowed = true;
+ }
+ regfree (&ex);
+ if (! allowed)
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
}
@@ -969,7 +1066,6 @@ handle_phase_verify (
res = verify_using_templates_fixed (uc);
break;
case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
- /* TODO: PAIVANA handle paivana-specific instantiation. */
res = verify_using_templates_paivana (uc);
break;
case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
@@ -1236,7 +1332,6 @@ handle_phase_compute_price (struct UseContext *uc)
switch (uc->template_type)
{
case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER:
- case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
uc->compute_price.totals
= GNUNET_new (struct TALER_Amount);
uc->compute_price.totals_len
@@ -1258,6 +1353,59 @@ handle_phase_compute_price (struct UseContext *uc)
case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
/* handled below */
break;
+ case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
+ {
+ const struct TALER_MERCHANT_TemplateContractPaivana *tcp
+ = &uc->template_contract.details.paivana;
+ json_t *choices;
+
+ choices = json_array ();
+ GNUNET_assert (NULL != choices);
+ for (size_t i = 0; i < tcp->choices_len; i++)
+ {
+ /* Make deep copy, we're going to MODIFY it! */
+ struct TALER_MERCHANT_ContractChoice choice
+ = tcp->choices[i];
+
+ if ( (! uc->parse_request.no_tip) &&
+ (GNUNET_YES !=
+ TALER_amount_cmp_currency (&choice.amount,
+ &uc->parse_request.tip)) )
+ continue; /* tip does not match choice currency */
+ if (0 >
+ TALER_amount_add (&choice.amount,
+ &choice.amount,
+ &uc->parse_request.tip))
+ {
+ GNUNET_break (0);
+ use_reply_with_error (uc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
+ "tip");
+ return;
+ }
+ GNUNET_assert (0 ==
+ json_array_append_new (
+ choices,
+ TALER_MERCHANT_json_from_contract_choice (&choice,
+ true)));
+ }
+ if (0 == json_array_size (choices))
+ {
+ GNUNET_break_op (0);
+ json_decref (choices);
+ use_reply_with_error (uc,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_POST_USING_TEMPLATES_NO_CURRENCY,
+ "tip");
+ return;
+ }
+ uc->compute_price.choices = choices;
+ }
+ /* Note: we already did the tip and pricing
+ fully here, so we skip these phases. */
+ uc->phase = USE_PHASE_CREATE_ORDER;
+ return;
case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
GNUNET_assert (0);
}
@@ -1276,17 +1424,19 @@ handle_phase_compute_price (struct UseContext *uc)
= GNUNET_new (struct TALER_Amount);
uc->compute_price.totals_len
= 1;
- ret = compute_inventory_total (uc->parse_request.inventory.items_len,
- uc->parse_request.inventory.items,
- primary_currency,
- uc->compute_price.totals);
+ ret = compute_inventory_total (
+ uc->parse_request.inventory.items_len,
+ uc->parse_request.inventory.items,
+ primary_currency,
+ uc->compute_price.totals);
}
if (GNUNET_SYSERR == ret)
{
- use_reply_with_error (uc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
- "calculation of currency totals failed");
+ use_reply_with_error (
+ uc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_FAILED_COMPUTE_AMOUNT,
+ "calculation of currency totals failed");
return;
}
if (GNUNET_NO == ret)
@@ -1500,12 +1650,6 @@ create_using_templates_inventory (struct UseContext *uc)
? uc->template_contract.summary
: uc->parse_request.summary),
GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("fulfillment_url",
- uc->parse_request.fulfillment_url)),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("fulfillment_message",
- uc->parse_request.fulfillment_message)),
- GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_steal ("products",
tip_products)))));
}
@@ -1521,7 +1665,6 @@ create_using_templates_fixed (struct UseContext *uc)
{
json_t *tip_products = get_tip_product (uc);
- GNUNET_assert (1 == uc->compute_price.totals_len);
uc->ihc.request_body
= GNUNET_JSON_PACK (
GNUNET_JSON_pack_allow_null (
@@ -1539,12 +1682,6 @@ create_using_templates_fixed (struct UseContext *uc)
? uc->template_contract.summary
: uc->parse_request.summary),
GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("fulfillment_url",
- uc->parse_request.fulfillment_url)),
- GNUNET_JSON_pack_allow_null (
- GNUNET_JSON_pack_string ("fulfillment_message",
- uc->parse_request.fulfillment_message)),
- GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_steal ("products",
tip_products)))));
}
@@ -1553,13 +1690,30 @@ create_using_templates_fixed (struct UseContext *uc)
/**
* Create order request for paivana templates.
*
- * @param uc use context
+ * @param[in,out] uc use context
*/
static void
create_using_templates_paivana (struct UseContext *uc)
{
- /* TODO: PAIVANA include paivana-specific order creation. */
- create_using_templates_fixed (uc);
+ uc->ihc.request_body
+ = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string (
+ "session_id",
+ uc->parse_request.paivana.paivana_id),
+ GNUNET_JSON_pack_object_steal (
+ "order",
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("version",
+ 1),
+ GNUNET_JSON_pack_array_incref ("choices",
+ uc->compute_price.choices),
+ GNUNET_JSON_pack_string (
+ "summary",
+ NULL == uc->parse_request.summary
+ ? uc->template_contract.summary
+ : uc->parse_request.summary),
+ GNUNET_JSON_pack_string ("fulfillment_url",
+ uc->parse_request.paivana.website))));
}
@@ -1573,7 +1727,6 @@ handle_phase_create_order (struct UseContext *uc)
create_using_templates_fixed (uc);
break;
case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
- /* TODO: PAIVANA handle paivana-specific instantiation. */
create_using_templates_paivana (uc);
break;
case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
@@ -1589,9 +1742,10 @@ handle_phase_create_order (struct UseContext *uc)
/* ***************** Main handler **************** */
MHD_RESULT
-TMH_post_using_templates_ID (const struct TMH_RequestHandler *rh,
- struct MHD_Connection *connection,
- struct TMH_HandlerContext *hc)
+TMH_post_using_templates_ID (
+ const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
{
struct UseContext *uc = hc->ctx;
diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h
@@ -283,9 +283,23 @@ struct TALER_MERCHANT_TemplateContractInventory
struct TALER_MERCHANT_TemplateContractPaivana
{
/**
- * Paivana reference ID.
+ * Paivana website regular expression.
+ * NULL to allow any site.
*/
- const char *paivana_id;
+ const char *website_regex;
+
+ /**
+ * Array of possible specific contracts the wallet/customer may choose
+ * from by selecting the respective index when signing the deposit
+ * confirmation.
+ */
+ struct TALER_MERCHANT_ContractChoice *choices;
+
+ /**
+ * Length of the @e choices array.
+ */
+ unsigned int choices_len;
+
};
/**
@@ -329,15 +343,21 @@ struct TALER_MERCHANT_TemplateContract
*/
struct GNUNET_TIME_Relative pay_duration;
- /**
- * Parsed fields for inventory templates.
- */
- struct TALER_MERCHANT_TemplateContractInventory inventory;
+ union
+ {
+
+ /**
+ * Parsed fields for inventory templates.
+ */
+ struct TALER_MERCHANT_TemplateContractInventory inventory;
+
+ /**
+ * Parsed fields for paivana templates.
+ */
+ struct TALER_MERCHANT_TemplateContractPaivana paivana;
+
+ } details;
- /**
- * Parsed fields for paivana templates.
- */
- struct TALER_MERCHANT_TemplateContractPaivana paivana;
};
/**
@@ -1281,6 +1301,21 @@ TALER_MERCHANT_json_spec_cit (const char *name,
/**
+ * Provide specification to parse given JSON array to contract terms
+ * choices. All fields from @a choices elements are copied.
+ *
+ * @param name name of the choices field in the JSON
+ * @param[out] choices where the contract choices array has to be written
+ * @param[out] choices_len length of the @a choices array
+ */
+struct GNUNET_JSON_Specification
+TALER_MERCHANT_spec_choices (
+ const char *name,
+ struct TALER_MERCHANT_ContractChoice **choices,
+ unsigned int *choices_len);
+
+
+/**
* Parse JSON contract terms choice input.
*
* @param[in] root JSON object containing choice input
diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c
@@ -362,7 +362,12 @@ parse_choices (
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
-
+ if (0 == json_array_size (root))
+ {
+ /* empty list of choices is not allowed */
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
GNUNET_array_grow (*choices,
*choices_len,
json_array_size (root));
@@ -487,16 +492,8 @@ parse_choices (
}
-/**
- * Provide specification to parse given JSON array to contract terms
- * choices. All fields from @a choices elements are copied.
- *
- * @param name name of the choices field in the JSON
- * @param[out] choices where the contract choices array has to be written
- * @param[out] choices_len length of the @a choices array
- */
-static struct GNUNET_JSON_Specification
-spec_choices (
+struct GNUNET_JSON_Specification
+TALER_MERCHANT_spec_choices (
const char *name,
struct TALER_MERCHANT_ContractChoice **choices,
unsigned int *choices_len)
@@ -1075,7 +1072,7 @@ parse_contract_v1 (
struct TALER_MERCHANT_Contract *contract)
{
struct GNUNET_JSON_Specification espec[] = {
- spec_choices (
+ TALER_MERCHANT_spec_choices (
"choices",
&contract->details.v1.choices,
&contract->details.v1.choices_len),
diff --git a/src/util/template_parse.c b/src/util/template_parse.c
@@ -26,6 +26,7 @@
#include <taler/taler_json_lib.h>
#include <taler/taler_util.h>
#include "taler_merchant_util.h"
+#include <regex.h>
enum TALER_MERCHANT_TemplateType
@@ -100,23 +101,24 @@ parse_template_inventory (const json_t *template_contract,
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_bool ("request_tip",
- &out->inventory.request_tip),
+ &out->details.inventory.request_tip),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_bool ("selected_all",
- &out->inventory.selected_all),
+ &out->details.inventory.selected_all),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("selected_categories",
- &out->inventory.selected_categories),
+ &out->details.inventory.selected_categories)
+ ,
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("selected_products",
- &out->inventory.selected_products),
+ &out->details.inventory.selected_products),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_bool ("choose_one",
- &out->inventory.choose_one),
+ &out->details.inventory.choose_one),
NULL),
GNUNET_JSON_spec_end ()
};
@@ -133,12 +135,13 @@ parse_template_inventory (const json_t *template_contract,
return GNUNET_SYSERR;
}
- if (NULL != out->inventory.selected_categories)
+ if (NULL != out->details.inventory.selected_categories)
{
const json_t *entry;
size_t idx;
- json_array_foreach ((json_t *) out->inventory.selected_categories, idx,
+ json_array_foreach ((json_t *) out->details.inventory.selected_categories,
+ idx,
entry)
{
if ( (! json_is_integer (entry)) ||
@@ -151,12 +154,14 @@ parse_template_inventory (const json_t *template_contract,
}
}
- if (NULL != out->inventory.selected_products)
+ if (NULL != out->details.inventory.selected_products)
{
const json_t *entry;
size_t idx;
- json_array_foreach ((json_t *) out->inventory.selected_products, idx, entry)
+ json_array_foreach ((json_t *) out->details.inventory.selected_products,
+ idx,
+ entry)
{
if (! json_is_string (entry))
{
@@ -184,8 +189,13 @@ parse_template_paivana (const json_t *template_contract,
const char **error_name)
{
struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_string ("paivana_id",
- &out->paivana.paivana_id),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("website_regex",
+ &out->details.paivana.website_regex),
+ NULL),
+ TALER_MERCHANT_spec_choices ("choices",
+ &out->details.paivana.choices,
+ &out->details.paivana.choices_len),
GNUNET_JSON_spec_end ()
};
@@ -200,6 +210,21 @@ parse_template_paivana (const json_t *template_contract,
*error_name);
return GNUNET_SYSERR;
}
+ if (NULL != out->details.paivana.website_regex)
+ {
+ regex_t ex;
+
+ if (0 != regcomp (&ex,
+ out->details.paivana.website_regex,
+ REG_NOSUB | REG_EXTENDED))
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Invalid paivana website_regex given\n");
+ return GNUNET_SYSERR;
+ }
+ regfree (&ex);
+ }
return GNUNET_OK;
}