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-attributes-get.c (23014B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025 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-attributes-get.c
     18  * @brief Return summary information about KYC attributes
     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_kyclogic_lib.h"
     29 #include "taler/taler_signatures.h"
     30 #include "taler-exchange-httpd.h"
     31 #include "taler/taler_exchangedb_plugin.h"
     32 #include "taler-exchange-httpd_aml-attributes-get.h"
     33 #include "taler-exchange-httpd_metrics.h"
     34 
     35 /**
     36  * Maximum number of records we return in one request.
     37  */
     38 #define MAX_RECORDS 1024
     39 
     40 
     41 /**
     42  * Closure for the detail_cb().
     43  */
     44 struct ResponseContext
     45 {
     46   /**
     47    * Format of the response we are to generate.
     48    */
     49   enum
     50   {
     51     RCF_JSON,
     52     RCF_PDF
     53   } format;
     54 
     55   /**
     56    * Stored in a DLL while suspended.
     57    */
     58   struct ResponseContext *next;
     59 
     60   /**
     61    * Stored in a DLL while suspended.
     62    */
     63   struct ResponseContext *prev;
     64 
     65   /**
     66    * Context for this request.
     67    */
     68   struct TEH_RequestContext *rc;
     69 
     70   /**
     71    * Async context used to run Typst.
     72    */
     73   struct TALER_MHD_TypstContext *tc;
     74 
     75   /**
     76    * Response to return.
     77    */
     78   struct MHD_Response *response;
     79 
     80   /**
     81    * HTTP status to use with @e response.
     82    */
     83   unsigned int http_status;
     84 
     85   /**
     86    * True if this is for a wallet.
     87    */
     88   bool is_wallet;
     89 
     90   /**
     91    * Where we store the response data.
     92    */
     93   union
     94   {
     95     /**
     96      * If @e format is #RCF_JSON.
     97      */
     98     json_t *json;
     99 
    100     /**
    101      * If @e format is #RCF_PDF.
    102      */
    103     struct
    104     {
    105 
    106       /**
    107        * Typst forms to compile.
    108        */
    109       struct TALER_MHD_TypstDocument docs[MAX_RECORDS];
    110 
    111       /**
    112        * JSON data for each Typst form.
    113        */
    114       json_t *jdata[MAX_RECORDS];
    115 
    116       /**
    117        * Next write offset into @e docs and @e jdata.
    118        */
    119       size_t off;
    120 
    121       /**
    122        * Global attributes we need to inject in to each @e jdata.
    123        */
    124       json_t *global_attrs;
    125 
    126     } pdf;
    127 
    128   } details;
    129 };
    130 
    131 
    132 /**
    133  * DLL of requests awaiting Typst.
    134  */
    135 static struct ResponseContext *rctx_head;
    136 
    137 /**
    138  * DLL of requests awaiting Typst.
    139  */
    140 static struct ResponseContext *rctx_tail;
    141 
    142 
    143 void
    144 TEH_handler_aml_attributes_get_cleanup ()
    145 {
    146   struct ResponseContext *rctx;
    147 
    148   while (NULL != (rctx = rctx_head))
    149   {
    150     GNUNET_CONTAINER_DLL_remove (rctx_head,
    151                                  rctx_tail,
    152                                  rctx);
    153     MHD_resume_connection (rctx->rc->connection);
    154   }
    155 }
    156 
    157 
    158 /**
    159  * Free resources from @a rc
    160  *
    161  * @param[in] rc context to clean up
    162  */
    163 static void
    164 free_rc (struct TEH_RequestContext *rc)
    165 {
    166   struct ResponseContext *rctx = rc->rh_ctx;
    167 
    168   if (NULL != rctx->tc)
    169   {
    170     TALER_MHD_typst_cancel (rctx->tc);
    171     rctx->tc = NULL;
    172   }
    173   if (NULL != rctx->response)
    174   {
    175     MHD_destroy_response (rctx->response);
    176     rctx->response = NULL;
    177   }
    178   switch (rctx->format)
    179   {
    180   case RCF_JSON:
    181     json_decref (rctx->details.json);
    182     break;
    183   case RCF_PDF:
    184     for (size_t i = 0; i<rctx->details.pdf.off; i++)
    185     {
    186       json_decref (rctx->details.pdf.jdata[i]);
    187       rctx->details.pdf.jdata[i] = NULL;
    188     }
    189     json_decref (rctx->details.pdf.global_attrs);
    190     rctx->details.pdf.global_attrs = NULL;
    191     break;
    192   }
    193   GNUNET_free (rctx);
    194 }
    195 
    196 
    197 /**
    198  * Dump attachment @a attach into @a rc.
    199  *
    200  * @param[in,out] rc response context to update
    201  * @param attach attachment to dump
    202  * @return true on success
    203  */
    204 static bool
    205 dump_attachment (struct ResponseContext *rc,
    206                  const json_t *attach)
    207 {
    208   if (! json_is_string (attach))
    209     return false;
    210   if (rc->details.pdf.off >= MAX_RECORDS)
    211   {
    212     GNUNET_break (0);
    213     return false;
    214   }
    215   rc->details.pdf.jdata[rc->details.pdf.off]
    216     = json_incref ((json_t *) attach);
    217   rc->details.pdf.docs[rc->details.pdf.off].form_name
    218     = NULL; /* inline PDF */
    219   rc->details.pdf.docs[rc->details.pdf.off].form_version
    220     = NULL; /* none */
    221   rc->details.pdf.docs[rc->details.pdf.off].data
    222     = rc->details.pdf.jdata[rc->details.pdf.off];
    223   rc->details.pdf.off++;
    224   return true;
    225 }
    226 
    227 
    228 /**
    229  * Recursively scan @a attrs for ".FILE" members and dump those
    230  * attachments into @a rc.
    231  *
    232  * @param[in,out] rc response context to update
    233  * @param attrs attributes to scan recursively
    234  * @return true on success
    235  */
    236 static bool
    237 dump_attachments (struct ResponseContext *rc,
    238                   const json_t *attrs)
    239 {
    240   bool ret = true;
    241 
    242   if (json_is_array (attrs))
    243   {
    244     const json_t *e;
    245     size_t i;
    246 
    247     json_array_foreach ((json_t *) attrs, i, e)
    248     if (! dump_attachments (rc,
    249                             e))
    250       ret = false;
    251   }
    252   if (json_is_object (attrs))
    253   {
    254     const json_t *e;
    255     const char *k;
    256 
    257     json_object_foreach ((json_t *) attrs, k, e)
    258     {
    259       if (0 == strcmp (k,
    260                        "CONTENTS"))
    261       {
    262         const char *v;
    263 
    264         /* Make sure this is the supported encoding */
    265         v = json_string_value (json_object_get (attrs,
    266                                                 "ENCODING"));
    267         if (NULL == v)
    268         {
    269           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    270                       "`CONTENTS' attribute found without `ENCODING', skipping dumping attachment\n");
    271           continue; /* maybe not a file!? */
    272         }
    273         if (0 != strcmp (v,
    274                          "base64"))
    275         {
    276           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    277                       "Attachment with unsupported encoding `%s' encountered, skipping attachment in PDF generation\n",
    278                       v);
    279           continue;
    280         }
    281         v = json_string_value (json_object_get (attrs,
    282                                                 "MIME_TYPE"));
    283         if (NULL == v)
    284         {
    285           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    286                       "`CONTENTS' attribute found without `MIME_TYPE', skipping dumping attachment\n");
    287           continue;
    288         }
    289         if (0 != strcmp (v,
    290                          "application/pdf"))
    291         {
    292           GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    293                       "Attachment with unsupported mime type `%s' encountered, skipping attachment in PDF generation\n",
    294                       v);
    295           continue;
    296         }
    297         /* Filename is optional, only log it */
    298         v = json_string_value (json_object_get (attrs,
    299                                                 "FILENAME"));
    300         if (NULL != v)
    301         {
    302           GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    303                       "Dumping attachment `%s'\n",
    304                       v);
    305         }
    306         if (! dump_attachment (rc,
    307                                e))
    308           ret = false;
    309       }
    310       else if (! dump_attachments (rc,
    311                                    e))
    312         ret = false;
    313     }
    314   }
    315   return ret;
    316 }
    317 
    318 
    319 /**
    320  * Return AML account attributes.
    321  *
    322  * @param cls closure
    323  * @param row_id current row in kyc_attributes table
    324  * @param collection_time when were the attributes collected
    325  * @param by_aml_officer was the attribute set filed by the AML officer
    326  * @param staff_name name of the officer, NULL if not @a by_aml_officer
    327  * @param enc_attributes_size length of @a enc_attributes
    328  * @param enc_attributes the encrypted collected attributes
    329  */
    330 static void
    331 detail_cb (
    332   void *cls,
    333   uint64_t row_id,
    334   struct GNUNET_TIME_Timestamp collection_time,
    335   bool by_aml_officer,
    336   const char *staff_name,
    337   size_t enc_attributes_size,
    338   const void *enc_attributes)
    339 {
    340   static char *datadir = NULL;
    341   struct ResponseContext *rc = cls;
    342   json_t *attrs;
    343   const char *form_id;
    344 
    345   if (NULL == datadir)
    346   {
    347     datadir = GNUNET_OS_installation_get_path (
    348       TALER_EXCHANGE_project_data (),
    349       GNUNET_OS_IPK_DATADIR);
    350   }
    351   attrs = TALER_CRYPTO_kyc_attributes_decrypt (&TEH_attribute_key,
    352                                                enc_attributes,
    353                                                enc_attributes_size);
    354   if (NULL == attrs)
    355   {
    356     GNUNET_break (0);
    357     return;
    358   }
    359   form_id = json_string_value (json_object_get (attrs,
    360                                                 "FORM_ID"));
    361   if (NULL == form_id)
    362   {
    363     GNUNET_break (0);
    364     return;
    365   }
    366   switch (rc->format)
    367   {
    368   case RCF_JSON:
    369     GNUNET_assert (
    370       0 ==
    371       json_array_append_new (
    372         rc->details.json,
    373         GNUNET_JSON_PACK (
    374           GNUNET_JSON_pack_int64 ("rowid",
    375                                   row_id),
    376           GNUNET_JSON_pack_bool ("by_aml_officer",
    377                                  by_aml_officer),
    378           GNUNET_JSON_pack_allow_null (
    379             GNUNET_JSON_pack_object_steal ("attributes",
    380                                            attrs)),
    381           GNUNET_JSON_pack_timestamp ("collection_time",
    382                                       collection_time)
    383           )));
    384     break;
    385   case RCF_PDF:
    386     if (rc->details.pdf.off >= MAX_RECORDS)
    387     {
    388       GNUNET_break (0);
    389       return;
    390     }
    391     GNUNET_assert (0 ==
    392                    json_object_set_new (attrs,
    393                                         "DATADIR",
    394                                         json_string (datadir)));
    395     GNUNET_assert (0 ==
    396                    json_object_set_new (attrs,
    397                                         "BY_AML_OFFICER",
    398                                         json_boolean (by_aml_officer)));
    399     GNUNET_assert (0 ==
    400                    json_object_set_new (attrs,
    401                                         "FILING_DATE",
    402                                         json_string (
    403                                           GNUNET_STRINGS_timestamp_to_string (
    404                                             collection_time))));
    405     if (by_aml_officer)
    406       GNUNET_assert (0 ==
    407                      json_object_set_new (attrs,
    408                                           "AML_STAFF_NAME",
    409                                           json_string (staff_name)));
    410     {
    411       char *have;
    412 
    413       GNUNET_asprintf (&have,
    414                        "HAVE_%s",
    415                        form_id);
    416       GNUNET_assert (0 ==
    417                      json_object_set_new (rc->details.pdf.global_attrs,
    418                                           have,
    419                                           json_true ()));
    420       GNUNET_free (have);
    421     }
    422     rc->details.pdf.jdata[rc->details.pdf.off]
    423       = attrs;
    424     rc->details.pdf.docs[rc->details.pdf.off].form_name
    425       = form_id;
    426     rc->details.pdf.docs[rc->details.pdf.off].form_version
    427       = NULL; /* not yet supported! */
    428     rc->details.pdf.docs[rc->details.pdf.off].data
    429       = rc->details.pdf.jdata[rc->details.pdf.off];
    430     rc->details.pdf.off++;
    431     if (! dump_attachments (rc,
    432                             attrs))
    433     {
    434       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    435                   "Failed to dump some attachment!\n");
    436     }
    437     break;
    438   }
    439 }
    440 
    441 
    442 /**
    443  * Function called with the result of a #TALER_MHD_typst() operation.
    444  *
    445  * @param cls closure
    446  * @param tr result of the operation
    447  */
    448 static void
    449 pdf_cb (void *cls,
    450         const struct TALER_MHD_TypstResponse *tr)
    451 {
    452   struct ResponseContext *rctx = cls;
    453 
    454   rctx->tc = NULL;
    455   GNUNET_CONTAINER_DLL_remove (rctx_head,
    456                                rctx_tail,
    457                                rctx);
    458   MHD_resume_connection (rctx->rc->connection);
    459   TALER_MHD_daemon_trigger ();
    460   if (TALER_EC_NONE != tr->ec)
    461   {
    462     rctx->http_status
    463       = TALER_ErrorCode_get_http_status (tr->ec);
    464     rctx->response
    465       = TALER_MHD_make_error (tr->ec,
    466                               tr->details.hint);
    467     return;
    468   }
    469   rctx->http_status
    470     = MHD_HTTP_OK;
    471   rctx->response
    472     = TALER_MHD_response_from_pdf_file (tr->details.filename);
    473 }
    474 
    475 
    476 /**
    477  * Function called with the latest AML decision on an
    478  * account. Used to build the cover page.
    479  *
    480  * @param cls a `struct ResponseContext *`
    481  * @param outcome_serial_id row ID of the decision
    482  * @param decision_time when was the decision taken
    483  * @param justification what was the given justification
    484  * @param decider_pub which key signed the decision
    485  * @param jproperties what are the new account properties
    486  * @param jnew_rules what are the new account rules
    487  * @param to_investigate should AML staff investigate
    488  *          after the decision
    489  * @param is_active is this the active decision
    490  */
    491 static void
    492 build_cover_page (
    493   void *cls,
    494   uint64_t outcome_serial_id,
    495   struct GNUNET_TIME_Timestamp decision_time,
    496   const char *justification,
    497   const struct TALER_AmlOfficerPublicKeyP *decider_pub,
    498   const json_t *jproperties,
    499   const json_t *jnew_rules,
    500   bool to_investigate,
    501   bool is_active)
    502 {
    503   struct ResponseContext *rc = cls;
    504   json_t *cover;
    505   json_t *defr = NULL;
    506 
    507   if (NULL == jnew_rules)
    508   {
    509     defr = TALER_KYCLOGIC_get_default_legi_rules (rc->is_wallet);
    510     jnew_rules = defr;
    511   }
    512   cover = GNUNET_JSON_PACK (
    513     GNUNET_JSON_pack_allow_null (
    514       GNUNET_JSON_pack_object_incref ("properties",
    515                                       (json_t *) jproperties)),
    516     GNUNET_JSON_pack_allow_null (
    517       GNUNET_JSON_pack_object_incref ("rules",
    518                                       (json_t *) jnew_rules)),
    519     GNUNET_JSON_pack_allow_null (
    520       GNUNET_JSON_pack_string ("last_justification",
    521                                justification)),
    522     GNUNET_JSON_pack_bool ("is_active",
    523                            is_active),
    524     GNUNET_JSON_pack_bool ("to_investigate",
    525                            to_investigate));
    526   if (NULL != defr)
    527     json_decref (defr);
    528   /* first page, so we really cannot have hit the maximum yet */
    529   GNUNET_assert (rc->details.pdf.off < MAX_RECORDS);
    530   rc->details.pdf.jdata[rc->details.pdf.off]
    531     = cover;
    532   rc->details.pdf.docs[rc->details.pdf.off].form_name
    533     = "_cover_";
    534   rc->details.pdf.docs[rc->details.pdf.off].form_version
    535     = NULL; /* not yet supported! */
    536   rc->details.pdf.docs[rc->details.pdf.off].data
    537     = rc->details.pdf.jdata[rc->details.pdf.off];
    538   rc->details.pdf.off++;
    539 }
    540 
    541 
    542 MHD_RESULT
    543 TEH_handler_aml_attributes_get (
    544   struct TEH_RequestContext *rc,
    545   const struct TALER_AmlOfficerPublicKeyP *officer_pub,
    546   const char *const args[])
    547 {
    548   struct ResponseContext *rctx = rc->rh_ctx;
    549   int64_t limit = -20;
    550   uint64_t offset;
    551   struct TALER_NormalizedPaytoHashP h_payto;
    552   uint64_t file_number;
    553 
    554   if (NULL == rctx)
    555   {
    556     rctx = GNUNET_new (struct ResponseContext);
    557     rctx->rc = rc;
    558     rc->rh_ctx = rctx;
    559     rc->rh_cleaner = &free_rc;
    560   }
    561   else
    562   {
    563     if (NULL == rctx->response)
    564     {
    565       GNUNET_break (0);
    566       return MHD_NO;
    567     }
    568     return MHD_queue_response (rc->connection,
    569                                rctx->http_status,
    570                                rctx->response);
    571   }
    572   if ( (NULL == args[0]) ||
    573        (NULL != args[1]) )
    574   {
    575     GNUNET_break_op (0);
    576     return TALER_MHD_reply_with_error (
    577       rc->connection,
    578       MHD_HTTP_NOT_FOUND,
    579       TALER_EC_GENERIC_ENDPOINT_UNKNOWN,
    580       rc->url);
    581   }
    582   if (GNUNET_OK !=
    583       GNUNET_STRINGS_string_to_data (args[0],
    584                                      strlen (args[0]),
    585                                      &h_payto,
    586                                      sizeof (h_payto)))
    587   {
    588     GNUNET_break_op (0);
    589     return TALER_MHD_reply_with_error (
    590       rc->connection,
    591       MHD_HTTP_BAD_REQUEST,
    592       TALER_EC_GENERIC_PATH_SEGMENT_MALFORMED,
    593       "h_payto");
    594   }
    595 
    596   TALER_MHD_parse_request_snumber (rc->connection,
    597                                    "limit",
    598                                    &limit);
    599   if (limit > 0)
    600     offset = 0;
    601   else
    602     offset = INT64_MAX;
    603   TALER_MHD_parse_request_number (rc->connection,
    604                                   "offset",
    605                                   &offset);
    606 
    607   {
    608     enum GNUNET_DB_QueryStatus qs;
    609 
    610     qs = TEH_plugin->lookup_aml_file_number (TEH_plugin->cls,
    611                                              &h_payto,
    612                                              &file_number,
    613                                              &rctx->is_wallet);
    614     switch (qs)
    615     {
    616     case GNUNET_DB_STATUS_HARD_ERROR:
    617     case GNUNET_DB_STATUS_SOFT_ERROR:
    618       GNUNET_break (0);
    619       return TALER_MHD_reply_with_error (
    620         rc->connection,
    621         MHD_HTTP_INTERNAL_SERVER_ERROR,
    622         TALER_EC_GENERIC_DB_FETCH_FAILED,
    623         "lookup_aml_file_number");
    624     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    625       /* Account unknown, return 404 */
    626       return TALER_MHD_reply_with_error (
    627         rc->connection,
    628         MHD_HTTP_NOT_FOUND,
    629         TALER_EC_EXCHANGE_GENERIC_TARGET_ACCOUNT_UNKNOWN,
    630         args[0]);
    631     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    632       break;
    633     }
    634   }
    635 
    636   {
    637     const char *mime;
    638 
    639     mime = MHD_lookup_connection_value (rc->connection,
    640                                         MHD_HEADER_KIND,
    641                                         MHD_HTTP_HEADER_ACCEPT);
    642     if (NULL == mime)
    643       mime = "application/json";
    644     if (0 == strcmp (mime,
    645                      "application/json"))
    646     {
    647       rctx->format = RCF_JSON;
    648       rctx->details.json = json_array ();
    649       GNUNET_assert (NULL != rctx->details.json);
    650     }
    651     else if (0 == strcmp (mime,
    652                           "application/pdf"))
    653     {
    654       enum GNUNET_DB_QueryStatus qs;
    655 
    656       rctx->format = RCF_PDF;
    657       rctx->details.pdf.global_attrs = json_object ();
    658       GNUNET_assert (NULL != rctx->details.pdf.global_attrs);
    659       if (NULL != TEH_global_pdf_form_data)
    660         GNUNET_assert (0 ==
    661                        json_object_update (rctx->details.pdf.global_attrs,
    662                                            TEH_global_pdf_form_data));
    663       GNUNET_assert (0 ==
    664                      json_object_set_new (rctx->details.pdf.global_attrs,
    665                                           "FILE_NUMBER",
    666                                           json_integer (file_number)));
    667       /* Lookup latest AML decision & account rules & properties
    668          to build the cover page */
    669       qs = TEH_plugin->lookup_aml_history (TEH_plugin->cls,
    670                                            &h_payto,
    671                                            UINT64_MAX, /* offset */
    672                                            -1, /* latest decision only */
    673                                            &build_cover_page,
    674                                            rctx);
    675       switch (qs)
    676       {
    677       case GNUNET_DB_STATUS_HARD_ERROR:
    678       case GNUNET_DB_STATUS_SOFT_ERROR:
    679         GNUNET_break (0);
    680         return TALER_MHD_reply_with_error (
    681           rc->connection,
    682           MHD_HTTP_INTERNAL_SERVER_ERROR,
    683           TALER_EC_GENERIC_DB_FETCH_FAILED,
    684           "select_aml_attributes");
    685       case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    686         /* no decision was ever taken, build empty cover page */
    687         GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    688                     "No decisions taken, creating empty cover page\n");
    689         build_cover_page (rctx,
    690                           0,
    691                           GNUNET_TIME_UNIT_ZERO_TS,
    692                           NULL,
    693                           NULL,
    694                           NULL,
    695                           NULL,
    696                           false,
    697                           false);
    698         break;
    699       case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    700         break;
    701       }
    702     }
    703     else
    704     {
    705       GNUNET_break_op (0);
    706       return TALER_MHD_REPLY_JSON_PACK (
    707         rc->connection,
    708         MHD_HTTP_NOT_ACCEPTABLE,
    709         GNUNET_JSON_pack_string ("hint",
    710                                  mime));
    711     }
    712   }
    713 
    714   {
    715     enum GNUNET_DB_QueryStatus qs;
    716 
    717     if (limit > MAX_RECORDS)
    718       limit = MAX_RECORDS;
    719     if (limit < -MAX_RECORDS)
    720       limit = -MAX_RECORDS;
    721     qs = TEH_plugin->select_aml_attributes (
    722       TEH_plugin->cls,
    723       &h_payto,
    724       offset,
    725       limit,
    726       &detail_cb,
    727       rctx);
    728     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    729                 "Obtained %d/%d attributes\n",
    730                 qs,
    731                 (int) (limit < 0 ? -limit : limit));
    732     switch (qs)
    733     {
    734     case GNUNET_DB_STATUS_HARD_ERROR:
    735     case GNUNET_DB_STATUS_SOFT_ERROR:
    736       GNUNET_break (0);
    737       return TALER_MHD_reply_with_error (
    738         rc->connection,
    739         MHD_HTTP_INTERNAL_SERVER_ERROR,
    740         TALER_EC_GENERIC_DB_FETCH_FAILED,
    741         "select_aml_attributes");
    742     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    743       if (RCF_JSON == rctx->format)
    744         return TALER_MHD_reply_static (
    745           rc->connection,
    746           MHD_HTTP_NO_CONTENT,
    747           NULL,
    748           NULL,
    749           0);
    750       break;
    751     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    752       break;
    753     }
    754   }
    755 
    756   switch (rctx->format)
    757   {
    758   case RCF_JSON:
    759     return TALER_MHD_REPLY_JSON_PACK (
    760       rc->connection,
    761       MHD_HTTP_OK,
    762       GNUNET_JSON_pack_array_incref ("details",
    763                                      rctx->details.json));
    764   case RCF_PDF:
    765     /* Cover page: off of 0 is impossible */
    766     GNUNET_assert (0 != rctx->details.pdf.off);
    767     for (unsigned int i = 0; i<rctx->details.pdf.off; i++)
    768     {
    769       /* The jdata[i] could be a *string* in case of attachments,
    770          ignore those here! */
    771       if (NULL == rctx->details.pdf.docs[i].form_name)
    772       {
    773         GNUNET_assert (json_is_string (rctx->details.pdf.jdata[i]));
    774       }
    775       else
    776       {
    777         GNUNET_assert (json_is_object (rctx->details.pdf.jdata[i]));
    778         GNUNET_assert (0 ==
    779                        json_object_update_missing (
    780                          rctx->details.pdf.jdata[i],
    781                          rctx->details.pdf.global_attrs)
    782                        );
    783       }
    784     }
    785     rctx->tc = TALER_MHD_typst (TALER_EXCHANGE_project_data (),
    786                                 TEH_cfg,
    787                                 false,
    788                                 "exchange",
    789                                 rctx->details.pdf.off,
    790                                 rctx->details.pdf.docs,
    791                                 &pdf_cb,
    792                                 rctx);
    793     if (NULL == rctx->tc)
    794     {
    795       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    796                   "Client requested PDF, but Typst is unavailable\n");
    797       return TALER_MHD_reply_with_error (
    798         rc->connection,
    799         MHD_HTTP_NOT_IMPLEMENTED,
    800         TALER_EC_EXCHANGE_GENERIC_NO_TYPST_OR_PDFTK,
    801         NULL);
    802     }
    803     GNUNET_CONTAINER_DLL_insert (rctx_head,
    804                                  rctx_tail,
    805                                  rctx);
    806     MHD_suspend_connection (rc->connection);
    807     return MHD_YES;
    808   }
    809   GNUNET_assert (0);
    810   return MHD_NO;
    811 }
    812 
    813 
    814 /* end of taler-exchange-httpd_aml-attributes_get.c */