merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 12951e288cba2b5fc647a12871f2ecce604e6bea
parent e4f7102c2ab178322f8d6f81746139f0d2230921
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 20 Jul 2025 16:46:03 +0200

attempt to fix #10194

Diffstat:
Msrc/backend/taler-merchant-httpd_config.c | 2+-
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 2380++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
2 files changed, 1325 insertions(+), 1057 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "21:0:0" +#define MERCHANT_PROTOCOL_VERSION "20:0:8" /** diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2024 Taler Systems SA + (C) 2014-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -157,6 +157,47 @@ struct RekeyExchange /** + * Data structure where we evaluate the viability of a given + * wire method for this order. + */ +struct WireMethodCandidate +{ + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *next; + + /** + * Kept in a DLL. + */ + struct WireMethodCandidate *prev; + + /** + * The wire method we are evaluating. + */ + const struct TMH_WireMethod *wm; + + /** + * List of exchanges to use when we use this wire method. + */ + json_t *exchanges; + + /** + * Array of maximum amounts that could be paid over all available exchanges + * for this @a wm. Used to determine if this order creation requests exceeds + * legal limits. + */ + struct TALER_Amount *total_exchange_limits; + + /** + * Length of the @e total_exchange_limits array. + */ + unsigned int num_total_exchange_limits; + +}; + + +/** * Information we keep per order we are processing. */ struct OrderContext @@ -232,19 +273,6 @@ struct OrderContext } parse_request; - - /** - * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase. - */ - struct - { - /** - * Wire method (and our bank account) we have selected - * to be included for this order. - */ - const struct TMH_WireMethod *wm; - } add_payment_details; - /** * Information set in the ORDER_PHASE_PARSE_ORDER phase. */ @@ -431,52 +459,78 @@ struct OrderContext } merge_inventory; /** - * Information set in the ORDER_PHASE_SET_EXCHANGES phase. + * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase. */ struct { + /** - * Array of exchanges we find acceptable for this - * order. + * DLL of wire methods under evaluation. */ - json_t *exchanges; + struct WireMethodCandidate *wmc_head; /** - * Forced requests to /keys to update our exchange - * information. + * DLL of wire methods under evaluation. */ - struct RekeyExchange *pending_reload_head; + struct WireMethodCandidate *wmc_tail; /** - * Forced requests to /keys to update our exchange - * information. + * Array of maximum amounts that appear in the contract choices + * per currency. + * Determines the maximum amounts that a client could pay for this + * order and which we must thus make sure is acceptable for the + * selected wire method/account if possible. */ - struct RekeyExchange *pending_reload_tail; + struct TALER_Amount *max_choice_limits; /** - * Did we previously force reloading of /keys from - * all exchanges? Set to 'true' to prevent us from - * doing it again (and again...). + * Length of the @e max_choice_limits array. */ - bool forced_reload; + unsigned int num_max_choice_limits; + + /** + * Set to true if we may need an exchange. True if any amount is non-zero. + */ + bool need_exchange; + + } add_payment_details; + + /** + * Information set in the ORDER_PHASE_SELECT_WIRE_METHOD phase. + */ + struct + { + + /** + * Array of exchanges we find acceptable for this order and wire method. + */ + json_t *exchanges; /** - * Set to true once we are sure that we have at - * least one good exchange. + * Wire method (and our bank account) we have selected + * to be included for this order. */ - bool exchange_good; + const struct TMH_WireMethod *wm; + + } select_wire_method; + + /** + * Information set in the ORDER_PHASE_SET_EXCHANGES phase. + */ + struct + { /** - * Array of maximum amounts that could be paid over all - * available exchanges. Used to determine if this - * order creation requests exceeds legal limits. + * Forced requests to /keys to update our exchange + * information. */ - struct TALER_Amount *total_exchange_limits; + struct RekeyExchange *pending_reload_head; /** - * Length of the @e total_exchange_limits array. + * Forced requests to /keys to update our exchange + * information. */ - unsigned int num_total_exchange_limits; + struct RekeyExchange *pending_reload_tail; /** * How long do we wait at most until giving up on getting keys? @@ -489,6 +543,19 @@ struct OrderContext struct GNUNET_SCHEDULER_Task *wakeup_task; /** + * Did we previously force reloading of /keys from + * all exchanges? Set to 'true' to prevent us from + * doing it again (and again...). + */ + bool forced_reload; + + /** + * Set to true once we have attempted to load exchanges + * for the first time. + */ + bool exchanges_tried; + + /** * Details depending on the contract version. */ union @@ -638,6 +705,7 @@ struct OrderContext ORDER_PHASE_MERGE_INVENTORY, ORDER_PHASE_ADD_PAYMENT_DETAILS, ORDER_PHASE_SET_EXCHANGES, + ORDER_PHASE_SELECT_WIRE_METHOD, ORDER_PHASE_SET_MAX_FEE, ORDER_PHASE_SERIALIZE_ORDER, ORDER_PHASE_SALT_FORGETTABLE, @@ -687,6 +755,66 @@ TMH_force_orders_resume () /** + * Add the given @a val to the @a array. Adds the + * amount to a given entry in @a array if one with the same + * currency exists, otherwise extends the @a array. + * + * @param[in,out] array pointer to array of amounts + * @param[in,out] array_len length of @a array + * @param val amount to add + * @param cap cap for the sums to enforce, can be NULL + */ +static void +add_to_currency_vector (struct TALER_Amount **array, + unsigned int *array_len, + const struct TALER_Amount *val, + const struct TALER_Amount *cap) +{ + for (unsigned int i = 0; i<*array_len; i++) + { + struct TALER_Amount *ai = &(*array)[i]; + + if (GNUNET_OK == + TALER_amount_cmp_currency (ai, + val)) + { + enum TALER_AmountArithmeticResult aar; + + aar = TALER_amount_add (ai, + ai, + val); + /* If we have a cap, we tolerate the overflow */ + GNUNET_assert ( (aar >= 0) || + ( (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) && + (NULL != cap) ) ); + if (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) + { + *ai = *cap; + } + else if (NULL != cap) + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + return; + } + } + GNUNET_array_append (*array, + *array_len, + *val); + if (NULL != cap) + { + struct TALER_Amount *ai = &(*array)[(*array_len) - 1]; + + GNUNET_assert (GNUNET_OK == + TALER_amount_min (ai, + ai, + cap)); + } +} + + +/** * Update the phase of @a oc based on @a mret. * * @param[in,out] oc order to update phase for @@ -748,6 +876,26 @@ reply_with_error (struct OrderContext *oc, /** + * Clean up memory used by @a wmc. + * + * @param[in,out] oc order context the WMC is part of + * @param[in] wmc wire method candidate to free + */ +static void +free_wmc (struct OrderContext *oc, + struct WireMethodCandidate *wmc) +{ + GNUNET_CONTAINER_DLL_remove (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + GNUNET_free (wmc); +} + + +/** * Clean up memory used by @a cls. * * @param[in] cls the `struct OrderContext` to clean up @@ -758,6 +906,9 @@ clean_order (void *cls) struct OrderContext *oc = cls; struct RekeyExchange *rx; + while (NULL != oc->add_payment_details.wmc_head) + free_wmc (oc, + oc->add_payment_details.wmc_head); while (NULL != (rx = oc->set_exchanges.pending_reload_head)) { GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, @@ -767,17 +918,19 @@ clean_order (void *cls) GNUNET_free (rx->url); GNUNET_free (rx); } + GNUNET_array_grow (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + 0); if (NULL != oc->set_exchanges.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } - if (NULL != oc->set_exchanges.exchanges) + if (NULL != oc->select_wire_method.exchanges) { - json_decref (oc->set_exchanges.exchanges); - oc->set_exchanges.exchanges = NULL; + json_decref (oc->select_wire_method.exchanges); + oc->select_wire_method.exchanges = NULL; } - GNUNET_free (oc->set_exchanges.total_exchange_limits); switch (oc->parse_order.version) { case TALER_MERCHANT_CONTRACT_VERSION_0: @@ -855,6 +1008,8 @@ clean_order (void *cls) } +/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */ + /** * Execute the database transaction to setup the order. * @@ -1035,7 +1190,7 @@ execute_transaction (struct OrderContext *oc) * @param[in,out] oc order context */ static void -execute_order (struct OrderContext *oc) +phase_execute_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; @@ -1229,6 +1384,9 @@ execute_order (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_CHECK_CONTRACT **************** */ + + /** * Check that the contract is now well-formed. Upon success, continue * processing with execute_order(). @@ -1236,7 +1394,7 @@ execute_order (struct OrderContext *oc) * @param[in,out] oc order context */ static void -check_contract (struct OrderContext *oc) +phase_check_contract (struct OrderContext *oc) { struct TALER_PrivateContractHashP h_control; @@ -1273,6 +1431,9 @@ check_contract (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SALT_FORGETTABLE **************** */ + + /** * Modify the final contract terms adding salts for * items that are forgettable. @@ -1280,7 +1441,7 @@ check_contract (struct OrderContext *oc) * @param[in,out] oc order context */ static void -salt_forgettable (struct OrderContext *oc) +phase_salt_forgettable (struct OrderContext *oc) { if (GNUNET_OK != TALER_JSON_contract_seed_forgettable (oc->parse_request.order, @@ -1298,6 +1459,8 @@ salt_forgettable (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SERIALIZE_ORDER **************** */ + /** * Get rounded time interval. @a start is calculated by rounding * @a ts down to the nearest multiple of @a precision. @@ -1959,7 +2122,7 @@ output_contract_choices (struct OrderContext *oc) * @param[in,out] oc order context */ static void -serialize_order (struct OrderContext *oc) +phase_serialize_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; @@ -2037,9 +2200,9 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_array_incref ("products", oc->merge_inventory.products), GNUNET_JSON_pack_data_auto ("h_wire", - &oc->add_payment_details.wm->h_wire), + &oc->select_wire_method.wm->h_wire), GNUNET_JSON_pack_string ("wire_method", - oc->add_payment_details.wm->wire_method), + oc->select_wire_method.wm->wire_method), GNUNET_JSON_pack_string ("order_id", oc->parse_order.order_id), GNUNET_JSON_pack_timestamp ("timestamp", @@ -2062,7 +2225,7 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_data_auto ("merchant_pub", &oc->hc->instance->merchant_pub), GNUNET_JSON_pack_array_incref ("exchanges", - oc->set_exchanges.exchanges), + oc->select_wire_method.exchanges), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) @@ -2129,6 +2292,9 @@ serialize_order (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SET_MAX_FEE **************** */ + + /** * Set @a max_fee in @a oc based on @a max_stefan_fee value if not overridden * by @a client_fee. If neither is set, set the fee to zero using currency @@ -2178,7 +2344,7 @@ compute_fee (struct OrderContext *oc, * @param[in,out] oc order context */ static void -set_max_fee (struct OrderContext *oc) +phase_set_max_fee (struct OrderContext *oc) { switch (oc->parse_order.version) { @@ -2210,6 +2376,159 @@ set_max_fee (struct OrderContext *oc) } +/* ***************** ORDER_PHASE_SELECT_WIRE_METHOD **************** */ + +/** + * Check that the @a brutto amount is at or below the + * limits we have for the respective wire method candidate. + * + * @param wmc wire method candidate to check + * @param brutto amount to check + * @param true if the amount is OK, false if it is too high + */ +static bool +check_limits (struct WireMethodCandidate *wmc, + const struct TALER_Amount *brutto) +{ + for (unsigned int i = 0; i<wmc->num_total_exchange_limits; i++) + { + const struct TALER_Amount *total_exchange_limit + = &wmc->total_exchange_limits[i]; + + if (GNUNET_OK != + TALER_amount_cmp_currency (brutto, + total_exchange_limit)) + continue; + if (1 != + TALER_amount_cmp (brutto, + total_exchange_limit)) + return true; + } + return false; +} + + +/** + * Phase to select a wire method that will be acceptable for the order. + * If none is "perfect" (allows all choices), might jump back to the + * previous phase to force "/keys" downloads to see if that helps. + * + * @param[in,out] oc order context + */ +static void +phase_select_wire_method (struct OrderContext *oc) +{ + const struct TALER_Amount *ea; + struct WireMethodCandidate *best = NULL; + unsigned int max_choices = 0; + unsigned int want_choices; + + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + unsigned int num_choices = 0; + + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + want_choices = 1; + ea = &oc->parse_order.details.v0.brutto; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + want_choices = oc->parse_choices.choices_len; + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + ea = &oc->parse_choices.choices[i].amount; + if (TALER_amount_is_zero (ea) || + check_limits (wmc, + ea)) + num_choices++; + } + break; + default: + GNUNET_assert (0); + } + if (num_choices > max_choices) + { + best = wmc; + max_choices = num_choices; + } + } + + if ( (want_choices > max_choices) && + (! oc->set_exchanges.forced_reload) ) + { + /* Not all choices in the contract can work with these + exchanges, try again with forcing /keys download */ + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + { + json_array_clear (wmc->exchanges); + GNUNET_array_grow (wmc->total_exchange_limits, + wmc->num_total_exchange_limits, + 0); + } + oc->phase = ORDER_PHASE_SET_EXCHANGES; + return; + } + + if ( (NULL == best) && + (NULL != oc->parse_request.payment_target) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Cannot create order: lacking suitable exchanges for payment target `%s'\n", + oc->parse_request.payment_target); + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, + oc->parse_request.payment_target); + return; + } + + if (NULL == best) + { + /* We actually do not have ANY workable exchange(s) */ + reply_with_error ( + oc, + MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, + NULL); + return; + } + + if (want_choices > max_choices) + { + /* Some choices are unpayable */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but some choices do not work with the selected wire method\n"); + } + if ( (0 == json_array_size (best->exchanges)) && + (oc->add_payment_details.need_exchange) ) + { + /* We did not find any reasonable exchange */ + GNUNET_log ( + GNUNET_ERROR_TYPE_WARNING, + "Creating order, but only for choices without payment\n"); + } + + oc->select_wire_method.wm + = best->wm; + oc->select_wire_method.exchanges + = json_incref (best->exchanges); + oc->phase++; +} + + +/* ***************** ORDER_PHASE_SET_EXCHANGES **************** */ + /** * Exchange `/keys` processing is done, resume handling * the order. @@ -2220,8 +2539,7 @@ static void resume_with_keys (struct OrderContext *oc) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Resuming order processing after /keys downloads (now have %u accounts)\n", - (unsigned int) json_array_size (oc->set_exchanges.exchanges)); + "Resuming order processing after /keys downloads\n"); GNUNET_assert (GNUNET_YES == oc->suspended); GNUNET_CONTAINER_DLL_remove (oc_head, oc_tail, @@ -2321,10 +2639,12 @@ update_stefan (struct OrderContext *oc, * too low and we failed to create an order. * * @param oc order context + * @param wmc wire method candidate to notify for * @param exchange_url exchange to notify about */ static void notify_kyc_required (const struct OrderContext *oc, + const struct WireMethodCandidate *wmc, const char *exchange_url) { struct GNUNET_DB_EventHeaderP es = { @@ -2335,8 +2655,8 @@ notify_kyc_required (const struct OrderContext *oc, char *extra; hws = GNUNET_STRINGS_data_to_string_alloc ( - &oc->add_payment_details.wm->h_wire, - sizeof (oc->add_payment_details.wm->h_wire)); + &wmc->wm->h_wire, + sizeof (wmc->wm->h_wire)); GNUNET_asprintf (&extra, "%s %s", @@ -2352,35 +2672,54 @@ notify_kyc_required (const struct OrderContext *oc, /** - * Compute the set of exchanges that would be acceptable - * for this order. + * Checks the limits that apply for this @a exchange and + * the @a wmc and if the exchange is acceptable at all, adds it + * to the list of exchanges for the @a wmc. * - * @param cls our `struct OrderContext` - * @param url base URL of an exchange (not used) + * @param oc context of the order * @param exchange internal handle for the exchange - * @param max_needed maximum amount needed in this currency + * @param exchange_url base URL of this exchange + * @param wmc wire method to evaluate this exchange for + * @return true if the exchange is acceptable for the contract */ -static void -get_acceptable (void *cls, - const char *url, +static bool +get_acceptable (struct OrderContext *oc, const struct TMH_Exchange *exchange, - const struct TALER_Amount *max_needed) + const char *exchange_url, + struct WireMethodCandidate *wmc) { - struct OrderContext *oc = cls; + const struct TALER_Amount *max_needed = NULL; unsigned int priority = 42; /* make compiler happy */ json_t *j_exchange; enum GNUNET_GenericReturnValue res; struct TALER_Amount max_amount; + for (unsigned int i = 0; i<oc->add_payment_details.num_max_choice_limits; i++) + { + struct TALER_Amount *val = &oc->add_payment_details.max_choice_limits[i]; + + if (0 == strcasecmp (val->currency, + TMH_EXCHANGES_get_currency (exchange))) + { + max_needed = val; + break; + } + } + if (NULL == max_needed) + { + /* exchange currency not relevant for any of our choices, skip it */ + return false; + } + max_amount = *max_needed; res = TMH_exchange_check_debit ( oc->hc->instance->settings.id, exchange, - oc->add_payment_details.wm, + wmc->wm, &max_amount); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s evaluated at %d with max %s\n", - url, + exchange_url, res, TALER_amount2s (&max_amount)); if (TALER_amount_is_zero (&max_amount)) @@ -2390,20 +2729,20 @@ get_acceptable (void *cls, /* Trigger re-checking the current deposit limit when * paying non-zero amount with zero deposit limit */ notify_kyc_required (oc, - url); + wmc, + exchange_url); } /* If deposit is impossible, we don't list the * exchange in the contract terms. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", - url); - return; + exchange_url); + return false; } switch (res) { case GNUNET_OK: priority = 1024; /* high */ - oc->set_exchanges.exchange_good = true; break; case GNUNET_NO: if (oc->set_exchanges.forced_reload) @@ -2420,47 +2759,12 @@ get_acceptable (void *cls, } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is %s, adding it!\n", - url, + exchange_url, TALER_amount2s (&max_amount)); - { - bool found = false; - - for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++) - { - struct TALER_Amount *limit - = &oc->set_exchanges.total_exchange_limits[i]; - if (GNUNET_OK == - TALER_amount_cmp_currency (limit, - &max_amount)) - { - GNUNET_assert (0 <= - TALER_amount_add (limit, - limit, - &max_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_min (limit, - limit, - max_needed)); - found = true; - } - } - if (! found) - { - struct TALER_Amount limit; - - GNUNET_assert (GNUNET_OK == - TALER_amount_min (&limit, - &max_amount, - max_needed)); - GNUNET_array_append (oc->set_exchanges.total_exchange_limits, - oc->set_exchanges.num_total_exchange_limits, - limit); - } - } j_exchange = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("url", - url), + exchange_url), GNUNET_JSON_pack_uint64 ("priority", priority), TALER_JSON_pack_amount ("max_contribution", @@ -2468,9 +2772,16 @@ get_acceptable (void *cls, GNUNET_JSON_pack_data_auto ("master_pub", TMH_EXCHANGES_get_master_pub (exchange))); GNUNET_assert (NULL != j_exchange); + /* Add exchange to list of exchanges for this wire method + candidate */ GNUNET_assert (0 == - json_array_append_new (oc->set_exchanges.exchanges, + json_array_append_new (wmc->exchanges, j_exchange)); + add_to_currency_vector (&wmc->total_exchange_limits, + &wmc->num_total_exchange_limits, + &max_amount, + max_needed); + return true; } @@ -2492,6 +2803,7 @@ keys_cb ( struct OrderContext *oc = rx->oc; const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; + bool applicable = false; rx->fo = NULL; GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head, @@ -2502,63 +2814,38 @@ keys_cb ( GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to download %skeys\n", rx->url); + goto cleanup; } - else + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response for %skeys\n", + rx->url); + + /* Evaluate the use of this exchange for each wire method candidate */ + for (unsigned int j = 0; j<keys->accounts_len; j++) { - bool currency_ok = false; - struct TALER_Amount max_needed; + struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri; + char *wire_method = TALER_payto_get_method (full_payto.full_payto); - switch (oc->parse_order.version) + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - if (0 == strcasecmp (keys->currency, - oc->parse_order.details.v0.brutto.currency)) + if (0 == strcmp (wmc->wm->wire_method, + wire_method) ) { - max_needed = oc->parse_order.details.v0.brutto; - currency_ok = true; + applicable |= get_acceptable (oc, + exchange, + rx->url, + wmc); } - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - { - const struct TALER_Amount *amount - = &oc->parse_choices.choices[i].amount; - - if (0 == strcasecmp (keys->currency, - amount->currency)) - { - if (currency_ok) - { - TALER_amount_max (&max_needed, - &max_needed, - amount); - } - else - { - max_needed = *amount; - currency_ok = true; - } - } - } - break; - default: - GNUNET_assert (0); - } - if ( (currency_ok) && - (! TALER_amount_is_zero (&max_needed)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response for %skeys\n", - rx->url); - if (settings->use_stefan) - update_stefan (oc, - keys); - get_acceptable (oc, - rx->url, - exchange, - &max_needed); } + GNUNET_free (wire_method); } + if (applicable && + settings->use_stefan) + update_stefan (oc, + keys); +cleanup: GNUNET_free (rx->url); GNUNET_free (rx); if (NULL != oc->set_exchanges.pending_reload_head) @@ -2624,102 +2911,49 @@ wakeup_timeout (void *cls) /** - * Check that the @a brutto amount is at or below the exchange - * limits we have for the respective currency. - * - * @param oc order context to check - * @param brutto amount to check - * @param true if the amount is OK, false if it is too high - */ -static bool -check_exchange_limits (const struct OrderContext *oc, - struct TALER_Amount *brutto) -{ - for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++) - { - const struct TALER_Amount *total_exchange_limit - = &oc->set_exchanges.total_exchange_limits[i]; - - if (GNUNET_OK != - TALER_amount_cmp_currency (brutto, - total_exchange_limit)) - continue; - if (1 != - TALER_amount_cmp (brutto, - total_exchange_limit)) - return true; - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: %s is above the sum of hard limits from supported exchanges\n", - TALER_amount2s (brutto)); - return false; -} - - -/** - * Set list of acceptable exchanges in @a oc. Upon success, continue - * processing with set_max_fee(). + * Set list of acceptable exchanges in @a oc. Upon success, continues + * processing with add_payment_details(). * * @param[in,out] oc order context * @return true to suspend execution */ static bool -set_exchanges (struct OrderContext *oc) +phase_set_exchanges (struct OrderContext *oc) { - bool need_exchange; - if (NULL != oc->set_exchanges.wakeup_task) { GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task); oc->set_exchanges.wakeup_task = NULL; } - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - need_exchange = ! TALER_amount_is_zero ( - &oc->parse_order.details.v0.brutto); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - need_exchange = false; - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) - if (! TALER_amount_is_zero (&oc->parse_choices.choices[i].amount)) - { - need_exchange = true; - break; - } - break; - default: - GNUNET_assert (0); - } - if (! need_exchange) + + if (! oc->add_payment_details.need_exchange) { /* Total amount is zero, so we don't actually need exchanges! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order total is zero, no need for exchanges\n"); - oc->set_exchanges.exchanges = json_array (); - GNUNET_assert (NULL != oc->set_exchanges.exchanges); - oc->phase++; + oc->select_wire_method.exchanges = json_array (); + GNUNET_assert (NULL != oc->select_wire_method.exchanges); + /* Pick first one, doesn't matter as the amount is zero */ + oc->select_wire_method.wm = oc->hc->instance->wm_head; + oc->phase = ORDER_PHASE_SET_MAX_FEE; return false; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Trying to find exchanges\n"); - if (NULL == oc->set_exchanges.exchanges) - { - oc->set_exchanges.keys_timeout - = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); - oc->set_exchanges.exchanges = json_array (); - GNUNET_assert (NULL != oc->set_exchanges.exchanges); - TMH_exchange_get_trusted (&get_exchange_keys, - oc); - } - else if (! oc->set_exchanges.exchange_good) + if (NULL == oc->set_exchanges.pending_reload_head) { - if (! oc->set_exchanges.forced_reload) + if (! oc->set_exchanges.exchanges_tried) + { + oc->set_exchanges.exchanges_tried = true; + oc->set_exchanges.keys_timeout + = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT); + TMH_exchange_get_trusted (&get_exchange_keys, + oc); + } + else if (! oc->set_exchanges.forced_reload) { + /* Try one more time with forcing /keys download */ oc->set_exchanges.forced_reload = true; - GNUNET_assert (0 == - json_array_clear (oc->set_exchanges.exchanges)); TMH_exchange_get_trusted (&get_exchange_keys, oc); } @@ -2754,616 +2988,276 @@ set_exchanges (struct OrderContext *oc) oc); return true; /* reloads pending */ } - if (0 == json_array_size (oc->set_exchanges.exchanges)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: lacking trusted exchanges\n"); - reply_with_error ( - oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD, - oc->add_payment_details.wm->wire_method); - return false; - } + oc->phase++; + return false; +} - { - bool ok; - struct TALER_Amount ea; - switch (oc->parse_order.version) +/* ***************** ORDER_PHASE_ADD_PAYMENT_DETAILS **************** */ + +/** + * Process the @a payment_target and add the details of how the + * order could be paid to @a order. On success, continue + * processing with add_payment_fees(). + * + * @param[in,out] oc order context + */ +static void +phase_add_payment_details (struct OrderContext *oc) +{ + /* First, determine the maximum amounts that could be paid per currency */ + switch (oc->parse_order.version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + oc->parse_order.details.v0.brutto); + if (! TALER_amount_is_zero ( + &oc->parse_order.details.v0.brutto)) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - ea = oc->parse_order.details.v0.brutto; - ok = check_exchange_limits (oc, - &ea); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - ok = true; - for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + oc->add_payment_details.need_exchange = true; + } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + const struct TALER_Amount *amount + = &oc->parse_choices.choices[i].amount; + bool found = false; + + if (! TALER_amount_is_zero (amount)) { - ea = oc->parse_choices.choices[i].amount; - if (! check_exchange_limits (oc, - &ea)) + oc->add_payment_details.need_exchange = true; + } + for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits; + j++) + { + struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j]; + if (GNUNET_YES == + TALER_amount_cmp_currency (mx, + amount)) { - ok = false; + TALER_amount_max (mx, + mx, + amount); + found = true; break; } } - break; - default: - GNUNET_assert (0); + if (! found) + { + GNUNET_array_append (oc->add_payment_details.max_choice_limits, + oc->add_payment_details.num_max_choice_limits, + *amount); + } } + break; + default: + GNUNET_assert (0); + } - if (! ok) - { - reply_with_error ( - oc, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, - TALER_amount2s (&ea)); - return false; - } + /* Then, create a candidate for each available wire method */ + for (struct TMH_WireMethod *wm = oc->hc->instance->wm_head; + NULL != wm; + wm = wm->next) + { + struct WireMethodCandidate *wmc; + + /* Locate wire method that has a matching payment target */ + if (! wm->active) + continue; /* ignore inactive methods */ + if ( (NULL != oc->parse_request.payment_target) && + (0 != strcasecmp (oc->parse_request.payment_target, + wm->wire_method) ) ) + continue; /* honor client preference */ + wmc = GNUNET_new (struct WireMethodCandidate); + wmc->wm = wm; + wmc->exchanges = json_array (); + GNUNET_assert (NULL != wmc->exchanges); + GNUNET_CONTAINER_DLL_insert (oc->add_payment_details.wmc_head, + oc->add_payment_details.wmc_tail, + wmc); } - if (! oc->set_exchanges.exchange_good) + if (NULL == oc->add_payment_details.wmc_head) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Creating order, but possibly without usable trusted exchanges\n"); + "No wire method available for instance '%s'\n", + oc->hc->instance->settings.id); + reply_with_error (oc, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, + oc->parse_request.payment_target); + return; } + + /* next, we'll evaluate available exchanges */ oc->phase++; - return false; } +/* ***************** ORDER_PHASE_MERGE_INVENTORY **************** */ + + /** - * Parse the order field of the request. Upon success, continue - * processing with parse_choices(). + * Merge the inventory products into products, querying the + * database about the details of those products. Upon success, + * continue processing by calling add_payment_details(). * - * @param[in,out] oc order context + * @param[in,out] oc order context to process */ static void -parse_order (struct OrderContext *oc) +phase_merge_inventory (struct OrderContext *oc) { - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - const char *merchant_base_url = NULL; - uint64_t version = 0; - const json_t *jmerchant = NULL; - const char *order_id = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("version", - &version), - NULL), - GNUNET_JSON_spec_string ("summary", - &oc->parse_order.summary), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("products", - &oc->parse_order.products), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("summary_i18n", - &oc->parse_order.summary_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("order_id", - &order_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_message", - &oc->parse_order.fulfillment_message), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("fulfillment_message_i18n", - &oc->parse_order.fulfillment_message_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_url", - &oc->parse_order.fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("public_reorder_url", - &oc->parse_order.public_reorder_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("merchant_base_url", - &merchant_base_url), - NULL), - /* For sanity check, this field must NOT be present */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("merchant", - &jmerchant), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("timestamp", - &oc->parse_order.timestamp), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &oc->parse_order.refund_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("pay_deadline", - &oc->parse_order.pay_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &oc->parse_order.wire_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("delivery_location", - &oc->parse_order.delivery_location), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &oc->parse_order.delivery_date), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &oc->parse_order.minimum_age), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("auto_refund", - &oc->parse_order.auto_refund), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("extra", - &oc->parse_order.extra), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - spec); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - if (NULL != order_id) + /** + * parse_request.inventory_products => instructions to add products to contract terms + * parse_order.products => contains products that are not from the backend-managed inventory. + */ + if (NULL != oc->parse_order.products) + oc->merge_inventory.products + = json_deep_copy (oc->parse_order.products); + else + oc->merge_inventory.products + = json_array (); + /* Populate products from inventory product array and database */ { - size_t len = strlen (order_id); - - for (size_t i = 0; i<len; i++) + GNUNET_assert (NULL != oc->merge_inventory.products); + for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) { - char c = order_id[i]; + const struct InventoryProduct *ip + = &oc->parse_request.inventory_products[i]; + struct TALER_MERCHANTDB_ProductDetails pd; + enum GNUNET_DB_QueryStatus qs; + size_t num_categories = 0; + uint64_t *categories = NULL; - if (! ( ( (c >= 'A') && - (c <= 'Z') ) || - ( (c >= 'a') && - (c <= 'z') ) || - ( (c >= '0') && - (c <= '9') ) || - (c == '-') || - (c == '_') || - (c == '.') || - (c == ':') ) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Invalid character `%c' in order ID `%s'\n", - c, - order_id); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "Invalid character in order_id"); - return; - } - } - } - switch (version) - { - case 0: - { - bool no_fee; - const json_t *choices = NULL; - struct GNUNET_JSON_Specification specv0[] = { - TALER_JSON_spec_amount_any ( - "amount", - &oc->parse_order.details.v0.brutto), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ( - "max_fee", - &oc->parse_order.details.v0.max_fee), - &no_fee), - /* for sanity check, must be *absent*! */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("choices", - &choices), - NULL), - GNUNET_JSON_spec_end () - }; - - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - specv0); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.max_fee)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "different currencies used for 'max_fee' and 'amount' currency"); - return; - } - if (! TMH_test_exchange_configured_for_currency ( - oc->parse_order.details.v0.brutto.currency)) + qs = TMH_db->lookup_product (TMH_db->cls, + oc->hc->instance->settings.id, + ip->product_id, + &pd, + &num_categories, + &categories); + if (qs <= 0) { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); + enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; + unsigned int http_status = 0; + + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_FETCH_FAILED; + break; + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; + break; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product %s from order unknown\n", + ip->product_id); + http_status = MHD_HTTP_NOT_FOUND; + ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; + break; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + /* case listed to make compilers happy */ + GNUNET_assert (0); + } reply_with_error (oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - oc->parse_order.details.v0.brutto.currency); + http_status, + ec, + ip->product_id); return; } - if (NULL != choices) + GNUNET_free (categories); + oc->parse_order.minimum_age + = GNUNET_MAX (oc->parse_order.minimum_age, + pd.minimum_age); { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, - "choices array must be null for v0 contracts"); - return; - } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0; - break; - } - case 1: - { - struct GNUNET_JSON_Specification specv1[] = { - GNUNET_JSON_spec_array_const ( - "choices", - &oc->parse_order.details.v1.choices), - GNUNET_JSON_spec_end () - }; + json_t *p; - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - specv1); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; + p = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_name", + pd.product_name), + GNUNET_JSON_pack_string ("description", + pd.description), + GNUNET_JSON_pack_object_steal ("description_i18n", + pd.description_i18n), + GNUNET_JSON_pack_string ("unit", + pd.unit), + TALER_JSON_pack_amount ("price", + &pd.price), + GNUNET_JSON_pack_array_steal ("taxes", + pd.taxes), + GNUNET_JSON_pack_string ("image", + pd.image), + GNUNET_JSON_pack_uint64 ( + "quantity", + ip->quantity)); + GNUNET_assert (NULL != p); + GNUNET_assert (0 == + json_array_append_new (oc->merge_inventory.products, + p)); } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1; - break; + GNUNET_free (pd.description); + GNUNET_free (pd.unit); + GNUNET_free (pd.image); + json_decref (pd.address); } - default: + } + /* check if final product list is well-formed */ + if (! TMH_products_array_valid (oc->merge_inventory.products)) + { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_VERSION_MALFORMED, - "invalid version specified in order, supported are null, '0' or '1'"); + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order:products"); return; } + oc->phase++; +} - /* Add order_id if it doesn't exist. */ - if (NULL != order_id) - { - oc->parse_order.order_id = GNUNET_strdup (order_id); - } - else - { - char buf[256]; - time_t timer; - struct tm *tm_info; - size_t off; - uint64_t rand; - char *last; - - time (&timer); - tm_info = localtime (&timer); - if (NULL == tm_info) - { - GNUNET_JSON_parse_free (spec); - reply_with_error ( - oc, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, - NULL); - return; - } - off = strftime (buf, - sizeof (buf) - 1, - "%Y.%j", - tm_info); - /* Check for error state of strftime */ - GNUNET_assert (0 != off); - buf[off++] = '-'; - rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, - UINT64_MAX); - last = GNUNET_STRINGS_data_to_string (&rand, - sizeof (uint64_t), - &buf[off], - sizeof (buf) - off); - GNUNET_assert (NULL != last); - *last = '\0'; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Assigning order ID `%s' server-side\n", - buf); +/* ***************** ORDER_PHASE_PARSE_CHOICES **************** */ - oc->parse_order.order_id = GNUNET_strdup (buf); - GNUNET_assert (NULL != oc->parse_order.order_id); - } +/** + * Parse contract choices. Upon success, continue + * processing with merge_inventory(). + * + * @param[in,out] oc order context + */ +static void +phase_parse_choices (struct OrderContext *oc) +{ + const json_t *jchoices; - /* Patch fulfillment URL with order_id (implements #6467). */ - if (NULL != oc->parse_order.fulfillment_url) + switch (oc->parse_order.version) { - const char *pos; + case TALER_MERCHANT_CONTRACT_VERSION_0: + oc->phase++; + return; + case TALER_MERCHANT_CONTRACT_VERSION_1: + /* handle below */ + break; + default: + GNUNET_assert (0); + } - pos = strstr (oc->parse_order.fulfillment_url, - "${ORDER_ID}"); - if (NULL != pos) - { - /* replace ${ORDER_ID} with the real order_id */ - char *nurl; + jchoices = oc->parse_order.details.v1.choices; - /* We only allow one placeholder */ - if (strstr (pos + strlen ("${ORDER_ID}"), - "${ORDER_ID}")) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "fulfillment_url"); - return; - } - - GNUNET_asprintf (&nurl, - "%.*s%s%s", - /* first output URL until ${ORDER_ID} */ - (int) (pos - oc->parse_order.fulfillment_url), - oc->parse_order.fulfillment_url, - /* replace ${ORDER_ID} with the right order_id */ - oc->parse_order.order_id, - /* append rest of original URL */ - pos + strlen ("${ORDER_ID}")); - - oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); - - GNUNET_free (nurl); - } - } - - /* Check soundness of refund deadline, and that a timestamp - * is actually present. */ - { - struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - - /* Add timestamp if it doesn't exist (or is zero) */ - if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) - { - oc->parse_order.timestamp = now; - } - - /* If no refund_deadline given, set one based on refund_delay. */ - if (GNUNET_TIME_absolute_is_never ( - oc->parse_order.refund_deadline.abs_time)) - { - if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Refund delay is zero, no refunds are possible for this order\n"); - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; - } - else - { - oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp ( - oc->parse_request.refund_delay); - } - } - - if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.delivery_date.abs_time)) && - (GNUNET_TIME_absolute_is_past ( - oc->parse_order.delivery_date.abs_time)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, - NULL); - return; - } - } - - if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) || - (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) ) - { - oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( - settings->default_pay_delay); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Pay deadline was zero (or never), setting to %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); - } - else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST, - NULL); - return; - } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Pay deadline is %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); - if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.refund_deadline.abs_time)) && - (GNUNET_TIME_absolute_is_past ( - oc->parse_order.refund_deadline.abs_time)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST, - NULL); - return; - } - - if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) - { - struct GNUNET_TIME_Timestamp t; - - t = GNUNET_TIME_relative_to_timestamp ( - GNUNET_TIME_relative_max (settings->default_wire_transfer_delay, - oc->parse_request.refund_delay)); - oc->parse_order.wire_deadline = GNUNET_TIME_timestamp_max ( - oc->parse_order.refund_deadline, - t); - if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, - "order:wire_transfer_deadline"); - return; - } - } - if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, - <, - oc->parse_order.refund_deadline)) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, - "order:wire_transfer_deadline;order:refund_deadline"); - return; - } - - if (NULL != merchant_base_url) - { - if (('\0' == *merchant_base_url) || - ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "merchant_base_url is not valid"); - return; - } - oc->parse_order.merchant_base_url - = GNUNET_strdup (merchant_base_url); - } - else - { - char *url; - - url = make_merchant_base_url (oc->connection, - settings->id); - if (NULL == url) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MISSING, - "order:merchant_base_url"); - return; - } - oc->parse_order.merchant_base_url = url; - } - - if ( (NULL != oc->parse_order.products) && - (! TMH_products_array_valid (oc->parse_order.products)) ) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.products"); - return; - } - - /* Merchant information must not already be present */ - if (NULL != jmerchant) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "'merchant' field already set, but must be provided by backend"); - return; - } - - if ( (NULL != oc->parse_order.delivery_location) && - (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) + if (! json_is_array (jchoices)) + GNUNET_assert (0); + if (0 == json_array_size (jchoices)) { - GNUNET_break_op (0); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "delivery_location"); - return; - } - - oc->phase++; -} - - -/** - * Parse contract choices. Upon success, continue - * processing with merge_inventory(). - * - * @param[in,out] oc order context - */ -static void -parse_choices (struct OrderContext *oc) -{ - const json_t *jchoices; - - switch (oc->parse_order.version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - oc->phase++; + "choices"); return; - case TALER_MERCHANT_CONTRACT_VERSION_1: - /* handle below */ - break; - default: - GNUNET_assert (0); } - - jchoices = oc->parse_order.details.v1.choices; - - if (! json_is_array (jchoices)) - GNUNET_assert (0); - GNUNET_array_grow (oc->parse_choices.choices, oc->parse_choices.choices_len, json_array_size (jchoices)); @@ -3418,307 +3312,675 @@ parse_choices (struct OrderContext *oc) TALER_amount_cmp_currency (&choice->amount, &choice->max_fee)) ) { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "different currencies used for 'max_fee' and 'amount' currency"); - return; + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; + } + + if (! TMH_test_exchange_configured_for_currency ( + choice->amount.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + choice->amount.currency); + return; + } + + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_ContractInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_input ((json_t *) jinput, + &input, + idx, + true)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "input"); + return; + } + + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; + + if (GNUNET_OK != + add_input_token_family (oc, + input.details.token.token_family_slug)) + + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + input.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + continue; + } + GNUNET_assert (0); + } + } + + if (NULL != joutputs) + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_output ((json_t *) joutput, + &output, + idx, + true)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output"); + return; + } + + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + GNUNET_break (0); /* FIXME-#9059: not yet implemented! */ + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + + if (0 == output.details.token.valid_at.abs_time.abs_value_us) + output.details.token.valid_at + = GNUNET_TIME_timestamp_get (); + if (GNUNET_OK != + add_output_token_family (oc, + output.details.token.token_family_slug, + output.details.token.valid_at, + &output.details.token.key_index)) + + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + output.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + continue; + } + GNUNET_assert (0); + } + } + } + oc->phase++; +} + + +/* ***************** ORDER_PHASE_PARSE_ORDER **************** */ + + +/** + * Parse the order field of the request. Upon success, continue + * processing with parse_choices(). + * + * @param[in,out] oc order context + */ +static void +phase_parse_order (struct OrderContext *oc) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = + &oc->hc->instance->settings; + const char *merchant_base_url = NULL; + uint64_t version = 0; + const json_t *jmerchant = NULL; + const char *order_id = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("version", + &version), + NULL), + GNUNET_JSON_spec_string ("summary", + &oc->parse_order.summary), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("products", + &oc->parse_order.products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("summary_i18n", + &oc->parse_order.summary_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("order_id", + &order_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("fulfillment_message", + &oc->parse_order.fulfillment_message), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("fulfillment_message_i18n", + &oc->parse_order.fulfillment_message_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("fulfillment_url", + &oc->parse_order.fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("public_reorder_url", + &oc->parse_order.public_reorder_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url ("merchant_base_url", + &merchant_base_url), + NULL), + /* For sanity check, this field must NOT be present */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("merchant", + &jmerchant), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("timestamp", + &oc->parse_order.timestamp), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &oc->parse_order.refund_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("pay_deadline", + &oc->parse_order.pay_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &oc->parse_order.wire_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("delivery_location", + &oc->parse_order.delivery_location), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("delivery_date", + &oc->parse_order.delivery_date), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("minimum_age", + &oc->parse_order.minimum_age), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("auto_refund", + &oc->parse_order.auto_refund), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("extra", + &oc->parse_order.extra), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue ret; + + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + spec); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + if (NULL != order_id) + { + size_t len = strlen (order_id); + + for (size_t i = 0; i<len; i++) + { + char c = order_id[i]; + + if (! ( ( (c >= 'A') && + (c <= 'Z') ) || + ( (c >= 'a') && + (c <= 'z') ) || + ( (c >= '0') && + (c <= '9') ) || + (c == '-') || + (c == '_') || + (c == '.') || + (c == ':') ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid character `%c' in order ID `%s'\n", + c, + order_id); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "Invalid character in order_id"); + return; + } + } + } + switch (version) + { + case 0: + { + bool no_fee; + const json_t *choices = NULL; + struct GNUNET_JSON_Specification specv0[] = { + TALER_JSON_spec_amount_any ( + "amount", + &oc->parse_order.details.v0.brutto), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ( + "max_fee", + &oc->parse_order.details.v0.max_fee), + &no_fee), + /* for sanity check, must be *absent*! */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("choices", + &choices), + NULL), + GNUNET_JSON_spec_end () + }; + + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + specv0); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + if ( (! no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, + &oc->parse_order.details.v0.max_fee)) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "different currencies used for 'max_fee' and 'amount' currency"); + return; + } + if (! TMH_test_exchange_configured_for_currency ( + oc->parse_order.details.v0.brutto.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + oc->parse_order.details.v0.brutto.currency); + return; + } + if (NULL != choices) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, + "choices array must be null for v0 contracts"); + return; + } + oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0; + break; + } + case 1: + { + struct GNUNET_JSON_Specification specv1[] = { + GNUNET_JSON_spec_array_const ( + "choices", + &oc->parse_order.details.v1.choices), + GNUNET_JSON_spec_end () + }; + + ret = TALER_MHD_parse_json_data (oc->connection, + oc->parse_request.order, + specv1); + if (GNUNET_OK != ret) + { + GNUNET_break_op (0); + finalize_order2 (oc, + ret); + return; + } + oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1; + break; } + default: + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_VERSION_MALFORMED, + "invalid version specified in order, supported are null, '0' or '1'"); + return; + } - if (! TMH_test_exchange_configured_for_currency ( - choice->amount.currency)) + /* Add order_id if it doesn't exist. */ + if (NULL != order_id) + { + oc->parse_order.order_id = GNUNET_strdup (order_id); + } + else + { + char buf[256]; + time_t timer; + struct tm *tm_info; + size_t off; + uint64_t rand; + char *last; + + time (&timer); + tm_info = localtime (&timer); + if (NULL == tm_info) { - GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - choice->amount.currency); + reply_with_error ( + oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME, + NULL); return; } + off = strftime (buf, + sizeof (buf) - 1, + "%Y.%j", + tm_info); + /* Check for error state of strftime */ + GNUNET_assert (0 != off); + buf[off++] = '-'; + rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, + UINT64_MAX); + last = GNUNET_STRINGS_data_to_string (&rand, + sizeof (uint64_t), + &buf[off], + sizeof (buf) - off); + GNUNET_assert (NULL != last); + *last = '\0'; - if (NULL != jinputs) - { - const json_t *jinput; - size_t idx; - json_array_foreach ((json_t *) jinputs, idx, jinput) - { - struct TALER_MERCHANT_ContractInput input = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_input ((json_t *) jinput, - &input, - idx, - true)) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "input"); - return; - } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Assigning order ID `%s' server-side\n", + buf); - switch (input.type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == input.details.token.count) - continue; + oc->parse_order.order_id = GNUNET_strdup (buf); + GNUNET_assert (NULL != oc->parse_order.order_id); + } - if (GNUNET_OK != - add_input_token_family (oc, - input.details.token.token_family_slug)) + /* Patch fulfillment URL with order_id (implements #6467). */ + if (NULL != oc->parse_order.fulfillment_url) + { + const char *pos; - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - input.details.token.token_family_slug); - return; - } + pos = strstr (oc->parse_order.fulfillment_url, + "${ORDER_ID}"); + if (NULL != pos) + { + /* replace ${ORDER_ID} with the real order_id */ + char *nurl; - GNUNET_array_append (choice->inputs, - choice->inputs_len, - input); - continue; - } - GNUNET_assert (0); + /* We only allow one placeholder */ + if (strstr (pos + strlen ("${ORDER_ID}"), + "${ORDER_ID}")) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "fulfillment_url"); + return; } - } - if (NULL != joutputs) - { - const json_t *joutput; - size_t idx; - json_array_foreach ((json_t *) joutputs, idx, joutput) - { - struct TALER_MERCHANT_ContractOutput output = { - .details.token.count = 1 - }; + GNUNET_asprintf (&nurl, + "%.*s%s%s", + /* first output URL until ${ORDER_ID} */ + (int) (pos - oc->parse_order.fulfillment_url), + oc->parse_order.fulfillment_url, + /* replace ${ORDER_ID} with the right order_id */ + oc->parse_order.order_id, + /* append rest of original URL */ + pos + strlen ("${ORDER_ID}")); - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_output ((json_t *) joutput, - &output, - idx, - true)) - { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "output"); - return; - } + oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); - switch (output.type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - GNUNET_break (0); /* FIXME-#9059: not yet implemented! */ - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == output.details.token.count) - continue; + GNUNET_free (nurl); + } + } - if (0 == output.details.token.valid_at.abs_time.abs_value_us) - output.details.token.valid_at - = GNUNET_TIME_timestamp_get (); - if (GNUNET_OK != - add_output_token_family (oc, - output.details.token.token_family_slug, - output.details.token.valid_at, - &output.details.token.key_index)) + /* Check soundness of refund deadline, and that a timestamp + * is actually present. */ + { + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - output.details.token.token_family_slug); - return; - } + /* Add timestamp if it doesn't exist (or is zero) */ + if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) + { + oc->parse_order.timestamp = now; + } - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - continue; - } - GNUNET_assert (0); + /* If no refund_deadline given, set one based on refund_delay. */ + if (GNUNET_TIME_absolute_is_never ( + oc->parse_order.refund_deadline.abs_time)) + { + if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Refund delay is zero, no refunds are possible for this order\n"); + oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; + } + else + { + oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp ( + oc->parse_request.refund_delay); } } + + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.delivery_date.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.delivery_date.abs_time)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST, + NULL); + return; + } } - oc->phase++; -} - -/** - * Process the @a payment_target and add the details of how the - * order could be paid to @a order. On success, continue - * processing with set_exchanges(). - * - * @param[in,out] oc order context - */ -static void -add_payment_details (struct OrderContext *oc) -{ - struct TMH_WireMethod *wm; - - wm = oc->hc->instance->wm_head; - /* Locate wire method that has a matching payment target */ - while ( (NULL != wm) && - ( (! wm->active) || - ( (NULL != oc->parse_request.payment_target) && - (0 != strcasecmp (oc->parse_request.payment_target, - wm->wire_method) ) ) ) ) - wm = wm->next; - if (NULL == wm) + if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) || + (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) ) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "No wire method available for instance '%s'\n", - oc->hc->instance->settings.id); - reply_with_error (oc, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE, - oc->parse_request.payment_target); + oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( + settings->default_pay_delay); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Pay deadline was zero (or never), setting to %s\n", + GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + } + else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST, + NULL); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Pay deadline is %s\n", + GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + if ( (! GNUNET_TIME_absolute_is_zero ( + oc->parse_order.refund_deadline.abs_time)) && + (GNUNET_TIME_absolute_is_past ( + oc->parse_order.refund_deadline.abs_time)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST, + NULL); return; } - oc->add_payment_details.wm = wm; - oc->phase++; -} + if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) + { + struct GNUNET_TIME_Timestamp t; -/** - * Merge the inventory products into products, querying the - * database about the details of those products. Upon success, - * continue processing by calling add_payment_details(). - * - * @param[in,out] oc order context to process - */ -static void -merge_inventory (struct OrderContext *oc) -{ - /** - * parse_request.inventory_products => instructions to add products to contract terms - * parse_order.products => contains products that are not from the backend-managed inventory. - */ - if (NULL != oc->parse_order.products) - oc->merge_inventory.products - = json_deep_copy (oc->parse_order.products); - else - oc->merge_inventory.products - = json_array (); - /* Populate products from inventory product array and database */ + t = GNUNET_TIME_relative_to_timestamp ( + GNUNET_TIME_relative_max (settings->default_wire_transfer_delay, + oc->parse_request.refund_delay)); + oc->parse_order.wire_deadline = GNUNET_TIME_timestamp_max ( + oc->parse_order.refund_deadline, + t); + if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER, + "order:wire_transfer_deadline"); + return; + } + } + if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, + <, + oc->parse_order.refund_deadline)) { - GNUNET_assert (NULL != oc->merge_inventory.products); - for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++) + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE, + "order:wire_transfer_deadline;order:refund_deadline"); + return; + } + + if (NULL != merchant_base_url) + { + if (('\0' == *merchant_base_url) || + ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) { - const struct InventoryProduct *ip - = &oc->parse_request.inventory_products[i]; - struct TALER_MERCHANTDB_ProductDetails pd; - enum GNUNET_DB_QueryStatus qs; - size_t num_categories = 0; - uint64_t *categories = NULL; + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, + "merchant_base_url is not valid"); + return; + } + oc->parse_order.merchant_base_url + = GNUNET_strdup (merchant_base_url); + } + else + { + char *url; - qs = TMH_db->lookup_product (TMH_db->cls, - oc->hc->instance->settings.id, - ip->product_id, - &pd, - &num_categories, - &categories); - if (qs <= 0) - { - enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; - unsigned int http_status = 0; + url = make_merchant_base_url (oc->connection, + settings->id); + if (NULL == url) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "order:merchant_base_url"); + return; + } + oc->parse_order.merchant_base_url = url; + } - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_FETCH_FAILED; - break; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ec = TALER_EC_GENERIC_DB_SOFT_FAILURE; - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Product %s from order unknown\n", - ip->product_id); - http_status = MHD_HTTP_NOT_FOUND; - ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN; - break; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - /* case listed to make compilers happy */ - GNUNET_assert (0); - } - reply_with_error (oc, - http_status, - ec, - ip->product_id); - return; - } - GNUNET_free (categories); - oc->parse_order.minimum_age - = GNUNET_MAX (oc->parse_order.minimum_age, - pd.minimum_age); - { - json_t *p; + if ( (NULL != oc->parse_order.products) && + (! TMH_products_array_valid (oc->parse_order.products)) ) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order.products"); + return; + } - p = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("product_name", - pd.product_name), - GNUNET_JSON_pack_string ("description", - pd.description), - GNUNET_JSON_pack_object_steal ("description_i18n", - pd.description_i18n), - GNUNET_JSON_pack_string ("unit", - pd.unit), - TALER_JSON_pack_amount ("price", - &pd.price), - GNUNET_JSON_pack_array_steal ("taxes", - pd.taxes), - GNUNET_JSON_pack_string ("image", - pd.image), - GNUNET_JSON_pack_uint64 ( - "quantity", - ip->quantity)); - GNUNET_assert (NULL != p); - GNUNET_assert (0 == - json_array_append_new (oc->merge_inventory.products, - p)); - } - GNUNET_free (pd.description); - GNUNET_free (pd.unit); - GNUNET_free (pd.image); - json_decref (pd.address); - } + /* Merchant information must not already be present */ + if (NULL != jmerchant) + { + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, + "'merchant' field already set, but must be provided by backend"); + return; } - /* check if final product list is well-formed */ - if (! TMH_products_array_valid (oc->merge_inventory.products)) + + if ( (NULL != oc->parse_order.delivery_location) && + (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) { GNUNET_break_op (0); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order:products"); + "delivery_location"); return; } + oc->phase++; } +/* ***************** ORDER_PHASE_PARSE_REQUEST **************** */ + /** * Parse the client request. Upon success, * continue processing by calling parse_order(). @@ -3726,7 +3988,7 @@ merge_inventory (struct OrderContext *oc) * @param[in,out] oc order context to process */ static void -parse_request (struct OrderContext *oc) +phase_parse_request (struct OrderContext *oc) { const json_t *ip = NULL; const json_t *uuid = NULL; @@ -3939,6 +4201,9 @@ parse_request (struct OrderContext *oc) } +/* ***************** Main handler **************** */ + + MHD_RESULT TMH_private_post_orders ( const struct TMH_RequestHandler *rh, @@ -3963,38 +4228,41 @@ TMH_private_post_orders ( switch (oc->phase) { case ORDER_PHASE_PARSE_REQUEST: - parse_request (oc); + phase_parse_request (oc); break; case ORDER_PHASE_PARSE_ORDER: - parse_order (oc); + phase_parse_order (oc); break; case ORDER_PHASE_PARSE_CHOICES: - parse_choices (oc); + phase_parse_choices (oc); break; case ORDER_PHASE_MERGE_INVENTORY: - merge_inventory (oc); + phase_merge_inventory (oc); break; case ORDER_PHASE_ADD_PAYMENT_DETAILS: - add_payment_details (oc); + phase_add_payment_details (oc); break; case ORDER_PHASE_SET_EXCHANGES: - if (set_exchanges (oc)) + if (phase_set_exchanges (oc)) return MHD_YES; break; + case ORDER_PHASE_SELECT_WIRE_METHOD: + phase_select_wire_method (oc); + break; case ORDER_PHASE_SET_MAX_FEE: - set_max_fee (oc); + phase_set_max_fee (oc); break; case ORDER_PHASE_SERIALIZE_ORDER: - serialize_order (oc); + phase_serialize_order (oc); break; case ORDER_PHASE_CHECK_CONTRACT: - check_contract (oc); + phase_check_contract (oc); break; case ORDER_PHASE_SALT_FORGETTABLE: - salt_forgettable (oc); + phase_salt_forgettable (oc); break; case ORDER_PHASE_EXECUTE_ORDER: - execute_order (oc); + phase_execute_order (oc); break; case ORDER_PHASE_FINISHED_MHD_YES: GNUNET_log (GNUNET_ERROR_TYPE_INFO,