Merge pull request #9599 from colemanw/CRM-19812
[civicrm-core.git] / CRM / Core / BAO / CustomValueTable.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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-2016
32 * $Id$
33 *
34 */
35 class CRM_Core_BAO_CustomValueTable {
36
37 /**
38 * @param array $customParams
39 *
40 * @throws Exception
41 */
42 public static function create(&$customParams) {
43 if (empty($customParams) ||
44 !is_array($customParams)
45 ) {
46 return;
47 }
48
49 foreach ($customParams as $tableName => $tables) {
50 foreach ($tables as $index => $fields) {
51 $sqlOP = NULL;
52 $hookID = NULL;
53 $hookOP = NULL;
54 $entityID = NULL;
55 $isMultiple = FALSE;
56 $set = array();
57 $params = array();
58 $count = 1;
59 foreach ($fields as $field) {
60 if (!$sqlOP) {
61 $entityID = $field['entity_id'];
62 $hookID = $field['custom_group_id'];
63 $isMultiple = $field['is_multiple'];
64 if (array_key_exists('id', $field)) {
65 $sqlOP = "UPDATE $tableName ";
66 $where = " WHERE id = %{$count}";
67 $params[$count] = array($field['id'], 'Integer');
68 $count++;
69 $hookOP = 'edit';
70 }
71 else {
72 $sqlOP = "INSERT INTO $tableName ";
73 $where = NULL;
74 $hookOP = 'create';
75 }
76 }
77
78 // fix the value before we store it
79 $value = $field['value'];
80 $type = $field['type'];
81 switch ($type) {
82 case 'StateProvince':
83 $type = 'Integer';
84 if (is_array($value)) {
85 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
86 $type = 'String';
87 }
88 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
89 //fix for multi select state, CRM-3437
90 $mulValues = explode(',', $value);
91 $validStates = array();
92 foreach ($mulValues as $key => $stateVal) {
93 $states = array();
94 $states['state_province'] = trim($stateVal);
95
96 CRM_Utils_Array::lookupValue($states, 'state_province',
97 CRM_Core_PseudoConstant::stateProvince(), TRUE
98 );
99 if (empty($states['state_province_id'])) {
100 CRM_Utils_Array::lookupValue($states, 'state_province',
101 CRM_Core_PseudoConstant::stateProvinceAbbreviation(), TRUE
102 );
103 }
104 $validStates[] = CRM_Utils_Array::value('state_province_id', $states);
105 }
106 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
107 $validStates
108 );
109 $type = 'String';
110 }
111 elseif (!$value) {
112 // CRM-3415
113 // using type of timestamp allows us to sneak in a null into db
114 // gross but effective hack
115 $value = NULL;
116 $type = 'Timestamp';
117 }
118 else {
119 $type = 'String';
120 }
121 break;
122
123 case 'Country':
124 $type = 'Integer';
125 $mulValues = explode(',', $value);
126 if (is_array($value)) {
127 $value = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $value) . CRM_Core_DAO::VALUE_SEPARATOR;
128 $type = 'String';
129 }
130 elseif (!is_numeric($value) && !strstr($value, CRM_Core_DAO::VALUE_SEPARATOR)) {
131 //fix for multi select country, CRM-3437
132 $mulValues = explode(',', $value);
133 $validCountries = array();
134 foreach ($mulValues as $key => $countryVal) {
135 $countries = array();
136 $countries['country'] = trim($countryVal);
137 CRM_Utils_Array::lookupValue($countries, 'country',
138 CRM_Core_PseudoConstant::country(), TRUE
139 );
140 if (empty($countries['country_id'])) {
141 CRM_Utils_Array::lookupValue($countries, 'country',
142 CRM_Core_PseudoConstant::countryIsoCode(), TRUE
143 );
144 }
145 $validCountries[] = CRM_Utils_Array::value('country_id', $countries);
146 }
147 $value = implode(CRM_Core_DAO::VALUE_SEPARATOR,
148 $validCountries
149 );
150 $type = 'String';
151 }
152 elseif (!$value) {
153 // CRM-3415
154 // using type of timestamp allows us to sneak in a null into db
155 // gross but effective hack
156 $value = NULL;
157 $type = 'Timestamp';
158 }
159 else {
160 $type = 'String';
161 }
162 break;
163
164 case 'File':
165 if (!$field['file_id']) {
166 CRM_Core_Error::fatal();
167 }
168
169 // need to add/update civicrm_entity_file
170 $entityFileDAO = new CRM_Core_DAO_EntityFile();
171 $entityFileDAO->file_id = $field['file_id'];
172 $entityFileDAO->find(TRUE);
173
174 $entityFileDAO->entity_table = $field['table_name'];
175 $entityFileDAO->entity_id = $field['entity_id'];
176 $entityFileDAO->file_id = $field['file_id'];
177 $entityFileDAO->save();
178 $entityFileDAO->free();
179 $value = $field['file_id'];
180 $type = 'String';
181 break;
182
183 case 'Date':
184 $value = CRM_Utils_Date::isoToMysql($value);
185 break;
186
187 case 'Int':
188 if (is_numeric($value)) {
189 $type = 'Integer';
190 }
191 else {
192 $type = 'Timestamp';
193 }
194 break;
195
196 case 'ContactReference':
197 if ($value == NULL) {
198 $type = 'Timestamp';
199 }
200 else {
201 $type = 'Integer';
202 }
203 break;
204
205 case 'RichTextEditor':
206 $type = 'String';
207 break;
208
209 case 'Boolean':
210 //fix for CRM-3290
211 $value = CRM_Utils_String::strtoboolstr($value);
212 if ($value === FALSE) {
213 $type = 'Timestamp';
214 }
215 break;
216
217 default:
218 break;
219 }
220 if (strtolower($value) === "null") {
221 // when unsetting a value to null, we don't need to validate the type
222 // https://projectllr.atlassian.net/browse/VGQBMP-20
223 $set[$field['column_name']] = $value;
224 }
225 else {
226 $set[$field['column_name']] = "%{$count}";
227 $params[$count] = array($value, $type);
228 $count++;
229 }
230 }
231
232 if (!empty($set)) {
233 $setClause = array();
234 foreach ($set as $n => $v) {
235 $setClause[] = "$n = $v";
236 }
237 $setClause = implode(',', $setClause);
238 if (!$where) {
239 // do this only for insert
240 $set['entity_id'] = "%{$count}";
241 $params[$count] = array($entityID, 'Integer');
242 $count++;
243
244 $fieldNames = implode(',', CRM_Utils_Type::escapeAll(array_keys($set), 'MysqlColumnNameOrAlias'));
245 $fieldValues = implode(',', array_values($set));
246 $query = "$sqlOP ( $fieldNames ) VALUES ( $fieldValues )";
247 // for multiple values we dont do on duplicate key update
248 if (!$isMultiple) {
249 $query .= " ON DUPLICATE KEY UPDATE $setClause";
250 }
251 }
252 else {
253 $query = "$sqlOP SET $setClause $where";
254 }
255 $dao = CRM_Core_DAO::executeQuery($query, $params);
256
257 CRM_Utils_Hook::custom($hookOP,
258 $hookID,
259 $entityID,
260 $fields
261 );
262 }
263 }
264 }
265 }
266
267 /**
268 * Given a field return the mysql data type associated with it.
269 *
270 * @param string $type
271 * @param int $maxLength
272 *
273 * @return string
274 * the mysql data store placeholder
275 */
276 public static function fieldToSQLType($type, $maxLength = 255) {
277 if (!isset($maxLength) ||
278 !is_numeric($maxLength) ||
279 $maxLength <= 0
280 ) {
281 $maxLength = 255;
282 }
283
284 switch ($type) {
285 case 'String':
286 case 'Link':
287 return "varchar($maxLength)";
288
289 case 'Boolean':
290 return 'tinyint';
291
292 case 'Int':
293 return 'int';
294
295 // the below three are FK's, and have constraints added to them
296
297 case 'ContactReference':
298 case 'StateProvince':
299 case 'Country':
300 case 'File':
301 return 'int unsigned';
302
303 case 'Float':
304 return 'double';
305
306 case 'Money':
307 return 'decimal(20,2)';
308
309 case 'Memo':
310 case 'RichTextEditor':
311 return 'text';
312
313 case 'Date':
314 return 'datetime';
315
316 default:
317 CRM_Core_Error::fatal();
318 }
319 }
320
321 /**
322 * @param array $params
323 * @param $entityTable
324 * @param int $entityID
325 */
326 public static function store(&$params, $entityTable, $entityID) {
327 $cvParams = array();
328 foreach ($params as $fieldID => $param) {
329 foreach ($param as $index => $customValue) {
330 $cvParam = array(
331 'entity_table' => $entityTable,
332 'entity_id' => $entityID,
333 'value' => $customValue['value'],
334 'type' => $customValue['type'],
335 'custom_field_id' => $customValue['custom_field_id'],
336 'custom_group_id' => $customValue['custom_group_id'],
337 'table_name' => $customValue['table_name'],
338 'column_name' => $customValue['column_name'],
339 'is_multiple' => CRM_Utils_Array::value('is_multiple', $customValue),
340 'file_id' => $customValue['file_id'],
341 );
342
343 // Fix Date type to be timestamp, since that is how we store in db.
344 if ($cvParam['type'] == 'Date') {
345 $cvParam['type'] = 'Timestamp';
346 }
347
348 if (!empty($customValue['id'])) {
349 $cvParam['id'] = $customValue['id'];
350 }
351 if (!array_key_exists($customValue['table_name'], $cvParams)) {
352 $cvParams[$customValue['table_name']] = array();
353 }
354
355 if (!array_key_exists($index, $cvParams[$customValue['table_name']])) {
356 $cvParams[$customValue['table_name']][$index] = array();
357 }
358
359 $cvParams[$customValue['table_name']][$index][] = $cvParam;
360 }
361 }
362 if (!empty($cvParams)) {
363 self::create($cvParams);
364 }
365 }
366
367 /**
368 * Post process function.
369 *
370 * @param array $params
371 * @param $entityTable
372 * @param int $entityID
373 * @param $customFieldExtends
374 */
375 public static function postProcess(&$params, $entityTable, $entityID, $customFieldExtends) {
376 $customData = CRM_Core_BAO_CustomField::postProcess($params,
377 $entityID,
378 $customFieldExtends
379 );
380
381 if (!empty($customData)) {
382 self::store($customData, $entityTable, $entityID);
383 }
384 }
385
386 /**
387 * Return an array of all custom values associated with an entity.
388 *
389 * @param int $entityID
390 * Identification number of the entity.
391 * @param string $entityType
392 * Type of entity that the entityID corresponds to, specified.
393 * as a string with format "'<EntityName>'". Comma separated
394 * list may be used to specify OR matches. Allowable values
395 * are enumerated types in civicrm_custom_group.extends field.
396 * Optional. Default value assumes entityID references a
397 * contact entity.
398 * @param array $fieldIDs
399 * Optional list of fieldIDs that we want to retrieve. If this.
400 * is set the entityType is ignored
401 *
402 * @param bool $formatMultiRecordField
403 * @param array $DTparams - CRM-17810 dataTable params for the multiValued custom fields.
404 *
405 * @return array
406 * Array of custom values for the entity with key=>value
407 * pairs specified as civicrm_custom_field.id => custom value.
408 * Empty array if no custom values found.
409 */
410 public static function &getEntityValues($entityID, $entityType = NULL, $fieldIDs = NULL, $formatMultiRecordField = FALSE, $DTparams = NULL) {
411 if (!$entityID) {
412 // adding this here since an empty contact id could have serious repurcussions
413 // like looping forever
414 CRM_Core_Error::fatal('Please file an issue with the backtrace');
415 return NULL;
416 }
417
418 $cond = array();
419 if ($entityType) {
420 $cond[] = "cg.extends IN ( '$entityType' )";
421 }
422 if ($fieldIDs &&
423 is_array($fieldIDs)
424 ) {
425 $fieldIDList = implode(',', $fieldIDs);
426 $cond[] = "cf.id IN ( $fieldIDList )";
427 }
428 if (empty($cond)) {
429 $cond[] = "cg.extends IN ( 'Contact', 'Individual', 'Household', 'Organization' )";
430 }
431 $cond = implode(' AND ', $cond);
432
433 $limit = $orderBy = '';
434 if (!empty($DTparams['rowCount']) && $DTparams['rowCount'] > 0) {
435 $limit = " LIMIT " . CRM_Utils_Type::escape($DTparams['offset'], 'Integer') . ", " . CRM_Utils_Type::escape($DTparams['rowCount'], 'Integer');
436 }
437 if (!empty($DTparams['sort'])) {
438 $orderBy = ' ORDER BY ' . CRM_Utils_Type::escape($DTparams['sort'], 'String');
439 }
440
441 // First find all the fields that extend this type of entity.
442 $query = "
443 SELECT cg.table_name,
444 cg.id as groupID,
445 cg.is_multiple,
446 cf.column_name,
447 cf.id as fieldID,
448 cf.data_type as fieldDataType
449 FROM civicrm_custom_group cg,
450 civicrm_custom_field cf
451 WHERE cf.custom_group_id = cg.id
452 AND cg.is_active = 1
453 AND cf.is_active = 1
454 AND $cond
455 ";
456 $dao = CRM_Core_DAO::executeQuery($query);
457
458 $select = $fields = $isMultiple = array();
459
460 while ($dao->fetch()) {
461 if (!array_key_exists($dao->table_name, $select)) {
462 $fields[$dao->table_name] = array();
463 $select[$dao->table_name] = array();
464 }
465 $fields[$dao->table_name][] = $dao->fieldID;
466 $select[$dao->table_name][] = "{$dao->column_name} AS custom_{$dao->fieldID}";
467 $isMultiple[$dao->table_name] = $dao->is_multiple ? TRUE : FALSE;
468 $file[$dao->table_name][$dao->fieldID] = $dao->fieldDataType;
469 }
470
471 $result = $sortedResult = array();
472 foreach ($select as $tableName => $clauses) {
473 if (!empty($DTparams['sort'])) {
474 $query = CRM_Core_DAO::executeQuery("SELECT id FROM {$tableName} WHERE entity_id = {$entityID}");
475 $count = 1;
476 while ($query->fetch()) {
477 $sortedResult["{$query->id}"] = $count;
478 $count++;
479 }
480 }
481
482 $query = "SELECT SQL_CALC_FOUND_ROWS id, " . implode(', ', $clauses) . " FROM $tableName WHERE entity_id = $entityID {$orderBy} {$limit}";
483 $dao = CRM_Core_DAO::executeQuery($query);
484 if (!empty($DTparams)) {
485 $result['count'] = CRM_Core_DAO::singleValueQuery('SELECT FOUND_ROWS()');
486 }
487 while ($dao->fetch()) {
488 foreach ($fields[$tableName] as $fieldID) {
489 $fieldName = "custom_{$fieldID}";
490 if ($isMultiple[$tableName]) {
491 if ($formatMultiRecordField) {
492 $result["{$dao->id}"]["{$fieldID}"] = $dao->$fieldName;
493 }
494 else {
495 $result["{$fieldID}_{$dao->id}"] = $dao->$fieldName;
496 }
497 }
498 else {
499 $result[$fieldID] = $dao->$fieldName;
500 }
501 }
502 }
503 }
504 if (!empty($sortedResult)) {
505 $result['sortedResult'] = $sortedResult;
506 }
507 return $result;
508 }
509
510 /**
511 * Take in an array of entityID, custom_XXX => value
512 * and set the value in the appropriate table. Should also be able
513 * to set the value to null. Follows api parameter/return conventions
514 *
515 * @array $params
516 *
517 * @param array $params
518 *
519 * @throws Exception
520 * @return array
521 */
522 public static function setValues(&$params) {
523
524 if (!isset($params['entityID']) ||
525 CRM_Utils_Type::escape($params['entityID'], 'Integer', FALSE) === NULL
526 ) {
527 return CRM_Core_Error::createAPIError(ts('entityID needs to be set and of type Integer'));
528 }
529
530 // first collect all the id/value pairs. The format is:
531 // custom_X => value or custom_X_VALUEID => value (for multiple values), VALUEID == -1, -2 etc for new insertions
532 $values = array();
533 $fieldValues = array();
534 foreach ($params as $n => $v) {
535 if ($customFieldInfo = CRM_Core_BAO_CustomField::getKeyID($n, TRUE)) {
536 $fieldID = (int ) $customFieldInfo[0];
537 if (CRM_Utils_Type::escape($fieldID, 'Integer', FALSE) === NULL) {
538 return CRM_Core_Error::createAPIError(ts('field ID needs to be of type Integer for index %1',
539 array(1 => $fieldID)
540 ));
541 }
542 if (!array_key_exists($fieldID, $fieldValues)) {
543 $fieldValues[$fieldID] = array();
544 }
545 $id = -1;
546 if ($customFieldInfo[1]) {
547 $id = (int ) $customFieldInfo[1];
548 }
549 $fieldValues[$fieldID][] = array(
550 'value' => $v,
551 'id' => $id,
552 );
553 }
554 }
555
556 $fieldIDList = implode(',', array_keys($fieldValues));
557
558 // format it so that we can just use create
559 $sql = "
560 SELECT cg.table_name as table_name ,
561 cg.id as cg_id ,
562 cg.is_multiple as is_multiple,
563 cf.column_name as column_name,
564 cf.id as cf_id ,
565 cf.data_type as data_type
566 FROM civicrm_custom_group cg,
567 civicrm_custom_field cf
568 WHERE cf.custom_group_id = cg.id
569 AND cf.id IN ( $fieldIDList )
570 ";
571
572 $dao = CRM_Core_DAO::executeQuery($sql);
573 $cvParams = array();
574
575 while ($dao->fetch()) {
576 $dataType = $dao->data_type == 'Date' ? 'Timestamp' : $dao->data_type;
577 foreach ($fieldValues[$dao->cf_id] as $fieldValue) {
578 // Format null values correctly
579 if ($fieldValue['value'] === NULL || $fieldValue['value'] === '') {
580 switch ($dataType) {
581 case 'String':
582 case 'Int':
583 case 'Link':
584 case 'Boolean':
585 $fieldValue['value'] = '';
586 break;
587
588 case 'Timestamp':
589 $fieldValue['value'] = NULL;
590 break;
591
592 case 'StateProvince':
593 case 'Country':
594 case 'Money':
595 case 'Float':
596 $fieldValue['value'] = (int) 0;
597 break;
598 }
599 }
600 // Ensure that value is of the right data type
601 elseif (CRM_Utils_Type::escape($fieldValue['value'], $dataType, FALSE) === NULL) {
602 return CRM_Core_Error::createAPIError(ts('value: %1 is not of the right field data type: %2',
603 array(
604 1 => $fieldValue['value'],
605 2 => $dao->data_type,
606 )
607 ));
608 }
609
610 $cvParam = array(
611 'entity_id' => $params['entityID'],
612 'value' => $fieldValue['value'],
613 'type' => $dataType,
614 'custom_field_id' => $dao->cf_id,
615 'custom_group_id' => $dao->cg_id,
616 'table_name' => $dao->table_name,
617 'column_name' => $dao->column_name,
618 'is_multiple' => $dao->is_multiple,
619 );
620
621 if ($cvParam['type'] == 'File') {
622 $cvParam['file_id'] = $fieldValue['value'];
623 }
624
625 if (!array_key_exists($dao->table_name, $cvParams)) {
626 $cvParams[$dao->table_name] = array();
627 }
628
629 if (!array_key_exists($fieldValue['id'], $cvParams[$dao->table_name])) {
630 $cvParams[$dao->table_name][$fieldValue['id']] = array();
631 }
632
633 if ($fieldValue['id'] > 0) {
634 $cvParam['id'] = $fieldValue['id'];
635 }
636 $cvParams[$dao->table_name][$fieldValue['id']][] = $cvParam;
637 }
638 }
639
640 if (!empty($cvParams)) {
641 self::create($cvParams);
642 return array('is_error' => 0, 'result' => 1);
643 }
644
645 return CRM_Core_Error::createAPIError(ts('Unknown error'));
646 }
647
648 /**
649 * Take in an array of entityID, custom_ID
650 * and gets the value from the appropriate table.
651 *
652 * To get the values of custom fields with IDs 13 and 43 for contact ID 1327, use:
653 * $params = array( 'entityID' => 1327, 'custom_13' => 1, 'custom_43' => 1 );
654 *
655 * Entity Type will be inferred by the custom fields you request
656 * Specify $params['entityType'] if you do not supply any custom fields to return
657 * and entity type is other than Contact
658 *
659 * @array $params
660 *
661 * @param array $params
662 *
663 * @throws Exception
664 * @return array
665 */
666 public static function &getValues(&$params) {
667 if (empty($params)) {
668 return NULL;
669 }
670 if (!isset($params['entityID']) ||
671 CRM_Utils_Type::escape($params['entityID'],
672 'Integer', FALSE
673 ) === NULL
674 ) {
675 return CRM_Core_Error::createAPIError(ts('entityID needs to be set and of type Integer'));
676 }
677
678 // first collect all the ids. The format is:
679 // custom_ID
680 $fieldIDs = array();
681 foreach ($params as $n => $v) {
682 $key = $idx = NULL;
683 if (substr($n, 0, 7) == 'custom_') {
684 $idx = substr($n, 7);
685 if (CRM_Utils_Type::escape($idx, 'Integer', FALSE) === NULL) {
686 return CRM_Core_Error::createAPIError(ts('field ID needs to be of type Integer for index %1',
687 array(1 => $idx)
688 ));
689 }
690 $fieldIDs[] = (int ) $idx;
691 }
692 }
693
694 $default = array('Contact', 'Individual', 'Household', 'Organization');
695 if (!($type = CRM_Utils_Array::value('entityType', $params)) ||
696 in_array($params['entityType'], $default)
697 ) {
698 $type = NULL;
699 }
700 else {
701 $entities = CRM_Core_SelectValues::customGroupExtends();
702 if (!array_key_exists($type, $entities)) {
703 if (in_array($type, $entities)) {
704 $type = $entities[$type];
705 if (in_array($type, $default)) {
706 $type = NULL;
707 }
708 }
709 else {
710 return CRM_Core_Error::createAPIError(ts('Invalid entity type') . ': "' . $type . '"');
711 }
712 }
713 }
714
715 $values = self::getEntityValues($params['entityID'],
716 $type,
717 $fieldIDs
718 );
719 if (empty($values)) {
720 // note that this behaviour is undesirable from an API point of view - it should return an empty array
721 // since this is also called by the merger code & not sure the consequences of changing
722 // are just handling undoing this in the api layer. ie. converting the error back into a success
723 $result = array(
724 'is_error' => 1,
725 'error_message' => 'No values found for the specified entity ID and custom field(s).',
726 );
727 return $result;
728 }
729 else {
730 $result = array(
731 'is_error' => 0,
732 'entityID' => $params['entityID'],
733 );
734 foreach ($values as $id => $value) {
735 $result["custom_{$id}"] = $value;
736 }
737 return $result;
738 }
739 }
740
741 }