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