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