Merge pull request #13398 from pradpnayak/DedupeBreak
[civicrm-core.git] / CRM / Utils / Type.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33 class CRM_Utils_Type {
34 const
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,
50 T_MEDIUMBLOB = 16384;
51
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.
56 const
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,
67 FORTYFIVE = 45,
68 HUGE = 45;
69
70 /**
71 * Maximum size of a MySQL BLOB or TEXT column in bytes.
72 */
73 const BLOB_SIZE = 65535;
74
75 /**
76 * Maximum value of a MySQL signed INT column.
77 */
78 const INT_MAX = 2147483647;
79
80 /**
81 * Gets the string representation for a data type.
82 *
83 * @param int $type
84 * Integer number identifying the data type.
85 *
86 * @return string
87 * String identifying the data type, e.g. 'Int' or 'String'.
88 */
89 public static function typeToString($type) {
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.
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
126 // CRM-10404
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
156 /**
157 * @return array
158 * An array of type in the form 'type name' => 'int representing type'
159 */
160 public static function getValidTypes() {
161 return array(
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,
175 );
176 }
177
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
200 /**
201 * Helper function to call escape on arrays.
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
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
224 /**
225 * Verify that a variable is of a given type, and apply a bit of processing.
226 *
227 * @param mixed $data
228 * The value to be verified/escaped.
229 * @param string $type
230 * The type to verify against.
231 * @param bool $abort
232 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
233 *
234 * @return mixed
235 * The data, escaped if necessary.
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)) {
242 return (int) $data;
243 }
244 break;
245
246 case 'Positive':
247 if (CRM_Utils_Rule::positiveInteger($data)) {
248 return (int) $data;
249 }
250 break;
251
252 // CRM-8925 for custom fields of this type
253 case 'Country':
254 case 'StateProvince':
255 // Handle multivalued data in delimited or array format
256 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
257 $valid = TRUE;
258 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
259 if (!CRM_Utils_Rule::positiveInteger($item)) {
260 $valid = FALSE;
261 }
262 }
263 if ($valid) {
264 return $data;
265 }
266 }
267 elseif (CRM_Utils_Rule::positiveInteger($data)) {
268 return (int) $data;
269 }
270 break;
271
272 case 'File':
273 if (CRM_Utils_Rule::positiveInteger($data)) {
274 return (int) $data;
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':
299 case 'Text':
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)) {
324 return (int) $data;
325 }
326 break;
327
328 case 'MysqlColumnNameOrAlias':
329 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
330 $data = str_replace('`', '', $data);
331 $parts = explode('.', $data);
332 $data = '`' . implode('`.`', $parts) . '`';
333
334 return $data;
335 }
336 break;
337
338 case 'MysqlOrderByDirection':
339 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
340 return strtolower($data);
341 }
342 break;
343
344 case 'MysqlOrderBy':
345 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
346 $parts = explode(',', $data);
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.
374 $part = preg_replace_callback('/^(?:(?:((?:`[\w-]{1,64}`|[\w-]{1,64}))(?:\.))?(`[\w-]{1,64}`|[\w-]{1,64})(?: (asc|desc))?)$/i', array('CRM_Utils_Type', 'mysqlOrderByCallback'), trim($part));
375 }
376 return implode(', ', $parts);
377 }
378 break;
379
380 default:
381 CRM_Core_Error::fatal(
382 $type . " is not a recognised (camel cased) data type."
383 );
384 break;
385 }
386
387 // @todo Use exceptions instead of CRM_Core_Error::fatal().
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 /**
396 * Verify that a variable is of a given type.
397 *
398 * @param mixed $data
399 * The value to validate.
400 * @param string $type
401 * The type to validate against.
402 * @param bool $abort
403 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
404 * @param string $name
405 * The name of the attribute
406 * @param bool $isThrowException
407 * Should an exception be thrown rather than a using a deprecated fatal error.
408 *
409 * @return mixed
410 * The data, escaped if necessary
411 *
412 * @throws \CRM_Core_Exception
413 */
414 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ', $isThrowException = FALSE) {
415
416 $possibleTypes = array(
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',
435 'Json',
436 'Alphanumeric',
437 );
438 if (!in_array($type, $possibleTypes)) {
439 if ($isThrowException) {
440 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
441 }
442 CRM_Core_Error::fatal(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
443 }
444 switch ($type) {
445 case 'Integer':
446 case 'Int':
447 if (CRM_Utils_Rule::integer($data)) {
448 return (int) $data;
449 }
450 break;
451
452 case 'Positive':
453 if (CRM_Utils_Rule::positiveInteger($data)) {
454 return (int) $data;
455 }
456 break;
457
458 case 'CommaSeparatedIntegers':
459 if (CRM_Utils_Rule::commaSeparatedIntegers($data)) {
460 return $data;
461 }
462 break;
463
464 case 'Boolean':
465 if (CRM_Utils_Rule::boolean($data)) {
466 return $data;
467 }
468 break;
469
470 case 'Float':
471 case 'Money':
472 if (CRM_Utils_Rule::numeric($data)) {
473 return $data;
474 }
475 break;
476
477 case 'Text':
478 case 'String':
479 case 'Link':
480 case 'Memo':
481 return $data;
482
483 case 'Date':
484 // a null date is valid
485 if (strlen(trim($data)) == 0) {
486 return trim($data);
487 }
488
489 if (preg_match('/^\d{8}$/', $data) &&
490 CRM_Utils_Rule::mysqlDate($data)
491 ) {
492 return $data;
493 }
494 break;
495
496 case 'Timestamp':
497 // a null timestamp is valid
498 if (strlen(trim($data)) == 0) {
499 return trim($data);
500 }
501
502 if ((preg_match('/^\d{14}$/', $data) ||
503 preg_match('/^\d{8}$/', $data)
504 ) &&
505 CRM_Utils_Rule::mysqlDate($data)
506 ) {
507 return $data;
508 }
509 break;
510
511 case 'ContactReference':
512 // null is valid
513 if (strlen(trim($data)) == 0) {
514 return trim($data);
515 }
516
517 if (CRM_Utils_Rule::validContact($data)) {
518 return $data;
519 }
520 break;
521
522 case 'MysqlColumnNameOrAlias':
523 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
524 return $data;
525 }
526 break;
527
528 case 'MysqlOrderByDirection':
529 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
530 return strtolower($data);
531 }
532 break;
533
534 case 'MysqlOrderBy':
535 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
536 return $data;
537 }
538 break;
539
540 case 'ExtensionKey':
541 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
542 return $data;
543 }
544 break;
545
546 case 'Json':
547 if (CRM_Utils_Rule::json($data)) {
548 return $data;
549 }
550 break;
551
552 case 'Alphanumeric':
553 if (CRM_Utils_Rule::alphanumeric($data)) {
554 return $data;
555 }
556 break;
557 }
558
559 if ($abort) {
560 $data = htmlentities($data);
561 if ($isThrowException) {
562 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
563 }
564 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
565 }
566
567 return NULL;
568 }
569
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
583 /**
584 * preg_replace_callback for MysqlOrderBy escape.
585 */
586 public static function mysqlOrderByCallback($matches) {
587 $output = '';
588 $matches = str_replace('`', '', $matches);
589
590 // Table name.
591 if (isset($matches[1]) && $matches[1]) {
592 $output .= '`' . $matches[1] . '`.';
593 }
594
595 // Column name.
596 if (isset($matches[2]) && $matches[2]) {
597 $output .= '`' . $matches[2] . '`';
598 }
599
600 // Sort order.
601 if (isset($matches[3]) && $matches[3]) {
602 $output .= ' ' . $matches[3];
603 }
604
605 return $output;
606 }
607
608 /**
609 * Get list of avaliable Data Types for Option Groups
610 *
611 * @return array
612 */
613 public static function dataTypes() {
614 $types = array(
615 'Integer',
616 'String',
617 'Date',
618 'Time',
619 'Timestamp',
620 'Money',
621 'Email',
622 );
623 return array_combine($types, $types);
624 }
625
626 }