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