taler-merchant-httpd_post-private-tokenfamilies.c (14603B)
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 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 return ( (0 == strcmp (tf1->slug, 58 tf2->slug)) && 59 (0 == strcmp (tf1->name, 60 tf2->name)) && 61 (0 == strcmp (tf1->description, 62 tf2->description)) && 63 (1 == json_equal (tf1->description_i18n, 64 tf2->description_i18n)) && 65 ( (tf1->extra_data == tf2->extra_data) || 66 (1 == json_equal (tf1->extra_data, 67 tf2->extra_data)) ) && 68 (GNUNET_TIME_timestamp_cmp (tf1->valid_after, 69 ==, 70 tf2->valid_after)) && 71 (GNUNET_TIME_timestamp_cmp (tf1->valid_before, 72 ==, 73 tf2->valid_before)) && 74 (GNUNET_TIME_relative_cmp (tf1->duration, 75 ==, 76 tf2->duration)) && 77 (GNUNET_TIME_relative_cmp (tf1->validity_granularity, 78 ==, 79 tf2->validity_granularity)) && 80 (GNUNET_TIME_relative_cmp (tf1->start_offset, 81 ==, 82 tf2->start_offset)) && 83 (tf1->kind == tf2->kind) ); 84 } 85 86 87 enum MHD_Result 88 TMH_private_post_token_families (const struct TMH_RequestHandler *rh, 89 struct MHD_Connection *connection, 90 struct TMH_HandlerContext *hc) 91 { 92 struct TMH_MerchantInstance *mi = hc->instance; 93 struct TALER_MERCHANTDB_TokenFamilyDetails details = { 94 .valid_before = GNUNET_TIME_UNIT_FOREVER_TS 95 }; 96 const char *kind = NULL; 97 bool no_valid_after = false; 98 enum GNUNET_DB_QueryStatus qs; 99 struct GNUNET_JSON_Specification spec[] = { 100 TALER_JSON_spec_slug ("slug", 101 (const char **) &details.slug), 102 GNUNET_JSON_spec_string ("name", 103 (const char **) &details.name), 104 GNUNET_JSON_spec_string ("description", 105 (const char **) &details.description), 106 GNUNET_JSON_spec_mark_optional ( 107 GNUNET_JSON_spec_json ("description_i18n", 108 &details.description_i18n), 109 NULL), 110 GNUNET_JSON_spec_mark_optional ( 111 GNUNET_JSON_spec_json ("extra_data", 112 &details.extra_data), 113 NULL), 114 GNUNET_JSON_spec_mark_optional ( 115 GNUNET_JSON_spec_timestamp ("valid_after", 116 &details.valid_after), 117 &no_valid_after), 118 GNUNET_JSON_spec_mark_optional ( 119 GNUNET_JSON_spec_timestamp ("valid_before", 120 &details.valid_before), 121 NULL), 122 GNUNET_JSON_spec_relative_time ("duration", 123 &details.duration), 124 GNUNET_JSON_spec_relative_time ("validity_granularity", 125 &details.validity_granularity), 126 GNUNET_JSON_spec_mark_optional ( 127 GNUNET_JSON_spec_relative_time ("start_offset", 128 &details.start_offset), 129 NULL), 130 GNUNET_JSON_spec_string ("kind", 131 &kind), 132 GNUNET_JSON_spec_end () 133 }; 134 struct GNUNET_TIME_Timestamp now 135 = GNUNET_TIME_timestamp_get (); 136 137 GNUNET_assert (NULL != mi); 138 { 139 enum GNUNET_GenericReturnValue res; 140 141 res = TALER_MHD_parse_json_data (connection, 142 hc->request_body, 143 spec); 144 if (GNUNET_OK != res) 145 { 146 GNUNET_break_op (0); 147 return (GNUNET_NO == res) 148 ? MHD_YES 149 : MHD_NO; 150 } 151 } 152 if (no_valid_after) 153 details.valid_after = now; 154 155 /* Ensure that valid_after is before valid_before */ 156 if (GNUNET_TIME_timestamp_cmp (details.valid_after, 157 >=, 158 details.valid_before)) 159 { 160 GNUNET_break_op (0); 161 GNUNET_JSON_parse_free (spec); 162 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 if (GNUNET_TIME_relative_cmp (details.duration, 170 <, 171 GNUNET_TIME_relative_add (details. 172 validity_granularity, 173 details.start_offset)) 174 ) 175 { 176 GNUNET_break_op (0); 177 GNUNET_JSON_parse_free (spec); 178 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 if (0 == 185 strcmp (kind, 186 "discount")) 187 details.kind = TALER_MERCHANTDB_TFK_Discount; 188 else if (0 == 189 strcmp (kind, 190 "subscription")) 191 details.kind = TALER_MERCHANTDB_TFK_Subscription; 192 else 193 { 194 GNUNET_break_op (0); 195 GNUNET_JSON_parse_free (spec); 196 return TALER_MHD_reply_with_error (connection, 197 MHD_HTTP_BAD_REQUEST, 198 TALER_EC_GENERIC_PARAMETER_MALFORMED, 199 "kind"); 200 } 201 202 if (NULL == details.description_i18n) 203 details.description_i18n = json_object (); 204 205 if (! TALER_JSON_check_i18n (details.description_i18n)) 206 { 207 GNUNET_break_op (0); 208 GNUNET_JSON_parse_free (spec); 209 return TALER_MHD_reply_with_error (connection, 210 MHD_HTTP_BAD_REQUEST, 211 TALER_EC_GENERIC_PARAMETER_MALFORMED, 212 "description_i18n"); 213 } 214 215 if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, 216 !=, 217 details.validity_granularity) && 218 GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply ( 219 GNUNET_TIME_UNIT_DAYS, 220 90), 221 !=, 222 details.validity_granularity) && 223 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, 224 !=, 225 details.validity_granularity) && 226 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS, 227 !=, 228 details.validity_granularity) && 229 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, 230 !=, 231 details.validity_granularity) && 232 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, 233 !=, 234 details.validity_granularity) && 235 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, 236 !=, 237 details.validity_granularity) 238 ) 239 { 240 GNUNET_break_op (0); 241 GNUNET_JSON_parse_free (spec); 242 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 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 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 TALER_MERCHANTDB_preflight (TMH_db); 260 if (GNUNET_OK != 261 TALER_MERCHANTDB_start (TMH_db, 262 "/post tokenfamilies")) 263 { 264 GNUNET_break (0); 265 GNUNET_JSON_parse_free (spec); 266 return TALER_MHD_reply_with_error (connection, 267 MHD_HTTP_INTERNAL_SERVER_ERROR, 268 TALER_EC_GENERIC_DB_START_FAILED, 269 NULL); 270 } 271 qs = TALER_MERCHANTDB_insert_token_family (TMH_db, 272 mi->settings.id, 273 details.slug, 274 &details); 275 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 276 "insert_token_family returned %d\n", 277 (int) qs); 278 switch (qs) 279 { 280 case GNUNET_DB_STATUS_HARD_ERROR: 281 GNUNET_break (0); 282 TALER_MERCHANTDB_rollback (TMH_db); 283 GNUNET_JSON_parse_free (spec); 284 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 case GNUNET_DB_STATUS_SOFT_ERROR: 290 GNUNET_break (0); 291 TALER_MERCHANTDB_rollback (TMH_db); 292 break; 293 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 294 qs = TALER_MERCHANTDB_get_token_family (TMH_db, 295 mi->settings.id, 296 details.slug, 297 &existing); 298 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 299 "get_token_family returned %d\n", 300 (int) qs); 301 switch (qs) 302 { 303 case GNUNET_DB_STATUS_HARD_ERROR: 304 /* Clean up and fail hard */ 305 GNUNET_break (0); 306 TALER_MERCHANTDB_rollback (TMH_db); 307 GNUNET_JSON_parse_free (spec); 308 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 case GNUNET_DB_STATUS_SOFT_ERROR: 314 TALER_MERCHANTDB_rollback (TMH_db); 315 break; 316 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 317 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 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 323 { 324 bool eq; 325 326 eq = token_families_equal (&details, 327 &existing); 328 TALER_MERCHANTDB_token_family_details_free (&existing); 329 TALER_MERCHANTDB_rollback (TMH_db); 330 GNUNET_JSON_parse_free (spec); 331 return eq 332 ? TALER_MHD_reply_static ( 333 connection, 334 MHD_HTTP_NO_CONTENT, 335 NULL, 336 NULL, 337 0) 338 : TALER_MHD_reply_with_error ( 339 connection, 340 MHD_HTTP_CONFLICT, 341 TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT, 342 details.slug); 343 } 344 } 345 break; 346 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 347 qs = TALER_MERCHANTDB_commit (TMH_db); 348 switch (qs) 349 { 350 case GNUNET_DB_STATUS_HARD_ERROR: 351 /* Clean up and fail hard */ 352 GNUNET_break (0); 353 TALER_MERCHANTDB_rollback (TMH_db); 354 GNUNET_JSON_parse_free (spec); 355 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 case GNUNET_DB_STATUS_SOFT_ERROR: 361 TALER_MERCHANTDB_rollback (TMH_db); 362 break; 363 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 364 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 365 break; 366 } 367 break; 368 } 369 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 370 break; 371 } /* for(i... MAX_RETRIES) */ 372 373 GNUNET_JSON_parse_free (spec); 374 if (qs < 0) 375 { 376 GNUNET_break (0); 377 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 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 */