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 */