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