merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

taler-merchant-httpd_patch-management-instances-INSTANCE.c (19888B)


      1 /*
      2   This file is part of TALER
      3   (C) 2020-2025 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_patch-management-instances-INSTANCE.c
     22  * @brief implementing PATCH /instances/$ID request handling
     23  * @author Christian Grothoff
     24  */
     25 #include "platform.h"
     26 #include "taler-merchant-httpd_patch-management-instances-INSTANCE.h"
     27 #include "taler-merchant-httpd_helper.h"
     28 #include <taler/taler_json_lib.h>
     29 #include <taler/taler_dbevents.h>
     30 #include "taler-merchant-httpd_mfa.h"
     31 #include "merchant-database/update_instance.h"
     32 #include "merchant-database/start.h"
     33 
     34 
     35 /**
     36  * How often do we retry the simple INSERT database transaction?
     37  */
     38 #define MAX_RETRIES 3
     39 
     40 
     41 /**
     42  * Free memory used by @a wm
     43  *
     44  * @param wm wire method to free
     45  */
     46 static void
     47 free_wm (struct TMH_WireMethod *wm)
     48 {
     49   GNUNET_free (wm->payto_uri.full_payto);
     50   GNUNET_free (wm->wire_method);
     51   GNUNET_free (wm);
     52 }
     53 
     54 
     55 /**
     56  * PATCH configuration of an existing instance, given its configuration.
     57  *
     58  * @param mi instance to patch
     59  * @param mfa_check true if a MFA check is required
     60  * @param connection the MHD connection to handle
     61  * @param[in,out] hc context with further information about the request
     62  * @return MHD result code
     63  */
     64 static enum MHD_Result
     65 patch_instances_ID (struct TMH_MerchantInstance *mi,
     66                     bool mfa_check,
     67                     struct MHD_Connection *connection,
     68                     struct TMH_HandlerContext *hc)
     69 {
     70   struct TALER_MERCHANTDB_InstanceSettings is;
     71   const char *name;
     72   struct TMH_WireMethod *wm_head = NULL;
     73   struct TMH_WireMethod *wm_tail = NULL;
     74   const char *iphone = NULL;
     75   bool no_transfer_delay;
     76   bool no_pay_delay;
     77   bool no_wire_transfer_rounding_interval;
     78   bool no_refund_delay;
     79   struct GNUNET_JSON_Specification spec[] = {
     80     GNUNET_JSON_spec_string ("name",
     81                              &name),
     82     GNUNET_JSON_spec_mark_optional (
     83       TALER_JSON_spec_web_url ("website",
     84                                (const char **) &is.website),
     85       NULL),
     86     GNUNET_JSON_spec_mark_optional (
     87       GNUNET_JSON_spec_string ("email",
     88                                (const char **) &is.email),
     89       NULL),
     90     GNUNET_JSON_spec_mark_optional (
     91       GNUNET_JSON_spec_string ("phone_number",
     92                                &iphone),
     93       NULL),
     94     GNUNET_JSON_spec_mark_optional (
     95       GNUNET_JSON_spec_string ("logo",
     96                                (const char **) &is.logo),
     97       NULL),
     98     GNUNET_JSON_spec_json ("address",
     99                            &is.address),
    100     GNUNET_JSON_spec_json ("jurisdiction",
    101                            &is.jurisdiction),
    102     GNUNET_JSON_spec_bool ("use_stefan",
    103                            &is.use_stefan),
    104     GNUNET_JSON_spec_mark_optional (
    105       GNUNET_JSON_spec_relative_time ("default_pay_delay",
    106                                       &is.default_pay_delay),
    107       &no_pay_delay),
    108     GNUNET_JSON_spec_mark_optional (
    109       GNUNET_JSON_spec_relative_time ("default_refund_delay",
    110                                       &is.default_refund_delay),
    111       &no_refund_delay),
    112     GNUNET_JSON_spec_mark_optional (
    113       GNUNET_JSON_spec_relative_time ("default_wire_transfer_delay",
    114                                       &is.default_wire_transfer_delay),
    115       &no_transfer_delay),
    116     GNUNET_JSON_spec_mark_optional (
    117       GNUNET_JSON_spec_time_rounder_interval (
    118         "default_wire_transfer_rounding_interval",
    119         &is.default_wire_transfer_rounding_interval),
    120       &no_wire_transfer_rounding_interval),
    121     GNUNET_JSON_spec_end ()
    122   };
    123   enum GNUNET_DB_QueryStatus qs;
    124 
    125   GNUNET_assert (NULL != mi);
    126   memset (&is,
    127           0,
    128           sizeof (is));
    129   {
    130     enum GNUNET_GenericReturnValue res;
    131 
    132     res = TALER_MHD_parse_json_data (connection,
    133                                      hc->request_body,
    134                                      spec);
    135     if (GNUNET_OK != res)
    136       return (GNUNET_NO == res)
    137              ? MHD_YES
    138              : MHD_NO;
    139   }
    140   if (! TMH_location_object_valid (is.address))
    141   {
    142     GNUNET_break_op (0);
    143     GNUNET_JSON_parse_free (spec);
    144     return TALER_MHD_reply_with_error (connection,
    145                                        MHD_HTTP_BAD_REQUEST,
    146                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    147                                        "address");
    148   }
    149   if ( (NULL != is.logo) &&
    150        (! TALER_MERCHANT_image_data_url_valid (is.logo)) )
    151   {
    152     GNUNET_break_op (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                                        "logo");
    158   }
    159 
    160   if (! TMH_location_object_valid (is.jurisdiction))
    161   {
    162     GNUNET_break_op (0);
    163     GNUNET_JSON_parse_free (spec);
    164     return TALER_MHD_reply_with_error (connection,
    165                                        MHD_HTTP_BAD_REQUEST,
    166                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    167                                        "jurisdiction");
    168   }
    169 
    170   if (no_transfer_delay)
    171     is.default_wire_transfer_delay = mi->settings.default_wire_transfer_delay;
    172   if (no_pay_delay)
    173     is.default_pay_delay = mi->settings.default_pay_delay;
    174   if (no_wire_transfer_rounding_interval)
    175     is.default_wire_transfer_rounding_interval =
    176       mi->settings.default_wire_transfer_rounding_interval;
    177   if (no_refund_delay)
    178     is.default_refund_delay = mi->settings.default_refund_delay;
    179   if (GNUNET_TIME_relative_is_forever (is.default_pay_delay))
    180   {
    181     GNUNET_break_op (0);
    182     GNUNET_JSON_parse_free (spec);
    183     return TALER_MHD_reply_with_error (connection,
    184                                        MHD_HTTP_BAD_REQUEST,
    185                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    186                                        "default_pay_delay");
    187   }
    188   if (GNUNET_TIME_relative_is_forever (is.default_refund_delay))
    189   {
    190     GNUNET_break_op (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                                        "default_refund_delay");
    196   }
    197   if (GNUNET_TIME_relative_is_forever (is.default_wire_transfer_delay))
    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                                        "default_wire_transfer_delay");
    205   }
    206   if (NULL != iphone)
    207   {
    208     is.phone = TALER_MERCHANT_phone_validate_normalize (iphone,
    209                                                         false);
    210     if (NULL == is.phone)
    211     {
    212       GNUNET_break_op (0);
    213       GNUNET_JSON_parse_free (spec);
    214       return TALER_MHD_reply_with_error (connection,
    215                                          MHD_HTTP_BAD_REQUEST,
    216                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    217                                          "phone_number");
    218     }
    219     if ( (NULL != TMH_phone_regex) &&
    220          (0 !=
    221           regexec (&TMH_phone_rx,
    222                    is.phone,
    223                    0,
    224                    NULL,
    225                    0)) )
    226     {
    227       GNUNET_break_op (0);
    228       GNUNET_JSON_parse_free (spec);
    229       return TALER_MHD_reply_with_error (connection,
    230                                          MHD_HTTP_BAD_REQUEST,
    231                                          TALER_EC_GENERIC_PARAMETER_MALFORMED,
    232                                          "phone_number");
    233     }
    234   }
    235   if ( (NULL != is.email) &&
    236        (! TALER_MERCHANT_email_valid (is.email)) )
    237   {
    238     GNUNET_break_op (0);
    239     GNUNET_JSON_parse_free (spec);
    240     GNUNET_free (is.phone);
    241     return TALER_MHD_reply_with_error (connection,
    242                                        MHD_HTTP_BAD_REQUEST,
    243                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
    244                                        "email");
    245   }
    246   if ( (NULL != is.phone) &&
    247        (NULL != mi->settings.phone) &&
    248        (0 == strcmp (mi->settings.phone,
    249                      is.phone)) )
    250     is.phone_validated = mi->settings.phone_validated;
    251   if ( (NULL != is.email) &&
    252        (NULL != mi->settings.email) &&
    253        (0 == strcmp (mi->settings.email,
    254                      is.email)) )
    255     is.email_validated = mi->settings.email_validated;
    256   if (mfa_check)
    257   {
    258     enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
    259     enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels;
    260 
    261     if ( (0 != (mtc & TMH_TCS_SMS)) &&
    262          (NULL != mi->settings.phone) &&
    263          (NULL == is.phone) )
    264     {
    265       GNUNET_break_op (0);
    266       GNUNET_JSON_parse_free (spec);
    267       GNUNET_free (is.phone);
    268       return TALER_MHD_reply_with_error (
    269         connection,
    270         MHD_HTTP_BAD_REQUEST,
    271         TALER_EC_GENERIC_PARAMETER_MISSING,
    272         "phone_number");
    273     }
    274     if ( (0 != (mtc & TMH_TCS_EMAIL)) &&
    275          (NULL != mi->settings.email) &&
    276          (NULL == is.email) )
    277     {
    278       GNUNET_break_op (0);
    279       GNUNET_JSON_parse_free (spec);
    280       GNUNET_free (is.phone);
    281       return TALER_MHD_reply_with_error (
    282         connection,
    283         MHD_HTTP_BAD_REQUEST,
    284         TALER_EC_GENERIC_PARAMETER_MISSING,
    285         "email");
    286     }
    287     if ( (is.phone_validated ||
    288           (NULL == is.phone) ) &&
    289          (0 != (mtc & TMH_TCS_SMS)) )
    290       mtc -= TMH_TCS_SMS;
    291     if ( (is.email_validated ||
    292           (NULL == is.email) ) &&
    293          (0 != (mtc & TMH_TCS_EMAIL)) )
    294       mtc -= TMH_TCS_EMAIL;
    295     switch (mtc)
    296     {
    297     case TMH_TCS_NONE:
    298       ret = GNUNET_OK;
    299       break;
    300     case TMH_TCS_SMS:
    301       GNUNET_assert (NULL != is.phone);
    302       is.phone_validated = true;
    303       /* validate new phone number, if possible require old e-mail
    304          address for authorization */
    305       ret = TMH_mfa_challenges_do (hc,
    306                                    mi->settings.id,
    307                                    TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
    308                                    true,
    309                                    TALER_MERCHANT_MFA_CHANNEL_SMS,
    310                                    is.phone,
    311                                    0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL
    312                                          & TEH_mandatory_tan_channels)
    313                                    ? TALER_MERCHANT_MFA_CHANNEL_NONE
    314                                    : TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    315                                    mi->settings.email,
    316                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    317       break;
    318     case TMH_TCS_EMAIL:
    319       GNUNET_assert (NULL != is.email);
    320       is.email_validated = true;
    321       /* validate new e-mail address, if possible require old phone
    322          address for authorization */
    323       ret = TMH_mfa_challenges_do (hc,
    324                                    mi->settings.id,
    325                                    TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
    326                                    true,
    327                                    TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    328                                    is.email,
    329                                    0 == (TALER_MERCHANT_MFA_CHANNEL_SMS
    330                                          & TEH_mandatory_tan_channels)
    331                                    ? TALER_MERCHANT_MFA_CHANNEL_NONE
    332                                    : TALER_MERCHANT_MFA_CHANNEL_SMS,
    333                                    mi->settings.phone,
    334                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    335       break;
    336     case TMH_TCS_EMAIL_AND_SMS:
    337       GNUNET_assert (NULL != is.phone);
    338       GNUNET_assert (NULL != is.email);
    339       is.phone_validated = true;
    340       is.email_validated = true;
    341       /* To change both, we require both old and both new
    342          addresses to consent */
    343       ret = TMH_mfa_challenges_do (hc,
    344                                    mi->settings.id,
    345                                    TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION,
    346                                    true,
    347                                    TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    348                                    mi->settings.email,
    349                                    TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    350                                    is.email,
    351                                    TALER_MERCHANT_MFA_CHANNEL_SMS,
    352                                    mi->settings.phone,
    353                                    TALER_MERCHANT_MFA_CHANNEL_SMS,
    354                                    is.phone,
    355                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    356       break;
    357     }
    358     if (GNUNET_OK != ret)
    359     {
    360       GNUNET_JSON_parse_free (spec);
    361       GNUNET_free (is.phone);
    362       return (GNUNET_NO == ret)
    363         ? MHD_YES
    364         : MHD_NO;
    365     }
    366   }
    367 
    368   for (unsigned int retry = 0; retry<MAX_RETRIES; retry++)
    369   {
    370     /* Cleanup after earlier loops */
    371     {
    372       struct TMH_WireMethod *wm;
    373 
    374       while (NULL != (wm = wm_head))
    375       {
    376         GNUNET_CONTAINER_DLL_remove (wm_head,
    377                                      wm_tail,
    378                                      wm);
    379         free_wm (wm);
    380       }
    381     }
    382     if (GNUNET_OK !=
    383         TALER_MERCHANTDB_start (TMH_db,
    384                                 "PATCH /instances"))
    385     {
    386       GNUNET_JSON_parse_free (spec);
    387       GNUNET_free (is.phone);
    388       return TALER_MHD_reply_with_error (connection,
    389                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    390                                          TALER_EC_GENERIC_DB_START_FAILED,
    391                                          NULL);
    392     }
    393     /* Check for equality of settings */
    394     if (! ( (0 == strcmp (mi->settings.name,
    395                           name)) &&
    396             ((mi->settings.email == is.email) ||
    397              (NULL != is.email && NULL != mi->settings.email &&
    398               0 == strcmp (mi->settings.email,
    399                            is.email))) &&
    400             ((mi->settings.phone == is.phone) ||
    401              (NULL != is.phone && NULL != mi->settings.phone &&
    402               0 == strcmp (mi->settings.phone,
    403                            is.phone))) &&
    404             ((mi->settings.website == is.website) ||
    405              (NULL != is.website && NULL != mi->settings.website &&
    406               0 == strcmp (mi->settings.website,
    407                            is.website))) &&
    408             ((mi->settings.logo == is.logo) ||
    409              (NULL != is.logo && NULL != mi->settings.logo &&
    410               0 == strcmp (mi->settings.logo,
    411                            is.logo))) &&
    412             (1 == json_equal (mi->settings.address,
    413                               is.address)) &&
    414             (1 == json_equal (mi->settings.jurisdiction,
    415                               is.jurisdiction)) &&
    416             (mi->settings.use_stefan == is.use_stefan) &&
    417             (GNUNET_TIME_relative_cmp (mi->settings.default_wire_transfer_delay,
    418                                        ==,
    419                                        is.default_wire_transfer_delay)) &&
    420             (GNUNET_TIME_relative_cmp (mi->settings.default_refund_delay,
    421                                        ==,
    422                                        is.default_refund_delay)) &&
    423             (GNUNET_TIME_relative_cmp (mi->settings.default_pay_delay,
    424                                        ==,
    425                                        is.default_pay_delay)) &&
    426             (mi->settings.default_wire_transfer_rounding_interval ==
    427              is.default_wire_transfer_rounding_interval) ) )
    428     {
    429       is.id = mi->settings.id;
    430       is.name = GNUNET_strdup (name);
    431       qs = TALER_MERCHANTDB_update_instance (TMH_db,
    432                                              &is);
    433       GNUNET_free (is.name);
    434       if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    435       {
    436         TALER_MERCHANTDB_rollback (TMH_db);
    437         goto retry;
    438       }
    439     }
    440     qs = TALER_MERCHANTDB_commit (TMH_db);
    441     if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    442       qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
    443 retry:
    444     if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    445       continue;
    446     break;
    447   } /* for(... MAX_RETRIES) */
    448   if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    449   {
    450     /* Hard error or retries exhausted: the transaction did not
    451        commit, so do NOT mutate our in-memory settings and report
    452        the failure to the client. */
    453     GNUNET_break (0);
    454     GNUNET_JSON_parse_free (spec);
    455     GNUNET_free (is.phone);
    456     return TALER_MHD_reply_with_error (connection,
    457                                        MHD_HTTP_INTERNAL_SERVER_ERROR,
    458                                        TALER_EC_GENERIC_DB_COMMIT_FAILED,
    459                                        NULL);
    460   }
    461   /* Update our 'settings' */
    462   GNUNET_free (mi->settings.name);
    463   GNUNET_free (mi->settings.email);
    464   GNUNET_free (mi->settings.phone);
    465   GNUNET_free (mi->settings.website);
    466   GNUNET_free (mi->settings.logo);
    467   json_decref (mi->settings.address);
    468   json_decref (mi->settings.jurisdiction);
    469   is.id = mi->settings.id;
    470   mi->settings = is;
    471   mi->settings.address = json_incref (mi->settings.address);
    472   mi->settings.jurisdiction = json_incref (mi->settings.jurisdiction);
    473   mi->settings.name = GNUNET_strdup (name);
    474   if (NULL != is.email)
    475     mi->settings.email = GNUNET_strdup (is.email);
    476   mi->settings.phone = is.phone;
    477   is.phone = NULL;
    478   if (NULL != is.website)
    479     mi->settings.website = GNUNET_strdup (is.website);
    480   if (NULL != is.logo)
    481     mi->settings.logo = GNUNET_strdup (is.logo);
    482 
    483   GNUNET_JSON_parse_free (spec);
    484   TMH_reload_instances (mi->settings.id);
    485   return TALER_MHD_reply_static (connection,
    486                                  MHD_HTTP_NO_CONTENT,
    487                                  NULL,
    488                                  NULL,
    489                                  0);
    490 }
    491 
    492 
    493 enum MHD_Result
    494 TMH_private_patch_instances_ID (const struct TMH_RequestHandler *rh,
    495                                 struct MHD_Connection *connection,
    496                                 struct TMH_HandlerContext *hc)
    497 {
    498   struct TMH_MerchantInstance *mi = hc->instance;
    499 
    500   return patch_instances_ID (mi,
    501                              true,
    502                              connection,
    503                              hc);
    504 }
    505 
    506 
    507 enum MHD_Result
    508 TMH_private_patch_instances_default_ID (const struct TMH_RequestHandler *rh,
    509                                         struct MHD_Connection *connection,
    510                                         struct TMH_HandlerContext *hc)
    511 {
    512   struct TMH_MerchantInstance *mi;
    513 
    514   mi = TMH_lookup_instance (hc->infix);
    515   if (NULL == mi)
    516   {
    517     return TALER_MHD_reply_with_error (connection,
    518                                        MHD_HTTP_NOT_FOUND,
    519                                        TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    520                                        hc->infix);
    521   }
    522   if (mi->deleted)
    523   {
    524     return TALER_MHD_reply_with_error (connection,
    525                                        MHD_HTTP_CONFLICT,
    526                                        TALER_EC_MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED,
    527                                        hc->infix);
    528   }
    529   return patch_instances_ID (mi,
    530                              false,
    531                              connection,
    532                              hc);
    533 }
    534 
    535 
    536 /* end of taler-merchant-httpd_patch-management-instances-INSTANCE.c */