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