Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2020-2025 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify
6 : it under the terms of the GNU Affero General Public License as
7 : published by the Free Software Foundation; either version 3,
8 : or (at your option) any later version.
9 :
10 : TALER is distributed in the hope that it will be useful, but
11 : WITHOUT ANY WARRANTY; without even the implied warranty of
12 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : GNU General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public
16 : License along with TALER; see the file COPYING. If not,
17 : see <http://www.gnu.org/licenses/>
18 : */
19 :
20 : /**
21 : * @file src/backend/taler-merchant-httpd_patch-management-instances-INSTANCE.c
22 : * @brief implementing PATCH /instances/$ID request handling
23 : * @author Christian Grothoff
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd_patch-management-instances-INSTANCE.h"
27 : #include "taler-merchant-httpd_helper.h"
28 : #include <taler/taler_json_lib.h>
29 : #include <taler/taler_dbevents.h>
30 : #include "taler-merchant-httpd_mfa.h"
31 : #include "merchant-database/update_instance.h"
32 : #include "merchant-database/start.h"
33 :
34 :
35 : /**
36 : * How often do we retry the simple INSERT database transaction?
37 : */
38 : #define MAX_RETRIES 3
39 :
40 :
41 : /**
42 : * Free memory used by @a wm
43 : *
44 : * @param wm wire method to free
45 : */
46 : static void
47 0 : free_wm (struct TMH_WireMethod *wm)
48 : {
49 0 : GNUNET_free (wm->payto_uri.full_payto);
50 0 : GNUNET_free (wm->wire_method);
51 0 : GNUNET_free (wm);
52 0 : }
53 :
54 :
55 : /**
56 : * PATCH configuration of an existing instance, given its configuration.
57 : *
58 : * @param mi instance to patch
59 : * @param mfa_check true if a MFA check is required
60 : * @param connection the MHD connection to handle
61 : * @param[in,out] hc context with further information about the request
62 : * @return MHD result code
63 : */
64 : static enum MHD_Result
65 4 : patch_instances_ID (struct TMH_MerchantInstance *mi,
66 : bool mfa_check,
67 : struct MHD_Connection *connection,
68 : struct TMH_HandlerContext *hc)
69 : {
70 : struct TALER_MERCHANTDB_InstanceSettings is;
71 : const char *name;
72 4 : struct TMH_WireMethod *wm_head = NULL;
73 4 : struct TMH_WireMethod *wm_tail = NULL;
74 4 : const char *iphone = NULL;
75 : bool no_transfer_delay;
76 : bool no_pay_delay;
77 : bool no_refund_delay;
78 : struct GNUNET_JSON_Specification spec[] = {
79 4 : GNUNET_JSON_spec_string ("name",
80 : &name),
81 4 : GNUNET_JSON_spec_mark_optional (
82 : TALER_JSON_spec_web_url ("website",
83 : (const char **) &is.website),
84 : NULL),
85 4 : GNUNET_JSON_spec_mark_optional (
86 : GNUNET_JSON_spec_string ("email",
87 : (const char **) &is.email),
88 : NULL),
89 4 : GNUNET_JSON_spec_mark_optional (
90 : GNUNET_JSON_spec_string ("phone_number",
91 : &iphone),
92 : NULL),
93 4 : GNUNET_JSON_spec_mark_optional (
94 : GNUNET_JSON_spec_string ("logo",
95 : (const char **) &is.logo),
96 : NULL),
97 4 : GNUNET_JSON_spec_json ("address",
98 : &is.address),
99 4 : GNUNET_JSON_spec_json ("jurisdiction",
100 : &is.jurisdiction),
101 4 : GNUNET_JSON_spec_bool ("use_stefan",
102 : &is.use_stefan),
103 4 : GNUNET_JSON_spec_mark_optional (
104 : GNUNET_JSON_spec_relative_time ("default_pay_delay",
105 : &is.default_pay_delay),
106 : &no_pay_delay),
107 4 : GNUNET_JSON_spec_mark_optional (
108 : GNUNET_JSON_spec_relative_time ("default_refund_delay",
109 : &is.default_refund_delay),
110 : &no_refund_delay),
111 4 : GNUNET_JSON_spec_mark_optional (
112 : GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay",
113 : &is.default_wire_transfer_delay),
114 : &no_transfer_delay),
115 4 : GNUNET_JSON_spec_mark_optional (
116 : GNUNET_JSON_spec_time_rounder_interval (
117 : "default_wire_transfer_rounding_interval",
118 : &is.default_wire_transfer_rounding_interval),
119 : NULL),
120 4 : GNUNET_JSON_spec_end ()
121 : };
122 : enum GNUNET_DB_QueryStatus qs;
123 :
124 4 : GNUNET_assert (NULL != mi);
125 4 : memset (&is,
126 : 0,
127 : sizeof (is));
128 : {
129 : enum GNUNET_GenericReturnValue res;
130 :
131 4 : res = TALER_MHD_parse_json_data (connection,
132 4 : hc->request_body,
133 : spec);
134 4 : if (GNUNET_OK != res)
135 : return (GNUNET_NO == res)
136 : ? MHD_YES
137 0 : : MHD_NO;
138 : }
139 4 : if (! TMH_location_object_valid (is.address))
140 : {
141 0 : GNUNET_break_op (0);
142 0 : GNUNET_JSON_parse_free (spec);
143 0 : return TALER_MHD_reply_with_error (connection,
144 : MHD_HTTP_BAD_REQUEST,
145 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
146 : "address");
147 : }
148 4 : if ( (NULL != is.logo) &&
149 0 : (! TALER_MERCHANT_image_data_url_valid (is.logo)) )
150 : {
151 0 : GNUNET_break_op (0);
152 0 : GNUNET_JSON_parse_free (spec);
153 0 : return TALER_MHD_reply_with_error (connection,
154 : MHD_HTTP_BAD_REQUEST,
155 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
156 : "logo");
157 : }
158 :
159 4 : if (! TMH_location_object_valid (is.jurisdiction))
160 : {
161 0 : GNUNET_break_op (0);
162 0 : GNUNET_JSON_parse_free (spec);
163 0 : return TALER_MHD_reply_with_error (connection,
164 : MHD_HTTP_BAD_REQUEST,
165 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
166 : "jurisdiction");
167 : }
168 :
169 4 : if (no_transfer_delay)
170 0 : is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay;
171 4 : if (no_pay_delay)
172 0 : is.default_pay_delay = mi->settings.default_pay_delay;
173 4 : if (no_refund_delay)
174 0 : is.default_refund_delay = mi->settings.default_refund_delay;
175 4 : if (GNUNET_TIME_relative_is_forever (is.default_pay_delay))
176 : {
177 0 : GNUNET_break_op (0);
178 0 : GNUNET_JSON_parse_free (spec);
179 0 : return TALER_MHD_reply_with_error (connection,
180 : MHD_HTTP_BAD_REQUEST,
181 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
182 : "default_pay_delay");
183 : }
184 4 : if (GNUNET_TIME_relative_is_forever (is.default_refund_delay))
185 : {
186 0 : GNUNET_break_op (0);
187 0 : GNUNET_JSON_parse_free (spec);
188 0 : return TALER_MHD_reply_with_error (connection,
189 : MHD_HTTP_BAD_REQUEST,
190 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
191 : "default_refund_delay");
192 : }
193 4 : if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay))
194 : {
195 0 : GNUNET_break_op (0);
196 0 : GNUNET_JSON_parse_free (spec);
197 0 : return TALER_MHD_reply_with_error (connection,
198 : MHD_HTTP_BAD_REQUEST,
199 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
200 : "default_wire_transfer_delay");
201 : }
202 4 : if (NULL != iphone)
203 : {
204 0 : is.phone = TALER_MERCHANT_phone_validate_normalize (iphone,
205 : false);
206 0 : if (NULL == is.phone)
207 : {
208 0 : GNUNET_break_op (0);
209 0 : GNUNET_JSON_parse_free (spec);
210 0 : return TALER_MHD_reply_with_error (connection,
211 : MHD_HTTP_BAD_REQUEST,
212 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
213 : "phone_number");
214 : }
215 0 : if ( (NULL != TMH_phone_regex) &&
216 : (0 !=
217 0 : regexec (&TMH_phone_rx,
218 0 : is.phone,
219 : 0,
220 : NULL,
221 : 0)) )
222 : {
223 0 : GNUNET_break_op (0);
224 0 : GNUNET_JSON_parse_free (spec);
225 0 : return TALER_MHD_reply_with_error (connection,
226 : MHD_HTTP_BAD_REQUEST,
227 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
228 : "phone_number");
229 : }
230 : }
231 4 : if ( (NULL != is.email) &&
232 0 : (! TALER_MERCHANT_email_valid (is.email)) )
233 : {
234 0 : GNUNET_break_op (0);
235 0 : GNUNET_JSON_parse_free (spec);
236 0 : GNUNET_free (is.phone);
237 0 : return TALER_MHD_reply_with_error (connection,
238 : MHD_HTTP_BAD_REQUEST,
239 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
240 : "email");
241 : }
242 4 : if ( (NULL != is.phone) &&
243 0 : (NULL != mi->settings.phone) &&
244 0 : (0 == strcmp (mi->settings.phone,
245 0 : is.phone)) )
246 0 : is.phone_validated = mi->settings.phone_validated;
247 4 : if ( (NULL != is.email) &&
248 0 : (NULL != mi->settings.email) &&
249 0 : (0 == strcmp (mi->settings.email,
250 0 : is.email)) )
251 0 : is.email_validated = mi->settings.email_validated;
252 4 : if (mfa_check)
253 : {
254 0 : enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
255 0 : enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels;
256 :
257 0 : if ( (0 != (mtc & TMH_TCS_SMS)) &&
258 0 : (NULL != mi->settings.phone) &&
259 0 : (NULL == is.phone) )
260 : {
261 0 : GNUNET_break_op (0);
262 0 : GNUNET_JSON_parse_free (spec);
263 0 : GNUNET_free (is.phone);
264 0 : return TALER_MHD_reply_with_error (
265 : connection,
266 : MHD_HTTP_BAD_REQUEST,
267 : TALER_EC_GENERIC_PARAMETER_MISSING,
268 : "phone_number");
269 : }
270 0 : if ( (0 != (mtc & TMH_TCS_EMAIL)) &&
271 0 : (NULL != mi->settings.email) &&
272 0 : (NULL == is.email) )
273 : {
274 0 : GNUNET_break_op (0);
275 0 : GNUNET_JSON_parse_free (spec);
276 0 : GNUNET_free (is.phone);
277 0 : return TALER_MHD_reply_with_error (
278 : connection,
279 : MHD_HTTP_BAD_REQUEST,
280 : TALER_EC_GENERIC_PARAMETER_MISSING,
281 : "email");
282 : }
283 0 : if ( (is.phone_validated ||
284 0 : (NULL == is.phone) ) &&
285 0 : (0 != (mtc & TMH_TCS_SMS)) )
286 0 : mtc -= TMH_TCS_SMS;
287 0 : if ( (is.email_validated ||
288 0 : (NULL == is.email) ) &&
289 0 : (0 != (mtc & TMH_TCS_EMAIL)) )
290 0 : mtc -= TMH_TCS_EMAIL;
291 0 : switch (mtc)
292 : {
293 0 : case TMH_TCS_NONE:
294 0 : ret = GNUNET_OK;
295 0 : break;
296 0 : case TMH_TCS_SMS:
297 0 : GNUNET_assert (NULL != is.phone);
298 0 : is.phone_validated = true;
299 : /* validate new phone number, if possible require old e-mail
300 : address for authorization */
301 0 : ret = TMH_mfa_challenges_do (hc,
302 0 : mi->settings.id,
303 : TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
304 : true,
305 : TALER_MERCHANT_MFA_CHANNEL_SMS,
306 : is.phone,
307 : 0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL
308 0 : & TEH_mandatory_tan_channels)
309 : ? TALER_MERCHANT_MFA_CHANNEL_NONE
310 : : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
311 : mi->settings.email,
312 : TALER_MERCHANT_MFA_CHANNEL_NONE);
313 0 : break;
314 0 : case TMH_TCS_EMAIL:
315 0 : GNUNET_assert (NULL != is.email);
316 0 : is.email_validated = true;
317 : /* validate new e-mail address, if possible require old phone
318 : address for authorization */
319 0 : ret = TMH_mfa_challenges_do (hc,
320 0 : mi->settings.id,
321 : TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
322 : true,
323 : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
324 : is.email,
325 : 0 == (TALER_MERCHANT_MFA_CHANNEL_SMS
326 : & TEH_mandatory_tan_channels)
327 : ? TALER_MERCHANT_MFA_CHANNEL_NONE
328 : : TALER_MERCHANT_MFA_CHANNEL_SMS,
329 : mi->settings.phone,
330 : TALER_MERCHANT_MFA_CHANNEL_NONE);
331 0 : break;
332 0 : case TMH_TCS_EMAIL_AND_SMS:
333 0 : GNUNET_assert (NULL != is.phone);
334 0 : GNUNET_assert (NULL != is.email);
335 0 : is.phone_validated = true;
336 0 : is.email_validated = true;
337 : /* To change both, we require both old and both new
338 : addresses to consent */
339 0 : ret = TMH_mfa_challenges_do (hc,
340 0 : mi->settings.id,
341 : TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
342 : true,
343 : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
344 : mi->settings.email,
345 : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
346 : is.email,
347 : TALER_MERCHANT_MFA_CHANNEL_SMS,
348 : mi->settings.phone,
349 : TALER_MERCHANT_MFA_CHANNEL_SMS,
350 : is.phone,
351 : TALER_MERCHANT_MFA_CHANNEL_NONE);
352 0 : break;
353 : }
354 0 : if (GNUNET_OK != ret)
355 : {
356 0 : GNUNET_JSON_parse_free (spec);
357 0 : GNUNET_free (is.phone);
358 : return (GNUNET_NO == ret)
359 : ? MHD_YES
360 0 : : MHD_NO;
361 : }
362 : }
363 :
364 4 : for (unsigned int retry = 0; retry<MAX_RETRIES; retry++)
365 : {
366 : /* Cleanup after earlier loops */
367 : {
368 : struct TMH_WireMethod *wm;
369 :
370 4 : while (NULL != (wm = wm_head))
371 : {
372 0 : GNUNET_CONTAINER_DLL_remove (wm_head,
373 : wm_tail,
374 : wm);
375 0 : free_wm (wm);
376 : }
377 : }
378 4 : if (GNUNET_OK !=
379 4 : TALER_MERCHANTDB_start (TMH_db,
380 : "PATCH /instances"))
381 : {
382 0 : GNUNET_JSON_parse_free (spec);
383 0 : GNUNET_free (is.phone);
384 0 : return TALER_MHD_reply_with_error (connection,
385 : MHD_HTTP_INTERNAL_SERVER_ERROR,
386 : TALER_EC_GENERIC_DB_START_FAILED,
387 : NULL);
388 : }
389 : /* Check for equality of settings */
390 4 : if (! ( (0 == strcmp (mi->settings.name,
391 2 : name)) &&
392 2 : ((mi->settings.email == is.email) ||
393 0 : (NULL != is.email && NULL != mi->settings.email &&
394 0 : 0 == strcmp (mi->settings.email,
395 0 : is.email))) &&
396 2 : ((mi->settings.phone == is.phone) ||
397 0 : (NULL != is.phone && NULL != mi->settings.phone &&
398 0 : 0 == strcmp (mi->settings.phone,
399 0 : is.phone))) &&
400 2 : ((mi->settings.website == is.website) ||
401 0 : (NULL != is.website && NULL != mi->settings.website &&
402 0 : 0 == strcmp (mi->settings.website,
403 0 : is.website))) &&
404 2 : ((mi->settings.logo == is.logo) ||
405 0 : (NULL != is.logo && NULL != mi->settings.logo &&
406 0 : 0 == strcmp (mi->settings.logo,
407 2 : is.logo))) &&
408 2 : (1 == json_equal (mi->settings.address,
409 2 : is.address)) &&
410 0 : (1 == json_equal (mi->settings.jurisdiction,
411 0 : is.jurisdiction)) &&
412 0 : (mi->settings.use_stefan == is.use_stefan) &&
413 0 : (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay,
414 : ==,
415 0 : is.default_wire_transfer_delay)) &&
416 0 : (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay,
417 : ==,
418 : is.default_refund_delay)) &&
419 0 : (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay,
420 : ==,
421 : is.default_pay_delay)) ) )
422 : {
423 4 : is.id = mi->settings.id;
424 4 : is.name = GNUNET_strdup (name);
425 4 : qs = TALER_MERCHANTDB_update_instance (TMH_db,
426 : &is);
427 4 : GNUNET_free (is.name);
428 4 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
429 : {
430 0 : TALER_MERCHANTDB_rollback (TMH_db);
431 0 : goto retry;
432 : }
433 : }
434 4 : qs = TALER_MERCHANTDB_commit (TMH_db);
435 4 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
436 4 : qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
437 0 : retry:
438 4 : if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
439 0 : continue;
440 4 : break;
441 : } /* for(... MAX_RETRIES) */
442 4 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
443 : {
444 : /* Hard error or retries exhausted: the transaction did not
445 : commit, so do NOT mutate our in-memory settings and report
446 : the failure to the client. */
447 0 : GNUNET_break (0);
448 0 : GNUNET_JSON_parse_free (spec);
449 0 : GNUNET_free (is.phone);
450 0 : return TALER_MHD_reply_with_error (connection,
451 : MHD_HTTP_INTERNAL_SERVER_ERROR,
452 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
453 : NULL);
454 : }
455 : /* Update our 'settings' */
456 4 : GNUNET_free (mi->settings.name);
457 4 : GNUNET_free (mi->settings.email);
458 4 : GNUNET_free (mi->settings.phone);
459 4 : GNUNET_free (mi->settings.website);
460 4 : GNUNET_free (mi->settings.logo);
461 4 : json_decref (mi->settings.address);
462 4 : json_decref (mi->settings.jurisdiction);
463 4 : is.id = mi->settings.id;
464 4 : mi->settings = is;
465 4 : mi->settings.address = json_incref (mi->settings.address);
466 4 : mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
467 4 : mi->settings.name = GNUNET_strdup (name);
468 4 : if (NULL != is.email)
469 0 : mi->settings.email = GNUNET_strdup (is.email);
470 4 : mi->settings.phone = is.phone;
471 4 : is.phone = NULL;
472 4 : if (NULL != is.website)
473 0 : mi->settings.website = GNUNET_strdup (is.website);
474 4 : if (NULL != is.logo)
475 0 : mi->settings.logo = GNUNET_strdup (is.logo);
476 :
477 4 : GNUNET_JSON_parse_free (spec);
478 4 : TMH_reload_instances (mi->settings.id);
479 4 : return TALER_MHD_reply_static (connection,
480 : MHD_HTTP_NO_CONTENT,
481 : NULL,
482 : NULL,
483 : 0);
484 : }
485 :
486 :
487 : enum MHD_Result
488 0 : TMH_private_patch_instances_ID (const struct TMH_RequestHandler *rh,
489 : struct MHD_Connection *connection,
490 : struct TMH_HandlerContext *hc)
491 : {
492 0 : struct TMH_MerchantInstance *mi = hc->instance;
493 :
494 0 : return patch_instances_ID (mi,
495 : true,
496 : connection,
497 : hc);
498 : }
499 :
500 :
501 : enum MHD_Result
502 4 : TMH_private_patch_instances_default_ID (const struct TMH_RequestHandler *rh,
503 : struct MHD_Connection *connection,
504 : struct TMH_HandlerContext *hc)
505 : {
506 : struct TMH_MerchantInstance *mi;
507 :
508 4 : mi = TMH_lookup_instance (hc->infix);
509 4 : if (NULL == mi)
510 : {
511 0 : return TALER_MHD_reply_with_error (connection,
512 : MHD_HTTP_NOT_FOUND,
513 : TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
514 0 : hc->infix);
515 : }
516 4 : if (mi->deleted)
517 : {
518 0 : return TALER_MHD_reply_with_error (connection,
519 : MHD_HTTP_CONFLICT,
520 : TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED,
521 0 : hc->infix);
522 : }
523 4 : return patch_instances_ID (mi,
524 : false,
525 : connection,
526 : hc);
527 : }
528 :
529 :
530 : /* end of taler-merchant-httpd_patch-management-instances-INSTANCE.c */
|