Drupal#38 - allow creating absolute URLs from CLI in D8
[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 '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 'MysqlOrderByDirection':
512 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
513 return strtolower($data);
514 }
515 break;
516
517 case 'ExtensionKey':
518 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
519 return $data;
520 }
521 break;
522
523 default:
524 $check = lcfirst($type);
525 if (CRM_Utils_Rule::$check($data)) {
526 return $data;
527 }
528 }
529
530 if ($abort) {
531 $data = htmlentities($data);
532 if ($isThrowException) {
533 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
534 }
535 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
536 }
537
538 return NULL;
539 }
540
541 /**
542 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
543 *
544 * Add backticks around the field name.
545 *
546 * @param string $clause
547 *
548 * @return string
549 */
550 public static function mysqlOrderByFieldFunctionCallback($clause) {
551 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
552 }
553
554 /**
555 * preg_replace_callback for MysqlOrderBy escape.
556 */
557 public static function mysqlOrderByCallback($matches) {
558 $output = '';
559 $matches = str_replace('`', '', $matches);
560
561 // Table name.
562 if (isset($matches[1]) && $matches[1]) {
563 $output .= '`' . $matches[1] . '`.';
564 }
565
566 // Column name.
567 if (isset($matches[2]) && $matches[2]) {
568 $output .= '`' . $matches[2] . '`';
569 }
570
571 // Sort order.
572 if (isset($matches[3]) && $matches[3]) {
573 $output .= ' ' . $matches[3];
574 }
575
576 return $output;
577 }
578
579 /**
580 * Get list of avaliable Data Types for Option Groups
581 *
582 * @return array
583 */
584 public static function dataTypes() {
585 $types = [
586 'Integer',
587 'String',
588 'Date',
589 'Time',
590 'Timestamp',
591 'Money',
592 'Email',
593 ];
594 return array_combine($types, $types);
595 }
596
597 }