taler-merchant-httpd_post-private-tokenfamilies.c (14526B)
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/lookup_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 = { 0 }; 94 const char *kind = NULL; 95 bool no_valid_after = false; 96 enum GNUNET_DB_QueryStatus qs; 97 struct GNUNET_JSON_Specification spec[] = { 98 GNUNET_JSON_spec_string ("slug", 99 (const char **) &details.slug), 100 GNUNET_JSON_spec_string ("name", 101 (const char **) &details.name), 102 GNUNET_JSON_spec_string ("description", 103 (const char **) &details.description), 104 GNUNET_JSON_spec_mark_optional ( 105 GNUNET_JSON_spec_json ("description_i18n", 106 &details.description_i18n), 107 NULL), 108 GNUNET_JSON_spec_mark_optional ( 109 GNUNET_JSON_spec_json ("extra_data", 110 &details.extra_data), 111 NULL), 112 GNUNET_JSON_spec_mark_optional ( 113 GNUNET_JSON_spec_timestamp ("valid_after", 114 &details.valid_after), 115 &no_valid_after), 116 GNUNET_JSON_spec_timestamp ("valid_before", 117 &details.valid_before), 118 GNUNET_JSON_spec_relative_time ("duration", 119 &details.duration), 120 GNUNET_JSON_spec_relative_time ("validity_granularity", 121 &details.validity_granularity), 122 GNUNET_JSON_spec_mark_optional ( 123 GNUNET_JSON_spec_relative_time ("start_offset", 124 &details.start_offset), 125 NULL), 126 GNUNET_JSON_spec_string ("kind", 127 &kind), 128 GNUNET_JSON_spec_end () 129 }; 130 struct GNUNET_TIME_Timestamp now 131 = GNUNET_TIME_timestamp_get (); 132 133 GNUNET_assert (NULL != mi); 134 { 135 enum GNUNET_GenericReturnValue res; 136 137 res = TALER_MHD_parse_json_data (connection, 138 hc->request_body, 139 spec); 140 if (GNUNET_OK != res) 141 { 142 GNUNET_break_op (0); 143 return (GNUNET_NO == res) 144 ? MHD_YES 145 : MHD_NO; 146 } 147 } 148 if (no_valid_after) 149 details.valid_after = now; 150 151 /* Ensure that valid_after is before valid_before */ 152 if (GNUNET_TIME_timestamp_cmp (details.valid_after, 153 >=, 154 details.valid_before)) 155 { 156 GNUNET_break (0); 157 GNUNET_JSON_parse_free (spec); 158 return TALER_MHD_reply_with_error (connection, 159 MHD_HTTP_BAD_REQUEST, 160 TALER_EC_GENERIC_PARAMETER_MALFORMED, 161 "valid_after >= valid_before"); 162 } 163 164 /* Ensure that duration exceeds rounding plus start_offset */ 165 if (GNUNET_TIME_relative_cmp (details.duration, 166 <, 167 GNUNET_TIME_relative_add (details. 168 validity_granularity, 169 details.start_offset)) 170 ) 171 { 172 GNUNET_break (0); 173 GNUNET_JSON_parse_free (spec); 174 return TALER_MHD_reply_with_error (connection, 175 MHD_HTTP_BAD_REQUEST, 176 TALER_EC_GENERIC_PARAMETER_MALFORMED, 177 "duration below validity_granularity plus start_offset"); 178 } 179 180 if (0 == 181 strcmp (kind, 182 "discount")) 183 details.kind = TALER_MERCHANTDB_TFK_Discount; 184 else if (0 == 185 strcmp (kind, 186 "subscription")) 187 details.kind = TALER_MERCHANTDB_TFK_Subscription; 188 else 189 { 190 GNUNET_break (0); 191 GNUNET_JSON_parse_free (spec); 192 return TALER_MHD_reply_with_error (connection, 193 MHD_HTTP_BAD_REQUEST, 194 TALER_EC_GENERIC_PARAMETER_MALFORMED, 195 "kind"); 196 } 197 198 if (NULL == details.description_i18n) 199 details.description_i18n = json_object (); 200 201 if (! TALER_JSON_check_i18n (details.description_i18n)) 202 { 203 GNUNET_break_op (0); 204 GNUNET_JSON_parse_free (spec); 205 return TALER_MHD_reply_with_error (connection, 206 MHD_HTTP_BAD_REQUEST, 207 TALER_EC_GENERIC_PARAMETER_MALFORMED, 208 "description_i18n"); 209 } 210 211 if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, 212 !=, 213 details.validity_granularity) && 214 GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply ( 215 GNUNET_TIME_UNIT_DAYS, 216 90), 217 !=, 218 details.validity_granularity) && 219 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, 220 !=, 221 details.validity_granularity) && 222 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS, 223 !=, 224 details.validity_granularity) && 225 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, 226 !=, 227 details.validity_granularity) && 228 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, 229 !=, 230 details.validity_granularity) && 231 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, 232 !=, 233 details.validity_granularity) 234 ) 235 { 236 GNUNET_break (0); 237 GNUNET_JSON_parse_free (spec); 238 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 239 "Received invalid validity_granularity value: %s\n", 240 GNUNET_STRINGS_relative_time_to_string (details. 241 validity_granularity, 242 false)); 243 return TALER_MHD_reply_with_error (connection, 244 MHD_HTTP_BAD_REQUEST, 245 TALER_EC_GENERIC_PARAMETER_MALFORMED, 246 "validity_granularity"); 247 } 248 249 /* finally, interact with DB until no serialization error */ 250 for (unsigned int i = 0; i<MAX_RETRIES; i++) 251 { 252 /* Test if a token family of this id is known */ 253 struct TALER_MERCHANTDB_TokenFamilyDetails existing; 254 255 TALER_MERCHANTDB_preflight (TMH_db); 256 if (GNUNET_OK != 257 TALER_MERCHANTDB_start (TMH_db, 258 "/post tokenfamilies")) 259 { 260 GNUNET_break (0); 261 GNUNET_JSON_parse_free (spec); 262 return TALER_MHD_reply_with_error (connection, 263 MHD_HTTP_INTERNAL_SERVER_ERROR, 264 TALER_EC_GENERIC_DB_START_FAILED, 265 NULL); 266 } 267 qs = TALER_MERCHANTDB_insert_token_family (TMH_db, 268 mi->settings.id, 269 details.slug, 270 &details); 271 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 272 "insert_token_family returned %d\n", 273 (int) qs); 274 switch (qs) 275 { 276 case GNUNET_DB_STATUS_HARD_ERROR: 277 GNUNET_break (0); 278 TALER_MERCHANTDB_rollback (TMH_db); 279 GNUNET_JSON_parse_free (spec); 280 return TALER_MHD_reply_with_error ( 281 connection, 282 MHD_HTTP_INTERNAL_SERVER_ERROR, 283 TALER_EC_GENERIC_DB_STORE_FAILED, 284 "insert_token_family"); 285 case GNUNET_DB_STATUS_SOFT_ERROR: 286 GNUNET_break (0); 287 TALER_MERCHANTDB_rollback (TMH_db); 288 break; 289 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 290 qs = TALER_MERCHANTDB_lookup_token_family (TMH_db, 291 mi->settings.id, 292 details.slug, 293 &existing); 294 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 295 "lookup_token_family returned %d\n", 296 (int) qs); 297 switch (qs) 298 { 299 case GNUNET_DB_STATUS_HARD_ERROR: 300 /* Clean up and fail hard */ 301 GNUNET_break (0); 302 TALER_MERCHANTDB_rollback (TMH_db); 303 GNUNET_JSON_parse_free (spec); 304 return TALER_MHD_reply_with_error ( 305 connection, 306 MHD_HTTP_INTERNAL_SERVER_ERROR, 307 TALER_EC_GENERIC_DB_FETCH_FAILED, 308 "lookup_token_family"); 309 case GNUNET_DB_STATUS_SOFT_ERROR: 310 TALER_MERCHANTDB_rollback (TMH_db); 311 break; 312 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 313 return TALER_MHD_reply_with_error ( 314 connection, 315 MHD_HTTP_INTERNAL_SERVER_ERROR, 316 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 317 "lookup_token_family failed after insert_token_family failed"); 318 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 319 { 320 bool eq; 321 322 eq = token_families_equal (&details, 323 &existing); 324 TALER_MERCHANTDB_token_family_details_free (&existing); 325 TALER_MERCHANTDB_rollback (TMH_db); 326 GNUNET_JSON_parse_free (spec); 327 return eq 328 ? TALER_MHD_reply_static ( 329 connection, 330 MHD_HTTP_NO_CONTENT, 331 NULL, 332 NULL, 333 0) 334 : TALER_MHD_reply_with_error ( 335 connection, 336 MHD_HTTP_CONFLICT, 337 TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT, 338 details.slug); 339 } 340 } 341 break; 342 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 343 qs = TALER_MERCHANTDB_commit (TMH_db); 344 switch (qs) 345 { 346 case GNUNET_DB_STATUS_HARD_ERROR: 347 /* Clean up and fail hard */ 348 GNUNET_break (0); 349 TALER_MERCHANTDB_rollback (TMH_db); 350 GNUNET_JSON_parse_free (spec); 351 return TALER_MHD_reply_with_error ( 352 connection, 353 MHD_HTTP_INTERNAL_SERVER_ERROR, 354 TALER_EC_GENERIC_DB_COMMIT_FAILED, 355 "insert_token_family"); 356 case GNUNET_DB_STATUS_SOFT_ERROR: 357 break; 358 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 359 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 360 break; 361 } 362 break; 363 } 364 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 365 TALER_MERCHANTDB_rollback (TMH_db); 366 else 367 break; 368 } /* for(i... MAX_RETRIES) */ 369 370 GNUNET_JSON_parse_free (spec); 371 if (qs < 0) 372 { 373 GNUNET_break (0); 374 return TALER_MHD_reply_with_error ( 375 connection, 376 MHD_HTTP_INTERNAL_SERVER_ERROR, 377 TALER_EC_GENERIC_DB_SOFT_FAILURE, 378 NULL); 379 } 380 return TALER_MHD_reply_static (connection, 381 MHD_HTTP_NO_CONTENT, 382 NULL, 383 NULL, 384 0); 385 } 386 387 388 /* end of taler-merchant-httpd_post-private-tokenfamilies.c */