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 (®ex, 122 pattern, 123 REG_EXTENDED | REG_NOSUB)); 124 is_valid = (0 == 125 regexec (®ex, 126 email, 127 0, 128 NULL, 129 0)); 130 regfree (®ex); 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 (®ex, 219 pattern, 220 REG_EXTENDED | REG_NOSUB | REG_ICASE)); 221 ret = regexec (®ex, 222 phone, 0, 223 NULL, 0); 224 regfree (®ex); 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 }