Configurable menubar color
[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 [
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', ['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 = [
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 'Color',
438 ];
439 if (!in_array($type, $possibleTypes)) {
440 if ($isThrowException) {
441 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
442 }
443 CRM_Core_Error::fatal(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
444 }
445 switch ($type) {
446 case 'Integer':
447 case 'Int':
448 if (CRM_Utils_Rule::integer($data)) {
449 return (int) $data;
450 }
451 break;
452
453 case 'Positive':
454 if (CRM_Utils_Rule::positiveInteger($data)) {
455 return (int) $data;
456 }
457 break;
458
459 case 'CommaSeparatedIntegers':
460 if (CRM_Utils_Rule::commaSeparatedIntegers($data)) {
461 return $data;
462 }
463 break;
464
465 case 'Boolean':
466 if (CRM_Utils_Rule::boolean($data)) {
467 return $data;
468 }
469 break;
470
471 case 'Float':
472 case 'Money':
473 if (CRM_Utils_Rule::numeric($data)) {
474 return $data;
475 }
476 break;
477
478 case 'Text':
479 case 'String':
480 case 'Link':
481 case 'Memo':
482 return $data;
483
484 case 'Date':
485 // a null date is valid
486 if (strlen(trim($data)) == 0) {
487 return trim($data);
488 }
489
490 if (preg_match('/^\d{8}$/', $data) &&
491 CRM_Utils_Rule::mysqlDate($data)
492 ) {
493 return $data;
494 }
495 break;
496
497 case 'Timestamp':
498 // a null timestamp is valid
499 if (strlen(trim($data)) == 0) {
500 return trim($data);
501 }
502
503 if ((preg_match('/^\d{14}$/', $data) ||
504 preg_match('/^\d{8}$/', $data)
505 ) &&
506 CRM_Utils_Rule::mysqlDate($data)
507 ) {
508 return $data;
509 }
510 break;
511
512 case 'ContactReference':
513 // null is valid
514 if (strlen(trim($data)) == 0) {
515 return trim($data);
516 }
517
518 if (CRM_Utils_Rule::validContact($data)) {
519 return $data;
520 }
521 break;
522
523 case 'MysqlColumnNameOrAlias':
524 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
525 return $data;
526 }
527 break;
528
529 case 'MysqlOrderByDirection':
530 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
531 return strtolower($data);
532 }
533 break;
534
535 case 'MysqlOrderBy':
536 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
537 return $data;
538 }
539 break;
540
541 case 'ExtensionKey':
542 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
543 return $data;
544 }
545 break;
546
547 case 'Json':
548 if (CRM_Utils_Rule::json($data)) {
549 return $data;
550 }
551 break;
552
553 case 'Alphanumeric':
554 if (CRM_Utils_Rule::alphanumeric($data)) {
555 return $data;
556 }
557 break;
558
559 case 'Color':
560 if (CRM_Utils_Rule::color($data)) {
561 return $data;
562 }
563 break;
564 }
565
566 if ($abort) {
567 $data = htmlentities($data);
568 if ($isThrowException) {
569 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
570 }
571 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
572 }
573
574 return NULL;
575 }
576
577 /**
578 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
579 *
580 * Add backticks around the field name.
581 *
582 * @param string $clause
583 *
584 * @return string
585 */
586 public static function mysqlOrderByFieldFunctionCallback($clause) {
587 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
588 }
589
590 /**
591 * preg_replace_callback for MysqlOrderBy escape.
592 */
593 public static function mysqlOrderByCallback($matches) {
594 $output = '';
595 $matches = str_replace('`', '', $matches);
596
597 // Table name.
598 if (isset($matches[1]) && $matches[1]) {
599 $output .= '`' . $matches[1] . '`.';
600 }
601
602 // Column name.
603 if (isset($matches[2]) && $matches[2]) {
604 $output .= '`' . $matches[2] . '`';
605 }
606
607 // Sort order.
608 if (isset($matches[3]) && $matches[3]) {
609 $output .= ' ' . $matches[3];
610 }
611
612 return $output;
613 }
614
615 /**
616 * Get list of avaliable Data Types for Option Groups
617 *
618 * @return array
619 */
620 public static function dataTypes() {
621 $types = [
622 'Integer',
623 'String',
624 'Date',
625 'Time',
626 'Timestamp',
627 'Money',
628 'Email',
629 ];
630 return array_combine($types, $types);
631 }
632
633 }