merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

validators.c (8547B)


      1 /*
      2   This file is part of TALER
      3   (C) 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 Lesser 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 General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file src/util/validators.c
     18  * @brief Input validators
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include <gnunet/gnunet_util_lib.h>
     23 #include <gnunet/gnunet_db_lib.h>
     24 #include <taler/taler_json_lib.h>
     25 #include "taler/taler_merchant_util.h"
     26 #include <regex.h>
     27 
     28 bool
     29 TALER_MERCHANT_image_data_url_valid (const char *image_data_url)
     30 {
     31   if (0 == strcmp (image_data_url,
     32                    ""))
     33     return true;
     34   if (0 != strncasecmp ("data:image/",
     35                         image_data_url,
     36                         strlen ("data:image/")))
     37   {
     38     GNUNET_break_op (0);
     39     return false;
     40   }
     41   if (NULL == strstr (image_data_url,
     42                       ";base64,"))
     43   {
     44     GNUNET_break_op (0);
     45     return false;
     46   }
     47   if (! TALER_url_valid_charset (image_data_url))
     48   {
     49     GNUNET_break_op (0);
     50     return false;
     51   }
     52   return true;
     53 }
     54 
     55 
     56 bool
     57 TALER_MERCHANT_email_valid (const char *email)
     58 {
     59   regex_t regex;
     60   bool is_valid;
     61 
     62   /*
     63    * Email regex pattern supporting:
     64    *
     65    * Local part (before @):
     66    * - Dot-atom: alphanumeric, dots, hyphens, underscores
     67    *   (no leading/trailing dots, no consecutive dots)
     68    * - Quoted-string: quoted text with escaped chars inside
     69    *
     70    * Domain part (after @):
     71    * - Domain labels: alphanumeric and hyphens
     72    *   (no leading/trailing hyphens per label)
     73    * - IP literals: [IPv4] or [IPv6:...]
     74    *
     75    * Pattern breakdown:
     76    * Local part:
     77    *   ([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+
     78    *    (\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*)
     79    *   = dot-atom (atext chars, dots allowed between parts)
     80    *
     81    *   |"([^"\\]|\\.)*"
     82    *   = quoted-string (anything in quotes with escaping)
     83    *
     84    * Domain part:
     85    *   ([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
     86    *    (\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)
     87    *   = domain labels (63 chars max, hyphens in middle)
     88    *
     89    *   |\[([0-9]{1,3}\.){3}[0-9]{1,3}\]
     90    *   = IPv4 literal
     91    *
     92    *   |\[IPv6:[0-9a-fA-F:]+\]
     93    *   = IPv6 literal
     94    */
     95   const char *pattern =
     96     "^("
     97     /* Local part: dot-atom-text or quoted-string */
     98     "([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\\.)?)*[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+"
     99     "|"
    100     "\"([^\"\\\\]|\\\\.)*\""
    101     ")"
    102     "@"
    103     "("
    104     /* Domain: domain labels (with at least one dot) or IP literal */
    105     "([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)"
    106     "|"
    107     "\\[((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}"
    108     "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\]"
    109     "|"
    110     "\\[IPv6:[0-9a-fA-F:]*[0-9a-fA-F]\\]"
    111     ")$";
    112 
    113   if ('\0' == email[0])
    114     return false;
    115 
    116   /* Maximum email length per RFC 5321 */
    117   if (strlen (email) > 254)
    118     return false;
    119 
    120   GNUNET_assert (0 ==
    121                  regcomp (&regex,
    122                           pattern,
    123                           REG_EXTENDED | REG_NOSUB));
    124   is_valid = (0 ==
    125               regexec (&regex,
    126                        email,
    127                        0,
    128                        NULL,
    129                        0));
    130   regfree (&regex);
    131   return is_valid;
    132 }
    133 
    134 
    135 char *
    136 TALER_MERCHANT_phone_validate_normalize (const char *phone,
    137                                          bool allow_letters)
    138 {
    139   if ('\0' == phone[0])
    140     return NULL;
    141 
    142   /* Maximum phone length (reasonable practical limit) */
    143   if (strlen (phone) > 30)
    144     return NULL;
    145 
    146   {
    147     regex_t regex;
    148     int ret;
    149 
    150     /*
    151      * Phone number regex pattern with +CC prefix requirement:
    152      *
    153      * Supports:
    154      * - Country codes (1-3 digits after +)
    155      * - Variable length national numbers
    156      * - Spaces, hyphens, and dots as separators
    157      * - Parentheses for area codes
    158      * - Optional letters representing digits (2-9) if allow_letters is true
    159      *
    160      * Examples:
    161      *   +1-202-555-0173
    162      *   +33 1 42 68 53 00
    163      *   +44.20.7946.0958
    164      *   +1 (202) 555-0173
    165      *   +886 2 2345 6789
    166      *   +1-800-CALL-NOW (if allow_letters is true)
    167      *
    168      * Pattern breakdown:
    169      * ^\+[0-9]{1,3}
    170      *   = Plus sign followed by 1-3 digit country code
    171      *
    172      * [-. ]?
    173      *   = Optional separator after country code
    174      *
    175      * (\([0-9]{1,4}\)[-. ]?)?
    176      *   = Optional parenthesized area code with separator
    177      *
    178      * [0-9A-Z]
    179      *   = Start with digit or letter
    180      *
    181      * ([-. ]?[0-9A-Z])*
    182      *   = Digit/letter groups with optional separators
    183      *
    184      * $
    185      *   = End of string
    186      */
    187     const char *pattern_digits =
    188       "^\\+[0-9]{1,3}"                     /* Plus and country code (1-3 digits) */
    189       "[-. ]?"                             /* Optional single separator */
    190       "("                                  /* Optional area code group */
    191       "\\([0-9]{1,4}\\)"                   /* Area code in parens */
    192       "[-. ]?"                             /* Optional separator after parens */
    193       ")?"
    194       "[0-9]"                              /* Start national number with digit */
    195       "("                                  /* National number: alternating digits and separators */
    196       "[-. ]?[0-9]"                        /* Separator optionally followed by digit */
    197       ")*"
    198       "$";
    199 
    200     const char *pattern_with_letters =
    201       "^\\+[0-9]{1,3}"                     /* Plus and country code (1-3 digits) */
    202       "[-. ]?"                             /* Optional single separator */
    203       "("                                  /* Optional area code group */
    204       "\\([0-9]{1,4}\\)"                   /* Area code in parens */
    205       "[-. ]?"                             /* Optional separator after parens */
    206       ")?"
    207       "[0-9A-Z]"                           /* Start national number with digit or letter */
    208       "("                                  /* National number: alternating digits/letters and separators */
    209       "[-. ]?[0-9A-Z]"                     /* Separator optionally followed by digit or letter */
    210       ")*"
    211       "$";
    212 
    213     const char *pattern = allow_letters
    214       ? pattern_with_letters
    215       : pattern_digits;
    216 
    217     GNUNET_assert (0 ==
    218                    regcomp (&regex,
    219                             pattern,
    220                             REG_EXTENDED | REG_NOSUB | REG_ICASE));
    221     ret = regexec (&regex,
    222                    phone, 0,
    223                    NULL, 0);
    224     regfree (&regex);
    225     if (0 != ret)
    226       return NULL; /* invalid number */
    227   }
    228 
    229   /* Phone is valid - normalize it */
    230   {
    231     char *normalized;
    232     char *out;
    233 
    234     normalized = GNUNET_malloc (strlen (phone) + 1);
    235     out = normalized;
    236     *out++ = '+';  /* Start with plus sign */
    237 
    238     for (const char *in = phone;
    239          '\0' != *in;
    240          in++)
    241     {
    242       if (isdigit ((unsigned char) *in))
    243       {
    244         /* Copy digit as-is */
    245         *out++ = *in;
    246       }
    247       else if (allow_letters && isalpha ((unsigned char) *in))
    248       {
    249         /* Convert letter to corresponding digit (A-Z maps to 2-9) */
    250         char upper = toupper ((unsigned char) *in);
    251         /* T9 keypad mapping:
    252          * 2: ABC
    253          * 3: DEF
    254          * 4: GHI
    255          * 5: JKL
    256          * 6: MNO
    257          * 7: PQRS
    258          * 8: TUV
    259          * 9: WXYZ
    260          */
    261         char digit;
    262 
    263         if (upper >= 'A' && upper <= 'C')
    264           digit = '2';
    265         else if (upper >= 'D' && upper <= 'F')
    266           digit = '3';
    267         else if (upper >= 'G' && upper <= 'I')
    268           digit = '4';
    269         else if (upper >= 'J' && upper <= 'L')
    270           digit = '5';
    271         else if (upper >= 'M' && upper <= 'O')
    272           digit = '6';
    273         else if (upper >= 'P' && upper <= 'S')
    274           digit = '7';
    275         else if (upper >= 'T' && upper <= 'V')
    276           digit = '8';
    277         else if (upper >= 'W' && upper <= 'Z')
    278           digit = '9';
    279         else
    280           digit = '0';  /* Fallback (shouldn't happen) */
    281         *out++ = digit;
    282       }
    283       /* Skip separators, parentheses, and spaces */
    284     }
    285     *out = '\0'; /* redundant, but helps analyzers... */
    286     return normalized;
    287   }
    288 }