Line data Source code
1 : /*
2 : This file is part of TALER
3 : (C) 2023, 2024 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_post-private-tokenfamilies.c
22 : * @brief implementing POST /tokenfamilies request handling
23 : * @author Christian Blättler
24 : */
25 : #include "platform.h"
26 : #include "taler-merchant-httpd_post-private-tokenfamilies.h"
27 : #include "taler-merchant-httpd_helper.h"
28 : #include <gnunet/gnunet_time_lib.h>
29 : #include <taler/taler_json_lib.h>
30 : #include "merchant-database/insert_token_family.h"
31 : #include "merchant-database/get_token_family.h"
32 : #include "merchant-database/preflight.h"
33 : #include "merchant-database/start.h"
34 :
35 :
36 : /**
37 : * How often do we retry the simple INSERT database transaction?
38 : */
39 : #define MAX_RETRIES 3
40 :
41 :
42 : /**
43 : * Check if the two token families are identical.
44 : *
45 : * @param tf1 token family to compare
46 : * @param tf2 other token family to compare
47 : * @return true if they are 'equal', false if not
48 : */
49 : static bool
50 0 : token_families_equal (const struct TALER_MERCHANTDB_TokenFamilyDetails *tf1,
51 : const struct TALER_MERCHANTDB_TokenFamilyDetails *tf2)
52 : {
53 : /* Note: we're not comparing 'cipher', as that is selected
54 : in the database to some default value and we currently
55 : do not allow the SPA to change it. As a result, it should
56 : always be "NULL" in tf1 and the DB-default in tf2. */
57 0 : return ( (0 == strcmp (tf1->slug,
58 0 : tf2->slug)) &&
59 0 : (0 == strcmp (tf1->name,
60 0 : tf2->name)) &&
61 0 : (0 == strcmp (tf1->description,
62 0 : tf2->description)) &&
63 0 : (1 == json_equal (tf1->description_i18n,
64 0 : tf2->description_i18n)) &&
65 0 : ( (tf1->extra_data == tf2->extra_data) ||
66 0 : (1 == json_equal (tf1->extra_data,
67 0 : tf2->extra_data)) ) &&
68 0 : (GNUNET_TIME_timestamp_cmp (tf1->valid_after,
69 : ==,
70 0 : tf2->valid_after)) &&
71 0 : (GNUNET_TIME_timestamp_cmp (tf1->valid_before,
72 : ==,
73 0 : tf2->valid_before)) &&
74 0 : (GNUNET_TIME_relative_cmp (tf1->duration,
75 : ==,
76 0 : tf2->duration)) &&
77 0 : (GNUNET_TIME_relative_cmp (tf1->validity_granularity,
78 : ==,
79 0 : tf2->validity_granularity)) &&
80 0 : (GNUNET_TIME_relative_cmp (tf1->start_offset,
81 : ==,
82 0 : tf2->start_offset)) &&
83 0 : (tf1->kind == tf2->kind) );
84 : }
85 :
86 :
87 : enum MHD_Result
88 6 : TMH_private_post_token_families (const struct TMH_RequestHandler *rh,
89 : struct MHD_Connection *connection,
90 : struct TMH_HandlerContext *hc)
91 : {
92 6 : struct TMH_MerchantInstance *mi = hc->instance;
93 6 : struct TALER_MERCHANTDB_TokenFamilyDetails details = {
94 : .valid_before = GNUNET_TIME_UNIT_FOREVER_TS
95 : };
96 6 : const char *kind = NULL;
97 6 : bool no_valid_after = false;
98 : enum GNUNET_DB_QueryStatus qs;
99 : struct GNUNET_JSON_Specification spec[] = {
100 6 : TALER_JSON_spec_slug ("slug",
101 : (const char **) &details.slug),
102 6 : GNUNET_JSON_spec_string ("name",
103 : (const char **) &details.name),
104 6 : GNUNET_JSON_spec_string ("description",
105 : (const char **) &details.description),
106 6 : GNUNET_JSON_spec_mark_optional (
107 : GNUNET_JSON_spec_json ("description_i18n",
108 : &details.description_i18n),
109 : NULL),
110 6 : GNUNET_JSON_spec_mark_optional (
111 : GNUNET_JSON_spec_json ("extra_data",
112 : &details.extra_data),
113 : NULL),
114 6 : GNUNET_JSON_spec_mark_optional (
115 : GNUNET_JSON_spec_timestamp ("valid_after",
116 : &details.valid_after),
117 : &no_valid_after),
118 6 : GNUNET_JSON_spec_mark_optional (
119 : GNUNET_JSON_spec_timestamp ("valid_before",
120 : &details.valid_before),
121 : NULL),
122 6 : GNUNET_JSON_spec_relative_time ("duration",
123 : &details.duration),
124 6 : GNUNET_JSON_spec_relative_time ("validity_granularity",
125 : &details.validity_granularity),
126 6 : GNUNET_JSON_spec_mark_optional (
127 : GNUNET_JSON_spec_relative_time ("start_offset",
128 : &details.start_offset),
129 : NULL),
130 6 : GNUNET_JSON_spec_string ("kind",
131 : &kind),
132 6 : GNUNET_JSON_spec_end ()
133 : };
134 : struct GNUNET_TIME_Timestamp now
135 6 : = GNUNET_TIME_timestamp_get ();
136 :
137 6 : GNUNET_assert (NULL != mi);
138 : {
139 : enum GNUNET_GenericReturnValue res;
140 :
141 6 : res = TALER_MHD_parse_json_data (connection,
142 6 : hc->request_body,
143 : spec);
144 6 : if (GNUNET_OK != res)
145 : {
146 0 : GNUNET_break_op (0);
147 : return (GNUNET_NO == res)
148 : ? MHD_YES
149 0 : : MHD_NO;
150 : }
151 : }
152 6 : if (no_valid_after)
153 2 : details.valid_after = now;
154 :
155 : /* Ensure that valid_after is before valid_before */
156 6 : if (GNUNET_TIME_timestamp_cmp (details.valid_after,
157 : >=,
158 : details.valid_before))
159 : {
160 0 : GNUNET_break_op (0);
161 0 : GNUNET_JSON_parse_free (spec);
162 0 : return TALER_MHD_reply_with_error (connection,
163 : MHD_HTTP_BAD_REQUEST,
164 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
165 : "valid_after >= valid_before");
166 : }
167 :
168 : /* Ensure that duration exceeds rounding plus start_offset */
169 6 : if (GNUNET_TIME_relative_cmp (details.duration,
170 : <,
171 : GNUNET_TIME_relative_add (details.
172 : validity_granularity,
173 : details.start_offset))
174 : )
175 : {
176 0 : GNUNET_break_op (0);
177 0 : GNUNET_JSON_parse_free (spec);
178 0 : return TALER_MHD_reply_with_error (connection,
179 : MHD_HTTP_BAD_REQUEST,
180 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
181 : "duration below validity_granularity plus start_offset");
182 : }
183 :
184 6 : if (0 ==
185 6 : strcmp (kind,
186 : "discount"))
187 1 : details.kind = TALER_MERCHANTDB_TFK_Discount;
188 5 : else if (0 ==
189 5 : strcmp (kind,
190 : "subscription"))
191 5 : details.kind = TALER_MERCHANTDB_TFK_Subscription;
192 : else
193 : {
194 0 : GNUNET_break_op (0);
195 0 : GNUNET_JSON_parse_free (spec);
196 0 : return TALER_MHD_reply_with_error (connection,
197 : MHD_HTTP_BAD_REQUEST,
198 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
199 : "kind");
200 : }
201 :
202 6 : if (NULL == details.description_i18n)
203 4 : details.description_i18n = json_object ();
204 :
205 6 : if (! TALER_JSON_check_i18n (details.description_i18n))
206 : {
207 0 : GNUNET_break_op (0);
208 0 : GNUNET_JSON_parse_free (spec);
209 0 : return TALER_MHD_reply_with_error (connection,
210 : MHD_HTTP_BAD_REQUEST,
211 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
212 : "description_i18n");
213 : }
214 :
215 6 : if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS,
216 : !=,
217 6 : details.validity_granularity) &&
218 6 : GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply (
219 : GNUNET_TIME_UNIT_DAYS,
220 : 90),
221 : !=,
222 6 : details.validity_granularity) &&
223 6 : GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS,
224 : !=,
225 0 : details.validity_granularity) &&
226 0 : GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS,
227 : !=,
228 0 : details.validity_granularity) &&
229 0 : GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS,
230 : !=,
231 0 : details.validity_granularity) &&
232 0 : GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS,
233 : !=,
234 0 : details.validity_granularity) &&
235 0 : GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES,
236 : !=,
237 : details.validity_granularity)
238 : )
239 : {
240 0 : GNUNET_break_op (0);
241 0 : GNUNET_JSON_parse_free (spec);
242 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
243 : "Received invalid validity_granularity value: %s\n",
244 : GNUNET_STRINGS_relative_time_to_string (details.
245 : validity_granularity,
246 : false));
247 0 : return TALER_MHD_reply_with_error (connection,
248 : MHD_HTTP_BAD_REQUEST,
249 : TALER_EC_GENERIC_PARAMETER_MALFORMED,
250 : "validity_granularity");
251 : }
252 :
253 : /* finally, interact with DB until no serialization error */
254 6 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
255 : {
256 : /* Test if a token family of this id is known */
257 : struct TALER_MERCHANTDB_TokenFamilyDetails existing;
258 :
259 6 : TALER_MERCHANTDB_preflight (TMH_db);
260 6 : if (GNUNET_OK !=
261 6 : TALER_MERCHANTDB_start (TMH_db,
262 : "/post tokenfamilies"))
263 : {
264 0 : GNUNET_break (0);
265 0 : GNUNET_JSON_parse_free (spec);
266 0 : return TALER_MHD_reply_with_error (connection,
267 : MHD_HTTP_INTERNAL_SERVER_ERROR,
268 : TALER_EC_GENERIC_DB_START_FAILED,
269 : NULL);
270 : }
271 6 : qs = TALER_MERCHANTDB_insert_token_family (TMH_db,
272 6 : mi->settings.id,
273 6 : details.slug,
274 : &details);
275 6 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
276 : "insert_token_family returned %d\n",
277 : (int) qs);
278 6 : switch (qs)
279 : {
280 0 : case GNUNET_DB_STATUS_HARD_ERROR:
281 0 : GNUNET_break (0);
282 0 : TALER_MERCHANTDB_rollback (TMH_db);
283 0 : GNUNET_JSON_parse_free (spec);
284 0 : return TALER_MHD_reply_with_error (
285 : connection,
286 : MHD_HTTP_INTERNAL_SERVER_ERROR,
287 : TALER_EC_GENERIC_DB_STORE_FAILED,
288 : "insert_token_family");
289 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
290 0 : GNUNET_break (0);
291 0 : TALER_MERCHANTDB_rollback (TMH_db);
292 0 : break;
293 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
294 0 : qs = TALER_MERCHANTDB_get_token_family (TMH_db,
295 0 : mi->settings.id,
296 0 : details.slug,
297 : &existing);
298 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
299 : "get_token_family returned %d\n",
300 : (int) qs);
301 0 : switch (qs)
302 : {
303 0 : case GNUNET_DB_STATUS_HARD_ERROR:
304 : /* Clean up and fail hard */
305 0 : GNUNET_break (0);
306 0 : TALER_MERCHANTDB_rollback (TMH_db);
307 0 : GNUNET_JSON_parse_free (spec);
308 0 : return TALER_MHD_reply_with_error (
309 : connection,
310 : MHD_HTTP_INTERNAL_SERVER_ERROR,
311 : TALER_EC_GENERIC_DB_FETCH_FAILED,
312 : "get_token_family");
313 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
314 0 : TALER_MERCHANTDB_rollback (TMH_db);
315 0 : break;
316 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
317 0 : return TALER_MHD_reply_with_error (
318 : connection,
319 : MHD_HTTP_INTERNAL_SERVER_ERROR,
320 : TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
321 : "get_token_family failed after insert_token_family failed");
322 0 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
323 : {
324 : bool eq;
325 :
326 0 : eq = token_families_equal (&details,
327 : &existing);
328 0 : TALER_MERCHANTDB_token_family_details_free (&existing);
329 0 : TALER_MERCHANTDB_rollback (TMH_db);
330 0 : GNUNET_JSON_parse_free (spec);
331 : return eq
332 0 : ? TALER_MHD_reply_static (
333 : connection,
334 : MHD_HTTP_NO_CONTENT,
335 : NULL,
336 : NULL,
337 : 0)
338 0 : : TALER_MHD_reply_with_error (
339 : connection,
340 : MHD_HTTP_CONFLICT,
341 : TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT,
342 0 : details.slug);
343 : }
344 : }
345 0 : break;
346 6 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
347 6 : qs = TALER_MERCHANTDB_commit (TMH_db);
348 6 : switch (qs)
349 : {
350 0 : case GNUNET_DB_STATUS_HARD_ERROR:
351 : /* Clean up and fail hard */
352 0 : GNUNET_break (0);
353 0 : TALER_MERCHANTDB_rollback (TMH_db);
354 0 : GNUNET_JSON_parse_free (spec);
355 0 : return TALER_MHD_reply_with_error (
356 : connection,
357 : MHD_HTTP_INTERNAL_SERVER_ERROR,
358 : TALER_EC_GENERIC_DB_COMMIT_FAILED,
359 : "insert_token_family");
360 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
361 0 : TALER_MERCHANTDB_rollback (TMH_db);
362 0 : break;
363 6 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
364 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
365 6 : break;
366 : }
367 6 : break;
368 : }
369 6 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
370 6 : break;
371 : } /* for(i... MAX_RETRIES) */
372 :
373 6 : GNUNET_JSON_parse_free (spec);
374 6 : if (qs < 0)
375 : {
376 0 : GNUNET_break (0);
377 0 : return TALER_MHD_reply_with_error (
378 : connection,
379 : MHD_HTTP_INTERNAL_SERVER_ERROR,
380 : TALER_EC_GENERIC_DB_SOFT_FAILURE,
381 : NULL);
382 : }
383 6 : return TALER_MHD_reply_static (connection,
384 : MHD_HTTP_NO_CONTENT,
385 : NULL,
386 : NULL,
387 : 0);
388 : }
389 :
390 :
391 : /* end of taler-merchant-httpd_post-private-tokenfamilies.c */
|