New tool to commit SEPA preorders.
authorWerner Koch <wk@gnupg.org>
Wed, 14 Oct 2015 15:42:34 +0000 (17:42 +0200)
committerWerner Koch <wk@gnupg.org>
Wed, 14 Oct 2015 15:42:34 +0000 (17:42 +0200)
* src/payproc-post.c: New.
* src/util.c (ascii_strupr): New.
(keyvalue_put_idx): New.
(keyvalue_put_meta): New.
* src/t-util.c: New.

* src/preorder.c (open_preorder_db): More prepared statements.
(get_text_column, (get_columns): New.
(get_preorder_record):  New.
(update_preorder_record): New.
(preorder_get_record): New.
(preorder_update_record): New.
* src/journal.c (jrnl_store_charge_record): Write Sepa-Ref.
* src/commands.c (cmd_commitpreorder):
(cmd_getpreorder):
(cmd_ppipnhd): New with code factored out from connection_handler.
(cmd_help): New.
(cmdtbl): New.
(connection_handler): Use a table to dispatch commands.
* src/protocol-io.c (read_data): Ignore comment lines.

13 files changed:
.gitignore
src/Makefile.am
src/commands.c
src/journal.c
src/journal.h
src/paypal.c
src/payproc-post.c [new file with mode: 0644]
src/preorder.c
src/preorder.h
src/protocol-io.c
src/t-util.c [new file with mode: 0644]
src/util.c
src/util.h

index 5a4bf70..ec4475f 100644 (file)
@@ -39,3 +39,5 @@
 /src/ppipnhd
 /src/payproc-stat
 /src/t-preorder
+/src/payproc-post
+/src/t-util
index b27bdc0..188630c 100644 (file)
@@ -18,7 +18,7 @@
 
 EXTRA_DIST = cJSON.readme tls-ca.pem
 
-bin_PROGRAMS = payprocd payproc-jrnl payproc-stat ppipnhd
+bin_PROGRAMS = payprocd payproc-jrnl payproc-stat payproc-post ppipnhd
 noinst_PROGRAMS = $(module_tests) t-http
 noinst_LIBRARIES = libcommon.a libcommonpth.a
 dist_pkglibexec_SCRIPTS = geteuroxref
@@ -75,11 +75,15 @@ payproc_stat_SOURCES = \
         payproc-stat.c \
        $(common_headers)
 
+payproc_post_SOURCES = \
+        payproc-post.c \
+       $(common_headers)
+
 ppipnhd_SOURCES = ppipnhd.c
 ppipnhd_CFLAGS =
 ppipnhd_LDADD =
 
-module_tests = t-commands t-preorder
+module_tests = t-util t-commands t-preorder
 
 AM_CFLAGS = $(GPG_ERROR_CFLAGS)
 LDADD  = -lm libcommon.a $(GPG_ERROR_LIBS)
@@ -94,6 +98,11 @@ t_http_SOURCES = t-http.c $(t_common_sources)
 t_http_CFLAGS  = $(t_common_cflags)
 t_http_LDADD   = $(t_common_ldadd)
 
+# (util.c is part of t_common_sources)
+t_util_SOURCES = t-util.c $(t_common_sources)
+t_util_CFLAGS  = $(t_common_cflags) $(LIBGCRYPT_CFLAGS)
+t_util_LDADD   = $(t_common_ldadd) $(LIBGCRYPT_LIBS)
+
 t_commands_SOURCES = t-commands.c stripe.c paypal.c paypal-ipn.c \
                     preorder.c \
                      journal.c session.c currency.c $(t_common_sources)
index 0d25575..0c086a1 100644 (file)
@@ -489,7 +489,7 @@ cmd_chargecard (conn_t conn, char *args)
   err = keyvalue_put (&conn->dataitems, "Amount", buf);
   if (err)
     goto leave;
-  jrnl_store_charge_record (&conn->dataitems, 1);
+  jrnl_store_charge_record (&conn->dataitems, PAYMENT_SERVICE_STRIPE);
 
  leave:
   if (err)
@@ -621,7 +621,7 @@ cmd_ppcheckout (conn_t conn, char *args)
       if (err)
         goto leave;
       dict = conn->dataitems;
-      jrnl_store_charge_record (&conn->dataitems, 2);
+      jrnl_store_charge_record (&conn->dataitems, PAYMENT_SERVICE_PAYPAL);
       dict = conn->dataitems;
     }
   else
@@ -684,10 +684,9 @@ cmd_ppcheckout (conn_t conn, char *args)
 
    On success these items are returned:
 
-   SEPA-Ref:   A string to be returned to the caller.
+   Sepa-Ref:   A string to be returned to the caller.
    Amount:     The reformatted amount
    Currency:   The value "EUR"
-   _timestamp: The timestamp as written to the preorder db.
 
  */
 static gpg_error_t
@@ -709,6 +708,7 @@ cmd_sepapreorder (conn_t conn, char *args)
       err = keyvalue_put (&conn->dataitems, "Currency", "EUR");
       if (err)
         goto leave;
+      dict = conn->dataitems;
     }
   else if (strcasecmp (s, "EUR"))
     {
@@ -763,6 +763,151 @@ cmd_sepapreorder (conn_t conn, char *args)
 }
 
 
+/* The COMMITPREORDER command updates a preorder record and logs the data.
+
+   Sepa-Ref:   The key referencing the preorder
+   Amount:     The actual amount of the payment.
+   Currency:   If given its value must be EUR.
+
+   On success these items are returned:
+
+   Sepa-Ref:   The Sepa-Ref string
+   XXX:        FIXME:
+
+ */
+static gpg_error_t
+cmd_commitpreorder (conn_t conn, char *args)
+{
+  gpg_error_t err;
+  keyvalue_t dict = conn->dataitems;
+  unsigned int cents;
+  keyvalue_t kv;
+  const char *s;
+  char *buf = NULL;
+
+  (void)args;
+
+  s = keyvalue_get_string (dict, "Sepa-Ref");
+  if (!*s)
+    {
+      set_error (MISSING_VALUE, "Key 'Sepa-Ref' not given");
+      goto leave;
+    }
+
+  /* Get currency and amount.  */
+  s = keyvalue_get (dict, "Currency");
+  if (!s)
+    {
+      err = keyvalue_put (&conn->dataitems, "Currency", "EUR");
+      if (err)
+        goto leave;
+      dict = conn->dataitems;
+    }
+  else if (strcasecmp (s, "EUR"))
+    {
+      set_error (INV_VALUE, "Currency must be \"EUR\" if given");
+      goto leave;
+    }
+
+  s = keyvalue_get_string (dict, "Amount");
+  if (!*s || !(cents = convert_amount (s, 2)))
+    {
+      set_error (MISSING_VALUE, "Amount missing or invalid");
+      goto leave;
+    }
+  err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
+  dict = conn->dataitems;
+  if (err)
+    goto leave;
+  buf = reconvert_amount (keyvalue_get_int (conn->dataitems, "_amount"), 2);
+  if (!buf)
+    {
+      err = gpg_error_from_syserror ();
+      conn->errdesc = "error converting _amount";
+      goto leave;
+    }
+  err = keyvalue_put (&conn->dataitems, "Amount", buf);
+  if (err)
+    goto leave;
+
+  err = preorder_update_record (conn->dataitems);
+
+ leave:
+  if (err)
+    {
+      es_fprintf (conn->stream, "ERR %d (%s)\n", err,
+                  conn->errdesc? conn->errdesc : gpg_strerror (err));
+      write_data_line (keyvalue_find (conn->dataitems, "failure"),
+                       conn->stream);
+      write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
+                       conn->stream);
+    }
+  else
+    {
+      es_fprintf (conn->stream, "OK\n");
+      for (kv = conn->dataitems; kv; kv = kv->next)
+        if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
+          write_data_line (kv, conn->stream);
+    }
+
+  es_free (buf);
+  return err;
+}
+
+
+/* The GETPREORDER command retrieves a record from the preorder table.
+
+   Sepa-Ref:   The key to lookup the rceord.
+
+   On success these items are returned:
+
+   Sepa-Ref:   The Sepa-Ref string
+   XXX:        FIXME:
+
+ */
+static gpg_error_t
+cmd_getpreorder (conn_t conn, char *args)
+{
+  gpg_error_t err;
+  keyvalue_t dict = conn->dataitems;
+  keyvalue_t kv;
+  const char *s;
+  char *buf = NULL;
+
+  (void)args;
+
+  s = keyvalue_get_string (dict, "Sepa-Ref");
+  if (!*s)
+    {
+      set_error (MISSING_VALUE, "Key 'Sepa-Ref' not given");
+      goto leave;
+    }
+
+  err = preorder_get_record (&conn->dataitems);
+
+ leave:
+  if (err)
+    {
+      es_fprintf (conn->stream, "ERR %d (%s)\n", err,
+                  conn->errdesc? conn->errdesc : gpg_strerror (err));
+      write_data_line (keyvalue_find (conn->dataitems, "failure"),
+                       conn->stream);
+      write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
+                       conn->stream);
+    }
+  else
+    {
+      es_fprintf (conn->stream, "OK\n");
+      for (kv = conn->dataitems; kv; kv = kv->next)
+        if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
+          write_data_line (kv, conn->stream);
+    }
+
+  es_free (buf);
+  return err;
+}
+
+
 \f
 /* The CHECKAMOUNT command checks whether a given amount is within the
    configured limits for payment.  It may eventually provide
@@ -842,6 +987,23 @@ cmd_checkamount (conn_t conn, char *args)
 
 
 \f
+/* PPIPNHD is a handler for PayPal notifications.
+
+   Note: This is an asynchronous call: We send okay, *close* the
+   socket, and only then process the IPN.  */
+static gpg_error_t
+cmd_ppipnhd (conn_t conn, char *args)
+{
+  (void)args;
+
+  es_fputs ("OK\n\n", conn->stream);
+  shutdown_connection_obj (conn);
+  paypal_proc_ipn (&conn->dataitems);
+  return 0;
+}
+
+
+\f
 /* GETINFO is a multipurpose command to return certain config data. It
    requires a subcommand.  See the online help for a list of
    subcommands.
@@ -905,12 +1067,54 @@ cmd_ping (conn_t conn, char *args)
 
 
 \f
+static gpg_error_t cmd_help (conn_t conn, char *args);
+
+/* The table with all commands. */
+static struct
+{
+  const char *name;
+  gpg_error_t (*handler)(conn_t conn, char *args);
+} cmdtbl[] =
+  {
+    { "SESSION",        cmd_session },
+    { "CARDTOKEN",      cmd_cardtoken },
+    { "CHARGECARD",     cmd_chargecard },
+    { "PPCHECKOUT",     cmd_ppcheckout },
+    { "SEPAPREORDER",   cmd_sepapreorder },
+    { "CHECKAMOUNT",    cmd_checkamount },
+    { "PPIPNHD",        cmd_ppipnhd },
+    { "GETINFO",        cmd_getinfo },
+    { "PING",           cmd_ping },
+    { "COMMITPREORDER", cmd_commitpreorder },
+    { "GETPREORDER",    cmd_getpreorder },
+    { "HELP",           cmd_help },
+    { NULL, NULL}
+  };
+
+
+/* The HELP command lists all commands.  */
+static gpg_error_t
+cmd_help (conn_t conn, char *args)
+{
+  int cmdidx;
+
+  (void)args;
+
+  es_fputs ("OK\n", conn->stream);
+  for (cmdidx=0; cmdtbl[cmdidx].name; cmdidx++)
+    es_fprintf (conn->stream, "# %s\n", cmdtbl[cmdidx].name);
+
+  return 0;
+}
+
+
 /* The handler serving a connection. */
 void
 connection_handler (conn_t conn)
 {
   gpg_error_t err;
   keyvalue_t kv;
+  int cmdidx;
   char *cmdargs;
 
   conn->stream = es_fdopen_nc (conn->fd, "r+,samethread");
@@ -931,30 +1135,14 @@ connection_handler (conn_t conn)
     }
   es_fflush (conn->stream);
 
-  if ((cmdargs = has_leading_keyword (conn->command, "SESSION")))
-    err = cmd_session (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "CARDTOKEN")))
-    err = cmd_cardtoken (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "CHARGECARD")))
-    err = cmd_chargecard (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "PPCHECKOUT")))
-    err = cmd_ppcheckout (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "SEPAPREORDER")))
-    err = cmd_sepapreorder (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "CHECKAMOUNT")))
-    err = cmd_checkamount (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "PPIPNHD")))
-    {
-      /* This is an asynchronous call.  Thus send okay, close the
-         socket, and only then process the IPN.  */
-      es_fputs ("OK\n\n", conn->stream);
-      shutdown_connection_obj (conn);
-      paypal_proc_ipn (&conn->dataitems);
-    }
-  else if ((cmdargs = has_leading_keyword (conn->command, "GETINFO")))
-    err = cmd_getinfo (conn, cmdargs);
-  else if ((cmdargs = has_leading_keyword (conn->command, "PING")))
-    err = cmd_ping (conn, cmdargs);
+  cmdargs = NULL;
+  for (cmdidx=0; cmdtbl[cmdidx].name; cmdidx++)
+    if ((cmdargs = has_leading_keyword (conn->command, cmdtbl[cmdidx].name)))
+      break;
+  if (cmdargs)
+    {
+      err = cmdtbl[cmdidx].handler (conn, cmdargs);
+    }
   else
     {
       es_fprintf (conn->stream, "ERR 1 (Unknown command)\n");
index bdeb080..20aa7d7 100644 (file)
    |  8 | meta     | Structured field with additional data          |
    |  9 | last4    | The last 4 digits of the card                  |
    | 10 | service  | Payment service (0=n/a, 1=stripe.com,2=PayPal, |
-   |    |          | 3=SEPA, 255=user)                              |
+   |    |          | 3=SEPA, 255=user (PAYMENT_SERVICE_xxx))        |
    | 11 | account  | Account number                                 |
    | 12 | chargeid | Charge id                                      |
    | 13 | txid     | Transaction id                                 |
    | 14 | rtxid    | Reference txid (e.g. for refunds)              |
+   |    |          | For preorders, this is the Sepa-Ref            |
    | 15 | euro     | amount converted to Euro                       |
    |----+----------+------------------------------------------------|
 
@@ -274,6 +275,8 @@ jrnl_store_charge_record (keyvalue_t *dictp, int service)
   es_putc (':', fp);
   write_escaped (keyvalue_get_string (dict, "balance-transaction"), fp);
   es_putc (':', fp);
+  if (service == PAYMENT_SERVICE_SEPA)
+    write_escaped (keyvalue_get_string (dict, "Sepa-Ref"), fp);
   es_fputs (":", fp);   /* rtxid */
   es_fputs (convert_currency (amountbuf, sizeof amountbuf, curr, amnt), fp);
   es_fputs (":", fp);   /* euro */
index e170ff2..158b02a 100644 (file)
 #ifndef JOURNAL_H
 #define JOURNAL_H
 
+#define PAYMENT_SERVICE_NONE   0
+#define PAYMENT_SERVICE_STRIPE 1
+#define PAYMENT_SERVICE_PAYPAL 2
+#define PAYMENT_SERVICE_SEPA   3
+#define PAYMENT_SERVICE_USER   255
+
+
 void jrnl_set_file (const char *fname);
 void jrnl_store_sys_record (const char *text);
 void jrnl_store_exchange_rate_record (const char *currency, double rate);
index 6b907ee..02576c0 100644 (file)
@@ -370,7 +370,7 @@ backup_field (keyvalue_t *targetp, keyvalue_t dict, const char *name)
 }
 
 
-/* Copy all "_Meta[FOO]" fields from DICT to TARGETP but remove tye
+/* Copy all "_Meta[FOO]" fields from DICT to TARGETP but remove the
    '_' prefix.  */
 static gpg_error_t
 restore_meta (keyvalue_t *targetp, keyvalue_t dict)
diff --git a/src/payproc-post.c b/src/payproc-post.c
new file mode 100644 (file)
index 0000000..d1140c2
--- /dev/null
@@ -0,0 +1,400 @@
+/* payproc-post.c - Do a posting to the payproc database
+ * Copyright (C) 2015 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+
+
+ */
+
+
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <gpg-error.h>
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "util.h"
+#include "logging.h"
+#include "argparse.h"
+#include "protocol-io.h"
+
+
+/* Constants to identify the options. */
+enum opt_values
+  {
+    aNull = 0,
+    oVerbose   = 'v',
+
+    oSeparator  = 500,
+    aSepa,
+    aPing,
+    aSepaPreorder,
+    aGetPreorder,
+
+    oLast
+  };
+
+
+/* The list of commands and options. */
+static ARGPARSE_OPTS opts[] = {
+  ARGPARSE_group (300, "@Commands:\n "),
+  ARGPARSE_c (aSepa, "sepa",  "Post a SEPA transaction (default)"),
+  ARGPARSE_c (aPing, "ping",  "Send a ping"),
+  ARGPARSE_c (aSepaPreorder, "sepa-preorder",  "Insert a SEPA preorder"),
+  ARGPARSE_c (aGetPreorder,  "get-preorder",   "Read one preorder"),
+
+  ARGPARSE_group (301, "@\nOptions:\n "),
+  ARGPARSE_s_n (oVerbose, "verbose",  "verbose diagnostics"),
+
+  ARGPARSE_end ()
+};
+
+
+static struct
+{
+  int verbose;
+
+} opt;
+
+
+\f
+/* Local prototypes.  */
+static gpg_error_t send_request (const char *command,
+                                 keyvalue_t indata, keyvalue_t *outdata);
+static void post_sepa (const char *refstring, const char *amountstr);
+static void getpreorder (const char *refstring);
+static void sepapreorder (const char *amountstr, const char *name,
+                          const char *email, const char *desc);
+
+
+\f
+static const char *
+my_strusage( int level )
+{
+  const char *p;
+
+  switch (level)
+    {
+    case 11: p = "payproc-post"; break;
+    case 13: p = PACKAGE_VERSION; break;
+    case 19: p = "Please report bugs to bugs@g10code.com.\n"; break;
+    case 1:
+    case 40:
+      p = ("Usage: payproc-post [options] [command] [args] (-h for help)");
+      break;
+    case 41:
+      p = ("Syntax: payproc-post [options] [--sepa] REF AMOUNT\n"
+           "        payproc-post [options] --sepa-preorder AMOUNT\n"
+           "Enter a posting to the payproc journal\n");
+      break;
+    default: p = NULL; break;
+    }
+  return p;
+}
+
+
+static void
+wrong_args (const char *text)
+{
+  fprintf (stderr, "usage: %s [options] %s\n", strusage (11), text);
+  exit (2);
+}
+
+
+int
+main (int argc, char **argv)
+{
+  ARGPARSE_ARGS pargs;
+  enum opt_values cmd = 0;
+
+  /* Set program name etc.  */
+  set_strusage (my_strusage);
+  log_set_prefix ("payproc-proc", JNLIB_LOG_WITH_PREFIX);
+
+  /* Make sure that our subsystems are ready.  */
+  gpgrt_init ();
+
+  /* Parse the command line. */
+  pargs.argc  = &argc;
+  pargs.argv  = &argv;
+  pargs.flags = ARGPARSE_FLAG_KEEP;
+  while (optfile_parse (NULL, NULL, NULL, &pargs, opts))
+    {
+      switch (pargs.r_opt)
+        {
+        case aSepa:
+        case aPing:
+        case aSepaPreorder:
+        case aGetPreorder:
+          if (cmd && cmd != pargs.r_opt)
+            {
+              log_error ("conflicting commands\n");
+              exit (2);
+            }
+          cmd = pargs.r_opt;
+          break;
+
+        case oVerbose: opt.verbose++; break;
+
+        default: pargs.err = ARGPARSE_PRINT_ERROR; break;
+       }
+    }
+
+  if (log_get_errorcount (0))
+    exit (2);
+
+  if (!cmd)
+    cmd = aSepa; /* Set default.  */
+
+  if (cmd == aPing)
+    {
+      keyvalue_t dict = NULL;
+
+      send_request ("PING", NULL, &dict);
+      keyvalue_release (dict);
+    }
+  else if (cmd == aSepa)
+    {
+      if (argc != 2)
+        wrong_args ("--sepa REF AMOUNT");
+      ascii_strupr (argv[0]);
+      post_sepa (argv[0], argv[1]);
+    }
+  else if (cmd == aGetPreorder)
+    {
+      if (argc != 1)
+        wrong_args ("--get-preorder REF");
+      ascii_strupr (argv[0]);
+      getpreorder (argv[0]);
+    }
+  else if (cmd == aSepaPreorder)
+    {
+      if (!argc || argc > 4)
+        wrong_args ("--sepa-preorder AMOUNT [NAME [EMAIL [DESC]]]");
+      sepapreorder (argv[0],
+                    argc > 1? argv[1] : "",
+                    argc > 2? argv[2] : "",
+                    argc > 3? argv[3] : "");
+    }
+  else
+    usage (1);
+
+
+  return !!log_get_errorcount (0);
+}
+
+
+/* Connect to the daemon and return an estream for the connected
+   socket.  On error returns NULL and sets ERRNO.  */
+static estream_t
+connect_daemon (const char *name)
+{
+  int sock;
+  struct sockaddr_un addr_un;
+  struct sockaddr    *addrp;
+  size_t addrlen;
+  estream_t fp;
+
+  if (strlen (name)+1 >= sizeof addr_un.sun_path)
+    {
+      gpg_err_set_errno (EINVAL);
+      return NULL;
+    }
+
+  memset (&addr_un, 0, sizeof addr_un);
+  addr_un.sun_family = AF_LOCAL;
+  strncpy (addr_un.sun_path, name, sizeof (addr_un.sun_path) - 1);
+  addr_un.sun_path[sizeof (addr_un.sun_path) - 1] = 0;
+  addrlen = SUN_LEN (&addr_un);
+  addrp = (struct sockaddr *)&addr_un;
+
+  sock = socket (AF_LOCAL, SOCK_STREAM, 0);
+  if (sock == -1)
+    return NULL;
+
+  if (connect (sock, addrp, addrlen))
+    {
+      int saveerr = errno;
+      close (sock);
+      errno = saveerr;
+      return NULL;
+    }
+
+  fp = es_fdopen (sock, "r+b");
+  if (!fp)
+    {
+      int saveerr = errno;
+      close (sock);
+      gpg_err_set_errno (saveerr);
+      return NULL;
+    }
+
+  return fp;
+}
+
+
+/* Send COMMAND and INDATA to the daemon.  On return OUTDATA is updated with the
+   response values.  */
+static gpg_error_t
+send_request (const char *command, keyvalue_t indata, keyvalue_t *outdata)
+{
+  gpg_error_t err;
+  estream_t fp;
+  keyvalue_t kv;
+  const char *s;
+
+  fp = connect_daemon (PAYPROCD_SOCKET_NAME);
+  if (!fp)
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("Error connecting payprocd: %s\n", gpg_strerror (err));
+      return err;
+    }
+
+  es_fprintf (fp, "%s\n", command);
+  for (kv = indata; kv; kv = kv->next)
+    es_fprintf (fp, "%s: %s\n", kv->name, kv->value);
+
+  es_putc ('\n', fp);
+
+  if (es_ferror (fp) || es_fflush (fp))
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("Error writing to payprocd: %s\n", gpg_strerror (err));
+      exit (1);
+    }
+
+  err = protocol_read_response (fp, outdata);
+  if (err && (s=keyvalue_get (*outdata, "_errdesc")))
+    log_error ("Command failed: %s %s%s%s\n",
+               gpg_strerror (err), *s == '('?"":"(", s, *s == '('?"":")");
+  else if (err)
+    log_error ("Error reading from payprocd: %s\n", gpg_strerror (err));
+
+  /* Eat the response for a clean connection shutdown.  */
+  while (es_getc (fp) != EOF)
+    ;
+
+  es_fclose (fp);
+
+  return err;
+}
+
+
+static void
+post_sepa (const char *refstring, const char *amountstr)
+{
+  gpg_error_t err;
+  keyvalue_t input = NULL;
+  keyvalue_t output = NULL;
+  keyvalue_t kv;
+
+  if (!*amountstr || !convert_amount (amountstr, 2))
+    {
+      log_error ("Syntax error in amount or value is not positive\n");
+      return;
+    }
+
+  /* Find reference.  */
+  err = keyvalue_put (&input, "Sepa-Ref", refstring);
+  if (!err)
+    err = keyvalue_put (&input, "Amount", amountstr);
+  if (!err)
+    err = keyvalue_put (&input, "Currency", "EUR");
+  if (err)
+    log_fatal ("keyvalue_put failed: %s\n", gpg_strerror (err));
+
+  if (!send_request ("COMMITPREORDER", input, &output))
+    {
+      for (kv = output; kv; kv = kv->next)
+        es_printf ("%s: %s\n", kv->name, kv->value);
+    }
+
+  keyvalue_release (input);
+  keyvalue_release (output);
+}
+
+
+static void
+getpreorder (const char *refstring)
+{
+  gpg_error_t err;
+  keyvalue_t input = NULL;
+  keyvalue_t output = NULL;
+  keyvalue_t kv;
+
+  /* Find reference.  */
+  err = keyvalue_put (&input, "Sepa-Ref", refstring);
+  if (err)
+    log_fatal ("keyvalue_put failed: %s\n", gpg_strerror (err));
+
+  if (!send_request ("GETPREORDER", input, &output))
+    {
+      for (kv = output; kv; kv = kv->next)
+        es_printf ("%s: %s\n", kv->name, kv->value);
+    }
+
+  keyvalue_release (input);
+  keyvalue_release (output);
+}
+
+
+static void
+sepapreorder (const char *amountstr, const char *name,
+              const char *email, const char *desc)
+{
+  gpg_error_t err;
+  keyvalue_t input = NULL;
+  keyvalue_t output = NULL;
+  keyvalue_t kv;
+
+  if (!*amountstr || !convert_amount (amountstr, 2))
+    {
+      log_error ("Syntax error in amount or value is not positive\n");
+      return;
+    }
+
+  /* Find reference.  */
+  err = keyvalue_put (&input, "Amount", amountstr);
+  if (!err && *name)
+    err = keyvalue_put (&input, "Meta[Name]", name);
+  if (!err && *email)
+    err = keyvalue_put (&input, "Email", email);
+  if (!err && *desc)
+    err = keyvalue_put (&input, "Desc", desc);
+  if (err)
+    log_fatal ("keyvalue_put failed: %s\n", gpg_strerror (err));
+
+  if (!send_request ("SEPAPREORDER", input, &output))
+    {
+      for (kv = output; kv; kv = kv->next)
+        es_printf ("%s: %s\n", kv->name, kv->value);
+    }
+
+  keyvalue_release (input);
+  keyvalue_release (output);
+}
index 362e206..0acb312 100644 (file)
@@ -40,7 +40,9 @@
            AND paid IS NULL;
 
   this has not been implemented here but should be done at startup and
-  once a day.
+  once a day.  Note that 'paid' tracks actual payments using this ref.
+  We do not delete it from the DB so that the ref can be used for
+  recurring payments.
 
  */
 
@@ -78,10 +80,26 @@ static npth_mutex_t preorder_db_lock = NPTH_MUTEX_INITIALIZER;
    protected by preorder_db_lock.  */
 static sqlite3_stmt *preorder_insert_stmt;
 
+/* This is a prepared statement for the UPDATE operation.  It is
+   protected by preorder_db_lock.  */
+static sqlite3_stmt *preorder_update_stmt;
+
+/* This is a prepared statement for the SELECT by REF operation.  It
+   is protected by preorder_db_lock.  */
+static sqlite3_stmt *preorder_select_stmt;
+
+/* This is a prepared statement for the SELECT by REFNN operation.  It
+   is protected by preorder_db_lock.  */
+static sqlite3_stmt *preorder_selectrefnn_stmt;
+
+/* This is a prepared statement for the SELECT all operation.  It
+   is protected by preorder_db_lock.  */
+static sqlite3_stmt *preorder_selectlist_stmt;
+
 
 
 \f
-/* Create a SEPA-Ref field and store it in BUFFER.  The format is:
+/* Create a Sepa-Ref field and store it in BUFFER.  The format is:
 
      AAAAA-NN
 
@@ -167,6 +185,14 @@ close_preorder_db (int do_close)
         {
           sqlite3_finalize (preorder_insert_stmt);
           preorder_insert_stmt = NULL;
+          sqlite3_finalize (preorder_update_stmt);
+          preorder_update_stmt = NULL;
+          sqlite3_finalize (preorder_select_stmt);
+          preorder_select_stmt = NULL;
+          sqlite3_finalize (preorder_selectrefnn_stmt);
+          preorder_selectrefnn_stmt = NULL;
+          sqlite3_finalize (preorder_selectlist_stmt);
+          preorder_selectlist_stmt = NULL;
           res = sqlite3_close (preorder_db);
         }
       if (res)
@@ -265,12 +291,69 @@ open_preorder_db (void)
     }
   preorder_insert_stmt = stmt;
 
+  /* Prepare an update statement.  */
+  res = sqlite3_prepare_v2 (preorder_db,
+                            "UPDATE preorder SET"
+                            " paid = ?2,"
+                            " npaid = npaid + 1"
+                            " WHERE ref=?1",
+                            -1, &stmt, NULL);
+  if (res)
+    {
+      log_error ("error preparing update statement: %s\n",
+                 sqlite3_errstr (res));
+      close_preorder_db (1);
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+  preorder_update_stmt = stmt;
+
+  /* Prepare a select statement.  */
+  res = sqlite3_prepare_v2 (preorder_db,
+                            "SELECT * FROM preorder WHERE ref=?1",
+                            -1, &stmt, NULL);
+  if (res)
+    {
+      log_error ("error preparing select statement: %s\n",
+                 sqlite3_errstr (res));
+      close_preorder_db (1);
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+  preorder_select_stmt = stmt;
+
+  /* Prepare a select-refnn statement.  */
+  res = sqlite3_prepare_v2 (preorder_db,
+                            "SELECT * FROM preorder "
+                            "WHERE refnn=?1 ORDER BY ref",
+                            -1, &stmt, NULL);
+  if (res)
+    {
+      log_error ("error preparing selectrefnn statement: %s\n",
+                 sqlite3_errstr (res));
+      close_preorder_db (1);
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+  preorder_selectrefnn_stmt = stmt;
+
+  /* Prepare a select-list statement.  */
+  res = sqlite3_prepare_v2 (preorder_db,
+                            "SELECT * FROM preorder "
+                            "ORDER BY created DESC, refnn ASC",
+                            -1, &stmt, NULL);
+  if (res)
+    {
+      log_error ("error preparing select statement: %s\n",
+                 sqlite3_errstr (res));
+      close_preorder_db (1);
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+  preorder_selectlist_stmt = stmt;
+
   return 0;
 }
 
 
 /* Insert a record into the preorder table.  The values are taken from
-   the dictionary at DICTP.  On return a SEPA-Ref value will have been
+   the dictionary at DICTP.  On return a Sepa-Ref value will have been
    inserted into it; that may happen even on error.  */
 static gpg_error_t
 insert_preorder_record (keyvalue_t *dictp)
@@ -285,7 +368,7 @@ insert_preorder_record (keyvalue_t *dictp)
 
  retry:
   make_sepa_ref (separef, sizeof separef);
-  err = keyvalue_put (dictp, "SEPA-Ref", separef);
+  err = keyvalue_put (dictp, "Sepa-Ref", separef);
   if (err)
     return err;
   dict = *dictp;
@@ -348,9 +431,175 @@ insert_preorder_record (keyvalue_t *dictp)
 }
 
 
+static gpg_error_t
+get_text_column (sqlite3_stmt *stmt, int idx, int icol, const char *name,
+                 keyvalue_t *dictp)
+{
+  gpg_error_t err;
+  const char *s;
+
+  s = sqlite3_column_text (stmt, icol);
+  if (!s && sqlite3_errcode (preorder_db) == SQLITE_NOMEM)
+    err = gpg_error (GPG_ERR_ENOMEM);
+  else if (!strcmp (name, "Meta"))
+    err = keyvalue_put_meta (dictp, s);
+  else
+    err = keyvalue_put_idx (dictp, name, idx, s);
+
+  return err;
+}
+
+
+/* Put all columns into DICTP.  */
+static gpg_error_t
+get_columns (sqlite3_stmt *stmt, int idx, keyvalue_t *dictp)
+{
+  gpg_error_t err;
+  char separef[9];
+  const char *s;
+  int i;
+
+  s = sqlite3_column_text (stmt, 0);
+  if (!s && sqlite3_errcode (preorder_db) == SQLITE_NOMEM)
+    err = gpg_error (GPG_ERR_ENOMEM);
+  else
+    {
+      strncpy (separef, s, 5);
+      i = sqlite3_column_int (stmt, 1);
+      if (!i && sqlite3_errcode (preorder_db) == SQLITE_NOMEM)
+        err = gpg_error (GPG_ERR_ENOMEM);
+      else if (i < 0 || i > 99)
+        err = gpg_error (GPG_ERR_INV_DATA);
+      else
+        {
+          snprintf (separef+5, 4, "-%02d", i);
+          err = keyvalue_put_idx (dictp, "Sepa-Ref", idx, separef);
+        }
+    }
+
+  if (!err)
+    err = get_text_column (stmt, idx, 2, "Created", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 3, "Paid", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 4, "N-Paid", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 5, "Amount", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 6, "Currency", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 7, "Desc", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 8, "Email", dictp);
+  if (!err)
+    err = get_text_column (stmt, idx, 9, "Meta", dictp);
+
+  return err;
+}
+
+
+/* Get a record from the preorder table.  The values are stored at the
+   dictionary at DICTP.  */
+static gpg_error_t
+get_preorder_record (const char *ref, keyvalue_t *dictp)
+{
+  gpg_error_t err;
+  int res;
+
+  if (strlen (ref) != 5)
+    return gpg_error (GPG_ERR_INV_LENGTH);
+
+  sqlite3_reset (preorder_select_stmt);
+
+  res = sqlite3_bind_text (preorder_select_stmt,
+                           1, ref, 5, SQLITE_TRANSIENT);
+  if (res)
+    {
+      log_error ("error binding a value for the preorder table: %s\n",
+                 sqlite3_errstr (res));
+      return gpg_error (GPG_ERR_GENERAL);
+    }
+
+  res = sqlite3_step (preorder_select_stmt);
+  if (res == SQLITE_ROW)
+    {
+      res = SQLITE_OK;
+      err = get_columns (preorder_select_stmt, -1, dictp);
+    }
+  else if (res == SQLITE_DONE)
+    {
+      res = SQLITE_OK;
+      err = gpg_error (GPG_ERR_NOT_FOUND);
+    }
+  else
+    err = gpg_error (GPG_ERR_GENERAL);
+
+  if (err)
+    {
+      if (res == SQLITE_OK)
+        log_error ("error selecting from preorder table: %s\n",
+                   gpg_strerror (err));
+      else
+        log_error ("error selecting from preorder table: %s [%s (%d)]\n",
+                   gpg_strerror (err), sqlite3_errstr (res), res);
+    }
+  return err;
+}
+
+
+/* Update a row specified by REF in the preorder table.  Also update
+   the timestamp field at DICTP. */
+static gpg_error_t
+update_preorder_record (const char *ref, keyvalue_t *dictp)
+{
+  gpg_error_t err;
+  int res;
+  char datetime_buf [DB_DATETIME_SIZE];
+
+  if (strlen (ref) != 5)
+    return gpg_error (GPG_ERR_INV_LENGTH);
+
+  sqlite3_reset (preorder_update_stmt);
 
+  res = sqlite3_bind_text (preorder_update_stmt,
+                           1, ref, 5,
+                           SQLITE_TRANSIENT);
+  if (!res)
+    res = sqlite3_bind_text (preorder_update_stmt,
+                             2, db_datetime_now (datetime_buf), -1,
+                             SQLITE_TRANSIENT);
+  if (res)
+    {
+      log_error ("error binding a value for the preorder table: %s\n",
+                 sqlite3_errstr (res));
+      return gpg_error (GPG_ERR_GENERAL);
+    }
 
-/* Create a new preorder record and store it.  Inserts a "SEPA-Ref"
+  res = sqlite3_step (preorder_update_stmt);
+  if (res == SQLITE_DONE)
+    {
+      if (!sqlite3_changes (preorder_db))
+        err = gpg_error (GPG_ERR_NOT_FOUND);
+      else
+        err = 0;
+    }
+  else
+    err = gpg_error (GPG_ERR_GENERAL);
+
+  if (!err)
+    err = keyvalue_put (dictp, "_timestamp", datetime_buf);
+
+  if (gpg_err_code (err) == GPG_ERR_GENERAL)
+    log_error ("error updating preorder table: %s [%s (%d)]\n",
+               gpg_strerror (err), sqlite3_errstr (res), res);
+  else
+    log_error ("error updating preorder table: %s\n",
+               gpg_strerror (err));
+  return err;
+}
+
+
+/* Create a new preorder record and store it.  Inserts a "Sepa-Ref"
    into DICT.  */
 gpg_error_t
 preorder_store_record (keyvalue_t *dictp)
@@ -367,3 +616,86 @@ preorder_store_record (keyvalue_t *dictp)
 
   return err;
 }
+
+
+/* Take the Sepa-Ref from DICTP, fetch the row, and update DICTP with
+   that data.  On error return an error code.  Note that DICTP may
+   even be changed on error.  */
+gpg_error_t
+preorder_get_record (keyvalue_t *dictp)
+{
+  gpg_error_t err;
+  char separef[9];
+  const char *s;
+  char *p;
+
+  s = keyvalue_get (*dictp, "Sepa-Ref");
+  if (!s || strlen (s) >= sizeof separef)
+    return gpg_error (GPG_ERR_INV_LENGTH);
+  strcpy (separef, s);
+  p = strchr (separef, '-');
+  if (p)
+    *p = 0;
+
+  err = open_preorder_db ();
+  if (err)
+    return err;
+
+  err = get_preorder_record (separef, dictp);
+
+  close_preorder_db (0);
+
+  return err;
+}
+
+
+/* Take the Sepa-Ref from NEWDATA and update the corresponding row with
+   the other data from NEWDATA.  On error return an error code.  */
+gpg_error_t
+preorder_update_record (keyvalue_t newdata)
+{
+  gpg_error_t err;
+  char separef[9];
+  const char *s;
+  char *p;
+  keyvalue_t olddata = NULL;
+
+  s = keyvalue_get (newdata, "Sepa-Ref");
+  if (!s || strlen (s) >= sizeof separef)
+    return gpg_error (GPG_ERR_INV_LENGTH);
+  strcpy (separef, s);
+  p = strchr (separef, '-');
+  if (p)
+    *p = 0;
+
+  err = open_preorder_db ();
+  if (err)
+    return err;
+
+  err = get_preorder_record (separef, &olddata);
+  if (err)
+    goto leave;
+
+  /* Update OLDDATA with the actual amount so that we can put the
+     correct amount into the log.  */
+  err = keyvalue_put (&olddata, "Amount",
+                      keyvalue_get_string (newdata, "Amount"));
+  if (err)
+    goto leave;
+
+  /* We pass OLDDATA so that _timestamp will be set.  */
+  err = update_preorder_record (separef, &olddata);
+  if (err)
+    goto leave;
+
+  /* FIXME: Unfortunately the journal function creates its own
+     timestamp.  */
+  jrnl_store_charge_record (&olddata, PAYMENT_SERVICE_SEPA);
+
+
+ leave:
+  close_preorder_db (0);
+  keyvalue_release (olddata);
+
+  return err;
+}
index 83398b6..cc4573a 100644 (file)
@@ -21,6 +21,8 @@
 #define PREORDER_H
 
 gpg_error_t preorder_store_record (keyvalue_t *dictp);
+gpg_error_t preorder_update_record (keyvalue_t dict);
+gpg_error_t preorder_get_record (keyvalue_t *dictp);
 
 
 #endif /*PREORDER_H*/
index f5d4c35..e8c9bef 100644 (file)
@@ -216,7 +216,7 @@ read_data (estream_t stream, int filter,
             buffer[--n] = 0;
         }
 
-      if (*buffer)
+      if (*buffer && *buffer != '#' )
         {
           err = store_data_line (buffer, filter, dataitems);
           if (err)
diff --git a/src/t-util.c b/src/t-util.c
new file mode 100644 (file)
index 0000000..f8f065f
--- /dev/null
@@ -0,0 +1,79 @@
+/* t-util.c - Regression test for util.c
+ * Copyright (C) 2015 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+#include <string.h>
+#include <assert.h>
+
+#include "t-common.h"
+
+#include "util.h" /* The module under test.  */
+
+
+static void
+test_keyvalue_put_meta (void)
+{
+  static struct { const char *string; } tests[] = {
+    { "Name=Werner&Email=wk=test@gnupg.org&" },
+    { "Name=" },
+    { "Name=&" },
+    { "Name==" },
+    { "Name=&Foo=%3d%26" },
+    /* Fixme: Add bad case tests.  */
+    { NULL }
+  };
+  gpg_error_t err;
+  keyvalue_t data = NULL;
+  keyvalue_t kv;
+  int idx;
+
+  for (idx=0; tests[idx].string; idx++)
+    {
+      if (verbose)
+        printf ("test %d: '%s'\n", idx, tests[idx].string);
+      err = keyvalue_put_meta (&data, tests[idx].string);
+      if (err)
+        {
+          fprintf (stderr, "test %d ('%s') failed: %s\n",
+                  idx, tests[idx].string, gpg_strerror (err));
+          fail (idx);
+        }
+      else if (verbose)
+        {
+          for (kv = data; kv; kv = kv->next)
+            printf ("  %s: %s\n", kv->name, kv->value);
+        }
+      keyvalue_release (data);
+      data = NULL;
+    }
+}
+
+
+int
+main (int argc, char **argv)
+{
+  if (argc > 1 && !strcmp (argv[1], "--verbose"))
+    verbose = 1;
+
+  test_keyvalue_put_meta ();
+
+  return !!errorcount;
+}
index 83f09e4..b14bdcc 100644 (file)
@@ -149,6 +149,20 @@ strconcat (const char *s1, ...)
 }
 
 
+/* Upcase all ASCII characters in S.  */
+char *
+ascii_strupr (char *s)
+{
+  char *p = s;
+
+  for (p=s; *p; p++ )
+    if (!(*p & 0x80) && *p >= 'a' && *p <= 'z')
+      *p &= ~0x20;
+
+  return s;
+}
+
+
 \f
 /*
  * Check whether STRING starts with KEYWORD.  The keyword is
@@ -317,7 +331,7 @@ keyvalue_append_with_nl (keyvalue_t kv, const char *value)
 
 
 /* Remove all newlines from the value of KV.  This is done in place
-   and and works always.  */
+   and works always.  */
 void
 keyvalue_remove_nl (keyvalue_t kv)
 {
@@ -367,6 +381,37 @@ keyvalue_put (keyvalue_t *list, const char *key, const char *value)
 }
 
 
+/* This is the same as keyvalue_put but KEY is modified to include
+   an index.  For example when using a value of 7 for IDX we get
+
+     "Desc"       -> "Desc[7]"
+     "Meta[Name]" -> "Meta[Name.7]"
+
+   If IDX is -1 the function is identical to keyvalue_put.
+ */
+gpg_error_t
+keyvalue_put_idx (keyvalue_t *list, const char *key, int idx, const char *value)
+{
+  char name[65];
+  size_t n;
+
+  if (idx < 0)
+    return keyvalue_put (list, key, value);
+
+  n = strlen (key);
+  if (n > 2 && key[n-1] == ']')
+    snprintf (name, sizeof name, "%.*s.%d]", (int)n-1, key, idx);
+  else
+    snprintf (name, sizeof name, "%s[%d]", key, idx);
+
+  if (strlen (name) >= sizeof name - 1)
+    return gpg_error (GPG_ERR_INV_LENGTH);
+
+  return keyvalue_put (list, name, value);
+}
+
+
+
 gpg_error_t
 keyvalue_del (keyvalue_t list, const char *key)
 {
@@ -398,6 +443,61 @@ keyvalue_putf (keyvalue_t *list, const char *key, const char *format, ...)
   return err;
 }
 
+/* Store STRING as "Meta" field at LIST.  */
+gpg_error_t
+keyvalue_put_meta (keyvalue_t *list, const char *string)
+{
+  gpg_error_t err;
+  char *buffer;
+  char key[64];
+  char *next, *p;
+  int i;
+
+  buffer = xtrystrdup (string);
+  if (!buffer)
+    return gpg_error_from_syserror ();
+
+  next = buffer;
+  do
+    {
+      strcpy (key, "Meta[");
+      for (i=5, p = next; *p != '='; i++, p++)
+        {
+          if (!*p || strchr ("%:&\n\r", *p) || i >= sizeof key - 3)
+            {
+              xfree (buffer);
+              return gpg_error (GPG_ERR_INV_VALUE);  /* No or invalid name.  */
+            }
+          else
+            key[i] = *p;
+        }
+      if (i==5)
+        {
+          xfree (buffer);
+          return gpg_error (GPG_ERR_INV_VALUE);  /* Zero length name.  */
+        }
+      key[i++] = ']';
+      key[i] = 0;
+      p++;
+
+      next = strchr (p, '&');
+      if (next)
+        *next++ = 0;
+
+      p[percent_unescape_inplace (p, 0)] = 0;
+      err = keyvalue_put (list, key, p);
+      if (err)
+        {
+          xfree (buffer);
+          return err;
+        }
+    }
+  while (next && *next);
+
+  xfree (buffer);
+  return 0;
+}
+
 
 void
 keyvalue_release (keyvalue_t kv)
index 1937015..555ada2 100644 (file)
@@ -117,6 +117,7 @@ char *xstrdup (const char *string);
    malloc error or if too many arguments are given.  */
 char *strconcat (const char *s1, ...) JNLIB_GCC_A_SENTINEL(0);
 
+char *ascii_strupr (char *s);
 
 char *has_leading_keyword (const char *string, const char *keyword);
 const char *memstr (const void *buffer, size_t buflen, const char *sub);
@@ -137,11 +138,14 @@ typedef struct keyvalue_s *keyvalue_t;
 
 gpg_error_t keyvalue_append_with_nl (keyvalue_t kv, const char *value);
 void keyvalue_remove_nl (keyvalue_t kv);
-gpg_error_t keyvalue_put (keyvalue_t *list,
-                               const char *key, const char *value);
+gpg_error_t keyvalue_put (keyvalue_t *list, const char *key,
+                          const char *value);
+gpg_error_t keyvalue_put_idx (keyvalue_t *list, const char *key, int idx,
+                              const char *value);
 gpg_error_t keyvalue_del (keyvalue_t list, const char *key);
 gpg_error_t keyvalue_putf (keyvalue_t *list, const char *key,
                            const char *format, ...) JNLIB_GCC_A_PRINTF (3,4);
+gpg_error_t keyvalue_put_meta (keyvalue_t *list, const char *string);
 void keyvalue_release (keyvalue_t kv);
 keyvalue_t keyvalue_find (keyvalue_t list, const char *key);
 const char *keyvalue_get (keyvalue_t list, const char *key);