Merge pull request #15815 from artfulrobot/issue-1108-fix-unsubscribe
[civicrm-core.git] / CRM / Utils / Type.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Utils_Type {
18 const
19 T_INT = 1,
20 T_STRING = 2,
21 T_ENUM = 2,
22 T_DATE = 4,
23 T_TIME = 8,
24 T_BOOLEAN = 16,
25 T_TEXT = 32,
26 T_LONGTEXT = 32,
27 T_BLOB = 64,
28 T_TIMESTAMP = 256,
29 T_FLOAT = 512,
30 T_MONEY = 1024,
31 T_EMAIL = 2048,
32 T_URL = 4096,
33 T_CCNUM = 8192,
34 T_MEDIUMBLOB = 16384;
35
36 // @TODO What's the point of these constants? Backwards compatibility?
37 //
38 // These are used for field size (<input type=text size=2>), but redundant TWO=2
39 // usages are rare and should be eliminated. See CRM-18810.
40 const
41 TWO = 2,
42 FOUR = 4,
43 SIX = 6,
44 EIGHT = 8,
45 TWELVE = 12,
46 SIXTEEN = 16,
47 TWENTY = 20,
48 MEDIUM = 20,
49 THIRTY = 30,
50 BIG = 30,
51 FORTYFIVE = 45,
52 HUGE = 45;
53
54 /**
55 * Maximum size of a MySQL BLOB or TEXT column in bytes.
56 */
57 const BLOB_SIZE = 65535;
58
59 /**
60 * Maximum value of a MySQL signed INT column.
61 */
62 const INT_MAX = 2147483647;
63
64 /**
65 * Gets the string representation for a data type.
66 *
67 * @param int $type
68 * Integer number identifying the data type.
69 *
70 * @return string
71 * String identifying the data type, e.g. 'Int' or 'String'.
72 */
73 public static function typeToString($type) {
74 // @todo Use constants in the case statements, e.g. "case T_INT:".
75 // @todo return directly, instead of assigning a value.
76 // @todo Use a lookup array, as a property or as a local variable.
77 switch ($type) {
78 case 1:
79 $string = 'Int';
80 break;
81
82 case 2:
83 $string = 'String';
84 break;
85
86 case 3:
87 $string = 'Enum';
88 break;
89
90 case 4:
91 $string = 'Date';
92 break;
93
94 case 8:
95 $string = 'Time';
96 break;
97
98 case 16:
99 $string = 'Boolean';
100 break;
101
102 case 32:
103 $string = 'Text';
104 break;
105
106 case 64:
107 $string = 'Blob';
108 break;
109
110 // CRM-10404
111 case 12:
112 case 256:
113 $string = 'Timestamp';
114 break;
115
116 case 512:
117 $string = 'Float';
118 break;
119
120 case 1024:
121 $string = 'Money';
122 break;
123
124 case 2048:
125 $string = 'Date';
126 break;
127
128 case 4096:
129 $string = 'Email';
130 break;
131
132 case 16384:
133 $string = 'Mediumblob';
134 break;
135 }
136
137 return (isset($string)) ? $string : "";
138 }
139
140 /**
141 * @return array
142 * An array of type in the form 'type name' => 'int representing type'
143 */
144 public static function getValidTypes() {
145 return [
146 'Int' => self::T_INT,
147 'String' => self::T_STRING,
148 'Enum' => self::T_ENUM,
149 'Date' => self::T_DATE,
150 'Time' => self::T_TIME,
151 'Boolean' => self::T_BOOLEAN,
152 'Text' => self::T_TEXT,
153 'Blob' => self::T_BLOB,
154 'Timestamp' => self::T_TIMESTAMP,
155 'Float' => self::T_FLOAT,
156 'Money' => self::T_MONEY,
157 'Email' => self::T_EMAIL,
158 'Mediumblob' => self::T_MEDIUMBLOB,
159 ];
160 }
161
162 /**
163 * Get the data_type for the field.
164 *
165 * @param array $fieldMetadata
166 * Metadata about the field.
167 *
168 * @return string
169 */
170 public static function getDataTypeFromFieldMetadata($fieldMetadata) {
171 if (isset($fieldMetadata['data_type'])) {
172 return $fieldMetadata['data_type'];
173 }
174 if (empty($fieldMetadata['type'])) {
175 // I would prefer to throw an e-notice but there is some,
176 // probably unnecessary logic, that only retrieves activity fields
177 // if they are 'in the profile' and probably they are not 'in'
178 // until they are added - which might lead to ? who knows!
179 return '';
180 }
181 return self::typeToString($fieldMetadata['type']);
182 }
183
184 /**
185 * Helper function to call escape on arrays.
186 *
187 * @see escape
188 */
189 public static function escapeAll($data, $type, $abort = TRUE) {
190 foreach ($data as $key => $value) {
191 $data[$key] = CRM_Utils_Type::escape($value, $type, $abort);
192 }
193 return $data;
194 }
195
196 /**
197 * Helper function to call validate on arrays
198 *
199 * @see validate
200 */
201 public static function validateAll($data, $type, $abort = TRUE) {
202 foreach ($data as $key => $value) {
203 $data[$key] = CRM_Utils_Type::validate($value, $type, $abort);
204 }
205 return $data;
206 }
207
208 /**
209 * Verify that a variable is of a given type, and apply a bit of processing.
210 *
211 * @param mixed $data
212 * The value to be verified/escaped.
213 * @param string $type
214 * The type to verify against.
215 * @param bool $abort
216 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
217 *
218 * @return mixed
219 * The data, escaped if necessary.
220 * @throws CRM_Core_Exception
221 */
222 public static function escape($data, $type, $abort = TRUE) {
223 switch ($type) {
224 case 'Integer':
225 case 'Int':
226 case 'Positive':
227 case 'Float':
228 case 'Money':
229 case 'Date':
230 case 'Timestamp':
231 case 'ContactReference':
232 case 'MysqlOrderByDirection':
233 $validatedData = self::validate($data, $type, $abort);
234 if (isset($validatedData)) {
235 return $validatedData;
236 }
237 break;
238
239 // CRM-8925 for custom fields of this type
240 case 'Country':
241 case 'StateProvince':
242 // Handle multivalued data in delimited or array format
243 if (is_array($data) || (strpos($data, CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE)) {
244 $valid = TRUE;
245 foreach (CRM_Utils_Array::explodePadded($data) as $item) {
246 if (!CRM_Utils_Rule::positiveInteger($item)) {
247 $valid = FALSE;
248 }
249 }
250 if ($valid) {
251 return $data;
252 }
253 }
254 elseif (CRM_Utils_Rule::positiveInteger($data)) {
255 return (int) $data;
256 }
257 break;
258
259 case 'File':
260 if (CRM_Utils_Rule::positiveInteger($data)) {
261 return (int) $data;
262 }
263 break;
264
265 case 'Link':
266 if (CRM_Utils_Rule::url($data = trim($data))) {
267 return $data;
268 }
269 break;
270
271 case 'Boolean':
272 if (CRM_Utils_Rule::boolean($data)) {
273 return $data;
274 }
275 break;
276
277 case 'String':
278 case 'Memo':
279 case 'Text':
280 return CRM_Core_DAO::escapeString(self::validate($data, $type, $abort));
281
282 case 'MysqlColumnNameOrAlias':
283 if (CRM_Utils_Rule::mysqlColumnNameOrAlias($data)) {
284 $data = str_replace('`', '', $data);
285 $parts = explode('.', $data);
286 $data = '`' . implode('`.`', $parts) . '`';
287
288 return $data;
289 }
290 break;
291
292 case 'MysqlOrderBy':
293 if (CRM_Utils_Rule::mysqlOrderBy($data)) {
294 $parts = explode(',', $data);
295
296 // The field() syntax is tricky here because it uses commas & when
297 // we separate by them we break it up. But we want to keep the clauses in order.
298 // so we just clumsily re-assemble it. Test cover exists.
299 $fieldClauseStart = NULL;
300 foreach ($parts as $index => &$part) {
301 if (substr($part, 0, 6) === 'field(') {
302 // Looking to escape a string like 'field(contribution_status_id,3,4,5) asc'
303 // to 'field(`contribution_status_id`,3,4,5) asc'
304 $fieldClauseStart = $index;
305 continue;
306 }
307 if ($fieldClauseStart !== NULL) {
308 // this is part of the list of field options. Concatenate it back on.
309 $parts[$fieldClauseStart] .= ',' . $part;
310 unset($parts[$index]);
311 if (!strstr($parts[$fieldClauseStart], ')')) {
312 // we have not reached the end of the list.
313 continue;
314 }
315 // We have the last piece of the field() clause, time to escape it.
316 $parts[$fieldClauseStart] = self::mysqlOrderByFieldFunctionCallback($parts[$fieldClauseStart]);
317 $fieldClauseStart = NULL;
318 continue;
319
320 }
321 // Normal clause.
322 $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));
323 }
324 return implode(', ', $parts);
325 }
326 break;
327
328 default:
329 CRM_Core_Error::fatal(
330 $type . " is not a recognised (camel cased) data type."
331 );
332 break;
333 }
334
335 // @todo Use exceptions instead of CRM_Core_Error::fatal().
336 if ($abort) {
337 $data = htmlentities($data);
338
339 CRM_Core_Error::fatal("$data is not of the type $type");
340 }
341 return NULL;
342 }
343
344 /**
345 * Verify that a variable is of a given type.
346 *
347 * @param mixed $data
348 * The value to validate.
349 * @param string $type
350 * The type to validate against.
351 * @param bool $abort
352 * If TRUE, the operation will CRM_Core_Error::fatal() on invalid data.
353 * @param string $name
354 * The name of the attribute
355 * @param bool $isThrowException
356 * Should an exception be thrown rather than a using a deprecated fatal error.
357 *
358 * @return mixed
359 * The data, escaped if necessary
360 *
361 * @throws \CRM_Core_Exception
362 */
363 public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ', $isThrowException = TRUE) {
364
365 $possibleTypes = [
366 'Integer',
367 'Int',
368 'Positive',
369 'CommaSeparatedIntegers',
370 'Boolean',
371 'Float',
372 'Money',
373 'Text',
374 'String',
375 'Link',
376 'Memo',
377 'Date',
378 'Timestamp',
379 'ContactReference',
380 'MysqlColumnNameOrAlias',
381 'MysqlOrderByDirection',
382 'MysqlOrderBy',
383 'ExtensionKey',
384 'Json',
385 'Alphanumeric',
386 'Color',
387 ];
388 if (!in_array($type, $possibleTypes)) {
389 if ($isThrowException) {
390 throw new CRM_Core_Exception(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
391 }
392 CRM_Core_Error::fatal(ts('Invalid type, must be one of : ' . implode($possibleTypes)));
393 }
394 switch ($type) {
395 case 'Integer':
396 case 'Int':
397 if (CRM_Utils_Rule::integer($data)) {
398 return (int) $data;
399 }
400 break;
401
402 case 'Positive':
403 if (CRM_Utils_Rule::positiveInteger($data)) {
404 return (int) $data;
405 }
406 break;
407
408 case 'Float':
409 case 'Money':
410 if (CRM_Utils_Rule::numeric($data)) {
411 return $data;
412 }
413 break;
414
415 case 'Text':
416 case 'String':
417 case 'Link':
418 case 'Memo':
419 return $data;
420
421 case 'Date':
422 case 'Timestamp':
423 // a null timestamp is valid
424 if (strlen(trim($data)) == 0) {
425 return trim($data);
426 }
427
428 if ((preg_match('/^\d{14}$/', $data) ||
429 preg_match('/^\d{8}$/', $data)
430 ) &&
431 CRM_Utils_Rule::mysqlDate($data)
432 ) {
433 return $data;
434 }
435 break;
436
437 case 'ContactReference':
438 // null is valid
439 if (strlen(trim($data)) == 0) {
440 return trim($data);
441 }
442
443 if (CRM_Utils_Rule::validContact($data)) {
444 return (int) $data;
445 }
446 break;
447
448 case 'MysqlOrderByDirection':
449 if (CRM_Utils_Rule::mysqlOrderByDirection($data)) {
450 return strtolower($data);
451 }
452 break;
453
454 case 'ExtensionKey':
455 if (CRM_Utils_Rule::checkExtensionKeyIsValid($data)) {
456 return $data;
457 }
458 break;
459
460 default:
461 $check = lcfirst($type);
462 if (CRM_Utils_Rule::$check($data)) {
463 return $data;
464 }
465 }
466
467 if ($abort) {
468 $data = htmlentities($data);
469 if ($isThrowException) {
470 throw new CRM_Core_Exception("$name (value: $data) is not of the type $type");
471 }
472 CRM_Core_Error::fatal("$name (value: $data) is not of the type $type");
473 }
474
475 return NULL;
476 }
477
478 /**
479 * Preg_replace_callback for mysqlOrderByFieldFunction escape.
480 *
481 * Add backticks around the field name.
482 *
483 * @param string $clause
484 *
485 * @return string
486 */
487 public static function mysqlOrderByFieldFunctionCallback($clause) {
488 return preg_replace('/field\((\w*)/', 'field(`${1}`', $clause);
489 }
490
491 /**
492 * preg_replace_callback for MysqlOrderBy escape.
493 */
494 public static function mysqlOrderByCallback($matches) {
495 $output = '';
496 $matches = str_replace('`', '', $matches);
497
498 // Table name.
499 if (isset($matches[1]) && $matches[1]) {
500 $output .= '`' . $matches[1] . '`.';
501 }
502
503 // Column name.
504 if (isset($matches[2]) && $matches[2]) {
505 $output .= '`' . $matches[2] . '`';
506 }
507
508 // Sort order.
509 if (isset($matches[3]) && $matches[3]) {
510 $output .= ' ' . $matches[3];
511 }
512
513 return $output;
514 }
515
516 /**
517 * Get list of avaliable Data Types for Option Groups
518 *
519 * @return array
520 */
521 public static function dataTypes() {
522 $types = [
523 'Integer',
524 'String',
525 'Date',
526 'Time',
527 'Timestamp',
528 'Money',
529 'Email',
530 ];
531 return array_combine($types, $types);
532 }
533
534 }