exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

taler-exchange-httpd_aml-accounts-get.c (15838B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file taler-exchange-httpd_aml-accounts-get.c
     18  * @brief Return summary information about accounts
     19  * @author Christian Grothoff
     20  */
     21 #include "taler/platform.h"
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include <jansson.h>
     24 #include <microhttpd.h>
     25 #include <pthread.h>
     26 #include "taler/taler_json_lib.h"
     27 #include "taler/taler_mhd_lib.h"
     28 #include "taler/taler_signatures.h"
     29 #include "taler-exchange-httpd.h"
     30 #include "taler/taler_exchangedb_plugin.h"
     31 #include "taler-exchange-httpd_aml-accounts-get.h"
     32 
     33 
     34 /**
     35  * Maximum number of records we return in one go. Must be
     36  * small enough to ensure that XML/CSV encoded results stay
     37  * below the 16MB limit of GNUNET_malloc().
     38  */
     39 #define MAX_RECORDS (1024 * 64)
     40 
     41 #define CSV_HEADER \
     42         "File number,Customer,Comments,Risky,Acquisition date,Exit date\r\n"
     43 #define CSV_FOOTER "\r\n"
     44 
     45 #define XML_HEADER "<?xml version=\"1.0\" encoding=\"UTF 8\"?>" \
     46         "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"" \
     47         " xmlns:c=\"urn:schemas-microsoft-com:office:component:spreadsheet\"" \
     48         " xmlns:html=\"http://www.w3.org/TR/REC-html40\"" \
     49         " xmlns:x2=\"http://schemas.microsoft.com/office/excel/2003/xml\"" \
     50         " xmlns:o=\"urn:schemas-microsoft-com:office:office\""        \
     51         " xmlns:x=\"urn:schemas-microsoft-com:office:excel\""         \
     52         " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">" \
     53         "<Styles><Style ss:ID=\"DateFormat\"><NumberFormat ss:Format=\"yyyy-mm-dd\"/></Style></Styles>\n" \
     54         "<Worksheet ss:Name=\"Accounts\">\n"                          \
     55         "<Table>\n" \
     56         "<Row>\n" \
     57         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">File number</Data></Cell>\n" \
     58         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Customer</Data></Cell>\n" \
     59         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Comments</Data></Cell>\n" \
     60         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Increased risk business relationship</Data></Cell>\n" \
     61         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Acquisition date</Data></Cell>\n" \
     62         "<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">Exit date</Data></Cell>\n" \
     63         "</Row>\n"
     64 #define XML_FOOTER "</Table></Worksheet></Workbook>"
     65 
     66 /**
     67  * Closure for the record_cb().
     68  */
     69 struct ResponseContext
     70 {
     71   /**
     72    * Format of the response we are to generate.
     73    */
     74   enum
     75   {
     76     RCF_JSON,
     77     RCF_XML,
     78     RCF_CSV
     79   } format;
     80 
     81   /**
     82    * Where we store the response data.
     83    */
     84   union
     85   {
     86     /**
     87      * If @e format is #RCF_JSON.
     88      */
     89     json_t *json;
     90 
     91     /**
     92      * If @e format is #RCF_XML.
     93      */
     94     struct GNUNET_Buffer xml;
     95 
     96     /**
     97      * If @e format is #RCF_CSV.
     98      */
     99     struct GNUNET_Buffer csv;
    100 
    101   } details;
    102 };
    103 
    104 
    105 /**
    106  * Free resources from @a rc
    107  *
    108  * @param[in] rc context to clean up
    109  */
    110 static void
    111 free_rc (struct ResponseContext *rc)
    112 {
    113   switch (rc->format)
    114   {
    115   case RCF_JSON:
    116     json_decref (rc->details.json);
    117     break;
    118   case RCF_XML:
    119     GNUNET_buffer_clear (&rc->details.xml);
    120     break;
    121   case RCF_CSV:
    122     GNUNET_buffer_clear (&rc->details.csv);
    123     break;
    124   }
    125 }
    126 
    127 
    128 /**
    129  * Return account summary information.
    130  *
    131  * @param cls closure
    132  * @param row_id current row in AML status table
    133  * @param h_payto account for which the attribute data is stored
    134  * @param open_time when was the account opened formally,
    135  *          GNUNET_TIME_UNIT_FOREVER_TS if it was never opened
    136  * @param close_time when was the account formally closed,
    137  *          GNUNET_TIME_UNIT_ZERO_TS if it was never closed
    138  * @param comments comments on the account
    139  * @param high_risk is this a high-risk business relationship
    140  * @param to_investigate TRUE if this account should be investigated
    141  * @param payto the payto URI of the account
    142  */
    143 static void
    144 record_cb (
    145   void *cls,
    146   uint64_t row_id,
    147   const struct TALER_NormalizedPaytoHashP *h_payto,
    148   struct GNUNET_TIME_Timestamp open_time,
    149   struct GNUNET_TIME_Timestamp close_time,
    150   const char *comments,
    151   bool high_risk,
    152   bool to_investigate,
    153   struct TALER_FullPayto payto)
    154 {
    155   struct ResponseContext *rc = cls;
    156 
    157   if ( (NULL == comments) &&
    158        (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
    159     comments = "transacted amounts below limits that trigger account opening";
    160   if (NULL == comments)
    161     comments = "";
    162   switch (rc->format)
    163   {
    164   case RCF_JSON:
    165     GNUNET_assert (
    166       0 ==
    167       json_array_append_new (
    168         rc->details.json,
    169         GNUNET_JSON_PACK (
    170           GNUNET_JSON_pack_data_auto ("h_payto",
    171                                       h_payto),
    172           TALER_JSON_pack_full_payto ("full_payto",
    173                                       payto),
    174           GNUNET_JSON_pack_bool ("high_risk",
    175                                  high_risk),
    176           GNUNET_JSON_pack_allow_null (
    177             GNUNET_JSON_pack_string ("comments",
    178                                      comments)),
    179           GNUNET_JSON_pack_int64 ("rowid",
    180                                   row_id),
    181           GNUNET_JSON_pack_timestamp ("open_time",
    182                                       open_time),
    183           GNUNET_JSON_pack_timestamp ("close_time",
    184                                       close_time),
    185           GNUNET_JSON_pack_bool ("to_investigate",
    186                                  to_investigate)
    187           )));
    188     return;
    189   case RCF_XML:
    190     {
    191       char *epayto;
    192       char *ecomments = NULL;
    193       char opentime_s[128];
    194       char closetime_s[128];
    195       const struct tm *tm;
    196       time_t tt;
    197 
    198       epayto = TALER_escape_xml (payto.full_payto);
    199       if ( (NULL == comments) &&
    200            (GNUNET_TIME_absolute_is_never (open_time.abs_time)) )
    201         comments =
    202           "transacted amounts below limits that trigger account opening";
    203       ecomments = TALER_escape_xml (comments);
    204       tt = (time_t) GNUNET_TIME_timestamp_to_s (open_time);
    205       tm = gmtime (&tt);
    206       strftime (opentime_s,
    207                 sizeof (opentime_s),
    208                 "%Y-%m-%dT%H:%M:%S",
    209                 tm);
    210       tt = (time_t) GNUNET_TIME_timestamp_to_s (close_time);
    211       tm = gmtime (&tt);
    212       strftime (closetime_s,
    213                 sizeof (closetime_s),
    214                 "%Y-%m-%dT%H:%M:%S",
    215                 tm);
    216       GNUNET_buffer_write_fstr (
    217         &rc->details.xml,
    218         "<Row>"
    219         "<Cell><Data ss:Type=\"Number\">%llu</Data></Cell>"
    220         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    221         "<Cell><Data ss:Type=\"String\">%s</Data></Cell>"
    222         "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>"
    223         "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>"
    224         "<Cell ss:StyleID=\"DateFormat\"><Data ss:Type=\"%s\">%s</Data></Cell>"
    225         "</Row>\n",
    226         (unsigned long long) row_id,
    227         epayto,
    228         NULL == ecomments
    229         ? ""
    230         : ecomments,
    231         high_risk ? "TRUE" : "FALSE",
    232         high_risk ? "1" : "0",
    233         GNUNET_TIME_absolute_is_never (open_time.abs_time)
    234         ? "String"
    235         : "DateTime",
    236         GNUNET_TIME_absolute_is_never (open_time.abs_time)
    237         ? "never"
    238         : opentime_s,
    239         GNUNET_TIME_absolute_is_never (close_time.abs_time)
    240         ? "String"
    241         : "DateTime",
    242         GNUNET_TIME_absolute_is_never (close_time.abs_time)
    243         ? "never"
    244         : closetime_s);
    245       GNUNET_free (ecomments);
    246       GNUNET_free (epayto);
    247       break;
    248     } /* end case RCF_XML */
    249   case RCF_CSV:
    250     {
    251       char *ecomments;
    252       char otbuf[64];
    253       char ctbuf[64];
    254       size_t len = strlen (comments);
    255       size_t wpos = 0;
    256 
    257       GNUNET_snprintf (otbuf,
    258                        sizeof (otbuf),
    259                        "%s",
    260                        GNUNET_TIME_timestamp2s (open_time));
    261       GNUNET_snprintf (ctbuf,
    262                        sizeof (ctbuf),
    263                        "%s",
    264                        GNUNET_TIME_timestamp2s (close_time));
    265       /* Escape 'comments' to double '"' as per RFC 4180, 2.7. */
    266       ecomments = GNUNET_malloc (2 * len + 1);
    267       for (size_t off = 0; off<len; off++)
    268       {
    269         if ('"' == comments[off])
    270           ecomments[wpos++] = '"';
    271         ecomments[wpos++] = comments[off];
    272       }
    273       GNUNET_buffer_write_fstr (&rc->details.csv,
    274                                 "%llu,\"%s\",\"%s\",%s,%s,%s\r\n",
    275                                 (unsigned long long) row_id,
    276                                 payto.full_payto,
    277                                 ecomments,
    278                                 high_risk ? "X":" ",
    279                                 GNUNET_TIME_absolute_is_never (open_time.
    280                                                                abs_time)
    281                                 ? "-"
    282                                 : otbuf,
    283                                 GNUNET_TIME_absolute_is_never (close_time.
    284                                                                abs_time)
    285                                 ? "-"
    286                                 : ctbuf);
    287       GNUNET_free (ecomments);
    288       break;
    289     } /* end case RCF_CSV */
    290   } /* end switch */
    291 }
    292 
    293 
    294 MHD_RESULT
    295 TEH_handler_aml_accounts_get (
    296   struct TEH_RequestContext *rc,
    297   const struct TALER_AmlOfficerPublicKeyP *officer_pub,
    298   const char *const args[])
    299 {
    300   struct ResponseContext rctx;
    301   int64_t limit = -20;
    302   uint64_t offset;
    303   enum TALER_EXCHANGE_YesNoAll open_filter;
    304   enum TALER_EXCHANGE_YesNoAll high_risk_filter;
    305   enum TALER_EXCHANGE_YesNoAll investigation_filter;
    306 
    307   memset (&rctx,
    308           0,
    309           sizeof (rctx));
    310   if (NULL != args[0])
    311   {
    312     GNUNET_break_op (0);
    313     return TALER_MHD_reply_with_error (
    314       rc->connection,
    315       MHD_HTTP_NOT_FOUND,
    316       TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
    317       args[0]);
    318   }
    319   TALER_MHD_parse_request_snumber (rc->connection,
    320                                    "limit",
    321                                    &limit);
    322   if (limit > 0)
    323     offset = 0;
    324   else
    325     offset = INT64_MAX;
    326   TALER_MHD_parse_request_number (rc->connection,
    327                                   "offset",
    328                                   &offset);
    329   if (offset > INT64_MAX)
    330   {
    331     GNUNET_break_op (0); /* broken client */
    332     offset = INT64_MAX;
    333   }
    334   TALER_MHD_parse_request_yna (rc->connection,
    335                                "open",
    336                                TALER_EXCHANGE_YNA_ALL,
    337                                &open_filter);
    338   TALER_MHD_parse_request_yna (rc->connection,
    339                                "investigation",
    340                                TALER_EXCHANGE_YNA_ALL,
    341                                &investigation_filter);
    342   TALER_MHD_parse_request_yna (rc->connection,
    343                                "high_risk",
    344                                TALER_EXCHANGE_YNA_ALL,
    345                                &high_risk_filter);
    346   {
    347     const char *mime;
    348 
    349     mime = MHD_lookup_connection_value (rc->connection,
    350                                         MHD_HEADER_KIND,
    351                                         MHD_HTTP_HEADER_ACCEPT);
    352     if (NULL == mime)
    353       mime = "application/json";
    354     if (0 == strcmp (mime,
    355                      "application/json"))
    356     {
    357       rctx.format = RCF_JSON;
    358       rctx.details.json = json_array ();
    359       GNUNET_assert (NULL != rctx.details.json);
    360     }
    361     else if (0 == strcmp (mime,
    362                           "application/vnd.ms-excel"))
    363     {
    364       rctx.format = RCF_XML;
    365       GNUNET_buffer_write_str (&rctx.details.xml,
    366                                XML_HEADER);
    367     }
    368     else if (0 == strcmp (mime,
    369                           "text/csv"))
    370     {
    371       rctx.format = RCF_CSV;
    372       GNUNET_buffer_write_str (&rctx.details.csv,
    373                                CSV_HEADER);
    374     }
    375     else
    376     {
    377       GNUNET_break_op (0);
    378       return TALER_MHD_reply_with_error (
    379         rc->connection,
    380         MHD_HTTP_NOT_ACCEPTABLE,
    381         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    382         mime);
    383     }
    384   }
    385 
    386   {
    387     enum GNUNET_DB_QueryStatus qs;
    388 
    389     if (limit > MAX_RECORDS)
    390       limit = MAX_RECORDS;
    391     if (limit < -MAX_RECORDS)
    392       limit = -MAX_RECORDS;
    393     qs = TEH_plugin->select_kyc_accounts (
    394       TEH_plugin->cls,
    395       investigation_filter,
    396       open_filter,
    397       high_risk_filter,
    398       offset,
    399       limit,
    400       &record_cb,
    401       &rctx);
    402     switch (qs)
    403     {
    404     case GNUNET_DB_STATUS_HARD_ERROR:
    405     case GNUNET_DB_STATUS_SOFT_ERROR:
    406       free_rc (&rctx);
    407       GNUNET_break (0);
    408       return TALER_MHD_reply_with_error (
    409         rc->connection,
    410         MHD_HTTP_INTERNAL_SERVER_ERROR,
    411         TALER_EC_GENERIC_DB_FETCH_FAILED,
    412         "select_kyx_accounts");
    413     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    414       free_rc (&rctx);
    415       return TALER_MHD_reply_static (
    416         rc->connection,
    417         MHD_HTTP_NO_CONTENT,
    418         NULL,
    419         NULL,
    420         0);
    421     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    422       break;
    423     } /* end switch (qs) */
    424 
    425     switch (rctx.format)
    426     {
    427     case RCF_JSON:
    428       return TALER_MHD_REPLY_JSON_PACK (
    429         rc->connection,
    430         MHD_HTTP_OK,
    431         GNUNET_JSON_pack_array_steal ("accounts",
    432                                       rctx.details.json));
    433     case RCF_XML:
    434       {
    435         struct MHD_Response *resp;
    436         MHD_RESULT mret;
    437 
    438         GNUNET_buffer_write_str (&rctx.details.xml,
    439                                  XML_FOOTER);
    440         /* FIXME: add support for compression */
    441         resp = MHD_create_response_from_buffer (rctx.details.xml.position,
    442                                                 rctx.details.xml.mem,
    443                                                 MHD_RESPMEM_MUST_FREE);
    444         TALER_MHD_add_global_headers (resp,
    445                                       false);
    446         GNUNET_break (MHD_YES ==
    447                       MHD_add_response_header (resp,
    448                                                MHD_HTTP_HEADER_CONTENT_TYPE,
    449                                                "application/vnd.ms-excel"));
    450         mret = MHD_queue_response (rc->connection,
    451                                    MHD_HTTP_OK,
    452                                    resp);
    453         MHD_destroy_response (resp);
    454         return mret;
    455       }
    456     case RCF_CSV:
    457       {
    458         struct MHD_Response *resp;
    459         MHD_RESULT mret;
    460 
    461         GNUNET_buffer_write_str (&rctx.details.csv,
    462                                  CSV_FOOTER);
    463         /* FIXME: add support for compression */
    464         resp = MHD_create_response_from_buffer (rctx.details.csv.position,
    465                                                 rctx.details.csv.mem,
    466                                                 MHD_RESPMEM_MUST_FREE);
    467         TALER_MHD_add_global_headers (resp,
    468                                       false);
    469         GNUNET_break (MHD_YES ==
    470                       MHD_add_response_header (resp,
    471                                                MHD_HTTP_HEADER_CONTENT_TYPE,
    472                                                "text/csv"));
    473         mret = MHD_queue_response (rc->connection,
    474                                    MHD_HTTP_OK,
    475                                    resp);
    476         MHD_destroy_response (resp);
    477         return mret;
    478       }
    479     } /* end switch (rctx.format) */
    480   }
    481   GNUNET_break (0);
    482   return MHD_NO;
    483 }
    484 
    485 
    486 /* end of taler-exchange-httpd_aml-accounts_get.c */