Merge pull request #17487 from eileenmcnaughton/date
[civicrm-core.git] / CRM / Utils / Type.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Utils_Type {
18 const
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,
34 T_MEDIUMBLOB = 16384;
35
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.
40 const
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,
51 FORTYFIVE = 45,
52 HUGE = 45;
53
54 /**
55 * Maximum size of a MySQL BLOB or TEXT column in bytes.
56 */
57 const BLOB_SIZE = 65535;
58
59 /**
60 * Maximum value of a MySQL signed INT column.
61 */
62 const INT_MAX = 2147483647;
63
64 /**
65 * Gets the string representation for a data type.
66 *
67 * @param int $type
68 * Integer number identifying the data type.
69 *
70 * @return string
71 * String identifying the data type, e.g. 'Int' or 'String'.
72 */
73 public static function typeToString($type) {
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.
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
110 // CRM-10404
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
140 /**
141 * @return array
142 * An array of type in the form 'type name' => 'int representing type'
143 */
144 public static function getValidTypes() {
145 return [
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,
159 ];
160 }
161
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
184 /**
185 * Helper function to call escape on arrays.
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
196 /**
197 * Helper function to call validate on arrays
198 *
199 * @param mixed $data
200 * @param string $type
201 *
202 * @return mixed
203 *
204 * @throws \CRM_Core_Exception
205 *
206 * @see validate
207 */
208 public static function validateAll($data, $type) {
209 foreach ($data as $key => $value) {
210 $data[$key] = CRM_Utils_Type::validate($value, $type);
211 }
212 return $data;
213 }
214
215 /**
216 * Verify that a variable is of a given type, and apply a bit of processing.
217 *
218 * @param mixed $data
219 * The value to be verified/escaped.
220 * @param string $type
221 * The type to verify against.
222 * @param bool $abort
223 * If TRUE, the operation will throw an CRM_Core_Exception on invalid data.
224 *
225 * @return mixed
226 * The data, escaped if necessary.
227 * @throws CRM_Core_Exception
228 */
229 public static function escape($data, $type, $abort = TRUE) {
230 switch ($type) {
231 case 'Integer':
232 case 'Int':
233 case 'Positive':
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;
243 }
244 break;
245
246 // CRM-8925 for custom fields of this type
247 case 'Country':
248 case 'StateProvince':
249 // Handle multivalued data in delimited or array format
250 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
251 $valid = TRUE;
252 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
253 if (!CRM_Utils_Rule::positiveInteger($item)) {
254 $valid = FALSE;
255 }
256 }
257 if ($valid) {
258 return $data;
259 }
260 }
261 elseif (CRM_Utils_Rule::positiveInteger($data)) {
262 return (int) $data;
263 }
264 break;
265
266 case 'File':
267 if (CRM_Utils_Rule::positiveInteger($data)) {
268 return (int) $data;
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
284 case 'String':
285 case 'Memo':
286 case 'Text':
287 return CRM_Core_DAO::escapeString(self::validate($data, $type, $abort));
288
289 case 'MysqlColumnNameOrAlias':
290 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
291 $data = str_replace('`', '', $data);
292 $parts = explode('.', $data);
293 $data = '`' . implode('`.`', $parts) . '`';
294
295 return $data;
296 }
297 break;
298
299 case 'MysqlOrderBy':
300 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
301 $parts = explode(',', $data);
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.
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));
330 }
331 return implode(', ', $parts);
332 }
333 break;
334
335 default:
336 throw new CRM_Core_Exception(
337 $type . " is not a recognised (camel cased) data type."
338 );
339 }
340
341 // @todo Use exceptions instead of CRM_Core_Error::fatal().
342 if ($abort) {
343 $data = htmlentities($data);
344
345 throw new CRM_Core_Exception("$data is not of the type $type");
346 }
347 return NULL;
348 }
349
350 /**
351 * Verify that a variable is of a given type.
352 *
353 * @param mixed $data
354 * The value to validate.
355 * @param string $type
356 * The type to validate against.
357 * @param bool $abort
358 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
359 * @param string $name
360 * The name of the attribute
361 *
362 * @return mixed
363 * The data, escaped if necessary
364 *
365 * @throws \CRM_Core_Exception
366 */
367 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ') {
368
369 $possibleTypes = [
370 'Integer',
371 'Int',
372 'Positive',
373 'CommaSeparatedIntegers',
374 'Boolean',
375 'Float',
376 'Money',
377 'Text',
378 'String',
379 'Link',
380 'Memo',
381 'Date',
382 'Timestamp',
383 'ContactReference',
384 'MysqlColumnNameOrAlias',
385 'MysqlOrderByDirection',
386 'MysqlOrderBy',
387 'ExtensionKey',
388 'Json',
389 'Alphanumeric',
390 'Color',
391 ];
392 if (!in_array($type, $possibleTypes)) {
393 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
394 }
395 switch ($type) {
396 case 'Integer':
397 case 'Int':
398 if (CRM_Utils_Rule::integer($data)) {
399 return (int) $data;
400 }
401 break;
402
403 case 'Positive':
404 if (CRM_Utils_Rule::positiveInteger($data)) {
405 return (int) $data;
406 }
407 break;
408
409 case 'Float':
410 case 'Money':
411 if (CRM_Utils_Rule::numeric($data)) {
412 return $data;
413 }
414 break;
415
416 case 'Text':
417 case 'String':
418 case 'Link':
419 case 'Memo':
420 return $data;
421
422 case 'Date':
423 case 'Timestamp':
424 // a null timestamp is valid
425 if (strlen(trim($data)) == 0) {
426 return trim($data);
427 }
428
429 if ((preg_match('/^\d{14}$/', $data) ||
430 preg_match('/^\d{8}$/', $data)
431 ) &&
432 CRM_Utils_Rule::mysqlDate($data)
433 ) {
434 return $data;
435 }
436 break;
437
438 case 'ContactReference':
439 // null is valid
440 if (strlen(trim($data)) == 0) {
441 return trim($data);
442 }
443
444 if (CRM_Utils_Rule::validContact($data)) {
445 return (int) $data;
446 }
447 break;
448
449 case 'MysqlOrderByDirection':
450 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
451 return strtolower($data);
452 }
453 break;
454
455 case 'ExtensionKey':
456 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
457 return $data;
458 }
459 break;
460
461 default:
462 $check = lcfirst($type);
463 if (CRM_Utils_Rule::$check($data)) {
464 return $data;
465 }
466 }
467
468 if ($abort) {
469 $data = htmlentities($data);
470 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
471 }
472
473 return NULL;
474 }
475
476 /**
477 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
478 *
479 * Add backticks around the field name.
480 *
481 * @param string $clause
482 *
483 * @return string
484 */
485 public static function mysqlOrderByFieldFunctionCallback($clause) {
486 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
487 }
488
489 /**
490 * preg_replace_callback for MysqlOrderBy escape.
491 */
492 public static function mysqlOrderByCallback($matches) {
493 $output = '';
494 $matches = str_replace('`', '', $matches);
495
496 // Table name.
497 if (isset($matches[1]) && $matches[1]) {
498 $output .= '`' . $matches[1] . '`.';
499 }
500
501 // Column name.
502 if (isset($matches[2]) && $matches[2]) {
503 $output .= '`' . $matches[2] . '`';
504 }
505
506 // Sort order.
507 if (isset($matches[3]) && $matches[3]) {
508 $output .= ' ' . $matches[3];
509 }
510
511 return $output;
512 }
513
514 /**
515 * Get list of avaliable Data Types for Option Groups
516 *
517 * @return array
518 */
519 public static function dataTypes() {
520 $types = [
521 'Integer',
522 'String',
523 'Date',
524 'Time',
525 'Timestamp',
526 'Money',
527 'Email',
528 ];
529 return array_combine($types, $types);
530 }
531
532 }