taler-merchant-httpd_private-post-token-families.c (14221B)
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 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 return ( (0 == strcmp (tf1->slug, 54 tf2->slug)) && 55 (0 == strcmp (tf1->name, 56 tf2->name)) && 57 (0 == strcmp (tf1->description, 58 tf2->description)) && 59 (1 == json_equal (tf1->description_i18n, 60 tf2->description_i18n)) && 61 ( (tf1->extra_data == tf2->extra_data) || 62 (1 == json_equal (tf1->extra_data, 63 tf2->extra_data)) ) && 64 (GNUNET_TIME_timestamp_cmp (tf1->valid_after, 65 ==, 66 tf2->valid_after)) && 67 (GNUNET_TIME_timestamp_cmp (tf1->valid_before, 68 ==, 69 tf2->valid_before)) && 70 (GNUNET_TIME_relative_cmp (tf1->duration, 71 ==, 72 tf2->duration)) && 73 (GNUNET_TIME_relative_cmp (tf1->validity_granularity, 74 ==, 75 tf2->validity_granularity)) && 76 (GNUNET_TIME_relative_cmp (tf1->start_offset, 77 ==, 78 tf2->start_offset)) && 79 (tf1->kind == tf2->kind) ); 80 } 81 82 83 MHD_RESULT 84 TMH_private_post_token_families (const struct TMH_RequestHandler *rh, 85 struct MHD_Connection *connection, 86 struct TMH_HandlerContext *hc) 87 { 88 struct TMH_MerchantInstance *mi = hc->instance; 89 struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 }; 90 const char *kind = NULL; 91 bool no_valid_after = false; 92 enum GNUNET_DB_QueryStatus qs; 93 struct GNUNET_JSON_Specification spec[] = { 94 GNUNET_JSON_spec_string ("slug", 95 (const char **) &details.slug), 96 GNUNET_JSON_spec_string ("name", 97 (const char **) &details.name), 98 GNUNET_JSON_spec_string ("description", 99 (const char **) &details.description), 100 GNUNET_JSON_spec_mark_optional ( 101 GNUNET_JSON_spec_json ("description_i18n", 102 &details.description_i18n), 103 NULL), 104 GNUNET_JSON_spec_mark_optional ( 105 GNUNET_JSON_spec_json ("extra_data", 106 &details.extra_data), 107 NULL), 108 GNUNET_JSON_spec_mark_optional ( 109 GNUNET_JSON_spec_timestamp ("valid_after", 110 &details.valid_after), 111 &no_valid_after), 112 GNUNET_JSON_spec_timestamp ("valid_before", 113 &details.valid_before), 114 GNUNET_JSON_spec_relative_time ("duration", 115 &details.duration), 116 GNUNET_JSON_spec_relative_time ("validity_granularity", 117 &details.validity_granularity), 118 GNUNET_JSON_spec_mark_optional ( 119 GNUNET_JSON_spec_relative_time ("start_offset", 120 &details.start_offset), 121 NULL), 122 GNUNET_JSON_spec_string ("kind", 123 &kind), 124 GNUNET_JSON_spec_end () 125 }; 126 struct GNUNET_TIME_Timestamp now 127 = GNUNET_TIME_timestamp_get (); 128 129 GNUNET_assert (NULL != mi); 130 { 131 enum GNUNET_GenericReturnValue res; 132 133 res = TALER_MHD_parse_json_data (connection, 134 hc->request_body, 135 spec); 136 if (GNUNET_OK != res) 137 { 138 GNUNET_break_op (0); 139 return (GNUNET_NO == res) 140 ? MHD_YES 141 : MHD_NO; 142 } 143 } 144 if (no_valid_after) 145 details.valid_after = now; 146 147 /* Ensure that valid_after is before valid_before */ 148 if (GNUNET_TIME_timestamp_cmp (details.valid_after, 149 >=, 150 details.valid_before)) 151 { 152 GNUNET_break (0); 153 GNUNET_JSON_parse_free (spec); 154 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 if (GNUNET_TIME_relative_cmp (details.duration, 162 <, 163 GNUNET_TIME_relative_add (details. 164 validity_granularity, 165 details.start_offset)) 166 ) 167 { 168 GNUNET_break (0); 169 GNUNET_JSON_parse_free (spec); 170 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 if (0 == 177 strcmp (kind, 178 "discount")) 179 details.kind = TALER_MERCHANTDB_TFK_Discount; 180 else if (0 == 181 strcmp (kind, 182 "subscription")) 183 details.kind = TALER_MERCHANTDB_TFK_Subscription; 184 else 185 { 186 GNUNET_break (0); 187 GNUNET_JSON_parse_free (spec); 188 return TALER_MHD_reply_with_error (connection, 189 MHD_HTTP_BAD_REQUEST, 190 TALER_EC_GENERIC_PARAMETER_MALFORMED, 191 "kind"); 192 } 193 194 if (NULL == details.description_i18n) 195 details.description_i18n = json_object (); 196 197 if (! TALER_JSON_check_i18n (details.description_i18n)) 198 { 199 GNUNET_break_op (0); 200 GNUNET_JSON_parse_free (spec); 201 return TALER_MHD_reply_with_error (connection, 202 MHD_HTTP_BAD_REQUEST, 203 TALER_EC_GENERIC_PARAMETER_MALFORMED, 204 "description_i18n"); 205 } 206 207 if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS, 208 !=, 209 details.validity_granularity) && 210 GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply ( 211 GNUNET_TIME_UNIT_DAYS, 212 90), 213 !=, 214 details.validity_granularity) && 215 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS, 216 !=, 217 details.validity_granularity) && 218 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS, 219 !=, 220 details.validity_granularity) && 221 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS, 222 !=, 223 details.validity_granularity) && 224 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS, 225 !=, 226 details.validity_granularity) && 227 GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES, 228 !=, 229 details.validity_granularity) 230 ) 231 { 232 GNUNET_break (0); 233 GNUNET_JSON_parse_free (spec); 234 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 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 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 TMH_db->preflight (TMH_db->cls); 252 if (GNUNET_OK != 253 TMH_db->start (TMH_db->cls, 254 "/post tokenfamilies")) 255 { 256 GNUNET_break (0); 257 GNUNET_JSON_parse_free (spec); 258 return TALER_MHD_reply_with_error (connection, 259 MHD_HTTP_INTERNAL_SERVER_ERROR, 260 TALER_EC_GENERIC_DB_START_FAILED, 261 NULL); 262 } 263 qs = TMH_db->insert_token_family (TMH_db->cls, 264 mi->settings.id, 265 details.slug, 266 &details); 267 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 268 "insert_token_family returned %d\n", 269 (int) qs); 270 switch (qs) 271 { 272 case GNUNET_DB_STATUS_HARD_ERROR: 273 GNUNET_break (0); 274 TMH_db->rollback (TMH_db->cls); 275 GNUNET_JSON_parse_free (spec); 276 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 case GNUNET_DB_STATUS_SOFT_ERROR: 282 GNUNET_break (0); 283 TMH_db->rollback (TMH_db->cls); 284 break; 285 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 286 qs = TMH_db->lookup_token_family (TMH_db->cls, 287 mi->settings.id, 288 details.slug, 289 &existing); 290 GNUNET_log (GNUNET_ERROR_TYPE_INFO, 291 "lookup_token_family returned %d\n", 292 (int) qs); 293 switch (qs) 294 { 295 case GNUNET_DB_STATUS_HARD_ERROR: 296 /* Clean up and fail hard */ 297 GNUNET_break (0); 298 TMH_db->rollback (TMH_db->cls); 299 GNUNET_JSON_parse_free (spec); 300 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 case GNUNET_DB_STATUS_SOFT_ERROR: 306 TMH_db->rollback (TMH_db->cls); 307 break; 308 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 309 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 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 315 { 316 bool eq; 317 318 eq = token_families_equal (&details, 319 &existing); 320 TALER_MERCHANTDB_token_family_details_free (&existing); 321 TMH_db->rollback (TMH_db->cls); 322 GNUNET_JSON_parse_free (spec); 323 return eq 324 ? TALER_MHD_reply_static ( 325 connection, 326 MHD_HTTP_NO_CONTENT, 327 NULL, 328 NULL, 329 0) 330 : TALER_MHD_reply_with_error ( 331 connection, 332 MHD_HTTP_CONFLICT, 333 TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT, 334 details.slug); 335 } 336 } 337 break; 338 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 339 qs = TMH_db->commit (TMH_db->cls); 340 switch (qs) 341 { 342 case GNUNET_DB_STATUS_HARD_ERROR: 343 /* Clean up and fail hard */ 344 GNUNET_break (0); 345 TMH_db->rollback (TMH_db->cls); 346 GNUNET_JSON_parse_free (spec); 347 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 case GNUNET_DB_STATUS_SOFT_ERROR: 353 break; 354 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 355 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 356 break; 357 } 358 break; 359 } 360 if (GNUNET_DB_STATUS_SOFT_ERROR == qs) 361 TMH_db->rollback (TMH_db->cls); 362 else 363 break; 364 } /* for(i... MAX_RETRIES) */ 365 366 GNUNET_JSON_parse_free (spec); 367 if (qs < 0) 368 { 369 GNUNET_break (0); 370 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 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 */