Merge pull request #12162 from seamuslee001/group_query_count
[civicrm-core.git] / CRM / Utils / Type.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
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-2018
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 * Gets the string representation for a data type.
72 *
73 * @param int $type
74 * Integer number identifying the data type.
75 *
76 * @return string
77 * String identifying the data type, e.g. 'Int' or 'String'.
78 */
79 public static function typeToString($type) {
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.
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
116 // CRM-10404
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
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
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
190 /**
191 * Helper function to call escape on arrays.
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
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
214 /**
215 * Verify that a variable is of a given type, and apply a bit of processing.
216 *
217 * @param mixed $data
218 * The value to be verified/escaped.
219 * @param string $type
220 * The type to verify against.
221 * @param bool $abort
222 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
223 *
224 * @return mixed
225 * The data, escaped if necessary.
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)) {
232 return (int) $data;
233 }
234 break;
235
236 case 'Positive':
237 if (CRM_Utils_Rule::positiveInteger($data)) {
238 return (int) $data;
239 }
240 break;
241
242 // CRM-8925 for custom fields of this type
243 case 'Country':
244 case 'StateProvince':
245 // Handle multivalued data in delimited or array format
246 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
247 $valid = TRUE;
248 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
249 if (!CRM_Utils_Rule::positiveInteger($item)) {
250 $valid = FALSE;
251 }
252 }
253 if ($valid) {
254 return $data;
255 }
256 }
257 elseif (CRM_Utils_Rule::positiveInteger($data)) {
258 return (int) $data;
259 }
260 break;
261
262 case 'File':
263 if (CRM_Utils_Rule::positiveInteger($data)) {
264 return (int) $data;
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':
289 case 'Text':
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)) {
314 return (int) $data;
315 }
316 break;
317
318 case 'MysqlColumnNameOrAlias':
319 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
320 $data = str_replace('`', '', $data);
321 $parts = explode('.', $data);
322 $data = '`' . implode('`.`', $parts) . '`';
323
324 return $data;
325 }
326 break;
327
328 case 'MysqlOrderByDirection':
329 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
330 return strtolower($data);
331 }
332 break;
333
334 case 'MysqlOrderBy':
335 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
336 $parts = explode(',', $data);
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.
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));
365 }
366 return implode(', ', $parts);
367 }
368 break;
369
370 default:
371 CRM_Core_Error::fatal(
372 $type . " is not a recognised (camel cased) data type."
373 );
374 break;
375 }
376
377 // @todo Use exceptions instead of CRM_Core_Error::fatal().
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 /**
386 * Verify that a variable is of a given type.
387 *
388 * @param mixed $data
389 * The value to validate.
390 * @param string $type
391 * The type to validate against.
392 * @param bool $abort
393 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
394 * @param string $name
395 * The name of the attribute
396 * @param bool $isThrowException
397 * Should an exception be thrown rather than a using a deprecated fatal error.
398 *
399 * @return mixed
400 * The data, escaped if necessary
401 *
402 * @throws \CRM_Core_Exception
403 */
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',
425 'Json',
426 );
427 if (!in_array($type, $possibleTypes)) {
428 if ($isThrowException) {
429 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
430 }
431 CRM_Core_Error::fatal(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
432 }
433 switch ($type) {
434 case 'Integer':
435 case 'Int':
436 if (CRM_Utils_Rule::integer($data)) {
437 return (int) $data;
438 }
439 break;
440
441 case 'Positive':
442 if (CRM_Utils_Rule::positiveInteger($data)) {
443 return (int) $data;
444 }
445 break;
446
447 case 'CommaSeparatedIntegers':
448 if (CRM_Utils_Rule::commaSeparatedIntegers($data)) {
449 return $data;
450 }
451 break;
452
453 case 'Boolean':
454 if (CRM_Utils_Rule::boolean($data)) {
455 return $data;
456 }
457 break;
458
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
511 case 'MysqlColumnNameOrAlias':
512 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
513 return $data;
514 }
515 break;
516
517 case 'MysqlOrderByDirection':
518 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
519 return strtolower($data);
520 }
521 break;
522
523 case 'MysqlOrderBy':
524 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
525 return $data;
526 }
527 break;
528
529 case 'ExtensionKey':
530 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
531 return $data;
532 }
533 break;
534
535 case 'Json':
536 if (CRM_Utils_Rule::json($data)) {
537 return $data;
538 }
539 break;
540 }
541
542 if ($abort) {
543 $data = htmlentities($data);
544 if ($isThrowException) {
545 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
546 }
547 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
548 }
549
550 return NULL;
551 }
552
553 /**
554 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
555 *
556 * Add backticks around the field name.
557 *
558 * @param string $clause
559 *
560 * @return string
561 */
562 public static function mysqlOrderByFieldFunctionCallback($clause) {
563 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
564 }
565
566 /**
567 * preg_replace_callback for MysqlOrderBy escape.
568 */
569 public static function mysqlOrderByCallback($matches) {
570 $output = '';
571 $matches = str_replace('`', '', $matches);
572
573 // Table name.
574 if (isset($matches[1]) && $matches[1]) {
575 $output .= '`' . $matches[1] . '`.';
576 }
577
578 // Column name.
579 if (isset($matches[2]) && $matches[2]) {
580 $output .= '`' . $matches[2] . '`';
581 }
582
583 // Sort order.
584 if (isset($matches[3]) && $matches[3]) {
585 $output .= ' ' . $matches[3];
586 }
587
588 return $output;
589 }
590
591 /**
592 * Get list of avaliable Data Types for Option Groups
593 *
594 * @return array
595 */
596 public static function dataTypes() {
597 $types = array(
598 'Integer',
599 'String',
600 'Date',
601 'Time',
602 'Timestamp',
603 'Money',
604 'Email',
605 );
606 return array_combine($types, $types);
607 }
608
609 }