Line data Source code
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 119 : TALER_MERCHANT_image_data_url_valid (const char *image_data_url)
30 : {
31 119 : if (0 == strcmp (image_data_url,
32 : ""))
33 65 : return true;
34 54 : if (0 != strncasecmp ("data:image/",
35 : image_data_url,
36 : strlen ("data:image/")))
37 : {
38 0 : GNUNET_break_op (0);
39 0 : return false;
40 : }
41 54 : if (NULL == strstr (image_data_url,
42 : ";base64,"))
43 : {
44 0 : GNUNET_break_op (0);
45 0 : return false;
46 : }
47 54 : if (! TALER_url_valid_charset (image_data_url))
48 : {
49 0 : GNUNET_break_op (0);
50 0 : return false;
51 : }
52 54 : return true;
53 : }
54 :
55 :
56 : bool
57 38 : 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 38 : 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 38 : if ('\0' == email[0])
114 1 : return false;
115 :
116 : /* Maximum email length per RFC 5321 */
117 37 : if (strlen (email) > 254)
118 0 : return false;
119 :
120 37 : GNUNET_assert (0 ==
121 : regcomp (®ex,
122 : pattern,
123 : REG_EXTENDED | REG_NOSUB));
124 37 : is_valid = (0 ==
125 37 : regexec (®ex,
126 : email,
127 : 0,
128 : NULL,
129 : 0));
130 37 : regfree (®ex);
131 37 : return is_valid;
132 : }
133 :
134 :
135 : char *
136 47 : TALER_MERCHANT_phone_validate_normalize (const char *phone,
137 : bool allow_letters)
138 : {
139 47 : if ('\0' == phone[0])
140 1 : return NULL;
141 :
142 : /* Maximum phone length (reasonable practical limit) */
143 46 : if (strlen (phone) > 30)
144 0 : 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 46 : 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 46 : 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 46 : const char *pattern = allow_letters
214 : ? pattern_with_letters
215 46 : : pattern_digits;
216 :
217 46 : GNUNET_assert (0 ==
218 : regcomp (®ex,
219 : pattern,
220 : REG_EXTENDED | REG_NOSUB | REG_ICASE));
221 46 : ret = regexec (®ex,
222 : phone, 0,
223 : NULL, 0);
224 46 : regfree (®ex);
225 46 : if (0 != ret)
226 16 : return NULL; /* invalid number */
227 : }
228 :
229 : /* Phone is valid - normalize it */
230 : {
231 : char *normalized;
232 : char *out;
233 :
234 30 : normalized = GNUNET_malloc (strlen (phone) + 1);
235 30 : out = normalized;
236 30 : *out++ = '+'; /* Start with plus sign */
237 :
238 30 : for (const char *in = phone;
239 441 : '\0' != *in;
240 411 : in++)
241 : {
242 411 : if (isdigit ((unsigned char) *in))
243 : {
244 : /* Copy digit as-is */
245 269 : *out++ = *in;
246 : }
247 142 : else if (allow_letters && isalpha ((unsigned char) *in))
248 : {
249 : /* Convert letter to corresponding digit (A-Z maps to 2-9) */
250 44 : 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 44 : if (upper >= 'A' && upper <= 'C')
264 8 : digit = '2';
265 36 : else if (upper >= 'D' && upper <= 'F')
266 7 : digit = '3';
267 29 : else if (upper >= 'G' && upper <= 'I')
268 4 : digit = '4';
269 25 : else if (upper >= 'J' && upper <= 'L')
270 8 : digit = '5';
271 17 : else if (upper >= 'M' && upper <= 'O')
272 7 : digit = '6';
273 10 : else if (upper >= 'P' && upper <= 'S')
274 4 : digit = '7';
275 6 : else if (upper >= 'T' && upper <= 'V')
276 0 : digit = '8';
277 6 : else if (upper >= 'W' && upper <= 'Z')
278 6 : digit = '9';
279 : else
280 0 : digit = '0'; /* Fallback (shouldn't happen) */
281 44 : *out++ = digit;
282 : }
283 : /* Skip separators, parentheses, and spaces */
284 : }
285 30 : *out = '\0'; /* redundant, but helps analyzers... */
286 30 : return normalized;
287 : }
288 : }
|