Merge pull request #15307 from seamuslee001/dev_core_1249
[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 *
199 * @see validate
200 */
201 public static function validateAll($data, $type, $abort = TRUE) {
202 foreach ($data as $key => $value) {
203 $data[$key] = CRM_Utils_Type::validate($value, $type, $abort);
204 }
205 return $data;
206 }
207
6a488035 208 /**
a3379cc1 209 * Verify that a variable is of a given type, and apply a bit of processing.
6a488035 210 *
a3379cc1
AH
211 * @param mixed $data
212 * The value to be verified/escaped.
213 * @param string $type
214 * The type to verify against.
77855840 215 * @param bool $abort
ee3db087 216 * If TRUE, the operation will throw an CRM_Core_Exception on invalid data.
6a488035 217 *
a3379cc1
AH
218 * @return mixed
219 * The data, escaped if necessary.
b832662c 220 * @throws CRM_Core_Exception
6a488035
TO
221 */
222 public static function escape($data, $type, $abort = TRUE) {
223 switch ($type) {
224 case 'Integer':
225 case 'Int':
6a488035 226 case 'Positive':
ab2fa56a
MWMC
227 case 'Float':
228 case 'Money':
229 case 'Date':
230 case 'Timestamp':
231 case 'ContactReference':
232 case 'MysqlOrderByDirection':
233 $validatedData = self::validate($data, $type, $abort);
234 if (isset($validatedData)) {
235 return $validatedData;
43b6f159
CW
236 }
237 break;
238
239 // CRM-8925 for custom fields of this type
6a488035
TO
240 case 'Country':
241 case 'StateProvince':
88ccd161
CW
242 // Handle multivalued data in delimited or array format
243 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
43b6f159 244 $valid = TRUE;
88ccd161 245 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
43b6f159
CW
246 if (!CRM_Utils_Rule::positiveInteger($item)) {
247 $valid = FALSE;
9ff5f6c0
N
248 }
249 }
43b6f159 250 if ($valid) {
9ff5f6c0
N
251 return $data;
252 }
253 }
9ff5f6c0 254 elseif (CRM_Utils_Rule::positiveInteger($data)) {
43b6f159 255 return (int) $data;
6a488035
TO
256 }
257 break;
258
e7dcccf0 259 case 'File':
6a488035 260 if (CRM_Utils_Rule::positiveInteger($data)) {
43b6f159 261 return (int) $data;
6a488035
TO
262 }
263 break;
264
265 case 'Link':
266 if (CRM_Utils_Rule::url($data = trim($data))) {
267 return $data;
268 }
269 break;
270
271 case 'Boolean':
272 if (CRM_Utils_Rule::boolean($data)) {
273 return $data;
274 }
275 break;
276
6a488035
TO
277 case 'String':
278 case 'Memo':
85bdc94e 279 case 'Text':
ab2fa56a 280 return CRM_Core_DAO::escapeString(self::validate($data, $type, $abort));
6a488035 281
f19a5565 282 case 'MysqlColumnNameOrAlias':
a33b83c5
MM
283 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
284 $data = str_replace('`', '', $data);
0fa4baf0 285 $parts = explode('.', $data);
da93a1ab 286 $data = '`' . implode('`.`', $parts) . '`';
0fa4baf0 287
00f11506
MM
288 return $data;
289 }
290 break;
291
0fa4baf0
MM
292 case 'MysqlOrderBy':
293 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
294 $parts = explode(',', $data);
9d5c7f14 295
296 // The field() syntax is tricky here because it uses commas & when
297 // we separate by them we break it up. But we want to keep the clauses in order.
298 // so we just clumsily re-assemble it. Test cover exists.
299 $fieldClauseStart = NULL;
300 foreach ($parts as $index => &$part) {
301 if (substr($part, 0, 6) === 'field(') {
302 // Looking to escape a string like 'field(contribution_status_id,3,4,5) asc'
303 // to 'field(`contribution_status_id`,3,4,5) asc'
304 $fieldClauseStart = $index;
305 continue;
306 }
307 if ($fieldClauseStart !== NULL) {
308 // this is part of the list of field options. Concatenate it back on.
309 $parts[$fieldClauseStart] .= ',' . $part;
310 unset($parts[$index]);
311 if (!strstr($parts[$fieldClauseStart], ')')) {
312 // we have not reached the end of the list.
313 continue;
314 }
315 // We have the last piece of the field() clause, time to escape it.
316 $parts[$fieldClauseStart] = self::mysqlOrderByFieldFunctionCallback($parts[$fieldClauseStart]);
317 $fieldClauseStart = NULL;
318 continue;
319
320 }
321 // Normal clause.
be2fb01f 322 $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
323 }
324 return implode(', ', $parts);
00f11506
MM
325 }
326 break;
327
6a488035 328 default:
ee3db087 329 throw new CRM_Core_Exception(
4f1da757
EM
330 $type . " is not a recognised (camel cased) data type."
331 );
6a488035
TO
332 }
333
d6528d9c 334 // @todo Use exceptions instead of CRM_Core_Error::fatal().
6a488035
TO
335 if ($abort) {
336 $data = htmlentities($data);
ab2fa56a 337
ee3db087 338 throw new CRM_Core_Exception("$data is not of the type $type");
6a488035
TO
339 }
340 return NULL;
341 }
342
343 /**
fe482240 344 * Verify that a variable is of a given type.
6a488035 345 *
a3379cc1
AH
346 * @param mixed $data
347 * The value to validate.
348 * @param string $type
349 * The type to validate against.
77855840 350 * @param bool $abort
a3379cc1 351 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
5e4ccea5 352 * @param string $name
a3379cc1 353 * The name of the attribute
5e4ccea5 354 * @param bool $isThrowException
355 * Should an exception be thrown rather than a using a deprecated fatal error.
6a488035 356 *
a3379cc1
AH
357 * @return mixed
358 * The data, escaped if necessary
5e4ccea5 359 *
360 * @throws \CRM_Core_Exception
6a488035 361 */
d8bf477d 362 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ', $isThrowException = TRUE) {
5e4ccea5 363
be2fb01f 364 $possibleTypes = [
5e4ccea5 365 'Integer',
366 'Int',
367 'Positive',
368 'CommaSeparatedIntegers',
369 'Boolean',
370 'Float',
371 'Money',
372 'Text',
373 'String',
374 'Link',
375 'Memo',
376 'Date',
377 'Timestamp',
378 'ContactReference',
379 'MysqlColumnNameOrAlias',
380 'MysqlOrderByDirection',
381 'MysqlOrderBy',
382 'ExtensionKey',
88251439 383 'Json',
d22982f3 384 'Alphanumeric',
8a52ae34 385 'Color',
be2fb01f 386 ];
5e4ccea5 387 if (!in_array($type, $possibleTypes)) {
388 if ($isThrowException) {
389 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
390 }
391 CRM_Core_Error::fatal(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
392 }
6a488035
TO
393 switch ($type) {
394 case 'Integer':
395 case 'Int':
396 if (CRM_Utils_Rule::integer($data)) {
2b0e7d03 397 return (int) $data;
6a488035
TO
398 }
399 break;
400
401 case 'Positive':
402 if (CRM_Utils_Rule::positiveInteger($data)) {
258570f7 403 return (int) $data;
6a488035
TO
404 }
405 break;
406
6a488035
TO
407 case 'Float':
408 case 'Money':
409 if (CRM_Utils_Rule::numeric($data)) {
410 return $data;
411 }
412 break;
413
414 case 'Text':
415 case 'String':
416 case 'Link':
417 case 'Memo':
418 return $data;
419
420 case 'Date':
6a488035
TO
421 case 'Timestamp':
422 // a null timestamp is valid
423 if (strlen(trim($data)) == 0) {
424 return trim($data);
425 }
426
427 if ((preg_match('/^\d{14}$/', $data) ||
428 preg_match('/^\d{8}$/', $data)
429 ) &&
430 CRM_Utils_Rule::mysqlDate($data)
431 ) {
432 return $data;
433 }
434 break;
435
436 case 'ContactReference':
437 // null is valid
438 if (strlen(trim($data)) == 0) {
439 return trim($data);
440 }
441
442 if (CRM_Utils_Rule::validContact($data)) {
ab2fa56a 443 return (int) $data;
6a488035
TO
444 }
445 break;
446
5d817a13
MM
447 case 'MysqlOrderByDirection':
448 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
0fa4baf0 449 return strtolower($data);
5d817a13
MM
450 }
451 break;
452
5df85a46 453 case 'ExtensionKey':
9e1d9d01 454 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
5df85a46
SL
455 return $data;
456 }
457 break;
88251439 458
03616e6d
CW
459 default:
460 $check = lcfirst($type);
461 if (CRM_Utils_Rule::$check($data)) {
8a52ae34
CW
462 return $data;
463 }
6a488035
TO
464 }
465
466 if ($abort) {
467 $data = htmlentities($data);
5e4ccea5 468 if ($isThrowException) {
469 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
470 }
6a488035
TO
471 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
472 }
473
474 return NULL;
475 }
96025800 476
9d5c7f14 477 /**
478 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
479 *
480 * Add backticks around the field name.
481 *
482 * @param string $clause
483 *
484 * @return string
485 */
486 public static function mysqlOrderByFieldFunctionCallback($clause) {
487 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
488 }
489
0fa4baf0
MM
490 /**
491 * preg_replace_callback for MysqlOrderBy escape.
492 */
493 public static function mysqlOrderByCallback($matches) {
494 $output = '';
a33b83c5 495 $matches = str_replace('`', '', $matches);
0fa4baf0 496
a33b83c5 497 // Table name.
f19a5565 498 if (isset($matches[1]) && $matches[1]) {
a33b83c5 499 $output .= '`' . $matches[1] . '`.';
0fa4baf0
MM
500 }
501
a33b83c5 502 // Column name.
0fa4baf0 503 if (isset($matches[2]) && $matches[2]) {
a33b83c5 504 $output .= '`' . $matches[2] . '`';
0fa4baf0
MM
505 }
506
507 // Sort order.
508 if (isset($matches[3]) && $matches[3]) {
509 $output .= ' ' . $matches[3];
510 }
511
512 return $output;
513 }
514
eaecfa20 515 /**
067c2e09 516 * Get list of avaliable Data Types for Option Groups
eaecfa20
SL
517 *
518 * @return array
519 */
520 public static function dataTypes() {
be2fb01f 521 $types = [
d67d221a 522 'Integer',
eaecfa20 523 'String',
d67d221a
SL
524 'Date',
525 'Time',
eaecfa20 526 'Timestamp',
d67d221a 527 'Money',
eaecfa20 528 'Email',
be2fb01f 529 ];
eaecfa20
SL
530 return array_combine($types, $types);
531 }
532
6a488035 533}