Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 5 | | | |
bc77d7c0 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
6a488035 TO |
11 | |
12 | /** | |
13 | * | |
14 | * @package CRM | |
ca5cec67 | 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
16 | */ |
17 | ||
18 | require_once 'HTML/QuickForm/Rule/Email.php'; | |
f942c321 | 19 | |
5bc392e6 EM |
20 | /** |
21 | * Class CRM_Utils_Rule | |
22 | */ | |
6a488035 TO |
23 | class CRM_Utils_Rule { |
24 | ||
5bc392e6 | 25 | /** |
fa3fdebc | 26 | * @param string|null $str |
5bc392e6 EM |
27 | * @param int $maxLength |
28 | * | |
29 | * @return bool | |
30 | */ | |
00be9182 | 31 | public static function title($str, $maxLength = 127) { |
6a488035 TO |
32 | |
33 | // check length etc | |
34 | if (empty($str) || strlen($str) > $maxLength) { | |
35 | return FALSE; | |
36 | } | |
37 | ||
38 | // Make sure it include valid characters, alpha numeric and underscores | |
39 | if (!preg_match('/^\w[\w\s\'\&\,\$\#\-\.\"\?\!]+$/i', $str)) { | |
40 | return FALSE; | |
41 | } | |
42 | ||
43 | return TRUE; | |
44 | } | |
45 | ||
5bc392e6 | 46 | /** |
fa3fdebc | 47 | * @param string|null $str |
5bc392e6 EM |
48 | * |
49 | * @return bool | |
50 | */ | |
00be9182 | 51 | public static function longTitle($str) { |
6a488035 TO |
52 | return self::title($str, 255); |
53 | } | |
54 | ||
5bc392e6 | 55 | /** |
fa3fdebc | 56 | * @param string|null $str |
5bc392e6 EM |
57 | * |
58 | * @return bool | |
59 | */ | |
00be9182 | 60 | public static function variable($str) { |
6a488035 TO |
61 | // check length etc |
62 | if (empty($str) || strlen($str) > 31) { | |
63 | return FALSE; | |
64 | } | |
65 | ||
50bfb460 | 66 | // make sure it includes valid characters, alpha numeric and underscores |
6a488035 TO |
67 | if (!preg_match('/^[\w]+$/i', $str)) { |
68 | return FALSE; | |
69 | } | |
70 | ||
71 | return TRUE; | |
72 | } | |
73 | ||
00f11506 | 74 | /** |
a33b83c5 | 75 | * Validate that a string is a valid MySQL column name or alias. |
b794b580 | 76 | * |
fa3fdebc | 77 | * @param string|null $str |
00f11506 MM |
78 | * |
79 | * @return bool | |
80 | */ | |
a33b83c5 | 81 | public static function mysqlColumnNameOrAlias($str) { |
10ed14b0 MM |
82 | // Check not empty. |
83 | if (empty($str)) { | |
00f11506 MM |
84 | return FALSE; |
85 | } | |
86 | ||
7cec4a9a CB |
87 | // Ensure $str conforms to expected format. Not a complete expression of |
88 | // what MySQL permits; this should permit the formats CiviCRM generates. | |
89 | // | |
90 | // * Table name prefix is optional. | |
91 | // * Table & column names & aliases: | |
92 | // * Composed of alphanumeric chars, underscore and hyphens. | |
93 | // * Maximum length of 64 chars. | |
94 | // * Optionally surrounded by backticks, in which case spaces also OK. | |
38c9ed00 | 95 | if (!preg_match('/^((`[-\w ]{1,64}`|[-\w]{1,64})\.)?(`[-\w ]{1,64}`|[-\w]{1,64})$/i', $str)) { |
00f11506 MM |
96 | return FALSE; |
97 | } | |
98 | ||
99 | return TRUE; | |
100 | } | |
101 | ||
102 | /** | |
b794b580 CB |
103 | * Validate that a string is ASC or DESC. |
104 | * | |
105 | * Empty string should be treated as invalid and ignored => default = ASC. | |
00f11506 | 106 | * |
fa3fdebc | 107 | * @param string $str |
00f11506 MM |
108 | * @return bool |
109 | */ | |
b794b580 | 110 | public static function mysqlOrderByDirection($str) { |
00f11506 MM |
111 | if (!preg_match('/^(asc|desc)$/i', $str)) { |
112 | return FALSE; | |
113 | } | |
114 | ||
115 | return TRUE; | |
116 | } | |
117 | ||
0fa4baf0 MM |
118 | /** |
119 | * Validate that a string is valid order by clause. | |
120 | * | |
fa3fdebc | 121 | * @param string $str |
0fa4baf0 MM |
122 | * @return bool |
123 | */ | |
124 | public static function mysqlOrderBy($str) { | |
be2fb01f | 125 | $matches = []; |
9d5c7f14 | 126 | // Using the field function in order by is valid. |
127 | // Look for a string like field(contribution_status_id,3,4,6). | |
128 | // or field(civicrm_contribution.contribution_status_id,3,4,6) | |
129 | if (preg_match('/field\([a-z_.]+,[0-9,]+\)/', $str, $matches)) { | |
130 | // We have checked these. Remove them as they will fail the next lot. | |
131 | // Our check currently only permits numbers & no back ticks. If we get a | |
132 | // need for strings or backticks we can add. | |
133 | $str = str_replace($matches, '', $str); | |
134 | } | |
135 | $str = trim($str); | |
136 | if (!empty($matches) && empty($str)) { | |
137 | // nothing left to check after the field check. | |
138 | return TRUE; | |
139 | } | |
0fa4baf0 MM |
140 | // Making a regex for a comma separated list is quite hard and not readable |
141 | // at all, so we split and loop over. | |
142 | $parts = explode(',', $str); | |
143 | foreach ($parts as $part) { | |
a99e69c2 | 144 | if (!preg_match('/^((`[\w-]{1,64}`|[\w-]{1,64})\.)*(`[\w-]{1,64}`|[\w-]{1,64})( (asc|desc))?$/i', trim($part))) { |
0fa4baf0 MM |
145 | return FALSE; |
146 | } | |
147 | } | |
148 | ||
149 | return TRUE; | |
150 | } | |
151 | ||
5bc392e6 | 152 | /** |
fa3fdebc | 153 | * @param string $str |
5bc392e6 EM |
154 | * |
155 | * @return bool | |
156 | */ | |
00be9182 | 157 | public static function qfVariable($str) { |
6a488035 TO |
158 | // check length etc |
159 | //if ( empty( $str ) || strlen( $str ) > 31 ) { | |
160 | if (strlen(trim($str)) == 0 || strlen($str) > 31) { | |
161 | return FALSE; | |
162 | } | |
163 | ||
50bfb460 | 164 | // make sure it includes valid characters, alpha numeric and underscores |
6a488035 TO |
165 | // added (. and ,) option (CRM-1336) |
166 | if (!preg_match('/^[\w\s\.\,]+$/i', $str)) { | |
167 | return FALSE; | |
168 | } | |
169 | ||
170 | return TRUE; | |
171 | } | |
172 | ||
5bc392e6 | 173 | /** |
fa3fdebc | 174 | * @param string|null $phone |
5bc392e6 EM |
175 | * |
176 | * @return bool | |
177 | */ | |
00be9182 | 178 | public static function phone($phone) { |
6a488035 TO |
179 | // check length etc |
180 | if (empty($phone) || strlen($phone) > 16) { | |
181 | return FALSE; | |
182 | } | |
183 | ||
50bfb460 | 184 | // make sure it includes valid characters, (, \s and numeric |
6a488035 TO |
185 | if (preg_match('/^[\d\(\)\-\.\s]+$/', $phone)) { |
186 | return TRUE; | |
187 | } | |
188 | return FALSE; | |
189 | } | |
190 | ||
5bc392e6 | 191 | /** |
fa3fdebc | 192 | * @param string|null $query |
5bc392e6 EM |
193 | * |
194 | * @return bool | |
195 | */ | |
00be9182 | 196 | public static function query($query) { |
6a488035 TO |
197 | // check length etc |
198 | if (empty($query) || strlen($query) < 3 || strlen($query) > 127) { | |
199 | return FALSE; | |
200 | } | |
201 | ||
50bfb460 | 202 | // make sure it includes valid characters, alpha numeric and underscores |
6a488035 TO |
203 | if (!preg_match('/^[\w\s\%\'\&\,\$\#]+$/i', $query)) { |
204 | return FALSE; | |
205 | } | |
206 | ||
207 | return TRUE; | |
208 | } | |
209 | ||
5bc392e6 | 210 | /** |
fa3fdebc | 211 | * @param string|null $url |
5bc392e6 EM |
212 | * |
213 | * @return bool | |
214 | */ | |
00be9182 | 215 | public static function url($url) { |
c7cd4e2c | 216 | if (!$url) { |
217 | // If this is required then that should be checked elsewhere - here we are not assuming it is required. | |
218 | return TRUE; | |
219 | } | |
1136a401 | 220 | if (preg_match('/^\//', $url)) { |
221 | // allow relative URL's (CRM-15598) | |
222 | $url = 'http://' . $_SERVER['HTTP_HOST'] . $url; | |
223 | } | |
c79f24a5 JG |
224 | // Convert URLs with Unicode to ASCII |
225 | if (strlen($url) != strlen(utf8_decode($url))) { | |
226 | $url = self::idnToAsci($url); | |
227 | } | |
228 | return (bool) filter_var($url, FILTER_VALIDATE_URL); | |
6a488035 TO |
229 | } |
230 | ||
d9d7e7dd | 231 | /** |
fa3fdebc | 232 | * @param string|null $url |
d9d7e7dd TO |
233 | * |
234 | * @return bool | |
235 | */ | |
236 | public static function urlish($url) { | |
237 | if (empty($url)) { | |
238 | return TRUE; | |
239 | } | |
e3d28c74 | 240 | $url = Civi::paths()->getUrl($url, 'absolute'); |
d9d7e7dd TO |
241 | return (bool) filter_var($url, FILTER_VALIDATE_URL); |
242 | } | |
243 | ||
5bc392e6 | 244 | /** |
fa3fdebc | 245 | * @param string $string |
5bc392e6 EM |
246 | * |
247 | * @return bool | |
248 | */ | |
00be9182 | 249 | public static function wikiURL($string) { |
6a488035 TO |
250 | $items = explode(' ', trim($string), 2); |
251 | return self::url($items[0]); | |
252 | } | |
253 | ||
5bc392e6 | 254 | /** |
fa3fdebc | 255 | * @param string $domain |
5bc392e6 EM |
256 | * |
257 | * @return bool | |
258 | */ | |
00be9182 | 259 | public static function domain($domain) { |
6a488035 TO |
260 | // not perfect, but better than the previous one; see CRM-1502 |
261 | if (!preg_match('/^[A-Za-z0-9]([A-Za-z0-9\.\-]*[A-Za-z0-9])?$/', $domain)) { | |
262 | return FALSE; | |
263 | } | |
264 | return TRUE; | |
265 | } | |
266 | ||
5bc392e6 | 267 | /** |
e0f5b841 | 268 | * @param string $value |
3fd42bb5 | 269 | * @param string|null $default |
5bc392e6 | 270 | * |
e0f5b841 | 271 | * @return string|null |
5bc392e6 | 272 | */ |
00be9182 | 273 | public static function date($value, $default = NULL) { |
6a488035 TO |
274 | if (is_string($value) && |
275 | preg_match('/^\d\d\d\d-?\d\d-?\d\d$/', $value) | |
276 | ) { | |
277 | return $value; | |
278 | } | |
279 | return $default; | |
280 | } | |
281 | ||
5bc392e6 | 282 | /** |
e0f5b841 | 283 | * @param string $value |
3fd42bb5 | 284 | * @param string|null $default |
5bc392e6 | 285 | * |
e0f5b841 | 286 | * @return string|null |
5bc392e6 | 287 | */ |
00be9182 | 288 | public static function dateTime($value, $default = NULL) { |
6a488035 TO |
289 | $result = $default; |
290 | if (is_string($value) && | |
291 | preg_match('/^\d\d\d\d-?\d\d-?\d\d(\s\d\d:\d\d(:\d\d)?|\d\d\d\d(\d\d)?)?$/', $value) | |
292 | ) { | |
293 | $result = $value; | |
294 | } | |
295 | ||
296 | return $result; | |
297 | } | |
298 | ||
299 | /** | |
100fef9d | 300 | * Check the validity of the date (in qf format) |
6a488035 TO |
301 | * note that only a year is valid, or a mon-year is |
302 | * also valid in addition to day-mon-year. The date | |
303 | * specified has to be beyond today. (i.e today or later) | |
304 | * | |
305 | * @param array $date | |
77855840 TO |
306 | * @param bool $monthRequired |
307 | * Check whether month is mandatory. | |
6a488035 | 308 | * |
a6c01b45 CW |
309 | * @return bool |
310 | * true if valid date | |
6a488035 | 311 | */ |
00be9182 | 312 | public static function currentDate($date, $monthRequired = TRUE) { |
6a488035 TO |
313 | $config = CRM_Core_Config::singleton(); |
314 | ||
9c1bc317 CW |
315 | $d = $date['d'] ?? NULL; |
316 | $m = $date['M'] ?? NULL; | |
317 | $y = $date['Y'] ?? NULL; | |
6a488035 TO |
318 | |
319 | if (!$d && !$m && !$y) { | |
320 | return TRUE; | |
321 | } | |
322 | ||
323 | // CRM-9017 CiviContribute/CiviMember form with expiration date format 'm Y' | |
8cc574cf | 324 | if (!$m && !empty($date['m'])) { |
9c1bc317 | 325 | $m = $date['m'] ?? NULL; |
6a488035 TO |
326 | } |
327 | ||
328 | $day = $mon = 1; | |
329 | $year = 0; | |
330 | if ($d) { | |
331 | $day = $d; | |
332 | } | |
333 | if ($m) { | |
334 | $mon = $m; | |
335 | } | |
336 | if ($y) { | |
337 | $year = $y; | |
338 | } | |
339 | ||
340 | // if we have day we need mon, and if we have mon we need year | |
341 | if (($d && !$m) || | |
342 | ($d && !$y) || | |
343 | ($m && !$y) | |
344 | ) { | |
345 | return FALSE; | |
346 | } | |
347 | ||
348 | $result = FALSE; | |
349 | if (!empty($day) || !empty($mon) || !empty($year)) { | |
350 | $result = checkdate($mon, $day, $year); | |
351 | } | |
352 | ||
353 | if (!$result) { | |
354 | return FALSE; | |
355 | } | |
356 | ||
357 | // ensure we have month if required | |
358 | if ($monthRequired && !$m) { | |
359 | return FALSE; | |
360 | } | |
361 | ||
362 | // now make sure this date is greater that today | |
363 | $currentDate = getdate(); | |
364 | if ($year > $currentDate['year']) { | |
365 | return TRUE; | |
366 | } | |
367 | elseif ($year < $currentDate['year']) { | |
368 | return FALSE; | |
369 | } | |
370 | ||
371 | if ($m) { | |
372 | if ($mon > $currentDate['mon']) { | |
373 | return TRUE; | |
374 | } | |
375 | elseif ($mon < $currentDate['mon']) { | |
376 | return FALSE; | |
377 | } | |
378 | } | |
379 | ||
380 | if ($d) { | |
381 | if ($day > $currentDate['mday']) { | |
382 | return TRUE; | |
383 | } | |
384 | elseif ($day < $currentDate['mday']) { | |
385 | return FALSE; | |
386 | } | |
387 | } | |
388 | ||
389 | return TRUE; | |
390 | } | |
391 | ||
392 | /** | |
100fef9d | 393 | * Check the validity of a date or datetime (timestamp) |
6a488035 TO |
394 | * value which is in YYYYMMDD or YYYYMMDDHHMMSS format |
395 | * | |
396 | * Uses PHP checkdate() - params are ( int $month, int $day, int $year ) | |
397 | * | |
398 | * @param string $date | |
399 | * | |
a6c01b45 CW |
400 | * @return bool |
401 | * true if valid date | |
6a488035 | 402 | */ |
00be9182 | 403 | public static function mysqlDate($date) { |
6a488035 TO |
404 | // allow date to be null |
405 | if ($date == NULL) { | |
406 | return TRUE; | |
407 | } | |
408 | ||
409 | if (checkdate(substr($date, 4, 2), substr($date, 6, 2), substr($date, 0, 4))) { | |
410 | return TRUE; | |
411 | } | |
412 | ||
413 | return FALSE; | |
414 | } | |
415 | ||
5bc392e6 | 416 | /** |
fa3fdebc | 417 | * @param mixed $value |
5bc392e6 EM |
418 | * |
419 | * @return bool | |
420 | */ | |
00be9182 | 421 | public static function integer($value) { |
6a488035 TO |
422 | if (is_int($value)) { |
423 | return TRUE; | |
424 | } | |
425 | ||
f942c321 DL |
426 | // CRM-13460 |
427 | // ensure number passed is always a string numeral | |
428 | if (!is_numeric($value)) { | |
429 | return FALSE; | |
430 | } | |
431 | ||
432 | // note that is_int matches only integer type | |
433 | // and not strings which are only integers | |
434 | // hence we do this here | |
435 | if (preg_match('/^\d+$/', $value)) { | |
436 | return TRUE; | |
437 | } | |
438 | ||
439 | if ($value < 0) { | |
6a488035 TO |
440 | $negValue = -1 * $value; |
441 | if (is_int($negValue)) { | |
442 | return TRUE; | |
443 | } | |
444 | } | |
445 | ||
6a488035 TO |
446 | return FALSE; |
447 | } | |
448 | ||
5bc392e6 | 449 | /** |
fa3fdebc | 450 | * @param mixed $value |
5bc392e6 EM |
451 | * |
452 | * @return bool | |
453 | */ | |
00be9182 | 454 | public static function positiveInteger($value) { |
6a488035 | 455 | if (is_int($value)) { |
91768280 | 456 | return !($value < 0); |
6a488035 TO |
457 | } |
458 | ||
f942c321 DL |
459 | // CRM-13460 |
460 | // ensure number passed is always a string numeral | |
461 | if (!is_numeric($value)) { | |
462 | return FALSE; | |
463 | } | |
464 | ||
91768280 | 465 | return (bool) preg_match('/^\d+$/', $value); |
6a488035 TO |
466 | } |
467 | ||
fe61faf3 | 468 | /** |
fa3fdebc | 469 | * @param mixed $value |
fe61faf3 CW |
470 | * |
471 | * @return bool | |
472 | */ | |
473 | public static function commaSeparatedIntegers($value) { | |
474 | foreach (explode(',', $value) as $val) { | |
62d7cac4 SL |
475 | // Remove any Whitespace around the key. |
476 | $val = trim($val); | |
fe61faf3 CW |
477 | if (!self::positiveInteger($val)) { |
478 | return FALSE; | |
479 | } | |
480 | } | |
481 | return TRUE; | |
482 | } | |
483 | ||
5bc392e6 | 484 | /** |
fa3fdebc | 485 | * @param mixed $value |
5bc392e6 EM |
486 | * |
487 | * @return bool | |
488 | */ | |
00be9182 | 489 | public static function numeric($value) { |
f942c321 DL |
490 | // lets use a php gatekeeper to ensure this is numeric |
491 | if (!is_numeric($value)) { | |
492 | return FALSE; | |
493 | } | |
494 | ||
fe0dbeda | 495 | return (bool) preg_match('/(^-?\d\d*\.\d*$)|(^-?\d\d*$)|(^-?\.\d\d*$)/', $value); |
6a488035 TO |
496 | } |
497 | ||
d22982f3 SM |
498 | /** |
499 | * Test whether $value is alphanumeric. | |
500 | * | |
501 | * Underscores and dashes are also allowed! | |
502 | * | |
503 | * This is the type of string you could expect to see in URL parameters | |
504 | * like `?mode=live` vs `?mode=test`. This function exists so that we can be | |
505 | * strict about what we accept for such values, thus mitigating against | |
506 | * potential security issues. | |
507 | * | |
508 | * @see \CRM_Utils_RuleTest::alphanumericData | |
509 | * for examples of vales that give TRUE/FALSE here | |
510 | * | |
fa3fdebc | 511 | * @param string $value |
d22982f3 SM |
512 | * |
513 | * @return bool | |
514 | */ | |
515 | public static function alphanumeric($value) { | |
fe0dbeda | 516 | return (bool) preg_match('/^[a-zA-Z0-9_-]*$/', $value); |
d22982f3 SM |
517 | } |
518 | ||
5bc392e6 | 519 | /** |
fa3fdebc BT |
520 | * @param string $value |
521 | * @param int $noOfDigit | |
5bc392e6 EM |
522 | * |
523 | * @return bool | |
524 | */ | |
00be9182 | 525 | public static function numberOfDigit($value, $noOfDigit) { |
fe0dbeda | 526 | return (bool) preg_match('/^\d{' . $noOfDigit . '}$/', $value); |
6a488035 TO |
527 | } |
528 | ||
8a52ae34 CW |
529 | /** |
530 | * Strict validation of 6-digit hex color notation per html5 <input type="color"> | |
531 | * | |
fa3fdebc | 532 | * @param string $value |
8a52ae34 CW |
533 | * @return bool |
534 | */ | |
535 | public static function color($value) { | |
536 | return (bool) preg_match('/^#([\da-fA-F]{6})$/', $value); | |
537 | } | |
538 | ||
5bc392e6 | 539 | /** |
83644f47 | 540 | * Strip thousand separator from a money string. |
541 | * | |
542 | * Note that this should be done at the form layer. Once we are processing | |
543 | * money at the BAO or processor layer we should be working with something that | |
544 | * is already in a normalised format. | |
545 | * | |
546 | * @param string $value | |
5bc392e6 | 547 | * |
83644f47 | 548 | * @return string |
5bc392e6 | 549 | */ |
00be9182 | 550 | public static function cleanMoney($value) { |
6a488035 | 551 | // first remove all white space |
be2fb01f | 552 | $value = str_replace([' ', "\t", "\n"], '', $value); |
6a488035 TO |
553 | |
554 | $config = CRM_Core_Config::singleton(); | |
555 | ||
e7292422 | 556 | //CRM-14868 |
ef88f444 | 557 | $currencySymbols = CRM_Core_PseudoConstant::get( |
353ffa53 | 558 | 'CRM_Contribute_DAO_Contribution', |
be2fb01f | 559 | 'currency', [ |
353ffa53 TO |
560 | 'keyColumn' => 'name', |
561 | 'labelColumn' => 'symbol', | |
be2fb01f | 562 | ] |
e70a7fc0 | 563 | ); |
e7292422 | 564 | $value = str_replace($currencySymbols, '', $value); |
ef88f444 | 565 | |
6a488035 TO |
566 | if ($config->monetaryThousandSeparator) { |
567 | $mon_thousands_sep = $config->monetaryThousandSeparator; | |
568 | } | |
569 | else { | |
570 | $mon_thousands_sep = ','; | |
571 | } | |
572 | ||
573 | // ugly fix for CRM-6391: do not drop the thousand separator if | |
574 | // it looks like it’s separating decimal part (because a given | |
575 | // value undergoes a second cleanMoney() call, for example) | |
b81f42da | 576 | // CRM-15835 - in case the amount/value contains 0 after decimal |
577 | // eg 150.5 the following if condition will pass | |
578 | if ($mon_thousands_sep != '.' or (substr($value, -3, 1) != '.' && substr($value, -2, 1) != '.')) { | |
6a488035 TO |
579 | $value = str_replace($mon_thousands_sep, '', $value); |
580 | } | |
581 | ||
582 | if ($config->monetaryDecimalPoint) { | |
583 | $mon_decimal_point = $config->monetaryDecimalPoint; | |
584 | } | |
585 | else { | |
586 | $mon_decimal_point = '.'; | |
587 | } | |
588 | $value = str_replace($mon_decimal_point, '.', $value); | |
589 | ||
590 | return $value; | |
591 | } | |
592 | ||
5bc392e6 | 593 | /** |
fa3fdebc | 594 | * @param string $value |
4b58c5c4 EM |
595 | * @param bool $checkSeparatorOrder |
596 | * Should the order of the separators be checked. ie if the thousand | |
597 | * separator is , then it should never be after the decimal separator . | |
598 | * so 1.300,23 would be invalid in that case. Honestly I'm amazed this | |
599 | * check wasn't being done but in the interest of caution adding as opt in. | |
600 | * Note clean money would convert this to 1.30023.... | |
5bc392e6 EM |
601 | * |
602 | * @return bool | |
603 | */ | |
4b58c5c4 EM |
604 | public static function money($value, $checkSeparatorOrder = FALSE) { |
605 | // We can't rely on only one var being passed so can't type-hint to a bool. | |
606 | if ($checkSeparatorOrder === TRUE) { | |
607 | $thousandSeparatorPosition = strpos((string) $value, \Civi::settings()->get('monetaryThousandSeparator')); | |
608 | $decimalSeparatorPosition = strpos((string) $value, \Civi::settings()->get('monetaryDecimalPoint')); | |
609 | if ($thousandSeparatorPosition && $decimalSeparatorPosition && $thousandSeparatorPosition > $decimalSeparatorPosition) { | |
610 | return FALSE; | |
611 | } | |
612 | } | |
6a488035 TO |
613 | $value = self::cleanMoney($value); |
614 | ||
615 | if (self::integer($value)) { | |
616 | return TRUE; | |
617 | } | |
618 | ||
ce18e8d1 MW |
619 | // Allow values such as -0, 1.024555, -.1 |
620 | // We need to support multiple decimal places here, not just the number allowed by locale | |
621 | // otherwise tax calculations break when you want the inclusive amount to be a round number (eg. £10 inc. VAT requires 8.333333333 here). | |
fe0dbeda | 622 | return (bool) preg_match('/(^-?\d+\.?\d*$)|(^-?\.\d+$)/', $value); |
6a488035 TO |
623 | } |
624 | ||
5bc392e6 | 625 | /** |
fa3fdebc | 626 | * @param mixed $value |
5bc392e6 EM |
627 | * @param int $maxLength |
628 | * | |
629 | * @return bool | |
630 | */ | |
00be9182 | 631 | public static function string($value, $maxLength = 0) { |
6a488035 TO |
632 | if (is_string($value) && |
633 | ($maxLength === 0 || strlen($value) <= $maxLength) | |
634 | ) { | |
635 | return TRUE; | |
636 | } | |
637 | return FALSE; | |
638 | } | |
639 | ||
5bc392e6 | 640 | /** |
fa3fdebc | 641 | * @param bool|string $value |
5bc392e6 EM |
642 | * |
643 | * @return bool | |
644 | */ | |
00be9182 | 645 | public static function boolean($value) { |
9281d89e JG |
646 | if ($value === TRUE || $value === FALSE) { |
647 | return TRUE; | |
648 | } | |
649 | // This is intentionally not using === comparison - but will fail on FALSE. | |
6a488035 TO |
650 | return preg_match( |
651 | '/(^(1|0)$)|(^(Y(es)?|N(o)?)$)|(^(T(rue)?|F(alse)?)$)/i', $value | |
652 | ) ? TRUE : FALSE; | |
653 | } | |
654 | ||
5bc392e6 | 655 | /** |
fa3fdebc | 656 | * @param mixed $value |
5bc392e6 EM |
657 | * |
658 | * @return bool | |
659 | */ | |
5ae46289 EM |
660 | public static function email($value): bool { |
661 | if (function_exists('idn_to_ascii')) { | |
662 | $parts = explode('@', $value); | |
663 | foreach ($parts as &$part) { | |
664 | // if the function returns FALSE then let filter_var have at it. | |
665 | $part = self::idnToAsci($part) ?: $part; | |
666 | if ($part === 'localhost') { | |
667 | // if we are in a dev environment add .com to trick it into accepting localhost. | |
668 | // this is a bit best-effort - ie we don't really care that it's in a bigger if. | |
669 | $part .= '.com'; | |
670 | } | |
671 | } | |
672 | $value = implode('@', $parts); | |
673 | } | |
6a488035 TO |
674 | return (bool) filter_var($value, FILTER_VALIDATE_EMAIL); |
675 | } | |
676 | ||
5bc392e6 | 677 | /** |
5ae46289 EM |
678 | * Convert domain string to ascii. |
679 | * | |
680 | * See https://lab.civicrm.org/dev/core/-/issues/2769 | |
681 | * and also discussion over in guzzle land | |
682 | * https://github.com/guzzle/guzzle/pull/2454 | |
683 | * | |
684 | * @param string $string | |
685 | * | |
686 | * @return string|false | |
687 | */ | |
688 | private static function idnToAsci(string $string) { | |
689 | if (!\extension_loaded('intl')) { | |
690 | return $string; | |
691 | } | |
692 | if (defined('INTL_IDNA_VARIANT_UTS46')) { | |
693 | return idn_to_ascii($string, 0, INTL_IDNA_VARIANT_UTS46); | |
694 | } | |
695 | return idn_to_ascii($string); | |
696 | } | |
697 | ||
698 | /** | |
699 | * @param string $list | |
5bc392e6 EM |
700 | * |
701 | * @return bool | |
702 | */ | |
00be9182 | 703 | public static function emailList($list) { |
6a488035 TO |
704 | $emails = explode(',', $list); |
705 | foreach ($emails as $email) { | |
706 | $email = trim($email); | |
707 | if (!self::email($email)) { | |
708 | return FALSE; | |
709 | } | |
710 | } | |
711 | return TRUE; | |
712 | } | |
713 | ||
5bc392e6 | 714 | /** |
4f1f1f2a CW |
715 | * allow between 4-6 digits as postal code since india needs 6 and US needs 5 (or |
716 | * if u disregard the first 0, 4 (thanx excel!) | |
717 | * FIXME: we need to figure out how to localize such rules | |
fa3fdebc | 718 | * @param string $value |
5bc392e6 EM |
719 | * |
720 | * @return bool | |
721 | */ | |
00be9182 | 722 | public static function postalCode($value) { |
6a488035 TO |
723 | if (preg_match('/^\d{4,6}(-\d{4})?$/', $value)) { |
724 | return TRUE; | |
725 | } | |
726 | return FALSE; | |
727 | } | |
728 | ||
729 | /** | |
100fef9d | 730 | * See how file rules are written in HTML/QuickForm/file.php |
6a488035 TO |
731 | * Checks to make sure the uploaded file is ascii |
732 | * | |
ea3ddccf | 733 | * @param string $elementValue |
734 | * | |
a6c01b45 | 735 | * @return bool |
ea3ddccf | 736 | * True if file has been uploaded, false otherwise |
6a488035 | 737 | */ |
00be9182 | 738 | public static function asciiFile($elementValue) { |
6a488035 TO |
739 | if ((isset($elementValue['error']) && $elementValue['error'] == 0) || |
740 | (!empty($elementValue['tmp_name']) && $elementValue['tmp_name'] != 'none') | |
741 | ) { | |
742 | return CRM_Utils_File::isAscii($elementValue['tmp_name']); | |
743 | } | |
744 | return FALSE; | |
745 | } | |
746 | ||
747 | /** | |
748 | * Checks to make sure the uploaded file is in UTF-8, recodes if it's not | |
749 | * | |
ea3ddccf | 750 | * @param array $elementValue |
751 | * | |
a6c01b45 | 752 | * @return bool |
ea3ddccf | 753 | * Whether file has been uploaded properly and is now in UTF-8. |
6a488035 | 754 | */ |
00be9182 | 755 | public static function utf8File($elementValue) { |
6a488035 TO |
756 | $success = FALSE; |
757 | ||
758 | if ((isset($elementValue['error']) && $elementValue['error'] == 0) || | |
759 | (!empty($elementValue['tmp_name']) && $elementValue['tmp_name'] != 'none') | |
760 | ) { | |
761 | ||
762 | $success = CRM_Utils_File::isAscii($elementValue['tmp_name']); | |
763 | ||
764 | // if it's a file, but not UTF-8, let's try and recode it | |
765 | // and then make sure it's an UTF-8 file in the end | |
766 | if (!$success) { | |
767 | $success = CRM_Utils_File::toUtf8($elementValue['tmp_name']); | |
768 | if ($success) { | |
769 | $success = CRM_Utils_File::isAscii($elementValue['tmp_name']); | |
770 | } | |
771 | } | |
772 | } | |
773 | return $success; | |
774 | } | |
775 | ||
6a488035 | 776 | /** |
fe482240 | 777 | * Check if there is a record with the same name in the db. |
6a488035 | 778 | * |
77855840 TO |
779 | * @param string $value |
780 | * The value of the field we are checking. | |
781 | * @param array $options | |
35b63106 | 782 | * The daoName, fieldName (optional) and DomainID (optional). |
6a488035 | 783 | * |
408b79bf | 784 | * @return bool |
a6c01b45 | 785 | * true if object exists |
6a488035 | 786 | */ |
00be9182 | 787 | public static function objectExists($value, $options) { |
6a488035 TO |
788 | $name = 'name'; |
789 | if (isset($options[2])) { | |
790 | $name = $options[2]; | |
791 | } | |
792 | ||
35b63106 | 793 | return CRM_Core_DAO::objectExists($value, CRM_Utils_Array::value(0, $options), CRM_Utils_Array::value(1, $options), CRM_Utils_Array::value(2, $options, $name), CRM_Utils_Array::value(3, $options)); |
6a488035 TO |
794 | } |
795 | ||
5bc392e6 EM |
796 | /** |
797 | * @param $value | |
798 | * @param $options | |
799 | * | |
800 | * @return bool | |
801 | */ | |
00be9182 | 802 | public static function optionExists($value, $options) { |
e6101f17 | 803 | return CRM_Core_OptionValue::optionExists($value, $options[0], $options[1], $options[2], CRM_Utils_Array::value(3, $options, 'name'), CRM_Utils_Array::value(4, $options, FALSE)); |
6a488035 TO |
804 | } |
805 | ||
5bc392e6 | 806 | /** |
fa3fdebc BT |
807 | * @param string $value |
808 | * @param string $type | |
5bc392e6 EM |
809 | * |
810 | * @return bool | |
811 | */ | |
00be9182 | 812 | public static function creditCardNumber($value, $type) { |
6a488035 TO |
813 | return Validate_Finance_CreditCard::number($value, $type); |
814 | } | |
815 | ||
5bc392e6 | 816 | /** |
fa3fdebc BT |
817 | * @param string $value |
818 | * @param string $type | |
5bc392e6 EM |
819 | * |
820 | * @return bool | |
821 | */ | |
00be9182 | 822 | public static function cvv($value, $type) { |
6a488035 TO |
823 | return Validate_Finance_CreditCard::cvv($value, $type); |
824 | } | |
825 | ||
5bc392e6 | 826 | /** |
fa3fdebc | 827 | * @param mixed $value |
5bc392e6 EM |
828 | * |
829 | * @return bool | |
830 | */ | |
00be9182 | 831 | public static function currencyCode($value) { |
6a488035 TO |
832 | static $currencyCodes = NULL; |
833 | if (!$currencyCodes) { | |
834 | $currencyCodes = CRM_Core_PseudoConstant::currencyCode(); | |
835 | } | |
836 | if (in_array($value, $currencyCodes)) { | |
837 | return TRUE; | |
838 | } | |
839 | return FALSE; | |
840 | } | |
841 | ||
88251439 | 842 | /** |
843 | * Validate json string for xss | |
844 | * | |
845 | * @param string $value | |
846 | * | |
847 | * @return bool | |
848 | * False if invalid, true if valid / safe. | |
849 | */ | |
850 | public static function json($value) { | |
88251439 | 851 | $array = json_decode($value, TRUE); |
852 | if (!$array || !is_array($array)) { | |
853 | return FALSE; | |
854 | } | |
855 | return self::arrayValue($array); | |
856 | } | |
857 | ||
5bc392e6 | 858 | /** |
fa3fdebc | 859 | * @param string $path |
5bc392e6 EM |
860 | * |
861 | * @return bool | |
862 | */ | |
00be9182 | 863 | public static function fileExists($path) { |
6a488035 TO |
864 | return file_exists($path); |
865 | } | |
866 | ||
d9d7e7dd TO |
867 | /** |
868 | * Determine whether the value contains a valid reference to a directory. | |
869 | * | |
870 | * Paths stored in the setting system may be absolute -- or may be | |
871 | * relative to the default data directory. | |
872 | * | |
873 | * @param string $path | |
874 | * @return bool | |
875 | */ | |
876 | public static function settingPath($path) { | |
e3d28c74 | 877 | return is_dir(Civi::paths()->getPath($path)); |
d9d7e7dd TO |
878 | } |
879 | ||
5bc392e6 | 880 | /** |
3fd42bb5 BT |
881 | * @param mixed $value |
882 | * @param mixed $actualElementValue | |
5bc392e6 EM |
883 | * |
884 | * @return bool | |
885 | */ | |
00be9182 | 886 | public static function validContact($value, $actualElementValue = NULL) { |
6a488035 TO |
887 | if ($actualElementValue) { |
888 | $value = $actualElementValue; | |
889 | } | |
890 | ||
258570f7 | 891 | return CRM_Utils_Rule::positiveInteger($value); |
6a488035 TO |
892 | } |
893 | ||
894 | /** | |
100fef9d | 895 | * Check the validity of the date (in qf format) |
6a488035 TO |
896 | * note that only a year is valid, or a mon-year is |
897 | * also valid in addition to day-mon-year | |
898 | * | |
899 | * @param array $date | |
900 | * | |
a6c01b45 CW |
901 | * @return bool |
902 | * true if valid date | |
6a488035 | 903 | */ |
00be9182 | 904 | public static function qfDate($date) { |
6a488035 TO |
905 | $config = CRM_Core_Config::singleton(); |
906 | ||
9c1bc317 CW |
907 | $d = $date['d'] ?? NULL; |
908 | $m = $date['M'] ?? NULL; | |
909 | $y = $date['Y'] ?? NULL; | |
6a488035 TO |
910 | if (isset($date['h']) || |
911 | isset($date['g']) | |
912 | ) { | |
9c1bc317 | 913 | $m = $date['M'] ?? NULL; |
6a488035 TO |
914 | } |
915 | ||
916 | if (!$d && !$m && !$y) { | |
917 | return TRUE; | |
918 | } | |
919 | ||
920 | $day = $mon = 1; | |
921 | $year = 0; | |
922 | if ($d) { | |
923 | $day = $d; | |
924 | } | |
925 | if ($m) { | |
926 | $mon = $m; | |
927 | } | |
928 | if ($y) { | |
929 | $year = $y; | |
930 | } | |
931 | ||
932 | // if we have day we need mon, and if we have mon we need year | |
933 | if (($d && !$m) || | |
934 | ($d && !$y) || | |
935 | ($m && !$y) | |
936 | ) { | |
937 | return FALSE; | |
938 | } | |
939 | ||
940 | if (!empty($day) || !empty($mon) || !empty($year)) { | |
941 | return checkdate($mon, $day, $year); | |
942 | } | |
943 | return FALSE; | |
944 | } | |
945 | ||
5bc392e6 | 946 | /** |
fa3fdebc | 947 | * @param mixed $key |
5bc392e6 EM |
948 | * |
949 | * @return bool | |
950 | */ | |
00be9182 | 951 | public static function qfKey($key) { |
6a488035 TO |
952 | return ($key) ? CRM_Core_Key::valid($key) : FALSE; |
953 | } | |
96025800 | 954 | |
79326ee2 SB |
955 | /** |
956 | * Check if the values in the date range are in correct chronological order. | |
957 | * | |
958 | * @param array $fields | |
959 | * Fields of the form. | |
960 | * @param $fieldName | |
961 | * Name of date range field. | |
962 | * @param $errors | |
963 | * The error array. | |
964 | * @param $title | |
965 | * Title of the date range to be displayed in the error message. | |
966 | */ | |
967 | public static function validDateRange($fields, $fieldName, &$errors, $title) { | |
968 | $lowDate = strtotime($fields[$fieldName . '_low']); | |
969 | $highDate = strtotime($fields[$fieldName . '_high']); | |
970 | ||
971 | if ($lowDate > $highDate) { | |
be2fb01f | 972 | $errors[$fieldName . '_range_error'] = ts('%1: Please check that your date range is in correct chronological order.', [1 => $title]); |
79326ee2 SB |
973 | } |
974 | } | |
975 | ||
5df85a46 SL |
976 | /** |
977 | * @param string $key Extension Key to check | |
978 | * @return bool | |
979 | */ | |
9e1d9d01 | 980 | public static function checkExtensionKeyIsValid($key = NULL) { |
5df85a46 SL |
981 | if (!empty($key) && !preg_match('/^[0-9a-zA-Z._-]+$/', $key)) { |
982 | return FALSE; | |
983 | } | |
984 | return TRUE; | |
985 | } | |
986 | ||
88251439 | 987 | /** |
988 | * Validate array recursively checking keys and values. | |
989 | * | |
990 | * @param array $array | |
991 | * @return bool | |
992 | */ | |
993 | protected static function arrayValue($array) { | |
994 | foreach ($array as $key => $item) { | |
995 | if (is_array($item)) { | |
d33b6121 | 996 | if (!self::arrayValue($item)) { |
88251439 | 997 | return FALSE; |
998 | } | |
999 | } | |
88251439 | 1000 | } |
1001 | return TRUE; | |
1002 | } | |
1003 | ||
6a488035 | 1004 | } |