Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 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 Lesser General Public License as published by the Free Software
7 : Foundation; either version 2.1, 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 Lesser General Public License for more details.
12 :
13 : You should have received a copy of the GNU Lesser General Public License along with
14 : TALER; see the file COPYING.LGPL. If not, see
15 : <http://www.gnu.org/licenses/>
16 : */
17 : /**
18 : * @file src/lib/merchant_api_get-private-statistics-amount-SLUG.c
19 : * @brief Implementation of the GET /private/statistics-amount/$SLUG request
20 : * @author Christian Grothoff
21 : */
22 : #include "platform.h"
23 : #include <curl/curl.h>
24 : #include <jansson.h>
25 : #include <microhttpd.h> /* just for HTTP status codes */
26 : #include <gnunet/gnunet_util_lib.h>
27 : #include <gnunet/gnunet_curl_lib.h>
28 : #include <taler/merchant/get-private-statistics-amount-SLUG.h>
29 : #include "merchant_api_curl_defaults.h"
30 : #include <taler/taler_json_lib.h>
31 :
32 :
33 : /**
34 : * Maximum number of statistics entries we return.
35 : */
36 : #define MAX_STATISTICS 1024
37 :
38 :
39 : /**
40 : * Handle for a GET /private/statistics-amount/$SLUG operation.
41 : */
42 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle
43 : {
44 : /**
45 : * Base URL of the merchant backend.
46 : */
47 : char *base_url;
48 :
49 : /**
50 : * The full URL for this request.
51 : */
52 : char *url;
53 :
54 : /**
55 : * Handle for the request.
56 : */
57 : struct GNUNET_CURL_Job *job;
58 :
59 : /**
60 : * Function to call with the result.
61 : */
62 : TALER_MERCHANT_GetPrivateStatisticsAmountCallback cb;
63 :
64 : /**
65 : * Closure for @a cb.
66 : */
67 : TALER_MERCHANT_GET_PRIVATE_STATISTICS_AMOUNT_RESULT_CLOSURE *cb_cls;
68 :
69 : /**
70 : * Reference to the execution context.
71 : */
72 : struct GNUNET_CURL_Context *ctx;
73 :
74 : /**
75 : * Statistics slug.
76 : */
77 : char *slug;
78 :
79 : /**
80 : * Aggregation mode.
81 : */
82 : enum TALER_MERCHANT_StatisticsType stype;
83 :
84 : /**
85 : * Whether stype was explicitly set.
86 : */
87 : bool have_stype;
88 : };
89 :
90 :
91 : /**
92 : * Parse interval and bucket data from the JSON response.
93 : *
94 : * @param json overall JSON reply
95 : * @param jbuckets JSON array with bucket data
96 : * @param buckets_description human-readable description for buckets
97 : * @param jintervals JSON array with interval data
98 : * @param intervals_description human-readable description for intervals
99 : * @param sah operation handle
100 : * @return #GNUNET_OK on success
101 : */
102 : static enum GNUNET_GenericReturnValue
103 2 : parse_intervals_and_buckets_amt (
104 : const json_t *json,
105 : const json_t *jbuckets,
106 : const char *buckets_description,
107 : const json_t *jintervals,
108 : const char *intervals_description,
109 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah)
110 : {
111 2 : unsigned int resp_buckets_len = json_array_size (jbuckets);
112 2 : unsigned int resp_intervals_len = json_array_size (jintervals);
113 2 : size_t total_amounts = 0;
114 2 : size_t amounts_pos = 0;
115 :
116 2 : if ( (json_array_size (jbuckets) != (size_t) resp_buckets_len) ||
117 2 : (json_array_size (jintervals) != (size_t) resp_intervals_len) ||
118 2 : (resp_buckets_len > MAX_STATISTICS) ||
119 : (resp_intervals_len > MAX_STATISTICS) )
120 : {
121 0 : GNUNET_break (0);
122 0 : return GNUNET_SYSERR;
123 : }
124 : {
125 : size_t index;
126 : json_t *value;
127 :
128 2 : json_array_foreach (jintervals, index, value) {
129 : const json_t *amounts_arr;
130 : struct GNUNET_JSON_Specification spec[] = {
131 0 : GNUNET_JSON_spec_array_const ("cumulative_amounts",
132 : &amounts_arr),
133 0 : GNUNET_JSON_spec_end ()
134 : };
135 :
136 0 : if (GNUNET_OK !=
137 0 : GNUNET_JSON_parse (value,
138 : spec,
139 : NULL, NULL))
140 : {
141 0 : GNUNET_break_op (0);
142 0 : return GNUNET_SYSERR;
143 : }
144 0 : total_amounts += json_array_size (amounts_arr);
145 : }
146 :
147 14 : json_array_foreach (jbuckets, index, value) {
148 : const json_t *amounts_arr;
149 : struct GNUNET_JSON_Specification spec[] = {
150 12 : GNUNET_JSON_spec_array_const ("cumulative_amounts",
151 : &amounts_arr),
152 12 : GNUNET_JSON_spec_end ()
153 : };
154 :
155 12 : if (GNUNET_OK !=
156 12 : GNUNET_JSON_parse (value,
157 : spec,
158 : NULL, NULL))
159 : {
160 0 : GNUNET_break_op (0);
161 0 : return GNUNET_SYSERR;
162 : }
163 12 : total_amounts += json_array_size (amounts_arr);
164 : }
165 2 : if (total_amounts > MAX_STATISTICS * (size_t) MAX_STATISTICS)
166 : {
167 : /* Bound the stack VLA declared below by an untrusted response. */
168 0 : GNUNET_break_op (0);
169 0 : return GNUNET_SYSERR;
170 : }
171 : }
172 2 : {
173 2 : struct TALER_Amount *glob_amt_arr
174 2 : = GNUNET_new_array (GNUNET_NZL (total_amounts),
175 : struct TALER_Amount);
176 2 : struct TALER_MERCHANT_GetPrivateStatisticsAmountByBucket resp_buckets[
177 2 : GNUNET_NZL (resp_buckets_len)];
178 2 : struct TALER_MERCHANT_GetPrivateStatisticsAmountByInterval resp_intervals[
179 2 : GNUNET_NZL (resp_intervals_len)];
180 : size_t index;
181 : json_t *value;
182 : enum GNUNET_GenericReturnValue ret;
183 :
184 2 : ret = GNUNET_OK;
185 2 : json_array_foreach (jintervals, index, value) {
186 0 : struct TALER_MERCHANT_GetPrivateStatisticsAmountByInterval *jinterval
187 : = &resp_intervals[index];
188 : const json_t *amounts_arr;
189 : size_t amounts_len;
190 : struct GNUNET_JSON_Specification spec[] = {
191 0 : GNUNET_JSON_spec_timestamp ("start_time",
192 : &jinterval->start_time),
193 0 : GNUNET_JSON_spec_array_const ("cumulative_amounts",
194 : &amounts_arr),
195 0 : GNUNET_JSON_spec_end ()
196 : };
197 :
198 0 : if (GNUNET_OK !=
199 0 : GNUNET_JSON_parse (value,
200 : spec,
201 : NULL, NULL))
202 : {
203 0 : GNUNET_break_op (0);
204 0 : GNUNET_free (glob_amt_arr);
205 0 : return GNUNET_SYSERR;
206 : }
207 0 : amounts_len = json_array_size (amounts_arr);
208 : {
209 0 : struct TALER_Amount *amt_arr = &glob_amt_arr[amounts_pos];
210 : size_t aindex;
211 : json_t *avalue;
212 :
213 0 : amounts_pos += amounts_len;
214 0 : jinterval->cumulative_amount_len = amounts_len;
215 0 : jinterval->cumulative_amounts = amt_arr;
216 0 : json_array_foreach (amounts_arr, aindex, avalue) {
217 0 : if (! json_is_string (avalue))
218 : {
219 0 : GNUNET_break_op (0);
220 0 : GNUNET_free (glob_amt_arr);
221 0 : return GNUNET_SYSERR;
222 : }
223 0 : if (GNUNET_OK !=
224 0 : TALER_string_to_amount (json_string_value (avalue),
225 0 : &amt_arr[aindex]))
226 : {
227 0 : GNUNET_break_op (0);
228 0 : GNUNET_free (glob_amt_arr);
229 0 : return GNUNET_SYSERR;
230 : }
231 : }
232 : }
233 : }
234 14 : json_array_foreach (jbuckets, index, value) {
235 12 : struct TALER_MERCHANT_GetPrivateStatisticsAmountByBucket *jbucket
236 : = &resp_buckets[index];
237 : const json_t *amounts_arr;
238 : size_t amounts_len;
239 : struct GNUNET_JSON_Specification spec[] = {
240 12 : GNUNET_JSON_spec_timestamp ("start_time",
241 : &jbucket->start_time),
242 12 : GNUNET_JSON_spec_timestamp ("end_time",
243 : &jbucket->end_time),
244 12 : GNUNET_JSON_spec_string ("range",
245 : &jbucket->range),
246 12 : GNUNET_JSON_spec_array_const ("cumulative_amounts",
247 : &amounts_arr),
248 12 : GNUNET_JSON_spec_end ()
249 : };
250 :
251 12 : if (GNUNET_OK !=
252 12 : GNUNET_JSON_parse (value,
253 : spec,
254 : NULL, NULL))
255 : {
256 0 : GNUNET_break_op (0);
257 0 : ret = GNUNET_SYSERR;
258 0 : break;
259 : }
260 12 : amounts_len = json_array_size (amounts_arr);
261 : {
262 12 : struct TALER_Amount *amt_arr = &glob_amt_arr[amounts_pos];
263 : size_t aindex;
264 : json_t *avalue;
265 :
266 12 : amounts_pos += amounts_len;
267 12 : jbucket->cumulative_amount_len = amounts_len;
268 12 : jbucket->cumulative_amounts = amt_arr;
269 24 : json_array_foreach (amounts_arr, aindex, avalue) {
270 12 : if (! json_is_string (avalue))
271 : {
272 0 : GNUNET_break_op (0);
273 0 : GNUNET_free (glob_amt_arr);
274 0 : return GNUNET_SYSERR;
275 : }
276 12 : if (GNUNET_OK !=
277 12 : TALER_string_to_amount (json_string_value (avalue),
278 12 : &amt_arr[aindex]))
279 : {
280 0 : GNUNET_break_op (0);
281 0 : GNUNET_free (glob_amt_arr);
282 0 : return GNUNET_SYSERR;
283 : }
284 : }
285 : }
286 : }
287 2 : if (GNUNET_OK == ret)
288 : {
289 2 : struct TALER_MERCHANT_GetPrivateStatisticsAmountResponse sagr = {
290 : .hr.http_status = MHD_HTTP_OK,
291 : .hr.reply = json,
292 : .details.ok.buckets_length = resp_buckets_len,
293 : .details.ok.buckets = resp_buckets,
294 : .details.ok.buckets_description = buckets_description,
295 : .details.ok.intervals_length = resp_intervals_len,
296 : .details.ok.intervals = resp_intervals,
297 : .details.ok.intervals_description = intervals_description,
298 : };
299 :
300 2 : sah->cb (sah->cb_cls,
301 : &sagr);
302 2 : sah->cb = NULL;
303 : }
304 2 : GNUNET_free (glob_amt_arr);
305 2 : return ret;
306 : }
307 : }
308 :
309 :
310 : /**
311 : * Function called when we're done processing the
312 : * HTTP GET /private/statistics-amount/$SLUG request.
313 : *
314 : * @param cls the `struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle`
315 : * @param response_code HTTP response code, 0 on error
316 : * @param response response body, NULL if not in JSON
317 : */
318 : static void
319 2 : handle_get_statistics_amount_finished (void *cls,
320 : long response_code,
321 : const void *response)
322 : {
323 2 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah = cls;
324 2 : const json_t *json = response;
325 2 : struct TALER_MERCHANT_GetPrivateStatisticsAmountResponse sagr = {
326 2 : .hr.http_status = (unsigned int) response_code,
327 : .hr.reply = json
328 : };
329 :
330 2 : sah->job = NULL;
331 2 : GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
332 : "Got /private/statistics-amount/$SLUG response with status code %u\n",
333 : (unsigned int) response_code);
334 2 : switch (response_code)
335 : {
336 2 : case MHD_HTTP_OK:
337 : {
338 : const json_t *buckets;
339 : const json_t *intervals;
340 2 : const char *buckets_description = NULL;
341 2 : const char *intervals_description = NULL;
342 : struct GNUNET_JSON_Specification spec[] = {
343 2 : GNUNET_JSON_spec_array_const ("buckets",
344 : &buckets),
345 2 : GNUNET_JSON_spec_mark_optional (
346 : GNUNET_JSON_spec_string ("buckets_description",
347 : &buckets_description),
348 : NULL),
349 2 : GNUNET_JSON_spec_array_const ("intervals",
350 : &intervals),
351 2 : GNUNET_JSON_spec_mark_optional (
352 : GNUNET_JSON_spec_string ("intervals_description",
353 : &intervals_description),
354 : NULL),
355 2 : GNUNET_JSON_spec_end ()
356 : };
357 :
358 2 : if (GNUNET_OK !=
359 2 : GNUNET_JSON_parse (json,
360 : spec,
361 : NULL, NULL))
362 : {
363 0 : sagr.hr.http_status = 0;
364 0 : sagr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
365 0 : break;
366 : }
367 2 : if (GNUNET_OK ==
368 2 : parse_intervals_and_buckets_amt (json,
369 : buckets,
370 : buckets_description,
371 : intervals,
372 : intervals_description,
373 : sah))
374 : {
375 2 : TALER_MERCHANT_get_private_statistics_amount_cancel (sah);
376 2 : return;
377 : }
378 0 : sagr.hr.http_status = 0;
379 0 : sagr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
380 0 : break;
381 : }
382 0 : case MHD_HTTP_UNAUTHORIZED:
383 0 : sagr.hr.ec = TALER_JSON_get_error_code (json);
384 0 : sagr.hr.hint = TALER_JSON_get_error_hint (json);
385 0 : break;
386 0 : case MHD_HTTP_NOT_FOUND:
387 0 : sagr.hr.ec = TALER_JSON_get_error_code (json);
388 0 : sagr.hr.hint = TALER_JSON_get_error_hint (json);
389 0 : break;
390 0 : default:
391 0 : sagr.hr.ec = TALER_JSON_get_error_code (json);
392 0 : sagr.hr.hint = TALER_JSON_get_error_hint (json);
393 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
394 : "Unexpected response code %u/%d\n",
395 : (unsigned int) response_code,
396 : (int) sagr.hr.ec);
397 0 : break;
398 : }
399 0 : sah->cb (sah->cb_cls,
400 : &sagr);
401 0 : TALER_MERCHANT_get_private_statistics_amount_cancel (sah);
402 : }
403 :
404 :
405 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *
406 2 : TALER_MERCHANT_get_private_statistics_amount_create (
407 : struct GNUNET_CURL_Context *ctx,
408 : const char *url,
409 : const char *slug)
410 : {
411 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah;
412 :
413 2 : sah = GNUNET_new (struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle);
414 2 : sah->ctx = ctx;
415 2 : sah->base_url = GNUNET_strdup (url);
416 2 : sah->slug = GNUNET_strdup (slug);
417 2 : sah->stype = TALER_MERCHANT_STATISTICS_ALL;
418 2 : return sah;
419 : }
420 :
421 :
422 : enum GNUNET_GenericReturnValue
423 2 : TALER_MERCHANT_get_private_statistics_amount_set_options_ (
424 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah,
425 : unsigned int num_options,
426 : const struct TALER_MERCHANT_GetPrivateStatisticsAmountOptionValue *options)
427 : {
428 4 : for (unsigned int i = 0; i < num_options; i++)
429 : {
430 4 : const struct TALER_MERCHANT_GetPrivateStatisticsAmountOptionValue *opt =
431 4 : &options[i];
432 :
433 4 : switch (opt->option)
434 : {
435 2 : case TALER_MERCHANT_GET_PRIVATE_STATISTICS_AMOUNT_OPTION_END:
436 2 : return GNUNET_OK;
437 2 : case TALER_MERCHANT_GET_PRIVATE_STATISTICS_AMOUNT_OPTION_TYPE:
438 2 : sah->stype = opt->details.type;
439 2 : sah->have_stype = true;
440 2 : break;
441 0 : default:
442 0 : GNUNET_break (0);
443 0 : return GNUNET_NO;
444 : }
445 : }
446 0 : return GNUNET_OK;
447 : }
448 :
449 :
450 : enum TALER_ErrorCode
451 2 : TALER_MERCHANT_get_private_statistics_amount_start (
452 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah,
453 : TALER_MERCHANT_GetPrivateStatisticsAmountCallback cb,
454 : TALER_MERCHANT_GET_PRIVATE_STATISTICS_AMOUNT_RESULT_CLOSURE *cb_cls)
455 : {
456 : CURL *eh;
457 :
458 2 : sah->cb = cb;
459 2 : sah->cb_cls = cb_cls;
460 : {
461 2 : const char *filter = NULL;
462 : char *path;
463 :
464 2 : switch (sah->stype)
465 : {
466 0 : case TALER_MERCHANT_STATISTICS_BY_BUCKET:
467 0 : filter = "bucket";
468 0 : break;
469 0 : case TALER_MERCHANT_STATISTICS_BY_INTERVAL:
470 0 : filter = "interval";
471 0 : break;
472 2 : case TALER_MERCHANT_STATISTICS_ALL:
473 2 : filter = NULL;
474 2 : break;
475 : }
476 2 : GNUNET_asprintf (&path,
477 : "private/statistics-amount/%s",
478 : sah->slug);
479 2 : sah->url = TALER_url_join (sah->base_url,
480 : path,
481 : "by",
482 : filter,
483 : NULL);
484 2 : GNUNET_free (path);
485 : }
486 2 : if (NULL == sah->url)
487 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
488 2 : eh = TALER_MERCHANT_curl_easy_get_ (sah->url);
489 2 : if (NULL == eh)
490 0 : return TALER_EC_GENERIC_CONFIGURATION_INVALID;
491 2 : sah->job = GNUNET_CURL_job_add (sah->ctx,
492 : eh,
493 : &handle_get_statistics_amount_finished,
494 : sah);
495 2 : if (NULL == sah->job)
496 0 : return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
497 2 : return TALER_EC_NONE;
498 : }
499 :
500 :
501 : void
502 2 : TALER_MERCHANT_get_private_statistics_amount_cancel (
503 : struct TALER_MERCHANT_GetPrivateStatisticsAmountHandle *sah)
504 : {
505 2 : if (NULL != sah->job)
506 : {
507 0 : GNUNET_CURL_job_cancel (sah->job);
508 0 : sah->job = NULL;
509 : }
510 2 : GNUNET_free (sah->url);
511 2 : GNUNET_free (sah->base_url);
512 2 : GNUNET_free (sah->slug);
513 2 : GNUNET_free (sah);
514 2 : }
515 :
516 :
517 : /* end of merchant_api_get-private-statistics-amount-SLUG-new.c */
|