Schema - Add base function for FiveFortyNine boolean search
[civicrm-core.git] / CRM / Utils / Type.php
CommitLineData
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 */
17class CRM_Utils_Type {
7da04cde 18 const
353ffa53
TO
19 T_INT = 1,
20 T_STRING = 2,
21 T_ENUM = 2,
22 T_DATE = 4,
23 T_TIME = 8,
24 T_BOOLEAN = 16,
25 T_TEXT = 32,
26 T_LONGTEXT = 32,
27 T_BLOB = 64,
28 T_TIMESTAMP = 256,
29 T_FLOAT = 512,
30 T_MONEY = 1024,
31 T_EMAIL = 2048,
32 T_URL = 4096,
33 T_CCNUM = 8192,
6a488035
TO
34 T_MEDIUMBLOB = 16384;
35
2c05985c
CB
36 // @TODO What's the point of these constants? Backwards compatibility?
37 //
38 // These are used for field size (<input type=text size=2>), but redundant TWO=2
39 // usages are rare and should be eliminated. See CRM-18810.
7da04cde 40 const
353ffa53
TO
41 TWO = 2,
42 FOUR = 4,
43 SIX = 6,
44 EIGHT = 8,
45 TWELVE = 12,
46 SIXTEEN = 16,
47 TWENTY = 20,
48 MEDIUM = 20,
49 THIRTY = 30,
50 BIG = 30,
6a488035 51 FORTYFIVE = 45,
353ffa53 52 HUGE = 45;
6a488035 53
7c7ab278 54 /**
55 * Maximum size of a MySQL BLOB or TEXT column in bytes.
56 */
57 const BLOB_SIZE = 65535;
58
d01ae020 59 /**
60 * Maximum value of a MySQL signed INT column.
61 */
62 const INT_MAX = 2147483647;
63
6a488035 64 /**
a3379cc1 65 * Gets the string representation for a data type.
6a488035 66 *
a3379cc1
AH
67 * @param int $type
68 * Integer number identifying the data type.
6a488035 69 *
a3379cc1
AH
70 * @return string
71 * String identifying the data type, e.g. 'Int' or 'String'.
6a488035 72 */
00be9182 73 public static function typeToString($type) {
d6528d9c
AH
74 // @todo Use constants in the case statements, e.g. "case T_INT:".
75 // @todo return directly, instead of assigning a value.
76 // @todo Use a lookup array, as a property or as a local variable.
6a488035
TO
77 switch ($type) {
78 case 1:
79 $string = 'Int';
80 break;
81
82 case 2:
83 $string = 'String';
84 break;
85
86 case 3:
87 $string = 'Enum';
88 break;
89
90 case 4:
91 $string = 'Date';
92 break;
93
94 case 8:
95 $string = 'Time';
96 break;
97
98 case 16:
99 $string = 'Boolean';
100 break;
101
102 case 32:
103 $string = 'Text';
104 break;
105
106 case 64:
107 $string = 'Blob';
108 break;
109
e7292422 110 // CRM-10404
6a488035
TO
111 case 12:
112 case 256:
113 $string = 'Timestamp';
114 break;
115
116 case 512:
117 $string = 'Float';
118 break;
119
120 case 1024:
121 $string = 'Money';
122 break;
123
124 case 2048:
125 $string = 'Date';
126 break;
127
128 case 4096:
129 $string = 'Email';
130 break;
131
132 case 16384:
133 $string = 'Mediumblob';
134 break;
135 }
136
137 return (isset($string)) ? $string : "";
138 }
139
067c2e09 140 /**
141 * @return array
142 * An array of type in the form 'type name' => 'int representing type'
143 */
144 public static function getValidTypes() {
be2fb01f 145 return [
067c2e09 146 'Int' => self::T_INT,
147 'String' => self::T_STRING,
148 'Enum' => self::T_ENUM,
149 'Date' => self::T_DATE,
150 'Time' => self::T_TIME,
151 'Boolean' => self::T_BOOLEAN,
152 'Text' => self::T_TEXT,
153 'Blob' => self::T_BLOB,
154 'Timestamp' => self::T_TIMESTAMP,
155 'Float' => self::T_FLOAT,
156 'Money' => self::T_MONEY,
157 'Email' => self::T_EMAIL,
158 'Mediumblob' => self::T_MEDIUMBLOB,
be2fb01f 159 ];
067c2e09 160 }
161
d86e674f 162 /**
163 * Get the data_type for the field.
164 *
165 * @param array $fieldMetadata
166 * Metadata about the field.
167 *
168 * @return string
169 */
170 public static function getDataTypeFromFieldMetadata($fieldMetadata) {
171 if (isset($fieldMetadata['data_type'])) {
172 return $fieldMetadata['data_type'];
173 }
174 if (empty($fieldMetadata['type'])) {
175 // I would prefer to throw an e-notice but there is some,
176 // probably unnecessary logic, that only retrieves activity fields
177 // if they are 'in the profile' and probably they are not 'in'
178 // until they are added - which might lead to ? who knows!
179 return '';
180 }
181 return self::typeToString($fieldMetadata['type']);
182 }
183
7c99af4f 184 /**
bf48aa29 185 * Helper function to call escape on arrays.
7c99af4f 186 *
187 * @see escape
188 */
189 public static function escapeAll($data, $type, $abort = TRUE) {
190 foreach ($data as $key => $value) {
191 $data[$key] = CRM_Utils_Type::escape($value, $type, $abort);
192 }
193 return $data;
194 }
195
0fa4baf0
MM
196 /**
197 * Helper function to call validate on arrays
198 *
95325bb8 199 * @param mixed $data
200 * @param string $type
201 *
202 * @return mixed
203 *
204 * @throws \CRM_Core_Exception
205 *
0fa4baf0
MM
206 * @see validate
207 */
95325bb8 208 public static function validateAll($data, $type) {
0fa4baf0 209 foreach ($data as $key => $value) {
95325bb8 210 $data[$key] = CRM_Utils_Type::validate($value, $type);
0fa4baf0
MM
211 }
212 return $data;
213 }
214
6a488035 215 /**
a3379cc1 216 * Verify that a variable is of a given type, and apply a bit of processing.
6a488035 217 *
a3379cc1
AH
218 * @param mixed $data
219 * The value to be verified/escaped.
220 * @param string $type
221 * The type to verify against.
77855840 222 * @param bool $abort
ee3db087 223 * If TRUE, the operation will throw an CRM_Core_Exception on invalid data.
6a488035 224 *
a3379cc1
AH
225 * @return mixed
226 * The data, escaped if necessary.
b832662c 227 * @throws CRM_Core_Exception
6a488035
TO
228 */
229 public static function escape($data, $type, $abort = TRUE) {
230 switch ($type) {
231 case 'Integer':
232 case 'Int':
6a488035 233 case 'Positive':
ab2fa56a
MWMC
234 case 'Float':
235 case 'Money':
236 case 'Date':
237 case 'Timestamp':
238 case 'ContactReference':
239 case 'MysqlOrderByDirection':
240 $validatedData = self::validate($data, $type, $abort);
241 if (isset($validatedData)) {
242 return $validatedData;
43b6f159
CW
243 }
244 break;
245
246 // CRM-8925 for custom fields of this type
6a488035
TO
247 case 'Country':
248 case 'StateProvince':
88ccd161
CW
249 // Handle multivalued data in delimited or array format
250 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
43b6f159 251 $valid = TRUE;
88ccd161 252 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
43b6f159
CW
253 if (!CRM_Utils_Rule::positiveInteger($item)) {
254 $valid = FALSE;
9ff5f6c0
N
255 }
256 }
43b6f159 257 if ($valid) {
9ff5f6c0
N
258 return $data;
259 }
260 }
9ff5f6c0 261 elseif (CRM_Utils_Rule::positiveInteger($data)) {
43b6f159 262 return (int) $data;
6a488035
TO
263 }
264 break;
265
e7dcccf0 266 case 'File':
6a488035 267 if (CRM_Utils_Rule::positiveInteger($data)) {
43b6f159 268 return (int) $data;
6a488035
TO
269 }
270 break;
271
272 case 'Link':
273 if (CRM_Utils_Rule::url($data = trim($data))) {
274 return $data;
275 }
276 break;
277
278 case 'Boolean':
279 if (CRM_Utils_Rule::boolean($data)) {
280 return $data;
281 }
282 break;
283
6a488035
TO
284 case 'String':
285 case 'Memo':
85bdc94e 286 case 'Text':
ab2fa56a 287 return CRM_Core_DAO::escapeString(self::validate($data, $type, $abort));
6a488035 288
f19a5565 289 case 'MysqlColumnNameOrAlias':
a33b83c5
MM
290 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
291 $data = str_replace('`', '', $data);
0fa4baf0 292 $parts = explode('.', $data);
da93a1ab 293 $data = '`' . implode('`.`', $parts) . '`';
0fa4baf0 294
00f11506
MM
295 return $data;
296 }
297 break;
298
0fa4baf0
MM
299 case 'MysqlOrderBy':
300 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
301 $parts = explode(',', $data);
9d5c7f14 302
303 // The field() syntax is tricky here because it uses commas & when
304 // we separate by them we break it up. But we want to keep the clauses in order.
305 // so we just clumsily re-assemble it. Test cover exists.
306 $fieldClauseStart = NULL;
307 foreach ($parts as $index => &$part) {
308 if (substr($part, 0, 6) === 'field(') {
309 // Looking to escape a string like 'field(contribution_status_id,3,4,5) asc'
310 // to 'field(`contribution_status_id`,3,4,5) asc'
311 $fieldClauseStart = $index;
312 continue;
313 }
314 if ($fieldClauseStart !== NULL) {
315 // this is part of the list of field options. Concatenate it back on.
316 $parts[$fieldClauseStart] .= ',' . $part;
317 unset($parts[$index]);
318 if (!strstr($parts[$fieldClauseStart], ')')) {
319 // we have not reached the end of the list.
320 continue;
321 }
322 // We have the last piece of the field() clause, time to escape it.
323 $parts[$fieldClauseStart] = self::mysqlOrderByFieldFunctionCallback($parts[$fieldClauseStart]);
324 $fieldClauseStart = NULL;
325 continue;
326
327 }
328 // Normal clause.
be2fb01f 329 $part = preg_replace_callback('/^(?:(?:((?:`[\w-]{1,64}`|[\w-]{1,64}))(?:\.))?(`[\w-]{1,64}`|[\w-]{1,64})(?: (asc|desc))?)$/i', ['CRM_Utils_Type', 'mysqlOrderByCallback'], trim($part));
0fa4baf0
MM
330 }
331 return implode(', ', $parts);
00f11506
MM
332 }
333 break;
334
6a488035 335 default:
ee3db087 336 throw new CRM_Core_Exception(
64f4eebe 337 $type . " is not a recognized (camel cased) data type."
4f1da757 338 );
6a488035
TO
339 }
340
341 if ($abort) {
342 $data = htmlentities($data);
ab2fa56a 343
ee3db087 344 throw new CRM_Core_Exception("$data is not of the type $type");
6a488035
TO
345 }
346 return NULL;
347 }
348
349 /**
fe482240 350 * Verify that a variable is of a given type.
6a488035 351 *
a3379cc1
AH
352 * @param mixed $data
353 * The value to validate.
354 * @param string $type
355 * The type to validate against.
77855840 356 * @param bool $abort
a3379cc1 357 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
5e4ccea5 358 * @param string $name
a3379cc1 359 * The name of the attribute
6a488035 360 *
a3379cc1
AH
361 * @return mixed
362 * The data, escaped if necessary
5e4ccea5 363 *
364 * @throws \CRM_Core_Exception
6a488035 365 */
8a6d5abd 366 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ') {
5e4ccea5 367
be2fb01f 368 $possibleTypes = [
5e4ccea5 369 'Integer',
370 'Int',
371 'Positive',
372 'CommaSeparatedIntegers',
373 'Boolean',
374 'Float',
375 'Money',
376 'Text',
377 'String',
378 'Link',
379 'Memo',
380 'Date',
381 'Timestamp',
382 'ContactReference',
383 'MysqlColumnNameOrAlias',
384 'MysqlOrderByDirection',
385 'MysqlOrderBy',
386 'ExtensionKey',
88251439 387 'Json',
d22982f3 388 'Alphanumeric',
8a52ae34 389 'Color',
be2fb01f 390 ];
5e4ccea5 391 if (!in_array($type, $possibleTypes)) {
8a6d5abd 392 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
5e4ccea5 393 }
6a488035
TO
394 switch ($type) {
395 case 'Integer':
396 case 'Int':
397 if (CRM_Utils_Rule::integer($data)) {
2b0e7d03 398 return (int) $data;
6a488035
TO
399 }
400 break;
401
402 case 'Positive':
403 if (CRM_Utils_Rule::positiveInteger($data)) {
258570f7 404 return (int) $data;
6a488035
TO
405 }
406 break;
407
6a488035
TO
408 case 'Float':
409 case 'Money':
410 if (CRM_Utils_Rule::numeric($data)) {
411 return $data;
412 }
413 break;
414
415 case 'Text':
416 case 'String':
417 case 'Link':
418 case 'Memo':
419 return $data;
420
421 case 'Date':
6a488035
TO
422 case 'Timestamp':
423 // a null timestamp is valid
424 if (strlen(trim($data)) == 0) {
425 return trim($data);
426 }
427
428 if ((preg_match('/^\d{14}$/', $data) ||
429 preg_match('/^\d{8}$/', $data)
430 ) &&
431 CRM_Utils_Rule::mysqlDate($data)
432 ) {
433 return $data;
434 }
435 break;
436
437 case 'ContactReference':
438 // null is valid
439 if (strlen(trim($data)) == 0) {
440 return trim($data);
441 }
442
443 if (CRM_Utils_Rule::validContact($data)) {
ab2fa56a 444 return (int) $data;
6a488035
TO
445 }
446 break;
447
5d817a13
MM
448 case 'MysqlOrderByDirection':
449 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
0fa4baf0 450 return strtolower($data);
5d817a13
MM
451 }
452 break;
453
5df85a46 454 case 'ExtensionKey':
9e1d9d01 455 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
5df85a46
SL
456 return $data;
457 }
458 break;
88251439 459
03616e6d
CW
460 default:
461 $check = lcfirst($type);
462 if (CRM_Utils_Rule::$check($data)) {
8a52ae34
CW
463 return $data;
464 }
6a488035
TO
465 }
466
467 if ($abort) {
468 $data = htmlentities($data);
8a6d5abd 469 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
6a488035
TO
470 }
471
472 return NULL;
473 }
96025800 474
55438cdf
TO
475 /**
476 * Validate that a value matches a PHP type.
477 *
478 * Note that, at a micro-level, this is probably slower than using real PHP type-checking, but it doesn't seem bad.
479 * (In light benchmarking of ~1000 validations on an i3-10100, there is no obvious effect on the execution-time.)
480 * Should be fast enough for validating business entities.
481 *
482 * Example usage: 'validatePhpType(123, 'int|double');`
483 *
484 * @param mixed $value
485 * @param string|string[] $types
486 * The list of acceptable PHP types and/or classnames.
487 * Either an array or a string (with '|' delimiters).
488 * Note that 'null' is a distinct type.
489 * Ex: 'int'
490 * Ex: 'Countable|null'
491 * Ex: 'string|bool'
492 * Ex: 'string|false'
493 * @param bool $isStrict
494 * If data is likely to come from another text medium, then you may want to
495 * allow (say) numbers and string-like-numbers to be used interchangably.
496 *
497 * With $isStrict=TRUE, the string "123" does not match type "int". The int 456 does not match type "double". etc.
498 *
499 * With $isStrict=FALSE, the string "123" will match types "string", "int", and "double".
500 * @return bool
501 */
502 public static function validatePhpType($value, $types, bool $isStrict = TRUE) {
503 if (is_string($types)) {
504 $types = preg_split('/ *\| */', $types);
505 }
506
507 $checkTypeStrict = function($type, $value) {
508 static $aliases = ['integer' => 'int', 'boolean' => 'bool', 'float' => 'double', 'NULL' => 'null'];
509 switch ($type) {
510 case 'mixed':
511 return TRUE;
512
513 case 'false':
514 case 'FALSE':
515 case 'true':
516 case 'TRUE':
517 $expectBool = mb_strtolower($type) === 'true';
518 return $value === $expectBool;
519 }
520 $realType = gettype($value);
521 if (($aliases[$realType] ?? $realType) === ($aliases[$type] ?? $type)) {
522 return TRUE;
523 }
524 if ($realType === 'object' && $value instanceof $type) {
525 return TRUE;
526 }
527 return FALSE;
528 };
529 $checkTypeRelaxed = function($type, $value) use ($checkTypeStrict) {
530 switch ($type) {
531 case 'string':
532 return is_string($value) || is_int($value) || is_float($value);
533
534 case 'bool':
535 case 'boolean':
536 return is_bool($value) || CRM_Utils_Rule::integer($value);
537
538 case 'int':
539 case' integer':
540 return CRM_Utils_Rule::integer($value);
541
542 case 'float':
543 case 'double':
544 return CRM_Utils_Rule::numeric($value);
545
546 default:
547 return $checkTypeStrict($type, $value);
548 }
549 };
550 $checkType = $isStrict ? $checkTypeStrict : $checkTypeRelaxed;
551
552 foreach ($types as $type) {
553 $isTypedArray = substr($type, -2, 2) === '[]';
554 if (!$isTypedArray && $checkType($type, $value)) {
555 return TRUE;
556 }
557 if ($isTypedArray && is_array($value)) {
558 $baseType = substr($type, 0, -2);
559 foreach ($value as $vItem) {
560 if (!\CRM_Utils_Type::validatePhpType($vItem, [$baseType], $isStrict)) {
561 continue 2;
562 }
563 }
564 return TRUE;
565 }
566 }
567 return FALSE;
568 }
569
9d5c7f14 570 /**
571 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
572 *
573 * Add backticks around the field name.
574 *
575 * @param string $clause
576 *
577 * @return string
578 */
579 public static function mysqlOrderByFieldFunctionCallback($clause) {
580 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
581 }
582
0fa4baf0
MM
583 /**
584 * preg_replace_callback for MysqlOrderBy escape.
585 */
586 public static function mysqlOrderByCallback($matches) {
587 $output = '';
a33b83c5 588 $matches = str_replace('`', '', $matches);
0fa4baf0 589
a33b83c5 590 // Table name.
f19a5565 591 if (isset($matches[1]) && $matches[1]) {
a33b83c5 592 $output .= '`' . $matches[1] . '`.';
0fa4baf0
MM
593 }
594
a33b83c5 595 // Column name.
0fa4baf0 596 if (isset($matches[2]) && $matches[2]) {
a33b83c5 597 $output .= '`' . $matches[2] . '`';
0fa4baf0
MM
598 }
599
600 // Sort order.
601 if (isset($matches[3]) && $matches[3]) {
602 $output .= ' ' . $matches[3];
603 }
604
605 return $output;
606 }
607
eaecfa20 608 /**
64f4eebe 609 * Get list of available Data Types for Option Groups
eaecfa20
SL
610 *
611 * @return array
612 */
613 public static function dataTypes() {
be2fb01f 614 $types = [
d67d221a 615 'Integer',
eaecfa20 616 'String',
d67d221a
SL
617 'Date',
618 'Time',
eaecfa20 619 'Timestamp',
d67d221a 620 'Money',
eaecfa20 621 'Email',
be2fb01f 622 ];
eaecfa20
SL
623 return array_combine($types, $types);
624 }
625
b520bbf0 626 /**
627 * Get all the types that are text-like.
628 *
629 * The returned types would all legitimately be compared to '' by mysql
630 * in a query.
631 *
632 * e.g
633 * WHERE display_name = '' is valid
634 * WHERE id = '' is not and in some mysql configurations and queries
635 * could cause an error.
636 *
637 * @return array
638 */
639 public static function getTextTypes(): array {
640 return [
641 self::T_STRING,
642 self::T_ENUM,
643 self::T_TEXT,
644 self::T_LONGTEXT,
645 self::T_BLOB,
646 self::T_EMAIL,
647 self::T_URL,
648 self::T_MEDIUMBLOB,
649 ];
650 }
651
6a488035 652}