fbe1c934e7922fbc7091c76422031754e3d2974d
[civicrm-core.git] / CRM / Utils / Type.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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-2017
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 * Get the data_type for the field.
148 *
149 * @param array $fieldMetadata
150 * Metadata about the field.
151 *
152 * @return string
153 */
154 public static function getDataTypeFromFieldMetadata($fieldMetadata) {
155 if (isset($fieldMetadata['data_type'])) {
156 return $fieldMetadata['data_type'];
157 }
158 if (empty($fieldMetadata['type'])) {
159 // I would prefer to throw an e-notice but there is some,
160 // probably unnecessary logic, that only retrieves activity fields
161 // if they are 'in the profile' and probably they are not 'in'
162 // until they are added - which might lead to ? who knows!
163 return '';
164 }
165 return self::typeToString($fieldMetadata['type']);
166 }
167
168 /**
169 * Helper function to call escape on arrays.
170 *
171 * @see escape
172 */
173 public static function escapeAll($data, $type, $abort = TRUE) {
174 foreach ($data as $key => $value) {
175 $data[$key] = CRM_Utils_Type::escape($value, $type, $abort);
176 }
177 return $data;
178 }
179
180 /**
181 * Helper function to call validate on arrays
182 *
183 * @see validate
184 */
185 public static function validateAll($data, $type, $abort = TRUE) {
186 foreach ($data as $key => $value) {
187 $data[$key] = CRM_Utils_Type::validate($value, $type, $abort);
188 }
189 return $data;
190 }
191
192 /**
193 * Verify that a variable is of a given type, and apply a bit of processing.
194 *
195 * @param mixed $data
196 * The value to be verified/escaped.
197 * @param string $type
198 * The type to verify against.
199 * @param bool $abort
200 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
201 *
202 * @return mixed
203 * The data, escaped if necessary.
204 */
205 public static function escape($data, $type, $abort = TRUE) {
206 switch ($type) {
207 case 'Integer':
208 case 'Int':
209 if (CRM_Utils_Rule::integer($data)) {
210 return (int) $data;
211 }
212 break;
213
214 case 'Positive':
215 if (CRM_Utils_Rule::positiveInteger($data)) {
216 return (int) $data;
217 }
218 break;
219
220 // CRM-8925 for custom fields of this type
221 case 'Country':
222 case 'StateProvince':
223 // Handle multivalued data in delimited or array format
224 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
225 $valid = TRUE;
226 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
227 if (!CRM_Utils_Rule::positiveInteger($item)) {
228 $valid = FALSE;
229 }
230 }
231 if ($valid) {
232 return $data;
233 }
234 }
235 elseif (CRM_Utils_Rule::positiveInteger($data)) {
236 return (int) $data;
237 }
238 break;
239
240 case 'File':
241 if (CRM_Utils_Rule::positiveInteger($data)) {
242 return (int) $data;
243 }
244 break;
245
246 case 'Link':
247 if (CRM_Utils_Rule::url($data = trim($data))) {
248 return $data;
249 }
250 break;
251
252 case 'Boolean':
253 if (CRM_Utils_Rule::boolean($data)) {
254 return $data;
255 }
256 break;
257
258 case 'Float':
259 case 'Money':
260 if (CRM_Utils_Rule::numeric($data)) {
261 return $data;
262 }
263 break;
264
265 case 'String':
266 case 'Memo':
267 case 'Text':
268 return CRM_Core_DAO::escapeString($data);
269
270 case 'Date':
271 case 'Timestamp':
272 // a null date or timestamp is valid
273 if (strlen(trim($data)) == 0) {
274 return trim($data);
275 }
276
277 if ((preg_match('/^\d{8}$/', $data) ||
278 preg_match('/^\d{14}$/', $data)
279 ) &&
280 CRM_Utils_Rule::mysqlDate($data)
281 ) {
282 return $data;
283 }
284 break;
285
286 case 'ContactReference':
287 if (strlen(trim($data)) == 0) {
288 return trim($data);
289 }
290
291 if (CRM_Utils_Rule::validContact($data)) {
292 return (int) $data;
293 }
294 break;
295
296 case 'MysqlColumnNameOrAlias':
297 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
298 $data = str_replace('`', '', $data);
299 $parts = explode('.', $data);
300 $data = '`' . implode('`.`', $parts) . '`';
301
302 return $data;
303 }
304 break;
305
306 case 'MysqlOrderByDirection':
307 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
308 return strtolower($data);
309 }
310 break;
311
312 case 'MysqlOrderBy':
313 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
314 $parts = explode(',', $data);
315
316 // The field() syntax is tricky here because it uses commas & when
317 // we separate by them we break it up. But we want to keep the clauses in order.
318 // so we just clumsily re-assemble it. Test cover exists.
319 $fieldClauseStart = NULL;
320 foreach ($parts as $index => &$part) {
321 if (substr($part, 0, 6) === 'field(') {
322 // Looking to escape a string like 'field(contribution_status_id,3,4,5) asc'
323 // to 'field(`contribution_status_id`,3,4,5) asc'
324 $fieldClauseStart = $index;
325 continue;
326 }
327 if ($fieldClauseStart !== NULL) {
328 // this is part of the list of field options. Concatenate it back on.
329 $parts[$fieldClauseStart] .= ',' . $part;
330 unset($parts[$index]);
331 if (!strstr($parts[$fieldClauseStart], ')')) {
332 // we have not reached the end of the list.
333 continue;
334 }
335 // We have the last piece of the field() clause, time to escape it.
336 $parts[$fieldClauseStart] = self::mysqlOrderByFieldFunctionCallback($parts[$fieldClauseStart]);
337 $fieldClauseStart = NULL;
338 continue;
339
340 }
341 // Normal clause.
342 $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));
343 }
344 return implode(', ', $parts);
345 }
346 break;
347
348 default:
349 CRM_Core_Error::fatal(
350 $type . " is not a recognised (camel cased) data type."
351 );
352 break;
353 }
354
355 // @todo Use exceptions instead of CRM_Core_Error::fatal().
356 if ($abort) {
357 $data = htmlentities($data);
358 CRM_Core_Error::fatal("$data is not of the type $type");
359 }
360 return NULL;
361 }
362
363 /**
364 * Verify that a variable is of a given type.
365 *
366 * @param mixed $data
367 * The value to validate.
368 * @param string $type
369 * The type to validate against.
370 * @param bool $abort
371 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
372 * @name string $name
373 * The name of the attribute
374 *
375 * @return mixed
376 * The data, escaped if necessary
377 */
378 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ') {
379 switch ($type) {
380 case 'Integer':
381 case 'Int':
382 if (CRM_Utils_Rule::integer($data)) {
383 return (int) $data;
384 }
385 break;
386
387 case 'Positive':
388 if (CRM_Utils_Rule::positiveInteger($data)) {
389 return (int) $data;
390 }
391 break;
392
393 case 'Boolean':
394 if (CRM_Utils_Rule::boolean($data)) {
395 return $data;
396 }
397 break;
398
399 case 'Float':
400 case 'Money':
401 if (CRM_Utils_Rule::numeric($data)) {
402 return $data;
403 }
404 break;
405
406 case 'Text':
407 case 'String':
408 case 'Link':
409 case 'Memo':
410 return $data;
411
412 case 'Date':
413 // a null date is valid
414 if (strlen(trim($data)) == 0) {
415 return trim($data);
416 }
417
418 if (preg_match('/^\d{8}$/', $data) &&
419 CRM_Utils_Rule::mysqlDate($data)
420 ) {
421 return $data;
422 }
423 break;
424
425 case 'Timestamp':
426 // a null timestamp is valid
427 if (strlen(trim($data)) == 0) {
428 return trim($data);
429 }
430
431 if ((preg_match('/^\d{14}$/', $data) ||
432 preg_match('/^\d{8}$/', $data)
433 ) &&
434 CRM_Utils_Rule::mysqlDate($data)
435 ) {
436 return $data;
437 }
438 break;
439
440 case 'ContactReference':
441 // null is valid
442 if (strlen(trim($data)) == 0) {
443 return trim($data);
444 }
445
446 if (CRM_Utils_Rule::validContact($data)) {
447 return $data;
448 }
449 break;
450
451 case 'MysqlColumnNameOrAlias':
452 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
453 return $data;
454 }
455 break;
456
457 case 'MysqlOrderByDirection':
458 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
459 return strtolower($data);
460 }
461 break;
462
463 case 'MysqlOrderBy':
464 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
465 return $data;
466 }
467 break;
468
469 case 'ExtensionKey':
470 if (CRM_Utils_Rule::checkExtesnionKeyIsValid($data)) {
471 return $data;
472 }
473 break;
474
475 default:
476 CRM_Core_Error::fatal("Cannot recognize $type for $data");
477 break;
478 }
479
480 if ($abort) {
481 $data = htmlentities($data);
482 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
483 }
484
485 return NULL;
486 }
487
488 /**
489 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
490 *
491 * Add backticks around the field name.
492 *
493 * @param string $clause
494 *
495 * @return string
496 */
497 public static function mysqlOrderByFieldFunctionCallback($clause) {
498 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
499 }
500
501 /**
502 * preg_replace_callback for MysqlOrderBy escape.
503 */
504 public static function mysqlOrderByCallback($matches) {
505 $output = '';
506 $matches = str_replace('`', '', $matches);
507
508 // Table name.
509 if (isset($matches[1]) && $matches[1]) {
510 $output .= '`' . $matches[1] . '`.';
511 }
512
513 // Column name.
514 if (isset($matches[2]) && $matches[2]) {
515 $output .= '`' . $matches[2] . '`';
516 }
517
518 // Sort order.
519 if (isset($matches[3]) && $matches[3]) {
520 $output .= ' ' . $matches[3];
521 }
522
523 return $output;
524 }
525
526 /**
527 * Get list of avaliable Data Tupes for Option Groups
528 *
529 * @return array
530 */
531 public static function dataTypes() {
532 $types = array(
533 'Integer',
534 'String',
535 'Date',
536 'Time',
537 'Timestamp',
538 'Money',
539 'Email',
540 );
541 return array_combine($types, $types);
542 }
543
544 }